v1.18.0 (#815)
Co-authored-by: Artur Tamazian <artur-tamazian> Co-authored-by: Javier Gonzalez <javierggt@yahoo.com>
This commit is contained in:
34 files changed
+2285
-464
No files matched your search
+9
-4
@@ -15,11 +15,16 @@
|
|||||||
|
|
||||||
The easiest way to install is probably [brew](https://brew.sh/). Simply run `brew install katrain` and it will download and install the latest pre-built .app, and also install katago if needed.
|
The easiest way to install is probably [brew](https://brew.sh/). Simply run `brew install katrain` and it will download and install the latest pre-built .app, and also install katago if needed.
|
||||||
|
|
||||||
You can also find downloadable .app files for macOS [here](https://github.com/sanderland/katrain/releases).
|
You can also find downloadable macOS installers [here](https://github.com/sanderland/katrain/releases). Recent releases include both Intel (`KaTrain-*-x86_64.dmg`) and Apple Silicon (`KaTrain-*-arm64.dmg`) installers, so download the one matching your Mac. Mount the `.dmg` and drag the `.app` to your Applications folder.
|
||||||
Simply download, unzip the file, mount the .dmg and drag the .app file to your application folder, everything is included.
|
|
||||||
The first time launching the application you may need to [control-click in finder to give permission for the 'unidentified' app to launch](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac). This is simply a result of Apple charging $99/year to developers to be 'identified'.
|
|
||||||
|
|
||||||
Users with the last generation M1 macs with different architecture should then `brew install katago` in addition to this. KaTrain will automatically detect this KataGo binary.
|
The first time you launch the app, macOS may block it as an app from an unknown developer.
|
||||||
|
Follow Apple's current guide [here](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac):
|
||||||
|
1. Try opening the app once from Finder.
|
||||||
|
2. Open `System Settings > Privacy & Security`.
|
||||||
|
3. In the Security section, click `Open` and then `Open Anyway` for KaTrain.
|
||||||
|
4. Enter your password to confirm.
|
||||||
|
|
||||||
|
This is simply a result of Apple charging $99/year to developers to be 'identified'.
|
||||||
|
|
||||||
### <a name="MacCommand"></a>Command line install guide
|
### <a name="MacCommand"></a>Command line install guide
|
||||||
|
|
||||||
|
|||||||
+38
-9
@@ -29,6 +29,12 @@ else:
|
|||||||
Config.set("kivy", "window_icon", ICON)
|
Config.set("kivy", "window_icon", ICON)
|
||||||
Config.set("input", "mouse", "mouse,multitouch_on_demand")
|
Config.set("input", "mouse", "mouse,multitouch_on_demand")
|
||||||
|
|
||||||
|
# Preload sounds before Window import creates the SDL2 window,
|
||||||
|
# as SoundLoader.load() deadlocks once the SDL2 window exists.
|
||||||
|
from katrain.gui.sound import preload_sounds
|
||||||
|
|
||||||
|
preload_sounds(PATHS["PACKAGE"] + "/sounds")
|
||||||
|
|
||||||
# next, certificates on package builds https://github.com/sanderland/katrain/issues/414
|
# next, certificates on package builds https://github.com/sanderland/katrain/issues/414
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
import ssl
|
import ssl
|
||||||
@@ -93,7 +99,7 @@ from katrain.gui.popups import (
|
|||||||
)
|
)
|
||||||
from katrain.gui.sound import play_sound
|
from katrain.gui.sound import play_sound
|
||||||
from katrain.core.base_katrain import KaTrainBase
|
from katrain.core.base_katrain import KaTrainBase
|
||||||
from katrain.core.engine import KataGoEngine
|
from katrain.core.remote_engine import make_engine
|
||||||
from katrain.core.contribute_engine import KataGoContributeEngine
|
from katrain.core.contribute_engine import KataGoContributeEngine
|
||||||
from katrain.core.game import Game, IllegalMoveException, KaTrainSGF, BaseGame
|
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
|
||||||
@@ -187,7 +193,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
if self.engine:
|
if self.engine:
|
||||||
return
|
return
|
||||||
self.board_gui.trainer_config = self.config("trainer")
|
self.board_gui.trainer_config = self.config("trainer")
|
||||||
self.engine = KataGoEngine(self, self.config("engine"))
|
self.engine = make_engine(self, self.config("engine"))
|
||||||
threading.Thread(target=self._message_loop_thread, daemon=True).start()
|
threading.Thread(target=self._message_loop_thread, daemon=True).start()
|
||||||
sgf_args = [
|
sgf_args = [
|
||||||
f
|
f
|
||||||
@@ -207,6 +213,20 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
|
|
||||||
MDApp.get_running_app().root_window.bind(focus=set_focus_event)
|
MDApp.get_running_app().root_window.bind(focus=set_focus_event)
|
||||||
|
|
||||||
|
def restart_engine(self):
|
||||||
|
"""Rebuild the analysis engine from current config and re-analyze.
|
||||||
|
Shared by the engine-settings save and the recovery popup's Retry."""
|
||||||
|
self.log("Restarting engine", OUTPUT_INFO)
|
||||||
|
self.controls.set_status(i18n._("restarting engine"), STATUS_INFO)
|
||||||
|
old_engine = self.engine
|
||||||
|
old_engine.shutdown(finish=False)
|
||||||
|
new_engine = make_engine(self, self.config("engine"))
|
||||||
|
self.engine = new_engine
|
||||||
|
self.game.engines = {"B": new_engine, "W": new_engine}
|
||||||
|
# old engine was possibly broken, so make sure we redo any failures
|
||||||
|
self.game.analyze_all_nodes(analyze_fast=True)
|
||||||
|
self.update_state()
|
||||||
|
|
||||||
def update_gui(self, cn, redraw_board=False):
|
def update_gui(self, cn, redraw_board=False):
|
||||||
# Handle prisoners and next player display
|
# Handle prisoners and next player display
|
||||||
prisoners = self.game.prisoner_count
|
prisoners = self.game.prisoner_count
|
||||||
@@ -226,7 +246,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
self.controls.players["B"].captures = prisoners["B"]
|
self.controls.players["B"].captures = prisoners["B"]
|
||||||
|
|
||||||
# update engine status dot
|
# update engine status dot
|
||||||
if not self.engine or not self.engine.katago_process or self.engine.katago_process.poll() is not None:
|
if not self.engine or not self.engine.check_alive():
|
||||||
self.board_controls.engine_status_col = Theme.ENGINE_DOWN_COLOR
|
self.board_controls.engine_status_col = Theme.ENGINE_DOWN_COLOR
|
||||||
elif self.engine.is_idle():
|
elif self.engine.is_idle():
|
||||||
self.board_controls.engine_status_col = Theme.ENGINE_READY_COLOR
|
self.board_controls.engine_status_col = Theme.ENGINE_READY_COLOR
|
||||||
@@ -512,7 +532,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
self.ai_settings_popup.content.popup = self.ai_settings_popup
|
self.ai_settings_popup.content.popup = self.ai_settings_popup
|
||||||
self.ai_settings_popup.open()
|
self.ai_settings_popup.open()
|
||||||
|
|
||||||
def _do_engine_recovery_popup(self, error_message, code):
|
def _do_engine_recovery_popup(self, error_message, code, engine_type="local"):
|
||||||
current_open = self.popup_open
|
current_open = self.popup_open
|
||||||
if current_open and isinstance(current_open.content, EngineRecoveryPopup):
|
if current_open and isinstance(current_open.content, EngineRecoveryPopup):
|
||||||
self.log(f"Not opening engine recovery popup with {error_message} as one is already open", OUTPUT_DEBUG)
|
self.log(f"Not opening engine recovery popup with {error_message} as one is already open", OUTPUT_DEBUG)
|
||||||
@@ -520,7 +540,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
|||||||
popup = I18NPopup(
|
popup = I18NPopup(
|
||||||
title_key="engine recovery",
|
title_key="engine recovery",
|
||||||
size=[dp(600), dp(700)],
|
size=[dp(600), dp(700)],
|
||||||
content=EngineRecoveryPopup(self, error_message=error_message, code=code),
|
content=EngineRecoveryPopup(self, error_message=error_message, code=code, engine_type=engine_type),
|
||||||
).__self__
|
).__self__
|
||||||
popup.content.popup = popup
|
popup.content.popup = popup
|
||||||
popup.open()
|
popup.open()
|
||||||
@@ -845,14 +865,19 @@ class KaTrainApp(MDApp):
|
|||||||
def is_valid_window_position(self, left, top, width, height):
|
def is_valid_window_position(self, left, top, width, height):
|
||||||
try:
|
try:
|
||||||
from screeninfo import get_monitors
|
from screeninfo import get_monitors
|
||||||
|
|
||||||
monitors = get_monitors()
|
monitors = get_monitors()
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
if (left >= monitor.x and left + width <= monitor.x + monitor.width and
|
if (
|
||||||
top >= monitor.y and top + height <= monitor.y + monitor.height):
|
left >= monitor.x
|
||||||
|
and left + width <= monitor.x + monitor.width
|
||||||
|
and top >= monitor.y
|
||||||
|
and top + height <= monitor.y + monitor.height
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return True # yolo
|
return True # yolo
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.icon = ICON # how you're supposed to set an icon
|
self.icon = ICON # how you're supposed to set an icon
|
||||||
@@ -905,7 +930,11 @@ class KaTrainApp(MDApp):
|
|||||||
win_size = [1300 * window_scale_fac, 1000 * window_scale_fac]
|
win_size = [1300 * window_scale_fac, 1000 * window_scale_fac]
|
||||||
self.gui.log(f"Setting window size to {win_size} and position to {[win_left, win_top]}", OUTPUT_DEBUG)
|
self.gui.log(f"Setting window size to {win_size} and position to {[win_left, win_top]}", OUTPUT_DEBUG)
|
||||||
Window.size = (win_size[0], win_size[1])
|
Window.size = (win_size[0], win_size[1])
|
||||||
if win_left is not None and win_top is not None and self.is_valid_window_position(win_left, win_top, win_size[0], win_size[1]):
|
if (
|
||||||
|
win_left is not None
|
||||||
|
and win_top is not None
|
||||||
|
and self.is_valid_window_position(win_left, win_top, win_size[0], win_size[1])
|
||||||
|
):
|
||||||
Window.left = win_left
|
Window.left = win_left
|
||||||
Window.top = win_top
|
Window.top = win_top
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"engine": {
|
"engine": {
|
||||||
|
"backend": "",
|
||||||
"katago": "",
|
"katago": "",
|
||||||
"altcommand": "",
|
"altcommand": "",
|
||||||
|
"remote_url": "",
|
||||||
"model": "katrain/models/kata1-b18c384nbt-s9996604416-d4316597426.bin.gz",
|
"model": "katrain/models/kata1-b18c384nbt-s9996604416-d4316597426.bin.gz",
|
||||||
"humanlike_model": "",
|
"humanlike_model": "",
|
||||||
"config": "katrain/KataGo/analysis_config.cfg",
|
"config": "katrain/KataGo/analysis_config.cfg",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
PROGRAM_NAME = "KaTrain"
|
PROGRAM_NAME = "KaTrain"
|
||||||
VERSION = "1.17.1"
|
VERSION = "1.18.0"
|
||||||
HOMEPAGE = "https://github.com/sanderland/katrain"
|
HOMEPAGE = "https://github.com/sanderland/katrain"
|
||||||
CONFIG_MIN_VERSION = "1.17.0" # keep config files from this version
|
CONFIG_MIN_VERSION = "1.17.0" # keep config files from this version
|
||||||
ANALYSIS_FORMAT_VERSION = "1.0"
|
ANALYSIS_FORMAT_VERSION = "1.0"
|
||||||
|
|||||||
+22
-2
@@ -26,6 +26,25 @@ from katrain.core.lang import i18n
|
|||||||
from katrain.core.sgf_parser import Move
|
from katrain.core.sgf_parser import Move
|
||||||
from katrain.core.utils import find_package_resource, json_truncate_arrays
|
from katrain.core.utils import find_package_resource, json_truncate_arrays
|
||||||
|
|
||||||
|
ENGINE_BACKENDS = ("local", "remote", "custom")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_engine_backend(config) -> str:
|
||||||
|
"""Which engine backend to use: 'local', 'remote' or 'custom'.
|
||||||
|
|
||||||
|
Uses the explicit `engine/backend` setting when present (set by the engine
|
||||||
|
settings tabs). Falls back to deriving it from the legacy fields so older
|
||||||
|
configs keep working: a remote_url means remote, an altcommand means custom.
|
||||||
|
"""
|
||||||
|
backend = (config.get("backend") or "").strip().lower()
|
||||||
|
if backend in ENGINE_BACKENDS:
|
||||||
|
return backend
|
||||||
|
if (config.get("remote_url") or "").strip():
|
||||||
|
return "remote"
|
||||||
|
if (config.get("altcommand") or "").strip():
|
||||||
|
return "custom"
|
||||||
|
return "local"
|
||||||
|
|
||||||
|
|
||||||
class BaseEngine: # some common elements between analysis and contribute engine
|
class BaseEngine: # some common elements between analysis and contribute engine
|
||||||
|
|
||||||
@@ -95,6 +114,7 @@ class KataGoEngine(BaseEngine):
|
|||||||
"""Starts and communicates with the KataGO analysis engine"""
|
"""Starts and communicates with the KataGO analysis engine"""
|
||||||
|
|
||||||
PONDER_KEY = "_kt_continuous"
|
PONDER_KEY = "_kt_continuous"
|
||||||
|
ENGINE_TYPE = "local" # passed to the recovery popup so it can tailor its advice
|
||||||
|
|
||||||
def __init__(self, katrain, config):
|
def __init__(self, katrain, config):
|
||||||
super().__init__(katrain, config)
|
super().__init__(katrain, config)
|
||||||
@@ -112,7 +132,7 @@ class KataGoEngine(BaseEngine):
|
|||||||
self.shell = False
|
self.shell = False
|
||||||
self.write_queue = queue.Queue()
|
self.write_queue = queue.Queue()
|
||||||
self.thread_lock = threading.Lock()
|
self.thread_lock = threading.Lock()
|
||||||
if config.get("altcommand", ""):
|
if resolve_engine_backend(config) == "custom":
|
||||||
self.command = config["altcommand"]
|
self.command = config["altcommand"]
|
||||||
self.shell = True
|
self.shell = True
|
||||||
else:
|
else:
|
||||||
@@ -146,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.katrain("engine_recovery_popup", message, code, self.ENGINE_TYPE)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
with self.thread_lock:
|
with self.thread_lock:
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
"""Remote KataGo Analysis Engine over WebSocket.
|
||||||
|
|
||||||
|
When `engine.remote_url` is set in the config, KaTrain runs queries
|
||||||
|
against a remote KataGo Analysis Engine instead of spawning a local
|
||||||
|
subprocess. The server is expected to expose the KataGo Analysis
|
||||||
|
Engine JSON protocol over a WebSocket.
|
||||||
|
|
||||||
|
Generic transport — any KataGo-compatible server works. KaTrain has
|
||||||
|
no awareness of who hosts the engine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
from websocket import ( # provided by `websocket-client`
|
||||||
|
ABNF,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketException,
|
||||||
|
WebSocketTimeoutException,
|
||||||
|
create_connection,
|
||||||
|
)
|
||||||
|
|
||||||
|
from katrain.core.constants import (
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
OUTPUT_EXTRA_DEBUG,
|
||||||
|
OUTPUT_INFO,
|
||||||
|
STATUS_INFO,
|
||||||
|
)
|
||||||
|
from katrain.core.engine import BaseEngine, KataGoEngine, resolve_engine_backend
|
||||||
|
from katrain.core.lang import i18n
|
||||||
|
from katrain.core.utils import json_truncate_arrays
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteKataGoEngine(KataGoEngine):
|
||||||
|
"""KataGo engine that talks to a remote Analysis Engine server
|
||||||
|
over a WebSocket instead of spawning a local subprocess.
|
||||||
|
|
||||||
|
Activated when `engine.remote_url` in the config is non-empty.
|
||||||
|
The URL must be `ws://...` or `wss://...`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ENGINE_TYPE = "remote" # recovery popup shows remote-specific advice (check URL, not executable)
|
||||||
|
|
||||||
|
READ_TIMEOUT_S = 120
|
||||||
|
|
||||||
|
# A dropped WebSocket is recoverable (server restart, flaky network),
|
||||||
|
# unlike a crashed local subprocess. Reconnect transparently with
|
||||||
|
# linear backoff before falling back to the recovery popup.
|
||||||
|
RECONNECT_ATTEMPTS = 6
|
||||||
|
RECONNECT_BACKOFF_S = 1.0
|
||||||
|
RECONNECT_MAX_BACKOFF_S = 10.0
|
||||||
|
|
||||||
|
def __init__(self, katrain, config):
|
||||||
|
# Bypass KataGoEngine.__init__'s subprocess setup; initialise
|
||||||
|
# only the bookkeeping the rest of KaTrain reads.
|
||||||
|
BaseEngine.__init__(self, katrain, config)
|
||||||
|
self.allow_recovery = self.config.get("allow_recovery", True)
|
||||||
|
self.queries = {}
|
||||||
|
# query id -> JSON payload, kept so outstanding analyses can be
|
||||||
|
# re-sent after a reconnect (the server loses them on disconnect).
|
||||||
|
self.sent_payloads = {}
|
||||||
|
self.ponder_query = None
|
||||||
|
self.query_counter = 0
|
||||||
|
self.katago_process = None # rest of the codebase checks this
|
||||||
|
self.base_priority = 0
|
||||||
|
self.override_settings = {"reportAnalysisWinratesAs": "BLACK"}
|
||||||
|
self.write_queue = queue.Queue()
|
||||||
|
self.thread_lock = threading.Lock()
|
||||||
|
self.shell = False
|
||||||
|
self.command = "<remote websocket>"
|
||||||
|
|
||||||
|
self.remote_url = (config.get("remote_url") or "").strip()
|
||||||
|
if not self.remote_url:
|
||||||
|
self.on_error(
|
||||||
|
i18n._("Remote KataGo URL is empty"),
|
||||||
|
"REMOTE-URL-MISSING",
|
||||||
|
allow_popup=False,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not self.remote_url.startswith(("ws://", "wss://")):
|
||||||
|
self.on_error(
|
||||||
|
i18n._("Remote KataGo URL must start with ws:// or wss://"),
|
||||||
|
"REMOTE-URL-INVALID",
|
||||||
|
allow_popup=False,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ws: WebSocket | None = None
|
||||||
|
self.ws_send_lock = threading.Lock()
|
||||||
|
self.analysis_thread = None
|
||||||
|
self.write_stdin_thread = None
|
||||||
|
self.stderr_thread = None
|
||||||
|
self._closing = False
|
||||||
|
self._reported_dead = False
|
||||||
|
self._reconnecting = False
|
||||||
|
# Bumped on each (re)connect. I/O threads capture the value they
|
||||||
|
# were started with so a stale thread blocked on a dead socket
|
||||||
|
# can't trigger a reconnect that would tear down a newer one.
|
||||||
|
self._conn_id = 0
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _create_connection(self) -> WebSocket:
|
||||||
|
# macOS's bundled Python may have no configured CA bundle,
|
||||||
|
# so provide certifi explicitly for secure WebSockets.
|
||||||
|
sslopt = {"ca_certs": certifi.where()} if self.remote_url.startswith("wss://") else None
|
||||||
|
return create_connection(
|
||||||
|
self.remote_url,
|
||||||
|
timeout=self.READ_TIMEOUT_S,
|
||||||
|
enable_multithread=True,
|
||||||
|
sslopt=sslopt,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _start_threads(self):
|
||||||
|
"""Bump the connection generation and launch fresh I/O threads
|
||||||
|
bound to it. Must be called while holding thread_lock."""
|
||||||
|
self._conn_id += 1
|
||||||
|
conn_id = self._conn_id
|
||||||
|
self.analysis_thread = threading.Thread(
|
||||||
|
target=self._analysis_read_thread,
|
||||||
|
args=(conn_id,),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self.write_stdin_thread = threading.Thread(
|
||||||
|
target=self._write_stdin_thread,
|
||||||
|
args=(conn_id,),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
# Dummy stderr thread so callers that join all three don't
|
||||||
|
# crash on None — remote engines have no stderr channel.
|
||||||
|
self.stderr_thread = threading.Thread(
|
||||||
|
target=lambda: None,
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self.analysis_thread.start()
|
||||||
|
self.write_stdin_thread.start()
|
||||||
|
self.stderr_thread.start()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
with self.thread_lock:
|
||||||
|
self.write_queue = queue.Queue()
|
||||||
|
self._closing = False
|
||||||
|
self._reported_dead = False
|
||||||
|
self._reconnecting = False
|
||||||
|
try:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Connecting to remote KataGo at {self.remote_url}",
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
)
|
||||||
|
self.ws = self._create_connection()
|
||||||
|
except Exception as e:
|
||||||
|
self.on_error(
|
||||||
|
i18n._("Connecting to remote KataGo failed").format(
|
||||||
|
url=self.remote_url,
|
||||||
|
error=e,
|
||||||
|
),
|
||||||
|
code="REMOTE-CONNECT",
|
||||||
|
)
|
||||||
|
self.ws = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._start_threads()
|
||||||
|
|
||||||
|
def _handle_disconnect(self, os_error="", conn_id=None):
|
||||||
|
"""Called by the read/write threads when the WebSocket drops.
|
||||||
|
Kicks off a single background reconnect attempt instead of
|
||||||
|
immediately surfacing the (local-engine-oriented) recovery popup.
|
||||||
|
|
||||||
|
`conn_id` is the generation the calling thread was started with;
|
||||||
|
a mismatch means a newer connection already exists and this is a
|
||||||
|
stale thread, so we ignore it.
|
||||||
|
"""
|
||||||
|
with self.thread_lock:
|
||||||
|
if self._closing or self._reconnecting:
|
||||||
|
return
|
||||||
|
if conn_id is not None and conn_id != self._conn_id:
|
||||||
|
return
|
||||||
|
self._reconnecting = True
|
||||||
|
old_ws, self.ws = self.ws, None
|
||||||
|
# Closing the dead socket unblocks the sibling thread still
|
||||||
|
# parked in recv() so it exits promptly rather than hanging.
|
||||||
|
if old_ws is not None:
|
||||||
|
try:
|
||||||
|
old_ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
threading.Thread(
|
||||||
|
target=self._reconnect_thread,
|
||||||
|
args=(os_error,),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def _reconnect_thread(self, os_error=""):
|
||||||
|
"""Try to re-establish the WebSocket with linear backoff. On
|
||||||
|
success, relaunch the I/O threads and re-send outstanding
|
||||||
|
queries. Only if every attempt fails do we report the engine as
|
||||||
|
dead (which opens the recovery popup)."""
|
||||||
|
reconnected = False
|
||||||
|
try:
|
||||||
|
for attempt in range(1, self.RECONNECT_ATTEMPTS + 1):
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
|
delay = min(self.RECONNECT_BACKOFF_S * attempt, self.RECONNECT_MAX_BACKOFF_S)
|
||||||
|
self.katrain.log(
|
||||||
|
f"Remote KataGo disconnected ({os_error}); "
|
||||||
|
f"reconnect attempt {attempt}/{self.RECONNECT_ATTEMPTS} in {delay:.0f}s",
|
||||||
|
OUTPUT_INFO,
|
||||||
|
)
|
||||||
|
self._set_status(f"Reconnecting to remote KataGo (attempt {attempt}/{self.RECONNECT_ATTEMPTS})...")
|
||||||
|
time.sleep(delay)
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ws = self._create_connection()
|
||||||
|
except Exception as e:
|
||||||
|
os_error = str(e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
with self.thread_lock:
|
||||||
|
if self._closing:
|
||||||
|
try:
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
self.ws = ws
|
||||||
|
self._reported_dead = False
|
||||||
|
self._start_threads()
|
||||||
|
self.katrain.log("Reconnected to remote KataGo", OUTPUT_INFO)
|
||||||
|
self._set_status("Reconnected to remote KataGo.")
|
||||||
|
self._resend_outstanding()
|
||||||
|
reconnected = True
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
# Keep _reconnecting set across the failure report below so
|
||||||
|
# external pollers (check_alive) stay quiet and can't latch
|
||||||
|
# _reported_dead in a way that suppresses the popup. Only clear
|
||||||
|
# it here on success/shutdown.
|
||||||
|
if reconnected or self._closing:
|
||||||
|
self._reconnecting = False
|
||||||
|
|
||||||
|
# Every attempt failed and we are not shutting down: report dead and
|
||||||
|
# open the recovery popup. _report_dead is idempotent, so a later
|
||||||
|
# poll from check_alive won't double-report.
|
||||||
|
if not self._closing:
|
||||||
|
self._report_dead(os_error, allow_popup=True)
|
||||||
|
self._reconnecting = False
|
||||||
|
|
||||||
|
def _resend_outstanding(self):
|
||||||
|
"""Re-send queries that were in flight when the connection
|
||||||
|
dropped. Keyed off self.queries so anything cleared by a new
|
||||||
|
game or an explicit terminate is not resurrected."""
|
||||||
|
with self.thread_lock:
|
||||||
|
payloads = [self.sent_payloads[qid] for qid in self.queries if qid in self.sent_payloads]
|
||||||
|
if not payloads:
|
||||||
|
return
|
||||||
|
self.katrain.log(
|
||||||
|
f"Re-sending {len(payloads)} outstanding queries after reconnect",
|
||||||
|
OUTPUT_INFO,
|
||||||
|
)
|
||||||
|
for query in payloads:
|
||||||
|
ws = self.ws
|
||||||
|
if ws is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with self.ws_send_lock:
|
||||||
|
ws.send(json.dumps(query))
|
||||||
|
except Exception as e:
|
||||||
|
self.katrain.log(f"Failed to re-send query after reconnect: {e}", OUTPUT_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _set_status(self, message):
|
||||||
|
try:
|
||||||
|
self.katrain.controls.set_status(message, STATUS_INFO)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_new_game(self):
|
||||||
|
# Parent clears self.queries; drop the payloads too so a later
|
||||||
|
# reconnect doesn't resurrect a previous game's analyses.
|
||||||
|
super().on_new_game()
|
||||||
|
self.sent_payloads = {}
|
||||||
|
|
||||||
|
def shutdown(self, finish=False):
|
||||||
|
self._closing = True
|
||||||
|
ws = self.ws
|
||||||
|
if finish and ws is not None:
|
||||||
|
self.wait_to_finish()
|
||||||
|
self.ws = None
|
||||||
|
self.sent_payloads = {}
|
||||||
|
if ws is not None:
|
||||||
|
try:
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if finish is not None:
|
||||||
|
for t in [self.write_stdin_thread, self.analysis_thread, self.stderr_thread]:
|
||||||
|
if t and t.is_alive():
|
||||||
|
t.join(timeout=2.0)
|
||||||
|
|
||||||
|
def wait_to_finish(self):
|
||||||
|
while self.queries and self.ws is not None:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def _report_dead(self, os_error, allow_popup):
|
||||||
|
"""Report the engine as disconnected exactly once. Guarded by
|
||||||
|
thread_lock + _reported_dead so a poll from check_alive and the
|
||||||
|
reconnect thread's final failure report can't double-fire or race."""
|
||||||
|
with self.thread_lock:
|
||||||
|
if self._reported_dead:
|
||||||
|
return
|
||||||
|
self._reported_dead = True
|
||||||
|
self.ws = None
|
||||||
|
self.on_error(
|
||||||
|
i18n._("Remote KataGo engine disconnected").format(error=os_error),
|
||||||
|
code="REMOTE-DISCONNECTED",
|
||||||
|
allow_popup=allow_popup,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_alive(self, os_error="", exception_if_dead=False, maybe_open_recovery=False):
|
||||||
|
# An in-progress auto-reconnect is recovery, not death. Callers that
|
||||||
|
# poll while the socket is briefly None (the AI move loops in ai.py
|
||||||
|
# spin on check_alive every 10ms) must not flag the engine dead.
|
||||||
|
# _reconnect_thread keeps _reconnecting set until it has reported a
|
||||||
|
# genuine failure, so the popup is never suppressed nor premature.
|
||||||
|
if self._reconnecting and not self._closing:
|
||||||
|
return True
|
||||||
|
ok = self.ws is not None and not self._closing
|
||||||
|
if not ok and exception_if_dead:
|
||||||
|
self._report_dead(os_error, allow_popup=maybe_open_recovery)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
def _read_stderr_thread(self):
|
||||||
|
# Remote engines have no stderr channel; warnings come via the
|
||||||
|
# `warning` field on responses. Override prevents the parent's
|
||||||
|
# stderr thread from reading from a None process.
|
||||||
|
return
|
||||||
|
|
||||||
|
def _write_stdin_thread(self, conn_id):
|
||||||
|
"""Pop (query, callback, error_callback, next_move, node)
|
||||||
|
tuples off write_queue, register the callback in self.queries
|
||||||
|
so the read thread can match responses by id, then send the
|
||||||
|
JSON query over the WebSocket. Ponder dedupe lives inside the
|
||||||
|
lock so rapid Ponder presses don't queue duplicate analyses.
|
||||||
|
|
||||||
|
Bound to the connection generation `conn_id`: once a reconnect
|
||||||
|
supersedes it, this thread stops and a fresh one takes over.
|
||||||
|
"""
|
||||||
|
ws = self.ws
|
||||||
|
while ws is not None and not self._closing and conn_id == self._conn_id:
|
||||||
|
try:
|
||||||
|
query, callback, error_callback, next_move, node = self.write_queue.get(
|
||||||
|
block=True,
|
||||||
|
timeout=0.1,
|
||||||
|
)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
if self._closing or conn_id != self._conn_id:
|
||||||
|
# Superseded mid-pop: hand the item back so the current
|
||||||
|
# generation's writer sends it after reconnecting.
|
||||||
|
self.write_queue.put((query, callback, error_callback, next_move, node))
|
||||||
|
return
|
||||||
|
with self.thread_lock:
|
||||||
|
if "id" not in query:
|
||||||
|
self.query_counter += 1
|
||||||
|
query["id"] = f"QUERY:{self.query_counter}"
|
||||||
|
|
||||||
|
ponder = query.pop(self.PONDER_KEY, False)
|
||||||
|
if ponder:
|
||||||
|
pq = self.ponder_query or {}
|
||||||
|
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:
|
||||||
|
self.stop_pondering()
|
||||||
|
query["maxVisits"] = 10_000_000
|
||||||
|
from katrain.core.constants import PONDERING_REPORT_DT
|
||||||
|
|
||||||
|
query["reportDuringSearchEvery"] = PONDERING_REPORT_DT
|
||||||
|
self.ponder_query = query
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
terminate = query.get("action") == "terminate"
|
||||||
|
if not terminate:
|
||||||
|
self.queries[query["id"]] = (
|
||||||
|
callback,
|
||||||
|
error_callback,
|
||||||
|
time.time(),
|
||||||
|
next_move,
|
||||||
|
node,
|
||||||
|
)
|
||||||
|
self.sent_payloads[query["id"]] = query
|
||||||
|
tag = "ponder " if ponder else ("terminate " if terminate else "")
|
||||||
|
self.katrain.log(
|
||||||
|
f"Sending {tag}query {query['id']}: {json.dumps(query)}",
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = json.dumps(query)
|
||||||
|
with self.ws_send_lock:
|
||||||
|
ws.send(payload)
|
||||||
|
except WebSocketException as e:
|
||||||
|
self._handle_disconnect(os_error=str(e), conn_id=conn_id)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Unexpected exception sending to remote KataGo: {e}",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
self._handle_disconnect(os_error=str(e), conn_id=conn_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _analysis_read_thread(self, conn_id):
|
||||||
|
"""Read JSON responses from the WebSocket and dispatch to the
|
||||||
|
matching callbacks in self.queries.
|
||||||
|
|
||||||
|
Bound to the connection generation `conn_id` and to the socket
|
||||||
|
captured at start, so a thread left over from a previous
|
||||||
|
connection never reads from (or tears down) a newer one.
|
||||||
|
"""
|
||||||
|
ws = self.ws
|
||||||
|
while ws is not None and not self._closing and conn_id == self._conn_id:
|
||||||
|
try:
|
||||||
|
# recv_data(control_frame=True) lets us inspect non-text frames
|
||||||
|
# (e.g. close, whose payload carries the status code + reason).
|
||||||
|
opcode, data = ws.recv_data(control_frame=True)
|
||||||
|
except WebSocketTimeoutException:
|
||||||
|
continue
|
||||||
|
except WebSocketException as e:
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
|
self._handle_disconnect(os_error=str(e), conn_id=conn_id)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
|
self.katrain.log(
|
||||||
|
f"Unexpected exception reading from remote KataGo: {e}",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
self._handle_disconnect(os_error=str(e), conn_id=conn_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
if not self._closing:
|
||||||
|
# RFC 6455 close payload: 2-byte status code + UTF-8 reason.
|
||||||
|
reason = "closed by remote"
|
||||||
|
if data and len(data) >= 2:
|
||||||
|
reason = data[2:].decode("utf-8", errors="replace").strip() or reason
|
||||||
|
self._handle_disconnect(os_error=reason, conn_id=conn_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if opcode not in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
# ping/pong/continuation — nothing to dispatch.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw = data.decode("utf-8") if isinstance(data, bytes) else data
|
||||||
|
|
||||||
|
# A frame may contain multiple newline-delimited JSON
|
||||||
|
# objects (matches KataGo's stdio framing).
|
||||||
|
for line in str(raw).splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
self._dispatch_response_line(line)
|
||||||
|
|
||||||
|
def _dispatch_response_line(self, line: str) -> None:
|
||||||
|
"""Parse one JSON response line and dispatch to the matching
|
||||||
|
query callback. Warning text is mirrored to the UI status
|
||||||
|
panel so users see notices like "visits capped" without
|
||||||
|
reading the log."""
|
||||||
|
try:
|
||||||
|
analysis = json.loads(line)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Bad JSON from remote KataGo: {e} (line: {line[:200]!r})",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "id" not in analysis:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Error without ID {analysis} received from remote KataGo",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
query_id = analysis["id"]
|
||||||
|
if query_id not in self.queries:
|
||||||
|
if analysis.get("action") != "terminate":
|
||||||
|
self.katrain.log(
|
||||||
|
f"Query result {query_id} discarded -- recent new game or node reset?",
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
callback, error_callback, start_time, next_move, _ = self.queries[query_id]
|
||||||
|
|
||||||
|
# Handled BEFORE the dispatch chain so analysis data
|
||||||
|
# alongside a warning still reaches the callback.
|
||||||
|
if "warning" in analysis:
|
||||||
|
warning_text = str(analysis.get("warning"))
|
||||||
|
self.katrain.log(
|
||||||
|
f"Remote KataGo warning: {warning_text}",
|
||||||
|
OUTPUT_INFO,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.katrain.controls.set_status(warning_text, STATUS_INFO)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "error" in analysis:
|
||||||
|
del self.queries[query_id]
|
||||||
|
self.sent_payloads.pop(query_id, None)
|
||||||
|
if error_callback:
|
||||||
|
error_callback(analysis)
|
||||||
|
elif not (next_move and "Illegal move" in analysis["error"]):
|
||||||
|
self.katrain.log(
|
||||||
|
f"{analysis} received from remote KataGo",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
elif "terminateId" in analysis:
|
||||||
|
self.katrain.log(
|
||||||
|
f"{analysis} received from remote KataGo",
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
partial_result = analysis.get("isDuringSearch", False)
|
||||||
|
if not partial_result:
|
||||||
|
del self.queries[query_id]
|
||||||
|
self.sent_payloads.pop(query_id, None)
|
||||||
|
time_taken = time.time() - start_time
|
||||||
|
results_exist = not analysis.get("noResults", False)
|
||||||
|
self.katrain.log(
|
||||||
|
f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] "
|
||||||
|
f"KataGo analysis received: {len(analysis.get('moveInfos', []))} "
|
||||||
|
f"candidate moves, "
|
||||||
|
f"{analysis['rootInfo']['visits'] if results_exist else 'n/a'} visits",
|
||||||
|
OUTPUT_DEBUG,
|
||||||
|
)
|
||||||
|
self.katrain.log(json_truncate_arrays(analysis), OUTPUT_EXTRA_DEBUG)
|
||||||
|
try:
|
||||||
|
if callback and results_exist:
|
||||||
|
callback(analysis, partial_result)
|
||||||
|
except Exception as e:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Error in engine callback for query {query_id}: {e}",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if getattr(self.katrain, "update_state", None):
|
||||||
|
self.katrain.update_state()
|
||||||
|
except Exception as e:
|
||||||
|
self.katrain.log(
|
||||||
|
f"Unexpected exception {e} processing remote KataGo output: {line[:200]!r}",
|
||||||
|
OUTPUT_ERROR,
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def make_engine(katrain, config):
|
||||||
|
"""Return the engine matching the selected backend (see resolve_engine_backend):
|
||||||
|
a RemoteKataGoEngine for the remote backend, otherwise a local-subprocess
|
||||||
|
KataGoEngine (which itself handles the local vs custom-command distinction)."""
|
||||||
|
if resolve_engine_backend(config) == "remote":
|
||||||
|
return RemoteKataGoEngine(katrain, config)
|
||||||
|
return KataGoEngine(katrain, config)
|
||||||
+62
-27
@@ -24,6 +24,7 @@ from kivymd.uix.selectioncontrol import MDCheckbox
|
|||||||
from kivymd.uix.textfield import MDTextField
|
from kivymd.uix.textfield import MDTextField
|
||||||
|
|
||||||
from katrain.core.ai import ai_rank_estimation, game_report
|
from katrain.core.ai import ai_rank_estimation, game_report
|
||||||
|
from katrain.core.engine import resolve_engine_backend
|
||||||
from katrain.core.constants import (
|
from katrain.core.constants import (
|
||||||
AI_CONFIG_DEFAULT,
|
AI_CONFIG_DEFAULT,
|
||||||
AI_DEFAULT,
|
AI_DEFAULT,
|
||||||
@@ -39,7 +40,6 @@ from katrain.core.constants import (
|
|||||||
PLAYER_HUMAN,
|
PLAYER_HUMAN,
|
||||||
ADDITIONAL_MOVE_ORDER,
|
ADDITIONAL_MOVE_ORDER,
|
||||||
)
|
)
|
||||||
from katrain.core.engine import KataGoEngine
|
|
||||||
from katrain.core.lang import i18n, rank_label
|
from katrain.core.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
|
||||||
@@ -455,11 +455,36 @@ class ConfigAIPopup(QuickConfigGui):
|
|||||||
class EngineRecoveryPopup(QuickConfigGui):
|
class EngineRecoveryPopup(QuickConfigGui):
|
||||||
error_message = StringProperty("")
|
error_message = StringProperty("")
|
||||||
code = ObjectProperty(None)
|
code = ObjectProperty(None)
|
||||||
|
engine_type = StringProperty("local")
|
||||||
|
recovery_message = StringProperty("")
|
||||||
|
|
||||||
def __init__(self, katrain, error_message, code):
|
def __init__(self, katrain, error_message, code, engine_type="local"):
|
||||||
super().__init__(katrain)
|
super().__init__(katrain)
|
||||||
self.error_message = str(error_message)
|
self.error_message = str(error_message)
|
||||||
self.code = code
|
self.code = code
|
||||||
|
self.engine_type = engine_type or "local"
|
||||||
|
self.recovery_message = self._build_message()
|
||||||
|
|
||||||
|
def _build_message(self):
|
||||||
|
settings_link = "[color=#CCCC11][u][ref=engine_settings]" + i18n._("menu:settings") + "[/ref][/u][/color]"
|
||||||
|
help_link = "[color=#CCCC11][u][ref=engine_help]" + i18n._("link_here") + "[/ref][/u][/color]"
|
||||||
|
if self.engine_type == "remote":
|
||||||
|
opening_key = "remote engine disconnected popup opening message"
|
||||||
|
suggestion = i18n._("remote engine check url suggestion").format(link=settings_link)
|
||||||
|
else:
|
||||||
|
opening_key = "engine died popup opening message"
|
||||||
|
suggestion = i18n._("change engine suggestion").format(link=settings_link)
|
||||||
|
opening = i18n._(opening_key).format(code=self.code, error_message=self.error_message)
|
||||||
|
help_text = i18n._("go to engine help page").format(link=help_link)
|
||||||
|
return opening + "\n\n" + suggestion + "\n\n" + help_text
|
||||||
|
|
||||||
|
def retry(self):
|
||||||
|
"""Rebuild the engine from current config and re-analyze. For a
|
||||||
|
remote engine this reconnects; for a local one it respawns the
|
||||||
|
subprocess. Recovers a transient failure without changing settings."""
|
||||||
|
if self.popup:
|
||||||
|
self.popup.dismiss()
|
||||||
|
Clock.schedule_once(lambda _dt: self.katrain.restart_engine(), 0)
|
||||||
|
|
||||||
|
|
||||||
class BaseConfigPopup(QuickConfigGui):
|
class BaseConfigPopup(QuickConfigGui):
|
||||||
@@ -481,23 +506,28 @@ class BaseConfigPopup(QuickConfigGui):
|
|||||||
|
|
||||||
KATAGOS = {
|
KATAGOS = {
|
||||||
"win": {
|
"win": {
|
||||||
"OpenCL v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-windows-x64.zip",
|
"OpenCL v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-windows-x64.zip",
|
||||||
"Eigen AVX2 (Modern CPUs) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigenavx2-windows-x64.zip",
|
"Eigen AVX2 (Modern CPUs) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigenavx2-windows-x64.zip",
|
||||||
"Eigen (CPU, Non-optimized) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigen-windows-x64.zip",
|
"Eigen (CPU, Non-optimized) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigen-windows-x64.zip",
|
||||||
"OpenCL v1.16.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-windows-x64+bs50.zip",
|
"OpenCL v1.16.5 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-windows-x64+bs50.zip",
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"OpenCL v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-linux-x64.zip",
|
"OpenCL v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-linux-x64.zip",
|
||||||
"Eigen AVX2 (Modern CPUs) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigenavx2-linux-x64.zip",
|
"Eigen AVX2 (Modern CPUs) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigenavx2-linux-x64.zip",
|
||||||
"Eigen (CPU, Non-optimized) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigen-linux-x64.zip",
|
"Eigen (CPU, Non-optimized) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigen-linux-x64.zip",
|
||||||
"OpenCL v1.16.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-linux-x64+bs50.zip",
|
"OpenCL v1.16.5 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-linux-x64+bs50.zip",
|
||||||
},
|
},
|
||||||
"just-descriptions": {},
|
"just-descriptions": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, katrain):
|
def __init__(self, katrain):
|
||||||
super().__init__(katrain)
|
super().__init__(katrain)
|
||||||
self.paths = [self.katrain.config("engine/model"), self.katrain.config("engine/humanlike_model"), "katrain/models", DATA_FOLDER]
|
self.paths = [
|
||||||
|
self.katrain.config("engine/model"),
|
||||||
|
self.katrain.config("engine/humanlike_model"),
|
||||||
|
"katrain/models",
|
||||||
|
DATA_FOLDER,
|
||||||
|
]
|
||||||
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
|
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
|
||||||
self.last_clicked_download_models = 0
|
self.last_clicked_download_models = 0
|
||||||
|
|
||||||
@@ -561,7 +591,9 @@ class BaseConfigPopup(QuickConfigGui):
|
|||||||
key=lambda descpath: ("Recommended" not in descpath[0], " - " not in descpath[0], descpath[0]),
|
key=lambda descpath: ("Recommended" not in descpath[0], " - " not in descpath[0], descpath[0]),
|
||||||
)
|
)
|
||||||
humanlike_models_available_msg = i18n._("models available").format(num=len(humanlike_model_files))
|
humanlike_models_available_msg = i18n._("models available").format(num=len(humanlike_model_files))
|
||||||
self.humanlike_model_files.values = [humanlike_models_available_msg] + [desc for desc, path in humanlike_model_files]
|
self.humanlike_model_files.values = [humanlike_models_available_msg] + [
|
||||||
|
desc for desc, path in humanlike_model_files
|
||||||
|
]
|
||||||
self.humanlike_model_files.value_keys = [""] + [path for desc, path in humanlike_model_files]
|
self.humanlike_model_files.value_keys = [""] + [path for desc, path in humanlike_model_files]
|
||||||
self.humanlike_model_files.text = humanlike_models_available_msg
|
self.humanlike_model_files.text = humanlike_models_available_msg
|
||||||
|
|
||||||
@@ -650,7 +682,9 @@ class BaseConfigPopup(QuickConfigGui):
|
|||||||
|
|
||||||
for name, url in {**self.MODELS, **dist_models}.items():
|
for name, url in {**self.MODELS, **dist_models}.items():
|
||||||
filename = os.path.split(url)[1]
|
filename = os.path.split(url)[1]
|
||||||
if not any(os.path.split(f)[1] == filename for f in self.model_files.values + self.humanlike_model_files.values):
|
if not any(
|
||||||
|
os.path.split(f)[1] == filename for f in self.model_files.values + self.humanlike_model_files.values
|
||||||
|
):
|
||||||
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
|
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
|
||||||
savepath_tmp = savepath + ".part"
|
savepath_tmp = savepath + ".part"
|
||||||
self.katrain.log(f"Downloading {name} from {url} to {savepath_tmp}", OUTPUT_INFO)
|
self.katrain.log(f"Downloading {name} from {url} to {savepath_tmp}", OUTPUT_INFO)
|
||||||
@@ -759,14 +793,28 @@ class BaseConfigPopup(QuickConfigGui):
|
|||||||
|
|
||||||
|
|
||||||
class ConfigPopup(BaseConfigPopup):
|
class ConfigPopup(BaseConfigPopup):
|
||||||
|
ENGINE_TAB_BUTTONS = {"local": "local_tab_button", "remote": "remote_tab_button", "custom": "custom_tab_button"}
|
||||||
|
|
||||||
def __init__(self, katrain):
|
def __init__(self, katrain):
|
||||||
super().__init__(katrain)
|
super().__init__(katrain)
|
||||||
Clock.schedule_once(self.check_katas)
|
Clock.schedule_once(self.check_katas)
|
||||||
|
Clock.schedule_once(self.select_engine_tab)
|
||||||
MDApp.get_running_app().bind(language=self.check_models)
|
MDApp.get_running_app().bind(language=self.check_models)
|
||||||
MDApp.get_running_app().bind(language=self.check_katas)
|
MDApp.get_running_app().bind(language=self.check_katas)
|
||||||
|
|
||||||
|
def select_engine_tab(self, *_args):
|
||||||
|
# The active tab is authoritative for which engine is used; pick it based on the current config.
|
||||||
|
backend = resolve_engine_backend(self.katrain.config("engine"))
|
||||||
|
self.engine_sm.current = backend
|
||||||
|
getattr(self, self.ENGINE_TAB_BUTTONS[backend]).state = "down"
|
||||||
|
|
||||||
def update_config(self, save_to_file=True, close_popup=True):
|
def update_config(self, save_to_file=True, close_popup=True):
|
||||||
|
old_backend = self.katrain.config("engine/backend", "")
|
||||||
|
backend = self.engine_sm.current
|
||||||
|
self.katrain._config["engine"]["backend"] = backend
|
||||||
updated = super().update_config(save_to_file=save_to_file, close_popup=close_popup)
|
updated = super().update_config(save_to_file=save_to_file, close_popup=close_popup)
|
||||||
|
if backend != old_backend:
|
||||||
|
updated.add("engine/backend")
|
||||||
self.katrain.debug_level = self.katrain.config("general/debug_level", OUTPUT_INFO)
|
self.katrain.debug_level = self.katrain.config("general/debug_level", OUTPUT_INFO)
|
||||||
|
|
||||||
ignore = {"max_visits", "fast_visits", "max_time", "enable_ownership", "wide_root_noise"}
|
ignore = {"max_visits", "fast_visits", "max_time", "enable_ownership", "wide_root_noise"}
|
||||||
@@ -774,21 +822,8 @@ class ConfigPopup(BaseConfigPopup):
|
|||||||
if detected_restart:
|
if detected_restart:
|
||||||
|
|
||||||
def restart_engine(_dt):
|
def restart_engine(_dt):
|
||||||
self.katrain.controls.set_status("", STATUS_INFO)
|
|
||||||
self.katrain.log(f"Restarting Engine after {detected_restart} settings change")
|
self.katrain.log(f"Restarting Engine after {detected_restart} settings change")
|
||||||
self.katrain.controls.set_status(i18n._("restarting engine"), STATUS_INFO)
|
self.katrain.restart_engine()
|
||||||
|
|
||||||
old_engine = self.katrain.engine # type: KataGoEngine
|
|
||||||
old_proc = old_engine.katago_process
|
|
||||||
if old_proc:
|
|
||||||
old_engine.shutdown(finish=False)
|
|
||||||
new_engine = KataGoEngine(self.katrain, self.katrain.config("engine"))
|
|
||||||
self.katrain.engine = new_engine
|
|
||||||
self.katrain.game.engines = {"B": new_engine, "W": new_engine}
|
|
||||||
self.katrain.game.analyze_all_nodes(
|
|
||||||
analyze_fast=True
|
|
||||||
) # old engine was possibly broken, so make sure we redo any failures
|
|
||||||
self.katrain.update_state()
|
|
||||||
|
|
||||||
Clock.schedule_once(restart_engine, 0)
|
Clock.schedule_once(restart_engine, 0)
|
||||||
|
|
||||||
|
|||||||
+45
-11
@@ -1,9 +1,9 @@
|
|||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivymd.app import MDApp
|
|
||||||
from kivy.core.audio import SoundLoader
|
from kivy.core.audio import SoundLoader
|
||||||
from kivy.utils import platform
|
from kivy.utils import platform
|
||||||
|
|
||||||
cached_sounds = {}
|
cached_sounds = {}
|
||||||
|
_audio_available = False
|
||||||
|
|
||||||
# prefer ffpyplayer on linux, then others, avoid gst and avoid or ffpyplayer on windows
|
# prefer ffpyplayer on linux, then others, avoid gst and avoid or ffpyplayer on windows
|
||||||
ranking = [("ffplay", 98 if platform in ["win", "macosx"] else -2), ("sdl", -1), ("gst", 99), ("", 0)]
|
ranking = [("ffplay", 98 if platform in ["win", "macosx"] else -2), ("sdl", -1), ("gst", 99), ("", 0)]
|
||||||
@@ -13,22 +13,56 @@ except Exception as e:
|
|||||||
print("Exception sorting sound loaders: ", e) # private vars, so could break with versions etc
|
print("Exception sorting sound loaders: ", e) # private vars, so could break with versions etc
|
||||||
|
|
||||||
|
|
||||||
def play_sound(file, volume=1, cache=True):
|
def preload_sounds(sound_dir):
|
||||||
def _play(sound):
|
"""Preload all sounds before the SDL2 window is created.
|
||||||
if sound:
|
|
||||||
sound.play()
|
|
||||||
sound.seek(0)
|
|
||||||
|
|
||||||
|
SoundLoader.load() deadlocks when no audio output device is available,
|
||||||
|
so we test audio in a subprocess first.
|
||||||
|
"""
|
||||||
|
global _audio_available
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
audio_files = [
|
||||||
|
os.path.join(sound_dir, fn) for fn in os.listdir(sound_dir)
|
||||||
|
if fn.endswith((".wav", ".ogg", ".mp3"))
|
||||||
|
]
|
||||||
|
if not audio_files:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test if audio subsystem works by loading one sound in a subprocess
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-c",
|
||||||
|
f"from kivy.core.audio import SoundLoader; SoundLoader.load({audio_files[0]!r})"],
|
||||||
|
timeout=3, capture_output=True,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
print(f"Warning: Audio unavailable ({e}). Sounds disabled.", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Audio works, preload all sounds
|
||||||
|
for path in audio_files:
|
||||||
|
cached_sounds[os.path.basename(path)] = SoundLoader.load(path)
|
||||||
|
_audio_available = True
|
||||||
|
|
||||||
|
|
||||||
|
def play_sound(file, volume=1, cache=True):
|
||||||
|
if not _audio_available:
|
||||||
|
return
|
||||||
|
|
||||||
|
# the following import causes the creation of an SDL2 window, and if there
|
||||||
|
# is no sound device, it deadlocks. We delay the import until we know audio
|
||||||
|
# is available
|
||||||
|
from kivymd.app import MDApp
|
||||||
app = MDApp.get_running_app()
|
app = MDApp.get_running_app()
|
||||||
if app and app.gui and app.gui.config("timer/sound"):
|
if app and app.gui and app.gui.config("timer/sound"):
|
||||||
sound = cached_sounds.get(file)
|
sound = cached_sounds.get(file)
|
||||||
if sound is None:
|
|
||||||
sound = SoundLoader.load(file)
|
|
||||||
if cache:
|
|
||||||
cached_sounds[file] = sound
|
|
||||||
if sound is not None:
|
if sound is not None:
|
||||||
sound.volume = volume
|
sound.volume = volume
|
||||||
Clock.schedule_once(lambda _dt: _play(sound), 0)
|
sound.play()
|
||||||
|
sound.seek(0)
|
||||||
|
|
||||||
|
|
||||||
def stop_sound(file):
|
def stop_sound(file):
|
||||||
|
|||||||
Binary file not shown.
@@ -872,7 +872,7 @@ msgstr "如果你还有任何困难,请参考疑难排解页面 {link}"
|
|||||||
|
|
||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr "首先,在 {link} 中,尝试下载且更换为不同版本的KataGo执行档是否能解决问题。"
|
msgstr "请先点击「重试」以重新启动引擎。如果没有帮助,请尝试在 {link} 中下载 KataGo 版本并改用其他可执行文件。"
|
||||||
|
|
||||||
#. as in "click here" to go to this page,
|
#. as in "click here" to go to this page,
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
@@ -998,3 +998,57 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"与远程 KataGo 引擎的连接已断开,且无法自动重新建立。\n"
|
||||||
|
"\n"
|
||||||
|
"错误为:[color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"错误代码为:[color=#CC2222]{code}[/color]。"
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "重试"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr "如果远程引擎之前能正常工作,请在其恢复后点击「重试」以重新连接。否则,请在 {link} 中再次检查远程引擎的网址。"
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "本地引擎"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "远程引擎"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "自定义命令"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "所选标签页决定使用的引擎。"
|
||||||
Binary file not shown.
@@ -940,8 +940,9 @@ msgstr ""
|
|||||||
#. - Link in tags should not be touched.
|
#. - Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Versuchen sie erstmal KataGo Versionen runterzuladen und ob eine andere "
|
"Drücken Sie zuerst auf Erneut versuchen, um die Engine neu zu starten. Falls"
|
||||||
"ausführbare Datei in {link} hilft. "
|
" das nicht hilft, laden Sie KataGo-Versionen herunter und wechseln Sie in "
|
||||||
|
"{link} zu einer anderen ausführbaren Datei."
|
||||||
|
|
||||||
#. - as in "click here" to go to this page,
|
#. - as in "click here" to go to this page,
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
@@ -1067,3 +1068,60 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"Die Verbindung zur entfernten KataGo-Engine ging verloren und konnte nicht automatisch wiederhergestellt werden.\n"
|
||||||
|
"\n"
|
||||||
|
"Der Fehler war: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"Der Fehlercode war: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Erneut versuchen"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"Wenn die entfernte Engine zuvor funktioniert hat, drücken Sie auf Erneut "
|
||||||
|
"versuchen, um die Verbindung wiederherzustellen, sobald sie wieder verfügbar"
|
||||||
|
" ist. Andernfalls überprüfen Sie die URL der entfernten Engine in {link} ."
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Lokale Engine"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Remote-Engine"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Eigener Befehl"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "Der ausgewählte Reiter bestimmt die genutzte Engine."
|
||||||
Binary file not shown.
@@ -653,6 +653,24 @@ msgstr ""
|
|||||||
"Full command used to start the engine. All other model options will be "
|
"Full command used to start the engine. All other model options will be "
|
||||||
"ignored."
|
"ignored."
|
||||||
|
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
msgid "engine:max_visits"
|
msgid "engine:max_visits"
|
||||||
msgstr "Maximum number of visits in analysis"
|
msgstr "Maximum number of visits in analysis"
|
||||||
|
|
||||||
@@ -753,8 +771,28 @@ msgstr ""
|
|||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"First, try downloading KataGo versions and try if changing to a different "
|
"First, press Retry to restart the engine. If that does not help, try "
|
||||||
"executable helps in {link} ."
|
"downloading KataGo versions and changing to a different executable in {link}"
|
||||||
|
" ."
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"The connection to the remote KataGo engine was lost and could not be re-established automatically.\n"
|
||||||
|
"\n"
|
||||||
|
"The error was: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"The error code was: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"If the remote engine has worked before, press Retry to reconnect once it is "
|
||||||
|
"back. Otherwise, double-check the remote engine URL in {link} ."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
#. Link in tags should not be touched (uses link_here msgid)
|
#. Link in tags should not be touched (uses link_here msgid)
|
||||||
msgid "go to engine help page"
|
msgid "go to engine help page"
|
||||||
@@ -1073,3 +1111,15 @@ msgstr "Distance of wall"
|
|||||||
#. tsumego: ko is good enough?
|
#. tsumego: ko is good enough?
|
||||||
msgid "tsumego:ko"
|
msgid "tsumego:ko"
|
||||||
msgstr "Ko allowed?"
|
msgstr "Ko allowed?"
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Local engine"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Remote engine"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Custom command"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "The selected tab determines the engine used."
|
||||||
Binary file not shown.
@@ -752,18 +752,18 @@ msgstr "Cache analysis in SGF"
|
|||||||
msgid "menu:save-as"
|
msgid "menu:save-as"
|
||||||
msgstr "Save Game As..."
|
msgstr "Save Game As..."
|
||||||
|
|
||||||
#. TODO
|
|
||||||
# sgf load button
|
# sgf load button
|
||||||
|
#. TODO
|
||||||
msgid "Load File"
|
msgid "Load File"
|
||||||
msgstr "Load File"
|
msgstr "Load File"
|
||||||
|
|
||||||
#. TODO
|
|
||||||
# sgf save popup
|
# sgf save popup
|
||||||
|
#. TODO
|
||||||
msgid "save sgf title"
|
msgid "save sgf title"
|
||||||
msgstr "Save SGF File"
|
msgstr "Save SGF File"
|
||||||
|
|
||||||
#. TODO
|
|
||||||
# sgf save as button
|
# sgf save as button
|
||||||
|
#. TODO
|
||||||
msgid "Save File"
|
msgid "Save File"
|
||||||
msgstr "Save File"
|
msgstr "Save File"
|
||||||
|
|
||||||
@@ -1029,3 +1029,39 @@ msgstr "Ko allowed?"
|
|||||||
#. TODO - Menu option for tsumego frame around position
|
#. TODO - Menu option for tsumego frame around position
|
||||||
msgid "analysis:tsumegoframe"
|
msgid "analysis:tsumegoframe"
|
||||||
msgstr "Tsumego Frame"
|
msgstr "Tsumego Frame"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Motor local"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Motor remoto"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Comando personalizado"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "La pestaña seleccionada determina el motor usado."
|
||||||
Binary file not shown.
@@ -929,8 +929,9 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"L'exécutable et le modèle KataGo utilisés doivent être compatibles. Vous "
|
"Commencez par appuyer sur Réessayer pour redémarrer le moteur. Si cela ne "
|
||||||
"pouvez en télécharger et/ou en sélectionner d'autres dans les {link}. "
|
"suffit pas, essayez de télécharger des versions de KataGo et de changer "
|
||||||
|
"d'exécutable dans {link} ."
|
||||||
|
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
msgstr "ce lien"
|
msgstr "ce lien"
|
||||||
@@ -1036,3 +1037,60 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"La connexion au moteur KataGo distant a été perdue et n'a pas pu être rétablie automatiquement.\n"
|
||||||
|
"\n"
|
||||||
|
"L'erreur était : [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"Le code d'erreur était : [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Réessayer"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"Si le moteur distant a déjà fonctionné, appuyez sur Réessayer pour vous "
|
||||||
|
"reconnecter une fois qu'il est de nouveau disponible. Sinon, vérifiez l'URL "
|
||||||
|
"du moteur distant dans {link} ."
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Moteur local"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Moteur distant"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Commande personnalisée"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "L'onglet sélectionné détermine le moteur utilisé."
|
||||||
Binary file not shown.
@@ -916,7 +916,9 @@ msgstr "それでもだめなら、{link}して最新のトラブルシューテ
|
|||||||
|
|
||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr "まずは、{link}から別バージョンのKataGoをダウンロードしてうまくいくか試してください。"
|
msgstr ""
|
||||||
|
"まず「再試行」を押してエンジンを再起動してください。解決しない場合は、{link} で KataGo "
|
||||||
|
"のバージョンをダウンロードし、別の実行ファイルに変更してみてください。"
|
||||||
|
|
||||||
#. as in "click here" to go to this page,
|
#. as in "click here" to go to this page,
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
@@ -1039,3 +1041,59 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"リモート KataGo エンジンへの接続が失われ、自動的に再接続できませんでした。\n"
|
||||||
|
"\n"
|
||||||
|
"エラー: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"エラーコード: [color=#CC2222]{code}[/color]。"
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "再試行"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"リモートエンジンが以前は動作していた場合は、復旧後に「再試行」を押して再接続してください。そうでない場合は、{link} でリモートエンジンの URL "
|
||||||
|
"を再確認してください。"
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "ローカルエンジン"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "リモートエンジン"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "カスタムコマンド"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "選択したタブが使用するエンジンを決定します。"
|
||||||
Binary file not shown.
@@ -867,7 +867,9 @@ msgid "go to engine help page"
|
|||||||
msgstr "아직 문제를 겪고 있으시다면, {link}에서 최신 문제 해결법을 찾아보십시오."
|
msgstr "아직 문제를 겪고 있으시다면, {link}에서 최신 문제 해결법을 찾아보십시오."
|
||||||
|
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr "우선, KataGo 버전들을 내려받아 보시고, {link}에서 다른 실행 파일로 바꾸면 괜찮아지는지 확인해 보십시오."
|
msgstr ""
|
||||||
|
"먼저 다시 시도를 눌러 엔진을 다시 시작하세요. 그래도 해결되지 않으면 {link} 에서 KataGo 버전을 내려받아 다른 실행 파일로 "
|
||||||
|
"변경해 보세요."
|
||||||
|
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
msgstr "여기"
|
msgstr "여기"
|
||||||
@@ -966,3 +968,59 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"원격 KataGo 엔진과의 연결이 끊겼으며 자동으로 다시 연결하지 못했습니다.\n"
|
||||||
|
"\n"
|
||||||
|
"오류: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"오류 코드: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "다시 시도"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"원격 엔진이 이전에 작동한 적이 있다면, 다시 사용 가능해졌을 때 다시 시도를 눌러 재연결하세요. 그렇지 않으면 {link} 에서 원격 "
|
||||||
|
"엔진 URL을 다시 확인하세요."
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "로컬 엔진"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "원격 엔진"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "사용자 지정 명령"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "선택한 탭이 사용할 엔진을 결정합니다."
|
||||||
Binary file not shown.
@@ -1002,3 +1002,63 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. TODO - Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"The connection to the remote KataGo engine was lost and could not be re-established automatically.\n"
|
||||||
|
"\n"
|
||||||
|
"The error was: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"The error code was: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. TODO - Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
|
#. TODO - Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"If the remote engine has worked before, press Retry to reconnect once it is "
|
||||||
|
"back. Otherwise, double-check the remote engine URL in {link} ."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Custom command"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "The selected tab determines the engine used."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Remote engine"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Local engine"
|
||||||
Binary file not shown.
@@ -733,8 +733,9 @@ msgstr ""
|
|||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"İlk önce, KataGo sürümlerini indirmeyi deneyin ve {link} bağlantısındaki "
|
"Önce motoru yeniden başlatmak için Yeniden Dene'ye basın. Bu işe yaramazsa, "
|
||||||
"farklı yürütme dosyalarını kullanmayı deneyin."
|
"{link} içinde KataGo sürümlerini indirip farklı bir yürütülebilir dosyaya "
|
||||||
|
"geçmeyi deneyin."
|
||||||
|
|
||||||
#. Link in tags should not be touched (uses link_here msgid)
|
#. Link in tags should not be touched (uses link_here msgid)
|
||||||
msgid "go to engine help page"
|
msgid "go to engine help page"
|
||||||
@@ -1102,3 +1103,60 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"Uzak KataGo motoruyla bağlantı kesildi ve otomatik olarak yeniden kurulamadı.\n"
|
||||||
|
"\n"
|
||||||
|
"Hata: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"Hata kodu: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Yeniden Dene"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"Uzak motor daha önce çalıştıysa, tekrar erişilebilir olduğunda yeniden "
|
||||||
|
"bağlanmak için Yeniden Dene'ye basın. Aksi takdirde, {link} içindeki uzak "
|
||||||
|
"motor URL'sini tekrar kontrol edin."
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Yerel motor"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Uzak motor"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Özel komut"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "Seçilen sekme, kullanılacak motoru belirler."
|
||||||
Binary file not shown.
@@ -898,7 +898,7 @@ msgstr "如果你還有任何困難,請參考疑難排解頁面 {link}"
|
|||||||
|
|
||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr "首先,在 {link} 中,嘗試下載且更換為不同版本的KataGo執行檔是否能解決問題。"
|
msgstr "請先按「重試」以重新啟動引擎。如果沒有幫助,請嘗試在 {link} 中下載 KataGo 版本並改用其他執行檔。"
|
||||||
|
|
||||||
#. as in "click here" to go to this page,
|
#. as in "click here" to go to this page,
|
||||||
msgid "link_here"
|
msgid "link_here"
|
||||||
@@ -1021,3 +1021,57 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"與遠端 KataGo 引擎的連線中斷,且無法自動重新建立。\n"
|
||||||
|
"\n"
|
||||||
|
"錯誤為:[color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"錯誤代碼為:[color=#CC2222]{code}[/color]。"
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "重試"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr "如果遠端引擎先前曾正常運作,請在它恢復後按「重試」以重新連線。否則,請在 {link} 中再次確認遠端引擎的網址。"
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "本機引擎"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "遠端引擎"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "自訂命令"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "所選分頁決定使用的引擎。"
|
||||||
Binary file not shown.
@@ -751,8 +751,9 @@ msgstr ""
|
|||||||
#. Link in tags should not be touched.
|
#. Link in tags should not be touched.
|
||||||
msgid "change engine suggestion"
|
msgid "change engine suggestion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Спочатку спробуйте завантажити інші версії KataGo і перевірте, чи допоможе "
|
"Спочатку натисніть «Повторити», щоб перезапустити рушій. Якщо це не "
|
||||||
"зміна виконуваного файлу в {link}."
|
"допомогло, спробуйте завантажити версії KataGo та змінити виконуваний файл у"
|
||||||
|
" {link} ."
|
||||||
|
|
||||||
#. Link in tags should not be touched (uses link_here msgid)
|
#. Link in tags should not be touched (uses link_here msgid)
|
||||||
msgid "go to engine help page"
|
msgid "go to engine help page"
|
||||||
@@ -1086,3 +1087,60 @@ msgstr ""
|
|||||||
#. TODO
|
#. TODO
|
||||||
msgid "engine:humanlike_model"
|
msgid "engine:humanlike_model"
|
||||||
msgstr "Path to human-like model"
|
msgstr "Path to human-like model"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url"
|
||||||
|
msgstr "KataGo WebSocket URL"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "engine:remote_url:hint"
|
||||||
|
msgstr "Leave blank to use a local KataGo engine. Use ws:// or wss://."
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL is empty"
|
||||||
|
msgstr "Remote KataGo URL is empty"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
msgstr "Remote KataGo URL must start with ws:// or wss://"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Connecting to remote KataGo failed"
|
||||||
|
msgstr "Connecting to remote KataGo at {url} failed: {error}"
|
||||||
|
|
||||||
|
#. TODO
|
||||||
|
msgid "Remote KataGo engine disconnected"
|
||||||
|
msgstr "Remote KataGo engine disconnected: {error}"
|
||||||
|
|
||||||
|
#. Shown instead of "engine died popup opening message" for remote
|
||||||
|
#. engines.
|
||||||
|
msgid "remote engine disconnected popup opening message"
|
||||||
|
msgstr ""
|
||||||
|
"З'єднання з віддаленим рушієм KataGo втрачено й не вдалося відновити автоматично.\n"
|
||||||
|
"\n"
|
||||||
|
"Помилка: [color=#CC2222]{error_message}[/color]\n"
|
||||||
|
"\n"
|
||||||
|
"Код помилки: [color=#CC2222]{code}[/color]."
|
||||||
|
|
||||||
|
#. Button to reconnect/restart the engine from the recovery popup.
|
||||||
|
msgid "engine recovery: retry"
|
||||||
|
msgstr "Повторити"
|
||||||
|
|
||||||
|
#. Link in tags should not be touched. Shown for remote engines.
|
||||||
|
msgid "remote engine check url suggestion"
|
||||||
|
msgstr ""
|
||||||
|
"Якщо віддалений рушій раніше працював, натисніть «Повторити», щоб "
|
||||||
|
"під'єднатися знову, коли він стане доступним. Інакше перевірте URL-адресу "
|
||||||
|
"віддаленого рушія в {link} ."
|
||||||
|
|
||||||
|
msgid "engine:tab:local"
|
||||||
|
msgstr "Локальний рушій"
|
||||||
|
|
||||||
|
msgid "engine:tab:remote"
|
||||||
|
msgstr "Віддалений рушій"
|
||||||
|
|
||||||
|
msgid "engine:tab:custom"
|
||||||
|
msgstr "Власна команда"
|
||||||
|
|
||||||
|
msgid "engine:backend:note"
|
||||||
|
msgstr "Вибрана вкладка визначає використовуваний рушій."
|
||||||
+201
-105
@@ -3,9 +3,11 @@
|
|||||||
#: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__
|
||||||
|
#:import NoTransition kivy.uix.screenmanager.NoTransition
|
||||||
|
|
||||||
#:import Theme katrain.gui.theme.Theme
|
#:import Theme katrain.gui.theme.Theme
|
||||||
#:import LIGHT_GREY katrain.gui.theme.LIGHT_GREY
|
#:import LIGHT_GREY katrain.gui.theme.LIGHT_GREY
|
||||||
|
#:import ORANGE katrain.gui.theme.ORANGE
|
||||||
|
|
||||||
#:set CP_SPACING Theme.CP_SPACING
|
#:set CP_SPACING Theme.CP_SPACING
|
||||||
#:set CP_SMALL_SPACING Theme.CP_SMALL_SPACING
|
#:set CP_SMALL_SPACING Theme.CP_SMALL_SPACING
|
||||||
@@ -92,131 +94,216 @@
|
|||||||
katago_files: katago_files
|
katago_files: katago_files
|
||||||
download_progress_box: download_progress_box
|
download_progress_box: download_progress_box
|
||||||
katago_download_progress_box: katago_download_progress_box
|
katago_download_progress_box: katago_download_progress_box
|
||||||
|
engine_sm: engine_sm
|
||||||
|
local_tab_button: local_tab_button
|
||||||
|
remote_tab_button: remote_tab_button
|
||||||
|
custom_tab_button: custom_tab_button
|
||||||
|
|
||||||
# Top Section - KataGo Controls
|
# Top Section - Engine source header
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: dp(50)
|
height: dp(36)
|
||||||
spacing: dp(20)
|
padding: dp(10), 0
|
||||||
padding: dp(10)
|
|
||||||
|
|
||||||
DescriptionLabel:
|
DescriptionLabel:
|
||||||
text: i18n._('katago settings')
|
text: i18n._('katago settings')
|
||||||
font_size: sp(Theme.DESC_FONT_SIZE) * 1.5
|
font_size: sp(Theme.DESC_FONT_SIZE) * 1.5
|
||||||
size_hint_x: 0.4
|
halign: 'left'
|
||||||
|
|
||||||
AutoSizedRoundedRectangleButton:
|
# Engine backend tabs, with the "tab is authoritative" note to their right
|
||||||
text: i18n._("download katago button")
|
|
||||||
on_press: root.download_katas()
|
|
||||||
size_hint: (0.3, 0.8)
|
|
||||||
|
|
||||||
AutoSizedRoundedRectangleButton:
|
|
||||||
text: i18n._("download models button")
|
|
||||||
on_press: root.download_models()
|
|
||||||
size_hint: (0.3, 0.8)
|
|
||||||
|
|
||||||
# Main Content Area
|
|
||||||
MDBoxLayout:
|
MDBoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'horizontal'
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(40)
|
||||||
|
spacing: dp(6)
|
||||||
|
padding: dp(8), 0
|
||||||
|
|
||||||
|
SizedRectangleToggleButton:
|
||||||
|
id: local_tab_button
|
||||||
|
size_hint: 0.22, 1
|
||||||
|
group: 'engine_backend'
|
||||||
|
allow_no_selection: False
|
||||||
|
text: i18n._('engine:tab:local')
|
||||||
|
on_release: engine_sm.current = 'local'
|
||||||
|
|
||||||
|
SizedRectangleToggleButton:
|
||||||
|
id: remote_tab_button
|
||||||
|
size_hint: 0.22, 1
|
||||||
|
group: 'engine_backend'
|
||||||
|
allow_no_selection: False
|
||||||
|
text: i18n._('engine:tab:remote')
|
||||||
|
on_release: engine_sm.current = 'remote'
|
||||||
|
|
||||||
|
SizedRectangleToggleButton:
|
||||||
|
id: custom_tab_button
|
||||||
|
size_hint: 0.22, 1
|
||||||
|
group: 'engine_backend'
|
||||||
|
allow_no_selection: False
|
||||||
|
text: i18n._('engine:tab:custom')
|
||||||
|
on_release: engine_sm.current = 'custom'
|
||||||
|
|
||||||
|
DescriptionLabel:
|
||||||
|
size_hint_x: 0.4
|
||||||
|
text: i18n._('engine:backend:note')
|
||||||
|
color: ORANGE
|
||||||
|
halign: 'left'
|
||||||
|
valign: 'center'
|
||||||
|
padding: dp(10), 0
|
||||||
|
|
||||||
|
# Engine backend content - one screen per tab
|
||||||
|
ScreenManager:
|
||||||
|
id: engine_sm
|
||||||
size_hint: (1, 6)
|
size_hint: (1, 6)
|
||||||
spacing: dp(15)
|
transition: NoTransition()
|
||||||
padding: dp(10)
|
|
||||||
|
|
||||||
# Model Section
|
Screen:
|
||||||
ConfigSectionGridLayout:
|
name: 'local'
|
||||||
cols: 2
|
MDBoxLayout:
|
||||||
DescriptionLabel:
|
orientation: 'vertical'
|
||||||
text: i18n._("engine:model")
|
spacing: dp(12)
|
||||||
size_hint: (0.33, 1)
|
padding: dp(10)
|
||||||
|
|
||||||
AnchorLayout:
|
# KataGo executable - download button sits under its label
|
||||||
LabelledPathInput:
|
ConfigSectionGridLayout:
|
||||||
id: model_path
|
cols: 2
|
||||||
input_property: "engine/model"
|
MDBoxLayout:
|
||||||
on_text: root.check_models()
|
orientation: 'vertical'
|
||||||
|
size_hint_x: 0.33
|
||||||
|
spacing: dp(6)
|
||||||
|
DescriptionLabel:
|
||||||
|
text: i18n._("engine:katago")
|
||||||
|
AnchorLayout:
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(34)
|
||||||
|
AutoSizedRoundedRectangleButton:
|
||||||
|
text: i18n._("download katago button")
|
||||||
|
on_press: root.download_katas()
|
||||||
|
size_hint: None, None
|
||||||
|
height: dp(32)
|
||||||
|
MDBoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
AnchorLayout:
|
||||||
|
LabelledPathInput:
|
||||||
|
id: katago_path
|
||||||
|
input_property: "engine/katago"
|
||||||
|
hint_text: i18n._("engine:katago:hint")
|
||||||
|
on_text: root.check_katas()
|
||||||
|
AnchorLayout:
|
||||||
|
KeyValueSpinner:
|
||||||
|
id: katago_files
|
||||||
|
text: ''
|
||||||
|
text_autoupdate: True
|
||||||
|
size_hint_y: 0.7
|
||||||
|
on_select: if self.selected_index != 0: katago_path.text = self.value_keys[self.selected_index]
|
||||||
|
|
||||||
Label:
|
# Model - download button sits under its label
|
||||||
size_hint: (0.33, 1)
|
ConfigSectionGridLayout:
|
||||||
|
cols: 2
|
||||||
|
MDBoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
size_hint_x: 0.33
|
||||||
|
spacing: dp(6)
|
||||||
|
DescriptionLabel:
|
||||||
|
text: i18n._("engine:model")
|
||||||
|
AnchorLayout:
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(34)
|
||||||
|
AutoSizedRoundedRectangleButton:
|
||||||
|
text: i18n._("download models button")
|
||||||
|
on_press: root.download_models()
|
||||||
|
size_hint: None, None
|
||||||
|
height: dp(32)
|
||||||
|
MDBoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
AnchorLayout:
|
||||||
|
LabelledPathInput:
|
||||||
|
id: model_path
|
||||||
|
input_property: "engine/model"
|
||||||
|
on_text: root.check_models()
|
||||||
|
AnchorLayout:
|
||||||
|
KeyValueSpinner:
|
||||||
|
id: model_files
|
||||||
|
text: ''
|
||||||
|
text_autoupdate: True
|
||||||
|
size_hint_y: 0.7
|
||||||
|
on_select: if self.selected_index != 0: model_path.text = self.value_keys[self.selected_index]
|
||||||
|
|
||||||
AnchorLayout:
|
# Human-like model
|
||||||
KeyValueSpinner:
|
ConfigSectionGridLayout:
|
||||||
id: model_files
|
cols: 2
|
||||||
text: ''
|
DescriptionLabel:
|
||||||
text_autoupdate: True
|
text: i18n._("engine:humanlike_model")
|
||||||
size_hint_y: 0.7
|
size_hint_x: 0.33
|
||||||
on_select: if self.selected_index != 0: model_path.text = self.value_keys[self.selected_index]
|
MDBoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
AnchorLayout:
|
||||||
|
LabelledPathInput:
|
||||||
|
id: humanlike_model_path
|
||||||
|
input_property: "engine/humanlike_model"
|
||||||
|
on_text: root.check_models()
|
||||||
|
AnchorLayout:
|
||||||
|
KeyValueSpinner:
|
||||||
|
id: humanlike_model_files
|
||||||
|
text: ''
|
||||||
|
text_autoupdate: True
|
||||||
|
size_hint_y: 0.7
|
||||||
|
on_select: if self.selected_index != 0: humanlike_model_path.text = self.value_keys[self.selected_index]
|
||||||
|
|
||||||
# Humanlike Model Section
|
# KataGo config file
|
||||||
ConfigSectionGridLayout:
|
ConfigSectionGridLayout:
|
||||||
cols: 2
|
cols: 2
|
||||||
DescriptionLabel:
|
DescriptionLabel:
|
||||||
text: i18n._("engine:humanlike_model")
|
text: i18n._("engine:config")
|
||||||
size_hint: (0.33, 1)
|
size_hint_x: 0.33
|
||||||
|
AnchorLayout:
|
||||||
|
LabelledPathInput:
|
||||||
|
input_property: "engine/config"
|
||||||
|
|
||||||
AnchorLayout:
|
Screen:
|
||||||
LabelledPathInput:
|
name: 'remote'
|
||||||
id: humanlike_model_path
|
MDBoxLayout:
|
||||||
input_property: "engine/humanlike_model"
|
orientation: 'vertical'
|
||||||
on_text: root.check_models()
|
spacing: dp(12)
|
||||||
|
padding: dp(10)
|
||||||
|
ConfigSectionGridLayout:
|
||||||
|
cols: 2
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(70)
|
||||||
|
DescriptionLabel:
|
||||||
|
text: i18n._("engine:remote_url")
|
||||||
|
size_hint_x: 0.33
|
||||||
|
AnchorLayout:
|
||||||
|
LabelledPathInput:
|
||||||
|
check_path: False
|
||||||
|
input_property: "engine/remote_url"
|
||||||
|
hint_text: i18n._("engine:remote_url:hint")
|
||||||
|
Widget:
|
||||||
|
|
||||||
Label:
|
Screen:
|
||||||
size_hint: (0.33, 1)
|
name: 'custom'
|
||||||
|
MDBoxLayout:
|
||||||
AnchorLayout:
|
orientation: 'vertical'
|
||||||
KeyValueSpinner:
|
spacing: dp(12)
|
||||||
id: humanlike_model_files
|
padding: dp(10)
|
||||||
text: ''
|
ConfigSectionGridLayout:
|
||||||
text_autoupdate: True
|
cols: 2
|
||||||
size_hint_y: 0.7
|
size_hint_y: None
|
||||||
on_select: if self.selected_index != 0: humanlike_model_path.text = self.value_keys[self.selected_index]
|
height: dp(70)
|
||||||
|
DescriptionLabel:
|
||||||
# KataGo Engine Section
|
text: i18n._("engine:altcommand")
|
||||||
ConfigSectionGridLayout:
|
size_hint_x: 0.33
|
||||||
cols: 2
|
AnchorLayout:
|
||||||
DescriptionLabel:
|
LabelledPathInput:
|
||||||
text: i18n._("engine:katago")
|
check_path: False
|
||||||
size_hint: (0.33, 1)
|
input_property: "engine/altcommand"
|
||||||
|
hint_text: i18n._("engine:altcommand:hint")
|
||||||
AnchorLayout:
|
Widget:
|
||||||
LabelledPathInput:
|
|
||||||
id: katago_path
|
|
||||||
input_property: "engine/katago"
|
|
||||||
hint_text: i18n._("engine:katago:hint")
|
|
||||||
on_text: root.check_katas()
|
|
||||||
|
|
||||||
Label:
|
|
||||||
size_hint: (0.33, 1)
|
|
||||||
|
|
||||||
AnchorLayout:
|
|
||||||
KeyValueSpinner:
|
|
||||||
id: katago_files
|
|
||||||
text: ''
|
|
||||||
text_autoupdate: True
|
|
||||||
size_hint_y: 0.7
|
|
||||||
on_select: if self.selected_index != 0: katago_path.text = self.value_keys[self.selected_index]
|
|
||||||
|
|
||||||
ConfigSectionGridLayout:
|
|
||||||
rows: 2
|
|
||||||
cols: 2
|
|
||||||
DescriptionLabel:
|
|
||||||
text: i18n._("engine:config")
|
|
||||||
size_hint: 0.33, 1
|
|
||||||
AnchorLayout:
|
|
||||||
LabelledPathInput:
|
|
||||||
input_property: "engine/config"
|
|
||||||
DescriptionLabel:
|
|
||||||
text: i18n._("engine:altcommand")
|
|
||||||
size_hint: 0.33, 1
|
|
||||||
AnchorLayout:
|
|
||||||
LabelledPathInput:
|
|
||||||
check_path: False
|
|
||||||
input_property: "engine/altcommand"
|
|
||||||
hint_text: i18n._("engine:altcommand:hint")
|
|
||||||
|
|
||||||
# Settings Headers
|
# Settings Headers
|
||||||
MDBoxLayout:
|
MDBoxLayout:
|
||||||
size_hint_y: 0.5
|
size_hint_y: None
|
||||||
|
height: dp(34)
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
padding: dp(10)
|
padding: dp(10)
|
||||||
spacing: dp(20)
|
spacing: dp(20)
|
||||||
@@ -874,14 +961,23 @@
|
|||||||
on_press: root.update_config(True)
|
on_press: root.update_config(True)
|
||||||
|
|
||||||
<EngineRecoveryPopup>:
|
<EngineRecoveryPopup>:
|
||||||
|
orientation: 'vertical'
|
||||||
Label:
|
Label:
|
||||||
|
size_hint: 1, 0.85
|
||||||
padding: CP_PADDING, CP_PADDING*3
|
padding: CP_PADDING, CP_PADDING*3
|
||||||
text: i18n._("engine died popup opening message").format(code=root.code,error_message=root.error_message) + "\n\n" + i18n._("change engine suggestion").format(link="[color=#CCCC11][u][ref=engine_settings]"+i18n._('menu:settings')+"[/ref][/u][/color]") + "\n\n" + i18n._("go to engine help page").format(link="[color=#CCCC11][u][ref=engine_help]"+i18n._('link_here')+"[/ref][/u][/color]")
|
text: root.recovery_message
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||||
markup: True
|
markup: True
|
||||||
valign: 'top'
|
valign: 'top'
|
||||||
on_ref_press: app.webbrowser('engine:help') if args[1]=='engine_help' else app.gui('config-popup')
|
on_ref_press: app.webbrowser('engine:help') if args[1]=='engine_help' else app.gui('config-popup')
|
||||||
|
AnchorLayout:
|
||||||
|
size_hint: 1, 0.15
|
||||||
|
AutoSizedRoundedRectangleButton:
|
||||||
|
padding_x: 15
|
||||||
|
size_hint: None, 0.66
|
||||||
|
text: i18n._("engine recovery: retry")
|
||||||
|
on_release: root.retry()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "KaTrain"
|
name = "KaTrain"
|
||||||
version = "1.17.1"
|
version = "1.18.0"
|
||||||
description = "Go/Baduk/Weiqi playing and teaching app with a variety of AIs"
|
description = "Go/Baduk/Weiqi playing and teaching app with a variety of AIs"
|
||||||
authors = [{ name = "Sander Land" }]
|
authors = [{ name = "Sander Land" }]
|
||||||
requires-python = ">=3.9,<3.14"
|
requires-python = ">=3.9,<3.14"
|
||||||
@@ -22,6 +22,8 @@ dependencies = [
|
|||||||
"urllib3>=2.2.2",
|
"urllib3>=2.2.2",
|
||||||
"kivy>=2.3.1",
|
"kivy>=2.3.1",
|
||||||
"kivymd==0.104.1",
|
"kivymd==0.104.1",
|
||||||
|
"websocket-client>=1.9.0",
|
||||||
|
"certifi>=2026.2.25",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""Tests for the remote (WebSocket) KataGo engine's reconnect logic.
|
||||||
|
|
||||||
|
These use an in-process fake WebSocket so no network/server is needed:
|
||||||
|
`create_connection` is monkeypatched to hand out controllable sockets
|
||||||
|
whose recv()/send()/close() we drive from the test thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from websocket import WebSocketException
|
||||||
|
|
||||||
|
from katrain.core import remote_engine
|
||||||
|
from katrain.core.remote_engine import RemoteKataGoEngine
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWS:
|
||||||
|
"""Minimal stand-in for a websocket-client connection."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.sent = []
|
||||||
|
self._recv = queue.Queue()
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
def recv_data(self, control_frame=True):
|
||||||
|
item = self._recv.get() # blocks like a real recv
|
||||||
|
if isinstance(item, BaseException):
|
||||||
|
raise item
|
||||||
|
return item
|
||||||
|
|
||||||
|
def send(self, payload):
|
||||||
|
if self.closed:
|
||||||
|
raise WebSocketException("send on closed socket")
|
||||||
|
self.sent.append(payload)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.closed = True
|
||||||
|
self._recv.put(WebSocketException("closed"))
|
||||||
|
|
||||||
|
def drop(self):
|
||||||
|
"""Simulate the server/network dropping the connection."""
|
||||||
|
self._recv.put(WebSocketException("connection lost"))
|
||||||
|
|
||||||
|
|
||||||
|
class FakeControls:
|
||||||
|
def set_status(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakeKatrain:
|
||||||
|
"""Records calls (e.g. the engine_recovery_popup trigger)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.controls = FakeControls()
|
||||||
|
self.calls = []
|
||||||
|
self.update_state = lambda *a, **k: None
|
||||||
|
|
||||||
|
def log(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
self.calls.append(args)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_until(pred, timeout=5.0, interval=0.02):
|
||||||
|
end = time.time() + timeout
|
||||||
|
while time.time() < end and not pred():
|
||||||
|
time.sleep(interval)
|
||||||
|
return pred()
|
||||||
|
|
||||||
|
|
||||||
|
def popup_codes(katrain):
|
||||||
|
return [args[2] for args in katrain.calls if args and args[0] == "engine_recovery_popup"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fast_backoff(monkeypatch):
|
||||||
|
monkeypatch.setattr(RemoteKataGoEngine, "RECONNECT_BACKOFF_S", 0.01)
|
||||||
|
monkeypatch.setattr(RemoteKataGoEngine, "RECONNECT_MAX_BACKOFF_S", 0.02)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconnect_resends_outstanding_queries(monkeypatch, fast_backoff):
|
||||||
|
created = []
|
||||||
|
|
||||||
|
def factory(*args, **kwargs):
|
||||||
|
ws = FakeWS()
|
||||||
|
created.append(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote_engine, "create_connection", factory)
|
||||||
|
|
||||||
|
katrain = FakeKatrain()
|
||||||
|
engine = RemoteKataGoEngine(katrain, {"remote_url": "ws://test", "allow_recovery": True})
|
||||||
|
try:
|
||||||
|
assert wait_until(lambda: len(created) == 1)
|
||||||
|
ws1 = created[0]
|
||||||
|
|
||||||
|
engine.send_query({"foo": "bar"}, lambda *a: None, None)
|
||||||
|
assert wait_until(lambda: len(ws1.sent) == 1)
|
||||||
|
assert wait_until(lambda: len(engine.queries) == 1)
|
||||||
|
|
||||||
|
# Connection drops while a query is outstanding.
|
||||||
|
ws1.drop()
|
||||||
|
|
||||||
|
# A new connection is established transparently...
|
||||||
|
assert wait_until(lambda: len(created) == 2)
|
||||||
|
ws2 = created[1]
|
||||||
|
assert wait_until(lambda: engine.ws is ws2)
|
||||||
|
|
||||||
|
# ...and the in-flight query is re-sent on it.
|
||||||
|
assert wait_until(lambda: len(ws2.sent) >= 1)
|
||||||
|
|
||||||
|
# No local-engine recovery popup was shown.
|
||||||
|
assert popup_codes(katrain) == []
|
||||||
|
finally:
|
||||||
|
engine.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconnect_failure_opens_recovery_popup(monkeypatch, fast_backoff):
|
||||||
|
monkeypatch.setattr(RemoteKataGoEngine, "RECONNECT_ATTEMPTS", 2)
|
||||||
|
first = FakeWS()
|
||||||
|
n = {"count": 0}
|
||||||
|
|
||||||
|
def factory(*args, **kwargs):
|
||||||
|
n["count"] += 1
|
||||||
|
if n["count"] == 1:
|
||||||
|
return first
|
||||||
|
raise WebSocketException("connection refused")
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote_engine, "create_connection", factory)
|
||||||
|
|
||||||
|
katrain = FakeKatrain()
|
||||||
|
engine = RemoteKataGoEngine(katrain, {"remote_url": "ws://test", "allow_recovery": True})
|
||||||
|
try:
|
||||||
|
assert wait_until(lambda: engine.ws is first)
|
||||||
|
|
||||||
|
first.drop()
|
||||||
|
|
||||||
|
# All reconnect attempts fail -> fall back to the recovery popup,
|
||||||
|
# tagged as a remote disconnect rather than a local crash.
|
||||||
|
assert wait_until(lambda: popup_codes(katrain) == ["REMOTE-DISCONNECTED"])
|
||||||
|
|
||||||
|
# The popup is told this is a remote engine so it shows remote-
|
||||||
|
# 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 popup_call[3] == "remote"
|
||||||
|
finally:
|
||||||
|
engine.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_alive_polling_during_reconnect_does_not_suppress_popup(monkeypatch, fast_backoff):
|
||||||
|
# Reproduces the ai.py move loops, which spin on check_alive(exception_if_dead=True)
|
||||||
|
# every ~10ms. While reconnecting the socket is briefly None; those polls must not
|
||||||
|
# latch the engine "dead" and steal the recovery popup that fires on real failure.
|
||||||
|
monkeypatch.setattr(RemoteKataGoEngine, "RECONNECT_ATTEMPTS", 4)
|
||||||
|
first = FakeWS()
|
||||||
|
n = {"count": 0}
|
||||||
|
|
||||||
|
def factory(*args, **kwargs):
|
||||||
|
n["count"] += 1
|
||||||
|
if n["count"] == 1:
|
||||||
|
return first
|
||||||
|
raise WebSocketException("connection refused")
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote_engine, "create_connection", factory)
|
||||||
|
|
||||||
|
katrain = FakeKatrain()
|
||||||
|
engine = RemoteKataGoEngine(katrain, {"remote_url": "ws://test", "allow_recovery": True})
|
||||||
|
|
||||||
|
stop = threading.Event()
|
||||||
|
seen_alive_while_reconnecting = {"v": False}
|
||||||
|
|
||||||
|
def poller():
|
||||||
|
while not stop.is_set():
|
||||||
|
alive = engine.check_alive(exception_if_dead=True)
|
||||||
|
if engine._reconnecting and alive:
|
||||||
|
seen_alive_while_reconnecting["v"] = True
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
t = threading.Thread(target=poller, daemon=True)
|
||||||
|
try:
|
||||||
|
assert wait_until(lambda: engine.ws is first)
|
||||||
|
t.start()
|
||||||
|
first.drop()
|
||||||
|
|
||||||
|
# Despite aggressive polling, the popup still fires exactly once.
|
||||||
|
assert wait_until(lambda: popup_codes(katrain) == ["REMOTE-DISCONNECTED"])
|
||||||
|
time.sleep(0.05) # let the poller keep hammering after the report
|
||||||
|
assert popup_codes(katrain) == ["REMOTE-DISCONNECTED"]
|
||||||
|
# And check_alive reported "alive" during the reconnect window, not dead.
|
||||||
|
assert seen_alive_while_reconnecting["v"]
|
||||||
|
finally:
|
||||||
|
stop.set()
|
||||||
|
t.join(timeout=1)
|
||||||
|
engine.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_game_clears_resend_backlog(monkeypatch, fast_backoff):
|
||||||
|
created = []
|
||||||
|
|
||||||
|
def factory(*args, **kwargs):
|
||||||
|
ws = FakeWS()
|
||||||
|
created.append(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote_engine, "create_connection", factory)
|
||||||
|
|
||||||
|
katrain = FakeKatrain()
|
||||||
|
engine = RemoteKataGoEngine(katrain, {"remote_url": "ws://test", "allow_recovery": True})
|
||||||
|
try:
|
||||||
|
ws1 = created[0]
|
||||||
|
engine.send_query({"foo": "bar"}, lambda *a: None, None)
|
||||||
|
assert wait_until(lambda: len(engine.sent_payloads) == 1)
|
||||||
|
|
||||||
|
# Starting a new game drops outstanding queries; they must not be
|
||||||
|
# resurrected by a later reconnect.
|
||||||
|
engine.on_new_game()
|
||||||
|
assert engine.sent_payloads == {}
|
||||||
|
|
||||||
|
ws1.drop()
|
||||||
|
assert wait_until(lambda: len(created) == 2)
|
||||||
|
ws2 = created[1]
|
||||||
|
assert wait_until(lambda: engine.ws is ws2)
|
||||||
|
# The previous game's analysis query is not resurrected (a leftover
|
||||||
|
# `terminate` command queued by on_new_game may still be sent).
|
||||||
|
time.sleep(0.1)
|
||||||
|
assert all("foo" not in payload for payload in ws2.sent)
|
||||||
|
finally:
|
||||||
|
engine.shutdown()
|
||||||
Reference in new issue
Block a user