47 files changed
+1250
-284
No files matched your search
@@ -1,3 +1,5 @@
|
||||
dist_sgf
|
||||
|
||||
# additions
|
||||
KataGoData
|
||||
katrain/KataGo/KataGoData
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=fr&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="French" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-fr.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ru&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Russian" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ru.png" width=50></a>
|
||||
<br/>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=zh-CN&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-cn.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=zh-CN&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Simplified Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-cn.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=zh-TW&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Traditional Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-tw.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ko&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Korean" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ko.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ja&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Japanese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-jp.png" width=50></a>
|
||||
|
||||
@@ -208,13 +209,13 @@ In addition to shortcuts mentioned above and those shown in the main menu:
|
||||
## <a name="support"></a> Support / Contribute
|
||||
|
||||
[](http://github.com/sanderland/katrain/issues)
|
||||
[](CONTRIBUTIONS.md)
|
||||
[](CONTRIBUTIONS.md)
|
||||
[](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).
|
||||
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ for lang in locales:
|
||||
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():
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+50
-15
@@ -7,6 +7,7 @@ if "KIVY_AUDIO" not in os.environ:
|
||||
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 @@ import traceback
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
# 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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
|
||||
self._do_ai_move(cn)
|
||||
Clock.schedule_once(self.board_gui.play_stone_sound, 0.25)
|
||||
if 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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
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 @@ class KaTrainApp(MDApp):
|
||||
|
||||
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
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
@@ -141,7 +141,7 @@ class KaTrainBase:
|
||||
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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+43
-22
@@ -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.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBU
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
)
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
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 @@ class KataGoEngine:
|
||||
"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 {})},
|
||||
}
|
||||
|
||||
+21
-10
@@ -67,11 +67,21 @@ class Game:
|
||||
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 @@ class Game:
|
||||
|
||||
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 @@ class Game:
|
||||
+ 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 @@ class Game:
|
||||
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 @@ class Game:
|
||||
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)
|
||||
|
||||
+43
-37
@@ -8,10 +8,10 @@ from typing import Dict, List, Optional, Tuple
|
||||
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 @@ class GameNode(SGFNode):
|
||||
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 @@ class GameNode(SGFNode):
|
||||
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", "<unknown>")
|
||||
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 @@ class GameNode(SGFNode):
|
||||
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 @@ class GameNode(SGFNode):
|
||||
]
|
||||
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 @@ class GameNode(SGFNode):
|
||||
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 @@ class GameNode(SGFNode):
|
||||
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 @@ class GameNode(SGFNode):
|
||||
{"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 @@ class GameNode(SGFNode):
|
||||
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):
|
||||
|
||||
+42
-13
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import chardet
|
||||
import math
|
||||
import re
|
||||
from collections import defaultdict
|
||||
@@ -29,7 +30,9 @@ class Move:
|
||||
@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 @@ class SGFNode:
|
||||
|
||||
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 @@ class 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 @@ class SGFNode:
|
||||
|
||||
|
||||
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 @@ class SGF:
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
+9
-5
@@ -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:
|
||||
|
||||
+13
-7
@@ -31,7 +31,7 @@ from katrain.core.constants import (
|
||||
)
|
||||
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 @@ class BadukPanWidget(Widget):
|
||||
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 @@ class BadukPanWidget(Widget):
|
||||
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]
|
||||
|
||||
@@ -24,9 +24,9 @@ class PlayAnalyzeSelect(MDFloatLayout):
|
||||
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 @@ class PlayAnalyzeSelect(MDFloatLayout):
|
||||
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
|
||||
|
||||
@@ -28,7 +28,15 @@ from kivymd.uix.navigationdrawer import MDNavigationDrawer
|
||||
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 @@ class TriStateMDCheckbox(MDCheckbox):
|
||||
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):
|
||||
|
||||
+80
-46
@@ -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 @@ from katrain.core.constants import (
|
||||
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 @@ class LabelledTextInput(MDTextField):
|
||||
|
||||
|
||||
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 @@ class NewGamePopup(QuickConfigGui):
|
||||
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 @@ class NewGamePopup(QuickConfigGui):
|
||||
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 @@ class ConfigAIPopup(QuickConfigGui):
|
||||
|
||||
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class ConfigPopup(QuickConfigGui):
|
||||
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 @@ class LoadSGFPopup(BoxLayout):
|
||||
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 @@ class SaveSGFPopup(BoxLayout):
|
||||
self.filesel.select_string = i18n._("Save File")
|
||||
|
||||
def on_submit(self):
|
||||
self.filesel.dispatch("on_success")
|
||||
self.filesel.button_clicked()
|
||||
|
||||
|
||||
class ReAnalyzeGamePopup(BoxLayout):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,7 +40,7 @@ a shortcut to the Documents directory added to the favorites bar::
|
||||
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 @@ Builder.load_string(
|
||||
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 @@ Builder.load_string(
|
||||
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 @@ Builder.load_string(
|
||||
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 @@ class LinkTree(TreeView):
|
||||
|
||||
|
||||
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 @@ class I18NFileBrowser(BoxLayout):
|
||||
|
||||
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")
|
||||
@@ -109,7 +109,7 @@ class MoveTreeCanvas(Widget):
|
||||
|
||||
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 @@ Builder.load_string(
|
||||
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)
|
||||
|
||||
@@ -71,7 +71,7 @@ class ProgressLoader(BoxLayout):
|
||||
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):
|
||||
|
||||
Binary file not shown.
@@ -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 "动画中两手棋时间间隔"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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 "手順アニメーションの一手毎の時間"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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 "수순을 보여줄 때 수 사이의 시간"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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 "Задержка между ходами в анимации"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
"<this many> 手棋的選點"
|
||||
|
||||
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 * <number of legal moves>` 並採用最佳的一手。 當 "
|
||||
"`pick_frac=1` 時,下棋風格像是直覺AI。 如果直覺值高於 `pick_override`,則採用最佳的一手以避免明顯錯誤。"
|
||||
|
||||
msgid "ai:p:local"
|
||||
msgstr "局部模式"
|
||||
|
||||
msgid "aihelp:p:local"
|
||||
msgstr ""
|
||||
"使加權 `pick_n + pick_frac * <number of legal moves>` 接近最後一手並選出最好的一手。如果直覺值大於 "
|
||||
"`pick_override`則採用最佳的一手以避免明顯錯誤。"
|
||||
|
||||
msgid "ai:p:tenuki"
|
||||
msgstr "脫先模式"
|
||||
|
||||
msgid "aihelp:p:tenuki"
|
||||
msgstr ""
|
||||
"使加權 `pick_n + pick_frac * <number of legal moves>` 遠離最後一手並選出最好的一手。如果直覺值大於 "
|
||||
"`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"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+5
-1
@@ -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
|
||||
@@ -47,6 +47,7 @@ setup(
|
||||
"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",
|
||||
|
||||
+14
-1
@@ -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 @@ class TestBoard:
|
||||
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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user