12 files changed
+129
-75
No files matched your search
@@ -51,6 +51,7 @@ Many thanks to these additional authors:
|
||||
* Carton He for contributions to sgf parsing and handling.
|
||||
* "blamarche" for adding the board coordinates toggle.
|
||||
* "pdeblanc" for adding the ancient chinese scoring option.
|
||||
* "LiamHz" for adding the undo to main branch keyboard shortcut.
|
||||
|
||||
## Translators
|
||||
|
||||
|
||||
@@ -184,6 +184,7 @@ In addition to shortcuts mentioned above and those shown in the main menu:
|
||||
* **[Ctrl-delete]** Delete current node.
|
||||
* **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point.
|
||||
* **[b]** Go back to the previous branching point.
|
||||
* **[Shift-b]** Go back the the main branch.
|
||||
* **[n]** As in clicking the forward red arrow, go to one move before the next mistake (orange or worse) by a human player.
|
||||
* **[Shift-n]** As in clicking the backward red arrow, go to one move before the previous mistake.
|
||||
* **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
|
||||
|
||||
@@ -12,11 +12,18 @@ Version 1.7 brings basic support for themes.
|
||||
```
|
||||
* All resources (including icons which can not be renamed for now) will be looked up in `<home dir>/.katrain` first, so files with identical names there can be used to override sounds and images.
|
||||
|
||||
## Available themes
|
||||
## Installation
|
||||
|
||||
* See [here](https://github.com/sanderland/katrain/blob/master/themes/) for available themes.
|
||||
* To install a theme, simply unzip the theme.zip to your .katrain folder.
|
||||
* On windows you can find it in C:\Users\you\.katrain and on linux in ~/.katrain.
|
||||
* When in doubt, the general settings dialog will also show the location.
|
||||
* To uninstall a theme, remove theme.json and all relevant images from that folder.
|
||||
|
||||
## Available themes
|
||||
|
||||
### Alternate board/stones theme by "koast"
|
||||
|
||||
[Download](https://github.com/sanderland/katrain/blob/master/themes/koast-theme.zip)
|
||||
|
||||

|
||||
|
||||
@@ -610,6 +610,8 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
|
||||
elif keycode[1] == "v" and ctrl_pressed:
|
||||
self.load_sgf_from_clipboard()
|
||||
elif keycode[1] == "b" and shift_pressed:
|
||||
self("undo", "main-branch")
|
||||
elif keycode[1] in shortcuts.keys() and not ctrl_pressed:
|
||||
shortcut = shortcuts[keycode[1]]
|
||||
if isinstance(shortcut, Widget):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PROGRAM_NAME = "KaTrain"
|
||||
VERSION = "1.7.1"
|
||||
VERSION = "1.7.2"
|
||||
HOMEPAGE = "https://github.com/sanderland/katrain"
|
||||
CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version
|
||||
ANALYSIS_FORMAT_VERSION = "1.0"
|
||||
|
||||
+46
-41
@@ -52,7 +52,7 @@ class KataGoEngine:
|
||||
self.write_stdin_thread = None
|
||||
self.shell = False
|
||||
self.write_queue = queue.Queue()
|
||||
|
||||
self.thread_lock = threading.Lock()
|
||||
exe = config.get("katago", "").strip()
|
||||
if config.get("altcommand", ""):
|
||||
self.command = config["altcommand"]
|
||||
@@ -92,39 +92,43 @@ 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
|
||||
if hasattr(subprocess, "STARTUPINFO"):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller
|
||||
self.katago_process = subprocess.Popen(
|
||||
self.command,
|
||||
startupinfo=startupinfo,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=self.shell,
|
||||
)
|
||||
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||
self.katrain.log(
|
||||
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()
|
||||
with self.thread_lock:
|
||||
self.write_queue = queue.Queue()
|
||||
try:
|
||||
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
|
||||
startupinfo = None
|
||||
if hasattr(subprocess, "STARTUPINFO"):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller
|
||||
self.katago_process = subprocess.Popen(
|
||||
self.command,
|
||||
startupinfo=startupinfo,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=self.shell,
|
||||
)
|
||||
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||
self.katrain.log(
|
||||
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
|
||||
for query_id in list(self.queries.keys()):
|
||||
self.terminate_query(query_id)
|
||||
self.queries = {}
|
||||
if not self.is_idle():
|
||||
with self.thread_lock:
|
||||
for query_id in list(self.queries.keys()):
|
||||
self.terminate_query(query_id)
|
||||
self.queries = {}
|
||||
self.write_queue = queue.Queue()
|
||||
|
||||
def restart(self):
|
||||
self.queries = {}
|
||||
@@ -248,16 +252,17 @@ class KataGoEngine:
|
||||
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)
|
||||
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)
|
||||
with self.thread_lock:
|
||||
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)
|
||||
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))
|
||||
|
||||
+14
-1
@@ -279,9 +279,14 @@ class Game:
|
||||
self._calculate_groups()
|
||||
return
|
||||
break_on_branch = False
|
||||
break_on_main_branch = False
|
||||
last_branching_node = cn
|
||||
if n_times == "branch":
|
||||
n_times = 9999
|
||||
break_on_branch = True
|
||||
elif n_times == "main-branch":
|
||||
n_times = 9999
|
||||
break_on_main_branch = True
|
||||
for move in range(n_times):
|
||||
if (
|
||||
stop_on_mistake is not None
|
||||
@@ -291,13 +296,21 @@ class Game:
|
||||
):
|
||||
self.set_current_node(cn.parent)
|
||||
return
|
||||
previous_cn = cn
|
||||
if cn.shortcut_from:
|
||||
cn = cn.shortcut_from
|
||||
elif not cn.is_root:
|
||||
cn = cn.parent
|
||||
else:
|
||||
break # root
|
||||
if break_on_branch and len(cn.children) > 1:
|
||||
break
|
||||
self.set_current_node(cn)
|
||||
elif break_on_main_branch and cn.ordered_children[0] != previous_cn: # implies > 1 child
|
||||
last_branching_node = cn
|
||||
if break_on_main_branch:
|
||||
cn = last_branching_node
|
||||
if cn is not self.current_node:
|
||||
self.set_current_node(cn)
|
||||
|
||||
def redo(self, n_times=1, stop_on_mistake=None):
|
||||
if self.insert_mode:
|
||||
|
||||
@@ -304,7 +304,7 @@ class SGFNode:
|
||||
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"
|
||||
return "B" if self.root.get_property("PL").upper().strip() == "B" else "W"
|
||||
elif root.children: # child exist, use it if not placement
|
||||
for child in root.children:
|
||||
for color in "BW":
|
||||
|
||||
+49
-24
@@ -3,6 +3,8 @@ import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from zipfile import ZipFile
|
||||
|
||||
@@ -426,7 +428,10 @@ class ConfigAIPopup(QuickConfigGui):
|
||||
|
||||
|
||||
class ConfigPopup(QuickConfigGui):
|
||||
MODEL_ENDPOINTS = {"Latest distributed model": "https://katagotraining.org/api/networks/newest_training/"}
|
||||
MODEL_ENDPOINTS = {
|
||||
"Latest distributed model": "https://katagotraining.org/api/networks/newest_training/",
|
||||
"Strongest distributed model": "https://katagotraining.org/api/networks/get_strongest/",
|
||||
}
|
||||
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",
|
||||
@@ -461,6 +466,7 @@ class ConfigPopup(QuickConfigGui):
|
||||
self.paths = [self.katrain.config("engine/model"), "katrain/models", DATA_FOLDER]
|
||||
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
|
||||
Clock.schedule_once(self.check_katas)
|
||||
self.last_clicked_download_models = 0
|
||||
MDApp.get_running_app().bind(language=self.check_models)
|
||||
MDApp.get_running_app().bind(language=self.check_katas)
|
||||
|
||||
@@ -506,7 +512,11 @@ class ConfigPopup(QuickConfigGui):
|
||||
self.paths.append(path) # persistent on paths with models found
|
||||
model_files += files
|
||||
|
||||
model_files = sorted([(find_description(path), path) for path in model_files])
|
||||
# no description to bottom
|
||||
model_files = sorted(
|
||||
[(find_description(path), path) for path in model_files],
|
||||
key=lambda descpath: "Z" * 10 + path if descpath[0] == descpath[1] else descpath[0],
|
||||
)
|
||||
models_available_msg = i18n._("models available").format(num=len(model_files))
|
||||
self.model_files.values = [models_available_msg] + [desc for desc, path in model_files]
|
||||
self.model_files.value_keys = [""] + [path for desc, path in model_files]
|
||||
@@ -557,18 +567,23 @@ class ConfigPopup(QuickConfigGui):
|
||||
self.katago_files.text = katas_available_msg
|
||||
|
||||
def download_models(self, *_largs):
|
||||
if time.time() - self.last_clicked_download_models > 5:
|
||||
self.last_clicked_download_models = time.time()
|
||||
threading.Thread(target=self._download_models, daemon=True).start()
|
||||
|
||||
def _download_models(self):
|
||||
def download_complete(req, tmp_path, path, model):
|
||||
try:
|
||||
os.rename(tmp_path, path)
|
||||
self.katrain.log(f"Download of {model} model complete -> {path}", OUTPUT_INFO)
|
||||
self.katrain.log(f"Download of {model} complete -> {path}", OUTPUT_INFO)
|
||||
except Exception as e:
|
||||
self.katrain.log(f"Download of {model} model complete, but could not move file: {e}", OUTPUT_ERROR)
|
||||
self.katrain.log(f"Download of {model} complete, but could not move file: {e}", OUTPUT_ERROR)
|
||||
self.check_models()
|
||||
|
||||
for c in self.download_progress_box.children:
|
||||
if isinstance(c, ProgressLoader) and c.request:
|
||||
c.request.cancel()
|
||||
self.download_progress_box.clear_widgets()
|
||||
Clock.schedule_once(lambda _dt: self.download_progress_box.clear_widgets(), -1) # main thread
|
||||
downloading = False
|
||||
|
||||
dist_models = {k: v for k, v in self.katrain.config("dist_models", {}).items() if k in self.MODEL_ENDPOINTS}
|
||||
@@ -577,6 +592,10 @@ class ConfigPopup(QuickConfigGui):
|
||||
try:
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request("GET", url)
|
||||
if response.status != 200:
|
||||
raise Exception(
|
||||
f"Request to {url} returned code {response.status} != 200: {response.data.decode()}"
|
||||
)
|
||||
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)
|
||||
@@ -590,27 +609,33 @@ class ConfigPopup(QuickConfigGui):
|
||||
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
|
||||
savepath_tmp = savepath + ".part"
|
||||
self.katrain.log(f"Downloading {name} model from {url} to {savepath_tmp}", OUTPUT_INFO)
|
||||
progress = ProgressLoader(
|
||||
download_url=url,
|
||||
path_to_file=savepath_tmp,
|
||||
downloading_text=f"Downloading {name} model: " + "{}",
|
||||
label_downloading_text=f"Starting download for {name} model",
|
||||
download_complete=lambda req, tmp=savepath_tmp, path=savepath, model=name: download_complete(
|
||||
req, tmp, path, model
|
||||
Clock.schedule_once(
|
||||
lambda _dt, _savepath=savepath, _savepath_tmp=savepath_tmp, _url=url, _name=name: ProgressLoader(
|
||||
self.download_progress_box,
|
||||
download_url=_url,
|
||||
path_to_file=_savepath_tmp,
|
||||
downloading_text=f"Downloading {_name}: " + "{}",
|
||||
label_downloading_text=f"Starting download for {_name}",
|
||||
download_complete=lambda req, tmp=_savepath_tmp, path=_savepath, model=_name: download_complete(
|
||||
req, tmp, path, model
|
||||
),
|
||||
download_redirected=lambda req, mname=_name: self.katrain.log(
|
||||
f"Download {mname} redirected {req.resp_headers}", OUTPUT_DEBUG
|
||||
),
|
||||
download_error=lambda req, error, mname=_name: self.katrain.log(
|
||||
f"Download of {mname} failed or cancelled ({error})", OUTPUT_ERROR
|
||||
),
|
||||
),
|
||||
download_redirected=lambda req, mname=name: self.katrain.log(
|
||||
f"Download {mname} redirected {req.resp_headers}", OUTPUT_DEBUG
|
||||
),
|
||||
download_error=lambda req, error, mname=name: self.katrain.log(
|
||||
f"Download of {mname} failed or cancelled ({error})", OUTPUT_ERROR
|
||||
),
|
||||
)
|
||||
progress.start(self.download_progress_box)
|
||||
0,
|
||||
) # main thread
|
||||
downloading = True
|
||||
if not downloading:
|
||||
self.download_progress_box.add_widget(
|
||||
Label(text=i18n._("All models downloaded"), font_name=i18n.font_name, text_size=(None, dp(50)))
|
||||
)
|
||||
Clock.schedule_once(
|
||||
lambda _dt: self.download_progress_box.add_widget(
|
||||
Label(text=i18n._("All models downloaded"), font_name=i18n.font_name, text_size=(None, dp(50)))
|
||||
),
|
||||
0,
|
||||
) # main thread
|
||||
|
||||
def download_katas(self, *_largs):
|
||||
def unzipped_name(zipfile):
|
||||
@@ -641,7 +666,7 @@ class ConfigPopup(QuickConfigGui):
|
||||
os.remove(tmp_path)
|
||||
else:
|
||||
os.rename(tmp_path, path)
|
||||
self.katrain.log(f"Download of katago binary {binary} model complete -> {path}", OUTPUT_INFO)
|
||||
self.katrain.log(f"Download of katago binary {binary} complete -> {path}", OUTPUT_INFO)
|
||||
except Exception as e:
|
||||
self.katrain.log(
|
||||
f"Download of katago binary {binary} complete, but could not move file: {e}", OUTPUT_ERROR
|
||||
|
||||
@@ -59,13 +59,13 @@ class ProgressLoader(BoxLayout):
|
||||
request = ObjectProperty()
|
||||
"""UrlRequest object."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, root_instance, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.root_instance = None
|
||||
self.request = None
|
||||
|
||||
def start(self, root_instance):
|
||||
self.root_instance = root_instance
|
||||
self.request = None
|
||||
Clock.schedule_once(self.start, 0)
|
||||
|
||||
def start(self, _dt):
|
||||
self.root_instance.add_widget(self)
|
||||
self.request_download_file(self.download_url, self.path_to_file)
|
||||
Clock.schedule_once(self.animation_show, 1)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 904 KiB |
Reference in new issue
Block a user