diff --git a/.github/workflows/osxbuild.yaml b/.github/workflows/osxbuild.yaml index dfb4535..d6c54b1 100644 --- a/.github/workflows/osxbuild.yaml +++ b/.github/workflows/osxbuild.yaml @@ -17,7 +17,7 @@ jobs: python-version: 3.8 - name: Build KataGo run: | - export OPENSSL_ROOT_DIR=$(brew --prefix openssl) + export OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1) export OPENSSL_LIBRARIES="${OPENSSL_ROOT_DIR}/lib" echo $OPENSSL_ROOT_DIR ls $OPENSSL_ROOT_DIR @@ -48,11 +48,13 @@ jobs: python -m pip install . - name: Finalize KaTrain bundle run: | + export KATRAIN_VERSION=`python -c 'from katrain.core.constants import VERSION;print(VERSION)' ` + echo "Setting version to ${KATRAIN_VERSION}" cd ../kivy-sdk-packager/osx pushd KaTrain.app/Contents/Resources/ ln -s ./venv/bin/KaTrain yourapp popd - ./fix-bundle-metadata.sh KaTrain.app -n KaTrain -v "1.9.0" -a "Sander Land" -o "org.katrain.KaTrain" -i "../../katrain/katrain/img/icon.ico" + ./fix-bundle-metadata.sh KaTrain.app -n KaTrain -v "${KATRAIN_VERSION}" -a "Sander Land" -o "org.katrain.KaTrain" -i "../../katrain/katrain/img/icon.ico" ./cleanup-app.sh KaTrain.app -g 1 ./relocate.sh KaTrain.app - name: Create dmg diff --git a/ENGINE.md b/ENGINE.md index 05f3b83..a6bb92a 100644 --- a/ENGINE.md +++ b/ENGINE.md @@ -62,13 +62,16 @@ Run DebugKaTrain.exe, which is released in the .zip file distributable in releas ### libzip compatibility -The most common KataGo issue relates to different libzip versions in the provided executables. -Although the provided executables should work on the latest versions of Ubuntu, various other versions and distros differ in their libzip version. +The most common KataGo issue relates to different libzip versions in the provided executables, + leading to an "Error 127". +Although the provided executables should work on the latest versions of Ubuntu with libzip installed, + various other versions and distros differ in their libzip version. -* First, try `sudo apt-get install libzip-dev` to +* First, try `****sudo apt-get install libzip-dev****` to get this library on recent systems. * Next, try installing all packages mentioned [here](INSTALL.md#LinuxTrouble). * A final alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself. ### Getting more information about errors -Check the terminal output around startup time. \ No newline at end of file +* Check the terminal output around startup time. +* Start KataGo by itself using `katrain/KataGo/katago` when running from source and check output. \ No newline at end of file diff --git a/katrain/KataGo/analysis_config.cfg b/katrain/KataGo/analysis_config.cfg index 3386251..65dd957 100644 --- a/katrain/KataGo/analysis_config.cfg +++ b/katrain/KataGo/analysis_config.cfg @@ -6,7 +6,6 @@ # Logs------------------------------------------------------------------------------------ # Where to output log? -logFile = "~/.katrain/katago_log.txt" # logFile = analysis.log # Use this instead of logDir to just specify a single file directly # logToStderr = true # Echo everything output to log file to stderr as well # logAllRequests = false # Log all input lines received to the analysis engine. diff --git a/katrain/KataGo/katago b/katrain/KataGo/katago index a0899ed..a957540 100755 Binary files a/katrain/KataGo/katago and b/katrain/KataGo/katago differ diff --git a/katrain/KataGo/katago.exe b/katrain/KataGo/katago.exe index 63261d9..17dd786 100644 Binary files a/katrain/KataGo/katago.exe and b/katrain/KataGo/katago.exe differ diff --git a/katrain/__main__.py b/katrain/__main__.py index 398f406..9cff5f7 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -47,6 +47,7 @@ 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 ( @@ -111,7 +112,7 @@ class KaTrainGui(Screen, KaTrainBase): self.timer_settings_popup = None self.contribute_popup = None - self.idle_analysis = False + self.pondering = False self.animate_contributing = False self.message_queue = Queue() @@ -141,7 +142,7 @@ class KaTrainGui(Screen, KaTrainBase): def handle_animations(self, *_args): if self.contributing and self.animate_contributing: self.engine.advance_showing_game() - if (self.contributing and self.animate_contributing) or self.idle_analysis: + if (self.contributing and self.animate_contributing) or self.pondering: self.board_controls.engine_status_pondering += 5 else: self.board_controls.engine_status_pondering = -1 @@ -154,9 +155,9 @@ class KaTrainGui(Screen, KaTrainBase): if self.contributing: self.animate_contributing = not self.animate_contributing else: - if self.idle_analysis: + if self.pondering: self.controls.set_status("", STATUS_INFO) - self.idle_analysis = not self.idle_analysis + self.pondering = not self.pondering self.update_state() def start(self): @@ -257,8 +258,11 @@ class KaTrainGui(Screen, KaTrainBase): ): # 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) - if self.engine.is_idle() and self.idle_analysis: - self("analyze-extra", "extra", continuous=True) + if self.engine: + if self.pondering: + self.game.analyze_extra("ponder") + else: + self.engine.stop_pondering() Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger? def update_player(self, bw, **kwargs): @@ -322,7 +326,7 @@ class KaTrainGui(Screen, KaTrainBase): self.message_queue.put([self.game.game_id, message, args, kwargs]) def _do_new_game(self, move_tree=None, analyze_fast=False, sgf_filename=None): - self.idle_analysis = False + self.pondering = False mode = self.play_analyze_mode 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 @@ -351,7 +355,7 @@ class KaTrainGui(Screen, KaTrainBase): self.contributing = self.animate_contributing = True # special mode if self.play_analyze_mode == MODE_PLAY: # switch to analysis view self.play_mode.switch_ui_mode() - self.idle_analysis = False + self.pondering = False self.board_gui.animating_pv = None for bw, player_info in self.players_info.items(): self.update_player(bw, player_type=PLAYER_AI, player_subtype=AI_DEFAULT) @@ -395,10 +399,6 @@ class KaTrainGui(Screen, KaTrainBase): self.board_gui.animating_pv = None getattr(self.game, fn)(9999, stop_on_mistake=self.config("trainer/eval_thresholds")[-4]) - def _do_cycle_children(self, *args): - self.board_gui.animating_pv = None - self.game.cycle_children(*args) - def _do_switch_branch(self, *args): self.board_gui.animating_pv = None self.controls.move_tree.switch_branch(*args) @@ -593,22 +593,21 @@ class KaTrainGui(Screen, KaTrainBase): self.log("Imported game from clipboard.", OUTPUT_INFO) def on_touch_up(self, touch): - if ( - self.board_gui.collide_point(*touch.pos) - or self.board_controls.collide_point(*touch.pos) - or self.controls.move_tree.collide_point(*touch.pos) - ): - if touch.is_mouse_scrolling: - if self.board_gui.animating_pv is None: - if touch.button == "scrollup": - self("redo") - elif touch.button == "scrolldown": - self("undo") - else: - if touch.button == "scrollup": - self.board_gui.adjust_animate_pv_index(1) - elif touch.button == "scrolldown": - self.board_gui.adjust_animate_pv_index(-1) + if touch.is_mouse_scrolling: + touching_board = self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos) + touching_control_nonscroll = self.controls.collide_point( + *touch.pos + ) and not self.controls.notes_panel.collide_point(*touch.pos) + if self.board_gui.animating_pv is not None and touching_board: + if touch.button == "scrollup": + self.board_gui.adjust_animate_pv_index(1) + elif touch.button == "scrolldown": + self.board_gui.adjust_animate_pv_index(-1) + elif touching_board or touching_control_nonscroll: # scroll through moves + if touch.button == "scrollup": + self("redo") + elif touch.button == "scrolldown": + self("undo") return super().on_touch_up(touch) @property @@ -653,7 +652,7 @@ class KaTrainGui(Screen, KaTrainBase): def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers): self.last_key_down = keycode - ctrl_pressed = "ctrl" in modifiers + ctrl_pressed = "ctrl" in modifiers or ("meta" in modifiers and kivy_platform == "macosx") shift_pressed = "shift" in modifiers if self.controls.note.focus: return # when making notes, don't allow keyboard shortcuts diff --git a/katrain/config.json b/katrain/config.json index eb19ab6..502d00e 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.9.0", + "version": "1.10.0", "load_fast_analysis": false, "load_sgf_rewind": true }, @@ -93,6 +93,7 @@ }, "ai": { "ai:default": {}, + "ai:antimirror": {}, "ai:handicap": { "automatic": true, "pda": 0 diff --git a/katrain/core/ai.py b/katrain/core/ai.py index 7c274e4..0bd8876 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -10,6 +10,7 @@ from katrain.core.constants import ( AI_INFLUENCE, AI_INFLUENCE_ELO_GRID, AI_JIGO, + AI_ANTIMIRROR, AI_LOCAL, AI_LOCAL_ELO_GRID, AI_PICK, @@ -264,6 +265,11 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, if not handicap_analysis: game.katrain.log("Error getting handicap-based move", OUTPUT_ERROR) ai_mode = AI_DEFAULT + elif ai_mode == AI_ANTIMIRROR: + antimirror_analysis = request_ai_analysis(game, cn, {"antiMirror": True}) + if not antimirror_analysis: + game.katrain.log("Error getting antimirror move", OUTPUT_ERROR) + ai_mode = AI_DEFAULT while not cn.analysis_complete: time.sleep(0.01) @@ -388,6 +394,8 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, candidate_ai_moves = cn.candidate_moves if ai_mode == AI_HANDICAP: candidate_ai_moves = handicap_analysis["moveInfos"] + elif ai_mode == AI_ANTIMIRROR: + candidate_ai_moves = antimirror_analysis["moveInfos"] top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) if top_cand.is_pass and ai_mode not in [ @@ -492,12 +500,14 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, else: raise (Exception("No moves found - are you using an older KataGo with no per-move ownership info?")) else: - if ai_mode not in [AI_DEFAULT, AI_HANDICAP]: + if ai_mode not in [AI_DEFAULT, AI_HANDICAP, AI_ANTIMIRROR]: game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO) ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback." aimove = top_cand if ai_mode == AI_HANDICAP: ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}" + if ai_mode == AI_ANTIMIRROR: + ai_thoughts += f"AntiMirror strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. antiMirror based score {cn.format_score(antimirror_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(antimirror_analysis['rootInfo']['winrate'])}" else: ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move" game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG) diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py index dc9c88a..f1a8f33 100644 --- a/katrain/core/base_katrain.py +++ b/katrain/core/base_katrain.py @@ -54,6 +54,13 @@ class Player: return f"{self.player_type} ({self.player_subtype})" +def parse_version(s): + parts = [int(p) for p in s.split(".")] + while len(parts) < 3: + parts.append(0) + return parts + + class KaTrainBase: USER_CONFIG_FILE = os.path.expanduser(os.path.join(DATA_FOLDER, "config.json")) PACKAGE_CONFIG_FILE = "katrain/config.json" @@ -101,11 +108,13 @@ class KaTrainBase: self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO) else: # user file exists try: - version = JsonStore(user_config_file).get("general")["version"] + version_str = JsonStore(user_config_file).get("general")["version"] + version = parse_version(version_str) except Exception: # noqa E722 broken file etc - version = "0.0.0" - if version < CONFIG_MIN_VERSION: - backup = user_config_file + f".{version}.backup" + version_str = "0.0.0" + version = [0, 0, 0] + if version < parse_version(CONFIG_MIN_VERSION): + backup = f"{user_config_file}.{version_str}.backup" shutil.copyfile(user_config_file, backup) shutil.copyfile(package_config_file, user_config_file) self.log( diff --git a/katrain/core/constants.py b/katrain/core/constants.py index cd52c01..d1ba53d 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,7 +1,7 @@ PROGRAM_NAME = "KaTrain" -VERSION = "1.9.3" +VERSION = "1.10.0" HOMEPAGE = "https://github.com/sanderland/katrain" -CONFIG_MIN_VERSION = "1.9.0" # keep config files from this version +CONFIG_MIN_VERSION = "1.10.0" # keep config files from this version ANALYSIS_FORMAT_VERSION = "1.0" DATA_FOLDER = "~/.katrain" @@ -42,6 +42,7 @@ AI_HANDICAP = "ai:handicap" AI_SCORELOSS = "ai:scoreloss" AI_WEIGHTED = "ai:p:weighted" AI_JIGO = "ai:jigo" +AI_ANTIMIRROR = "ai:antimirror" AI_POLICY = "ai:policy" AI_PICK = "ai:p:pick" AI_LOCAL = "ai:p:local" @@ -54,7 +55,7 @@ AI_SETTLE_STONES = "ai:settle" AI_CONFIG_DEFAULT = AI_RANK -AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO] # AI_SETTLE_STONES +AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO, AI_ANTIMIRROR] AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK] AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY @@ -63,20 +64,21 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [ AI_RANK, AI_HANDICAP, AI_SIMPLE_OWNERSHIP, - # AI_SETTLE_STONES, AI_SCORELOSS, AI_POLICY, AI_WEIGHTED, + AI_JIGO, + AI_ANTIMIRROR, AI_PICK, AI_LOCAL, AI_TENUKI, AI_TERRITORY, AI_INFLUENCE, - AI_JIGO, ] AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some? AI_DEFAULT: 9, + AI_ANTIMIRROR: 9, AI_POLICY: 5, AI_JIGO: float("nan"), AI_SCORELOSS: -4, @@ -260,7 +262,8 @@ TOP_MOVE_OPTIONS = [ # TOP_MOVE_UTILITY, # TOP_MOVE_UTILITYLCB ] -REPORT_DT = 0.25 +REPORT_DT = 1 +PONDERING_REPORT_DT = 0.25 SGF_INTERNAL_COMMENTS_MARKER = "\u3164\u200b" SGF_SEPARATOR_MARKER = "\u3164\u3164" diff --git a/katrain/core/contribute_engine.py b/katrain/core/contribute_engine.py index cb68510..c6e20fc 100644 --- a/katrain/core/contribute_engine.py +++ b/katrain/core/contribute_engine.py @@ -243,7 +243,9 @@ class KataGoContributeEngine(BaseEngine): game_properties["RU"] = json.dumps(analysis["rules"]) game_properties["PB"] = analysis["blackPlayer"] game_properties["PW"] = analysis["whitePlayer"] - current_game = BaseGame(self.katrain, game_properties=game_properties) + current_game = BaseGame( + self.katrain, game_properties=game_properties, bypass_config=True + ) self.active_games[game_id] = current_game last_node = current_game.sync_branch( [Move.from_gtp(coord, pl) for pl, coord in analysis["moves"]] diff --git a/katrain/core/engine.py b/katrain/core/engine.py index ec7e3ab..3011268 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -19,6 +19,7 @@ from katrain.core.constants import ( OUTPUT_KATAGO_STDERR, DATA_FOLDER, KATAGO_EXCEPTION, + PONDERING_REPORT_DT, ) from katrain.core.game_node import GameNode from katrain.core.lang import i18n @@ -93,11 +94,14 @@ class BaseEngine: # some common elements between analysis and contribute engine class KataGoEngine(BaseEngine): """Starts and communicates with the KataGO analysis engine""" + PONDER_KEY = "_kt_continuous" + def __init__(self, katrain, config): super().__init__(katrain, config) self.allow_recovery = self.config.get("allow_recovery", True) # if false, don't give popups self.queries = {} # outstanding query id -> start time and callback + self.ponder_query = None self.query_counter = 0 self.katago_process = None self.base_priority = 0 @@ -166,6 +170,7 @@ class KataGoEngine(BaseEngine): with self.thread_lock: self.write_queue = queue.Queue() self.terminate_queries(only_for_node=None, lock=False) + self.ponder_query = None self.queries = {} def terminate_queries(self, only_for_node=None, lock=True): @@ -176,10 +181,19 @@ class KataGoEngine(BaseEngine): if only_for_node is None or only_for_node is node: self.terminate_query(query_id) - def terminate_query(self, query_id): + def stop_pondering(self): + pq = self.ponder_query + if pq: + self.terminate_query(pq["id"], ignore_further_results=False) + self.ponder_query = None + + def terminate_query(self, query_id, ignore_further_results=True): + self.katrain.log(f"Terminating query {query_id}", OUTPUT_DEBUG) + if query_id is not None: self.send_query({"action": "terminate", "terminateId": query_id}, None, None) - self.queries.pop(query_id, None) + if ignore_further_results: + self.queries.pop(query_id, None) def restart(self): self.queries = {} @@ -269,9 +283,10 @@ class KataGoEngine(BaseEngine): continue query_id = analysis["id"] if query_id not in self.queries: - self.katrain.log( - f"Query result {query_id} discarded -- recent new game or node reset?", OUTPUT_DEBUG - ) + if analysis.get("action") != "terminate": + self.katrain.log( + f"Query result {query_id} discarded -- recent new game or node reset?", OUTPUT_DEBUG + ) continue callback, error_callback, start_time, next_move, _ = self.queries[query_id] if "error" in analysis: @@ -314,12 +329,38 @@ class KataGoEngine(BaseEngine): except queue.Empty: continue with self.thread_lock: + self.katrain.log(f"GOT QUERY: {query}", OUTPUT_DEBUG) if "id" not in query: self.query_counter += 1 query["id"] = f"QUERY:{str(self.query_counter)}" - if query.get("action") != "terminate": + + ponder = query.pop(self.PONDER_KEY, False) + if ponder: # handle pondering in here to be in lock and such + pq = self.ponder_query or {} + # basically we handle pondering by just asking for these queries a lot and ignoring duplicates + # when a different ponder query comes in, e.g. due to selecting a roi or different node, switch + differences = { + k: (pq.get(k), query.get(k)) + for k in (query.keys() | pq.keys()) - {"id", "maxVisits", "reportDuringSearchEvery"} + 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["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" + if not terminate: self.queries[query["id"]] = (callback, error_callback, time.time(), next_move, node) - self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG) + tag = "ponder " if ponder else ("terminate " if terminate else "") + self.katrain.log(f"Sending {tag}query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG) try: self.katago_process.stdin.write((json.dumps(query) + "\n").encode()) self.katago_process.stdin.flush() @@ -341,6 +382,7 @@ class KataGoEngine(BaseEngine): find_alternatives: bool = False, region_of_interest: Optional[List] = None, priority: int = 0, + ponder=False, # infinite visits, cancellable ownership: Optional[bool] = None, next_move: Optional[GameNode] = None, extra_settings: Optional[Dict] = None, @@ -358,6 +400,7 @@ class KataGoEngine(BaseEngine): moves.append(next_move) if ownership is None: ownership = self.config["_enable_ownership"] and not next_move + if visits is None: visits = self.config["max_visits"] if analyze_fast and self.config.get("fast_visits"): @@ -411,6 +454,7 @@ class KataGoEngine(BaseEngine): "initialPlayer": analysis_node.initial_player, "moves": [[m.player, m.gtp()] for m in moves], "overrideSettings": {**settings, **(extra_settings or {})}, + self.PONDER_KEY: ponder, } if report_every is not None: query["reportDuringSearchEvery"] = report_every diff --git a/katrain/core/game.py b/katrain/core/game.py index 91dfcb5..82349b7 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -53,6 +53,7 @@ class BaseGame: move_tree: GameNode = None, game_properties: Optional[Dict] = None, sgf_filename=None, + bypass_config=False, # TODO: refactor? ): self.katrain = katrain self._lock = threading.Lock() @@ -83,18 +84,24 @@ class BaseGame: ): # not really according to sgf, and not sure if still needed, last clause for fox self.root.place_handicap_stones(handicap) else: - board_size = katrain.config("game/size") - rules = katrain.config("game/rules") - self.komi = katrain.config("game/komi") + default_properties = {**Game.DEFAULT_PROPERTIES, "DT": self.game_id} + if not bypass_config: + default_properties.update( + { + "SZ": katrain.config("game/size"), + "KM": katrain.config("game/komi"), + "RU": katrain.config("game/rules"), + } + ) self.root = GameNode( properties={ - **Game.DEFAULT_PROPERTIES, - **{"SZ": board_size, "KM": self.komi, "DT": self.game_id, "RU": rules}, + **default_properties, **(game_properties or {}), } ) + self.komi = self.root.komi handicap = katrain.config("game/handicap") - if handicap: + if not bypass_config and handicap: self.root.place_handicap_stones(handicap) if not self.root.get_property("RU"): # if rules missing in sgf, inherit current @@ -558,22 +565,27 @@ class Game(BaseGame): cn = self.current_node if mode == "stop": + self.katrain.pondering = False for e in set(self.engines.values()): + e.stop_pondering() e.terminate_queries() - self.katrain.idle_analysis = False return engine = self.engines[cn.next_player] Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0) + if mode == "ponder": + cn.analyze( + engine, + ponder=True, + priority=PRIORITY_EXTRA_ANALYSIS, + region_of_interest=self.region_of_interest, + time_limit=False, + ) + return + if mode == "extra": - if kwargs.get("continuous", False): - visits = min( - 1_000_000_000, max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25)) - ) - else: - visits = cn.analysis_visits_requested + engine.config["max_visits"] - self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) + visits = cn.analysis_visits_requested + engine.config["max_visits"] self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS) cn.analyze( engine, @@ -583,6 +595,7 @@ class Game(BaseGame): time_limit=False, ) return + if mode == "game": nodes = self.root.nodes_in_tree only_mistakes = kwargs.get("mistakes_only", False) diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 79e7bfb..b83f9b6 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -187,6 +187,7 @@ class GameNode(SGFNode): engine, priority=PRIORITY_DEFAULT, visits=None, + ponder=False, time_limit=True, refine_move=None, analyze_fast=False, @@ -201,6 +202,7 @@ class GameNode(SGFNode): ), priority=priority, visits=visits, + ponder=ponder, analyze_fast=analyze_fast, time_limit=time_limit, next_move=refine_move, @@ -278,6 +280,10 @@ class GameNode(SGFNode): def analysis_complete(self): return self.analysis["completed"] and self.analysis["root"] is not None + @property + def root_visits(self): + return ((self.analysis or {}).get("root") or {}).get("visits", 0) + @property def score(self) -> Optional[float]: if self.analysis_exists: diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index 03abdd3..51cc7d7 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -45,7 +45,7 @@ class Move: self.coords = coords def __repr__(self): - return f"Move({self.player}{self.gtp()})" + return f"Move({self.player or ''}{self.gtp()})" def __eq__(self, other): return self.coords == other.coords and self.player == other.player @@ -247,22 +247,38 @@ class SGFNode: ] return self.moves_cache + def _expanded_placements(self, player): + sgf_pl = player if player is not None else "E" # AE + placements = self.get_list_property("A" + sgf_pl, []) + if not placements: + return [] + to_be_expanded = [p for p in placements if ":" in p] + board_size = self.board_size + if to_be_expanded: + coords = { + Move.from_sgf(sgf_coord, player=player, board_size=board_size) + for sgf_coord in placements + if ":" not in sgf_coord + } + for p in to_be_expanded: + from_coord, to_coord = [Move.from_sgf(c, board_size=board_size) for c in p.split(":")[:2]] + for x in range(from_coord.coords[0], to_coord.coords[0] + 1): + for y in range(to_coord.coords[1], from_coord.coords[1] + 1): # sgf upside dn + if 0 <= x < board_size[0] and 0 <= y < board_size[1]: + coords.add(Move((x, y), player=player)) + return list(coords) + else: + return [Move.from_sgf(sgf_coord, player=player, board_size=board_size) for sgf_coord in placements] + @property def placements(self) -> List[Move]: """Returns all placements (AB/AW) in the node.""" - return [ - Move.from_sgf(sgf_coords, player=pl, board_size=self.board_size) - for pl in Move.PLAYERS - for sgf_coords in self.get_list_property("A" + pl, []) - ] + return [coord for pl in Move.PLAYERS for coord in self._expanded_placements(pl)] @property def clear_placements(self) -> List[Move]: """Returns all AE clear square commends in the node.""" - return [ - Move.from_sgf(sgf_coords, player=None, board_size=self.board_size) - for sgf_coords in self.get_list_property("AE", []) - ] + return self._expanded_placements(None) @property def move_with_placements(self) -> List[Move]: diff --git a/katrain/gui.kv b/katrain/gui.kv index b111a48..9c04f7e 100644 --- a/katrain/gui.kv +++ b/katrain/gui.kv @@ -550,6 +550,7 @@ timer:timer graph: graph stats: stats + notes_panel: notes_panel info: info note: note status: status diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py index 25bba1e..7d47cf0 100644 --- a/katrain/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -103,14 +103,24 @@ class BadukPanWidget(Widget): self.redraw_hover_contents_trigger() def on_touch_down(self, touch): - if touch.button not in ["scrollup", "scrolldown"]: - self.set_animating_pv(None, None) # any click kills PV from label/move - if "button" in touch.profile and touch.button != "left": - return - if self.selecting_region_of_interest: - self.update_box_selection(touch, second_point=False) - else: - self.check_next_move_ghost(touch) + animating_pv = self.animating_pv + if "button" in touch.profile: + if touch.button == "left": + if self.selecting_region_of_interest: + self.update_box_selection(touch, second_point=False) + else: + self.check_next_move_ghost(touch) + elif touch.button == "middle" and animating_pv: + pv, node, _, _ = animating_pv + upto = self.animating_pv_index or 1e9 + for i, gtpmove in enumerate(pv): + if i <= upto: # up to move when scrolling, or all + node = node.play(Move.from_gtp(gtpmove, node.next_player)) + node.analyze(self.katrain.engine, analyze_fast=True) + self.katrain.controls.move_tree.redraw_tree_trigger() + + if ("button" not in touch.profile) or (touch.button not in ["scrollup", "scrolldown", "middle"]): + self.set_animating_pv(None, None) # any click/touch kills PV from label/move def on_touch_move(self, touch): if "button" in touch.profile and touch.button != "left": @@ -762,7 +772,7 @@ class BadukPanWidget(Widget): if self.animating_pv: pv, node, start_time, _ = self.animating_pv delay = self.katrain.config("general/anim_pv_time", 0.5) - return (time.time() - start_time) / delay + return (time.time() - start_time) / max(delay, 0.1) else: return 0 diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 142d16f..6a3aa65 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -479,20 +479,22 @@ class BaseConfigPopup(QuickConfigGui): KATAGOS = { "win": { - "OpenCL v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-opencl-windows-x64.zip", - "Eigen AVX2 (Modern CPUs) v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-eigenavx2-windows-x64.zip", - "Eigen (CPU, Non-optimized) v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-eigen-windows-x64.zip", - "OpenCL v1.9 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-opencl-windows-x64+bs29.zip", + "OpenCL v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-opencl-windows-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-eigenavx2-windows-x64.zip", + "Eigen (CPU, Non-optimized) v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-eigen-windows-x64.zip", + "OpenCL v1.10.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-opencl-windows-x64+bs29.zip", }, "linux": { - "OpenCL v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-opencl-linux-x64.zip", - "Eigen AVX2 (Modern CPUs) v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-eigenavx2-linux-x64.zip", - "Eigen (CPU, Non-optimized) v1.9.1": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-eigen-linux-x64.zip", - "OpenCL v1.9 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-opencl-linux-x64+bs29.zip", + "OpenCL v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-opencl-linux-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-eigenavx2-linux-x64.zip", + "Eigen (CPU, Non-optimized) v1.10.0": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-eigen-linux-x64.zip", + "OpenCL v1.10.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-opencl-linux-x64+bs29.zip", }, "just-descriptions": { - "CUDA v1.9.1 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-cuda11.2-windows-x64.zip", - "CUDA v1.9.1 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.9.1/katago-v1.9.1-cuda11.1-linux-x64.zip", + "CUDA v1.10.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-cuda11.2-windows-x64.zip", + "CUDA v1.10.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-cuda11.1-linux-x64.zip", + "Cuda/TensorRT v1.10.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-trt8.2-cuda11.2-windows-x64.zip", + "Cuda/TensorRT v1.10.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.10.0/katago-v1.10.0-trt8.2-cuda11.1-linux-x64.zip", }, } diff --git a/katrain/gui/sound.py b/katrain/gui/sound.py index 94ad626..a7c3e2c 100644 --- a/katrain/gui/sound.py +++ b/katrain/gui/sound.py @@ -6,7 +6,7 @@ from kivy.utils import platform cached_sounds = {} # prefer ffpyplayer on linux, then others, avoid gst and avoid or ffpyplayer on windows -ranking = [("ffpy", 98 if platform in ["win",'macosx'] else -2), ("sdl", -1), ("gst", 99), ("", 0)] +ranking = [("ffpy", 98 if platform in ["win", "macosx"] else -2), ("sdl", -1), ("gst", 99), ("", 0)] try: SoundLoader._classes.sort(key=lambda cls: [v for k, v in ranking if k in cls.__name__.lower()][0]) except Exception as e: diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index 087d611..fe5b777 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 86ed1ea..b268760 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -915,3 +915,7 @@ msgstr "AI最佳选点重合度 %" #. game filter in performance report msgid "Entire Game" msgstr "全局" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo index d392375..efc2fe1 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 37d26ea..8dd89b5 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -984,3 +984,7 @@ msgstr "Bester KI Zug Übereinstimmung %" #. - game filter in performance report msgid "Entire Game" msgstr "Gesamtes Spiel" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index b4414de..b8e484f 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 fd6733e..bc5fc50 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -772,6 +772,9 @@ msgstr "" msgid "ai:jigo" msgstr "KataJigo" +msgid "ai:antimirror" +msgstr "KataAntiMirror" + msgid "aihelp:jigo" msgstr "" "Will try to win by a set amount of points (default 0.5), without further " diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo index 768cd29..d6096a6 100644 Binary files a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po index 502481a..8dc3812 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -999,3 +999,7 @@ msgstr "AI Best Move Match %" #. TODO - game filter in performance report msgid "Entire Game" msgstr "Entire Game" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index a5b0f0b..ed0bb0e 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 3e322ee..74d0bf5 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -960,3 +960,7 @@ msgstr "Ce que jouerait KataGo" msgid "Entire Game" msgstr "Partie entière" + +#. TODO +msgid "ai:antimirror" +msgstr "AntiMirrorGo" diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index 9f8df3a..8508b7a 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 b7c2950..224a609 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -959,3 +959,7 @@ msgstr "AIの最善手との一致率" #. game filter in performance report msgid "Entire Game" msgstr "全局" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index 8e19ee1..a81fc83 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 f1e291e..6734514 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -898,3 +898,7 @@ msgstr "인공지능 최선수 일치율(%)" msgid "Entire Game" msgstr "모두" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index 02a8df5..25d0f48 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 680684e..b8b1922 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -924,3 +924,7 @@ msgstr "" msgid "Entire Game" msgstr "Полная игра" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo index 4b23bba..801b4ef 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 feb3027..4c4464f 100644 --- a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po @@ -941,3 +941,7 @@ msgstr "AI最佳選點重合度 %" #. game filter in performance report msgid "Entire Game" msgstr "全局" + +#. TODO +msgid "ai:antimirror" +msgstr "KataAntiMirror" diff --git a/tests/test_parser.py b/tests/test_parser.py index 2c7333a..13a92a0 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -187,3 +187,11 @@ def test_next_player(): input_sgf = "(;GM[1]FF[4]AB[aa];W[dd])" # branch exists assert "W" == SGF.parse_sgf(input_sgf).next_player assert "W" == SGF.parse_sgf(input_sgf).initial_player + + +def test_placements(): + input_sgf = "(;GM[1]FF[4]SZ[19]DT[2020-04-12]AB[dd][aa:ee]AW[ff:zz]AE[aa][bb][cc:dd])" + root = SGF.parse_sgf(input_sgf) + print(root.properties) + assert 6 == len(root.clear_placements) + assert 25 + 14 * 14 == len(root.placements)