From 485b55c42df4dd1c77abf21eefc23c9a17d6a512 Mon Sep 17 00:00:00 2001 From: Sander Land <48946947+sanderland@users.noreply.github.com> Date: Thu, 23 Jun 2022 19:14:56 +0200 Subject: [PATCH] 1.11.1 (#520) * certificate fix * Added a sound effect for the capturing of stones (#523) * fix cache missing * Fixes for some edge cases for the capturing sound effect (#526) * Added a sound effect for the capturing of stones * Fixed edge cases for the capturing sound effect * minor bug fix * Refactoring sounds for playing a move * simplify capture sound, upgrade black * fix handicap * fix stone sound bug, note on sleep deadlock * board rotation (#532) * implementation of the rotation of the board view and some refactoring (#531) * minor cleanup Co-authored-by: Patrick <63347771+Funkenschlag1@users.noreply.github.com> Co-authored-by: Sander Land Co-authored-by: Sander Land Co-authored-by: Patrick <63347771+Funkenschlag1@users.noreply.github.com> Co-authored-by: Sander Land --- CONTRIBUTIONS.md | 1 + LICENSE | 3 +- katrain/__main__.py | 34 ++- katrain/core/ai.py | 2 +- katrain/core/contribute_engine.py | 1 - katrain/core/game.py | 1 + katrain/core/game_node.py | 2 +- katrain/core/sgf_parser.py | 2 + katrain/gui.kv | 3 + katrain/gui/badukpan.py | 437 +++++++++++++++++++++-------- katrain/gui/theme.py | 1 + katrain/gui/widgets/filebrowser.py | 4 +- katrain/img/Rotate.png | Bin 0 -> 5632 bytes katrain/popups.kv | 4 +- katrain/sounds/capturing.wav | Bin 0 -> 93218 bytes test_report.py | 107 ------- 16 files changed, 356 insertions(+), 246 deletions(-) create mode 100644 katrain/img/Rotate.png create mode 100644 katrain/sounds/capturing.wav delete mode 100644 test_report.py 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 0000000000000000000000000000000000000000..f0144b58fa5d41d1883bc6bd33dc0c0f1c62f700 GIT binary patch literal 5632 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 L-~53YE%rYE1}Sb1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2dfe3180ade3dc75647431e2bb94d21b36902db6 GIT binary patch literal 93218 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 literal 0 HcmV?d00001 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()