Merge pull request #308 from sanderland/1.7.1

1.7.1
This commit is contained in:
sanderland authored and GitHub committed 2021-01-17 03:00:10 +09:00
commit a58394445a
47 files changed
+1250 -284

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
dist_sgf
# additions
KataGoData
katrain/KataGo/KataGoData
+4 -2
View File
@@ -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
+4 -3
View File
@@ -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
[![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).
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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]
+7 -4
View File
@@ -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
+44 -7
View File
@@ -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
View File
@@ -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):
+4
View File
@@ -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
+13 -5
View File
@@ -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")
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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.
+4 -11
View File
@@ -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.
+4 -11
View File
@@ -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.
+3 -13
View File
@@ -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.
+4 -11
View File
@@ -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.
+4 -15
View File
@@ -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.
+3 -13
View File
@@ -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.
+4 -11
View File
@@ -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.
+4 -13
View File
@@ -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&notes"
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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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)
+11
View File
@@ -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