diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4989a61..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: sanderland # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # sanderbaduk -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5b7d7fa..40348ce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md index 3395703..3a6a181 100644 --- a/CONTRIBUTIONS.md +++ b/CONTRIBUTIONS.md @@ -56,6 +56,8 @@ Many thanks to these additional authors: * "electricRGB" for help with adding configurable keyboard shortcuts. * "milescrawford" for work on restyling the territory estimate. * "Funkenschlag1" for capturing stones sound and implementation, and board rotation. +* "waltheri" for one of the wooden board textures. +* Jacob Minsky ("jacobm-tech") for various contributions including analysis move range and improvements to territory display. ## Translators diff --git a/INSTALL.md b/INSTALL.md index bbaf272..d09e86c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -82,7 +82,7 @@ Simply download and run, everything is included. If you have a working Python 3.6-3.8 available, you should be able to simply: -* Run `pip3 install -U katrain` +* Run `pip3 install -U katrain` to install or upgrade. * Run the program by executing `katrain` in a terminal. ### Installation from sources diff --git a/README.md b/README.md index 4675819..762efe1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![License:MIT](http://img.shields.io/pypi/l/katrain)](http://en.wikipedia.org/wiki/MIT_License) [![GitHub Downloads](http://img.shields.io/github/downloads/sanderland/katrain/total?color=%23336699&label=github%20downloads)](http://github.com/sanderland/katrain/releases) [![PyPI Downloads](http://pepy.tech/badge/katrain)](http://pepy.tech/project/katrain) -[![Github sponsors](http://img.shields.io/static/v1?label=sponsor&message=%E2%9D%A4&logo=GitHub&color=dcb424&link=http://github.com/sponsors/sanderland/)](http://github.com/sponsors/sanderland) [![Discord](http://img.shields.io/discord/417022162348802048?logo=discord)](http://discord.com/channels/417022162348802048/629446365688365067) KaTrain is a tool for analyzing games and playing go with AI feedback from KataGo: @@ -167,6 +166,7 @@ Keyboard shortcuts are shown with **[key]**. * **[i]**: Start insertion mode. Allows you to insert moves, to improve analysis when both players ignore an important exchange or life and death situation. Press again to stop inserting and copy the rest of the branch. * **[l]**: Play out the game until the end and add as a collapsed branch, to visualize the potential effect of mistakes. This is done in the background, and can be started at several nodes at once when comparing the results at different starting positions. * **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on. + * **[shift+spacebar]**: As above, but does not turn 'top moves' hints on when it is off. * **[enter]** AI move. Makes the AI move for the current player regardless of current player selection. * **[F2]**: Deeper full game analysis. Analyze the entire game to a higher number of visits. * **[F3]**: Performance report. Show an overview of performance statistics for both players. @@ -245,7 +245,7 @@ See [these instructions](THEMES.md) for how to modify the look of any graphics o * Ideas, feedback, and contributions to code or translations are all very welcome. * For suggestions and planned improvements, see [open issues](http://github.com/sanderland/katrain/issues) on github to check if the functionality is already planned. -* You can contact me on the [Leela Zero & Friends Discord](http://discord.gg/AjTPFpN) (use the #gui channel) to get help, discuss improvements, or simply show your appreciation. +* You can join the [Computer Go Community Discord (formerly Leela Zero & Friends)](http://discord.gg/AjTPFpN) (use the #gui channel) to get help, discuss improvements, or simply show your appreciation. Please do not use github issues to ask for technical help, this is only for bugs, suggestions and discussing contributions. diff --git a/THEMES.md b/THEMES.md index 99cf491..11d67e4 100644 --- a/THEMES.md +++ b/THEMES.md @@ -16,6 +16,34 @@ Version 1.7 brings basic support for themes, and 1.9 extends it to include keybo * All resources (including icons, which can not be renamed for now) will be looked up in `/.katrain` first, so files with identical names there can be used to override sounds and images. * If variables are specified in multiple theme files, the *latest* alphabetically takes precedence. That is, each later theme file overwrites the settings from any previous one. +## Expected territory options + +* KaTrain supports different styles of display of expected territory: + * Blended style colors the board with an intensity proportional to the likelihood of a player controlling that territory at the end of the game. + * In the Marks style, each point of the board is marked with a square of size which is proportional to ownership likelihood. + * The Blocks style divides the whole board into black, white, and neutral territory, based on a likelihood threshold. This style is appropriate as a counting aid, but may be misleading before endgame if much of the territory is unsettled. +* Marks can also appear on stones to indicate the likelihood of these stones living at the end of the game. Three styles are supported: + * All stones can be marked, with the color of the mark indicating the expected ownership and the size of the mark indicating certainty. + * Weak stones only - marks will appear only on stones which are over 50% likely to die before the end of the game. + * No stone marks. + +|
Blended style, all stones marked |
Marks on intersections, weak stones marked | +|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +|
Territory blocks, no stones marked |
Blended territory, weak stones marked | + + +The game used in the screenshots is [Albert Yen vs. Eric Yoder](https://www.usgo.org/news/2022/03/members-edition-midwest-open-round-2-the-broken-ladder-game). + +The stone marks and territory style are independent; the table above presents 4 possible variants out of 9. +The relevant variables are: +``` + TERRITORY_DISPLAY = "blended" | "marks" | "blocks" + STONE_MARKS = "all" | "weak" | "none" + OWNERSHIP_MAX_ALPHA = 0.7 + BLOCKS_THRESHOLD = 0.6 + MARK_SIZE = 0.42 # as fraction of stone size +``` + ## Installation * To install a theme, simply unzip the theme.zip to your .katrain folder. diff --git a/katrain/__main__.py b/katrain/__main__.py index 14fb5d1..9cfd21b 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -5,6 +5,14 @@ import sys os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning") +from kivy.utils import platform as kivy_platform + +if kivy_platform == "win": + from ctypes import windll, c_int64 + + if hasattr(windll.user32, "SetProcessDpiAwarenessContext"): + windll.user32.SetProcessDpiAwarenessContext(c_int64(-4)) + import kivy kivy.require("2.0.0") @@ -12,7 +20,6 @@ kivy.require("2.0.0") # next, icon from katrain.core.utils import find_package_resource, PATHS from kivy.config import Config -from kivy.utils import platform ICON = find_package_resource("katrain/img/icon.ico") Config.set("kivy", "window_icon", ICON) @@ -52,7 +59,6 @@ from kivy.properties import NumericProperty, ObjectProperty, StringProperty from kivy.clock import Clock from kivy.metrics import dp from katrain.core.ai import generate_ai_move -from kivy.utils import platform as kivy_platform from katrain.core.lang import DEFAULT_LANGUAGE, i18n from katrain.core.constants import ( @@ -156,12 +162,14 @@ class KaTrainGui(Screen, KaTrainBase): def play_analyze_mode(self): return self.play_mode.mode - def toggle_continuous_analysis(self): + def toggle_continuous_analysis(self, quiet=False): if self.contributing: self.animate_contributing = not self.animate_contributing else: if self.pondering: self.controls.set_status("", STATUS_INFO) + elif not quiet: # See #549 + Clock.schedule_once(self.analysis_controls.hints.activate, 0) self.pondering = not self.pondering self.update_state() @@ -337,7 +345,7 @@ class KaTrainGui(Screen, KaTrainBase): if (move_tree is not None and mode == MODE_PLAY) or (move_tree is None and mode == MODE_ANALYZE): self.play_mode.switch_ui_mode() # for new game, go to play, for loaded, analyze self.board_gui.animating_pv = None - self.board_gui.rotation_degree = 0 + self.board_gui.reset_rotation() self.engine.on_new_game() # clear queries self.game = Game( self, @@ -401,6 +409,7 @@ class KaTrainGui(Screen, KaTrainBase): def _do_redo(self, n_times=1): self.board_gui.animating_pv = None self.game.redo(n_times) + def _do_rotate(self): self.board_gui.rotate_gridpos() @@ -412,7 +421,7 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None self.controls.move_tree.switch_branch(*args) - def _play_stone_sound(self,_dt=None): + def _play_stone_sound(self, _dt=None): play_sound(random.choice(Theme.STONE_SOUNDS)) def _do_play(self, coords): @@ -730,7 +739,7 @@ class KaTrainGui(Screen, KaTrainBase): return if keycode[1] == Theme.KEY_TOGGLE_CONTINUOUS_ANALYSIS: - self.toggle_continuous_analysis() + self.toggle_continuous_analysis(quiet=shift_pressed) elif keycode[1] == Theme.KEY_TOGGLE_COORDINATES: self.board_gui.toggle_coordinates() elif keycode[1] in Theme.KEY_PAUSE_TIMER and not ctrl_pressed: diff --git a/katrain/config.json b/katrain/config.json index cd3b7a1..3014789 100644 --- a/katrain/config.json +++ b/katrain/config.json @@ -28,7 +28,7 @@ "anim_pv_time": 0.5, "debug_level": 0, "lang": "en", - "version": "1.11.2", + "version": "1.11.3", "load_fast_analysis": false, "load_sgf_rewind": true }, diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 593c59a..db2663f 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -244,7 +244,7 @@ def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optio extra_settings=extra_settings, ) while not (error or analysis): - time.sleep(0.01) # TODO: prevent deadlock if esc, check node in queries? + time.sleep(0.01) # TODO: prevent deadlock if esc, check node in queries? engine.check_alive(exception_if_dead=True) return analysis diff --git a/katrain/core/constants.py b/katrain/core/constants.py index 3f685de..15aa369 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,5 +1,5 @@ PROGRAM_NAME = "KaTrain" -VERSION = "1.11.2" +VERSION = "1.12.0" HOMEPAGE = "https://github.com/sanderland/katrain" CONFIG_MIN_VERSION = "1.11.0" # keep config files from this version ANALYSIS_FORMAT_VERSION = "1.0" diff --git a/katrain/core/engine.py b/katrain/core/engine.py index b56b46c..f1aaf5f 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -344,15 +344,11 @@ class KataGoEngine(BaseEngine): if pq.get(k) != query.get(k) } if differences: - # TODO:remove - self.katrain.log(f"Found differences in ponder check: {differences}", OUTPUT_EXTRA_DEBUG) self.stop_pondering() - query["maxVisits"] = 1_000_000 + query["maxVisits"] = 10_000_000 query["reportDuringSearchEvery"] = PONDERING_REPORT_DT self.ponder_query = query else: - # TODO:remove - self.katrain.log("Found no differences in ponder check, skipping", OUTPUT_EXTRA_DEBUG) continue terminate = query.get("action") == "terminate" @@ -434,10 +430,10 @@ class KataGoEngine(BaseEngine): avoid = [] settings = copy.copy(self.override_settings) + settings["wideRootNoise"] = self.config["wide_root_noise"] if time_limit: settings["maxTime"] = self.config["max_time"] - if self.config.get("wide_root_noise", 0.0) > 0.0: # don't send if 0.0, so older versions don't error - settings["wideRootNoise"] = self.config["wide_root_noise"] + query = { "rules": self.get_rules(analysis_node.ruleset), "priority": self.base_priority + priority, diff --git a/katrain/core/game.py b/katrain/core/game.py index 94d2f8f..1faa90a 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -575,7 +575,6 @@ class Game(BaseGame): return engine = self.engines[cn.next_player] - Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0) if mode == "ponder": cn.analyze( @@ -602,6 +601,10 @@ class Game(BaseGame): if mode == "game": nodes = self.root.nodes_in_tree only_mistakes = kwargs.get("mistakes_only", False) + move_range = kwargs.get("move_range", None) + if move_range: + if move_range[1] < move_range[0]: + move_range = reversed(move_range) threshold = self.katrain.config("trainer/eval_thresholds")[-4] if "visits" in kwargs: visits = kwargs["visits"] @@ -610,9 +613,20 @@ class Game(BaseGame): visits = min_visits + engine.config["max_visits"] for node in nodes: max_point_loss = max(c.points_lost or 0 for c in [node] + node.children) - if not only_mistakes or max_point_loss > threshold: - node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None) - self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS) + if only_mistakes and max_point_loss <= threshold: + continue + if move_range and (not node.depth - 1 in range(move_range[0], move_range[1] + 1)): + continue + node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None) + if not move_range: + self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS) + else: + self.katrain.controls.set_status( + i18n._("move range analysis").format( + start_move=move_range[0], end_move=move_range[1], visits=visits + ), + STATUS_ANALYSIS, + ) return elif mode == "sweep": diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index 7089160..61cbc4e 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -374,7 +374,7 @@ class SGFNode: def place_handicap_stones(self, n_handicaps, tygem=False): board_size_x, board_size_y = self.board_size if min(board_size_x, board_size_y) < 3: - return # No + return # No near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1) near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1) far_x = board_size_x - 1 - near_x diff --git a/katrain/gui.kv b/katrain/gui.kv index 4da4939..c0e8d5a 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -736,6 +736,7 @@ size_hint: None, 0.7 pos_hint: {'center_y':0.5} checkbox: checkbox + label: label ClickableLabel: id: label text: root.text diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 02a8bd9..3f8f91d 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -1,11 +1,12 @@ import math import time from typing import List, Optional -import numpy as np +import copy from kivy.clock import Clock from kivy.core.window import Window -from kivy.graphics.context_instructions import Color +from kivy.graphics.texture import Texture +from kivy.graphics.context_instructions import Color, Rotate, Translate, PushMatrix, PopMatrix from kivy.graphics.vertex_instructions import Ellipse, Line, Rectangle from kivy.metrics import dp from kivy.properties import BooleanProperty, ListProperty, NumericProperty, ObjectProperty @@ -41,7 +42,7 @@ class BadukPanWidget(Widget): super(BadukPanWidget, self).__init__(**kwargs) self.trainer_config = {} self.ghost_stone = [] - self.gridpos = np.zeros((0,0,2)) + self.gridpos = None self.initial_gridpos_x = [] self.initial_gridpos_y = [] self.rotation_degree = 0 @@ -62,6 +63,10 @@ class BadukPanWidget(Widget): self.bind(size=self.redraw_trigger, pos=self.redraw_trigger) Clock.schedule_interval(self.animate_pv, 0.1) + def reset_rotation(self): + while self.rotation_degree: + self.rotate_gridpos() + def toggle_coordinates(self): self.draw_coords_enabled = not self.draw_coords_enabled self.redraw_trigger() @@ -72,16 +77,16 @@ class BadukPanWidget(Widget): # stone placement functions def _find_closest(self, pos_x, pos_y): - xd = abs(self.gridpos[0,0,0] - pos_x) + xd = abs(self.gridpos[0][0][0] - pos_x) xp = 0 - yd = abs(self.gridpos[0,0,1] - pos_y) + yd = abs(self.gridpos[0][0][1] - pos_y) yp = 0 for y in range(0, len(self.gridpos)): for x in range(0, len(self.gridpos[0])): - if abs(self.gridpos[y,x,0] - pos_x) <= xd and abs(self.gridpos[y,x,1] - pos_y) <= yd: - xd = abs(self.gridpos[y,x,0] - pos_x) + if abs(self.gridpos[y][x][0] - pos_x) <= xd and abs(self.gridpos[y][x][1] - pos_y) <= yd: + xd = abs(self.gridpos[y][x][0] - pos_x) xp = x - yd = abs(self.gridpos[y,x,1] - pos_y) + yd = abs(self.gridpos[y][x][1] - pos_y) yp = y return xd, xp, yd, yp @@ -147,9 +152,9 @@ class BadukPanWidget(Widget): (pv, node) for move, pv, node in self.active_pv_moves if move[0] < len(self.gridpos[0]) - and move[1] < len(self.gridpos) - and abs(rel_pos[0] - self.gridpos[move[1],move[0],0]) < self.grid_size / 2 - and abs(rel_pos[1] - self.gridpos[move[1],move[0],1]) < self.grid_size / 2 + and move[1] < len(self.gridpos) + and abs(rel_pos[0] - self.gridpos[move[1]][move[0]][0]) < self.grid_size / 2 + and abs(rel_pos[1] - self.gridpos[move[1]][move[0]][1]) < self.grid_size / 2 ] if near_move: self.set_animating_pv(near_move[0][0], near_move[0][1]) @@ -221,14 +226,54 @@ class BadukPanWidget(Widget): self.draw_board() self.draw_board_contents() - def draw_stone(self, x, y, player, alpha=1, innercol=None, evalcol=None, evalscale=1.0, scale=1.0): + def draw_stone( + self, x, y, player, alpha=1, innercol=None, evalcol=None, evalscale=1.0, scale=1.0, ownership=None, loss=None + ): stone_size = self.stone_size * scale + if ownership is not None: + (owner, other) = ("B", "W") if ownership > 0 else ("W", "B") + if Theme.TERRITORY_DISPLAY != "marks": + if player == owner: + alpha = Theme.STONE_MIN_ALPHA + (1.0 - Theme.STONE_MIN_ALPHA) * abs(ownership) + else: + alpha = Theme.STONE_MIN_ALPHA Color(1, 1, 1, alpha) Rectangle( - pos=(self.gridpos[y,x,0] - stone_size, self.gridpos[y,x,1] - stone_size), + pos=(self.gridpos[y][x][0] - stone_size, self.gridpos[y][x][1] - stone_size), size=(2 * stone_size, 2 * stone_size), texture=cached_texture(Theme.STONE_TEXTURE[player]), ) + # Draw ownership marks on stones; the mark is a square with an outline. + if (ownership is not None or loss is not None) and ( + Theme.STONE_MARKS == "all" or (Theme.STONE_MARKS == "weak" and player != owner) + ): + if ownership is not None: + mark_color = *Theme.STONE_COLORS[owner][:3], 1.0 + other_color = *Theme.STONE_COLORS[other][:3], 1.0 + outline_color = tuple(map(lambda y: sum(y) / float(len(y)), zip(*(mark_color, other_color)))) + if loss is not None: + mark_color = *Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss + outline_color = mark_color + + mark_size = Theme.MARK_SIZE * abs(ownership if ownership else loss) * self.stone_size * 2.0 + Color(*mark_color) + Rectangle( + pos=( + self.gridpos[y][x][0] - mark_size / 2, + self.gridpos[y][x][1] - mark_size / 2, + ), + size=(mark_size, mark_size), + ) + Color(*outline_color) + Line( + rectangle=( + self.gridpos[y][x][0] - mark_size / 2, + self.gridpos[y][x][1] - mark_size / 2, + mark_size, + mark_size, + ), + width=1.0, + ) if evalcol: eval_radius = math.sqrt(evalscale) # scale area by evalscale evalsize = self.stone_size * ( @@ -236,7 +281,7 @@ class BadukPanWidget(Widget): ) Color(*evalcol) Rectangle( - pos=(self.gridpos[y,x,0] - evalsize, self.gridpos[y,x,1] - evalsize), + pos=(self.gridpos[y][x][0] - evalsize, self.gridpos[y][x][1] - evalsize), size=(2 * evalsize, 2 * evalsize), texture=cached_texture(Theme.EVAL_DOT_TEXTURE), ) @@ -244,7 +289,7 @@ class BadukPanWidget(Widget): Color(*innercol) inner_size = stone_size * 0.8 Rectangle( - pos=(self.gridpos[y,x,0] - inner_size, self.gridpos[y,x,1] - inner_size), + pos=(self.gridpos[y][x][0] - inner_size, self.gridpos[y][x][1] - inner_size), size=(2 * inner_size, 2 * inner_size), texture=cached_texture(Theme.LAST_MOVE_TEXTURE), ) @@ -267,23 +312,45 @@ class BadukPanWidget(Widget): grid_spaces_margin_x, grid_spaces_margin_y = self.get_grid_spaces_margins() h = round(self.height, 4) w = round(self.width, 4) - x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces(board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y) + x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces( + board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y + ) self.grid_size = self.calculate_grid_size(w, h, x_grid_spaces, y_grid_spaces) - board_width_with_margins, board_height_with_margins = self.calculate_board_margins(x_grid_spaces, y_grid_spaces, self.grid_size) - extra_px_margin_x, extra_px_margin_y = self.calculate_extra_px_margins(w, h, board_width_with_margins, board_height_with_margins) + board_width_with_margins, board_height_with_margins = self.calculate_board_margins( + x_grid_spaces, y_grid_spaces, self.grid_size + ) + extra_px_margin_x, extra_px_margin_y = self.calculate_extra_px_margins( + w, h, board_width_with_margins, board_height_with_margins + ) self.stone_size = self.calculate_stone_size(self.grid_size) # if not initiated or if changed if ( - (len(self.gridpos) == 0 or abs(self.pos[0] + extra_px_margin_x - + math.floor(grid_spaces_margin_x[0] * self.grid_size + 0.5) - self.initial_gridpos_x[0]) > 0.001) + self.gridpos is None + or abs( + self.pos[0] + + extra_px_margin_x + + math.floor(grid_spaces_margin_x[0] * self.grid_size + 0.5) + - self.initial_gridpos_x[0] + ) + > 0.001 ): - self.initial_gridpos_x, self.initial_gridpos_y = self.initialize_gridpos_x_y(board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y, extra_px_margin_x, extra_px_margin_y, self.grid_size) + self.initial_gridpos_x, self.initial_gridpos_y = self.initialize_gridpos_x_y( + board_size_x, + board_size_y, + grid_spaces_margin_x, + grid_spaces_margin_y, + extra_px_margin_x, + extra_px_margin_y, + self.grid_size, + ) if self.rotation_degree == 0: self.initialize_gridpos() if (self.rotation_degree == 90 or self.rotation_degree == 270) and board_size_x != board_size_y: - board_size_y, board_size_x = katrain.game.board_size - x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces(board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y) + # Note that we use the board_size_y, board_size_x order for this rotation. + x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces( + board_size_y, board_size_x, grid_spaces_margin_x, grid_spaces_margin_y + ) self.grid_size = self.calculate_grid_size(w, h, x_grid_spaces, y_grid_spaces) self.stone_size = self.calculate_stone_size(self.grid_size) current_gridpos_x, current_gridpos_y = self.calculate_rotated_gridpos() @@ -293,18 +360,28 @@ class BadukPanWidget(Widget): # if window size got changed if ( - (self.gridpos[0,0,0] not in current_gridpos_x or self.gridpos[0,0,1] not in current_gridpos_y - or (self.gridpos[len(self.gridpos) - 1,len(self.gridpos[0]) - 1,0] in current_gridpos_x - or self.gridpos[len(self.gridpos) - 1,len(self.gridpos[0]) - 1,1] in current_gridpos_y)) + self.gridpos[0][0][0] not in current_gridpos_x + or self.gridpos[0][0][1] not in current_gridpos_y + or ( + self.gridpos[len(self.gridpos) - 1][len(self.gridpos[0]) - 1][0] in current_gridpos_x + or self.gridpos[len(self.gridpos) - 1][len(self.gridpos[0]) - 1][1] in current_gridpos_y + ) ): self.resize_board() - self.draw_board_background(katrain, current_gridpos_x, current_gridpos_y, x_grid_spaces, y_grid_spaces, grid_spaces_margin_x, grid_spaces_margin_y) + self.draw_board_background( + katrain, + current_gridpos_x, + current_gridpos_y, + x_grid_spaces, + y_grid_spaces, + grid_spaces_margin_x, + grid_spaces_margin_y, + ) self.draw_lines(current_gridpos_x, current_gridpos_y) self.draw_star_points(board_size_x, board_size_y) self.draw_coordinates(current_gridpos_x, current_gridpos_y) - def get_grid_spaces_margins(self): if self.draw_coords_enabled: grid_spaces_margin_x = [1.5, 0.75] # left, right @@ -314,46 +391,49 @@ class BadukPanWidget(Widget): grid_spaces_margin_y = [0.75, 0.75] # bottom, top return grid_spaces_margin_x, grid_spaces_margin_y - def calculate_grid_spaces(self, board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y): x_grid_spaces = board_size_x - 1 + sum(grid_spaces_margin_x) y_grid_spaces = board_size_y - 1 + sum(grid_spaces_margin_y) return x_grid_spaces, y_grid_spaces - def calculate_grid_size(self, width, height, x_grid_spaces, y_grid_spaces): # grid size is rounded to an integer to avoid rounding errors that # produce tiny gaps between shaded grid squares return math.floor(min(width / x_grid_spaces, height / y_grid_spaces) + 0.1) - def calculate_board_margins(self, x_grid_spaces, y_grid_spaces, grid_size): board_width_with_margins = x_grid_spaces * grid_size board_height_with_margins = y_grid_spaces * grid_size return board_width_with_margins, board_height_with_margins - def calculate_extra_px_margins(self, width, height, board_width_with_margins, board_height_with_margins): extra_px_margin_x = round((width - board_width_with_margins) / 2, 4) extra_px_margin_y = round((height - board_height_with_margins) / 2, 4) return extra_px_margin_x, extra_px_margin_y - def calculate_stone_size(self, grid_size): return grid_size * Theme.STONE_SIZE - def initialize_gridpos(self): - self.gridpos = np.zeros((len(self.initial_gridpos_y), len(self.initial_gridpos_x),2)) + self.gridpos = [[None for x in range(len(self.initial_gridpos_x))] for y in range(len(self.initial_gridpos_y))] + #[[None]*len(self.initial_gridpos_x)]*len(self.initial_gridpos_y) for y in range(len(self.initial_gridpos_y)): for x in range(len(self.initial_gridpos_x)): self.gridpos[y][x] = [self.initial_gridpos_x[x], self.initial_gridpos_y[y]] - - def initialize_gridpos_x_y(self, board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y, extra_px_margin_x, extra_px_margin_y, grid_size): + def initialize_gridpos_x_y( + self, + board_size_x, + board_size_y, + grid_spaces_margin_x, + grid_spaces_margin_y, + extra_px_margin_x, + extra_px_margin_y, + grid_size, + ): gridpos_x = [ round(self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * grid_size + 0.5), 4) for i in range(board_size_x) @@ -365,20 +445,32 @@ class BadukPanWidget(Widget): return gridpos_x, gridpos_y - def calculate_rotated_gridpos(self): board_size_y, board_size_x = self.katrain.game.board_size grid_spaces_margin_x, grid_spaces_margin_y = self.get_grid_spaces_margins() h = round(self.height, 4) w = round(self.width, 4) - x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces(board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y) - grid_size = self.calculate_grid_size(w, h , x_grid_spaces, y_grid_spaces) - board_width_with_margins, board_height_with_margins = self.calculate_board_margins(x_grid_spaces, y_grid_spaces, grid_size) - extra_px_margin_x, extra_px_margin_y = self.calculate_extra_px_margins(w, h, board_width_with_margins, board_height_with_margins) - - return self.initialize_gridpos_x_y(board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y, extra_px_margin_x, extra_px_margin_y, grid_size) + x_grid_spaces, y_grid_spaces = self.calculate_grid_spaces( + board_size_x, board_size_y, grid_spaces_margin_x, grid_spaces_margin_y + ) + grid_size = self.calculate_grid_size(w, h, x_grid_spaces, y_grid_spaces) + board_width_with_margins, board_height_with_margins = self.calculate_board_margins( + x_grid_spaces, y_grid_spaces, grid_size + ) + extra_px_margin_x, extra_px_margin_y = self.calculate_extra_px_margins( + w, h, board_width_with_margins, board_height_with_margins + ) + return self.initialize_gridpos_x_y( + board_size_x, + board_size_y, + grid_spaces_margin_x, + grid_spaces_margin_y, + extra_px_margin_x, + extra_px_margin_y, + grid_size, + ) def resize_board(self): rotated_gridpos_x, rotated_gridpos_y = self.calculate_rotated_gridpos() @@ -388,8 +480,8 @@ class BadukPanWidget(Widget): for yi in range(len(self.gridpos)): for xi in range(len(self.gridpos[0])): - current_gridpos_x.append(self.gridpos[yi,xi,0]) - current_gridpos_y.append(self.gridpos[yi,xi,1]) + current_gridpos_x.append(self.gridpos[yi][xi][0]) + current_gridpos_y.append(self.gridpos[yi][xi][1]) sorted_current_gridpos_x = list(set(current_gridpos_x)) sorted_current_gridpos_x.sort() sorted_current_gridpos_y = list(set(current_gridpos_y)) @@ -397,15 +489,16 @@ class BadukPanWidget(Widget): for yi in range(len(self.gridpos)): for xi in range(len(self.gridpos[0])): - index_x = sorted_current_gridpos_x.index(self.gridpos[yi,xi,0]) - index_y = sorted_current_gridpos_y.index(self.gridpos[yi,xi,1]) + index_x = sorted_current_gridpos_x.index(self.gridpos[yi][xi][0]) + index_y = sorted_current_gridpos_y.index(self.gridpos[yi][xi][1]) if self.rotation_degree == 90 or self.rotation_degree == 270: self.gridpos[yi][xi] = [rotated_gridpos_x[index_x], rotated_gridpos_y[index_y]] else: self.gridpos[yi][xi] = [self.initial_gridpos_x[index_x], self.initial_gridpos_y[index_y]] - - def draw_board_background(self, katrain, gridpos_x, gridpos_y, x_grid_spaces, y_grid_spaces, grid_spaces_margin_x, grid_spaces_margin_y): + def draw_board_background( + self, katrain, gridpos_x, gridpos_y, x_grid_spaces, y_grid_spaces, grid_spaces_margin_x, grid_spaces_margin_y + ): if katrain.game.insert_mode: Color(*Theme.INSERT_BOARD_COLOR_TINT) # dreamy else: @@ -426,21 +519,17 @@ class BadukPanWidget(Widget): for i in range(len(gridpos_y)): Line(points=[(gridpos_x[0], gridpos_y[i]), (gridpos_x[-1], gridpos_y[i])]) - def draw_star_points(self, board_size_x, board_size_y): def star_point_coords(size): star_point_pos = 3 if size <= 11 else 4 if size < 7: return [] - return [star_point_pos - 1, size - star_point_pos] + ( - [int(size / 2)] if size % 2 == 1 and size > 7 else [] - ) + return [star_point_pos - 1, size - star_point_pos] + ([int(size / 2)] if size % 2 == 1 and size > 7 else []) starpt_size = self.grid_size * Theme.STARPOINT_SIZE for x in star_point_coords(board_size_x): for y in star_point_coords(board_size_y): - draw_circle((self.gridpos[y,x,0], self.gridpos[y,x,1]), starpt_size, Theme.LINE_COLOR) - + draw_circle((self.gridpos[y][x][0], self.gridpos[y][x][1]), starpt_size, Theme.LINE_COLOR) def draw_coordinates(self, gridpos_x, gridpos_y): if self.draw_coords_enabled: @@ -466,38 +555,37 @@ class BadukPanWidget(Widget): font_name="Roboto", ) - def get_x_coordinate_text(self, i, board_size_x): x_text = Move.GTP_COORD[i] - if (self.rotation_degree == 90): + if self.rotation_degree == 90: x_text = str(i + 1) - elif (self.rotation_degree == 180): + elif self.rotation_degree == 180: x_text = Move.GTP_COORD[board_size_x - i - 1] - elif (self.rotation_degree == 270): + elif self.rotation_degree == 270: x_text = str(board_size_x - i) return x_text - def get_y_coordinate_text(self, i, board_size_y): y_text = str(i + 1) - if (self.rotation_degree == 90): + if self.rotation_degree == 90: y_text = Move.GTP_COORD[board_size_y - i - 1] - elif (self.rotation_degree == 180): + elif self.rotation_degree == 180: y_text = str(board_size_y - i) - elif (self.rotation_degree == 270): + elif self.rotation_degree == 270: y_text = Move.GTP_COORD[i] return y_text - def draw_board_contents(self, *_args): if not (self.katrain and self.katrain.game): return katrain = self.katrain board_size_x, board_size_y = katrain.game.board_size - if self.gridpos.shape[0] < board_size_x or self.gridpos.shape[1] < board_size_y: + if self.gridpos is None or len(self.gridpos) < board_size_y or len(self.gridpos[0]) < board_size_x: return # race condition show_n_eval = self.trainer_config.get("eval_on_show_last", 3) + ownership_grid = None + loss_grid = None with self.canvas: self.canvas.clear() @@ -517,32 +605,15 @@ class BadukPanWidget(Widget): [a - b for a, b in zip(current_node.children[-1].ownership, ownership)], (board_size_x, board_size_y), ) - for y in range(board_size_y - 1, -1, -1): for x in range(board_size_x): - loss = max(0, (-1 if current_node.children[-1].move.player == "B" else 1) * loss_grid[y][x]) - if loss > 0: - Color(*Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss) - Rectangle( - pos=( - self.gridpos[y,x,0] - self.grid_size / 2, - self.gridpos[y,x,1] - self.grid_size / 2, - ), - size=(self.grid_size, self.grid_size), - ) + loss_grid[y][x] = max( + 0, (-1 if current_node.children[-1].move.player == "B" else 1) * loss_grid[y][x] + ) + self.draw_territory(loss_grid, Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3]) else: ownership_grid = var_to_grid(ownership, (board_size_x, board_size_y)) - for y in range(board_size_y - 1, -1, -1): - for x in range(board_size_x): - ix_owner = "B" if ownership_grid[y][x] > 0 else "W" - Color( - *Theme.STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x]) * Theme.OWNERSHIP_MAX_ALPHA - ) - Rectangle( - pos=(self.gridpos[y,x,0] - self.grid_size / 2, self.gridpos[y,x,1] - self.grid_size / 2), - size=(self.grid_size, self.grid_size), - ) - + self.draw_territory(ownership_grid) # stones all_dots_off = not katrain.analysis_controls.eval.active has_stone = {} @@ -567,6 +638,7 @@ class BadukPanWidget(Widget): evalscale = min(1, max(0, realized_points_lost / points_lost)) placements = node.placements for m in node.moves + placements: + new_move = (current_node.move and m.coords == current_node.move.coords) and not current_node.ownership if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for move_eval_on = not all_dots_off and show_dots_for.get(m.player) and i < show_n_eval if move_eval_on and points_lost is not None: @@ -582,6 +654,10 @@ class BadukPanWidget(Widget): innercol=inner, evalcol=evalcol, evalscale=evalscale, + ownership=ownership_grid[m.coords[1]][m.coords[0]] + if ownership_grid and not loss_grid and not new_move + else None, + loss=loss_grid[m.coords[1]][m.coords[0]] if loss_grid else None, ) realized_points_lost = node.parent_realized_points_lost @@ -620,7 +696,7 @@ class BadukPanWidget(Widget): pol_order = max(0, 5 + int(math.log10(max(1e-9, move_policy - 1e-9)))) if move_policy > text_lb: draw_circle( - (self.gridpos[y,x,0], self.gridpos[y,x,1]), + (self.gridpos[y][x][0], self.gridpos[y][x][1]), self.stone_size * Theme.HINT_SCALE * 0.98, Theme.APPROX_BOARD_COLOR, ) @@ -628,14 +704,14 @@ class BadukPanWidget(Widget): else: scale = 0.5 draw_circle( - (self.gridpos[y,x,0], self.gridpos[y,x,1]), + (self.gridpos[y][x][0], self.gridpos[y][x][1]), Theme.HINT_SCALE * self.stone_size * scale, (*colors[pol_order][:3], Theme.POLICY_ALPHA), ) if move_policy > text_lb: Color(*Theme.HINT_TEXT_COLOR) draw_text( - pos=(self.gridpos[y,x,0], self.gridpos[y,x,1]), + pos=(self.gridpos[y][x][0], self.gridpos[y][x][1]), text=f"{100 * move_policy :.2f}"[:4] + "%", font_name="Roboto", font_size=self.grid_size / 4, @@ -645,8 +721,8 @@ class BadukPanWidget(Widget): Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA) Line( circle=( - self.gridpos[y,x,0], - self.gridpos[y,x,1], + self.gridpos[y][x][0], + self.gridpos[y][x][1], self.stone_size - dp(1.2), ), width=dp(2), @@ -664,15 +740,116 @@ class BadukPanWidget(Widget): self.redraw_hover_contents_trigger() - def draw_roi_box(self, region_of_interest, width:float=2): - xmin, xmax, ymin, ymax = region_of_interest + def draw_territory(self, grid, loss_color=None): + if Theme.TERRITORY_DISPLAY == "marks": + self.draw_territory_marks(grid, loss_color=None) + else: + self.draw_territory_color(grid, loss_color=None) + + def draw_territory_marks(self, grid, loss_color=None): + board_size_x, board_size_y = self.katrain.game.board_size + for y in range(board_size_y - 1, -1, -1): + for x in range(board_size_x): + if abs(grid[y][x]) < 0.01: + continue + (ix_owner, other) = ("B", "W") if grid[y][x] > 0 else ("W", "B") + Color( + *Theme.STONE_COLORS[ix_owner][:3], + 1.0 + ) + rect_size = Theme.MARK_SIZE * abs(grid[y][x]) * self.stone_size * 2.0 + Rectangle( + pos=( + self.gridpos[y][x][0] - rect_size / 2, + self.gridpos[y][x][1] - rect_size / 2, + ), + # radius=[rect_size / 4], + size=(rect_size, rect_size), + ) + + def draw_territory_color(self, grid, loss_color=None): + # This draws the expected black and white territories, or the loss during a teching game. + # We draw a blended territory by creating a small texture of size 19x19 (more precisely board_size) + # and painting it over the whole board. This causes Kivy to produce a smooth texture. + + # We add extra rows and columns (so the texture for a 19x19 board is actually 21x21) + # in order to ensure smooth rolloff of the painted area at the edges. The alpha in the + # extra rows is 0. + + board_size_x, board_size_y = self.katrain.game.board_size + texture = Texture.create(size=(board_size_x + 2, board_size_y + 2), colorfmt="rgba") + bytes = bytearray(4 * (board_size_y + 2) * (board_size_x + 2)) + for y in range(board_size_y + 2): + for x in range(board_size_x + 2): + x_coord = x - 1 + y_coord = y - 1 + + if x_coord < 0 or x_coord > board_size_x - 1 or y_coord < 0 or y_coord > board_size_y - 1: + # We're in the extra rows/columns outside the board + alpha = 0 + else: + alpha = abs(grid[y_coord][x_coord]) + if Theme.TERRITORY_DISPLAY == "blocks": + alpha = 1 if alpha > Theme.BLOCKS_THRESHOLD else 0 + alpha = alpha**(1.0/Theme.OWNERSHIP_GAMMA) + + x_coord = max(0, min(x_coord, board_size_x - 1)) + y_coord = max(0, min(y_coord, board_size_y - 1)) + + ix_owner = "B" if grid[y_coord][x_coord] > 0 else "W" + if loss_color is None: + pixel = Theme.OWNERSHIP_COLORS[ix_owner][:4] + pixel[3] *= alpha + else: + pixel = *loss_color, min(1.0, alpha) + pixel = tuple(map(lambda p: int(p * 255), pixel)) + idx = 4 * y * (board_size_x + 2) + x * 4 + bytes[idx : idx + 4] = pixel + + if Theme.TERRITORY_DISPLAY == "blocks": + texture.mag_filter = "nearest" + texture.blit_buffer(bytes, colorfmt="rgba", bufferfmt="ubyte") + Color(1, 1, 1, 1) + lx = board_size_x - 1 + ly = board_size_y - 1 + left = min(self.gridpos[0][0][1], self.gridpos[ly][lx][1]) + bottom = min(self.gridpos[0][0][0], self.gridpos[ly][lx][0]) + + # Our texture is 3 squares larger than the grid of lines: we added 2 rows/columns + # for the edge blending, and the additional 1 is because the grid of + # intersections is 1 smaller than the board state. We will shift the texture by 3/2 square + # to align it. + left = left - self.grid_size * 3 / 2 + bottom = bottom - self.grid_size * 3 / 2 + + PushMatrix() + + Rotate(origin=(bottom, left), axis=(0, 0, 1), angle=-self.rotation_degree) + if self.rotation_degree in (90, 180): + Translate(-self.grid_size * (board_size_x + 2), 0, 0) + if self.rotation_degree in (180, 270): + Translate(0, -self.grid_size * (board_size_y + 2), 0) + + Rectangle( + pos=(bottom, left), + size=(self.grid_size * (board_size_x + 2), self.grid_size * (board_size_y + 2)), + texture=texture, + ) + + PopMatrix() + + def draw_roi_box(self, region_of_interest, width: float = 2): + x1, x2, y1, y2 = region_of_interest + x_start, y_start = self.gridpos[y1][x1] + x_end, y_end = self.gridpos[y2][x2] + Color(*Theme.REGION_BORDER_COLOR) Line( rectangle=( - self.gridpos[ymin,xmin,0] - self.grid_size / 3, - self.gridpos[ymin,xmin,1] - self.grid_size / 3, - (xmax - xmin + 2 / 3) * self.grid_size, - (ymax - ymin + 2 / 3) * self.grid_size, + min(x_start, x_end) - self.grid_size / 3, + min(y_start, y_end) - self.grid_size / 3, + abs(x_start - x_end) + (2 / 3) * self.grid_size, + abs(y_start - y_end) + (2 / 3) * self.grid_size, ), width=width, ) @@ -746,6 +923,9 @@ class BadukPanWidget(Widget): text_on = False alpha = Theme.HINTS_LO_ALPHA + if scale <= 0: # if theme turns hints off, do not draw them + continue + if "pv" in move_dict: self.active_pv_moves.append((move.coords, move_dict["pv"], current_node)) else: @@ -754,14 +934,20 @@ class BadukPanWidget(Widget): evalcol = self.eval_color(move_dict["pointsLost"]) if text_on and top_moves_show: # remove grid lines using a board colored circle draw_circle( - (self.gridpos[move.coords[1],move.coords[0],0], self.gridpos[move.coords[1],move.coords[0],1]), + ( + self.gridpos[move.coords[1]][move.coords[0]][0], + self.gridpos[move.coords[1]][move.coords[0]][1], + ), self.stone_size * scale * 0.98, Theme.APPROX_BOARD_COLOR, ) Color(*evalcol[:3], alpha) Rectangle( - pos=(self.gridpos[move.coords[1],move.coords[0],0] - evalsize, self.gridpos[move.coords[1],move.coords[0],1] - evalsize), + pos=( + self.gridpos[move.coords[1]][move.coords[0]][0] - evalsize, + self.gridpos[move.coords[1]][move.coords[0]][1] - evalsize, + ), size=(2 * evalsize, 2 * evalsize), texture=cached_texture(Theme.TOP_MOVE_TEXTURE), ) @@ -791,7 +977,10 @@ class BadukPanWidget(Widget): Color(*Theme.HINT_TEXT_COLOR) draw_text( - pos=(self.gridpos[move.coords[1],move.coords[0],0], self.gridpos[move.coords[1],move.coords[0],1]), + pos=( + self.gridpos[move.coords[1]][move.coords[0]][0], + self.gridpos[move.coords[1]][move.coords[0]][1], + ), text=fmt.format(**keys), font_name="Roboto", markup=True, @@ -804,8 +993,8 @@ class BadukPanWidget(Widget): Color(*Theme.TOP_MOVE_BORDER_COLOR) Line( circle=( - self.gridpos[move.coords[1],move.coords[0],0], - self.gridpos[move.coords[1],move.coords[0],1], + self.gridpos[move.coords[1]][move.coords[0]][0], + self.gridpos[move.coords[1]][move.coords[0]][1], self.stone_size - dp(1.2), ), width=dp(1.2), @@ -826,8 +1015,8 @@ class BadukPanWidget(Widget): Color(*Theme.NEXT_MOVE_DASH_CONTRAST_COLORS[child_node.player]) Line( circle=( - self.gridpos[move.coords[1],move.coords[0],0], - self.gridpos[move.coords[1],move.coords[0],1], + self.gridpos[move.coords[1]][move.coords[0]][0], + self.gridpos[move.coords[1]][move.coords[0]][1], self.stone_size - dp(1.2), ), width=dp(1.2), @@ -838,8 +1027,8 @@ class BadukPanWidget(Widget): for s in range(0, 360, 30): Line( circle=( - self.gridpos[move.coords[1],move.coords[0],0], - self.gridpos[move.coords[1],move.coords[0],1], + self.gridpos[move.coords[1]][move.coords[0]][0], + self.gridpos[move.coords[1]][move.coords[0]][1], self.stone_size - dp(1.2), s, s + dashed_width, @@ -848,8 +1037,7 @@ class BadukPanWidget(Widget): ) if self.selecting_region_of_interest and len(self.region_of_interest) == 4: - x1, x2, y1, y2 = self.region_of_interest - self.draw_roi_box([min(x1, x2), max(x1, x2), min(y1, y2), max(y1, y2)], width=dp(2)) + self.draw_roi_box(self.region_of_interest, width=dp(2)) else: # hover next move ghost stone if self.ghost_stone: @@ -890,7 +1078,10 @@ class BadukPanWidget(Widget): hide_node = cn while hide_node and hide_node.move and hide_node != node: if not hide_node.move.is_pass: - pos = (self.gridpos[hide_node.move.coords[1],hide_node.move.coords[0],0], self.gridpos[hide_node.move.coords[1],hide_node.move.coords[0],1]) + pos = ( + self.gridpos[hide_node.move.coords[1]][hide_node.move.coords[0]][0], + self.gridpos[hide_node.move.coords[1]][hide_node.move.coords[0]][1], + ) draw_circle(pos, self.stone_size, [0.85, 0.68, 0.40, 0.8]) hide_node = hide_node.parent for i, gtpmove in enumerate(pv): @@ -907,7 +1098,7 @@ class BadukPanWidget(Widget): katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2, ] else: - board_coords = (self.gridpos[coords[1],coords[0],0], self.gridpos[coords[1],coords[0],1]) + board_coords = (self.gridpos[coords[1]][coords[0]][0], self.gridpos[coords[1]][coords[0]][1]) sizefac = 1 stone_size = self.stone_size * sizefac @@ -957,10 +1148,10 @@ class BadukPanWidget(Widget): x0 = self.initial_gridpos_x[0] y0 = self.initial_gridpos_y[0] - pos = self.gridpos.tolist() + pos = copy.deepcopy(self.gridpos) for yi in range(len(self.gridpos)): for xi in range(len(self.gridpos[0])): - if (self.rotation_degree == 90 or self.rotation_degree == 270): + if self.rotation_degree == 90 or self.rotation_degree == 270: gridpos_x = pos[len(self.gridpos) - 1 - yi][:] else: gridpos_x = pos[yi][:] @@ -973,9 +1164,11 @@ class BadukPanWidget(Widget): elif y0 > x0: x = round(x - diff, 4) y = round(y + diff, 4) - self.gridpos[yi][xi] = (x, y) + self.gridpos[yi][xi] = [x, y] else: - self.gridpos = np.rot90(self.gridpos, -1, (0, 1)) + # This is a rot90 for list of lists. Based on the code found in + # stackoverflow.com/questions/8421337/rotating-a-two-dimensional-array-in-python + self.gridpos = list(list(x) for x in zip(*reversed(self.gridpos))) self.rotation_degree += 90 if self.rotation_degree == 360: @@ -990,9 +1183,10 @@ class BadukPanWidget(Widget): class AnalysisDropDown(DropDown): def open_game_analysis_popup(self, *_args): - analysis_popup = I18NPopup(title_key="analysis:game", size=[dp(500), dp(350)], content=ReAnalyzeGamePopup()) + analysis_popup = I18NPopup( + title_key="analysis:game", size=[dp(500), dp(350)], content=ReAnalyzeGamePopup(MDApp.get_running_app().gui) + ) analysis_popup.content.popup = analysis_popup - analysis_popup.content.katrain = MDApp.get_running_app().gui analysis_popup.open() def open_report_popup(self, *_args): diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 48fd53f..a82ebdb 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -839,9 +839,27 @@ class SaveSGFPopup(BoxLayout): class ReAnalyzeGamePopup(BoxLayout): - katrain = ObjectProperty(None) popup = ObjectProperty(None) + def on_checkbox_active(self, checkbox, value): + self.start_move.opacity = 1.0 if value else 0.3 + self.end_move.opacity = 1.0 if value else 0.3 + self.start_move.disabled = not value + self.end_move.disabled = not value + + def __init__(self, katrain, **kwargs): + super().__init__(**kwargs) + + self.katrain = katrain + self.move_range.bind(active=self.on_checkbox_active) + + self.start_move.disabled = True + self.end_move.disabled = True + self.start_move.opacity = 0.3 + self.end_move.opacity = 0.3 + + self.start_move.text = str(katrain.game.current_node.depth) + def on_submit(self): self.button.trigger_action(duration=0) diff --git a/katrain/gui/theme.py b/katrain/gui/theme.py index 203124a..de6c501 100644 --- a/katrain/gui/theme.py +++ b/katrain/gui/theme.py @@ -65,6 +65,7 @@ class Theme: LAST_MOVE_TEXTURE = "inner.png" TOP_MOVE_TEXTURE = "topmove.png" BOARD_TEXTURE = "board.png" + BOARD_TEXTURE = "wood6.jpg" GRAPH_TEXTURE = "graph_bg.png" # sounds STONE_SOUNDS = [f"stone{i}.wav" for i in [1, 2, 3, 4, 5]] @@ -98,12 +99,12 @@ class Theme: # board theme APPROX_BOARD_COLOR = [0.95, 0.75, 0.47, 1] # for drawing on top of / hiding what's under it - BOARD_COLOR_TINT = [1, 0.95, 0.8, 1] # multiplied by texture + BOARD_COLOR_TINT = [1, 1, 1, 1] # multiplied by texture HINT_TEXT_COLOR = BLACK REGION_BORDER_COLOR = LIGHTER_BACKGROUND_COLOR - INSERT_BOARD_COLOR_TINT = [1, 1, 1, 0.75] + INSERT_BOARD_COLOR_TINT = [1, 1, 1, 0.6] PASS_CIRCLE_COLOR = [0.45, 0.05, 0.45, 0.7] PASS_CIRCLE_TEXT_COLOR = [0.85, 0.85, 0.85] @@ -120,7 +121,17 @@ class Theme: GHOST_ALPHA = 0.6 POLICY_ALPHA = 0.5 - OWNERSHIP_MAX_ALPHA = 0.7 # max ownership brightness + OWNERSHIP_COLORS = {"B": [0.0, 0.0, 0.10, 0.75], "W": [0.92, 0.92, 1.0, 0.800]} + OWNERSHIP_GAMMA = 1.33 + STONE_MIN_ALPHA = 0.7 # the minimal alpha for dead/weak stones + + TERRITORY_DISPLAY = "blended" # other possibilities are "marks" and "blocks" + BLOCKS_THRESHOLD = 0.3 # in "blocks" mode, territory which is this likely to be + # a certain player's gets his color + STONE_MARKS = "all" # all: always display marks on stones + # none: no marks on stones, indicate ownership by transparency only + # weak: draw marks only on stones likely (>50%) to be captured + MARK_SIZE = 0.42 # stone mark size as fraction of stone size HINTS_LO_ALPHA = 0.6 HINTS_ALPHA = 0.8 diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index 27544c3..e15d650 100644 Binary files a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po index b3febb4..dc03016 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -945,3 +945,20 @@ msgstr "Ko allowed?" #. TODO - Menu option for tsumego frame around position msgid "analysis:tsumegoframe" msgstr "Tsumego Frame" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo index a457c23..74f10d3 100644 Binary files a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po index a6f88ae..7332435 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -1014,3 +1014,20 @@ msgstr "Ko allowed?" #. TODO - Menu option for tsumego frame around position msgid "analysis:tsumegoframe" msgstr "Tsumego Frame" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index 0a48d0c..ab952cd 100644 Binary files a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po index ad2eb95..7d3913d 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -105,6 +105,16 @@ msgstr "Fast analysis of all moves" msgid "analysis:game" msgstr "Deeper full game analysis" +#. Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" + +msgid "from move" +msgstr "from move" + +msgid "to move" +msgstr "to move" + #. Menu option for tsumego frame around position msgid "analysis:tsumegoframe" msgstr "Tsumego Frame" @@ -210,6 +220,11 @@ msgstr "Equalizing analysis of candidate moves to {visits} visits" msgid "game re-analysis" msgstr "Re-analyzing entire game using {visits} visits" +#. status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + #. right panel buttons and below board msgid "pass-button-text" msgstr "Pass" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index a79d1cc..f1b7b6c 100644 Binary files a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po index 2d59fc4..e1ffe2a 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -984,3 +984,19 @@ msgstr "Permettre les kos" msgid "analysis:tsumegoframe" msgstr "Composer un problème" + +#. Restrict analysis to move range +msgid "limit to moves" +msgstr "Limiter aux coups" + +msgid "from move" +msgstr "du coup" + +msgid "to move" +msgstr "au coup" + +#. status message +msgid "move range analysis" +msgstr "" +"Analyse de partie approfondie du coup {start_move} au coup {end_move} " +"({visits} visites/coup)." diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index 14c3c59..a5cc9c8 100644 Binary files a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po index 2730b7c..6d8c36f 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -986,3 +986,20 @@ msgstr "劫でもOK" #. Menu option for tsumego frame around position msgid "analysis:tsumegoframe" msgstr "詰碁用の外枠" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index 9926e21..de0d073 100644 Binary files a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po index 3424cd7..7bb7333 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -919,3 +919,20 @@ msgstr "패로 충분?" msgid "analysis:tsumegoframe" msgstr "묘수풀이 틀" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index 45ac6d2..34ef8fe 100644 Binary files a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po index ce3b104..edd7b2d 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -722,9 +722,9 @@ msgstr "Изменение счёта" msgid "show stats if" msgstr "Числа только если посещений больше чем" -#. TODO - point loss precision options +#. point loss precision options msgid "show two digits for point loss near zero" -msgstr "Show two digits for point loss near zero" +msgstr "Показазывать две цифры после запятой для малой потери очков" msgid "cache analysis to sgf" msgstr "Кэшировать анализ в SGF" @@ -929,28 +929,40 @@ msgstr "" msgid "Entire Game" msgstr "Полная игра" -#. TODO msgid "ai:antimirror" -msgstr "KataAntiMirror" +msgstr "Противозеркальная" -#. TODO - on move tree editing, deletes all other branches splitting off from +#. on move tree editing, deletes all other branches splitting off from #. the #. path UP TO this node msgid "Prune Branch" -msgstr "Prune Branch" +msgstr "Удалить ответвления" -#. TODO msgid "aihelp:antimirror" -msgstr "Anti Mirror-go Mode of KataGo." +msgstr "Режим противодействия зеркальной стратегии." -#. TODO - distance of wall to existing stones +#. distance of wall to existing stones msgid "tsumego:walldistance" -msgstr "Distance of wall" +msgstr "Расстояние до стены" -#. TODO - tsumego: ko is good enough? +#. tsumego: ko is good enough? msgid "tsumego:ko" -msgstr "Ko allowed?" +msgstr "Разрешить ко?" -#. TODO - Menu option for tsumego frame around position +#. Menu option for tsumego frame around position msgid "analysis:tsumegoframe" -msgstr "Tsumego Frame" +msgstr "Рамка для цумэ-го" + +#. Move range analysis messages +msgid "from move" +msgstr "от хода" + +msgid "to move" +msgstr "до хода" + +msgid "limit to moves" +msgstr "Только для ходов" + +#. status message +msgid "move range analysis" +msgstr "Анализ ходов от {start_move} до {end_move} с {visits} посещениями" diff --git a/katrain/i18n/locales/tr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/tr/LC_MESSAGES/katrain.mo index 4db2207..334fdf2 100644 Binary files a/katrain/i18n/locales/tr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/tr/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/tr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/tr/LC_MESSAGES/katrain.po index c6a1eae..b1db245 100644 --- a/katrain/i18n/locales/tr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/tr/LC_MESSAGES/katrain.po @@ -1049,3 +1049,20 @@ msgstr "Tsumego Çerçevesi" #. TODO - point loss precision options msgid "show two digits for point loss near zero" msgstr "Show two digits for point loss near zero" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo index e124d13..38ca493 100644 Binary files a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po index d8664bd..5f3caef 100644 --- a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po @@ -968,3 +968,20 @@ msgstr "允許打劫?" #. Menu option for tsumego frame around position msgid "analysis:tsumegoframe" msgstr "詰碁框" + +#. TODO +msgid "from move" +msgstr "from move" + +#. TODO - status message +msgid "move range analysis" +msgstr "" +"Re-analyzing moves from {start_move} to {end_move} using {visits} visits" + +#. TODO +msgid "to move" +msgstr "to move" + +#. TODO - Restrict analysis to move range +msgid "limit to moves" +msgstr "Limit to moves" diff --git a/katrain/img/wood6.jpg b/katrain/img/wood6.jpg new file mode 100644 index 0000000..335650c Binary files /dev/null and b/katrain/img/wood6.jpg differ diff --git a/katrain/popups.kv b/katrain/popups.kv index b8a0bce..6801ca4 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -811,37 +811,73 @@ on_ref_press: app.webbrowser('engine:help') if args[1]=='engine_help' else app.gui('config-popup') + + : visits: visits orientation: 'vertical' button: button + move_range: move_range + start_move: start_move + end_move: end_move + mistakes: mistakes BoxLayout: size_hint: 1, 1.5 - orientation: 'horizontal' - DescriptionLabel: - text: i18n._('reanalyze max visits') + orientation: 'vertical' + GridLayout: + cols: 2 + rows: 2 + size_hint: 1, 2 + DescriptionLabel: + text: i18n._('reanalyze max visits') + AnchorLayout: + LabelledIntInput: + id: visits + text: '2500' + BoxLayout: + AnchorLayout: + size_hint: 0.3333, 1 + LabelledCheckBox: + id: move_range + default_active: False + DescriptionLabel: + size_hint: 0.6667, 1 + text: i18n._('limit to moves') + halign: 'left' + BoxLayout: + AnchorLayout: + LabelledIntInput: + helper_text: i18n._('from move') + helper_text_mode: "persistent" + id: start_move + text: '0' + AnchorLayout: + LabelledIntInput: + helper_text: i18n._('to move') + helper_text_mode: "persistent" + id: end_move + text: '999' + BoxLayout: + size_hint: 1, 1 + AnchorLayout: + size_hint: 0.16666, 1 + LabelledCheckBox: + id: mistakes + default_active: False + DescriptionLabel: + size_hint: 0.833333, 1 + text: i18n._('reanalyze mistakes only') + halign: 'left' AnchorLayout: - LabelledIntInput: - id: visits - text: '2500' - BoxLayout: - size_hint: 1, 1.5 - orientation: 'horizontal' - DescriptionLabel: - text: i18n._('reanalyze mistakes only') - AnchorLayout: - LabelledCheckBox: - id: mistakes - AnchorLayout: - size_hint: 1, 1 - AutoSizedRoundedRectangleButton: - id: button - padding_x: 15 - size_hint: None,0.66 - text: i18n._("analysis:game") # re-use string - on_press: - root.katrain("analyze-extra", "game", visits=visits.input_value, mistakes_only=mistakes.input_value) - root.popup.dismiss() + size_hint: 1, 0.66 + AutoSizedRoundedRectangleButton: + id: button + padding_x: 15 + size_hint: None,0.66 + text: i18n._("analysis:game") # re-use string + on_press: + root.katrain("analyze-extra", "game", visits=visits.input_value, move_range=(start_move.input_value, end_move.input_value) if move_range.input_value else None, mistakes_only=mistakes.input_value) + root.popup.dismiss() : ko: ko diff --git a/requirements.txt b/requirements.txt index e02342c..0a69d6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ kivymd==0.104.1 Pillow==9.0.1 Pygments==2.7.4 requests==2.25.0 -urllib3==1.26.5 -numpy==1.22.3 +urllib3==1.26.5 \ No newline at end of file diff --git a/setup.py b/setup.py index 4325adb..bb4e5e7 100644 --- a/setup.py +++ b/setup.py @@ -42,16 +42,15 @@ setup( "wheel", "setuptools", "kivy[full]>=2.1.0", - "kivymd==0.104.1", # 0.104.2 was breaking MRO + "kivymd==0.104.1", # 0.104.2+ was breaking MRO "ffpyplayer", "urllib3", - "numpy", "pygame;platform_system=='Darwin'", # some mac versions need this for kivy "screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos "chardet", # for automatic encoding detection ], dependency_links=["https://kivy.org/downloads/simple/"], - python_requires=">=3.7, <3.10", + python_requires=">=3.7, <3.11", entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]}, classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/spec/KaTrain.spec b/spec/KaTrain.spec index 2730d66..6c34cfb 100644 --- a/spec/KaTrain.spec +++ b/spec/KaTrain.spec @@ -2,6 +2,7 @@ from kivy_deps import sdl2, glew from kivymd import hooks_path as kivymd_hooks_path import subprocess +import sys block_cipher = None @@ -25,7 +26,7 @@ a = Analysis( ], hiddenimports=["win32file", "win32timezone", "six"], # FileChooser in kivy loads win32file conditionally, mkl needs six hookspath=[kivymd_hooks_path], - excludes=["matplotlib", "docutils", "scipy", "pandas"], + excludes=["scipy", "pandas", "numpy", "matplotlib", "docutils", "mkl"], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=None, @@ -48,6 +49,11 @@ console_names = {True:"DebugKaTrain",False:"KaTrain"} powershell = subprocess.Popen(["powershell"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) +# load and run script to buid VSVersionInfo object +sys.path.append(SPECPATH) +import file_version as versionModule + + for console, name in console_names.items(): @@ -63,7 +69,8 @@ for console, name in console_names.items(): strip=False, upx=True, console=console, - icon="C:\\icon.ico", + icon="..\\katrain\img\\icon.ico", + version=versionModule.versionInfo, ) coll = COLLECT( @@ -90,7 +97,8 @@ for console, name in console_names.items(): upx=True, name=name, console=console, - icon="C:\\icon.ico", + icon="..\\katrain\img\\icon.ico", + version=versionModule.versionInfo, ) powershell.stdin.write(f"Set-AuthenticodeSignature dist/{name}.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)\n".encode('ascii')) powershell.stdin.write(f"Set-AuthenticodeSignature dist/{name}/{name}.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert)\n".encode('ascii')) diff --git a/spec/file_version.py b/spec/file_version.py new file mode 100644 index 0000000..907f8b5 --- /dev/null +++ b/spec/file_version.py @@ -0,0 +1,59 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.win32 import versioninfo as ver +import os + +# only create VSVersionInfo object on Windows +versionInfo = None + +if os.name == "nt": + + # load constants.py to get version number and program name + import katrain.core.constants as constants + + # build VSVersionInfo compatible version numbers + verParts = constants.VERSION.split(".") + numParts = len(verParts) + verList = list() + verString = "" + + for i in range(4): + if i <= numParts - 1: + verList.append(int(verParts[i])) + verString = verString + verParts[i] + "." + else: + verList.append(int(0)) + verString = verString + "0." + + verString = verString.rstrip(".") + verTuple = tuple(verList) + + versionInfo = ver.VSVersionInfo( + ffi=ver.FixedFileInfo( + filevers=verTuple, # must be 4 item tuple + prodvers=verTuple, # must be 4 item tuple + mask=0x3F, # Contains a bitmask that specifies the valid bits 'flags'r + flags=0x0, # Contains a bitmask that specifies the Boolean attributes of the file. + OS=0x40004, # The operating system for which this file was designed. 0x4 - NT and there is no need to change it. + fileType=0x1, # The general type of file. 0x1 - the file is an application. + subtype=0x0, # The function of the file. 0x0 - the function is not defined for this fileType + date=(0, 0), # Creation date and time stamp. + ), + kids=[ + ver.StringFileInfo( + [ + ver.StringTable( + "040904B0", + [ + ver.StringStruct("FileDescription", constants.PROGRAM_NAME), + ver.StringStruct("FileVersion", verString), + ver.StringStruct("LegalCopyright", constants.HOMEPAGE), + ver.StringStruct("OriginalFilename", constants.PROGRAM_NAME + ".exe"), + ver.StringStruct("ProductName", constants.PROGRAM_NAME), + ver.StringStruct("ProductVersion", verString), + ], + ) + ] + ), + ver.VarFileInfo([ver.VarStruct("Translation", [1033, 1200])]), + ], + ) diff --git a/themes/blended-all.png b/themes/blended-all.png new file mode 100644 index 0000000..f39ebc1 Binary files /dev/null and b/themes/blended-all.png differ diff --git a/themes/blended-weak.png b/themes/blended-weak.png new file mode 100644 index 0000000..d866b30 Binary files /dev/null and b/themes/blended-weak.png differ diff --git a/themes/blocks-none.png b/themes/blocks-none.png new file mode 100644 index 0000000..a7b800c Binary files /dev/null and b/themes/blocks-none.png differ diff --git a/themes/eric-lizzie-look.zip b/themes/eric-lizzie-look.zip index f1b489d..b98a06c 100644 Binary files a/themes/eric-lizzie-look.zip and b/themes/eric-lizzie-look.zip differ diff --git a/themes/koast-theme.zip b/themes/koast-theme.zip index c0731ba..a6d2faa 100644 Binary files a/themes/koast-theme.zip and b/themes/koast-theme.zip differ diff --git a/themes/marks-weak.png b/themes/marks-weak.png new file mode 100644 index 0000000..6927269 Binary files /dev/null and b/themes/marks-weak.png differ