Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba1cccea17 | ||
|
|
9b5881412c | ||
|
|
e1bd8a1cff | ||
|
|
39893a852c | ||
|
|
8e25103a0f | ||
|
|
a764e3d40d |
No files matched your search
@@ -0,0 +1,62 @@
|
|||||||
|
# KaTrain Qt Port
|
||||||
|
|
||||||
|
This repository is a Qt 6 port of KaTrain, a KataGo frontend for Go/Baduk.
|
||||||
|
|
||||||
|
## Repository Context
|
||||||
|
|
||||||
|
- The port starts from upstream KaTrain 1.19.0.
|
||||||
|
- Development currently happens on `qt-0.19.0`, which presently matches upstream `v0.19.0`.
|
||||||
|
- `../katrain-reference` is a read-only checkout of upstream KaTrain 1.19.0. Use it to compare behavior and implementation, but never modify it.
|
||||||
|
- Make all changes in this repository on `qt-0.19.0`.
|
||||||
|
|
||||||
|
## Porting Direction
|
||||||
|
|
||||||
|
- Upstream KaTrain is a Python application built with Kivy.
|
||||||
|
- Kivy does not work well on Wayland, so this project is a Qt 6 port.
|
||||||
|
- The goal is a PySide6 frontend for KaTrain.
|
||||||
|
- `katrain/core` is not fully UI-independent. The first major goal is to separate the actual core functionality from UI-specific concerns before or alongside the Qt UI port.
|
||||||
|
- Preserve upstream behavior where practical, while keeping UI abstractions out of the reusable core.
|
||||||
|
- Do not rewrite working core logic unless necessary. Keep divergence small so upstream versions can continue to be tracked.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Engine or model downloading.
|
||||||
|
- KataGo contribution.
|
||||||
|
- Mobile support.
|
||||||
|
- Kivy compatibility in the new frontend.
|
||||||
|
- Sabaki theme package installation.
|
||||||
|
|
||||||
|
### Required Functionality
|
||||||
|
|
||||||
|
- SGF loading and saving.
|
||||||
|
- KataGo analysis.
|
||||||
|
- Analysis graph, move tree, candidate moves, ownership display, and principal variation display.
|
||||||
|
- Mistake coloring.
|
||||||
|
- Play against AI.
|
||||||
|
- Teaching and training modes.
|
||||||
|
- Human-like AI settings.
|
||||||
|
- Engine restart and settings.
|
||||||
|
- Localization, where practical.
|
||||||
|
|
||||||
|
## Board Themes
|
||||||
|
|
||||||
|
- Support arbitrary board texture, black stone, and white stone images.
|
||||||
|
- Allow board, black stone, and white stone components to be mixed independently.
|
||||||
|
- Support multiple stone image variants internally.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Development environment: Gentoo Linux with OpenRC.
|
||||||
|
- Shell: Zsh.
|
||||||
|
- Git pushes to `origin` are available.
|
||||||
|
- The local KataGo OpenCL binary requires `RUSTICL_ENABLE=radeonsi` to use the Radeon Pro WX 4100.
|
||||||
|
- Run `RUSTICL_ENABLE=radeonsi katrain/KataGo/katago tuner -config katrain/KataGo/analysis_config.cfg -model katrain/models/b10c384h6nbttflrs.bin.gz` before KataGo integration tests. Re-run it whenever the KataGo binary, model, or analysis config changes.
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- Push development work only to `origin/qt-0.19.0`.
|
||||||
|
- Do not merge directly into `main`.
|
||||||
|
- Merge to `main` only through a pull request after reaching a meaningful milestone.
|
||||||
|
- Use commit-message prefixes such as `FEAT:`, `FIX:`, `MINOR:`, `CORE:`, `UI:`, `DOC:` or `STYLE:`.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Qt Port TODO
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
- [x] Remove Kivy and `katrain.gui` imports from `katrain/core`.
|
||||||
|
- [x] Replace direct core-to-widget calls with framework-neutral events.
|
||||||
|
- [x] Move Kivy language observation, widget fonts, PV markup, and UI updates into Kivy adapters.
|
||||||
|
- [x] Preserve behavior with core-boundary, parser, board, AI, and remote-engine tests.
|
||||||
|
|
||||||
|
## 1. Finish Core Host Separation
|
||||||
|
|
||||||
|
- [ ] Define `Protocol` types for the remaining host contract: configuration, logging, player/session state, and event delivery.
|
||||||
|
- [ ] Change `BaseGame`, engines, contribution code, and AI helpers to accept those narrow dependencies instead of an untyped `katrain` object.
|
||||||
|
- [ ] Move game/session ownership (`game`, `pondering`, active-game replacement) into a framework-neutral application controller.
|
||||||
|
- [ ] Replace engine-thread callbacks that mutate game state directly with controller-dispatched work.
|
||||||
|
- [ ] Add unit tests for event ordering, thread dispatch boundaries, configuration migration/backup, and translator fallback.
|
||||||
|
|
||||||
|
Done when `katrain/core` has no dependency on a concrete application object and can be embedded by a non-Kivy command-line test host.
|
||||||
|
|
||||||
|
## 2. Establish the Qt Application Shell
|
||||||
|
|
||||||
|
- [ ] Add PySide6 as the frontend dependency and create a Qt entry point without importing Kivy.
|
||||||
|
- [ ] Implement Qt versions of the core event sink and language adapter using queued Qt signals.
|
||||||
|
- [ ] Create application/session, configuration, engine-lifecycle, and error/recovery controllers.
|
||||||
|
- [ ] Add a minimal window with startup/shutdown, settings loading, KataGo restart, and localized status display.
|
||||||
|
- [ ] Confirm all core-originated UI events execute on the Qt main thread.
|
||||||
|
|
||||||
|
Done when Qt can open a window, start/stop KataGo, show engine failures, and load persisted settings without loading Kivy.
|
||||||
|
|
||||||
|
## 3. Build the Game Workspace
|
||||||
|
|
||||||
|
- [ ] Implement board rendering with independent board, black-stone, and white-stone image selection plus stone variants.
|
||||||
|
- [ ] Implement move entry, navigation, branches, insert mode, region-of-interest selection, and undo/redo.
|
||||||
|
- [ ] Implement the move tree, candidate moves, principal variation interaction, ownership display, and analysis graph.
|
||||||
|
- [ ] Implement SGF open/save, recent paths, metadata, comments, and generated analysis feedback.
|
||||||
|
- [ ] Add component and controller tests for SGF round trips, navigation, analysis refresh, and thread-safe redraws.
|
||||||
|
|
||||||
|
Done when an existing SGF can be opened, analyzed, navigated, edited, and saved from the Qt UI.
|
||||||
|
|
||||||
|
## 4. Port Play, Teaching, and Training
|
||||||
|
|
||||||
|
- [ ] Implement player setup, AI strategy settings, human-like model settings, and play-versus-AI flow.
|
||||||
|
- [ ] Implement timer behavior, mistake coloring, sounds where supported, teaching undo, and training feedback.
|
||||||
|
- [ ] Implement self-play/setup-game workflows and analysis modes: extra, sweep, equalize, alternatives, local, and full-game reanalysis.
|
||||||
|
- [ ] Implement tsumego frame generation and region analysis UI.
|
||||||
|
- [ ] Add deterministic controller tests with fake engines; keep KataGo strategy coverage as an integration test.
|
||||||
|
|
||||||
|
Done when normal play, teaching, and training use cases work at parity with the required functionality list in `AGENTS.md`.
|
||||||
|
|
||||||
|
## 5. Port Settings, Localization, and Remote Engine Support
|
||||||
|
|
||||||
|
- [ ] Build Qt settings dialogs for general, engine, AI, timer, and trainer configuration.
|
||||||
|
- [ ] Connect locale switching to Qt retranslation and per-language font selection.
|
||||||
|
- [ ] Build remote-engine configuration, reconnect status, warnings, and recovery UI using core events.
|
||||||
|
- [ ] Test custom, local, and remote engine backend selection without widget mocks.
|
||||||
|
|
||||||
|
Done when settings changes persist and apply immediately, language changes retranslate the active UI, and remote recovery is visible in Qt.
|
||||||
|
|
||||||
|
## 6. Release Readiness
|
||||||
|
|
||||||
|
- [ ] Add a core-only CI job that runs without Kivy installed.
|
||||||
|
- [ ] Add a Qt headless smoke-test job and keep KataGo integration tests separately marked.
|
||||||
|
- [ ] Run KataGo integration tests with `RUSTICL_ENABLE=radeonsi`; rerun the tuner after changing KataGo, its model, or analysis config.
|
||||||
|
- [ ] Remove Kivy runtime/build dependencies only after Qt reaches required-feature parity.
|
||||||
|
- [ ] Document packaging for Linux/Wayland and supported KataGo asset locations.
|
||||||
|
|
||||||
|
Done when the Qt frontend passes its smoke/integration suites and can be packaged without a Kivy runtime dependency.
|
||||||
+3
-1
@@ -70,7 +70,7 @@ from kivy.clock import Clock
|
|||||||
from kivy.metrics import dp
|
from kivy.metrics import dp
|
||||||
from katrain.core.ai import generate_ai_move
|
from katrain.core.ai import generate_ai_move
|
||||||
|
|
||||||
from katrain.core.lang import DEFAULT_LANGUAGE, i18n
|
from katrain.gui.kivy_lang import DEFAULT_LANGUAGE, i18n
|
||||||
from katrain.core.constants import (
|
from katrain.core.constants import (
|
||||||
OUTPUT_ERROR,
|
OUTPUT_ERROR,
|
||||||
OUTPUT_KATAGO_STDERR,
|
OUTPUT_KATAGO_STDERR,
|
||||||
@@ -105,6 +105,7 @@ from katrain.core.game import Game, IllegalMoveException, KaTrainSGF, BaseGame
|
|||||||
from katrain.core.sgf_parser import Move, ParseError
|
from katrain.core.sgf_parser import Move, ParseError
|
||||||
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, ConfigAIPopup
|
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, ConfigAIPopup
|
||||||
from katrain.gui.theme import Theme
|
from katrain.gui.theme import Theme
|
||||||
|
from katrain.gui.kivy_adapter import KivyEventSink
|
||||||
from kivymd.app import MDApp
|
from kivymd.app import MDApp
|
||||||
|
|
||||||
# used in kv
|
# used in kv
|
||||||
@@ -122,6 +123,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.events = KivyEventSink(self)
|
||||||
self.engine = None
|
self.engine = None
|
||||||
self.contributing = False
|
self.contributing = False
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
from kivy import Config
|
|
||||||
from kivy.storage.jsonstore import JsonStore
|
|
||||||
|
|
||||||
from katrain.core.ai import ai_rank_estimation
|
from katrain.core.ai import ai_rank_estimation
|
||||||
from katrain.core.constants import (
|
from katrain.core.constants import (
|
||||||
@@ -75,12 +73,6 @@ class KaTrainBase:
|
|||||||
self.config_file = self._load_config(force_package_config=force_package_config)
|
self.config_file = self._load_config(force_package_config=force_package_config)
|
||||||
self.debug_level = self.config("general/debug_level", OUTPUT_INFO) if debug_level is None else debug_level
|
self.debug_level = self.config("general/debug_level", OUTPUT_INFO) if debug_level is None else debug_level
|
||||||
|
|
||||||
Config.set("kivy", "log_level", "warning")
|
|
||||||
if self.debug_level >= OUTPUT_DEBUG:
|
|
||||||
Config.set("kivy", "log_enable", 1)
|
|
||||||
Config.set("kivy", "log_level", "debug")
|
|
||||||
# if self.debug_level >= OUTPUT_EXTRA_DEBUG:
|
|
||||||
# Config.set("kivy", "log_level", "trace")
|
|
||||||
self.players_info = {"B": Player("B"), "W": Player("W")}
|
self.players_info = {"B": Player("B"), "W": Player("W")}
|
||||||
self.reset_players()
|
self.reset_players()
|
||||||
|
|
||||||
@@ -113,7 +105,8 @@ class KaTrainBase:
|
|||||||
self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO)
|
self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO)
|
||||||
else: # user file exists
|
else: # user file exists
|
||||||
try:
|
try:
|
||||||
version_str = JsonStore(user_config_file).get("general")["version"]
|
with open(user_config_file, encoding="utf-8") as config_handle:
|
||||||
|
version_str = json.load(config_handle)["general"]["version"]
|
||||||
version = parse_version(version_str)
|
version = parse_version(version_str)
|
||||||
self.log(f"Parsed version: {version}", OUTPUT_DEBUG)
|
self.log(f"Parsed version: {version}", OUTPUT_DEBUG)
|
||||||
except Exception as e: # noqa E722 broken file etc
|
except Exception as e: # noqa E722 broken file etc
|
||||||
@@ -138,7 +131,7 @@ class KaTrainBase:
|
|||||||
OUTPUT_INFO,
|
OUTPUT_INFO,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self._config_store = JsonStore(config_file, indent=4)
|
self._config_store = JsonConfigStore(config_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR)
|
self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -187,3 +180,18 @@ class KaTrainBase:
|
|||||||
@property
|
@property
|
||||||
def next_player_info(self) -> Player:
|
def next_player_info(self) -> Player:
|
||||||
return self.players_info[self.game.current_node.next_player]
|
return self.players_info[self.game.current_node.next_player]
|
||||||
|
|
||||||
|
|
||||||
|
class JsonConfigStore(dict):
|
||||||
|
"""Small replacement for Kivy's JsonStore used by the reusable core."""
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
with open(filename, encoding="utf-8") as config_handle:
|
||||||
|
super().__init__(json.load(config_handle))
|
||||||
|
|
||||||
|
def put(self, key, **value):
|
||||||
|
self[key] = value
|
||||||
|
with open(self.filename, "w", encoding="utf-8") as config_handle:
|
||||||
|
json.dump(self, config_handle, indent=4)
|
||||||
|
config_handle.write("\n")
|
||||||
@@ -12,6 +12,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_KATAGO_STDERR, DATA_FOLDER
|
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_KATAGO_STDERR, DATA_FOLDER
|
||||||
from katrain.core.engine import BaseEngine
|
from katrain.core.engine import BaseEngine
|
||||||
|
from katrain.core.events import event_sink
|
||||||
from katrain.core.game import BaseGame
|
from katrain.core.game import BaseGame
|
||||||
from katrain.core.lang import i18n
|
from katrain.core.lang import i18n
|
||||||
from katrain.core.sgf_parser import Move
|
from katrain.core.sgf_parser import Move
|
||||||
@@ -103,7 +104,7 @@ class KataGoContributeEngine(BaseEngine):
|
|||||||
if current_game.current_node.children:
|
if current_game.current_node.children:
|
||||||
current_game.redo(1)
|
current_game.redo(1)
|
||||||
self.last_advance = time.time()
|
self.last_advance = time.time()
|
||||||
self.katrain("update-state")
|
event_sink(self.katrain).state_changed()
|
||||||
elif time.time() - self.last_advance > self.GIVE_UP_AFTER:
|
elif time.time() - self.last_advance > self.GIVE_UP_AFTER:
|
||||||
self.katrain.log(
|
self.katrain.log(
|
||||||
f"Giving up on game {self.showing_game} which appears stuck, finding a new one", OUTPUT_INFO
|
f"Giving up on game {self.showing_game} which appears stuck, finding a new one", OUTPUT_INFO
|
||||||
@@ -125,8 +126,7 @@ class KataGoContributeEngine(BaseEngine):
|
|||||||
self.last_advance = time.time()
|
self.last_advance = time.time()
|
||||||
self.katrain.log(f"Showing game {self.showing_game}, {best_count} moves left to show.", OUTPUT_INFO)
|
self.katrain.log(f"Showing game {self.showing_game}, {best_count} moves left to show.", OUTPUT_INFO)
|
||||||
|
|
||||||
self.katrain.game = self.active_games[self.showing_game]
|
event_sink(self.katrain).contribution_game_changed(self.active_games[self.showing_game])
|
||||||
self.katrain("update-state", redraw_board=True)
|
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
return f"Contributing to distributed training\nGames: {self.uploaded_games_count} uploaded, {len(self.active_games)} in buffer, {len(self.finished_games)} shown\n{self.move_count} moves played ({60*self.move_count/(time.time()-self.start_time):.1f}/min, {self.visits_count / (time.time() - self.start_time):.1f} visits/s)\n"
|
return f"Contributing to distributed training\nGames: {self.uploaded_games_count} uploaded, {len(self.active_games)} in buffer, {len(self.finished_games)} shown\n{self.move_count} moves played ({60*self.move_count/(time.time()-self.start_time):.1f}/min, {self.visits_count / (time.time() - self.start_time):.1f} visits/s)\n"
|
||||||
@@ -287,7 +287,7 @@ class KataGoContributeEngine(BaseEngine):
|
|||||||
f"[{time.time()-self.start_time:.1f}] Game {game_id} Move {analysis['turnNumber']}: {' '.join(analysis['move'])} Visits {analysis['rootInfo']['visits']} Time {dt:.1f}s\t Moves/min {60*self.move_count/(time.time()-self.start_time):.1f} Visits/s {self.visits_count/(time.time()-self.start_time):.1f}",
|
f"[{time.time()-self.start_time:.1f}] Game {game_id} Move {analysis['turnNumber']}: {' '.join(analysis['move'])} Visits {analysis['rootInfo']['visits']} Time {dt:.1f}s\t Moves/min {60*self.move_count/(time.time()-self.start_time):.1f} Visits/s {self.visits_count/(time.time()-self.start_time):.1f}",
|
||||||
OUTPUT_DEBUG,
|
OUTPUT_DEBUG,
|
||||||
)
|
)
|
||||||
self.katrain("update-state")
|
event_sink(self.katrain).state_changed()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.katrain.log(f"Exception {e} in parsing or processing JSON: {line}", OUTPUT_ERROR)
|
self.katrain.log(f"Exception {e} in parsing or processing JSON: {line}", OUTPUT_ERROR)
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import sys
|
||||||
from typing import Callable, Dict, List, Optional
|
from typing import Callable, Dict, List, Optional
|
||||||
|
|
||||||
from kivy.utils import platform as kivy_platform
|
|
||||||
|
|
||||||
from katrain.core.constants import (
|
from katrain.core.constants import (
|
||||||
OUTPUT_DEBUG,
|
OUTPUT_DEBUG,
|
||||||
OUTPUT_ERROR,
|
OUTPUT_ERROR,
|
||||||
@@ -21,6 +20,7 @@ from katrain.core.constants import (
|
|||||||
KATAGO_EXCEPTION,
|
KATAGO_EXCEPTION,
|
||||||
PONDERING_REPORT_DT,
|
PONDERING_REPORT_DT,
|
||||||
)
|
)
|
||||||
|
from katrain.core.events import event_sink
|
||||||
from katrain.core.game_node import GameNode
|
from katrain.core.game_node import GameNode
|
||||||
from katrain.core.lang import i18n
|
from katrain.core.lang import i18n
|
||||||
from katrain.core.sgf_parser import Move
|
from katrain.core.sgf_parser import Move
|
||||||
@@ -82,9 +82,9 @@ class BaseEngine: # some common elements between analysis and contribute engine
|
|||||||
|
|
||||||
def get_engine_path(self, exe):
|
def get_engine_path(self, exe):
|
||||||
if not exe:
|
if not exe:
|
||||||
if kivy_platform == "win":
|
if os.name == "nt":
|
||||||
exe = "katrain/KataGo/katago.exe"
|
exe = "katrain/KataGo/katago.exe"
|
||||||
elif kivy_platform == "linux":
|
elif sys.platform.startswith("linux"):
|
||||||
exe = "katrain/KataGo/katago"
|
exe = "katrain/KataGo/katago"
|
||||||
else:
|
else:
|
||||||
exe = find_package_resource("katrain/KataGo/katago-osx") # github actions built
|
exe = find_package_resource("katrain/KataGo/katago-osx") # github actions built
|
||||||
@@ -166,7 +166,7 @@ class KataGoEngine(BaseEngine):
|
|||||||
def on_error(self, message, code=None, allow_popup=True):
|
def on_error(self, message, code=None, allow_popup=True):
|
||||||
self.katrain.log(message, OUTPUT_ERROR)
|
self.katrain.log(message, OUTPUT_ERROR)
|
||||||
if self.allow_recovery and allow_popup:
|
if self.allow_recovery and allow_popup:
|
||||||
self.katrain("engine_recovery_popup", message, code, self.ENGINE_TYPE)
|
event_sink(self.katrain).engine_failure(message, code, self.ENGINE_TYPE)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
with self.thread_lock:
|
with self.thread_lock:
|
||||||
@@ -347,8 +347,7 @@ class KataGoEngine(BaseEngine):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR)
|
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if getattr(self.katrain, "update_state", None): # easier mocking etc
|
event_sink(self.katrain).state_changed()
|
||||||
self.katrain.update_state()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.katrain.log(f"Unexpected exception {e} while processing KataGo output {line}", OUTPUT_ERROR)
|
self.katrain.log(f"Unexpected exception {e} while processing KataGo output {line}", OUTPUT_ERROR)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Framework-neutral notifications emitted by core services."""
|
||||||
|
|
||||||
|
|
||||||
|
class CoreEventSink:
|
||||||
|
"""Application boundary for status, refresh, and recovery requests.
|
||||||
|
|
||||||
|
Frontends must marshal these calls to their own UI thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def status(self, message, priority, node=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def state_changed(self, redraw_board=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def engine_failure(self, message, code, engine_type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_mode_changed(self, insert_node):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def move_tree_changed(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def contribution_game_changed(self, game):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
NULL_EVENTS = CoreEventSink()
|
||||||
|
|
||||||
|
|
||||||
|
def event_sink(host):
|
||||||
|
"""Return a host's event sink without making it a required UI object."""
|
||||||
|
return getattr(host, "events", NULL_EVENTS)
|
||||||
+19
-21
@@ -6,8 +6,6 @@ import threading
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
from kivy.clock import Clock
|
|
||||||
|
|
||||||
from katrain.core.constants import (
|
from katrain.core.constants import (
|
||||||
OUTPUT_DEBUG,
|
OUTPUT_DEBUG,
|
||||||
OUTPUT_EXTRA_DEBUG,
|
OUTPUT_EXTRA_DEBUG,
|
||||||
@@ -28,6 +26,7 @@ from katrain.core.constants import (
|
|||||||
PRIORITY_DEFAULT,
|
PRIORITY_DEFAULT,
|
||||||
)
|
)
|
||||||
from katrain.core.engine import KataGoEngine
|
from katrain.core.engine import KataGoEngine
|
||||||
|
from katrain.core.events import event_sink
|
||||||
from katrain.core.game_node import GameNode
|
from katrain.core.game_node import GameNode
|
||||||
from katrain.core.lang import i18n, rank_label
|
from katrain.core.lang import i18n, rank_label
|
||||||
from katrain.core.sgf_parser import SGF, Move
|
from katrain.core.sgf_parser import SGF, Move
|
||||||
@@ -467,7 +466,7 @@ class Game(BaseGame):
|
|||||||
|
|
||||||
def set_current_node(self, node):
|
def set_current_node(self, node):
|
||||||
if self.insert_mode:
|
if self.insert_mode:
|
||||||
self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR)
|
event_sink(self.katrain).status(i18n._("finish inserting before navigating"), STATUS_ERROR)
|
||||||
return
|
return
|
||||||
super().set_current_node(node)
|
super().set_current_node(node)
|
||||||
|
|
||||||
@@ -505,7 +504,7 @@ class Game(BaseGame):
|
|||||||
self.insert_mode = False
|
self.insert_mode = False
|
||||||
else:
|
else:
|
||||||
self.insert_after = self.current_node.ordered_children[0]
|
self.insert_after = self.current_node.ordered_children[0]
|
||||||
self.katrain.controls.set_status(i18n._("starting insert mode"), STATUS_INFO)
|
event_sink(self.katrain).status(i18n._("starting insert mode"), STATUS_INFO)
|
||||||
else:
|
else:
|
||||||
copy_from_node = self.insert_after
|
copy_from_node = self.insert_after
|
||||||
copy_to_node = self.current_node
|
copy_to_node = self.current_node
|
||||||
@@ -531,15 +530,14 @@ class Game(BaseGame):
|
|||||||
except IllegalMoveException:
|
except IllegalMoveException:
|
||||||
pass # illegal move = stop
|
pass # illegal move = stop
|
||||||
self._calculate_groups() # recalculate groups
|
self._calculate_groups() # recalculate groups
|
||||||
self.katrain.controls.set_status(
|
event_sink(self.katrain).status(
|
||||||
i18n._("ending insert mode").format(num_copied=num_copied), STATUS_INFO
|
i18n._("ending insert mode").format(num_copied=num_copied), STATUS_INFO
|
||||||
)
|
)
|
||||||
self.analyze_all_nodes(analyze_fast=True, even_if_present=False)
|
self.analyze_all_nodes(analyze_fast=True, even_if_present=False)
|
||||||
else:
|
else:
|
||||||
self.katrain.controls.set_status("", STATUS_INFO)
|
event_sink(self.katrain).status("", STATUS_INFO)
|
||||||
self.katrain.controls.move_tree.insert_node = self.insert_after if self.insert_mode else None
|
event_sink(self.katrain).insert_mode_changed(self.insert_after if self.insert_mode else None)
|
||||||
self.katrain.controls.move_tree.redraw()
|
event_sink(self.katrain).state_changed(redraw_board=True)
|
||||||
self.katrain.update_state(redraw_board=True)
|
|
||||||
|
|
||||||
# Play a Move from the current position, raise IllegalMoveException if invalid.
|
# Play a Move from the current position, raise IllegalMoveException if invalid.
|
||||||
def play(self, move: Move, ignore_ko: bool = False, analyze=True):
|
def play(self, move: Move, ignore_ko: bool = False, analyze=True):
|
||||||
@@ -561,7 +559,7 @@ class Game(BaseGame):
|
|||||||
self.region_of_interest = [xmin, xmax, ymin, ymax]
|
self.region_of_interest = [xmin, xmax, ymin, ymax]
|
||||||
else:
|
else:
|
||||||
self.region_of_interest = None
|
self.region_of_interest = None
|
||||||
self.katrain.controls.set_status("", OUTPUT_INFO)
|
event_sink(self.katrain).status("", OUTPUT_INFO)
|
||||||
|
|
||||||
def analyze_extra(self, mode, **kwargs):
|
def analyze_extra(self, mode, **kwargs):
|
||||||
stones = {s.coords for s in self.stones}
|
stones = {s.coords for s in self.stones}
|
||||||
@@ -588,7 +586,7 @@ class Game(BaseGame):
|
|||||||
|
|
||||||
if mode == "extra":
|
if mode == "extra":
|
||||||
visits = cn.analysis_visits_requested + engine.config["max_visits"]
|
visits = cn.analysis_visits_requested + engine.config["max_visits"]
|
||||||
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
|
event_sink(self.katrain).status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||||
cn.analyze(
|
cn.analyze(
|
||||||
engine,
|
engine,
|
||||||
visits=visits,
|
visits=visits,
|
||||||
@@ -619,9 +617,9 @@ class Game(BaseGame):
|
|||||||
continue
|
continue
|
||||||
node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
|
node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
|
||||||
if not move_range:
|
if not move_range:
|
||||||
self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
|
event_sink(self.katrain).status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||||
else:
|
else:
|
||||||
self.katrain.controls.set_status(
|
event_sink(self.katrain).status(
|
||||||
i18n._("move range analysis").format(
|
i18n._("move range analysis").format(
|
||||||
start_move=move_range[0], end_move=move_range[1], visits=visits
|
start_move=move_range[0], end_move=move_range[1], visits=visits
|
||||||
),
|
),
|
||||||
@@ -655,19 +653,19 @@ class Game(BaseGame):
|
|||||||
if (x, y) not in stones
|
if (x, y) not in stones
|
||||||
]
|
]
|
||||||
visits = engine.config["fast_visits"]
|
visits = engine.config["fast_visits"]
|
||||||
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
|
event_sink(self.katrain).status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||||
priority = PRIORITY_SWEEP
|
priority = PRIORITY_SWEEP
|
||||||
elif mode in ["equalize", "alternative", "local"]:
|
elif mode in ["equalize", "alternative", "local"]:
|
||||||
if not cn.analysis_complete and mode != "local":
|
if not cn.analysis_complete and mode != "local":
|
||||||
self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node)
|
event_sink(self.katrain).status(i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node)
|
||||||
return
|
return
|
||||||
if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird
|
if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird
|
||||||
self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS)
|
event_sink(self.katrain).status(i18n._("alternative analysis"), STATUS_ANALYSIS)
|
||||||
cn.analyze(engine, priority=PRIORITY_ALTERNATIVES, time_limit=False, find_alternatives="alternative")
|
cn.analyze(engine, priority=PRIORITY_ALTERNATIVES, time_limit=False, find_alternatives="alternative")
|
||||||
visits = engine.config["fast_visits"]
|
visits = engine.config["fast_visits"]
|
||||||
else: # equalize
|
else: # equalize
|
||||||
visits = max(d["visits"] for d in cn.analysis["moves"].values())
|
visits = max(d["visits"] for d in cn.analysis["moves"].values())
|
||||||
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
|
event_sink(self.katrain).status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||||
priority = PRIORITY_EQUALIZE
|
priority = PRIORITY_EQUALIZE
|
||||||
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
|
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
|
||||||
else:
|
else:
|
||||||
@@ -761,7 +759,7 @@ class Game(BaseGame):
|
|||||||
new_node.ai_thoughts = ai_thoughts
|
new_node.ai_thoughts = ai_thoughts
|
||||||
if until_move != "end" and target_b_advantage is not None:
|
if until_move != "end" and target_b_advantage is not None:
|
||||||
self.set_current_node(new_node)
|
self.set_current_node(new_node)
|
||||||
self.katrain.controls.set_status(
|
event_sink(self.katrain).status(
|
||||||
i18n._("setup game status message").format(move=new_node.depth, until_move=until_move),
|
i18n._("setup game status message").format(move=new_node.depth, until_move=until_move),
|
||||||
STATUS_INFO,
|
STATUS_INFO,
|
||||||
)
|
)
|
||||||
@@ -770,7 +768,7 @@ class Game(BaseGame):
|
|||||||
node.remove_shortcut()
|
node.remove_shortcut()
|
||||||
cn.add_shortcut(new_node)
|
cn.add_shortcut(new_node)
|
||||||
|
|
||||||
self.katrain.controls.move_tree.redraw_tree_trigger()
|
event_sink(self.katrain).move_tree_changed()
|
||||||
request_analysis_for_node(new_node)
|
request_analysis_for_node(new_node)
|
||||||
|
|
||||||
request_analysis_for_node(cn)
|
request_analysis_for_node(cn)
|
||||||
@@ -797,7 +795,7 @@ class Game(BaseGame):
|
|||||||
node.auto_undo = undo
|
node.auto_undo = undo
|
||||||
if undo:
|
if undo:
|
||||||
self.undo(1)
|
self.undo(1)
|
||||||
self.katrain.controls.set_status(
|
event_sink(self.katrain).status(
|
||||||
i18n._("teaching undo message").format(move=move.gtp(), points_lost=points_lost), STATUS_TEACHING
|
i18n._("teaching undo message").format(move=move.gtp(), points_lost=points_lost), STATUS_TEACHING
|
||||||
)
|
)
|
||||||
self.katrain.update_state()
|
event_sink(self.katrain).state_changed()
|
||||||
@@ -18,7 +18,6 @@ from katrain.core.constants import (
|
|||||||
from katrain.core.lang import i18n
|
from katrain.core.lang import i18n
|
||||||
from katrain.core.sgf_parser import Move, SGFNode
|
from katrain.core.sgf_parser import Move, SGFNode
|
||||||
from katrain.core.utils import evaluation_class, pack_floats, unpack_floats, var_to_grid
|
from katrain.core.utils import evaluation_class, pack_floats, unpack_floats, var_to_grid
|
||||||
from katrain.gui.theme import Theme
|
|
||||||
|
|
||||||
|
|
||||||
def analysis_dumps(analysis):
|
def analysis_dumps(analysis):
|
||||||
@@ -318,13 +317,11 @@ class GameNode(SGFNode):
|
|||||||
return ix + 1, p, policy_ranking
|
return ix + 1, p, policy_ranking
|
||||||
return None, 0.0, []
|
return None, 0.0, []
|
||||||
|
|
||||||
def make_pv(self, player, pv, interactive):
|
def make_pv(self, player, pv, interactive=False):
|
||||||
pvtext = f"{player}{' '.join(pv)}"
|
pvtext = f"{player}{' '.join(pv)}"
|
||||||
if interactive:
|
|
||||||
pvtext = f"[u][ref={pvtext}][color={Theme.INFO_PV_COLOR}]{pvtext}[/color][/ref][/u]"
|
|
||||||
return pvtext
|
return pvtext
|
||||||
|
|
||||||
def comment(self, sgf=False, teach=False, details=False, interactive=True):
|
def comment(self, sgf=False, teach=False, details=False, interactive=True, pv_formatter=None):
|
||||||
single_move = self.move
|
single_move = self.move
|
||||||
if not self.parent or not single_move: # root
|
if not self.parent or not single_move: # root
|
||||||
if self.root:
|
if self.root:
|
||||||
@@ -359,7 +356,9 @@ class GameNode(SGFNode):
|
|||||||
else:
|
else:
|
||||||
text += i18n._("Info:best move") + "\n"
|
text += i18n._("Info:best move") + "\n"
|
||||||
if previous_top_move.get("pv") and (sgf or details):
|
if previous_top_move.get("pv") and (sgf or details):
|
||||||
pv = self.make_pv(single_move.player, previous_top_move["pv"], interactive)
|
pv = self.make_pv(single_move.player, previous_top_move["pv"])
|
||||||
|
if interactive and pv_formatter:
|
||||||
|
pv = pv_formatter(pv)
|
||||||
text += i18n._("Info:PV").format(pv=pv) + "\n"
|
text += i18n._("Info:PV").format(pv=pv) + "\n"
|
||||||
if sgf or details or teach:
|
if sgf or details or teach:
|
||||||
currmove_pol_rank, currmove_pol_prob, policy_ranking = self.move_policy_stats()
|
currmove_pol_rank, currmove_pol_prob, policy_ranking = self.move_policy_stats()
|
||||||
|
|||||||
+2
-52
@@ -1,78 +1,28 @@
|
|||||||
import gettext
|
import gettext
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from kivy._event import Observable
|
|
||||||
|
|
||||||
from katrain.core.utils import find_package_resource
|
from katrain.core.utils import find_package_resource
|
||||||
from katrain.gui.theme import Theme
|
class Lang:
|
||||||
|
|
||||||
|
|
||||||
class Lang(Observable):
|
|
||||||
observers = []
|
|
||||||
callbacks = []
|
|
||||||
FONTS = {"jp": "NotoSansJP-Regular.otf", "tr": "NotoSans-Regular.ttf", "ua": "NotoSans-Regular.ttf"}
|
FONTS = {"jp": "NotoSansJP-Regular.otf", "tr": "NotoSans-Regular.ttf", "ua": "NotoSans-Regular.ttf"}
|
||||||
|
|
||||||
def __init__(self, lang):
|
def __init__(self, lang):
|
||||||
super(Lang, self).__init__()
|
|
||||||
self.lang = None
|
self.lang = None
|
||||||
self.switch_lang(lang)
|
self.switch_lang(lang)
|
||||||
|
|
||||||
def _(self, text):
|
def _(self, text):
|
||||||
return self.ugettext(text)
|
return self.ugettext(text)
|
||||||
|
|
||||||
def set_widget_font(self, widget):
|
|
||||||
widget.font_name = self.font_name
|
|
||||||
for sub_widget in [getattr(widget, "_hint_lbl", None), getattr(widget, "_msg_lbl", None)]: # MDText
|
|
||||||
if sub_widget:
|
|
||||||
sub_widget.font_name = self.font_name
|
|
||||||
|
|
||||||
def fbind(self, name, func, *args):
|
|
||||||
if name == "_":
|
|
||||||
widget, property, *_ = args[0]
|
|
||||||
self.observers.append((widget, func, args))
|
|
||||||
try:
|
|
||||||
self.set_widget_font(widget)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
# pass
|
|
||||||
else:
|
|
||||||
return super(Lang, self).fbind(name, func, *args)
|
|
||||||
|
|
||||||
def funbind(self, name, func, *args):
|
|
||||||
if name == "_":
|
|
||||||
widget, *_ = args[0]
|
|
||||||
key = (widget, func, args)
|
|
||||||
if key in self.observers:
|
|
||||||
self.observers.remove(key)
|
|
||||||
else:
|
|
||||||
return super(Lang, self).funbind(name, func, *args)
|
|
||||||
|
|
||||||
def switch_lang(self, lang):
|
def switch_lang(self, lang):
|
||||||
if lang == self.lang:
|
if lang == self.lang:
|
||||||
return
|
return
|
||||||
# get the right locales directory, and instantiate a gettext
|
# get the right locales directory, and instantiate a gettext
|
||||||
self.lang = lang
|
self.lang = lang
|
||||||
self.font_name = self.FONTS.get(lang) or Theme.DEFAULT_FONT
|
self.font_name = self.FONTS.get(lang)
|
||||||
i18n_dir, _ = os.path.split(find_package_resource("katrain/i18n/__init__.py"))
|
i18n_dir, _ = os.path.split(find_package_resource("katrain/i18n/__init__.py"))
|
||||||
locale_dir = os.path.join(i18n_dir, "locales")
|
locale_dir = os.path.join(i18n_dir, "locales")
|
||||||
locales = gettext.translation("katrain", locale_dir, languages=[lang, DEFAULT_LANGUAGE])
|
locales = gettext.translation("katrain", locale_dir, languages=[lang, DEFAULT_LANGUAGE])
|
||||||
self.ugettext = locales.gettext
|
self.ugettext = locales.gettext
|
||||||
|
|
||||||
# update all the kv rules attached to this text
|
|
||||||
for widget, func, args in self.observers:
|
|
||||||
try:
|
|
||||||
func(args[0], None, None)
|
|
||||||
self.set_widget_font(widget)
|
|
||||||
except ReferenceError:
|
|
||||||
pass # proxy no longer exists
|
|
||||||
except Exception as e:
|
|
||||||
print("Error in switching languages", e)
|
|
||||||
for cb in self.callbacks:
|
|
||||||
try:
|
|
||||||
cb(self)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed callback on language change: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_LANGUAGE = "en"
|
DEFAULT_LANGUAGE = "en"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from katrain.core.constants import (
|
|||||||
OUTPUT_INFO,
|
OUTPUT_INFO,
|
||||||
STATUS_INFO,
|
STATUS_INFO,
|
||||||
)
|
)
|
||||||
|
from katrain.core.events import event_sink
|
||||||
from katrain.core.engine import BaseEngine, KataGoEngine, resolve_engine_backend
|
from katrain.core.engine import BaseEngine, KataGoEngine, resolve_engine_backend
|
||||||
from katrain.core.lang import i18n
|
from katrain.core.lang import i18n
|
||||||
from katrain.core.utils import json_truncate_arrays
|
from katrain.core.utils import json_truncate_arrays
|
||||||
@@ -277,10 +278,7 @@ class RemoteKataGoEngine(KataGoEngine):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def _set_status(self, message):
|
def _set_status(self, message):
|
||||||
try:
|
event_sink(self.katrain).status(message, STATUS_INFO)
|
||||||
self.katrain.controls.set_status(message, STATUS_INFO)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_new_game(self):
|
def on_new_game(self):
|
||||||
# Parent clears self.queries; drop the payloads too so a later
|
# Parent clears self.queries; drop the payloads too so a later
|
||||||
@@ -520,10 +518,7 @@ class RemoteKataGoEngine(KataGoEngine):
|
|||||||
f"Remote KataGo warning: {warning_text}",
|
f"Remote KataGo warning: {warning_text}",
|
||||||
OUTPUT_INFO,
|
OUTPUT_INFO,
|
||||||
)
|
)
|
||||||
try:
|
event_sink(self.katrain).status(warning_text, STATUS_INFO)
|
||||||
self.katrain.controls.set_status(warning_text, STATUS_INFO)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if "error" in analysis:
|
if "error" in analysis:
|
||||||
del self.queries[query_id]
|
del self.queries[query_id]
|
||||||
@@ -565,8 +560,7 @@ class RemoteKataGoEngine(KataGoEngine):
|
|||||||
)
|
)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if getattr(self.katrain, "update_state", None):
|
event_sink(self.katrain).state_changed()
|
||||||
self.katrain.update_state()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.katrain.log(
|
self.katrain.log(
|
||||||
f"Unexpected exception {e} processing remote KataGo output: {line[:200]!r}",
|
f"Unexpected exception {e} processing remote KataGo output: {line[:200]!r}",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#:kivy 2.3.0
|
#:kivy 2.3.0
|
||||||
#:import i18n katrain.core.lang.i18n
|
#:import i18n katrain.gui.kivy_lang.i18n
|
||||||
|
|
||||||
#:import PLAYER_TYPES katrain.core.constants.PLAYER_TYPES
|
#:import PLAYER_TYPES katrain.core.constants.PLAYER_TYPES
|
||||||
#:import PLAYER_HUMAN katrain.core.constants.PLAYER_HUMAN
|
#:import PLAYER_HUMAN katrain.core.constants.PLAYER_HUMAN
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from katrain.core.constants import (
|
|||||||
TOP_MOVE_WINRATE,
|
TOP_MOVE_WINRATE,
|
||||||
)
|
)
|
||||||
from katrain.core.game import Move
|
from katrain.core.game import Move
|
||||||
from katrain.core.lang import i18n
|
from katrain.gui.kivy_lang import i18n
|
||||||
from katrain.core.utils import evaluation_class, format_visits, var_to_grid, json_truncate_arrays
|
from katrain.core.utils import evaluation_class, format_visits, var_to_grid, json_truncate_arrays
|
||||||
from katrain.gui.kivyutils import draw_circle, draw_text, cached_texture
|
from katrain.gui.kivyutils import draw_circle, draw_text, cached_texture
|
||||||
from katrain.gui.popups import I18NPopup, ReAnalyzeGamePopup, GameReportPopup, TsumegoFramePopup
|
from katrain.gui.popups import I18NPopup, ReAnalyzeGamePopup, GameReportPopup, TsumegoFramePopup
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ from katrain.core.constants import (
|
|||||||
AI_DEFAULT,
|
AI_DEFAULT,
|
||||||
PLAYER_AI,
|
PLAYER_AI,
|
||||||
)
|
)
|
||||||
from katrain.core.lang import rank_label
|
from katrain.gui.kivy_lang import rank_label
|
||||||
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
|
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
|
||||||
from katrain.gui.theme import Theme
|
from katrain.gui.theme import Theme
|
||||||
from katrain.gui.sound import play_sound, stop_sound
|
from katrain.gui.sound import play_sound, stop_sound
|
||||||
|
|
||||||
|
|
||||||
|
def format_interactive_pv(pv):
|
||||||
|
return f"[u][ref={pv}][color={Theme.INFO_PV_COLOR}]{pv}[/color][/ref][/u]"
|
||||||
|
|
||||||
|
|
||||||
class PlayAnalyzeSelect(MDFloatLayout):
|
class PlayAnalyzeSelect(MDFloatLayout):
|
||||||
katrain = ObjectProperty(None)
|
katrain = ObjectProperty(None)
|
||||||
mode = OptionProperty(MODE_PLAY, options=[MODE_PLAY, MODE_ANALYZE])
|
mode = OptionProperty(MODE_PLAY, options=[MODE_PLAY, MODE_ANALYZE])
|
||||||
@@ -160,7 +164,9 @@ class ControlsPanel(BoxLayout):
|
|||||||
|
|
||||||
if move or current_node.is_root:
|
if move or current_node.is_root:
|
||||||
info += self.active_comment_node.comment(
|
info += self.active_comment_node.comment(
|
||||||
teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details
|
teach=katrain.players_info[self.active_comment_node.player].being_taught,
|
||||||
|
details=details,
|
||||||
|
pv_formatter=format_interactive_pv,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.active_comment_node.analysis_exists:
|
if self.active_comment_node.analysis_exists:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Kivy implementations of framework-neutral core notifications."""
|
||||||
|
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
|
||||||
|
class KivyEventSink:
|
||||||
|
def __init__(self, katrain):
|
||||||
|
self.katrain = katrain
|
||||||
|
|
||||||
|
def status(self, message, priority, node=None):
|
||||||
|
def update(_dt):
|
||||||
|
if self.katrain.controls:
|
||||||
|
self.katrain.controls.set_status(message, priority, at_node=node)
|
||||||
|
|
||||||
|
Clock.schedule_once(update, -1)
|
||||||
|
|
||||||
|
def state_changed(self, redraw_board=False):
|
||||||
|
self.katrain.update_state(redraw_board=redraw_board)
|
||||||
|
|
||||||
|
def engine_failure(self, message, code, engine_type):
|
||||||
|
Clock.schedule_once(
|
||||||
|
lambda _dt: self.katrain._do_engine_recovery_popup(message, code, engine_type),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def insert_mode_changed(self, insert_node):
|
||||||
|
def update(_dt):
|
||||||
|
move_tree = self.katrain.controls.move_tree
|
||||||
|
move_tree.insert_node = insert_node
|
||||||
|
move_tree.redraw()
|
||||||
|
|
||||||
|
Clock.schedule_once(update, -1)
|
||||||
|
|
||||||
|
def move_tree_changed(self):
|
||||||
|
Clock.schedule_once(lambda _dt: self.katrain.controls.move_tree.redraw_tree_trigger(), -1)
|
||||||
|
|
||||||
|
def contribution_game_changed(self, game):
|
||||||
|
self.katrain.game = game
|
||||||
|
self.state_changed(redraw_board=True)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Kivy observable adapter for the framework-neutral translator."""
|
||||||
|
|
||||||
|
from kivy._event import Observable
|
||||||
|
|
||||||
|
from katrain.core.lang import DEFAULT_LANGUAGE, Lang as CoreLang
|
||||||
|
from katrain.gui.theme import Theme
|
||||||
|
|
||||||
|
|
||||||
|
class Lang(CoreLang, Observable):
|
||||||
|
observers = []
|
||||||
|
|
||||||
|
def __init__(self, lang):
|
||||||
|
Observable.__init__(self)
|
||||||
|
self.callbacks = []
|
||||||
|
super().__init__(lang)
|
||||||
|
|
||||||
|
def set_widget_font(self, widget):
|
||||||
|
widget.font_name = self.font_name or Theme.DEFAULT_FONT
|
||||||
|
for sub_widget in [getattr(widget, "_hint_lbl", None), getattr(widget, "_msg_lbl", None)]:
|
||||||
|
if sub_widget:
|
||||||
|
sub_widget.font_name = widget.font_name
|
||||||
|
|
||||||
|
def fbind(self, name, func, *args):
|
||||||
|
if name != "_":
|
||||||
|
return Observable.fbind(self, name, func, *args)
|
||||||
|
widget, _property, *_ = args[0]
|
||||||
|
self.observers.append((widget, func, args))
|
||||||
|
self.set_widget_font(widget)
|
||||||
|
|
||||||
|
def funbind(self, name, func, *args):
|
||||||
|
if name != "_":
|
||||||
|
return Observable.funbind(self, name, func, *args)
|
||||||
|
widget, *_ = args[0]
|
||||||
|
key = (widget, func, args)
|
||||||
|
if key in self.observers:
|
||||||
|
self.observers.remove(key)
|
||||||
|
|
||||||
|
def switch_lang(self, lang):
|
||||||
|
previous = self.lang
|
||||||
|
super().switch_lang(lang)
|
||||||
|
if lang == previous:
|
||||||
|
return
|
||||||
|
for widget, func, args in self.observers:
|
||||||
|
try:
|
||||||
|
func(args[0], None, None)
|
||||||
|
self.set_widget_font(widget)
|
||||||
|
except ReferenceError:
|
||||||
|
pass
|
||||||
|
for callback in self.callbacks:
|
||||||
|
callback(self)
|
||||||
|
|
||||||
|
|
||||||
|
i18n = Lang(DEFAULT_LANGUAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def rank_label(rank):
|
||||||
|
if rank is None:
|
||||||
|
return "??k"
|
||||||
|
if rank >= 0.5:
|
||||||
|
return f"{rank:.0f}{i18n._('strength:dan')}"
|
||||||
|
return f"{1-rank:.0f}{i18n._('strength:kyu')}"
|
||||||
|
|
||||||
|
__all__ = ["DEFAULT_LANGUAGE", "i18n", "rank_label"]
|
||||||
@@ -37,7 +37,7 @@ from katrain.core.constants import (
|
|||||||
PLAYING_NORMAL,
|
PLAYING_NORMAL,
|
||||||
PLAYING_TEACHING,
|
PLAYING_TEACHING,
|
||||||
)
|
)
|
||||||
from katrain.core.lang import i18n
|
from katrain.gui.kivy_lang import i18n
|
||||||
from katrain.gui.theme import Theme
|
from katrain.gui.theme import Theme
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from katrain.core.constants import (
|
|||||||
PLAYER_HUMAN,
|
PLAYER_HUMAN,
|
||||||
ADDITIONAL_MOVE_ORDER,
|
ADDITIONAL_MOVE_ORDER,
|
||||||
)
|
)
|
||||||
from katrain.core.lang import i18n, rank_label
|
from katrain.gui.kivy_lang import i18n, rank_label
|
||||||
from katrain.core.sgf_parser import Move
|
from katrain.core.sgf_parser import Move
|
||||||
from katrain.core.utils import PATHS, find_package_resource, evaluation_class
|
from katrain.core.utils import PATHS, find_package_resource, evaluation_class
|
||||||
from katrain.gui.kivyutils import (
|
from katrain.gui.kivyutils import (
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#:kivy 2.3.0
|
#:kivy 2.3.0
|
||||||
#:import i18n katrain.core.lang.i18n
|
#:import i18n katrain.gui.kivy_lang.i18n
|
||||||
#:import expanduser os.path.expanduser
|
#:import expanduser os.path.expanduser
|
||||||
#:import abspath os.path.abspath
|
#:import abspath os.path.abspath
|
||||||
#:import kivy_version kivy.__version__
|
#:import kivy_version kivy.__version__
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Regression tests for Kivy-free reusable core modules."""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
|
||||||
|
CORE_MODULES = (
|
||||||
|
"katrain.core.base_katrain",
|
||||||
|
"katrain.core.lang",
|
||||||
|
"katrain.core.game_node",
|
||||||
|
"katrain.core.engine",
|
||||||
|
"katrain.core.game",
|
||||||
|
"katrain.core.remote_engine",
|
||||||
|
"katrain.core.contribute_engine",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_imports_without_kivy(monkeypatch):
|
||||||
|
original_import = builtins.__import__
|
||||||
|
|
||||||
|
def reject_kivy(name, *args, **kwargs):
|
||||||
|
if name == "kivy" or name.startswith("kivy."):
|
||||||
|
raise AssertionError(f"core imported Kivy: {name}")
|
||||||
|
return original_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", reject_kivy)
|
||||||
|
for module in CORE_MODULES:
|
||||||
|
importlib.reload(importlib.import_module(module))
|
||||||
|
|
||||||
|
|
||||||
|
def test_pv_text_has_no_frontend_markup():
|
||||||
|
from katrain.core.game_node import GameNode
|
||||||
|
|
||||||
|
assert GameNode().make_pv("B", ["D4", "Q16"], interactive=True) == "BD4 Q16"
|
||||||
+18
-13
@@ -44,25 +44,31 @@ class FakeWS:
|
|||||||
self._recv.put(WebSocketException("connection lost"))
|
self._recv.put(WebSocketException("connection lost"))
|
||||||
|
|
||||||
|
|
||||||
class FakeControls:
|
class FakeEvents:
|
||||||
def set_status(self, *args, **kwargs):
|
def __init__(self):
|
||||||
pass
|
self.failures = []
|
||||||
|
self.statuses = []
|
||||||
|
self.state_changes = []
|
||||||
|
|
||||||
|
def engine_failure(self, message, code, engine_type):
|
||||||
|
self.failures.append((message, code, engine_type))
|
||||||
|
|
||||||
|
def status(self, message, priority, node=None):
|
||||||
|
self.statuses.append((message, priority, node))
|
||||||
|
|
||||||
|
def state_changed(self, redraw_board=False):
|
||||||
|
self.state_changes.append(redraw_board)
|
||||||
|
|
||||||
|
|
||||||
class FakeKatrain:
|
class FakeKatrain:
|
||||||
"""Records calls (e.g. the engine_recovery_popup trigger)."""
|
"""Minimal framework-neutral engine host."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.controls = FakeControls()
|
self.events = FakeEvents()
|
||||||
self.calls = []
|
|
||||||
self.update_state = lambda *a, **k: None
|
|
||||||
|
|
||||||
def log(self, *args, **kwargs):
|
def log(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
self.calls.append(args)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_until(pred, timeout=5.0, interval=0.02):
|
def wait_until(pred, timeout=5.0, interval=0.02):
|
||||||
end = time.time() + timeout
|
end = time.time() + timeout
|
||||||
@@ -72,7 +78,7 @@ def wait_until(pred, timeout=5.0, interval=0.02):
|
|||||||
|
|
||||||
|
|
||||||
def popup_codes(katrain):
|
def popup_codes(katrain):
|
||||||
return [args[2] for args in katrain.calls if args and args[0] == "engine_recovery_popup"]
|
return [code for _message, code, _engine_type in katrain.events.failures]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -144,8 +150,7 @@ def test_reconnect_failure_opens_recovery_popup(monkeypatch, fast_backoff):
|
|||||||
|
|
||||||
# The popup is told this is a remote engine so it shows remote-
|
# The popup is told this is a remote engine so it shows remote-
|
||||||
# specific advice (check URL) instead of the local executable hints.
|
# specific advice (check URL) instead of the local executable hints.
|
||||||
popup_call = next(args for args in katrain.calls if args and args[0] == "engine_recovery_popup")
|
assert katrain.events.failures[0][2] == "remote"
|
||||||
assert popup_call[3] == "remote"
|
|
||||||
finally:
|
finally:
|
||||||
engine.shutdown()
|
engine.shutdown()
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user