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.
* "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
+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.
* **[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.
+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.
## 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)
![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)
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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
+5 -5
View File
@@ -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.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 904 KiB