diff --git a/.gitignore b/.gitignore
index 031afa54..df2424a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+dist_sgf
+
# additions
KataGoData
katrain/KataGo/KataGoData
diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md
index c3bcd763..5e48e29f 100644
--- a/CONTRIBUTIONS.md
+++ b/CONTRIBUTIONS.md
@@ -48,8 +48,9 @@ Many thanks to these additional authors:
* "kaorahi" for bug fixes and SGF parser improvements.
* "ajkenny84" for the red-green colourblind theme.
* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
-* Carton He for a fix to handling empty komi values in sgfs.
-* "blamarche" for adding the board coordinates toggle
+* Carton He for contributions to sgf parsing and handling.
+* "blamarche" for adding the board coordinates toggle.
+* "pdeblanc" for adding the ancient chinese scoring option.
## Translators
@@ -62,6 +63,7 @@ Many thanks to the following contributors for translations.
* Russian: Dmitry Ivankov and Alexander Kiselev
* Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin
* Japanese: "kaorahi"
+* Traditional Chinese: "Tony-Liou"
## Additional thanks to
diff --git a/README.md b/README.md
index 6b14c2f1..daf7b9c3 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,8 @@
-
+
+
@@ -208,13 +209,13 @@ In addition to shortcuts mentioned above and those shown in the main menu:
## Support / Contribute
[![GitHub issues](http://img.shields.io/github/issues/sanderland/katrain)](http://github.com/sanderland/katrain/issues)
-[![Contributors](http://img.shields.io/static/v1?label=contributors&message=24&color=dcb424)](CONTRIBUTIONS.md)
+[![Contributors](http://img.shields.io/static/v1?label=contributors&message=26&color=dcb424)](CONTRIBUTIONS.md)
[![Github sponsors](http://img.shields.io/static/v1?label=sponsor&message=%E2%9D%A4&logo=GitHub&color=dcb424&link=http://github.com/sponsors/sanderland/)](http://github.com/sponsors/sanderland)
* Ideas, feedback, and contributions to code or translations are all very welcome.
* For suggestions and planned improvements, see [open issues](http://github.com/sanderland/katrain/issues) on github to check if the functionality is already planned.
* I am looking for contributors of more translations of both this manual and the program itself. The best way to help with this is to contact me on discord.
-* You can contact me on [discord](http://discord.gg/AjTPFpN) (Sander#3278) or [Reddit](http://reddit.com/u/sanderbaduk) to get help, discuss improvements, or simply show your appreciation.
+* You can contact me on the [Leela Zero & Friends Discord](http://discord.gg/AjTPFpN) (use the #gui channel) to get help, discuss improvements, or simply show your appreciation.
* You can also donate to the project through [Github Sponsors](http://github.com/sponsors/sanderland).
diff --git a/i18n.py b/i18n.py
index 9adf47b8..76fe841c 100644
--- a/i18n.py
+++ b/i18n.py
@@ -70,7 +70,7 @@
po[lang].append(copied_entry)
errors = True
else:
- print(f"MISSING IN DEFAULT AND {lang}", strings_to_langs[msgid])
+ print(f"MISSING IN DEFAULT AND {lang}", msgid)
errors = True
for msgid, lang_entries in strings_to_langs.items():
diff --git a/katrain/KataGo/katago b/katrain/KataGo/katago
index a85dce48..e1ad5246 100755
Binary files a/katrain/KataGo/katago and b/katrain/KataGo/katago differ
diff --git a/katrain/KataGo/katago.exe b/katrain/KataGo/katago.exe
index f65582aa..b6347645 100644
Binary files a/katrain/KataGo/katago.exe and b/katrain/KataGo/katago.exe differ
diff --git a/katrain/__main__.py b/katrain/__main__.py
index a49b68e8..1834d358 100644
--- a/katrain/__main__.py
+++ b/katrain/__main__.py
@@ -7,6 +7,7 @@
os.environ["KIVY_AUDIO"] = "sdl2" # some backends hard crash / this seems to be most stable
import kivy
+
kivy.require("2.0.0")
# next, icon
@@ -42,6 +43,7 @@
from queue import Queue
import urllib3
import webbrowser
+import time
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.app import App
@@ -112,9 +114,8 @@ def __init__(self, **kwargs):
self.idle_analysis = False
self.message_queue = Queue()
- self._keyboard = Window.request_keyboard(None, self, "")
- self._keyboard.bind(on_key_down=self._on_keyboard_down)
- Clock.schedule_interval(self.animate_pondering, 0.1)
+ self.last_key_down = None
+ self.last_focus_event = 0
def log(self, message, level=OUTPUT_INFO):
super().log(message, level)
@@ -156,7 +157,23 @@ def start(self):
self.board_gui.trainer_config = self.config("trainer")
self.engine = KataGoEngine(self, self.config("engine"))
threading.Thread(target=self._message_loop_thread, daemon=True).start()
- self._do_new_game()
+ sgf_args = [
+ f
+ for f in sys.argv[1:]
+ if os.path.isfile(f) and any(f.lower().endswith(ext) for ext in ["sgf", "ngf", "gib"])
+ ]
+ if sgf_args:
+ self.load_sgf_file(sgf_args[0], fast=True, rewind=True)
+ else:
+ self._do_new_game()
+
+ Clock.schedule_interval(self.animate_pondering, 0.1)
+ Window.request_keyboard(None, self, "").bind(on_key_down=self._on_keyboard_down, on_key_up=self._on_keyboard_up)
+
+ def set_focus_event(*args):
+ self.last_focus_event = time.time()
+
+ MDApp.get_running_app().root_window.bind(focus=set_focus_event)
def update_gui(self, cn, redraw_board=False):
# Handle prisoners and next player display
@@ -178,11 +195,11 @@ def update_gui(self, cn, redraw_board=False):
# update engine status dot
if not self.engine or not self.engine.katago_process or self.engine.katago_process.poll() is not None:
self.board_controls.engine_status_col = Theme.ENGINE_DOWN_COLOR
- elif len(self.engine.queries) == 0:
+ elif self.engine.is_idle():
self.board_controls.engine_status_col = Theme.ENGINE_READY_COLOR
else:
self.board_controls.engine_status_col = Theme.ENGINE_BUSY_COLOR
- self.board_controls.queries_remaining = len(self.engine.queries)
+ self.board_controls.queries_remaining = self.engine.queries_remaining()
# redraw board/stones
if redraw_board:
@@ -223,7 +240,7 @@ def _do_update_state(
): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
self._do_ai_move(cn)
Clock.schedule_once(self.board_gui.play_stone_sound, 0.25)
- if len(self.engine.queries) == 0 and self.idle_analysis:
+ if self.engine.is_idle() and self.idle_analysis:
self("analyze-extra", "extra", continuous=True)
Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger?
@@ -275,7 +292,13 @@ def _do_new_game(self, move_tree=None, analyze_fast=False, sgf_filename=None):
self.play_mode.switch_ui_mode() # for new game, go to play, for loaded, analyze
self.board_gui.animating_pv = None
self.engine.on_new_game() # clear queries
- self.game = Game(self, self.engine, move_tree=move_tree, analyze_fast=analyze_fast, sgf_filename=sgf_filename)
+ self.game = Game(
+ self,
+ self.engine,
+ move_tree=move_tree,
+ analyze_fast=analyze_fast or not move_tree,
+ sgf_filename=sgf_filename,
+ )
if move_tree:
for bw, player_info in self.players_info.items():
player_info.player_type = PLAYER_HUMAN
@@ -532,6 +555,7 @@ def popup_open(self) -> Popup:
return first_child if isinstance(first_child, Popup) else None
def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
+ self.last_key_down = keycode
ctrl_pressed = "ctrl" in modifiers
if self.controls.note.focus:
return # when making notes, don't allow keyboard shortcuts
@@ -549,11 +573,7 @@ def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
return
shift_pressed = "shift" in modifiers
shortcuts = self.shortcuts
- if keycode[1] == "tab":
- self.play_mode.switch_ui_mode()
- elif keycode[1] == "alt":
- self.nav_drawer.set_state("toggle")
- elif keycode[1] == "spacebar":
+ if keycode[1] == "spacebar":
self.toggle_continuous_analysis()
elif keycode[1] == "k":
self.board_gui.toggle_coordinates()
@@ -610,7 +630,23 @@ def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
filename = f"callgrind.{int(time.time())}.prof"
stats.save(filename, type="callgrind")
self.log(f"wrote profiling results to {filename}", OUTPUT_ERROR)
- return True
+
+ def _on_keyboard_up(self, _keyboard, keycode):
+ if keycode[1] in ["alt", "tab"]:
+ Clock.schedule_once(lambda *_args: self._single_key_action(keycode), 0.05)
+
+ def _single_key_action(self, keycode):
+ if (
+ self.controls.note.focus
+ or self.popup_open
+ or keycode != self.last_key_down
+ or time.time() - self.last_focus_event < 0.2 # this is here to prevent alt-tab from firing alt or tab
+ ):
+ return
+ if keycode[1] == "alt":
+ self.nav_drawer.set_state("toggle")
+ elif keycode[1] == "tab":
+ self.play_mode.switch_ui_mode()
class KaTrainApp(MDApp):
@@ -651,7 +687,6 @@ def build(self):
Window.bind(on_request_close=self.on_request_close)
Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file.decode("utf8")))
-
self.gui = KaTrainGui()
Builder.load_file(popup_kv_file)
return self.gui
diff --git a/katrain/config.json b/katrain/config.json
index 58337137..090e9799 100644
--- a/katrain/config.json
+++ b/katrain/config.json
@@ -6,7 +6,7 @@
"config": "katrain/KataGo/analysis_config.cfg",
"threads": 12,
"max_visits": 500,
- "fast_visits": 50,
+ "fast_visits": 25,
"max_time": 8.0,
"wide_root_noise": 0.0,
"_enable_ownership": true
@@ -17,7 +17,7 @@
"anim_pv_time": 0.5,
"debug_level": 0,
"lang": "en",
- "version": "1.7.0"
+ "version": "1.7.1"
},
"timer": {
"byo_length": 30,
diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py
index c5e2e491..d562fcf4 100644
--- a/katrain/core/base_katrain.py
+++ b/katrain/core/base_katrain.py
@@ -141,7 +141,7 @@ def config(self, setting, default=None):
cat, key = setting.split("/")
return self._config.get(cat, {}).get(key, default)
else:
- return self._config[setting]
+ return self._config.get(setting, default)
except KeyError:
self.log(f"Missing configuration option {setting}", OUTPUT_ERROR)
diff --git a/katrain/core/constants.py b/katrain/core/constants.py
index a7d2a99f..c955d03a 100644
--- a/katrain/core/constants.py
+++ b/katrain/core/constants.py
@@ -1,5 +1,5 @@
PROGRAM_NAME = "KaTrain"
-VERSION = "1.7.0"
+VERSION = "1.7.1"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version
ANALYSIS_FORMAT_VERSION = "1.0"
diff --git a/katrain/core/engine.py b/katrain/core/engine.py
index 56c7ea99..6db94124 100644
--- a/katrain/core/engine.py
+++ b/katrain/core/engine.py
@@ -1,6 +1,7 @@
import copy
import json
import os
+import queue
import shlex
import subprocess
import threading
@@ -14,7 +15,7 @@
from katrain.core.game_node import GameNode
from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move
-from katrain.core.utils import find_package_resource
+from katrain.core.utils import find_package_resource, json_truncate_arrays
class EngineDiedException(Exception):
@@ -25,7 +26,13 @@ class KataGoEngine:
"""Starts and communicates with the KataGO analysis engine"""
# TODO: we don't support suicide in game.py, so no "tt": "tromp-taylor", "nz": "new-zealand"
- RULESETS_ABBR = [("jp", "japanese"), ("cn", "chinese"), ("ko", "korean"), ("aga", "aga")]
+ RULESETS_ABBR = [
+ ("jp", "japanese"),
+ ("cn", "chinese"),
+ ("ko", "korean"),
+ ("aga", "aga"),
+ ("stone_scoring", "stone_scoring"),
+ ]
RULESETS = {fromkey: name for abbr, name in RULESETS_ABBR for fromkey in [abbr, name]}
@staticmethod
@@ -40,10 +47,11 @@ def __init__(self, katrain, config):
self.katago_process = None
self.base_priority = 0
self.override_settings = {"reportAnalysisWinratesAs": "BLACK"} # force these settings
- self._lock = threading.Lock()
self.analysis_thread = None
self.stderr_thread = None
+ self.write_stdin_thread = None
self.shell = False
+ self.write_queue = queue.Queue()
exe = config.get("katago", "").strip()
if config.get("altcommand", ""):
@@ -84,6 +92,7 @@ def __init__(self, katrain, config):
self.start()
def start(self):
+ self.write_queue = queue.Queue()
try:
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
startupinfo = None
@@ -100,13 +109,16 @@ def start(self):
)
except (FileNotFoundError, PermissionError, OSError) as e:
self.katrain.log(
- i18n._("Starting Kata failed").format(command=self.command, error=e), OUTPUT_ERROR,
+ i18n._("Starting Kata failed").format(command=self.command, error=e),
+ OUTPUT_ERROR,
)
return # don't start
self.analysis_thread = threading.Thread(target=self._analysis_read_thread, daemon=True)
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
+ self.write_stdin_thread = threading.Thread(target=self._write_stdin_thread, daemon=True)
self.analysis_thread.start()
self.stderr_thread.start()
+ self.write_stdin_thread.start()
def on_new_game(self):
self.base_priority += 1
@@ -129,7 +141,8 @@ def check_alive(self, os_error="", exception_if_dead=False):
else:
os_error += f"status {code}"
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
- self.katrain.log(died_msg, OUTPUT_ERROR)
+ if code != 1: # deliberate exit, already showed message?
+ self.katrain.log(died_msg, OUTPUT_ERROR)
self.katago_process = None
else:
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
@@ -147,13 +160,15 @@ def shutdown(self, finish=False):
if process:
self.katago_process = None
process.terminate()
- if self.stderr_thread:
- self.stderr_thread.join()
- if self.analysis_thread:
- self.analysis_thread.join()
+ for t in [self.stderr_thread, self.analysis_thread, self.write_stdin_thread]:
+ if t:
+ t.join()
def is_idle(self):
- return not self.queries
+ return not self.queries and self.write_queue.empty()
+
+ def queries_remaining(self):
+ return len(self.queries) + int(not self.write_queue.empty())
def _read_stderr_thread(self):
while self.katago_process is not None:
@@ -215,7 +230,7 @@ def _analysis_read_thread(self):
f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] KataGo analysis received: {len(analysis.get('moveInfos',[]))} candidate moves, {analysis['rootInfo']['visits'] if results_exist else 'n/a'} visits",
OUTPUT_DEBUG,
)
- self.katrain.log(line, OUTPUT_EXTRA_DEBUG)
+ self.katrain.log(json_truncate_arrays(analysis), OUTPUT_EXTRA_DEBUG)
try:
if callback and results_exist:
callback(analysis, partial_result)
@@ -227,19 +242,25 @@ def _analysis_read_thread(self):
self.katrain.log(f"Unexpected exception {e} while processing KataGo output {line}", OUTPUT_ERROR)
traceback.print_exc()
- def send_query(self, query, callback, error_callback, next_move=None):
- with self._lock:
- self.query_counter += 1
+ def _write_stdin_thread(self): # flush only in a thread since it returns only when the other program reads
+ while self.katago_process is not None:
+ try:
+ query, callback, error_callback, next_move = self.write_queue.get(block=True, timeout=0.1)
+ except queue.Empty:
+ continue
if "id" not in query:
+ self.query_counter += 1
query["id"] = f"QUERY:{str(self.query_counter)}"
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
- if self.katago_process:
- self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
- try:
- self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
- self.katago_process.stdin.flush()
- except OSError as e:
- self.check_alive(os_error=str(e), exception_if_dead=True)
+ self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
+ try:
+ self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
+ self.katago_process.stdin.flush()
+ except OSError as e:
+ self.check_alive(os_error=str(e), exception_if_dead=False)
+
+ def send_query(self, query, callback, error_callback, next_move=None):
+ self.write_queue.put((query, callback, error_callback, next_move))
def terminate_query(self, query_id):
if query_id is not None:
@@ -319,7 +340,7 @@ def request_analysis(
"includeMovesOwnership": ownership and not next_move,
"includePolicy": not next_move,
"initialStones": [[m.player, m.gtp()] for m in initial_stones],
- "initialPlayer": analysis_node.root.next_player,
+ "initialPlayer": analysis_node.initial_player,
"moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": {**settings, **(extra_settings or {})},
}
diff --git a/katrain/core/game.py b/katrain/core/game.py
index dcf8401d..603d78d9 100644
--- a/katrain/core/game.py
+++ b/katrain/core/game.py
@@ -67,11 +67,21 @@ def __init__(
self.root = move_tree
self.external_game = PROGRAM_NAME not in self.root.get_property("AP", "")
self.komi = self.root.komi
- handicap = int(self.root.get_property("HA", 0))
+ handicap = int(self.root.handicap)
+ num_starting_moves_black = 0
+ node = self.root
+ while node.children:
+ node = node.children[0]
+ if node.player == "B":
+ num_starting_moves_black += 1
+ else:
+ break
+
if (
handicap >= 2
and not self.root.placements
- and not (not self.root.move_with_placements and self.root.children and self.root.children[0].placements)
+ and not (num_starting_moves_black == handicap)
+ and not (self.root.children and self.root.children[0].placements)
): # not really according to sgf, and not sure if still needed, last clause for fox
self.root.place_handicap_stones(handicap)
else:
@@ -109,7 +119,8 @@ def __init__(
def analyze_all_nodes(self, priority=0, analyze_fast=False, even_if_present=True):
for node in self.root.nodes_in_tree:
- if even_if_present or not node.analysis_loaded:
+ # forced, or not present, or something went wrong in loading
+ if even_if_present or not node.analysis_from_sgf or not node.load_analysis():
node.clear_analysis()
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
@@ -396,7 +407,7 @@ def __repr__(self):
+ f"\ncaptures: {self.prisoner_count}"
)
- def generate_filename(self):
+ def update_root_properties(self):
def player_name(player_info):
if player_info.name and player_info.player_type == PLAYER_HUMAN:
return player_info.name
@@ -413,23 +424,23 @@ def player_name(player_info):
x_properties[bw + "R"] = rank_label(player_info.calculated_rank)
if "+" in str(self.end_result):
x_properties["RE"] = self.end_result
- x_properties["KTV"] = ANALYSIS_FORMAT_VERSION
self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}}
+
+ def generate_filename(self):
+ self.update_root_properties()
player_names = {
bw: re.sub(r"[\u200b\u3164'<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"
}
base_game_name = f"{PROGRAM_NAME}_{player_names['B']} vs {player_names['W']}"
return f"{base_game_name} {self.game_id}.sgf"
- def write_sgf(
- self, filename: str = None, trainer_config: Optional[Dict] = None,
- ):
+ def write_sgf(self, filename: str, trainer_config: Optional[Dict] = None):
if trainer_config is None:
trainer_config = self.katrain.config("trainer", {})
save_feedback = trainer_config.get("save_feedback", False)
eval_thresholds = trainer_config["eval_thresholds"]
save_analysis = trainer_config.get("save_analysis", False)
-
+ self.update_root_properties()
show_dots_for = {
bw: trainer_config.get("eval_show_ai", True) or self.katrain.players_info[bw].human for bw in "BW"
}
@@ -570,7 +581,7 @@ def set_analysis(result, _partial):
analyze_and_play_policy(new_node)
self.engines[node.next_player].request_analysis(
- new_node, callback=set_analysis, priority=-1000, analyze_fast=True,
+ new_node, callback=set_analysis, priority=-1000, analyze_fast=True
)
analyze_and_play_policy(cn)
diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py
index 148b3375..249fc055 100644
--- a/katrain/core/game_node.py
+++ b/katrain/core/game_node.py
@@ -8,10 +8,10 @@
from katrain.core.constants import (
ANALYSIS_FORMAT_VERSION,
PROGRAM_NAME,
+ REPORT_DT,
SGF_INTERNAL_COMMENTS_MARKER,
SGF_SEPARATOR_MARKER,
VERSION,
- REPORT_DT,
)
from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move, SGFNode
@@ -33,19 +33,6 @@ def analysis_dumps(analysis):
]
-def analysis_loads(property_array, board_squares, version):
- if version > ANALYSIS_FORMAT_VERSION:
- raise ValueError(f"Can not decode analysis data with version {version}, please update {PROGRAM_NAME}")
- ownership_data, policy_data, main_data, *_ = [
- gzip.decompress(base64.standard_b64decode(data)) for data in property_array
- ]
- return {
- **json.loads(main_data),
- "policy": unpack_floats(policy_data, board_squares + 1),
- "ownership": unpack_floats(ownership_data, board_squares),
- }
-
-
class GameNode(SGFNode):
"""Represents a single game node, with one or more moves and placements."""
@@ -60,7 +47,7 @@ def __init__(self, parent=None, properties=None, move=None):
self.end_state = None
self.shortcuts_to = []
self.shortcut_from = None
- self.analysis_loaded = False
+ self.analysis_from_sgf = None
self.clear_analysis()
def add_shortcut(self, to_node): # collapses the branch between them
@@ -78,15 +65,31 @@ def remove_shortcut(self):
from_node.shortcuts_to = [(m, v) for m, v in from_node.shortcuts_to if m != self]
self.shortcut_from = None
+ def load_analysis(self):
+ if not self.analysis_from_sgf:
+ return False
+ try:
+ szx, szy = self.root.board_size
+ board_squares = szx * szy
+ version = self.root.get_property("KTV", ANALYSIS_FORMAT_VERSION)
+ if version > ANALYSIS_FORMAT_VERSION:
+ raise ValueError(f"Can not decode analysis data with version {version}, please update {PROGRAM_NAME}")
+ ownership_data, policy_data, main_data, *_ = [
+ gzip.decompress(base64.standard_b64decode(data)) for data in self.analysis_from_sgf
+ ]
+ self.analysis = {
+ **json.loads(main_data),
+ "policy": unpack_floats(policy_data, board_squares + 1),
+ "ownership": unpack_floats(ownership_data, board_squares),
+ }
+ return True
+ except Exception as e:
+ print(f"Error in loading analysis: {e}")
+ return False
+
def add_list_property(self, property: str, values: List):
if property == "KT":
- try:
- szx, szy = self.root.board_size
- version = self.root.get_property("KTV", "")
- self.analysis = analysis_loads(values, szx * szy, version)
- self.analysis_loaded = True
- except Exception as e:
- print(f"Error in loading analysis: {e}")
+ self.analysis_from_sgf = values
elif property == "C":
comments = [ # strip out all previously auto generated comments
c
@@ -126,7 +129,9 @@ def sgf_properties(
candidate_moves = self.parent.candidate_moves
top_x = Move.from_gtp(candidate_moves[0]["move"]).sgf(self.board_size)
best_sq = [
- Move.from_gtp(d["move"]).sgf(self.board_size) for d in candidate_moves[1:] if d["pointsLost"] <= 0.5
+ Move.from_gtp(d["move"]).sgf(self.board_size)
+ for d in candidate_moves
+ if d["pointsLost"] <= 0.5 and d["move"] != "pass" and d["order"] != 0
]
if best_sq and "SQ" not in properties:
properties["SQ"] = best_sq
@@ -141,6 +146,7 @@ def sgf_properties(
]
properties["CA"] = ["UTF-8"]
properties["AP"] = [f"{PROGRAM_NAME}:{VERSION}"]
+ properties["KTV"] = [ANALYSIS_FORMAT_VERSION]
if self.shortcut_from:
properties["KTSF"] = [id(self.shortcut_from)]
elif "KTSF" in properties:
@@ -176,11 +182,10 @@ def analyze(
region_of_interest=None,
report_every=REPORT_DT,
):
- additional_moves = bool(find_alternatives or region_of_interest)
engine.request_analysis(
self,
callback=lambda result, partial_result: self.set_analysis(
- result, refine_move, additional_moves, partial_result
+ result, refine_move, find_alternatives, region_of_interest, partial_result
),
priority=priority,
visits=visits,
@@ -210,6 +215,7 @@ def set_analysis(
analysis_json: Dict,
refine_move: Optional[Move] = None,
additional_moves: bool = False,
+ region_of_interest=None,
partial_result: bool = False,
):
if refine_move:
@@ -218,25 +224,25 @@ def set_analysis(
{"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp()
)
else:
- if additional_moves:
+ if additional_moves: # additional moves: old order matters, ignore new order
for m in analysis_json["moveInfos"]:
- del m["order"] # avoid changing order
- if refine_move is None and not additional_moves:
+ del m["order"]
+ elif refine_move is None: # normal update: old moves to end, new order matters. also for region?
for move_dict in self.analysis["moves"].values():
move_dict["order"] = 999 # old moves to end
for move_analysis in analysis_json["moveInfos"]:
self.update_move_analysis(move_analysis, move_analysis["move"])
self.analysis["ownership"] = analysis_json.get("ownership")
self.analysis["policy"] = analysis_json.get("policy")
- if not additional_moves:
+ if not additional_moves and not region_of_interest:
self.analysis["root"] = analysis_json["rootInfo"]
- if self.parent and self.move:
- analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + (
- analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
- )
- self.parent.update_move_analysis(
- analysis_json["rootInfo"], self.move.gtp()
- ) # update analysis in parent for consistency
+ if self.parent and self.move:
+ analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + (
+ analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
+ )
+ self.parent.update_move_analysis(
+ analysis_json["rootInfo"], self.move.gtp()
+ ) # update analysis in parent for consistency
is_normal_query = refine_move is None and not additional_moves
self.analysis["completed"] = self.analysis["completed"] or (is_normal_query and not partial_result)
@@ -313,7 +319,7 @@ def comment(self, sgf=False, teach=False, details=False, interactive=True):
text += i18n._("Info:point loss").format(points_lost=points_lost) + "\n"
top_move = previous_top_move["move"]
score = self.format_score(previous_top_move["scoreLead"])
- text += i18n._("Info:top move").format(top_move=top_move, score=score,) + "\n"
+ text += i18n._("Info:top move").format(top_move=top_move, score=score) + "\n"
else:
text += i18n._("Info:best move") + "\n"
if previous_top_move.get("pv") and (sgf or details):
diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py
index 158d6a83..441780f3 100644
--- a/katrain/core/sgf_parser.py
+++ b/katrain/core/sgf_parser.py
@@ -1,4 +1,5 @@
import copy
+import chardet
import math
import re
from collections import defaultdict
@@ -29,7 +30,9 @@ def from_gtp(cls, gtp_coords, player="B"):
@classmethod
def from_sgf(cls, sgf_coords, board_size, player="B"):
"""Initialize a move from SGF coordinates and player"""
- if sgf_coords == "" or Move.SGF_COORD.index(sgf_coords[0]) == board_size[0]: # some servers use [tt] for pass
+ if sgf_coords == "" or (
+ sgf_coords == "tt" and board_size[0] <= 19 and board_size[1] <= 19
+ ): # [tt] can be used as "pass" for <= 19x19 board
return cls(coords=None, player=player)
return cls(
coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size[1] - Move.SGF_COORD.index(sgf_coords[1]) - 1),
@@ -210,6 +213,13 @@ def komi(self) -> float:
return km
+ @property
+ def handicap(self) -> int:
+ try:
+ return int(self.root.get_property("HA", 0))
+ except ValueError:
+ return 0
+
@property
def ruleset(self) -> str:
"""Retrieves the root's RU property, or 'japanese' if missing"""
@@ -291,17 +301,33 @@ def play(self, move) -> "SGFNode":
return self.__class__(parent=self, move=move)
@property
- def next_player(self):
- """Returns player to move"""
- if "PL" in self.properties: # explicit
+ def initial_player(self): # player for first node
+ root = self.root
+ if "PL" in root.properties: # explicit
return "B" if self.get_property("PL").upper().strip() == "B" else "W"
- elif "B" in self.properties or (
- "AB" in self.properties and "W" not in self.properties and "AW" not in self.properties
- ): # b move or setup with only black moves like root handicap
+ elif root.children: # child exist, use it if not placement
+ for child in root.children:
+ for color in "BW":
+ if color in child.properties:
+ return color
+ # b move or setup with only black moves like handicap
+ if "AB" in self.properties and "AW" not in self.properties:
return "W"
else:
return "B"
+ @property
+ def next_player(self):
+ """Returns player to move"""
+ if self.is_root:
+ return self.initial_player
+ elif "B" in self.properties:
+ return "W"
+ elif "W" in self.properties:
+ return "B"
+ else: # only placements, find a parent node with a real move. TODO: better placement support
+ return self.parent.next_player
+
@property
def player(self):
"""Returns player that moved last. nb root is considered white played if no handicap stones are placed"""
@@ -343,7 +369,7 @@ def place_handicap_stones(self, n_handicaps, tygem=False):
class SGF:
- DEFAULT_ENCODING = "ISO-8859-1" # as specified by the standard
+ DEFAULT_ENCODING = "UTF-8"
_NODE_CLASS = SGFNode # Class used for SGF Nodes, can change this to something that inherits from SGFNode
# https://xkcd.com/1171/
@@ -383,11 +409,14 @@ def parse_file(cls, filename, encoding=None) -> SGFNode:
if match:
encoding = match[1].decode("ascii", errors="ignore")
else:
- encoding = cls.DEFAULT_ENCODING
- try:
- decoded = bin_contents.decode(encoding=encoding, errors="ignore")
- except LookupError:
- decoded = bin_contents.decode(encoding=cls.DEFAULT_ENCODING, errors="ignore")
+ encoding = chardet.detect(bin_contents[:300])["encoding"]
+ # workaround for some compatibility issues for Windows-1252 and GB2312 encodings
+ if encoding == "Windows-1252" or encoding == "GB2312":
+ encoding = "GBK"
+ try:
+ decoded = bin_contents.decode(encoding=encoding, errors="ignore")
+ except LookupError:
+ decoded = bin_contents.decode(encoding=cls.DEFAULT_ENCODING, errors="ignore")
if is_ngf:
return cls.parse_ngf(decoded)
if is_gib:
diff --git a/katrain/core/utils.py b/katrain/core/utils.py
index 9bd57386..7d01a2ea 100644
--- a/katrain/core/utils.py
+++ b/katrain/core/utils.py
@@ -57,10 +57,14 @@ def find_package_resource(path, silent_errors=False):
def pack_floats(float_list):
+ if float_list is None:
+ return b""
return struct.pack(f"{len(float_list)}e", *float_list)
def unpack_floats(str, num):
+ if not str:
+ return None
return struct.unpack(f"{num}e", str)
@@ -72,3 +76,16 @@ def format_visits(n):
if n < 1e6:
return f"{n/1000:.0f}k"
return f"{n/1e6:.0f}k"
+
+
+def json_truncate_arrays(data, lim=20):
+ if isinstance(data, list):
+ if data and isinstance(data[0], dict):
+ return [json_truncate_arrays(d) for d in data]
+ if len(data) > lim:
+ data = [f"{len(data)} x {type(data[0]).__name__}"]
+ return data
+ elif isinstance(data, dict):
+ return {k: json_truncate_arrays(v) for k, v in data.items()}
+ else:
+ return data
diff --git a/katrain/gui.kv b/katrain/gui.kv
index 78a86975..c2fb0022 100644
--- a/katrain/gui.kv
+++ b/katrain/gui.kv
@@ -271,6 +271,7 @@
outline_color: Theme.BOX_BACKGROUND_COLOR
outline_width: 2
padding: [2*CP_PADDING,CP_PADDING,CP_PADDING,CP_PADDING]
+ subtype_label: subtype_label
CircleWithText:
size_hint: 0.32, 1
player: root.player
@@ -289,7 +290,8 @@
halign: 'center'
valign: 'middle'
Label: # todo: proper scaling/shortening label instead of this jank
- text: (root.name if root.name and root.player_type==PLAYER_HUMAN and root.player_subtype == PLAYING_NORMAL else i18n._(root.player_subtype) ) + (" ({})".format(root.rank) if root.rank and root.player_subtype != PLAYING_TEACHING else "")
+ id: subtype_label
+ text: i18n._(root.player_subtype)
size_hint: 1,0.4
font_size: self.height * 0.7 if root.player_type==PLAYER_HUMAN else self.height * 0.7 * min(1,18/len(self.text))
text_size: self.size if root.player_type==PLAYER_HUMAN else (None,self.height)
@@ -942,7 +944,6 @@
icon: 'New-Game.png'
shortcut: 'Ctrl-N'
on_action: root.katrain("new-game-popup")
- height: root.item_height
MainMenuItem:
text: i18n._('menu:save')
icon: 'Save-Game.png'
@@ -1002,6 +1003,9 @@
LangButton:
icon: 'flags/flag-cn.png'
on_press: app.language = 'cn'
+ LangButton:
+ icon: 'flags/flag-tw.png'
+ on_press: app.language = 'tw'
LangButton:
icon: 'flags/flag-ko.png'
on_press: app.language = 'ko'
@@ -1071,18 +1075,18 @@
AnalysisControls:
id: analysis_controls
size_hint_y: None
- height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5))
+ height: 1 if root.zen > 0 else max(sp(Theme.CONTROLS_PANEL_MIN_HEIGHT),min(sp(Theme.CONTROLS_PANEL_MAX_HEIGHT),self.width / Theme.CONTROLS_PANEL_ASPECT_RATIO))
opacity: 0 if root.zen > 0 else 1
BadukPanWidget:
id: board_gui
BadukPanControls:
id: board_controls
size_hint_y: None
- height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5))
+ height: 1 if root.zen > 0 else max(sp(Theme.CONTROLS_PANEL_MIN_HEIGHT),min(sp(Theme.CONTROLS_PANEL_MAX_HEIGHT),self.width / Theme.CONTROLS_PANEL_ASPECT_RATIO))
opacity: 0 if root.zen > 0 else 1
MDBoxLayout:
size_hint_x: None
- width: 1 if root.zen == 2 else self.height * 0.4
+ width: 1 if root.zen == 2 else self.height * Theme.RIGHT_PANEL_ASPECT_RATIO
opacity: 0 if root.zen == 2 else 1
orientation: 'vertical'
PlayAnalyzeSelect:
diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py
index eacf143a..3d3047d4 100644
--- a/katrain/gui/badukpan.py
+++ b/katrain/gui/badukpan.py
@@ -31,7 +31,7 @@
)
from katrain.core.game import Move
from katrain.core.lang import i18n
-from katrain.core.utils import evaluation_class, format_visits, var_to_grid
+from katrain.core.utils import evaluation_class, format_visits, var_to_grid, json_truncate_arrays
from katrain.gui.kivyutils import draw_circle, draw_text, cached_texture
from katrain.gui.popups import I18NPopup, ReAnalyzeGamePopup
from katrain.gui.theme import Theme
@@ -186,9 +186,16 @@ def on_touch_up(self, touch):
katrain.game.set_current_node(nodes_here[-1].parent)
katrain.update_state()
else: # load comments & pv
- katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_EXTRA_DEBUG)
- katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_EXTRA_DEBUG)
- katrain.log(f"\nRoot Stats:\n{nodes_here[-1].analysis['root']}", OUTPUT_DEBUG)
+ katrain.log(
+ f"\nAnalysis:\n{json_truncate_arrays(nodes_here[-1].analysis,lim=5)}", OUTPUT_EXTRA_DEBUG
+ )
+ katrain.log(
+ f"\nParent Analysis:\n{json_truncate_arrays(nodes_here[-1].parent.analysis,lim=5)}",
+ OUTPUT_EXTRA_DEBUG,
+ )
+ katrain.log(
+ f"\nRoot Stats:\n{json_truncate_arrays(nodes_here[-1].analysis['root'],lim=5)}", OUTPUT_DEBUG
+ )
katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
katrain.controls.active_comment_node = nodes_here[-1]
if nodes_here[-1].parent.analysis_exists:
@@ -466,13 +473,12 @@ def draw_board_contents(self, *_args):
pos=(self.gridpos_x[x], self.gridpos_y[y]),
text=f"{100 * move_policy :.2f}"[:4] + "%",
font_name="Roboto",
+ font_size=self.grid_size / 4,
halign="center",
)
if move_policy == best_move_policy:
Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA)
- Line(
- circle=(self.gridpos_x[x], self.gridpos_y[y], self.stone_size - dp(1.2),), width=dp(2),
- )
+ Line(circle=(self.gridpos_x[x], self.gridpos_y[y], self.stone_size - dp(1.2)), width=dp(2))
with pass_btn.canvas.after:
move_policy = policy[-1]
diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py
index 5fa5e6f5..ef4f7324 100644
--- a/katrain/gui/controlspanel.py
+++ b/katrain/gui/controlspanel.py
@@ -24,9 +24,9 @@ def save_ui_state(self):
self.katrain._config["ui_state"] = self.katrain._config.get("ui_state", {})
self.katrain._config["ui_state"][self.mode] = {
"analysis_controls": {
- id: checkbox.active
- for id, checkbox in self.katrain.analysis_controls.ids.items()
- if isinstance(checkbox, AnalysisToggle)
+ id: toggle.active if not toggle.checkbox.slashed else None # troolean ftw
+ for id, toggle in self.katrain.analysis_controls.ids.items()
+ if isinstance(toggle, AnalysisToggle)
},
"panels": {
id: (panel.state, panel.option_state)
@@ -39,7 +39,10 @@ def save_ui_state(self):
def load_ui_state(self, _dt=None):
state = self.katrain.config(f"ui_state/{self.mode}", {})
for id, active in state.get("analysis_controls", {}).items():
- self.katrain.analysis_controls.ids[id].checkbox.active = active
+ cb = self.katrain.analysis_controls.ids[id].checkbox
+ cb.active = bool(active)
+ if cb.tri_state:
+ cb.slashed = active is None
for id, (panel_state, button_state) in state.get("panels", {}).items():
self.katrain.controls.ids[id].set_option_state(button_state)
self.katrain.controls.ids[id].state = panel_state
diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py
index fbf6a819..ead23747 100644
--- a/katrain/gui/kivyutils.py
+++ b/katrain/gui/kivyutils.py
@@ -28,7 +28,15 @@
from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField
-from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, MODE_PLAY, PLAYER_AI
+from katrain.core.constants import (
+ AI_STRATEGIES_RECOMMENDED_ORDER,
+ GAME_TYPES,
+ MODE_PLAY,
+ PLAYER_AI,
+ PLAYER_HUMAN,
+ PLAYING_NORMAL,
+ PLAYING_TEACHING,
+)
from katrain.core.lang import i18n
from katrain.gui.theme import Theme
@@ -372,6 +380,26 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin):
rank = StringProperty("", allownone=True)
active = BooleanProperty(True)
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.bind(player_type=self.set_label, player_subtype=self.set_label, name=self.set_label, rank=self.set_label)
+
+ def set_label(self, *args):
+ if not self.subtype_label: # building
+ return
+ show_player_name = self.name and self.player_type == PLAYER_HUMAN and self.player_subtype == PLAYING_NORMAL
+ if show_player_name:
+ text = self.name
+ else:
+ text = i18n._(self.player_subtype)
+ if (
+ self.rank
+ and self.player_subtype != PLAYING_TEACHING
+ and (show_player_name or self.player_type == PLAYER_AI)
+ ):
+ text += " ({})".format(self.rank)
+ self.subtype_label.text = text
+
class TimerOrMoveTree(MDBoxLayout):
mode = StringProperty(MODE_PLAY)
@@ -385,6 +413,11 @@ class Timer(BGBoxLayout):
class TriStateMDCheckbox(MDCheckbox):
tri_state = BooleanProperty(False)
slashed = BooleanProperty(False)
+ checkbox_icon_slashed = StringProperty("checkbox-blank-off-outline")
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.bind(slashed=self.update_icon)
def _do_press(self):
if not self.tri_state:
@@ -392,15 +425,21 @@ def _do_press(self):
if self.slashed:
self.state = "normal"
self.slashed = False
- self.icon = "checkbox-blank-outline"
elif self.state == "down":
self.state = "normal"
self.slashed = True
- self.icon = "checkbox-blank-off-outline"
else:
self.state = "down"
self.slashed = False
- self.icon = "checkbox-marked-outline"
+ self.update_icon()
+
+ def update_icon(self, *args):
+ if self.tri_state and self.slashed:
+ self.icon = self.checkbox_icon_slashed
+ elif self.state == "down":
+ self.icon = self.checkbox_icon_down
+ else:
+ self.icon = self.checkbox_icon_normal
class AnalysisToggle(MDBoxLayout):
@@ -624,9 +663,7 @@ def cached_text_texture(text, font_name, markup, _cache={}, **kwargs):
def draw_text(pos, text, font_name=None, markup=False, **kwargs):
texture = cached_text_texture(text, font_name, markup, **kwargs)
- Rectangle(
- texture=texture, pos=(pos[0] - texture.size[0] / 2, pos[1] - texture.size[1] / 2), size=texture.size,
- )
+ Rectangle(texture=texture, pos=(pos[0] - texture.size[0] / 2, pos[1] - texture.size[1] / 2), size=texture.size)
def draw_circle(pos, r, col):
diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py
index b2436e5f..ae286354 100644
--- a/katrain/gui/popups.py
+++ b/katrain/gui/popups.py
@@ -1,10 +1,12 @@
import glob
+import json
import os
import re
import stat
from typing import Any, Dict, List, Tuple, Union
from zipfile import ZipFile
+import urllib3
from kivy.clock import Clock
from kivy.metrics import dp
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty
@@ -25,11 +27,12 @@
AI_KEY_PROPERTIES,
AI_OPTION_VALUES,
AI_STRATEGIES_RECOMMENDED_ORDER,
+ DATA_FOLDER,
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_INFO,
+ SGF_INTERNAL_COMMENTS_MARKER,
STATUS_INFO,
- DATA_FOLDER,
)
from katrain.core.engine import KataGoEngine
from katrain.core.lang import i18n, rank_label
@@ -43,8 +46,10 @@ class I18NPopup(Popup):
title_key = StringProperty("")
font_name = StringProperty(Theme.DEFAULT_FONT)
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
+ def __init__(self, size=None, **kwargs):
+ if size: # do not exceed window height
+ size[1] = min(MDApp.get_running_app().gui.height, size[1])
+ super().__init__(size=size, **kwargs)
self.bind(on_dismiss=Clock.schedule_once(lambda _dt: MDApp.get_running_app().gui.update_state(), 1))
@@ -62,13 +67,15 @@ def raw_input_value(self):
class LabelledPathInput(LabelledTextInput):
+ check_path = BooleanProperty(True)
+
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.check_error, 0)
def check_error(self, _dt=None):
file = find_package_resource(self.input_value, silent_errors=True)
- self.error = not (file and os.path.exists(file))
+ self.error = self.check_path and not (file and os.path.exists(file))
def on_text(self, widget, text):
self.check_error()
@@ -267,7 +274,7 @@ def update_playerinfo(self, *args):
def update_from_current_game(self, *args):
for bw in "BW":
name = self.katrain.game.root.get_property("P" + bw, None)
- if name:
+ if name and SGF_INTERNAL_COMMENTS_MARKER not in name:
self.player_name[bw].text = name
rules = self.normalized_rules()
self.km.text = str(self.katrain.game.root.komi)
@@ -301,7 +308,7 @@ def update_game(self):
self.update_playerinfo()
if changed:
self.katrain.engine.on_new_game()
- self.katrain.game.analyze_all_nodes()
+ self.katrain.game.analyze_all_nodes(analyze_fast=True)
self.popup.dismiss()
@@ -419,6 +426,36 @@ def update_config(self, save_to_file=True):
class ConfigPopup(QuickConfigGui):
+ MODEL_ENDPOINTS = {"Latest distributed model": "https://katagotraining.org/api/networks/newest_training/"}
+ MODELS = {
+ "20 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170e-b20c256x2-s5303129600-d1228401921.bin.gz",
+ "30 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b30c320x2-s4824661760-d1229536699.bin.gz",
+ "40 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b40c256x2-s5095420928-d1229425124.bin.gz",
+ }
+ MODEL_DESC = {
+ "Fat 40 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b40c384x2-s2348692992-d1229892979.zip",
+ "15 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b15c192-s1672170752-d466197061.bin.gz",
+ }
+
+ KATAGOS = {
+ "win": {
+ "OpenCL v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-opencl-windows-x64.zip",
+ "Eigen AVX2 (Modern CPUs) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigenavx2-windows-x64.zip",
+ "Eigen (CPU, Non-optimized) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigen-windows-x64.zip",
+ "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-windows-x64.zip",
+ },
+ "linux": {
+ "OpenCL v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-opencl-linux-x64.zip",
+ "Eigen AVX2 (Modern CPUs) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigenavx2-linux-x64.zip",
+ "Eigen (CPU, Non-optimized) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigen-linux-x64.zip",
+ "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-linux-x64.zip",
+ },
+ "just-descriptions": {
+ "CUDA v1.8.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-gpu-cuda10.2-windows-x64.zip",
+ "CUDA v1.8.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-gpu-cuda10.2-linux-x64.zip",
+ },
+ }
+
def __init__(self, katrain):
super().__init__(katrain)
self.paths = [self.katrain.config("engine/model"), "katrain/models", DATA_FOLDER]
@@ -431,13 +468,17 @@ def build_and_set_properties(self, *_args):
super().build_and_set_properties()
def check_models(self, *args):
+ all_models = [self.MODELS, self.MODEL_DESC, self.katrain.config("dist_models", {})]
+
+ def extract_model_file(model):
+ try:
+ return re.match(r".*/([^/]+)", model)[1].replace(".zip", ".bin.gz")
+ except (TypeError, IndexError):
+ return None
+
def find_description(path):
file = os.path.split(path)[1]
- file_to_desc = {
- re.match(r".*/([^/]+)", model)[1].replace(".zip", ".bin.gz"): desc
- for mods in [self.MODELS, self.MODEL_DESC]
- for desc, model in mods.items()
- }
+ file_to_desc = {extract_model_file(model): desc for mods in all_models for desc, model in mods.items()}
if file in file_to_desc:
return f"{file_to_desc[file]} - {path}"
else:
@@ -515,35 +556,6 @@ def find_description(path):
self.katago_files.value_keys = ["", ""] + [path for path, desc in kata_files]
self.katago_files.text = katas_available_msg
- MODELS = {
- "Latest 20 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170e-b20c256x2-s5303129600-d1228401921.bin.gz",
- "Latest 30 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b30c320x2-s4824661760-d1229536699.bin.gz",
- "Latest 40 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170-b40c256x2-s5095420928-d1229425124.bin.gz",
- }
- MODEL_DESC = {
- "Fat 40 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b40c384x2-s2348692992-d1229892979.zip",
- "Latest 15 block model": "https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/g170e-b15c192-s1672170752-d466197061.bin.gz",
- }
-
- KATAGOS = {
- "win": {
- "OpenCL v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-windows-x64.zip",
- "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-windows-x64.zip",
- "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-windows-x64.zip",
- "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-windows-x64.zip",
- },
- "linux": {
- "OpenCL v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-linux-x64.zip",
- "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-linux-x64.zip",
- "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-linux-x64.zip",
- "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-linux-x64.zip",
- },
- "just-descriptions": {
- "CUDA v1.7.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-windows-x64.zip",
- "CUDA v1.7.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-linux-x64.zip",
- },
- }
-
def download_models(self, *_largs):
def download_complete(req, tmp_path, path, model):
try:
@@ -558,7 +570,21 @@ def download_complete(req, tmp_path, path, model):
c.request.cancel()
self.download_progress_box.clear_widgets()
downloading = False
- for name, url in self.MODELS.items():
+
+ dist_models = {k: v for k, v in self.katrain.config("dist_models", {}).items() if k in self.MODEL_ENDPOINTS}
+
+ for name, url in self.MODEL_ENDPOINTS.items():
+ try:
+ http = urllib3.PoolManager()
+ response = http.request("GET", url)
+ dist_models[name] = json.loads(response.data.decode("utf-8"))["model_file"]
+ except Exception as e:
+ self.katrain.log(f"Failed to retrieve info for model: {e}", OUTPUT_INFO)
+
+ self.katrain._config["dist_models"] = dist_models
+ self.katrain.save_config(key="dist_models")
+
+ for name, url in {**self.MODELS, **dist_models}.items():
filename = os.path.split(url)[1]
if not any(os.path.split(f)[1] == filename for f in self.model_files.values):
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
@@ -607,8 +633,11 @@ def download_complete(req, tmp_path, path, binary):
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR | stat.S_IXGRP)
for f in zipObj.namelist():
if f.lower().endswith("dll"):
- with open(os.path.join(os.path.split(path)[0], f), "wb") as fout:
- fout.write(zipObj.read(f))
+ try:
+ with open(os.path.join(os.path.split(path)[0], f), "wb") as fout:
+ fout.write(zipObj.read(f))
+ except: # already there? no problem
+ pass
os.remove(tmp_path)
else:
os.rename(tmp_path, path)
@@ -662,7 +691,7 @@ def update_config(self, save_to_file=True):
updated = super().update_config(save_to_file=save_to_file)
self.katrain.debug_level = self.katrain.config("general/debug_level", OUTPUT_INFO)
- ignore = {"max_visits", "max_time", "enable_ownership", "wide_root_noise"}
+ ignore = {"max_visits", "fast_visits", "max_time", "enable_ownership", "wide_root_noise"}
detected_restart = [key for key in updated if "engine" in key and not any(ig in key for ig in ignore)]
if detected_restart:
@@ -678,7 +707,9 @@ def restart_engine(_dt):
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() # old engine was possibly broken, so make sure we redo any failures
+ 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)
@@ -695,6 +726,9 @@ def __init__(self, **kwargs):
self.filesel.path = os.path.abspath(os.path.expanduser(app.gui.config("general/sgf_load")))
self.filesel.select_string = i18n._("Load File")
+ def on_submit(self):
+ self.filesel.button_clicked()
+
class SaveSGFPopup(BoxLayout):
def __init__(self, suggested_filename, **kwargs):
@@ -715,7 +749,7 @@ def set_suggested(_widget, path):
self.filesel.select_string = i18n._("Save File")
def on_submit(self):
- self.filesel.dispatch("on_success")
+ self.filesel.button_clicked()
class ReAnalyzeGamePopup(BoxLayout):
diff --git a/katrain/gui/theme.py b/katrain/gui/theme.py
index 3b1296c4..00580918 100644
--- a/katrain/gui/theme.py
+++ b/katrain/gui/theme.py
@@ -49,6 +49,10 @@ class Theme:
MISTAKE_BUTTON_COLOR = [0.79, 0.06, 0.06, 1]
# gui spacing
+ RIGHT_PANEL_ASPECT_RATIO = 0.4 # W/H
+ CONTROLS_PANEL_ASPECT_RATIO = 13.5 # W/H
+ CONTROLS_PANEL_MIN_HEIGHT = 50
+ CONTROLS_PANEL_MAX_HEIGHT = 75 # dp
CP_SPACING = 6
CP_SMALL_SPACING = 3
CP_PADDING = 6
diff --git a/katrain/gui/widgets/filebrowser.py b/katrain/gui/widgets/filebrowser.py
index 9c679ebf..467f4a76 100644
--- a/katrain/gui/widgets/filebrowser.py
+++ b/katrain/gui/widgets/filebrowser.py
@@ -40,7 +40,7 @@ def _fbrowser_submit(self, instance):
import string
from functools import partial
from os import walk
-from os.path import dirname, expanduser, getmtime, isdir, join, sep
+from os.path import dirname, expanduser, getmtime, isdir, isfile, join, sep
from kivy import Config
from kivy.clock import Clock
@@ -172,6 +172,7 @@ class I18NFileChooserListLayout(FileChooserListLayout):
spacing: 5
padding: [6, 6, 6, 6]
select_state: select_button.state
+ file_text: file_text
filename: file_text.text
browser: list_view
on_favorites: link_tree.reload_favs(self.favorites)
@@ -207,7 +208,7 @@ class I18NFileChooserListLayout(FileChooserListLayout):
id: list_view
path: root.path
sort_func: root.sort_func
- filters: root.filters
+ filters: root.filters + [f.upper() for f in root.filters]
filter_dirs: root.filter_dirs
show_hidden: root.show_hidden
multiselect: root.multiselect
@@ -232,7 +233,7 @@ class I18NFileChooserListLayout(FileChooserListLayout):
height: '40dp'
size_hint_x: None
text: root.select_string
- on_release: root.dispatch('on_success')
+ on_release: root.button_clicked()
"""
)
@@ -314,11 +315,12 @@ def trigger_populate(self, node):
class I18NFileBrowser(BoxLayout):
- """I18NFileBrowser class, see module documentation for more information.
- """
+ """I18NFileBrowser class, see module documentation for more information."""
__events__ = ("on_success", "on_submit")
+ file_must_exist = BooleanProperty(False) # whether new file paths can be pointed at
+
select_state = OptionProperty("normal", options=("normal", "down"))
"""State of the 'select' button, must be one of 'normal' or 'down'.
The state is 'down' only when the button is currently touched/clicked,
@@ -464,3 +466,9 @@ def _shorten_filenames(self, filenames):
def _attr_callback(self, attr, obj, value):
setattr(self, attr, getattr(obj, attr))
+
+ def button_clicked(self):
+ if isdir(self.file_text.text):
+ self.path = self.file_text.text
+ elif not self.file_must_exist or isfile(self.file_text.text):
+ self.dispatch("on_success")
diff --git a/katrain/gui/widgets/movetree.py b/katrain/gui/widgets/movetree.py
index a8481088..a83a0417 100644
--- a/katrain/gui/widgets/movetree.py
+++ b/katrain/gui/widgets/movetree.py
@@ -109,7 +109,7 @@ def toggle_selected_node_collapse(self):
def switch_branch(self, direction=1):
pos = self.move_pos.get(self.scroll_view_widget.current_node)
- if not self.scroll_view_widget:
+ if not self.scroll_view_widget or not pos:
return
same_x_moves = sorted([(y, n) for n, (x, y) in self.move_pos.items() if x == pos[0]])
new_index = next((i for i, (y, n) in enumerate(same_x_moves) if y == pos[1]), 0) + direction
@@ -284,7 +284,7 @@ def on_scroll_start(self, touch, check_children=True):
MoveTreeDropdownItem:
text: i18n._("Delete Node")
icon: 'delete.png'
- shortcut: 'Ctrl+Del'
+ shortcut: 'Ctr+Del'
on_action: root.katrain.controls.move_tree.delete_selected_node()
-background_color: Theme.LIGHTER_BACKGROUND_COLOR
-height: dp(45)
diff --git a/katrain/gui/widgets/progress_loader.py b/katrain/gui/widgets/progress_loader.py
index d2c3aae9..c7f4b14b 100644
--- a/katrain/gui/widgets/progress_loader.py
+++ b/katrain/gui/widgets/progress_loader.py
@@ -71,7 +71,7 @@ def start(self, root_instance):
Clock.schedule_once(self.animation_show, 1)
def animation_show(self, _dt):
- animation = Animation(opacity=1, d=0.2, t="out_quad",)
+ animation = Animation(opacity=1, d=0.2, t="out_quad")
animation.start(self)
def request_download_file(self, url, path):
diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo
index 469ca4a4..72b89fef 100644
Binary files a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
index e6e997a4..81bb71a3 100644
--- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
@@ -276,6 +276,10 @@ msgstr "韩国规则"
msgid "aga"
msgstr "AGA规则"
+#. TODO check
+msgid "stone_scoring"
+msgstr "古代中国规则"
+
msgid "clear cache"
msgstr "清除缓存"
@@ -322,11 +326,6 @@ msgstr ""
"AI的\n"
"显示选点/SGF评价"
-msgid "lock ai when playing"
-msgstr ""
-"对局模式中\n"
-"不允许最佳选点"
-
#. submit button
msgid "update teacher"
msgstr "更新指导棋设置"
@@ -383,12 +382,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "时间单位是秒"
-msgid "general:sgf_load"
-msgstr "加载SGF文件路径"
-
-msgid "general:sgf_save"
-msgstr "保存SGF文件路径"
-
msgid "general:anim_pv_time"
msgstr "动画中两手棋时间间隔"
diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo
index 240981d4..3a49e67a 100644
Binary files a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
index cd7890cb..76d22f4a 100644
--- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
@@ -283,6 +283,10 @@ msgstr "Koreanisch"
msgid "aga"
msgstr "AGA"
+#. TODO check
+msgid "stone_scoring"
+msgstr "Alte Chinesisch"
+
msgid "clear cache"
msgstr "Cache leeren"
@@ -329,11 +333,6 @@ msgstr ""
"Dots/SGF-Kommentare \n"
" für AI Spieler zeigen"
-msgid "lock ai when playing"
-msgstr ""
-"Beste Züge während \n"
-" Spiel deaktivieren"
-
#. submit button
msgid "update teacher"
msgstr "Feedbackeinstellungen aktualisieren"
@@ -393,12 +392,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "Zeit in Sekunden"
-msgid "general:sgf_load"
-msgstr "Pfad um SGF-Dateien zu laden"
-
-msgid "general:sgf_save"
-msgstr "Pfad um SGF-Dateien zu speichern"
-
msgid "general:anim_pv_time"
msgstr "Zeit zwischen Zügen, wenn Sequenz animiert wird"
diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo
index c0677f09..2e29a936 100644
Binary files a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
index 20f1d6e9..14178a88 100644
--- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
@@ -394,6 +394,9 @@ msgstr "Korean"
msgid "aga"
msgstr "AGA"
+msgid "stone_scoring"
+msgstr "Ancient Chinese"
+
msgid "clear cache"
msgstr "Clear cache"
@@ -450,12 +453,6 @@ msgstr ""
msgid "cache analysis to sgf"
msgstr "Cache analysis in SGF"
-#. removed as an option / can ignore
-msgid "lock ai when playing"
-msgstr ""
-"Disable top moves\n"
-" while in play mode"
-
#. submit button
msgid "update teacher"
msgstr "Update Feedback Settings"
@@ -523,13 +520,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "Time in seconds"
-#. no longer used, can ignore
-msgid "general:sgf_load"
-msgstr "Path for loading SGF files"
-
-msgid "general:sgf_save"
-msgstr "Path for saving SGF files"
-
msgid "general:anim_pv_time"
msgstr "Time between moves when animating a sequence"
diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo
index 5baaa1ea..b92c9362 100644
Binary files a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
index cff7acec..cb88853d 100644
--- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
@@ -287,6 +287,10 @@ msgstr "Koreanas"
msgid "aga"
msgstr "AGA"
+#. TODO check
+msgid "stone_scoring"
+msgstr "Antiguas Chinas"
+
msgid "clear cache"
msgstr "Vaciar Cache"
@@ -333,11 +337,6 @@ msgstr ""
"Mostrar puntos/comentarios\n"
" para motores de IA"
-msgid "lock ai when playing"
-msgstr ""
-"Deshabilitar jugadas superiores top moves\n"
-" en modo juego"
-
#. submit button
msgid "update teacher"
msgstr "Actualizar conf"
@@ -398,12 +397,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "Tiempo en segundos"
-msgid "general:sgf_load"
-msgstr "Ruta para cargar los archivos SGF"
-
-msgid "general:sgf_save"
-msgstr "Ruta para guardar los archivos SGF"
-
msgid "general:anim_pv_time"
msgstr "Tiempo entre juagadas animando una secuencia"
diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo
index a9301513..0f0aa329 100644
Binary files a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
index 81694f1d..f1d74975 100644
--- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
@@ -302,6 +302,10 @@ msgstr "Coréennes"
msgid "aga"
msgstr "AGA"
+#. TODO check
+msgid "stone_scoring"
+msgstr "Chinoises anciennes"
+
msgid "clear cache"
msgstr "Vider le cache"
@@ -344,11 +348,6 @@ msgstr "Dernier(s) coup(s) à marquer :"
msgid "show ai dots"
msgstr "Étudier les coups du joueur artificiel"
-msgid "lock ai when playing"
-msgstr ""
-"Ne pas recommander de coups \n"
-"en mode “Partie”"
-
#. submit button
msgid "update teacher"
msgstr "Mettre à jour"
@@ -428,16 +427,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "En secondes"
-msgid "general:sgf_load"
-msgstr ""
-"Chemin d'accès du \n"
-"fichier SGF à analyser"
-
-msgid "general:sgf_save"
-msgstr ""
-"Chemin d'accès du \n"
-"fichier SGF à enregistrer"
-
msgid "general:anim_pv_time"
msgstr ""
"Durée entre chaque coup \n"
diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo
index 928ba4ba..3511d380 100644
Binary files a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
index 4ea0d5c7..fcf96b7f 100644
--- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
@@ -308,6 +308,9 @@ msgstr "韓国"
msgid "aga"
msgstr "AGA"
+msgid "stone_scoring"
+msgstr "古代中国"
+
msgid "clear cache"
msgstr "思考記録をクリア"
@@ -354,12 +357,6 @@ msgstr ""
"AIの手にもドットや\n"
"SGFコメントをつける"
-#. removed as an option / can ignore
-msgid "lock ai when playing"
-msgstr ""
-"Disable top moves\n"
-" while in play mode"
-
#. submit button
msgid "update teacher"
msgstr "指導設定を更新"
@@ -419,13 +416,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "秒数"
-#. no longer used, can ignore
-msgid "general:sgf_load"
-msgstr "Path for loading SGF files"
-
-msgid "general:sgf_save"
-msgstr "SGFファイルの保存場所"
-
msgid "general:anim_pv_time"
msgstr "手順アニメーションの一手毎の時間"
diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo
index 0ea1b770..5290e244 100644
Binary files a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
index 7ea4495b..c44af394 100644
--- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
@@ -280,6 +280,10 @@ msgstr "한국"
msgid "aga"
msgstr "AGA"
+#. TODO check
+msgid "stone_scoring"
+msgstr "고대 중국"
+
msgid "clear cache"
msgstr "캐시 지우기"
@@ -326,11 +330,6 @@ msgstr ""
"인공지능 대국자에 대해서도\n"
"평가를 색으로 표시"
-msgid "lock ai when playing"
-msgstr ""
-"대국 중에\n"
-"후보수 분석하지 않기"
-
#. submit button
msgid "update teacher"
msgstr "피드백 설정 갱신"
@@ -392,12 +391,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "시간(초)"
-msgid "general:sgf_load"
-msgstr "SGF 파일을 불러올 경로"
-
-msgid "general:sgf_save"
-msgstr "SGF 파일을 저장할 경로"
-
msgid "general:anim_pv_time"
msgstr "수순을 보여줄 때 수 사이의 시간"
diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo
index b583b4ca..49f48cca 100644
Binary files a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
index 2a3e83f4..489a683e 100644
--- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
@@ -306,6 +306,10 @@ msgstr "Корейские"
msgid "aga"
msgstr "AGA"
+#. TODO check
+msgid "stone_scoring"
+msgstr "Древние китайские"
+
msgid "clear cache"
msgstr "Очистить кэш"
@@ -352,12 +356,6 @@ msgstr ""
"Показать точки и SGF комментарии\n"
" для ИИ игроков"
-#. removed as an option / can ignore
-msgid "lock ai when playing"
-msgstr ""
-"Отключить лучшие ходы\n"
-" во время игры"
-
#. submit button
msgid "update teacher"
msgstr "Сохранить настройки анализа"
@@ -417,13 +415,6 @@ msgstr ""
msgid "engine:time:hint"
msgstr "Время в секундах"
-#. no longer used, can ignore
-msgid "general:sgf_load"
-msgstr "Путь для загрузки файлов SGF"
-
-msgid "general:sgf_save"
-msgstr "Путь для сохранения файлов SGF"
-
msgid "general:anim_pv_time"
msgstr "Задержка между ходами в анимации"
diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo
new file mode 100644
index 00000000..bde68db5
Binary files /dev/null and b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po
new file mode 100644
index 00000000..9e8b9224
--- /dev/null
+++ b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po
@@ -0,0 +1,785 @@
+# KaTrain localization file
+msgid ""
+msgstr ""
+"Language: Traditional Chinese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. main hamburger menu
+msgid "menu:playersetup"
+msgstr "棋手設定"
+
+msgid "menu:newgame"
+msgstr "新對局/更改規則"
+
+msgid "menu:save"
+msgstr "儲存對局"
+
+msgid "menu:save-as"
+msgstr "另存對局為..."
+
+msgid "menu:load"
+msgstr "載入對局"
+
+msgid "menu:settings"
+msgstr "一般和引擎設定"
+
+msgid "menu:clocksettings"
+msgstr "用時設定"
+
+msgid "menu:teachsettings"
+msgstr "指導/分析設定"
+
+msgid "menu:aisettings"
+msgstr "AI設定"
+
+msgid "menu:lang"
+msgstr "語言"
+
+msgid "menu:support"
+msgstr "幫助"
+
+msgid "menu:manual"
+msgstr "使用手册"
+
+#. player options in the setup and on the right
+msgid "player:type"
+msgstr "棋手"
+
+msgid "player:human"
+msgstr "人類"
+
+msgid "player:ai"
+msgstr "AI"
+
+msgid "aistrategy"
+msgstr "AI策略"
+
+msgid "gametype"
+msgstr "對局類型"
+
+msgid "game:normal"
+msgstr "分先對局"
+
+msgid "game:teach"
+msgstr "指導棋"
+
+#. buttons at the top - should all include a line break
+msgid "analysis:nextmoves"
+msgstr ""
+"下\n"
+"一手"
+
+msgid "analysis:dots"
+msgstr ""
+"顯示\n"
+"選點"
+
+msgid "analysis:topmoves"
+msgstr ""
+"最佳\n"
+"選點"
+
+msgid "analysis:policy"
+msgstr ""
+"自定義下\n"
+"最佳選點"
+
+msgid "analysis:territory"
+msgstr ""
+"形勢\n"
+"判断"
+
+#. analysis menu items
+msgid "analysis:extra"
+msgstr "深度分析"
+
+msgid "analysis:equalize"
+msgstr "選點平均"
+
+msgid "analysis:sweep"
+msgstr "快速全盤分析"
+
+#. Analyze every node in the game more deeply
+msgid "analysis:game"
+msgstr "深度全盤分析"
+
+#. Analyze again but exclude all currently considered moves
+msgid "analysis:alternatives"
+msgstr "尋找其他選點"
+
+#. Restrict analysis to a specific region
+msgid "analysis:region"
+msgstr "設定欲分析區域"
+
+#. Status message
+msgid "analysis:region:start"
+msgstr "拖移以選取區域。選擇其他點來取消已選取的區域。"
+
+#. Menu option
+msgid "analysis:playtoend"
+msgstr "快速下完"
+
+#. Keep improving current position analysis when there is nothing else to do
+msgid "analysis:continuous"
+msgstr "切換持續分析"
+
+msgid "analysis:aimove"
+msgstr "要求AI產生下一手"
+
+#. Option in dropdown to insert some moves between the current and next
+msgid "analysis:insert"
+msgstr "插入棋子"
+
+#. status message
+msgid "extra analysis"
+msgstr "執行 {visits} 步額外分析"
+
+#. status message
+msgid "sweep analysis"
+msgstr "使用 {visits} 走訪/步來分析整盤棋"
+
+#. status message
+msgid "alternative analysis"
+msgstr "搜尋其他棋步中"
+
+#. status message
+msgid "local analysis"
+msgstr "搜尋局部下法中"
+
+#. status message
+msgid "equalizing analysis"
+msgstr "對所有選點進行 {visits} 步分析"
+
+#. status message
+msgid "game re-analysis"
+msgstr "使用 {visits} 步來重新分析整盤棋"
+
+#. right panel buttons and below board
+msgid "pass-button-text"
+msgstr "虛手"
+
+#. resign the game
+msgid "resign"
+msgstr "認輸"
+
+#. Shown when a move is undone in teaching mode.
+msgid "teaching undo message"
+msgstr ""
+"復原第 {move} 手損失了 {points_lost:.1f} "
+"目。請再試一次。若要查看提示,將游標停於該手以查看預期的反制手段;或查看預估的領地來看可能損失目數的棋盤區域。"
+
+msgid "ai-ponder"
+msgstr "AI引擎狀態指示燈,從綠色(閒置)變為橘色(處理中)。當引擎崩潰或無法啟動時,會變為紅色。"
+
+#. undo button on the right
+msgid "undo-button-text"
+msgstr "復原"
+
+#. tabs and labels on the right panel
+msgid "tab:score"
+msgstr "分數"
+
+msgid "tab:winrate"
+msgstr "勝率"
+
+#. shown on a graph tab
+msgid "tab:rank_est"
+msgstr "棋力預估"
+
+msgid "tab:points"
+msgstr "目數損失"
+
+msgid "tab:info"
+msgstr "資訊"
+
+msgid "tab:info-details"
+msgstr "詳細資訊"
+
+msgid "tab:notes"
+msgstr "註記"
+
+msgid "closedlabel:scoregraph"
+msgstr "分數圖"
+
+#. on the graph
+msgid "Jigo"
+msgstr "和棋"
+
+msgid "closedlabel:movestats"
+msgstr "每步統計"
+
+msgid "closedlabel:info¬es"
+msgstr "每步資訊和註記"
+
+#. on the board after passing once/twice
+msgid "board-pass"
+msgstr "虛手"
+
+msgid "board-game-end"
+msgstr ""
+"對局\n"
+"結束"
+
+#. Select mode
+msgid "btn:Play"
+msgstr "下棋"
+
+msgid "btn:Analysis"
+msgstr "分析"
+
+msgid "btn:Analyze"
+msgstr "分析選項"
+
+msgid "stats:winrate"
+msgstr "勝率"
+
+msgid "stats:score"
+msgstr "預估分數"
+
+msgid "stats:pointslost"
+msgstr "目數損失"
+
+#. better than expected best move
+msgid "stats:pointsgained"
+msgstr "目數增加"
+
+#. SGF and move comment messages
+msgid "SGF start message"
+msgstr " 'X' 是KataGo的最佳選點,正方形為損失少於0.5目的選點"
+
+msgid "Info:score"
+msgstr "分數: {score}"
+
+msgid "Info:winrate"
+msgstr "勝率: {winrate}"
+
+msgid "Info:point loss"
+msgstr "目數損失: {points_lost:.1f}"
+
+#. as in 'Move 125: B5'
+msgid "move"
+msgstr "手數 {number}"
+
+msgid "Info:top move"
+msgstr "預測最佳選點為 {top_move} ({score})。"
+
+msgid "Info:best move"
+msgstr "該手為最佳選點"
+
+msgid "Info:PV"
+msgstr "PV: {pv}"
+
+msgid "Info:policy rank"
+msgstr "直覺選點下該手棋是 #{rank} ({probability:.2%})."
+
+msgid "Info:policy best"
+msgstr "最佳直覺選點是 {move} ({probability:.1%})."
+
+msgid "Info:teaching undo"
+msgstr "指導棋模式下該手棋自動悔棋"
+
+msgid "Info:undo predicted PV"
+msgstr "建議下一手: {pv}"
+
+msgid "Info:AI thoughts"
+msgstr "AI思考過程: {thoughts}"
+
+msgid "No analysis available"
+msgstr "沒有可用分析"
+
+msgid "Analyzing move..."
+msgstr "分析中..."
+
+msgid "SGF Notes Hint"
+msgstr "SGF註記"
+
+# sgf load popup
+msgid "load sgf title"
+msgstr "載入SGF文件"
+
+# sgf load button
+msgid "Load File"
+msgstr "載入檔案"
+
+# sgf save as button
+msgid "Save File"
+msgstr "儲存檔案"
+
+# sgf save popup
+msgid "save sgf title"
+msgstr "儲存SGF檔案"
+
+msgid "load sgf fast analysis"
+msgstr "快速分析"
+
+msgid "load sgf rewind"
+msgstr "回到第一步"
+
+# timer settings from here
+msgid "timer settings"
+msgstr "用時設定"
+
+msgid "byoyomi length"
+msgstr "讀秒時長"
+
+msgid "byoyomi periods"
+msgstr "讀秒次數"
+
+msgid "count down sound"
+msgstr "開啟讀秒聲音"
+
+#. in minutes
+msgid "main time"
+msgstr "保留時間(分鐘)"
+
+#. ignore moves if timer is on and less than this is used
+msgid "minimal time use"
+msgstr "讀秒最短時間"
+
+msgid "move too fast"
+msgstr "請最少思考 {num} 秒再落子。"
+
+#. submit button
+msgid "update timer"
+msgstr "更新用時設定"
+
+# new game settings from here
+msgid "New Game title"
+msgstr "新對局設定"
+
+#. in new game options
+msgid "player names"
+msgstr "棋手名稱"
+
+#. hint on player name input
+msgid "black player name hint"
+msgstr "黑棋棋手名稱"
+
+#. hint on player name input
+msgid "white player name hint"
+msgstr "白棋棋手名稱"
+
+msgid "board size"
+msgstr "棋盤大小"
+
+msgid "handicap"
+msgstr "讓子"
+
+msgid "komi"
+msgstr "貼目"
+
+msgid "ruleset"
+msgstr "規則設定"
+
+#. ruleset names
+msgid "japanese"
+msgstr "日本規則"
+
+msgid "chinese"
+msgstr "中國規則"
+
+msgid "korean"
+msgstr "韓國規則"
+
+msgid "aga"
+msgstr "AGA規則"
+
+msgid "clear cache"
+msgstr "清除快取"
+
+#. clear cache help
+msgid "avoids replaying"
+msgstr ""
+"避免重複\n"
+"相同對局"
+
+#. hints on new game
+msgid "non square board hint"
+msgstr "使用 x:y 來設定非正方形棋盤(例如19:9)"
+
+#. submit new game
+msgid "new game"
+msgstr "開始新對局"
+
+#. change komi etc in current game
+msgid "change current game"
+msgstr ""
+"在進行中對局\n"
+"更新貼目/規則"
+
+# teacher settings from here
+msgid "teacher settings"
+msgstr "指導棋設定"
+
+msgid "point loss threshold"
+msgstr "目數損失下限"
+
+msgid "num undos"
+msgstr "悔棋次數"
+
+msgid "dot color"
+msgstr "選點顏色顯示"
+
+msgid "show dots"
+msgstr "顯示選點"
+
+msgid "save dots"
+msgstr "存入SGF"
+
+msgid "show last n dots"
+msgstr ""
+"顯示最後\n"
+" 手棋的選點"
+
+msgid "show ai dots"
+msgstr ""
+"顯示AI的\n"
+"選點/SGF評論"
+
+#. analysis option
+msgid "cache analysis to sgf"
+msgstr "儲存分析到SGF中"
+
+#. submit button
+msgid "update teacher"
+msgstr "更新指導棋設定"
+
+# main config settings
+msgid "restarting engine"
+msgstr "改變設定後,重新啟動引擎"
+
+# title
+msgid "general settings title"
+msgstr "一般和引擎設定"
+
+msgid "file not found"
+msgstr "路徑不存在"
+
+msgid "general settings"
+msgstr "一般設定"
+
+msgid "engine settings"
+msgstr "KataGo設定"
+
+msgid "config file path"
+msgstr "所有設定儲存在:"
+
+msgid "engine:katago"
+msgstr "KataGo執行檔路徑"
+
+msgid "engine:katago:hint"
+msgstr "留白以使用其他執行檔案"
+
+msgid "engine:model"
+msgstr "KataGo模型檔路徑"
+
+msgid "engine:config"
+msgstr "KataGo設定檔路徑"
+
+msgid "engine:altcommand"
+msgstr "覆寫引擎指令"
+
+msgid "engine:altcommand:hint"
+msgstr "啟動引擎的完整指令。其他所有模型的設定將會被忽略。"
+
+msgid "engine:max_visits"
+msgstr "分析模式中,每手棋最大運算步數"
+
+msgid "engine:fast_visits"
+msgstr "快速分析模式中,每手棋最大運算步數"
+
+msgid "engine:max_time"
+msgstr "分析時間上限"
+
+msgid "engine:wide_root_noise"
+msgstr "寬根雜訊 (增加了納入考慮的選點多樣性)"
+
+msgid "engine:wide_root_noise:hint"
+msgstr ""
+"使用 0.02-0.1 以顯示\n"
+"更多可能選點"
+
+msgid "engine:time:hint"
+msgstr "時間單位是秒"
+
+msgid "general:anim_pv_time"
+msgstr "動畫中,兩手棋的時間間隔"
+
+msgid "general:debug_level:hint"
+msgstr "設為1時,回報錯誤"
+
+msgid "general:debug_level"
+msgstr "Console中的偵錯層級"
+
+msgid "katago settings"
+msgstr "KataGo設定"
+
+msgid "update settings"
+msgstr "更新設定"
+
+# ai settings from here
+msgid "ai settings"
+msgstr "進階AI設定"
+
+msgid "Select AI"
+msgstr "選擇AI策略"
+
+msgid "update ai settings"
+msgstr "更新AI設定"
+
+#. misc errors and info messages
+msgid "sgf written"
+msgstr "將包含分析的SGF寫入 {file_name}"
+
+msgid "wait-before-extra-analysis"
+msgstr "優化前,等待初始分析完成"
+
+msgid "Copied SGF to clipboard."
+msgstr "複製SGF到剪貼簿"
+
+msgid "Failed to import from clipboard"
+msgstr ""
+"從剪貼簿匯入對局失敗: {error}\n"
+"剪貼簿内容: {contents}..."
+
+msgid "Failed to load SGF"
+msgstr "載入SGF失敗: {error}"
+
+msgid "Kata exe not found"
+msgstr "KataGo執行檔 {exe} 不存在,請檢查一般設定。"
+
+msgid "Kata exe not found in path"
+msgstr ""
+"在 PATH 中找不到KataGo執行檔 `{exe}`,請檢查環境變數或在一般設定中使用完整路徑代替。若是在 MacOS "
+"上,請先閱讀使用手冊中的如何使用brew安裝KataGo。"
+
+msgid "Kata model not found"
+msgstr "KataGo模型 {model} 不存在,請檢查一般設定。"
+
+msgid "Kata config not found"
+msgstr "KataGo組態檔 {config} 不存在,請檢查一般設定。"
+
+msgid "Starting Kata failed"
+msgstr "使用指令 '{command}' 啟動KataGo發生錯誤 {error}。"
+
+msgid "Engine died unexpectedly"
+msgstr "引擎意外崩潰沒有輸出,可能為記憶體不足導致: {error}"
+
+msgid "Engine missing DLL"
+msgstr "遺失DLL因此無法啟動KataGo。請參考KataGo文件中的所需事項,或嘗試單獨啟動KataGo來取得詳細資訊。"
+
+#. AI names, help etc
+msgid "strength:kyu"
+msgstr "級"
+
+msgid "strength:dan"
+msgstr "段"
+
+msgid "ai:default"
+msgstr "KataGo"
+
+msgid "aihelp:default"
+msgstr ""
+"最強的KataGo AI。其棋力由在一般設定中 `引擎` 區塊中的 `最大走訪數` 和 `模型` 以及引擎組態文件決定。此處沒有可供調整的選項。"
+
+#. ai which handles handicap games better
+msgid "ai:handicap"
+msgstr "Kata讓子棋"
+
+msgid "aihelp:handicap"
+msgstr ""
+"KataGo 對讓子棋做了最佳化的對局版本。以黑棋的觀點來看,KataGo 中的 `pda` 設定對應到的是 "
+"`playoutDoublingAdvantage`。當被設定為 `自動` 時,KaTrain 將會找自動找到適合的值。"
+
+msgid "ai:simple"
+msgstr "單純模式"
+
+msgid "aihelp:simple"
+msgstr ""
+"下法會導致盤面狀態簡單化。每步在損失最多 '最大目數損失' 目和最少 '最少走訪數'中,較傾向於損失較少目數、安定自身領地(搭配 importance "
+"settled_weight)、突破對方領地(importance settled_weight * "
+"opponent_fac)、避免脫先(5路以上,importance tenuki_penalty)和避免碰(importance "
+"attachment_penalty)。權重可以是負數使對局複雜化,而寬根雜訊(在一般設定中)的使用,可以以棋力強度作為代價,加強風格。"
+
+msgid "ai:jigo"
+msgstr "Kata控目"
+
+msgid "aihelp:jigo"
+msgstr "在沒有更多限制的情況下,嘗試按指定目數(預設0.5)赢棋。"
+
+msgid "ai:scoreloss"
+msgstr "分數損失"
+
+msgid "aihelp:scoreloss"
+msgstr "傾向選擇低機率損失較多目數的落點。在較高計算深度次數下,棋力可能更不穩定/更弱。"
+
+msgid "ai:policy"
+msgstr "直覺"
+
+msgid "aihelp:policy"
+msgstr "按最佳直覺行棋,沒有任何計算。棋力主要由引擎設定中的 `模型` 決定。布局落點設定影響布局中隨機落點的數量。"
+
+msgid "ai:p:weighted"
+msgstr "直覺權重"
+
+msgid "aihelp:p:weighted"
+msgstr ""
+"按直覺機率行棋, `weaken_fac` 影響較弱選點的多寡。當機率極低時,它的下棋風格變得很像直覺AI。 `pick_override` "
+"決定在選擇最佳選點時的無隨機性;而 `lower_bound` 決定了允許的直覺值的下限。"
+
+msgid "ai:p:pick"
+msgstr "盲選直覺"
+
+msgid "aihelp:p:pick"
+msgstr ""
+"隨機選擇 `pick_n + pick_frac * ` 並採用最佳的一手。 當 "
+"`pick_frac=1` 時,下棋風格像是直覺AI。 如果直覺值高於 `pick_override`,則採用最佳的一手以避免明顯錯誤。"
+
+msgid "ai:p:local"
+msgstr "局部模式"
+
+msgid "aihelp:p:local"
+msgstr ""
+"使加權 `pick_n + pick_frac * ` 接近最後一手並選出最好的一手。如果直覺值大於 "
+"`pick_override`則採用最佳的一手以避免明顯錯誤。"
+
+msgid "ai:p:tenuki"
+msgstr "脫先模式"
+
+msgid "aihelp:p:tenuki"
+msgstr ""
+"使加權 `pick_n + pick_frac * ` 遠離最後一手並選出最好的一手。如果直覺值大於 "
+"`pick_override` 則採用最佳的一手以避免明顯錯誤。更高的 `stddev` 使其偏離更多。"
+
+msgid "ai:p:influence"
+msgstr "取勢模式"
+
+msgid "aihelp:p:influence"
+msgstr ""
+"在偏移值高於 `threshold` 線的點中選出最好的一手。增加 `line_weight` 以加重對靠近邊緣選點的懲罰。在 '終局' "
+"部分結束後停止。"
+
+msgid "ai:p:territory"
+msgstr "取地模式"
+
+msgid "aihelp:p:territory"
+msgstr ""
+"在偏移值低於 `threshold` 線的點中選出最好的一手。增加 `line_weight` 以加重對靠近中央選點的懲罰。在 '終局' "
+"部分結束後停止。"
+
+msgid "ai:p:rank"
+msgstr "可變棋力"
+
+msgid "aihelp:p:rank"
+msgstr "隨機選擇有限數量的合法選點,並下出最佳的一手。愈強的設定有愈多的選擇來選出最佳的一手。因為沒有0級/段,因此 3段 = -2級。"
+
+#. in AI settings
+msgid "estimated strength"
+msgstr "預估棋力"
+
+#. button in general settings for downloading models
+msgid "download models button"
+msgstr "下載模型"
+
+#. button in general settings for downloading katago executables
+msgid "download katago button"
+msgstr "下載多種 KataGo 版本"
+
+#. shown on on the model select dropdown
+msgid "models available"
+msgstr "找到 {num} 個可用模型"
+
+#. shown on on the katago binary select dropdown
+msgid "default katago option"
+msgstr "偵測平台並使用所提供OpenCL KataGo執行檔"
+
+#. shown on on the binaries select dropdown
+msgid "katago binaries available"
+msgstr "找到 {num} 個可用的KataGo版本"
+
+#. error message on trying to download models when already done
+msgid "All models downloaded"
+msgstr "已下載所有可用模型"
+
+#. error message on trying to download models when already done
+msgid "All binaries downloaded"
+msgstr "已下載所有可用的執行檔"
+
+#. label in little popup for analyzing entire game
+msgid "reanalyze max visits"
+msgstr "每步棋的計算深度"
+
+#. on move tree editing
+msgid "Delete Node"
+msgstr "刪除模式"
+
+#. on move tree editing
+msgid "Toggle Collapse Branch"
+msgstr "擴展/摺疊"
+
+#. on move tree editing
+msgid "Make Main Branch"
+msgstr "設為主要變化"
+
+#. theme label
+msgid "theme"
+msgstr "顏色主題"
+
+#. theme name
+msgid "theme:normal"
+msgstr "預設主題"
+
+#. theme name
+msgid "theme:red-green-colourblind"
+msgstr "紅綠色盲友好"
+
+#. setting: what to show on top move (e.g. point loss, visits, ...)
+msgid "stats on top move"
+msgstr "最佳選點統計"
+
+#. Dropdown menu option
+msgid "top_move_delta_score"
+msgstr "分數波動"
+
+#. Dropdown menu option
+msgid "top_move_score"
+msgstr "分數"
+
+#. Dropdown menu option
+msgid "top_move_delta_winrate"
+msgstr "勝率波動"
+
+#. Dropdown menu option
+msgid "top_move_winrate"
+msgstr "勝率"
+
+#. Dropdown menu option
+msgid "top_move_visits"
+msgstr "計算深度"
+
+#. Dropdown menu option
+msgid "top_move_nothing"
+msgstr "(無)"
+
+#. top move stats show threshold description
+msgid "show stats if"
+msgstr "計算深度...以上時顯示統計"
+
+#. status message on starting insert mode
+msgid "starting insert mode"
+msgstr "插入模式,按 i 結束插入。"
+
+#. status message on ending insert mode and copying the moves
+msgid "ending insert mode"
+msgstr "插入完畢,複製 {num_copied} 手棋。"
+
+#. when a user tries to go to a different move/node while in insert
+#. mode
+msgid "finish inserting before navigating"
+msgstr "此功能在插入模式無法使用,按 i 結束插入。"
+
+#. TODO
+msgid "stone_scoring"
+msgstr "Ancient Chinese"
diff --git a/katrain/img/flags/flag-tw.png b/katrain/img/flags/flag-tw.png
new file mode 100644
index 00000000..970e3e30
Binary files /dev/null and b/katrain/img/flags/flag-tw.png differ
diff --git a/katrain/popups.kv b/katrain/popups.kv
index 14fdd3cc..8d5e420f 100644
--- a/katrain/popups.kv
+++ b/katrain/popups.kv
@@ -160,6 +160,7 @@
size_hint: 0.33, 1
AnchorLayout:
LabelledPathInput:
+ check_path: False
input_property: "engine/altcommand"
hint_text: i18n._("engine:altcommand:hint")
BoxLayout:
@@ -298,6 +299,7 @@
Label:
size_hint: 0.1, 1
text: '&'
+ font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
LabelledSpinner:
id: top_moves_show_secondary
value_refs: TOP_MOVE_OPTIONS
@@ -470,7 +472,7 @@
text: i18n._("ruleset")
AnchorLayout:
LabelledSpinner:
- size_hint: 0.8,0.8
+ size_hint: 0.95,0.8
input_property: 'game/rules'
font_size: sp(Theme.DESC_FONT_SIZE)
id: rules_spinner
@@ -608,6 +610,7 @@
I18NFileBrowser:
id: filesel
multiselect: False
+ file_must_exist: True
filters: ["*.sgf","*.gib","*.ngf"]
path: "."
size_hint: 1,7
@@ -618,6 +621,7 @@
I18NFileBrowser:
id: filesel
multiselect: False
+ file_must_exist: False
filters: ["*.sgf","*.gib","*.ngf"]
path: "."
size_hint: 1,7
\ No newline at end of file
diff --git a/setup.py b/setup.py
index a0bc74aa..bcdd9efa 100644
--- a/setup.py
+++ b/setup.py
@@ -47,6 +47,7 @@ def include_data_files(directory):
"urllib3",
"pygame;platform_system=='Darwin'", # some mac versions need this for kivy
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
+ "chardet", # for automatic encoding detection
],
dependency_links=["https://kivy.org/downloads/simple/"],
python_requires=">=3.6, <4",
diff --git a/tests/test_board.py b/tests/test_board.py
index 7014a085..496c1d14 100644
--- a/tests/test_board.py
+++ b/tests/test_board.py
@@ -1,7 +1,7 @@
import pytest
from katrain.core.base_katrain import KaTrainBase
-from katrain.core.game import Game, IllegalMoveException, Move
+from katrain.core.game import Game, IllegalMoveException, Move, KaTrainSGF
from katrain.core.game_node import GameNode
@@ -105,3 +105,16 @@ def test_ko(self, new_game):
b.play(Move(coords=None, player="B"))
b.play(Move.from_gtp("A1", player="W"))
assert 3 == len(b.prisoners)
+
+ def test_handicap_load(self):
+ input_sgf = (
+ "(;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2]RU[Chinese]SZ[19]HA[2]KM[0.50]TM[600]OT[5x30 byo-yomi]PW[kneh]PB[ayabot003]WR[4k]BR[6k]DT[2021-01-04]PC[The KGS Go Server at http://www.gokgs.com/]C[ayabot003 [6k\\"
+ "]: GTP Engine for ayabot003 (black): Aya version 7.85x]RE[W+Resign];B[pd]BL[599.647];B[dp]BL[599.477];W[pp]WL[597.432];B[cd]BL[598.896];W[ed]WL[595.78];B[ec]BL[598.558])"
+ )
+ root = KaTrainSGF.parse_sgf(input_sgf)
+ game = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=root)
+ assert 0 == len(game.root.placements)
+
+ root2 = KaTrainSGF.parse_sgf("(;GM[1]FF[4]SZ[19]HA[2];)")
+ game2 = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=root2)
+ assert 2 == len(game2.root.placements)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index cdbfcd44..83a18e57 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -157,11 +157,22 @@ def test_foxwq():
def test_next_player():
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb])"
assert "B" == SGF.parse_sgf(input_sgf).next_player
+ assert "B" == SGF.parse_sgf(input_sgf).initial_player
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb]PL[B])"
assert "B" == SGF.parse_sgf(input_sgf).next_player
+ assert "B" == SGF.parse_sgf(input_sgf).initial_player
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb]PL[W])"
assert "W" == SGF.parse_sgf(input_sgf).next_player
+ assert "W" == SGF.parse_sgf(input_sgf).initial_player
input_sgf = "(;GM[1]FF[4]AB[aa])"
assert "W" == SGF.parse_sgf(input_sgf).next_player
+ assert "W" == SGF.parse_sgf(input_sgf).initial_player
input_sgf = "(;GM[1]FF[4]AB[aa]PL[B])"
assert "B" == SGF.parse_sgf(input_sgf).next_player
+ assert "B" == SGF.parse_sgf(input_sgf).initial_player
+ input_sgf = "(;GM[1]FF[4]AB[aa];B[dd])" # branch exists
+ assert "B" == SGF.parse_sgf(input_sgf).next_player
+ assert "B" == SGF.parse_sgf(input_sgf).initial_player
+ input_sgf = "(;GM[1]FF[4]AB[aa];W[dd])" # branch exists
+ assert "W" == SGF.parse_sgf(input_sgf).next_player
+ assert "W" == SGF.parse_sgf(input_sgf).initial_player