diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md index 7b8a0bf..3395703 100644 --- a/CONTRIBUTIONS.md +++ b/CONTRIBUTIONS.md @@ -55,6 +55,7 @@ Many thanks to these additional authors: * "xiaoyifang" for adding the reset analysis option, feature to save options on the loading screen, and scrolling through variations. * "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. ## Translators diff --git a/LICENSE b/LICENSE index 46e7116..2786133 100644 --- a/LICENSE +++ b/LICENSE @@ -5,10 +5,11 @@ For on related licenses for these binaries and libraries see https://github.com/ 2. Icons from www.flaticon.com, used with permission with the following attributions: - Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com -- Other Menu icons, Finish, Collaboration, and Flag icons: derived from work by Freepik from www.flaticon.com +- Other Menu icons, Finish, Collaboration and Flag icons: derived from work by Freepik from www.flaticon.com - Collapse branch icon: derived from work by Kirill Kazachek from www.flaticon.com - Prune icon: derived from work by Pixelmeetup from www.flaticon.com - Reset icon: derived from work by Pixel Perfect from www.flaticon.com +- Rotate icon: derived from work by Frey Wazza from www.flaticon.com 3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use. diff --git a/katrain/__main__.py b/katrain/__main__.py index a980ee8..14fb5d1 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -1,10 +1,9 @@ """isort:skip_file""" # first, logging level lower import os +import sys os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning") -# if "KIVY_AUDIO" not in os.environ: # trying default again -# os.environ["KIVY_AUDIO"] = "sdl2" # some backends hard crash / this seems to be most stable import kivy @@ -17,13 +16,19 @@ from kivy.utils import platform ICON = find_package_resource("katrain/img/icon.ico") Config.set("kivy", "window_icon", ICON) - Config.set("input", "mouse", "mouse,multitouch_on_demand") +# next, certificates on package builds https://github.com/sanderland/katrain/issues/414 +if getattr(sys, "frozen", False): + import ssl + + if ssl.get_default_verify_paths().cafile is None and hasattr(sys, "_MEIPASS"): + os.environ["SSL_CERT_FILE"] = os.path.join(sys._MEIPASS, "certifi", "cacert.pem") + + import re import signal import json -import sys import threading import traceback from queue import Queue @@ -198,6 +203,7 @@ class KaTrainGui(Screen, KaTrainBase): self.board_controls.mid_circles_container.clear_widgets() self.board_controls.mid_circles_container.add_widget(bot) self.board_controls.mid_circles_container.add_widget(top) + self.controls.players["W"].captures = prisoners["W"] self.controls.players["B"].captures = prisoners["B"] @@ -257,7 +263,7 @@ class KaTrainGui(Screen, KaTrainBase): and not (teaching_undo and cn.auto_undo is None) ): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly. self._do_ai_move(cn) - Clock.schedule_once(self.board_gui.play_stone_sound, 0.25) + Clock.schedule_once(self._play_stone_sound, 0.25) if self.engine: if self.pondering: self.game.analyze_extra("ponder") @@ -331,6 +337,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.engine.on_new_game() # clear queries self.game = Game( self, @@ -394,6 +401,8 @@ 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() def _do_find_mistake(self, fn="redo"): self.board_gui.animating_pv = None @@ -403,10 +412,19 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None self.controls.move_tree.switch_branch(*args) + def _play_stone_sound(self,_dt=None): + play_sound(random.choice(Theme.STONE_SOUNDS)) + def _do_play(self, coords): self.board_gui.animating_pv = None try: + old_prisoner_count = self.game.prisoner_count["W"] + self.game.prisoner_count["B"] self.game.play(Move(coords, player=self.next_player_info.player)) + if old_prisoner_count < self.game.prisoner_count["W"] + self.game.prisoner_count["B"]: + play_sound(Theme.CAPTURING_SOUND) + elif not self.game.current_node.is_pass: + self._play_stone_sound() + except IllegalMoveException as e: self.controls.set_status(f"Illegal Move: {str(e)}", STATUS_ERROR) @@ -443,7 +461,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.timer.paused = True if not self.teacher_settings_popup: self.teacher_settings_popup = I18NPopup( - title_key="teacher settings", size=[dp(800), dp(800)], content=ConfigTeacherPopup(self) + title_key="teacher settings", size=[dp(800), dp(825)], content=ConfigTeacherPopup(self) ).__self__ self.teacher_settings_popup.content.popup = self.teacher_settings_popup self.teacher_settings_popup.open() @@ -513,8 +531,8 @@ class KaTrainGui(Screen, KaTrainBase): self.update_state(redraw_board=True) def play_mistake_sound(self, node): - if self.config("timer/sound") and node.played_sound is None and Theme.MISTAKE_SOUNDS: - node.played_sound = True + if self.config("timer/sound") and node.played_mistake_sound is None and Theme.MISTAKE_SOUNDS: + node.played_mistake_sound = True play_sound(random.choice(Theme.MISTAKE_SOUNDS)) def load_sgf_file(self, file, fast=False, rewind=True): diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 0bd8876..593c59a 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) + 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/contribute_engine.py b/katrain/core/contribute_engine.py index 9a46ff6..a2a9a9f 100644 --- a/katrain/core/contribute_engine.py +++ b/katrain/core/contribute_engine.py @@ -137,7 +137,6 @@ class KataGoContributeEngine(BaseEngine): def queries_remaining(self): return 1 - def start(self): try: self.katrain.log(f"Starting Distributed KataGo with {self.command}", OUTPUT_INFO) diff --git a/katrain/core/game.py b/katrain/core/game.py index b93085b..94d2f8f 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -62,6 +62,7 @@ class BaseGame: self.insert_mode = False self.external_game = False # not generated by katrain at some point + if move_tree: self.root = move_tree self.external_game = PROGRAM_NAME not in self.root.get_property("AP", "") diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index b83f9b6..933788f 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -41,7 +41,7 @@ class GameNode(SGFNode): def __init__(self, parent=None, properties=None, move=None): super().__init__(parent=parent, properties=properties, move=move) self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move) - self.played_sound = None + self.played_mistake_sound = None self.ai_thoughts = "" self.note = "" self.move_number = 0 diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index c406278..7089160 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -373,6 +373,8 @@ 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 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 e4119bc..4da4939 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -272,6 +272,9 @@ icon: 'Next-Mistake.png' color: Theme.MISTAKE_BUTTON_COLOR on_press: root.katrain('find-mistake','redo') + NavIconButton: + icon: 'Rotate.png' + on_press: root.katrain('rotate') ClickableLabel: pos_hint: {'center_x': 0.85,'center_y':0.5} size_hint: None, 1 diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 01d04a8..02a8bd9 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -1,7 +1,7 @@ import math -import random import time from typing import List, Optional +import numpy as np from kivy.clock import Clock from kivy.core.window import Window @@ -14,7 +14,6 @@ from kivy.uix.widget import Widget from kivymd.app import MDApp from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.floatlayout import MDFloatLayout -from katrain.gui.sound import play_sound from katrain.core.constants import ( MODE_PLAY, @@ -40,11 +39,12 @@ from katrain.gui.theme import Theme class BadukPanWidget(Widget): def __init__(self, **kwargs): super(BadukPanWidget, self).__init__(**kwargs) - self.stones_sounds = [] self.trainer_config = {} self.ghost_stone = [] - self.gridpos_x = [] - self.gridpos_y = [] + self.gridpos = np.zeros((0,0,2)) + self.initial_gridpos_x = [] + self.initial_gridpos_y = [] + self.rotation_degree = 0 self.grid_size = 0 self.stone_size = 0 self.selecting_region_of_interest = False @@ -70,18 +70,25 @@ class BadukPanWidget(Widget): def get_enable_coordinates(self): return self.draw_coords_enabled - def play_stone_sound(self, *_args): - play_sound(random.choice(Theme.STONE_SOUNDS)) - # stone placement functions - def _find_closest(self, pos, gridpos): - return sorted([(abs(p - pos), i) for i, p in enumerate(gridpos)])[0] + def _find_closest(self, pos_x, pos_y): + xd = abs(self.gridpos[0,0,0] - pos_x) + xp = 0 + 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) + xp = x + yd = abs(self.gridpos[y,x,1] - pos_y) + yp = y + return xd, xp, yd, yp def check_next_move_ghost(self, touch): - if not self.gridpos_x: + if not self.initial_gridpos_x: return - xd, xp = self._find_closest(touch.x, self.gridpos_x) - yd, yp = self._find_closest(touch.y, self.gridpos_y) + xd, xp, yd, yp = self._find_closest(touch.x, touch.y) prev_ghost = self.ghost_stone if max(yd, xd) < self.grid_size / 2 and (xp, yp) not in [m.coords for m in self.katrain.game.stones]: self.ghost_stone = (xp, yp) @@ -91,10 +98,9 @@ class BadukPanWidget(Widget): self.redraw_hover_contents_trigger() def update_box_selection(self, touch, second_point=True): - if not self.gridpos_x: + if not self.initial_gridpos_x: return - _, xp = self._find_closest(touch.x, self.gridpos_x) - _, yp = self._find_closest(touch.y, self.gridpos_y) + _, xp, _, yp = self._find_closest(touch.x, touch.y) if second_point and len(self.region_of_interest) == 4: self.region_of_interest[1] = xp self.region_of_interest[3] = yp @@ -140,10 +146,10 @@ class BadukPanWidget(Widget): near_move = [ (pv, node) for move, pv, node in self.active_pv_moves - if move[0] < len(self.gridpos_x) - and move[1] < len(self.gridpos_y) - and abs(rel_pos[0] - self.gridpos_x[move[0]]) < self.grid_size / 2 - and abs(rel_pos[1] - self.gridpos_y[move[1]]) < self.grid_size / 2 + 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 ] if near_move: self.set_animating_pv(near_move[0][0], near_move[0][1]) @@ -151,12 +157,12 @@ class BadukPanWidget(Widget): self.set_animating_pv(None, None) # any click kills PV from label/move if inside and self.animating_pv is not None: d_sq = (pos[0] - self.animating_pv[3][0]) ** 2 + (pos[1] - self.animating_pv[3][1]) - if d_sq > 2 * self.stone_size ** 2: # move too far from where it was activated + if d_sq > 2 * self.stone_size**2: # move too far from where it was activated self.set_animating_pv(None, None) # any click kills PV from label/move self.last_mouse_pos = pos def on_touch_up(self, touch): - if ("button" in touch.profile and touch.button != "left") or not self.gridpos_x: + if ("button" in touch.profile and touch.button != "left") or not self.initial_gridpos_x: return katrain = self.katrain if self.selecting_region_of_interest: @@ -182,11 +188,8 @@ class BadukPanWidget(Widget): ) else: katrain("play", self.ghost_stone) - self.play_stone_sound() elif not self.ghost_stone: - xd, xp = self._find_closest(touch.x, self.gridpos_x) - yd, yp = self._find_closest(touch.y, self.gridpos_y) - + xd, xp, yd, yp = self._find_closest(touch.x, touch.y) nodes_here = [ node for node in katrain.game.current_node.nodes_from_root if node.move and node.move.coords == (xp, yp) ] @@ -222,7 +225,7 @@ class BadukPanWidget(Widget): stone_size = self.stone_size * scale Color(1, 1, 1, alpha) Rectangle( - pos=(self.gridpos_x[x] - stone_size, self.gridpos_y[y] - 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]), ) @@ -233,7 +236,7 @@ class BadukPanWidget(Widget): ) Color(*evalcol) Rectangle( - pos=(self.gridpos_x[x] - evalsize, self.gridpos_y[y] - 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), ) @@ -241,7 +244,7 @@ class BadukPanWidget(Widget): Color(*innercol) inner_size = stone_size * 0.8 Rectangle( - pos=(self.gridpos_x[x] - inner_size, self.gridpos_y[y] - 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), ) @@ -260,91 +263,239 @@ class BadukPanWidget(Widget): with self.canvas.before: self.canvas.before.clear() - # set up margins and grid lines - if self.draw_coords_enabled: - grid_spaces_margin_x = [1.5, 0.75] # left, right - grid_spaces_margin_y = [1.5, 0.75] # bottom, top - else: # no coordinates means remove the offset - grid_spaces_margin_x = [0.75, 0.75] # left, right - grid_spaces_margin_y = [0.75, 0.75] # bottom, top - x_grid_spaces = board_size_x - 1 + sum(grid_spaces_margin_x) - y_grid_spaces = board_size_y - 1 + sum(grid_spaces_margin_y) - # grid size is rounded to an integer to avoid rounding errors that - # produce tiny gaps between shaded grid squares - self.grid_size = math.floor(min(self.width / x_grid_spaces, self.height / y_grid_spaces) + 0.1) - board_width_with_margins = x_grid_spaces * self.grid_size - board_height_with_margins = y_grid_spaces * self.grid_size - extra_px_margin_x = (self.width - board_width_with_margins) / 2 - extra_px_margin_y = (self.height - board_height_with_margins) / 2 - self.stone_size = self.grid_size * Theme.STONE_SIZE - self.gridpos_x = [ - self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5) - for i in range(board_size_x) - ] - self.gridpos_y = [ - self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * self.grid_size + 0.5) - for i in range(board_size_y) - ] + 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) + 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) + 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.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 katrain.game.insert_mode: - Color(*Theme.INSERT_BOARD_COLOR_TINT) # dreamy + 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) + 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() else: - Color(*Theme.BOARD_COLOR_TINT) # image is a bit too light - Rectangle( - pos=( - self.gridpos_x[0] - self.grid_size * grid_spaces_margin_x[0], - self.gridpos_y[0] - self.grid_size * grid_spaces_margin_y[0], - ), - size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces), - texture=cached_texture(Theme.BOARD_TEXTURE), + current_gridpos_x = self.initial_gridpos_x[:] + current_gridpos_y = self.initial_gridpos_y[:] + + # 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.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_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 + grid_spaces_margin_y = [1.5, 0.75] # bottom, top + else: # no coordinates means remove the offset + grid_spaces_margin_x = [0.75, 0.75] # left, right + 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)) + 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): + 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) + ] + gridpos_y = [ + round(self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * grid_size + 0.5), 4) + for i in range(board_size_y) + ] + + 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) + + + def resize_board(self): + rotated_gridpos_x, rotated_gridpos_y = self.calculate_rotated_gridpos() + + current_gridpos_x = [] + current_gridpos_y = [] + + 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]) + sorted_current_gridpos_x = list(set(current_gridpos_x)) + sorted_current_gridpos_x.sort() + sorted_current_gridpos_y = list(set(current_gridpos_y)) + sorted_current_gridpos_y.sort() + + 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]) + 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): + if katrain.game.insert_mode: + Color(*Theme.INSERT_BOARD_COLOR_TINT) # dreamy + else: + Color(*Theme.BOARD_COLOR_TINT) # image is a bit too light + Rectangle( + pos=( + gridpos_x[0] - self.grid_size * grid_spaces_margin_x[0], + gridpos_y[0] - self.grid_size * grid_spaces_margin_y[0], + ), + size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces), + texture=cached_texture(Theme.BOARD_TEXTURE), + ) + + def draw_lines(self, gridpos_x, gridpos_y): + Color(*Theme.LINE_COLOR) + for i in range(len(gridpos_x)): + Line(points=[(gridpos_x[i], gridpos_y[0]), (gridpos_x[i], gridpos_y[-1])]) + 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 [] ) - Color(*Theme.LINE_COLOR) - for i in range(board_size_x): - Line(points=[(self.gridpos_x[i], self.gridpos_y[0]), (self.gridpos_x[i], self.gridpos_y[-1])]) - for i in range(board_size_y): - Line(points=[(self.gridpos_x[0], self.gridpos_y[i]), (self.gridpos_x[-1], self.gridpos_y[i])]) + 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) - # star points - 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 [] + + def draw_coordinates(self, gridpos_x, gridpos_y): + if self.draw_coords_enabled: + board_size_x, board_size_y = self.katrain.game.board_size + Color(0.25, 0.25, 0.25) + coord_offset = round(self.grid_size * 1.5 / 2, 12) + + if (self.rotation_degree == 90 or self.rotation_degree == 270) and board_size_x != board_size_y: + board_size_y, board_size_x = self.katrain.game.board_size + + for i in range(board_size_x): + draw_text( + pos=(gridpos_x[i], gridpos_y[0] - coord_offset), + text=self.get_x_coordinate_text(i, board_size_x), + font_size=self.grid_size / 1.5, + font_name="Roboto", + ) + for i in range(board_size_y): + draw_text( + pos=(gridpos_x[0] - coord_offset, gridpos_y[i]), + text=self.get_y_coordinate_text(i, board_size_y), + font_size=self.grid_size / 1.5, + font_name="Roboto", ) - 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_x[x], self.gridpos_y[y]), starpt_size, Theme.LINE_COLOR) - # coordinates - if self.draw_coords_enabled: - Color(0.25, 0.25, 0.25) - coord_offset = self.grid_size * 1.5 / 2 - for i in range(board_size_x): - draw_text( - pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset), - text=Move.GTP_COORD[i], - font_size=self.grid_size / 1.5, - font_name="Roboto", - ) - for i in range(board_size_y): - draw_text( - pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), - text=str(i + 1), - font_size=self.grid_size / 1.5, - font_name="Roboto", - ) + def get_x_coordinate_text(self, i, board_size_x): + x_text = Move.GTP_COORD[i] + if (self.rotation_degree == 90): + x_text = str(i + 1) + elif (self.rotation_degree == 180): + x_text = Move.GTP_COORD[board_size_x - i - 1] + 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): + y_text = Move.GTP_COORD[board_size_y - i - 1] + elif (self.rotation_degree == 180): + + y_text = str(board_size_y - i) + 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 len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: + if self.gridpos.shape[0] < board_size_x or self.gridpos.shape[1] < board_size_y: return # race condition show_n_eval = self.trainer_config.get("eval_on_show_last", 3) @@ -374,8 +525,8 @@ class BadukPanWidget(Widget): Color(*Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss) Rectangle( pos=( - self.gridpos_x[x] - self.grid_size / 2, - self.gridpos_y[y] - self.grid_size / 2, + 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), ) @@ -388,7 +539,7 @@ class BadukPanWidget(Widget): *Theme.STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x]) * Theme.OWNERSHIP_MAX_ALPHA ) Rectangle( - pos=(self.gridpos_x[x] - self.grid_size / 2, self.gridpos_y[y] - self.grid_size / 2), + 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), ) @@ -469,7 +620,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_x[x], self.gridpos_y[y]), + (self.gridpos[y,x,0], self.gridpos[y,x,1]), self.stone_size * Theme.HINT_SCALE * 0.98, Theme.APPROX_BOARD_COLOR, ) @@ -477,14 +628,14 @@ class BadukPanWidget(Widget): else: scale = 0.5 draw_circle( - (self.gridpos_x[x], self.gridpos_y[y]), + (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_x[x], self.gridpos_y[y]), + 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, @@ -494,8 +645,8 @@ class BadukPanWidget(Widget): Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA) Line( circle=( - self.gridpos_x[x], - self.gridpos_y[y], + self.gridpos[y,x,0], + self.gridpos[y,x,1], self.stone_size - dp(1.2), ), width=dp(2), @@ -513,13 +664,13 @@ class BadukPanWidget(Widget): self.redraw_hover_contents_trigger() - def draw_roi_box(self, region_of_interest, width=2): + def draw_roi_box(self, region_of_interest, width:float=2): xmin, xmax, ymin, ymax = region_of_interest Color(*Theme.REGION_BORDER_COLOR) Line( rectangle=( - self.gridpos_x[xmin] - self.grid_size / 3, - self.gridpos_y[ymin] - self.grid_size / 3, + 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, ), @@ -544,7 +695,7 @@ class BadukPanWidget(Widget): next_player = current_node.next_player board_size_x, board_size_y = katrain.game.board_size - if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y: + if len(self.gridpos[0]) < board_size_x or len(self.gridpos) < board_size_y: return # race condition with self.canvas.after: @@ -603,14 +754,14 @@ 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_x[move.coords[0]], self.gridpos_y[move.coords[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_x[move.coords[0]] - evalsize, self.gridpos_y[move.coords[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), ) @@ -640,7 +791,7 @@ class BadukPanWidget(Widget): Color(*Theme.HINT_TEXT_COLOR) draw_text( - pos=(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[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, @@ -653,8 +804,8 @@ class BadukPanWidget(Widget): Color(*Theme.TOP_MOVE_BORDER_COLOR) Line( circle=( - self.gridpos_x[move.coords[0]], - self.gridpos_y[move.coords[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), @@ -675,8 +826,8 @@ class BadukPanWidget(Widget): Color(*Theme.NEXT_MOVE_DASH_CONTRAST_COLORS[child_node.player]) Line( circle=( - self.gridpos_x[move.coords[0]], - self.gridpos_y[move.coords[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), @@ -687,8 +838,8 @@ class BadukPanWidget(Widget): for s in range(0, 360, 30): Line( circle=( - self.gridpos_x[move.coords[0]], - self.gridpos_y[move.coords[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, @@ -721,7 +872,7 @@ class BadukPanWidget(Widget): else: text = i18n._("board-pass") Color(*Theme.PASS_CIRCLE_COLOR) - center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)]) + center = (self.initial_gridpos_x[int(board_size_x / 2)], self.initial_gridpos_y[int(board_size_y / 2)]) size = min(self.width, self.height) * 0.227 Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size)) Color(*Theme.PASS_CIRCLE_TEXT_COLOR) @@ -739,7 +890,7 @@ 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_x[hide_node.move.coords[0]], self.gridpos_y[hide_node.move.coords[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): @@ -756,7 +907,7 @@ class BadukPanWidget(Widget): katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2, ] else: - board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[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 @@ -793,6 +944,46 @@ class BadukPanWidget(Widget): return self.animating_pv_index + def rotate_gridpos(self): + board_size_x, board_size_y = self.katrain.game.board_size + if board_size_x != board_size_y: + if self.rotation_degree == 90 or self.rotation_degree == 270: + rotated_gridpos_x, rotated_gridpos_y = self.calculate_rotated_gridpos() + diff = round(abs(rotated_gridpos_x[0] - rotated_gridpos_y[0]), 4) + x0 = rotated_gridpos_x[0] + y0 = rotated_gridpos_y[0] + else: + diff = round(abs(self.initial_gridpos_x[0] - self.initial_gridpos_y[0]), 4) + x0 = self.initial_gridpos_x[0] + y0 = self.initial_gridpos_y[0] + + pos = self.gridpos.tolist() + 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): + gridpos_x = pos[len(self.gridpos) - 1 - yi][:] + else: + gridpos_x = pos[yi][:] + gridpos_x.reverse() + x = pos[yi][xi][1] + y = gridpos_x[xi][0] + if x0 > y0: + x = round(x + diff, 4) + y = round(y - diff, 4) + elif y0 > x0: + x = round(x - diff, 4) + y = round(y + diff, 4) + self.gridpos[yi][xi] = (x, y) + else: + self.gridpos = np.rot90(self.gridpos, -1, (0, 1)) + + self.rotation_degree += 90 + if self.rotation_degree == 360: + self.rotation_degree = 0 + if board_size_x != board_size_y: + self.resize_board() + Clock.schedule_once(self.redraw) + def show_pv_from_comments(self, pv_str): self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent) diff --git a/katrain/gui/theme.py b/katrain/gui/theme.py index a3b95af..203124a 100644 --- a/katrain/gui/theme.py +++ b/katrain/gui/theme.py @@ -68,6 +68,7 @@ class Theme: GRAPH_TEXTURE = "graph_bg.png" # sounds STONE_SOUNDS = [f"stone{i}.wav" for i in [1, 2, 3, 4, 5]] + CAPTURING_SOUND = "capturing.wav" COUNTDOWN_SOUND = "countdownbeep.wav" MINIMUM_TIME_PASSED_SOUND = "boing.wav" MISTAKE_SOUNDS = [] diff --git a/katrain/gui/widgets/filebrowser.py b/katrain/gui/widgets/filebrowser.py index cff23f1..9bcca75 100644 --- a/katrain/gui/widgets/filebrowser.py +++ b/katrain/gui/widgets/filebrowser.py @@ -275,7 +275,7 @@ class LinkTree(TreeView): sig_new = [] for path, name in get_drives(): if platform == "win": - text = u"{}({})".format((name + " ") if name else "", path) + text = "{}({})".format((name + " ") if name else "", path) else: text = name nodes_new.append((text, path)) @@ -351,7 +351,7 @@ class I18NFileBrowser(BoxLayout): .. versionchanged:: 1.1 """ - path = StringProperty(u"/") + path = StringProperty("/") """ :class:`~kivy.properties.StringProperty`, defaults to the current working directory as a unicode string. It specifies the path on the filesystem that diff --git a/katrain/img/Rotate.png b/katrain/img/Rotate.png new file mode 100644 index 0000000..f0144b5 Binary files /dev/null and b/katrain/img/Rotate.png differ diff --git a/katrain/popups.kv b/katrain/popups.kv index 4eb7a52..b8a0bce 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -439,10 +439,10 @@ DescriptionLabel: text: i18n._("save dots") AnchorLayout: - size_hint: 1, 4.25 + size_hint: 1, 5 GridLayout: cols: 2 - rows: 6 + rows: 7 padding: 4 size_hint: 0.95, 1 DescriptionLabel: diff --git a/katrain/sounds/capturing.wav b/katrain/sounds/capturing.wav new file mode 100644 index 0000000..2dfe318 Binary files /dev/null and b/katrain/sounds/capturing.wav differ diff --git a/test_report.py b/test_report.py deleted file mode 100644 index 7d11ff5..0000000 --- a/test_report.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import time - -import pandas as pd -import numpy as np -from unidecode import unidecode - -from katrain.core.ai import game_report -from katrain.core.base_katrain import KaTrainBase -from katrain.core.engine import KataGoEngine -from katrain.core.game import KaTrainSGF, Game -import matplotlib.pyplot as plt - -pd.set_option("display.max_rows", 5000) - -settings = { - "fast_visits": 25, - "max_visits": 500, - "threads": 64, - "model": "C:\\Users\\sande\\.katrain\\kata1-b40c256-s7907049728-d1917596640.bin.gz", -} -# settings["model"] = "C:\\Users\\sande\\.katrain\\g170e-b20c256x2-s5303129600-d1228401921.bin.gz" - - -def dan(rank): - rank = rank.lower() - if rank[-1] in ["d", "p"]: - return int(rank[:-1]) - elif rank == "?": - return np.nan - else: - assert rank[-1] == "k", f"unexpected rank {rank}" - return 1 - int(rank[:-1]) - - -def polyfit(x, y, degree=1): - coeffs = np.polyfit(x, y, degree) - correlation = np.corrcoef(x, y)[0, 1] - results = {"coef": coeffs.tolist(), "r": correlation, "rsq": correlation ** 2} - return results - - -katrain = KaTrainBase(force_package_config=True, debug_level=0) -combined_settings = {**katrain.config("engine"), **settings} -engine = KataGoEngine(katrain, {**katrain.config("engine"), **settings}) -thresholds = katrain.config("trainer/eval_thresholds") - -games = [] -n = 0 -for sgf in os.listdir("sgftest/"): - if sgf.lower().endswith("sgf"): - print(sgf) - with open(os.path.join("sgftest", sgf)) as f: - move_tree = KaTrainSGF.parse_sgf(f.read()) - games.append(Game(katrain, engine, move_tree=move_tree, analyze_fast=False)) - n += 1 - if n >= 30000: # small test=3 - break - -while not engine.is_idle(): - print(f"waiting for engine to finish...{engine.queries_remaining()} queries left") - time.sleep(0.5) -engine.shutdown(finish=None) - -reports = [] -for game in games: - sum_stats, _, _ = game_report(game, thresholds=thresholds) - for bw in "BW": - oppbw = "B" if bw == "W" else "W" - info = { - f"name": game.root.get_property(f"P{bw}", "??"), - "rank": game.root.get_property(f"{bw}R", "9p"), - "opp_rank": game.root.get_property(f"{oppbw}R", "9p"), - **sum_stats[bw], - } - reports.append(info) - -df = pd.DataFrame(reports).sort_values(by="accuracy", ascending=False).reset_index(drop=True) -df.name = [unidecode(n) for n in df.name] -print(df) - -df["numrank"] = [dan(rank) for rank in df["rank"]] - - -def subplot(sp, ynames): - global df - plt.subplot(2, 2, sp) - legend = [] - xfull = np.array(range(df["numrank"].min(), df["numrank"].max() + 1)) - cols = "bgr" - for i, yname in enumerate(ynames): - plt.plot(df["numrank"], df[yname], cols[i] + "x") - for i, yname in enumerate(ynames): - fit = polyfit(df["numrank"], df[yname]) - a, b = fit["coef"] - plt.plot(xfull, xfull * a + b, cols[i] + ":") - legend.append(f"{yname}: r^2 = {fit['rsq']:.3f}") - plt.xlabel("dan rank") - plt.legend(legend) - - -plt.subplots(2, 2) -subplot(1, ["accuracy"]) -subplot(2, ["complexity"]) -subplot(3, ["ai_top_move", "ai_top5_move"]) -subplot(4, ["mean_ptloss", "weighted_ptloss"]) -plt.show()