Merge pull request #325 from sanderland/1.7.2

1.7.2
This commit is contained in:
Sander Land authored and GitHub committed 2021-01-23 18:56:05 +09:00
commit 0bf25c1fc5
12 files changed
+129 -75

No files matched your search

+1
View File
@@ -51,6 +51,7 @@ Many thanks to these additional authors:
* Carton He for contributions to sgf parsing and handling. * Carton He for contributions to sgf parsing and handling.
* "blamarche" for adding the board coordinates toggle. * "blamarche" for adding the board coordinates toggle.
* "pdeblanc" for adding the ancient chinese scoring option. * "pdeblanc" for adding the ancient chinese scoring option.
* "LiamHz" for adding the undo to main branch keyboard shortcut.
## Translators ## Translators
+1
View File
@@ -184,6 +184,7 @@ In addition to shortcuts mentioned above and those shown in the main menu:
* **[Ctrl-delete]** Delete current node. * **[Ctrl-delete]** Delete current node.
* **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point. * **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point.
* **[b]** Go back 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. * **[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. * **[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. * **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
+9 -2
View File
@@ -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. * 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. * 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. * 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. * 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. * 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)
![Preview](https://raw.githubusercontent.com/sanderland/katrain/master/themes/koast.png)
+2
View File
@@ -610,6 +610,8 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO) self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
elif keycode[1] == "v" and ctrl_pressed: elif keycode[1] == "v" and ctrl_pressed:
self.load_sgf_from_clipboard() 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: elif keycode[1] in shortcuts.keys() and not ctrl_pressed:
shortcut = shortcuts[keycode[1]] shortcut = shortcuts[keycode[1]]
if isinstance(shortcut, Widget): if isinstance(shortcut, Widget):
+1 -1
View File
@@ -1,5 +1,5 @@
PROGRAM_NAME = "KaTrain" PROGRAM_NAME = "KaTrain"
VERSION = "1.7.1" VERSION = "1.7.2"
HOMEPAGE = "https://github.com/sanderland/katrain" HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version
ANALYSIS_FORMAT_VERSION = "1.0" ANALYSIS_FORMAT_VERSION = "1.0"
+46 -41
View File
@@ -52,7 +52,7 @@ class KataGoEngine:
self.write_stdin_thread = None self.write_stdin_thread = None
self.shell = False self.shell = False
self.write_queue = queue.Queue() self.write_queue = queue.Queue()
self.thread_lock = threading.Lock()
exe = config.get("katago", "").strip() exe = config.get("katago", "").strip()
if config.get("altcommand", ""): if config.get("altcommand", ""):
self.command = config["altcommand"] self.command = config["altcommand"]
@@ -92,39 +92,43 @@ class KataGoEngine:
self.start() self.start()
def start(self): def start(self):
self.write_queue = queue.Queue() with self.thread_lock:
try: self.write_queue = queue.Queue()
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG) try:
startupinfo = None self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
if hasattr(subprocess, "STARTUPINFO"): startupinfo = None
startupinfo = subprocess.STARTUPINFO() if hasattr(subprocess, "STARTUPINFO"):
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller startupinfo = subprocess.STARTUPINFO()
self.katago_process = subprocess.Popen( startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller
self.command, self.katago_process = subprocess.Popen(
startupinfo=startupinfo, self.command,
stdin=subprocess.PIPE, startupinfo=startupinfo,
stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE, stdout=subprocess.PIPE,
shell=self.shell, stderr=subprocess.PIPE,
) shell=self.shell,
except (FileNotFoundError, PermissionError, OSError) as e: )
self.katrain.log( except (FileNotFoundError, PermissionError, OSError) as e:
i18n._("Starting Kata failed").format(command=self.command, error=e), self.katrain.log(
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) return # don't start
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True) self.analysis_thread = threading.Thread(target=self._analysis_read_thread, daemon=True)
self.write_stdin_thread = threading.Thread(target=self._write_stdin_thread, daemon=True) self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
self.analysis_thread.start() self.write_stdin_thread = threading.Thread(target=self._write_stdin_thread, daemon=True)
self.stderr_thread.start() self.analysis_thread.start()
self.write_stdin_thread.start() self.stderr_thread.start()
self.write_stdin_thread.start()
def on_new_game(self): def on_new_game(self):
self.base_priority += 1 self.base_priority += 1
for query_id in list(self.queries.keys()): if not self.is_idle():
self.terminate_query(query_id) with self.thread_lock:
self.queries = {} for query_id in list(self.queries.keys()):
self.terminate_query(query_id)
self.queries = {}
self.write_queue = queue.Queue()
def restart(self): def restart(self):
self.queries = {} self.queries = {}
@@ -248,16 +252,17 @@ class KataGoEngine:
query, callback, error_callback, next_move = self.write_queue.get(block=True, timeout=0.1) query, callback, error_callback, next_move = self.write_queue.get(block=True, timeout=0.1)
except queue.Empty: except queue.Empty:
continue continue
if "id" not in query: with self.thread_lock:
self.query_counter += 1 if "id" not in query:
query["id"] = f"QUERY:{str(self.query_counter)}" self.query_counter += 1
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move) query["id"] = f"QUERY:{str(self.query_counter)}"
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG) self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
try: self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
self.katago_process.stdin.write((json.dumps(query) + "\n").encode()) try:
self.katago_process.stdin.flush() self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
except OSError as e: self.katago_process.stdin.flush()
self.check_alive(os_error=str(e), exception_if_dead=False) 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): def send_query(self, query, callback, error_callback, next_move=None):
self.write_queue.put((query, callback, error_callback, next_move)) self.write_queue.put((query, callback, error_callback, next_move))
+14 -1
View File
@@ -279,9 +279,14 @@ class Game:
self._calculate_groups() self._calculate_groups()
return return
break_on_branch = False break_on_branch = False
break_on_main_branch = False
last_branching_node = cn
if n_times == "branch": if n_times == "branch":
n_times = 9999 n_times = 9999
break_on_branch = True break_on_branch = True
elif n_times == "main-branch":
n_times = 9999
break_on_main_branch = True
for move in range(n_times): for move in range(n_times):
if ( if (
stop_on_mistake is not None stop_on_mistake is not None
@@ -291,13 +296,21 @@ class Game:
): ):
self.set_current_node(cn.parent) self.set_current_node(cn.parent)
return return
previous_cn = cn
if cn.shortcut_from: if cn.shortcut_from:
cn = cn.shortcut_from cn = cn.shortcut_from
elif not cn.is_root: elif not cn.is_root:
cn = cn.parent cn = cn.parent
else:
break # root
if break_on_branch and len(cn.children) > 1: if break_on_branch and len(cn.children) > 1:
break 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): def redo(self, n_times=1, stop_on_mistake=None):
if self.insert_mode: if self.insert_mode:
+1 -1
View File
@@ -304,7 +304,7 @@ class SGFNode:
def initial_player(self): # player for first node def initial_player(self): # player for first node
root = self.root root = self.root
if "PL" in root.properties: # explicit 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 elif root.children: # child exist, use it if not placement
for child in root.children: for child in root.children:
for color in "BW": for color in "BW":
+49 -24
View File
@@ -3,6 +3,8 @@ import json
import os import os
import re import re
import stat import stat
import threading
import time
from typing import Any, Dict, List, Tuple, Union from typing import Any, Dict, List, Tuple, Union
from zipfile import ZipFile from zipfile import ZipFile
@@ -426,7 +428,10 @@ class ConfigAIPopup(QuickConfigGui):
class ConfigPopup(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 = { MODELS = {
"20 block model": "https://github.com/lightvector/KataGo/releases/download/v1.4.5/g170e-b20c256x2-s5303129600-d1228401921.bin.gz", "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", "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.paths = [self.katrain.config("engine/model"), "katrain/models", DATA_FOLDER]
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER] self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
Clock.schedule_once(self.check_katas) 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_models)
MDApp.get_running_app().bind(language=self.check_katas) 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 self.paths.append(path) # persistent on paths with models found
model_files += files 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)) 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.values = [models_available_msg] + [desc for desc, path in model_files]
self.model_files.value_keys = [""] + [path 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 self.katago_files.text = katas_available_msg
def download_models(self, *_largs): 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): def download_complete(req, tmp_path, path, model):
try: try:
os.rename(tmp_path, path) 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: 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() self.check_models()
for c in self.download_progress_box.children: for c in self.download_progress_box.children:
if isinstance(c, ProgressLoader) and c.request: if isinstance(c, ProgressLoader) and c.request:
c.request.cancel() 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 downloading = False
dist_models = {k: v for k, v in self.katrain.config("dist_models", {}).items() if k in self.MODEL_ENDPOINTS} 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: try:
http = urllib3.PoolManager() http = urllib3.PoolManager()
response = http.request("GET", url) 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"] dist_models[name] = json.loads(response.data.decode("utf-8"))["model_file"]
except Exception as e: except Exception as e:
self.katrain.log(f"Failed to retrieve info for model: {e}", OUTPUT_INFO) 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 = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
savepath_tmp = savepath + ".part" savepath_tmp = savepath + ".part"
self.katrain.log(f"Downloading {name} model from {url} to {savepath_tmp}", OUTPUT_INFO) self.katrain.log(f"Downloading {name} model from {url} to {savepath_tmp}", OUTPUT_INFO)
progress = ProgressLoader( Clock.schedule_once(
download_url=url, lambda _dt, _savepath=savepath, _savepath_tmp=savepath_tmp, _url=url, _name=name: ProgressLoader(
path_to_file=savepath_tmp, self.download_progress_box,
downloading_text=f"Downloading {name} model: " + "{}", download_url=_url,
label_downloading_text=f"Starting download for {name} model", path_to_file=_savepath_tmp,
download_complete=lambda req, tmp=savepath_tmp, path=savepath, model=name: download_complete( downloading_text=f"Downloading {_name}: " + "{}",
req, tmp, path, model 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( 0,
f"Download {mname} redirected {req.resp_headers}", OUTPUT_DEBUG ) # main thread
),
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)
downloading = True downloading = True
if not downloading: if not downloading:
self.download_progress_box.add_widget( Clock.schedule_once(
Label(text=i18n._("All models downloaded"), font_name=i18n.font_name, text_size=(None, dp(50))) 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 download_katas(self, *_largs):
def unzipped_name(zipfile): def unzipped_name(zipfile):
@@ -641,7 +666,7 @@ class ConfigPopup(QuickConfigGui):
os.remove(tmp_path) os.remove(tmp_path)
else: else:
os.rename(tmp_path, path) 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: except Exception as e:
self.katrain.log( self.katrain.log(
f"Download of katago binary {binary} complete, but could not move file: {e}", OUTPUT_ERROR f"Download of katago binary {binary} complete, but could not move file: {e}", OUTPUT_ERROR
+5 -5
View File
@@ -59,13 +59,13 @@ class ProgressLoader(BoxLayout):
request = ObjectProperty() request = ObjectProperty()
"""UrlRequest object.""" """UrlRequest object."""
def __init__(self, **kwargs): def __init__(self, root_instance, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.root_instance = None
self.request = None
def start(self, root_instance):
self.root_instance = 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.root_instance.add_widget(self)
self.request_download_file(self.download_url, self.path_to_file) self.request_download_file(self.download_url, self.path_to_file)
Clock.schedule_once(self.animation_show, 1) Clock.schedule_once(self.animation_show, 1)
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 904 KiB