* refactor priorities

* performance report doodling

* shutup pycharm

* more doodling

* doodle tricol

* tweak report, fix bug in insert

* reorder colors

* another metric

* sorting engines, trying to catch exc

* restructure engine err

* restructure engine err

* test stuff

* test report

* test report

* test and readme

* test and readme

* layout

* engine recov

* pixelfix and exes

* readme

* keyboard shortcut override

* oops

* mistakes only deeper

* black and readme

* readme

* fix #421, scale repor

* Add shortcut variables (#420)

* Add shortcut variables

* Address comments and missed shortcuts

* fix shortcuts

* rename, design report, fix report

* make translators work more

* relative mid/opening/endgame bounds

* win size dbg

* more errros

* entry

* jeff mode

* fix

* readme

* edit THEMES.md

* edit katrain/core/ai.py, edit katrain/gui/kivyutils.py

* jp (#423)

* edit THEMES.md

* edit katrain/__main__.py, edit katrain/gui/kivyutils.py and 1 other changes

* Update katrain.po (#425)

1.9 first French .po

* edit katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo

* edit katrain/core/ai.py, edit katrain/gui/popups.py

* bluedabadee

* fix sgf and black

* edit katrain/i18n/locales/fr/LC_MESSAGES/katrain.po

* ko

* Update zh-TW translation (#426)

* cn

* cn

* Some corrections to Chinese translations (#427)

* some corrections in cn and tw

* better trans

* edit CONTRIBUTIONS.md, edit katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and 1 other changes

* de

* tw

* player what only

* add green background to better value (#428)

* edit katrain/gui/popups.py, edit katrain/gui/theme.py

* Russian for 1.9 (#432)

* edit katrain/i18n/locales/ru/LC_MESSAGES/katrain.po, edit katrain/gui.kv and 1 other changes

* sound fix

* sound fix

* fmt

* fix sound

* edit THEMES.md, edit CONTRIBUTIONS.md and 1 other changes

* sound fix

* Do not use ffpyplayer on Windows (#434)

* edit katrain/gui/sound.py

* no pr

* jeff theme

* stopsound

* sound and theme fix

* font fix and more errs

* French po corrections (#441)

Former translations were misleading. It should be accurate now.

* add mouse scroll to browser through node variations. (#442)

* add mouse scroll to browser through node variations.

* add mouse scroll to browser through node variations.

* add mouse scroll to browser through node variations.

* edit katrain/popups.kv, edit katrain/__main__.py and 2 other changes

* windows binary, black

* linux binary

* edit INSTALL.md

* edit .github/workflows/osxbuild.yaml

* edit .github/workflows/osxbuild.yaml

* edit .github/workflows/osxbuild.yaml

Co-authored-by: Sander Land <sander.land@cognite.com>
Co-authored-by: roux g. buciu <11182210+electricRGB@users.noreply.github.com>
Co-authored-by: kaorahi <38910552+kaorahi@users.noreply.github.com>
Co-authored-by: Dontbtme <66470440+Dontbtme@users.noreply.github.com>
Co-authored-by: Liou, Guan-Cheng <Tony-Liou@users.noreply.github.com>
Co-authored-by: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com>
Co-authored-by: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Co-authored-by: Alexander Kiselev <satoshi.yoda.register.264@gmail.com>
Co-authored-by: Sander Land <Sander@bioeng219-lap.isd.kcl.ac.uk>
Co-authored-by: Carton He <cartonhe@gmail.com>
Co-authored-by: Sander Land <sander.land@aroni.fritz.box>
Co-authored-by: Sander Land <sander.land@futurefacts.nl>
This commit is contained in:
50 files changed
+1657 -284

No files matched your search

+2 -10
View File
@@ -52,7 +52,7 @@ jobs:
pushd KaTrain.app/Contents/Resources/
ln -s ./venv/bin/KaTrain yourapp
popd
./fix-bundle-metadata.sh KaTrain.app -n KaTrain -v "1.8.0" -a "Sander Land" -o "org.katrain.KaTrain" -i "../../katrain/katrain/img/icon.ico"
./fix-bundle-metadata.sh KaTrain.app -n KaTrain -v "1.9.0" -a "Sander Land" -o "org.katrain.KaTrain" -i "../../katrain/katrain/img/icon.ico"
./cleanup-app.sh KaTrain.app -g 1
./relocate.sh KaTrain.app
- name: Create dmg
@@ -67,12 +67,4 @@ jobs:
with:
name: KaTrainOSX
path: osx_app
- name: Upload to GitHub Release
uses: softprops/action-gh-release@78c309ef59fdb9557cd6574f2e0be552936ed728
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: KaTrainOSX/*
draft: true
fail_on_unmatched_files: true
+1
View File
@@ -1,4 +1,5 @@
dist_sgf
sgftest
# additions
KataGoData
+3 -2
View File
@@ -52,7 +52,8 @@ Many thanks to these additional authors:
* "blamarche" for adding the board coordinates toggle.
* "pdeblanc" for adding the ancient chinese scoring option, fixing a bug in query termination
* "LiamHz" for adding the 'back to main branch' keyboard shortcut.
* "xiaoyifang" for adding the reset analysis option, feature to save options on the loading screen.
* "xiaoyifang" for adding the reset analysis option, feature to save options on the loading screen, and scrolling through variations.
* "electricRGB" for help with adding configurable keyboard shortcuts.
## Translators
@@ -65,7 +66,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"
* Traditional Chinese: "Tony-Liou" with contributions from Ching-yu Lin
## Additional thanks to
+74
View File
@@ -0,0 +1,74 @@
# KataGo troubleshooting
This page lists common ways in which the provided KataGo fails to work out of the box, and how to resolve these issues.
If you find your problem is not in here, you can ask on the [Leela Zero & Friends Discord](http://discord.gg/AjTPFpN) (use the #gui channel),
providing detailed information about your error.
* [General](#General)
* [GPU vs CPU](#CPU)
* [Windows specific help](#Windows)
* [MacOS specific help](#Mac)
* [Linux specific help](#Linux)
## <a name="General"></a> General
### <a name="CPU"></a> GPU vs CPU
The standard executables assume you have a compatible graphics card (GPU).
If you don't, KataGo will fail to start in ways that are difficult for KaTrain to pick up.
On Windows and Linux, you should be able to resolve this by:
* Going to general and engine settings (F8)
* Click 'download katago versions' and wait for downloads to finish.
* Select a CPU based KataGo version (named 'Eigen' after the library it uses).
Keep in mind that a CPU based engine can be significantly slower, and you may want to set your maximum number of
visits to a lower number to compensate for this.
### <a name="Models"></a> KataGo model versions
KataGo models have changed over time, and selecting an older executable with a newer model can lead to errors.
Of the provided binaries, this is typically the case for the 1.6.1 'bigger boards' binary, which should
only be used with the standard 15/20/30/40 block models, and not the newer distributed training models.
## <a name="Mac"></a><img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="macOs" height="35"/> For macOS users
### Running from source
Make sure you `brew install katago` or set the engine path to your own KataGo binary, as there is no executable included.
### New Macs with M1 architecture
Make sure you `brew install katago` as the provided executable does not work on rosetta.
### Getting more information about errors
On macOS, the .app distributable will not show a console, so you will need install using `pip` to see the console window.
## <a name="Windows"></a><img src="https://upload.wikimedia.org/wikipedia/commons/5/5f/Windows_logo_-_2012.svg" alt="Windows" height="35"/> For Windows users
### Getting more information about errors
Run DebugKaTrain.exe, which is released in the .zip file distributable in releases. This will show a console window
which typically tells you more.
## <a name="Linux"></a><img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Linux_Logo_in_Linux_Libertine_Font.svg" alt="Linux" height="35"/> For Linux users
### libzip compatibility
The most common KataGo issue relates to different libzip versions in the provided executables.
Although the provided executables should work on the latest versions of Ubuntu, various other versions and distros differ in their libzip version.
* First, try `sudo apt-get install libzip-dev` to
* Next, try installing all packages mentioned [here](INSTALL.md#LinuxTrouble).
* A final alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
### Getting more information about errors
Check the terminal output around startup time.
+15 -5
View File
@@ -6,6 +6,8 @@
* [Troubleshooting and installation from sources](#WindowsSources)
* [Quick install guide for Linux](#LinuxQuick)
* [Troubleshooting and installation from sources](#LinuxSources)
* [Configuring Multiple GPUS](#GPU)
* [Troubleshooting KataGo](#KataGo)
## <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="macOs" height="35"/> Installation for macOS users
@@ -15,8 +17,9 @@ The easiest way to install is probably [brew](https://brew.sh/). Simply run `bre
You can also find downloadable .app files for macOS [here](https://github.com/sanderland/katrain/releases).
Simply download, unzip the file, mount the .dmg and drag the .app file to your application folder, everything is included.
The first time launching the application you may need to [control-click in finder to give permission for the 'unidentified' app to launch](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac). This is simply a result of Apple charging $99/year to developers to be 'identified'.
Users with the last generation M1 macs with different architecture should then `brew install katago` in addition to this. KaTrain will automatically detect this katago binary.
Users with the last generation M1 macs with different architecture should then `brew install katago` in addition to this. KaTrain will automatically detect this KataGo binary.
### <a name="MacCommand"></a>Command line install guide
@@ -99,7 +102,7 @@ It assumes you have a working Python 3.6+ installation.
A binary for KataGo is included, but if you have compiled your own, press F8 to open general settings and change the
KataGo executable path to the relevant KataGo v1.4+ binary.
### Troubleshooting and advanced installation from sources
### <a name="LinuxTrouble"></a>Troubleshooting and advanced installation from sources
You can try to manually install dependencies to resolve some issues relating to missing dependencies,
e.g. the binary 'wheel' is not provided, KataGo is not starting, or sounds are not working.
@@ -128,7 +131,9 @@ You can now start KaTrain by running `python3 -m katrain`
In case KataGo does not start, an alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
## Configuring the GPU(s) KataGo uses
## <a name="GPU"></a> Configuring the GPU(s) KataGo uses
In most cases KataGo detects your configuration correctly, automatically searching for OpenCL devices and select the highest scoring device.
However, if you have multiple GPUs or want to force a specific device you will need to edit the 'analysis_config.cfg' file in the KataGo folder.
@@ -148,8 +153,8 @@ As you can see it scores about twice as high as the Intel UHD chip and KataGo ha
* Open the 'analysis_config.cfg' file in the `katrain/KataGo` folder in your python packages, or local sources.
If you can't find it, turn on `debug_level=1` in general settings and look for the command that is used to start KataGo.
* Search for `numNNServerThreadsPerModel` (~line 75), uncomment the line by deleting the # and set the value to 2. The line should read `numNNServerThreadsPerModel = 2`.
* Search for `openclDeviceToUseThread` (~line 117), uncomment by deleting the # and set the values to the device ID numbers identified in the terminal.
* Search for `numNNServerThreadsPerModel` (~line 108), uncomment the line by deleting the # and set the value to 2. The line should read `numNNServerThreadsPerModel = 2`.
* Search for `openclDeviceToUseThread` (~line 164), uncomment by deleting the # and set the values to the device ID numbers identified in the terminal.
From the example above, we would want to use devices 1 and 2, for the Intel and AMD GPUs, but not device 0 (the CPU). In our case, the lines should read:
```
openclDeviceToUseThread0 = 1
@@ -165,3 +170,8 @@ openclDeviceToUseThread1 = 2
Using OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) OpenCL 1.2
Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
```
## <a name="KataGo"></a> Troubleshooting and advanced KataGo settings
See [here](ENGINE.md) for an overview of how to resolve various issues with KataGo.
+18 -6
View File
@@ -1,21 +1,25 @@
# Themes
Version 1.7 brings basic support for themes.
Version 1.7 brings basic support for themes, and 1.9 extends it to include keyboard shortcuts and support for multiple theme files.
## Creating and editing themes
* Look at the `Theme` class in `katrain/gui/theme.py`.
* Make a `theme.json` file in your `<home dir>/.katrain` directory and specify any variables from the above class you want to override, e.g.
* Look at the `Theme` class in [`katrain/gui/theme.py`](https://github.com/sanderland/katrain/blob/master/katrain/gui/theme.py).
* Make a `theme<yourthemename>.json` file in your `<home dir>/.katrain` directory and specify any variables from the above class you want to override, e.g.
```json
{
"BACKGROUND_COLOR": [1,0,0,1]
"BACKGROUND_COLOR": [1,0,0,1],
"KEY_STOP_ANALYSIS": "f10",
"MISTAKE_SOUNDS": ["jeff.wav","what.wav"]
}
```
* 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.
* If variables are specified in multiple theme files, the *latest* alphabetically takes precedence. That is, each later theme file overwrites the settings from any previous one.
## Installation
* 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.
* To uninstall a theme, remove theme.json and all relevant images from that folder.
@@ -37,3 +41,11 @@ Version 1.7 brings basic support for themes.
![Preview](https://raw.githubusercontent.com/sanderland/katrain/master/themes/eric-lizzie.png)
### Jeff sounds
* This theme makes Jeff comment `Ahhh?` and `What?!` when you make mistakes.
* Sounds provided by Mikkgo.
[Download](https://github.com/sanderland/katrain/blob/master/themes/jeff-sounds.zip)
+23 -20
View File
@@ -32,9 +32,11 @@ for lang in locales:
strings_to_langs[entry.msgid][lang] = entry
strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
if entry.msgid in lang_to_strings[lang]:
print("duplicate", entry.msgid, "in", lang)
print("duplicate", entry.msgid, "in", lang, "--> deleting", entry.msgstr)
errors = True
lang_to_strings[lang].add(entry.msgid)
po[lang].remove(entry)
else:
lang_to_strings[lang].add(entry.msgid)
if todos[lang] and any("todo" in a for a in sys.argv):
print(f"========== {lang} has {len(todos[lang])} TODO entries ========== ")
for item in todos[lang]:
@@ -76,24 +78,25 @@ for lang in locales:
for msgid, lang_entries in strings_to_langs.items():
if lang in lang_entries and "TODO" in lang_entries[lang].comment:
if any(e.msgstr == lang_entries[lang].msgstr for ll, e in lang_entries.items() if ll != lang):
todo_comment = (
f"TODO - {lang_entries[DEFAULT_LANG].comment}" if lang_entries[DEFAULT_LANG].comment else "TODO"
) # update todo
if (
lang_entries[lang].msgstr != lang_entries[DEFAULT_LANG].msgstr
or lang_entries[lang].comment.replace("\n", " ") != todo_comment
):
print(
[
lang_entries[lang].msgstr,
lang_entries[DEFAULT_LANG].msgstr,
lang_entries[lang].comment,
todo_comment,
]
)
lang_entries[lang].msgstr = lang_entries[DEFAULT_LANG].msgstr # update
lang_entries[lang].comment = todo_comment
print(f"{lang}/{msgid} todo entry updated")
if lang_entries.get(DEFAULT_LANG):
todo_comment = (
f"TODO - {lang_entries[DEFAULT_LANG].comment}" if lang_entries[DEFAULT_LANG].comment else "TODO"
) # update todo
if (
lang_entries[lang].msgstr != lang_entries[DEFAULT_LANG].msgstr
or lang_entries[lang].comment.replace("\n", " ") != todo_comment
):
print(
[
lang_entries[lang].msgstr,
lang_entries[DEFAULT_LANG].msgstr,
lang_entries[lang].comment,
todo_comment,
]
)
lang_entries[lang].msgstr = lang_entries[DEFAULT_LANG].msgstr # update
lang_entries[lang].comment = todo_comment
print(f"{lang}/{msgid} todo entry updated")
po[lang].save(pofile[lang])
mofile = pofile[lang].replace(".po", ".mo")
Binary file not shown.
Binary file not shown.
+132 -67
View File
@@ -1,10 +1,10 @@
"""isort:skip_file"""
# first, logging level lower and force audio framework
# first, logging level lower
import os
os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning")
if "KIVY_AUDIO" not in os.environ:
os.environ["KIVY_AUDIO"] = "sdl2" # some backends hard crash / this seems to be most stable
# if "KIVY_AUDIO" not in os.environ: # trying default again
# os.environ["KIVY_AUDIO"] = "sdl2" # some backends hard crash / this seems to be most stable
import kivy
@@ -30,6 +30,8 @@ from queue import Queue
import urllib3
import webbrowser
import time
import random
import glob
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.app import App
@@ -65,7 +67,15 @@ from katrain.core.constants import (
DATA_FOLDER,
AI_DEFAULT,
)
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup, SaveSGFPopup, ContributePopup
from katrain.gui.popups import (
ConfigTeacherPopup,
ConfigTimerPopup,
I18NPopup,
SaveSGFPopup,
ContributePopup,
EngineRecoveryPopup,
)
from katrain.gui.sound import play_sound
from katrain.core.base_katrain import KaTrainBase
from katrain.core.engine import KataGoEngine
from katrain.core.contribute_engine import KataGoContributeEngine
@@ -221,6 +231,14 @@ class KaTrainGui(Screen, KaTrainBase):
if not self.contributing:
last_player, next_player = self.players_info[cn.player], self.players_info[cn.next_player]
if self.play_analyze_mode == MODE_PLAY and self.nav_drawer.state != "open" and self.popup_open is None:
points_lost = cn.points_lost
if (
last_player.human
and cn.analysis_complete
and points_lost is not None
and points_lost > self.config("trainer/eval_thresholds")[-4]
):
self.play_mistake_sound(cn)
teaching_undo = cn.player and last_player.being_taught and cn.parent
if (
teaching_undo
@@ -437,6 +455,7 @@ class KaTrainGui(Screen, KaTrainBase):
title_key="general settings title", size=[dp(1200), dp(950)], content=ConfigPopup(self)
).__self__
self.config_popup.content.popup = self.config_popup
self.config_popup.title += ": " + self.config_file
self.config_popup.open()
def _do_contribute_popup(self):
@@ -456,10 +475,30 @@ class KaTrainGui(Screen, KaTrainBase):
self.ai_settings_popup.content.popup = self.ai_settings_popup
self.ai_settings_popup.open()
def _do_engine_recovery_popup(self, error_message, code):
current_open = self.popup_open
self.log(f"engine rec {current_open}", OUTPUT_ERROR)
if current_open and isinstance(current_open.content, EngineRecoveryPopup):
self.log(f"Not opening engine recovery popup with {error_message} as one is already open", OUTPUT_DEBUG)
return
popup = I18NPopup(
title_key="engine recovery",
size=[dp(600), dp(700)],
content=EngineRecoveryPopup(self, error_message=error_message, code=code),
).__self__
popup.content.popup = popup
popup.open()
def play_mistake_sound(self, node):
if self.config("timer/sound") and node.played_sound is None and Theme.MISTAKE_SOUNDS:
node.played_sound = True
play_sound(random.choice(Theme.MISTAKE_SOUNDS))
def load_sgf_file(self, file, fast=False, rewind=True):
if self.contributing:
return
try:
file = os.path.abspath(file)
move_tree = KaTrainSGF.parse_file(file)
except (ParseError, FileNotFoundError) as e:
self.log(i18n._("Failed to load SGF").format(error=e), OUTPUT_ERROR)
@@ -561,40 +600,49 @@ class KaTrainGui(Screen, KaTrainBase):
or self.controls.move_tree.collide_point(*touch.pos)
):
if touch.is_mouse_scrolling:
if touch.button == "scrollup":
self("redo")
elif touch.button == "scrolldown":
self("undo")
if self.board_gui.animating_pv is None:
if touch.button == "scrollup":
self("redo")
elif touch.button == "scrolldown":
self("undo")
else:
if touch.button == "scrollup":
self.board_gui.adjust_animate_pv_index(1)
elif touch.button == "scrolldown":
self.board_gui.adjust_animate_pv_index(-1)
return super().on_touch_up(touch)
@property
def shortcuts(self):
return {
"q": self.analysis_controls.show_children,
"w": self.analysis_controls.eval,
"e": self.analysis_controls.hints,
"t": self.analysis_controls.ownership,
"r": self.analysis_controls.policy,
"enter": ("ai-move",),
"numpadenter": ("ai-move",),
"a": ("analyze-extra", "extra"),
"s": ("analyze-extra", "equalize"),
"d": ("analyze-extra", "sweep"),
"f": ("analyze-extra", "alternative"),
"g": ("select-box",),
"h": ("reset-analysis",),
"i": ("insert-mode",),
"p": ("play", None),
"l": ("selfplay-setup", "end", None),
"b": ("undo", "branch"),
"down": ("switch-branch", 1),
"up": ("switch-branch", -1),
"f5": ("timer-popup",),
"f6": ("teacher-popup",),
"f7": ("ai-popup",),
"f8": ("config-popup",),
"f9": ("contribute-popup",),
"escape": ("analyze-extra", "stop"),
k: v
for ks, v in [
(Theme.KEY_ANALYSIS_CONTROLS_SHOW_CHILDREN, self.analysis_controls.show_children),
(Theme.KEY_ANALYSIS_CONTROLS_EVAL, self.analysis_controls.eval),
(Theme.KEY_ANALYSIS_CONTROLS_HINTS, self.analysis_controls.hints),
(Theme.KEY_ANALYSIS_CONTROLS_OWNERSHIP, self.analysis_controls.ownership),
(Theme.KEY_ANALYSIS_CONTROLS_POLICY, self.analysis_controls.policy),
(Theme.KEY_AI_MOVE, ("ai-move",)),
(Theme.KEY_ANALYZE_EXTRA_EXTRA, ("analyze-extra", "extra")),
(Theme.KEY_ANALYZE_EXTRA_EQUALIZE, ("analyze-extra", "equalize")),
(Theme.KEY_ANALYZE_EXTRA_SWEEP, ("analyze-extra", "sweep")),
(Theme.KEY_ANALYZE_EXTRA_ALTERNATIVE, ("analyze-extra", "alternative")),
(Theme.KEY_SELECT_BOX, ("select-box",)),
(Theme.KEY_RESET_ANALYSIS, ("reset-analysis",)),
(Theme.KEY_INSERT_MODE, ("insert-mode",)),
(Theme.KEY_PASS, ("play", None)),
(Theme.KEY_SELFPLAY_TO_END, ("selfplay-setup", "end", None)),
(Theme.KEY_NAV_PREV_BRANCH, ("undo", "branch")),
(Theme.KEY_NAV_BRANCH_DOWN, ("switch-branch", 1)),
(Theme.KEY_NAV_BRANCH_UP, ("switch-branch", -1)),
(Theme.KEY_TIMER_POPUP, ("timer-popup",)),
(Theme.KEY_TEACHER_POPUP, ("teacher-popup",)),
(Theme.KEY_AI_POPUP, ("ai-popup",)),
(Theme.KEY_CONFIG_POPUP, ("config-popup",)),
(Theme.KEY_CONTRIBUTE_POPUP, ("contribute-popup",)),
(Theme.KEY_STOP_ANALYSIS, ("analyze-extra", "stop")),
]
for k in (ks if isinstance(ks, list) else [ks])
}
@property
@@ -607,67 +655,72 @@ class KaTrainGui(Screen, KaTrainBase):
def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
self.last_key_down = keycode
ctrl_pressed = "ctrl" in modifiers
shift_pressed = "shift" in modifiers
if self.controls.note.focus:
return # when making notes, don't allow keyboard shortcuts
popup = self.popup_open
if popup:
if keycode[1] in ["f5", "f6", "f7", "f8", "f9"]: # switch between popups
if keycode[1] in [
Theme.KEY_DEEPERANALYSIS_POPUP,
Theme.KEY_REPORT_POPUP,
Theme.KEY_TIMER_POPUP,
Theme.KEY_TEACHER_POPUP,
Theme.KEY_AI_POPUP,
Theme.KEY_CONFIG_POPUP,
Theme.KEY_CONTRIBUTE_POPUP,
]: # switch between popups
popup.dismiss()
return
elif keycode[1] in ["enter", "numpadenter"]:
elif keycode[1] in Theme.KEY_SUBMIT_POPUP:
fn = getattr(popup.content, "on_submit", None)
if fn:
fn()
return
else:
return
shift_pressed = "shift" in modifiers
shortcuts = self.shortcuts
if keycode[1] == "spacebar":
if keycode[1] == Theme.KEY_TOGGLE_CONTINUOUS_ANALYSIS:
self.toggle_continuous_analysis()
elif keycode[1] == "k":
elif keycode[1] == Theme.KEY_TOGGLE_COORDINATES:
self.board_gui.toggle_coordinates()
elif keycode[1] in ["pause", "break", "f15"] and not ctrl_pressed:
elif keycode[1] in Theme.KEY_PAUSE_TIMER and not ctrl_pressed:
self.controls.timer.paused = not self.controls.timer.paused
elif keycode[1] in ["`", "~", "f12"]:
elif keycode[1] in Theme.KEY_ZEN:
self.zen = (self.zen + 1) % 3
elif keycode[1] in ["left", "z"]:
elif keycode[1] in Theme.KEY_NAV_PREV:
self("undo", 1 + shift_pressed * 9 + ctrl_pressed * 9999)
elif keycode[1] in ["right", "x"]:
elif keycode[1] in Theme.KEY_NAV_NEXT:
self("redo", 1 + shift_pressed * 9 + ctrl_pressed * 9999)
elif keycode[1] == "home":
elif keycode[1] == Theme.KEY_NAV_GAME_START:
self("undo", 9999)
elif keycode[1] == "end":
elif keycode[1] == Theme.KEY_NAV_GAME_END:
self("redo", 9999)
elif keycode[1] == "pageup":
elif keycode[1] == Theme.KEY_MOVE_TREE_MAKE_SELECTED_NODE_MAIN_BRANCH:
self.controls.move_tree.make_selected_node_main_branch()
elif keycode[1] == "n" and not ctrl_pressed:
elif keycode[1] == Theme.KEY_NAV_MISTAKE and not ctrl_pressed:
self("find-mistake", "undo" if shift_pressed else "redo")
elif keycode[1] == "delete" and ctrl_pressed:
elif keycode[1] == Theme.KEY_MOVE_TREE_DELETE_SELECTED_NODE and ctrl_pressed:
self.controls.move_tree.delete_selected_node()
elif keycode[1] == "c" and not ctrl_pressed:
elif keycode[1] == Theme.KEY_MOVE_TREE_TOGGLE_SELECTED_NODE_COLLAPSE and not ctrl_pressed:
self.controls.move_tree.toggle_selected_node_collapse()
elif keycode[1] == "n" and ctrl_pressed:
elif keycode[1] == Theme.KEY_NEW_GAME and ctrl_pressed:
self("new-game-popup")
elif keycode[1] == "l" and ctrl_pressed:
elif keycode[1] == Theme.KEY_LOAD_GAME and ctrl_pressed:
self("analyze-sgf-popup")
elif keycode[1] == "s" and ctrl_pressed:
elif keycode[1] == Theme.KEY_SAVE_GAME and ctrl_pressed:
self("save-game")
elif keycode[1] == "d" and ctrl_pressed:
elif keycode[1] == Theme.KEY_SAVE_GAME_AS and ctrl_pressed:
self("save-game-as-popup")
elif keycode[1] == "c" and ctrl_pressed:
elif keycode[1] == Theme.KEY_COPY and ctrl_pressed:
Clipboard.copy(self.game.root.sgf())
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
elif keycode[1] == "v" and ctrl_pressed:
elif keycode[1] == Theme.KEY_PASTE and ctrl_pressed:
self.load_sgf_from_clipboard()
elif keycode[1] == "b" and shift_pressed:
elif keycode[1] == Theme.KEY_NAV_PREV_BRANCH 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):
shortcut.trigger_action(duration=0)
else:
self(*shortcut)
elif keycode[1] == Theme.KEY_DEEPERANALYSIS_POPUP:
self.analysis_controls.dropdown.open_game_analysis_popup()
elif keycode[1] == Theme.KEY_REPORT_POPUP:
self.analysis_controls.dropdown.open_report_popup()
elif keycode[1] == "f10" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
import yappi
@@ -682,6 +735,13 @@ 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)
elif not ctrl_pressed:
shortcut = self.shortcuts.get(keycode[1])
if shortcut is not None:
if isinstance(shortcut, Widget):
shortcut.trigger_action(duration=0)
else:
self(*shortcut)
def _on_keyboard_up(self, _keyboard, keycode):
if keycode[1] in ["alt", "tab"]:
@@ -723,8 +783,8 @@ class KaTrainApp(MDApp):
resource_add_path(PATHS["PACKAGE"] + "/img")
resource_add_path(os.path.abspath(os.path.expanduser(DATA_FOLDER))) # prefer resources in .katrain
theme_file = resource_find("theme.json")
if theme_file:
theme_files = glob.glob(os.path.join(os.path.expanduser(DATA_FOLDER), "theme*.json"))
for theme_file in sorted(theme_files):
try:
with open(theme_file) as f:
theme_overrides = json.load(f)
@@ -742,9 +802,11 @@ class KaTrainApp(MDApp):
self.gui = KaTrainGui()
Builder.load_file(popup_kv_file)
win_size = self.gui.config("ui_state/size", [])
win_left = self.gui.config("ui_state/left", None)
win_top = self.gui.config("ui_state/top", None)
win_left = win_top = win_size = None
if self.gui.config("ui_state/restoresize", True):
win_size = self.gui.config("ui_state/size", [])
win_left = self.gui.config("ui_state/left", None)
win_top = self.gui.config("ui_state/top", None)
if not win_size:
window_scale_fac = 1
try:
@@ -755,6 +817,7 @@ class KaTrainApp(MDApp):
except Exception as e:
window_scale_fac = 0.85
win_size = [1300 * window_scale_fac, 1000 * window_scale_fac]
self.gui.log(f"Setting window size to {win_size} and position to {[win_left, win_top]}", OUTPUT_DEBUG)
Window.size = (win_size[0], win_size[1])
if win_left is not None and win_top is not None:
Window.left = win_left
@@ -776,6 +839,7 @@ class KaTrainApp(MDApp):
"homepage": HOMEPAGE + "#manual",
"support": HOMEPAGE + "#support",
"contribute:signup": "http://katagotraining.org/accounts/signup/",
"engine:help": HOMEPAGE + "/blob/master/ENGINE.md",
}
if site_key in websites:
webbrowser.open(websites[site_key])
@@ -814,6 +878,7 @@ def run_app():
ex_type, ex, tb = sys.exc_info()
trace = "".join(traceback.format_tb(tb))
app = MDApp.get_running_app()
if app and app.gui:
app.gui.log(
f"Exception {inst.__class__.__name__}: {', '.join(repr(a) for a in inst.args)}\n{trace}",
+3 -2
View File
@@ -8,7 +8,7 @@
"max_visits": 500,
"fast_visits": 25,
"max_time": 8.0,
"wide_root_noise": 0.0,
"wide_root_noise": 0.04,
"_enable_ownership": true
},
"contribute": {
@@ -28,7 +28,7 @@
"anim_pv_time": 0.5,
"debug_level": 0,
"lang": "en",
"version": "1.8.0",
"version": "1.9.0",
"load_fast_analysis": false,
"load_sgf_rewind": true
},
@@ -159,6 +159,7 @@
}
},
"ui_state": {
"restoresize": true,
"size": [],
"play": {
"analysis_controls": {
+67 -2
View File
@@ -33,9 +33,11 @@ from katrain.core.constants import (
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_INFO,
PRIORITY_EXTRA_AI_QUERY,
ADDITIONAL_MOVE_ORDER,
)
from katrain.core.game import Game, GameNode, Move
from katrain.core.utils import var_to_grid, weighted_selection_without_replacement
from katrain.core.utils import var_to_grid, weighted_selection_without_replacement, evaluation_class
def interp_ix(lst, x):
@@ -91,6 +93,69 @@ def ai_rank_estimation(strategy, settings) -> int:
return AI_STRENGTH[strategy]
def game_report(game, thresholds, depth_filter=None):
cn = game.current_node
nodes = cn.nodes_from_root
while cn.children: # main branch
cn = cn.children[0]
nodes.append(cn)
x, y = game.board_size
depth_filter = [math.ceil(board_frac * x * y) for board_frac in depth_filter or (0, 1e9)]
nodes = [n for n in nodes if n.move and not n.is_root and depth_filter[0] <= n.depth < depth_filter[1]]
histogram = [{"B": 0, "W": 0} for _ in thresholds]
ai_top_move_count = {"B": 0, "W": 0}
ai_approved_move_count = {"B": 0, "W": 0}
player_ptloss = {"B": [], "W": []}
weights = {"B": [], "W": []}
for n in nodes:
points_lost = n.points_lost
if n.points_lost is None:
continue
else:
points_lost = max(0, points_lost)
bucket = len(thresholds) - 1 - evaluation_class(points_lost, thresholds)
player_ptloss[n.player].append(points_lost)
histogram[bucket][n.player] += 1
cands = n.parent.candidate_moves
filtered_cands = [d for d in cands if d["order"] < ADDITIONAL_MOVE_ORDER and "prior" in d]
weight = min(
1.0,
sum([max(d["pointsLost"], 0) * d["prior"] for d in filtered_cands])
/ (sum(d["prior"] for d in filtered_cands) or 1e-6),
) # complexity capped at 1
# adj_weight between 0.05 - 1, dependent on difficulty and points lost
adj_weight = max(0.05, min(1.0, max(weight, points_lost / 4)))
weights[n.player].append((weight, adj_weight))
if n.parent.analysis_complete:
ai_top_move_count[n.player] += int(cands[0]["move"] == n.move.gtp())
ai_approved_move_count[n.player] += int(
n.move.gtp()
in [d["move"] for d in filtered_cands if d["order"] == 0 or (d["pointsLost"] < 0.5 and d["order"] < 5)]
)
wt_loss = {
bw: sum(s * aw for s, (w, aw) in zip(player_ptloss[bw], weights[bw]))
/ (sum(aw for _, aw in weights[bw]) or 1e-6)
for bw in "BW"
}
sum_stats = {
bw: {
"accuracy": 100 * 0.75 ** wt_loss[bw],
"complexity": sum(w for w, aw in weights[bw]) / len(player_ptloss[bw]),
"mean_ptloss": sum(player_ptloss[bw]) / len(player_ptloss[bw]),
"weighted_ptloss": wt_loss[bw],
"ai_top_move": ai_top_move_count[bw] / len(player_ptloss[bw]),
"ai_top5_move": ai_approved_move_count[bw] / len(player_ptloss[bw]),
}
if len(player_ptloss[bw]) > 0
else {}
for bw in "BW"
}
return sum_stats, histogram, player_ptloss
def dirichlet_noise(num, dir_alpha=0.3):
sample = [random.gammavariate(dir_alpha, 1) for _ in range(num)]
sum_sample = sum(sample)
@@ -173,7 +238,7 @@ def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optio
cn,
callback=set_analysis,
error_callback=set_error,
priority=1_000,
priority=PRIORITY_EXTRA_AI_QUERY,
ownership=False,
extra_settings=extra_settings,
)
+15 -2
View File
@@ -1,21 +1,34 @@
PROGRAM_NAME = "KaTrain"
VERSION = "1.8.4"
VERSION = "1.9.0"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.8.0" # keep config files from this version
CONFIG_MIN_VERSION = "1.9.0" # keep config files from this version
ANALYSIS_FORMAT_VERSION = "1.0"
DATA_FOLDER = "~/.katrain"
OUTPUT_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5
OUTPUT_INFO = 0
OUTPUT_DEBUG = 1
OUTPUT_EXTRA_DEBUG = 2
KATAGO_EXCEPTION = "KATAGO-INTERNAL-ERROR"
STATUS_ANALYSIS = 1.0 # same priority for analysis/info
STATUS_INFO = 1.1
STATUS_TEACHING = 2.0
STATUS_ERROR = 1000.0
ADDITIONAL_MOVE_ORDER = 999
PRIORITY_GAME_ANALYSIS = -100
PRIORITY_SWEEP = -10 # sweep is live, but slow, so deprioritize
PRIORITY_ALTERNATIVES = 100 # extra analysis, live interaction
PRIORITY_EQUALIZE = 100
PRIORITY_EXTRA_ANALYSIS = 100
PRIORITY_DEFAULT = 1000 # new move, high pri
PRIORITY_EXTRA_AI_QUERY = 10_000
PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai"
PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI]
+7 -11
View File
@@ -10,7 +10,7 @@ import traceback
from collections import defaultdict
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_KATAGO_STDERR, DATA_FOLDER
from katrain.core.engine import EngineDiedException, BaseEngine
from katrain.core.engine import BaseEngine
from katrain.core.game import BaseGame
from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move
@@ -162,9 +162,9 @@ class KataGoContributeEngine(BaseEngine):
self.stdout_thread.start()
self.stderr_thread.start()
def check_alive(self, os_error="", exception_if_dead=False):
def check_alive(self, os_error="", maybe_open_help=False):
ok = self.katago_process and self.katago_process.poll() is None
if not ok and exception_if_dead:
if not ok:
if self.katago_process:
code = self.katago_process and self.katago_process.poll()
if code == 3221225781:
@@ -175,10 +175,6 @@ class KataGoContributeEngine(BaseEngine):
if code != 1 and not self.server_error: # 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)
if not self.server_error: # dont raise if already know what happened
raise EngineDiedException(died_msg)
return ok
def shutdown(self, finish=False):
@@ -210,8 +206,8 @@ class KataGoContributeEngine(BaseEngine):
self.katrain.log(message, OUTPUT_KATAGO_STDERR)
except Exception as e:
print("ERROR in processing KataGo stderr:", line, "Exception", e)
elif self.katago_process:
self.check_alive(exception_if_dead=True)
elif self.katago_process and not self.check_alive():
return
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
return
@@ -273,8 +269,8 @@ class KataGoContributeEngine(BaseEngine):
self.uploaded_games_count += 1
else:
self.katrain.log(line, OUTPUT_KATAGO_STDERR)
elif self.katago_process:
self.check_alive(exception_if_dead=False) # stderr will do this
elif self.katago_process and not self.check_alive(): # stderr will do this
return
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
return
+44 -33
View File
@@ -5,7 +5,6 @@ import platform
import queue
import shlex
import subprocess
import sys
import threading
import time
import traceback
@@ -13,17 +12,20 @@ from typing import Callable, Dict, List, Optional
from kivy.utils import platform as kivy_platform
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, DATA_FOLDER
from katrain.core.constants import (
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_EXTRA_DEBUG,
OUTPUT_KATAGO_STDERR,
DATA_FOLDER,
KATAGO_EXCEPTION,
)
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, json_truncate_arrays
class EngineDiedException(Exception):
pass
class BaseEngine: # some common elements between analysis and contribute engine
RULESETS_ABBR = [
@@ -46,7 +48,7 @@ class BaseEngine: # some common elements between analysis and contribute engine
if ruleset.strip().startswith("{"):
try:
ruleset = json.loads(ruleset)
except:
except json.JSONDecodeError:
pass
if isinstance(ruleset, dict):
return ruleset
@@ -73,17 +75,20 @@ class BaseEngine: # some common elements between analysis and contribute engine
exepath, exename = os.path.split(exe)
if exepath and not os.path.isfile(exe):
self.katrain.log(i18n._("Kata exe not found").format(exe=exe), OUTPUT_ERROR)
self.on_error(i18n._("Kata exe not found").format(exe=exe), "KATAGO-EXE")
return None
elif not exepath:
paths = os.getenv("PATH", ".").split(os.pathsep) + ["/opt/homebrew/bin/"]
exe_with_paths = [os.path.join(path, exe) for path in paths if os.path.isfile(os.path.join(path, exe))]
if not exe_with_paths:
self.katrain.log(i18n._("Kata exe not found in path").format(exe=exe), OUTPUT_ERROR)
self.on_error(i18n._("Kata exe not found in path").format(exe=exe), "KATAGO-EXE")
return None
exe = exe_with_paths[0]
return exe
def on_error(self, message, code, allow_popup):
print("ERROR", message, code)
class KataGoEngine(BaseEngine):
"""Starts and communicates with the KataGO analysis engine"""
@@ -91,6 +96,7 @@ class KataGoEngine(BaseEngine):
def __init__(self, katrain, config):
super().__init__(katrain, config)
self.allow_recovery = self.config.get("allow_recovery", True) # if false, don't give popups
self.queries = {} # outstanding query id -> start time and callback
self.query_counter = 0
self.katago_process = None
@@ -102,29 +108,32 @@ class KataGoEngine(BaseEngine):
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"]
self.shell = True
else:
model = find_package_resource(config["model"])
cfg = find_package_resource(config["config"])
exe = self.get_engine_path(config["katago"])
exe = self.get_engine_path(config.get("katago", "").strip())
if not exe:
return
if not os.path.isfile(model):
self.katrain.log(i18n._("Kata model not found").format(model=model), OUTPUT_ERROR)
self.on_error(i18n._("Kata model not found").format(model=model), code="KATAGO-FILES")
return # don't start
if not os.path.isfile(cfg):
self.katrain.log(i18n._("Kata config not found").format(config=cfg), OUTPUT_ERROR)
self.on_error(i18n._("Kata config not found").format(config=cfg), code="KATAGO-FILES")
return # don't start
self.command = shlex.split(
f'"{exe}" analysis -model "{model}" -config "{cfg}" -analysis-threads {config["threads"]} -override-config "homeDataDir={os.path.expanduser(DATA_FOLDER)}"'
)
self.start()
def start(self):
def on_error(self, message, code=None, allow_popup=True):
self.katrain.log(message, OUTPUT_ERROR)
if self.allow_recovery and allow_popup:
self.katrain("engine_recovery_popup", message, code)
def start(self):
with self.thread_lock:
self.write_queue = queue.Queue()
try:
@@ -142,10 +151,7 @@ class KataGoEngine(BaseEngine):
shell=self.shell,
)
except (FileNotFoundError, PermissionError, OSError) as e:
self.katrain.log(
i18n._("Starting Kata failed").format(command=self.command, error=e),
OUTPUT_ERROR,
)
self.on_error(i18n._("Starting Kata failed").format(command=self.command, error=e), code="c")
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)
@@ -180,7 +186,7 @@ class KataGoEngine(BaseEngine):
self.shutdown(finish=False)
self.start()
def check_alive(self, os_error="", exception_if_dead=False):
def check_alive(self, os_error="", exception_if_dead=False, maybe_open_recovery=False):
ok = self.katago_process and self.katago_process.poll() is None
if not ok and exception_if_dead:
if self.katago_process:
@@ -188,14 +194,12 @@ class KataGoEngine(BaseEngine):
if code == 3221225781:
died_msg = i18n._("Engine missing DLL")
else:
os_error += f"status {code}"
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
if code != 1: # deliberate exit, already showed message?
self.katrain.log(died_msg, OUTPUT_ERROR)
self.katago_process = None
died_msg = i18n._("Engine died unexpectedly").format(error=f"{os_error} status {code}")
if code != 1: # deliberate exit
self.on_error(died_msg, code, allow_popup=maybe_open_recovery)
self.katago_process = None # return from threads
else:
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
raise EngineDiedException(died_msg)
self.katrain.log(i18n._("Engine died unexpectedly").format(error=os_error), OUTPUT_DEBUG)
return ok
def wait_to_finish(self):
@@ -227,14 +231,18 @@ class KataGoEngine(BaseEngine):
try:
line = self.katago_process.stderr.readline()
if line:
if b"Uncaught exception" in line or b"what()" in line: # linux=what
msg = f"KataGo Engine Failed: {line.decode(errors='ignore')[9:].strip()}"
self.on_error(msg, KATAGO_EXCEPTION)
return
try:
self.katrain.log(line.decode(errors="ignore").strip(), OUTPUT_KATAGO_STDERR)
except Exception as e:
print("ERROR in processing KataGo stderr:", line, "Exception", e)
elif self.katago_process:
self.check_alive(exception_if_dead=True)
elif not self.check_alive(exception_if_dead=True):
return
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
self.katrain.log(f"Exception in reading stderr: {e}", OUTPUT_DEBUG)
return
def _analysis_read_thread(self):
@@ -242,13 +250,15 @@ class KataGoEngine(BaseEngine):
try:
line = self.katago_process.stdout.readline().strip()
if self.katago_process and not line:
self.check_alive(exception_if_dead=True)
if not self.check_alive(exception_if_dead=True, maybe_open_recovery=True):
return
except OSError as e:
self.check_alive(os_error=str(e), exception_if_dead=True)
self.check_alive(os_error=str(e), exception_if_dead=True, maybe_open_recovery=True)
return
if b"Uncaught exception" in line:
self.katrain.log(f"KataGo Engine Failed: {line.decode(errors='ignore')}", OUTPUT_ERROR)
msg = f"KataGo Engine Failed: {line.decode(errors='ignore')}"
self.on_error(msg, KATAGO_EXCEPTION)
return
if not line:
continue
@@ -314,7 +324,8 @@ class KataGoEngine(BaseEngine):
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)
self.katrain.log(f"Exception in writing to katago: {e}", OUTPUT_DEBUG)
return # some other thread will take care of this
def send_query(self, query, callback, error_callback, next_move=None, node=None):
self.write_queue.put((query, callback, error_callback, next_move, node))
+30 -17
View File
@@ -1,7 +1,6 @@
import copy
import math
import os
import random
import re
import threading
from datetime import datetime
@@ -21,6 +20,12 @@ from katrain.core.constants import (
STATUS_ERROR,
STATUS_INFO,
STATUS_TEACHING,
PRIORITY_GAME_ANALYSIS,
PRIORITY_EXTRA_ANALYSIS,
PRIORITY_SWEEP,
PRIORITY_ALTERNATIVES,
PRIORITY_EQUALIZE,
PRIORITY_DEFAULT,
)
from katrain.core.engine import KataGoEngine
from katrain.core.game_node import GameNode
@@ -92,7 +97,7 @@ class BaseGame:
if handicap:
self.root.place_handicap_stones(handicap)
if not self.root.get_property("RU"):
if not self.root.get_property("RU"): # if rules missing in sgf, inherit current
self.root.set_property("RU", katrain.config("game/rules"))
self.set_current_node(self.root)
@@ -439,11 +444,11 @@ class Game(BaseGame):
self.region_of_interest = None
threading.Thread(
target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast, even_if_present=False),
target=lambda: self.analyze_all_nodes(analyze_fast=analyze_fast, even_if_present=False),
daemon=True,
).start() # return faster, but bypass Kivy Clock
def analyze_all_nodes(self, priority=0, analyze_fast=False, even_if_present=True):
def analyze_all_nodes(self, priority=PRIORITY_GAME_ANALYSIS, analyze_fast=False, even_if_present=True):
for node in self.root.nodes_in_tree:
# 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():
@@ -502,14 +507,14 @@ class Game(BaseGame):
]
try:
while True:
if copy_from_node.move not in already_inserted_moves:
for m in copy_from_node.move_with_placements:
for m in copy_from_node.move_with_placements:
if m not in already_inserted_moves:
self._validate_move_and_update_chains(m, True)
# this inserts
copy_to_node = GameNode(
parent=copy_to_node, properties=copy.deepcopy(copy_from_node.properties)
)
num_copied += 1
# this inserts
copy_to_node = GameNode(
parent=copy_to_node, properties=copy.deepcopy(copy_from_node.properties)
)
num_copied += 1
if not copy_from_node.children:
break
copy_from_node = copy_from_node.ordered_children[0]
@@ -571,18 +576,26 @@ class Game(BaseGame):
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
cn.analyze(
engine, visits=visits, priority=-1_000, region_of_interest=self.region_of_interest, time_limit=False
engine,
visits=visits,
priority=PRIORITY_EXTRA_ANALYSIS,
region_of_interest=self.region_of_interest,
time_limit=False,
)
return
if mode == "game":
nodes = self.root.nodes_in_tree
only_mistakes = kwargs.get("mistakes_only", False)
threshold = self.katrain.config("trainer/eval_thresholds")[-4]
if "visits" in kwargs:
visits = kwargs["visits"]
else:
min_visits = min(node.analysis_visits_requested for node in nodes)
visits = min_visits + engine.config["max_visits"]
for node in nodes:
node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
max_point_loss = max(c.points_lost or 0 for c in [node] + node.children)
if not only_mistakes or max_point_loss > threshold:
node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
return
@@ -613,19 +626,19 @@ class Game(BaseGame):
]
visits = engine.config["fast_visits"]
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000_000_000
priority = PRIORITY_SWEEP
elif mode in ["equalize", "alternative", "local"]:
if not cn.analysis_complete and mode != "local":
self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node)
return
if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird
self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS)
cn.analyze(engine, priority=-500, time_limit=False, find_alternatives="alternative")
cn.analyze(engine, priority=PRIORITY_ALTERNATIVES, time_limit=False, find_alternatives="alternative")
visits = engine.config["fast_visits"]
else: # equalize
visits = max(d["visits"] for d in cn.analysis["moves"].values())
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000
priority = PRIORITY_EQUALIZE
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
else:
raise ValueError("Invalid analysis mode")
@@ -653,7 +666,7 @@ class Game(BaseGame):
self.engines[node.player].request_analysis(
node,
callback=lambda result, _partial: set_analysis(node, result),
priority=-1000,
priority=PRIORITY_DEFAULT,
analyze_fast=True,
extra_settings=engine_settings,
**analysis_kwargs,
+14 -5
View File
@@ -12,6 +12,8 @@ from katrain.core.constants import (
SGF_INTERNAL_COMMENTS_MARKER,
SGF_SEPARATOR_MARKER,
VERSION,
PRIORITY_DEFAULT,
ADDITIONAL_MOVE_ORDER,
)
from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move, SGFNode
@@ -39,6 +41,7 @@ class GameNode(SGFNode):
def __init__(self, parent=None, properties=None, move=None):
super().__init__(parent=parent, properties=properties, move=move)
self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move)
self.played_sound = None
self.ai_thoughts = ""
self.note = ""
self.move_number = 0
@@ -182,7 +185,7 @@ class GameNode(SGFNode):
def analyze(
self,
engine,
priority=0,
priority=PRIORITY_DEFAULT,
visits=None,
time_limit=True,
refine_move=None,
@@ -211,13 +214,17 @@ class GameNode(SGFNode):
if cur is None:
self.analysis["moves"][move_gtp] = {
"move": move_gtp,
"order": 999,
"order": ADDITIONAL_MOVE_ORDER,
**move_analysis,
} # some default values for keys missing in rootInfo
else:
cur["order"] = min(cur["order"], move_analysis.get("order", 999)) # parent arriving after child
cur["order"] = min(
cur["order"], move_analysis.get("order", ADDITIONAL_MOVE_ORDER)
) # parent arriving after child
if cur["visits"] < move_analysis["visits"]:
cur.update(move_analysis)
else: # prior etc only
cur.update({k: v for k, v in move_analysis.items() if k not in cur})
def set_analysis(
self,
@@ -238,7 +245,7 @@ class GameNode(SGFNode):
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
move_dict["order"] = ADDITIONAL_MOVE_ORDER # 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")
@@ -344,7 +351,6 @@ class GameNode(SGFNode):
if previous_top_move.get("pv") and (sgf or details):
pv = self.make_pv(single_move.player, previous_top_move["pv"], interactive)
text += i18n._("Info:PV").format(pv=pv) + "\n"
if sgf or details or teach:
currmove_pol_rank, currmove_pol_prob, policy_ranking = self.move_policy_stats()
if currmove_pol_rank is not None:
@@ -417,10 +423,13 @@ class GameNode(SGFNode):
root_score = self.analysis["root"]["scoreLead"]
root_winrate = self.analysis["root"]["winrate"]
move_dicts = list(self.analysis["moves"].values()) # prevent incoming analysis from causing crash
top_move = [d for d in move_dicts if d["order"] == 0]
top_score_lead = top_move[0]["scoreLead"] if top_move else root_score
return sorted(
[
{
"pointsLost": self.player_sign(self.next_player) * (root_score - d["scoreLead"]),
"relativePointsLost": self.player_sign(self.next_player) * (top_score_lead - d["scoreLead"]),
"winrateLost": self.player_sign(self.next_player) * (root_winrate - d["winrate"]),
**d,
}
+2 -2
View File
@@ -448,7 +448,7 @@ class SGF:
try:
self.ix = self.contents.index("(") + 1
except ValueError:
raise ParseError("Parse error: Expected '('")
raise ParseError(f"Parse error: Expected '(' at start, found {self.contents[:50]}")
self.root = self._NODE_CLASS()
self._parse_branch(self.root)
@@ -465,7 +465,7 @@ class SGF:
self._parse_branch(self._NODE_CLASS(parent=current_move))
elif matched_item == ";":
# ignore ;) for old SGF
useless = self.ix < len(self.contents) and self.contents[self.ix] == ")"
useless = self.ix < len(self.contents) and self.contents[self.ix :].strip() == ")"
# ignore ; that generate empty nodes
if not (current_move.empty or useless):
current_move = self._NODE_CLASS(parent=current_move)
+58 -2
View File
@@ -25,11 +25,13 @@
#<Label>:
# canvas.before:
# Color:
# rgba: RED
# rgba: [1,0,0,1]
# Line:
# rectangle: (*self.pos,*self.size)
# width: 1
# mixins
<BackgroundMixin>:
canvas.before:
@@ -45,6 +47,49 @@
rounded_rectangle: (self.x + root.outline_width, self.y + root.outline_width, self.width - 2 * root.outline_width, self.height - 2*root.outline_width, root.background_radius,root.background_radius,root.background_radius,root.background_radius,max(1,root.height))
width: root.outline_width or 1
# table cells, stats panel mainly
<TableCellLabel>:
font_size: self.height/3
padding: [Theme.CP_PADDING,Theme.CP_PADDING]
line_height: 0.8
halign: 'center'
canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
canvas:
Color:
rgba: root.outline_color
Line: # left
points: (root.x + root.outline_width,root.y,root.x + root.outline_width,root.y+root.size[1])
width: root.outline_width if 'left' in root.outlines else 1e-6
cap: 'square'
Line: # right
points: (root.x + root.size[0] - root.outline_width,root.y,root.x + root.size[0] - root.outline_width,root.y+root.size[1])
width: root.outline_width if 'right' in root.outlines else 1e-6
cap: 'square'
Line: # top
points: (root.x, root.y+root.size[1]-root.outline_width, root.x + root.size[0], root.y+root.size[1]-root.outline_width)
width: root.outline_width if 'top' in root.outlines else 1e-6
cap: 'square'
Line: # bottom
points: (root.x, root.y+root.outline_width, root.x + root.size[0], root.y+root.outline_width)
width: root.outline_width * int('bottom' in root.outlines) + 1e-6
cap: 'square'
<TableHeaderLabel>:
font_size: self.height/2.5
<TableStatLabel>:
canvas.before:
Color:
rgba: root.bar_color
Rectangle:
pos: root.x if root.side=='right' else root.x+root.size[0]*(1 - min(1,root.value/root.scale)), root.y
size: root.size[0] * min(1,root.value/root.scale), root.size[1]
# new widgets
@@ -273,7 +318,8 @@
padding: [2*CP_PADDING,CP_PADDING,CP_PADDING,CP_PADDING]
subtype_label: subtype_label
CircleWithText:
size_hint: 0.32, 1
size_hint_x: 0.32 if root.alignment=='right' else EPSILON
opacity: 1 if root.alignment=='right' else 0
player: root.player
text: str(root.captures)
BoxLayout:
@@ -299,6 +345,11 @@
valign: 'middle'
shorten: root.player_type==PLAYER_HUMAN # overflow/cut rank off AI names
max_lines: 1
CircleWithText:
size_hint_x: EPSILON if root.alignment=='right' else 0.32
opacity: 0 if root.alignment=='right' else 1
player: root.player
text: str(root.captures)
<PauseButton>:
height: self.width
@@ -765,6 +816,11 @@
icon: 'analysis.png'
shortcut: ''
on_action: root.open_game_analysis_popup()
AnalysisMenuItem:
text: i18n._("analysis:report")
icon: 'analysis.png'
shortcut: 'F3'
on_action: root.open_report_popup()
AnalysisMenuItem:
text: i18n._("analysis:continuous")
icon: 'play.png'
+32 -12
View File
@@ -4,7 +4,6 @@ import time
from typing import List, Optional
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.core.window import Window
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Ellipse, Line, Rectangle
@@ -15,6 +14,7 @@ from kivy.uix.widget import Widget
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from katrain.gui.sound import play_sound
from katrain.core.constants import (
MODE_PLAY,
@@ -33,7 +33,7 @@ from katrain.core.game import Move
from katrain.core.lang import i18n
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.popups import I18NPopup, ReAnalyzeGamePopup, GameReportPopup
from katrain.gui.theme import Theme
@@ -53,6 +53,7 @@ class BadukPanWidget(Widget):
self.active_pv_moves = []
self.animating_pv = None
self.animating_pv_index = None
self.last_mouse_pos = (0, 0)
Window.bind(mouse_pos=self.on_mouse_pos)
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents, 0.05)
@@ -70,12 +71,7 @@ class BadukPanWidget(Widget):
return self.draw_coords_enabled
def play_stone_sound(self, *_args):
if self.katrain.config("timer/sound"):
if not self.stones_sounds:
self.stones_sounds = [SoundLoader.load(file) for file in Theme.STONE_SOUNDS]
sound = random.choice(self.stones_sounds)
if sound:
sound.play()
play_sound(random.choice(Theme.STONE_SOUNDS))
# stone placement functions
def _find_closest(self, pos, gridpos):
@@ -107,7 +103,8 @@ class BadukPanWidget(Widget):
self.redraw_hover_contents_trigger()
def on_touch_down(self, touch):
self.set_animating_pv(None, None) # any click kills PV from label/move
if touch.button not in ["scrollup", "scrolldown"]:
self.set_animating_pv(None, None) # any click kills PV from label/move
if "button" in touch.profile and touch.button != "left":
return
if self.selecting_region_of_interest:
@@ -685,8 +682,7 @@ class BadukPanWidget(Widget):
animating_pv = self.animating_pv
if animating_pv:
pv, node, start_time, _ = animating_pv
delay = self.katrain.config("general/anim_pv_time", 0.5)
up_to_move = (time.time() - start_time) / delay
up_to_move = self.get_animate_pv_index()
self.draw_pv(pv, node, up_to_move)
if getattr(self.katrain.game, "region_of_interest", None):
@@ -749,6 +745,7 @@ class BadukPanWidget(Widget):
draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
def set_animating_pv(self, pv, node):
self.animating_pv_index = None
if pv is None:
self.animating_pv = None
elif node is not None and (
@@ -757,17 +754,40 @@ class BadukPanWidget(Widget):
self.animating_pv = (pv, node, time.time(), self.last_mouse_pos)
self.redraw_hover_contents_trigger()
def adjust_animate_pv_index(self, delta=1):
self.animating_pv_index = max(0, self.get_animate_pv_index() + delta)
def get_animate_pv_index(self):
if self.animating_pv_index is None:
if self.animating_pv:
pv, node, start_time, _ = self.animating_pv
delay = self.katrain.config("general/anim_pv_time", 0.5)
return (time.time() - start_time) / delay
else:
return 0
return self.animating_pv_index
def show_pv_from_comments(self, pv_str):
self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent)
class AnalysisDropDown(DropDown):
def open_game_analysis_popup(self, *_args):
analysis_popup = I18NPopup(title_key="analysis:game", size=[dp(500), dp(300)], content=ReAnalyzeGamePopup())
analysis_popup = I18NPopup(title_key="analysis:game", size=[dp(500), dp(350)], content=ReAnalyzeGamePopup())
analysis_popup.content.popup = analysis_popup
analysis_popup.content.katrain = MDApp.get_running_app().gui
analysis_popup.open()
def open_report_popup(self, *_args):
report_popup = I18NPopup(
title_key="analysis:report",
size=[dp(750), dp(750)],
content=GameReportPopup(katrain=MDApp.get_running_app().gui),
)
report_popup.content.popup = report_popup
report_popup.open()
class AnalysisControls(MDBoxLayout):
dropdown = ObjectProperty(None)
+5 -11
View File
@@ -1,7 +1,6 @@
import time
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.properties import ObjectProperty, OptionProperty
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
@@ -18,6 +17,7 @@ from katrain.core.constants import (
from katrain.core.lang import rank_label
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
from katrain.gui.theme import Theme
from katrain.gui.sound import play_sound, stop_sound
class PlayAnalyzeSelect(MDFloatLayout):
@@ -80,10 +80,6 @@ class ControlsPanel(BoxLayout):
self.status_state = (None, -1e9, None)
self.active_comment_node = None
self.last_timer_update = (None, 0, False)
self.beep = SoundLoader.load(Theme.COUNTDOWN_SOUND)
self.boing = SoundLoader.load(Theme.MINIMUM_TIME_PASSED_SOUND)
if self.boing:
self.boing.volume = 0.1
self.beep_start = 5.2
self.timer_interval = 0.07
@@ -228,13 +224,12 @@ class ControlsPanel(BoxLayout):
if (
min_use
and not new_beeping
and self.boing
and boing_at_remaining - self.timer_interval
< time_remaining
< boing_at_remaining + self.timer_interval
and player.periods_used < byo_num
):
self.boing.play()
play_sound(Theme.MINIMUM_TIME_PASSED_SOUND, volume=0.1)
else:
new_beeping = False
@@ -247,10 +242,9 @@ class ControlsPanel(BoxLayout):
if sounds_on:
if beeping and not new_beeping and not used_period:
self.beep.stop()
elif not beeping and new_beeping and self.beep:
self.beep.volume = 0.5 if periods_rem > 1 else 1
self.beep.play()
stop_sound(Theme.COUNTDOWN_SOUND)
elif not beeping and new_beeping:
play_sound(Theme.COUNTDOWN_SOUND, volume=0.5 if periods_rem > 1 else 1)
self.last_timer_update = (current_node, now, new_beeping)
+34 -1
View File
@@ -48,6 +48,38 @@ class BackgroundMixin(Widget): # -- mixins
outline_width = NumericProperty(1)
class BackgroundLabel(BackgroundMixin, Label):
pass
class TableCellLabel(Label):
background_color = ListProperty([0, 0, 0, 0])
line_width = NumericProperty(0)
outlines = ListProperty([])
outline_color = Theme.LINE_COLOR
outline_width = NumericProperty(1.1)
def __init__(self, **kwargs):
kwargs["font_name"] = kwargs.get("font_name", i18n.font_name)
super().__init__(**kwargs)
class TableStatLabel(TableCellLabel):
side = StringProperty("right")
value = NumericProperty(0)
scale = NumericProperty(100)
bar_color = ListProperty([0, 0, 0, 1])
def __init__(self, **kwargs):
super().__init__(**kwargs)
if "outlines" not in kwargs:
self.outlines = ["left"] if self.side == "right" else ["right"]
class TableHeaderLabel(TableCellLabel):
outlines = ["bottom"]
class LeftButtonBehavior(ButtonBehavior): # stops buttons etc activating on right click
def __init__(self, **kwargs):
self.register_event_type("on_left_release")
@@ -371,7 +403,7 @@ class PlayerSetupBlock(MDBoxLayout):
class PlayerInfo(MDBoxLayout, BackgroundMixin):
captures = NumericProperty(0)
captures = ObjectProperty(0)
player = OptionProperty("B", options=["B", "W"])
player_type = StringProperty("Player")
komi = NumericProperty(0)
@@ -379,6 +411,7 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin):
name = StringProperty("", allownone=True)
rank = StringProperty("", allownone=True)
active = BooleanProperty(True)
alignment = StringProperty("right")
def __init__(self, **kwargs):
super().__init__(**kwargs)
+136 -17
View File
@@ -14,6 +14,7 @@ from kivy.metrics import dp
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from kivy.utils import platform
@@ -22,7 +23,7 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField
from katrain.core.ai import ai_rank_estimation
from katrain.core.ai import ai_rank_estimation, game_report
from katrain.core.constants import (
AI_CONFIG_DEFAULT,
AI_DEFAULT,
@@ -35,11 +36,24 @@ from katrain.core.constants import (
OUTPUT_INFO,
SGF_INTERNAL_COMMENTS_MARKER,
STATUS_INFO,
PLAYER_HUMAN,
ADDITIONAL_MOVE_ORDER,
)
from katrain.core.engine import KataGoEngine
from katrain.core.lang import i18n, rank_label
from katrain.core.utils import PATHS, find_package_resource
from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner
from katrain.core.sgf_parser import Move
from katrain.core.utils import PATHS, find_package_resource, evaluation_class
from katrain.gui.kivyutils import (
BackgroundMixin,
I18NSpinner,
BackgroundLabel,
TableHeaderLabel,
TableCellLabel,
TableStatLabel,
PlayerInfo,
SizedRectangleButton,
AutoSizedRectangleButton,
)
from katrain.gui.theme import Theme
from katrain.gui.widgets.progress_loader import ProgressLoader
@@ -353,9 +367,9 @@ class ConfigTeacherPopup(QuickConfigGui):
for k in ["dot color", "point loss threshold", "num undos", "show dots", "save dots"]:
self.options_grid.add_widget(DescriptionLabel(text=i18n._(k), font_name=i18n.font_name, font_size=dp(17)))
for i, (color, threshold, undo, show_dot, savesgf) in enumerate(
zip(Theme.EVAL_COLORS[theme], thresholds, undos, show_dots, savesgfs)
):
for i, color, threshold, undo, show_dot, savesgf in list(
zip(range(len(thresholds)), Theme.EVAL_COLORS[theme], thresholds, undos, show_dots, savesgfs)
)[::-1]:
self.add_option_widgets(
[
BackgroundMixin(background_color=color, size_hint=[0.9, 0.9]),
@@ -438,6 +452,16 @@ class ConfigAIPopup(QuickConfigGui):
Clock.schedule_once(self.katrain.controls.update_players, 0)
class EngineRecoveryPopup(QuickConfigGui):
error_message = StringProperty("")
code = ObjectProperty(None)
def __init__(self, katrain, error_message, code):
super().__init__(katrain)
self.error_message = str(error_message)
self.code = code
class BaseConfigPopup(QuickConfigGui):
MODEL_ENDPOINTS = {
"Latest distributed model": "https://katagotraining.org/api/networks/newest_training/",
@@ -455,20 +479,20 @@ class BaseConfigPopup(QuickConfigGui):
KATAGOS = {
"win": {
"OpenCL v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-opencl-windows-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigenavx2-windows-x64.zip",
"Eigen (CPU, Non-optimized) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-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",
"OpenCL v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-opencl-windows-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-eigenavx2-windows-x64.zip",
"Eigen (CPU, Non-optimized) v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-eigen-windows-x64.zip",
"OpenCL v1.9 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-opencl-windows-x64+bs29.zip",
},
"linux": {
"OpenCL v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-opencl-linux-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigenavx2-linux-x64.zip",
"Eigen (CPU, Non-optimized) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-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",
"OpenCL v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-opencl-linux-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-eigenavx2-linux-x64.zip",
"Eigen (CPU, Non-optimized) v1.9.0": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-eigen-linux-x64.zip",
"OpenCL v1.9 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-opencl-linux-x64+bs29.zip",
},
"just-descriptions": {
"CUDA v1.8.1 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-gpu-cuda10.2-windows-x64.zip",
"CUDA v1.8.1 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-gpu-cuda10.2-linux-x64.zip",
"CUDA v1.9.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-cuda11.2-windows-x64.zip",
"CUDA v1.9.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.9.0/katago-v1.9.0-cuda11.1-linux-x64.zip",
},
}
@@ -565,7 +589,10 @@ class BaseConfigPopup(QuickConfigGui):
self.paths.append(path) # persistent on paths with models found
kata_files += files
kata_files = [(path, find_description(path)) for path in sorted(kata_files, key=lambda f: "bs29" in f)]
kata_files = sorted(
[(path, find_description(path)) for path in kata_files],
key=lambda f: ("bs29" in f[0]) * 0.1 - (f[0] != f[1]),
)
katas_available_msg = i18n._("katago binaries available").format(num=len(kata_files))
self.katago_files.values = [katas_available_msg, i18n._("default katago option")] + [
desc for path, desc in kata_files
@@ -815,3 +842,95 @@ class ReAnalyzeGamePopup(BoxLayout):
def on_submit(self):
self.button.trigger_action(duration=0)
class GameReportPopup(BoxLayout):
def __init__(self, katrain, **kwargs):
super().__init__(**kwargs)
self.katrain = katrain
self.depth_filter = None
Clock.schedule_once(self._refresh, 0)
def set_depth_filter(self, filter):
self.depth_filter = filter
Clock.schedule_once(self._refresh, 0)
def _refresh(self, _dt=0):
game = self.katrain.game
thresholds = self.katrain.config("trainer/eval_thresholds")
sum_stats, histogram, player_ptloss = game_report(game, depth_filter=self.depth_filter, thresholds=thresholds)
labels = [f"≥ {pt}" if pt > 0 else f"< {thresholds[-2]}" for pt in thresholds]
table = GridLayout(cols=3, rows=6 + len(thresholds))
colors = [
[cp * 0.75 for cp in col[:3]] + [1] for col in Theme.EVAL_COLORS[self.katrain.config("trainer/theme")]
]
table.add_widget(TableHeaderLabel(text="", background_color=Theme.BACKGROUND_COLOR))
table.add_widget(TableHeaderLabel(text=i18n._("header:keystats"), background_color=Theme.BACKGROUND_COLOR))
table.add_widget(TableHeaderLabel(text="", background_color=Theme.BACKGROUND_COLOR))
for i, (label, fmt, stat, scale, more_is_better) in enumerate(
[
("accuracy", "{:.1f}", "accuracy", 100, True),
("meanpointloss", "{:.1f}", "mean_ptloss", 5, False),
("aitopmove", "{:.1%}", "ai_top_move", 1, True),
("aitop5", "{:.1%}", "ai_top5_move", 1, True),
]
):
statcell = {
bw: TableStatLabel(
text=fmt.format(sum_stats[bw][stat]) if stat in sum_stats[bw] else "",
side=side,
value=sum_stats[bw].get(stat, 0),
scale=scale,
bar_color=Theme.STAT_BETTER_COLOR
if (sum_stats[bw].get(stat, 0) < sum_stats[Move.opponent_player(bw)].get(stat, 0)) ^ more_is_better
else Theme.STAT_WORSE_COLOR,
background_color=Theme.BOX_BACKGROUND_COLOR,
)
for (bw, side) in zip("BW", ["left", "right"])
}
table.add_widget(statcell["B"])
table.add_widget(TableCellLabel(text=i18n._(f"stat:{label}"), background_color=Theme.BOX_BACKGROUND_COLOR))
table.add_widget(statcell["W"])
table.add_widget(TableHeaderLabel(text=i18n._("header:num moves"), background_color=Theme.BACKGROUND_COLOR))
table.add_widget(TableHeaderLabel(text=i18n._("stats:pointslost"), background_color=Theme.BACKGROUND_COLOR))
table.add_widget(TableHeaderLabel(text=i18n._("header:num moves"), background_color=Theme.BACKGROUND_COLOR))
for i, (col, label, pt) in enumerate(zip(colors[::-1], labels[::-1], thresholds[::-1])):
statcell = {
bw: TableStatLabel(
text=str(histogram[i][bw]),
side=side,
value=histogram[i][bw],
scale=len(player_ptloss[bw]) + 1e-6,
bar_color=col,
background_color=Theme.BOX_BACKGROUND_COLOR,
)
for (bw, side) in zip("BW", ["left", "right"])
}
table.add_widget(statcell["B"])
table.add_widget(TableCellLabel(text=label, background_color=col))
table.add_widget(statcell["W"])
self.stats.clear_widgets()
self.stats.add_widget(table)
for bw, player_info in self.katrain.players_info.items():
self.player_infos[bw].player_type = player_info.player_type
self.player_infos[bw].captures = "" # ;)
self.player_infos[bw].player_subtype = player_info.player_subtype
self.player_infos[bw].name = player_info.name
self.player_infos[bw].rank = (
player_info.sgf_rank
if player_info.player_type == PLAYER_HUMAN
else rank_label(player_info.calculated_rank)
)
# if not done analyzing, check again in 1s
if not self.katrain.engine.is_idle():
Clock.schedule_once(self._refresh, 1)
+38
View File
@@ -0,0 +1,38 @@
from kivy.clock import Clock
from kivymd.app import MDApp
from kivy.core.audio import SoundLoader
from kivy.utils import platform
cached_sounds = {}
# prefer ffpyplayer on linux, then others, avoid gst and avoid or ffpyplayer on windows
ranking = [("ffpy", 98 if platform == "win" else -2), ("sdl", -1), ("gst", 99), ("", 0)]
try:
SoundLoader._classes.sort(key=lambda cls: [v for k, v in ranking if k in cls.__name__.lower()][0])
except Exception as e:
print("Exception sorting sound loaders: ", e) # private vars, so could break with versions etc
def play_sound(file, volume=1, cache=True):
def _play(sound):
if sound:
sound.play()
sound.seek(0)
app = MDApp.get_running_app()
if app and app.gui and app.gui.config("timer/sound"):
sound = cached_sounds.get(file)
if sound is None:
sound = SoundLoader.load(file)
if cache:
cached_sounds[file] = sound
if sound is not None:
sound.volume = volume
Clock.schedule_once(lambda _dt: _play(sound), 0)
def stop_sound(file):
sound = cached_sounds.get(file)
if sound:
sound.stop()
+58 -1
View File
@@ -47,6 +47,8 @@ class Theme:
PLAY_ANALYZE_TAB_COLOR = YELLOW
INPUT_FONT_COLOR = WHITE
MISTAKE_BUTTON_COLOR = [0.79, 0.06, 0.06, 1]
STAT_WORSE_COLOR = [0.8, 0.4, 0.2, 1]
STAT_BETTER_COLOR = [0.2, 0.5, 0.1, 1]
# gui spacing
RIGHT_PANEL_ASPECT_RATIO = 0.4 # W/H
@@ -68,6 +70,7 @@ class Theme:
STONE_SOUNDS = [f"stone{i}.wav" for i in [1, 2, 3, 4, 5]]
COUNTDOWN_SOUND = "countdownbeep.wav"
MINIMUM_TIME_PASSED_SOUND = "boing.wav"
MISTAKE_SOUNDS = []
# eval dots
EVAL_COLORS = {
@@ -109,7 +112,7 @@ class Theme:
PV_TEXT_COLORS = {"W": BLACK, "B": WHITE} # numbers in PV
# board
LINE_COLOR = [0, 0, 0]
LINE_COLOR = [0, 0, 0, 1]
STARPOINT_SIZE = 0.1
BOARD_COLOR = [0.85, 0.68, 0.40, 1]
STONE_SIZE = 0.505 # texture edge is transparent
@@ -147,3 +150,57 @@ class Theme:
MOVE_TREE_INSERT_OTHER = LIGHT_ORANGE
MOVE_TREE_COLLAPSED = LIGHT_GREY
MOVE_TREE_STONE_OUTLINE_COLORS = {"W": BLACK, "B": WHITE}
# keyboard shortcuts
KEY_AI_MOVE = ["enter", "numpadenter"]
KEY_PASS = "p"
KEY_DEEPERANALYSIS_POPUP = "f2"
KEY_REPORT_POPUP = "f3"
KEY_TIMER_POPUP = "f5"
KEY_TEACHER_POPUP = "f6"
KEY_AI_POPUP = "f7"
KEY_CONFIG_POPUP = "f8"
KEY_CONTRIBUTE_POPUP = "f9"
KEY_NEW_GAME = "n"
KEY_SAVE_GAME = "s"
KEY_SAVE_GAME_AS = "d"
KEY_LOAD_GAME = "l"
KEY_SUBMIT_POPUP = ["enter", "numpadenter"]
KEY_ANALYSIS_CONTROLS_SHOW_CHILDREN = "q"
KEY_ANALYSIS_CONTROLS_EVAL = "w"
KEY_ANALYSIS_CONTROLS_HINTS = "e"
KEY_ANALYSIS_CONTROLS_POLICY = "r"
KEY_ANALYSIS_CONTROLS_OWNERSHIP = "t"
KEY_ANALYZE_EXTRA_EXTRA = "a"
KEY_ANALYZE_EXTRA_EQUALIZE = "s"
KEY_ANALYZE_EXTRA_SWEEP = "d"
KEY_ANALYZE_EXTRA_ALTERNATIVE = "f"
KEY_SELECT_BOX = "g"
KEY_RESET_ANALYSIS = "h"
KEY_INSERT_MODE = "i"
KEY_SELFPLAY_TO_END = "l"
KEY_STOP_ANALYSIS = "escape"
KEY_TOGGLE_CONTINUOUS_ANALYSIS = "spacebar"
KEY_COPY = "c"
KEY_PASTE = "v"
KEY_NAV_BRANCH_DOWN = "down"
KEY_NAV_BRANCH_UP = "up"
KEY_NAV_NEXT = ["right", "x"]
KEY_NAV_PREV = ["left", "z"]
KEY_NAV_GAME_START = "home"
KEY_NAV_GAME_END = "end"
KEY_NAV_PREV_BRANCH = "b"
KEY_NAV_MISTAKE = "n"
KEY_MOVE_TREE_DELETE_SELECTED_NODE = "delete"
KEY_MOVE_TREE_MAKE_SELECTED_NODE_MAIN_BRANCH = "pageup"
KEY_MOVE_TREE_TOGGLE_SELECTED_NODE_COLLAPSE = "c"
KEY_PAUSE_TIMER = ["pause", "break", "f15"]
KEY_TOGGLE_COORDINATES = "k"
KEY_ZEN = ["`", "~", "f12"]
Binary file not shown.
+73 -3
View File
@@ -838,10 +838,80 @@ msgstr "这会产生部分的对局让你接著下。例如:你可以使用它
msgid "editgame"
msgstr "编辑对局资讯"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "新西兰"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
#. View a player performance report
msgid "analysis:report"
msgstr "表现报告"
msgid "engine died popup opening message"
msgstr ""
"KataGo引擎启动失败或崩溃。\n"
"\n"
"错误:{error_message}\n"
"\n"
"错误码:{code}."
#. Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr "如果你还有任何困难,请参考疑难排解页面 {link}"
#. Link in tags should not be touched.
msgid "change engine suggestion"
msgstr "首先,在 {link} 中,尝试下载且更换为不同版本的KataGo执行档是否能解决问题。"
#. as in "click here" to go to this page,
msgid "link_here"
msgstr "点击此处"
#. popup title
msgid "engine recovery"
msgstr "KataGo设定指南"
#. label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "仅分析错误操作"
#. statistic in performance report
msgid "stat:accuracy"
msgstr "准确度评分"
#. header in performance report
msgid "header:keystats"
msgstr "关键数据"
#. header in performance report
msgid "header:num moves"
msgstr "步数"
#. opening of the game filter in performance report
msgid "Opening"
msgstr "开局"
#. end of the game filter in performance report
msgid "Endgame"
msgstr "终盘"
#. middle of the game filter in performance report
msgid "Midgame"
msgstr "中盘"
#. statistic in performance report
msgid "stat:meanpointloss"
msgstr "平均损失目数"
#. statistic in performance report
msgid "stat:aitop5"
msgstr "AI前5佳选点重合度 %"
#. statistic in performance report
msgid "stat:aitopmove"
msgstr "AI最佳选点重合度 %"
#. game filter in performance report
msgid "Entire Game"
msgstr "全局"
Binary file not shown.
+78 -6
View File
@@ -435,9 +435,7 @@ msgstr ""
"Einstellungen auf die richtige KataGo-Datei zeigt."
msgid "Engine died unexpectedly"
msgstr ""
"Engine ist unerwartet abgestürzt, ohne Ausgabe zu senden, wahrscheinlich "
"wegen zu wenig Speicher: {error}"
msgstr "Engine ist unerwartet abgestürzt: {error}"
#. AI names, help etc
msgid "strength:kyu"
@@ -905,10 +903,84 @@ msgstr ""
msgid "editgame"
msgstr "Spiel editieren"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "Neuseeländisch"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
#. - View a player performance report
msgid "analysis:report"
msgstr "Leistungsbericht"
msgid "engine died popup opening message"
msgstr ""
"Das KataGo Engine konnte nicht starten, oder ist abgestürzt.\n"
"\n"
"Der Fehler war: [color=#CC2222]{error_message}[/color]\n"
"\n"
"Der Fehlercode war: [color=#CC2222]{code}[/color]."
#. - Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr ""
"Wenn sie immer noch Schwierigkeiten haben, {link} für eine Hilfe-Seite auf "
"dem neuesten Stand."
#. - Link in tags should not be touched.
msgid "change engine suggestion"
msgstr ""
"Versuchen sie erstmal KataGo Versionen runterzuladen und ob eine andere "
"ausführbare Datei in {link} hilft. "
#. - as in "click here" to go to this page,
msgid "link_here"
msgstr "klicken sie hier"
#. - popup title
msgid "engine recovery"
msgstr "Hilfe für KataGo Konfiguration"
#. - label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "Analysis auf Fehler begrenzen"
#. - statistic in performance report
msgid "stat:accuracy"
msgstr "Genauigkeitsbewertung"
#. - header in performance report
msgid "header:keystats"
msgstr "Schlüsselstatistiken"
#. - header in performance report
msgid "header:num moves"
msgstr "# Züge"
#. - opening of the game filter in performance report
msgid "Opening"
msgstr "Eröffnung"
#. - end of the game filter in performance report
msgid "Endgame"
msgstr "Endspiel"
#. - middle of the game filter in performance report
msgid "Midgame"
msgstr "Mittelspiel"
#. - statistic in performance report
msgid "stat:meanpointloss"
msgstr "Durchschnittlicher Punkteverlust"
#. - statistic in performance report
msgid "stat:aitop5"
msgstr "KI Top 5 Übereinstimmung %"
#. - statistic in performance report
msgid "stat:aitopmove"
msgstr "Bester KI Zug Übereinstimmung %"
#. - game filter in performance report
msgid "Entire Game"
msgstr "Gesamtes Spiel"
Binary file not shown.
+77 -3
View File
@@ -105,6 +105,50 @@ msgstr "Fast analysis of all moves"
msgid "analysis:game"
msgstr "Deeper full game analysis"
#. View a player performance report
msgid "analysis:report"
msgstr "Performance Report"
#. game filter in performance report
msgid "Entire Game"
msgstr "Entire Game"
#. opening of the game filter in performance report
msgid "Opening"
msgstr "Opening"
#. middle of the game filter in performance report
msgid "Midgame"
msgstr "Midgame"
#. end of the game filter in performance report
msgid "Endgame"
msgstr "Endgame"
#. header in performance report
msgid "header:keystats"
msgstr "Key Statistics"
#. statistic in performance report
msgid "stat:accuracy"
msgstr "Accuracy Rating"
#. statistic in performance report
msgid "stat:meanpointloss"
msgstr "Mean Point Loss"
#. statistic in performance report
msgid "stat:aitopmove"
msgstr "AI Best Move Match %"
#. statistic in performance report
msgid "stat:aitop5"
msgstr "AI Top 5 Match %"
#. header in performance report
msgid "header:num moves"
msgstr "# Moves"
#. Analyze again but exclude all currently considered moves
msgid "analysis:alternatives"
msgstr "Find alternative moves"
@@ -648,15 +692,41 @@ msgid "Starting Kata failed"
msgstr "Starting KataGo with command '{command}' failed with error {error}."
msgid "Engine died unexpectedly"
msgstr ""
"Engine died unexpectedly without sending output, possibly due to out of "
"memory: {error}"
msgstr "Engine died unexpectedly without sending output: {error}"
msgid "Engine missing DLL"
msgstr ""
"Failed to start KataGo due to missing DLL. Check the requirements in the "
"KataGo documentation, or try running the binary by itself for details. "
#. popup title
msgid "engine recovery"
msgstr "Help with KataGo setup"
msgid "engine died popup opening message"
msgstr ""
"The KataGo Engine failed to start, or crashed.\n"
"\n"
"The error was: [color=#CC2222]{error_message}[/color]\n"
"\n"
"The error code was: [color=#CC2222]{code}[/color]."
#. Link in tags should not be touched.
msgid "change engine suggestion"
msgstr ""
"First, try downloading KataGo versions and try if changing to a different "
"executable helps in {link} ."
#. Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr ""
"If you are still having difficulties, {link} for an up-to-date "
"troubleshooting page."
#. as in "click here" to go to this page,
msgid "link_here"
msgstr "click here"
#. AI names, help etc
msgid "strength:kyu"
msgstr "k"
@@ -826,6 +896,10 @@ msgstr "All available binaries already downloaded"
msgid "reanalyze max visits"
msgstr "Number of visits per move"
#. label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "Limit analysis to mistakes only"
#. on move tree editing
msgid "Delete Node"
msgstr "Delete Node"
Binary file not shown.
+78 -3
View File
@@ -441,9 +441,7 @@ msgstr ""
"'engine' al valor correcto del ejecutable KataGo."
msgid "Engine died unexpectedly"
msgstr ""
"El motor murió inesperadamente sin mandar alguna señal, posiblemente debido "
"a memoria: {error}"
msgstr "El motor murió inesperadamente sin mandar alguna señal: {error}"
#. AI names, help etc
msgid "strength:kyu"
@@ -924,3 +922,80 @@ msgstr "New Zealand"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
#. TODO - View a player performance report
msgid "analysis:report"
msgstr "Performance Report"
#. TODO
msgid "engine died popup opening message"
msgstr ""
"The KataGo Engine failed to start, or crashed.\n"
"\n"
"The error was: [color=#661111]{error_message}[/color]\n"
"\n"
"The error code was: [color=#661111]{code}[/color]."
#. TODO - Link in tags should not be touched.
msgid "change engine suggestion"
msgstr ""
"First, try downloading KataGo versions and try if changing to a different "
"executable helps in {link} ."
#. TODO - as in "click here" to go to this page,
msgid "link_here"
msgstr "click here"
#. TODO - Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr ""
"If you are still having difficulties, {link} for an up-to-date "
"troubleshooting page."
#. TODO - popup title
msgid "engine recovery"
msgstr "Help with KataGo setup"
#. TODO - label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "Limit analysis to mistakes only"
#. TODO - statistic in performance report
msgid "stat:accuracy"
msgstr "Accuracy Rating"
#. TODO - header in performance report
msgid "header:keystats"
msgstr "Key Statistics"
#. TODO - header in performance report
msgid "header:num moves"
msgstr "# Moves"
#. TODO - opening of the game filter in performance report
msgid "Opening"
msgstr "Opening"
#. TODO - end of the game filter in performance report
msgid "Endgame"
msgstr "Endgame"
#. TODO - middle of the game filter in performance report
msgid "Midgame"
msgstr "Midgame"
#. TODO - statistic in performance report
msgid "stat:meanpointloss"
msgstr "Mean Point Loss"
#. TODO - statistic in performance report
msgid "stat:aitop5"
msgstr "AI Top 5 Match %"
#. TODO - statistic in performance report
msgid "stat:aitopmove"
msgstr "AI Best Move Match %"
#. TODO - game filter in performance report
msgid "Entire Game"
msgstr "Entire Game"
Binary file not shown.
+68 -14
View File
@@ -80,8 +80,8 @@ msgstr ""
msgid "analysis:policy"
msgstr ""
"Coups\n"
"intuitifs"
"Carte des\n"
"intuitions"
msgid "analysis:territory"
msgstr ""
@@ -90,7 +90,7 @@ msgstr ""
#. analysis menu items
msgid "analysis:extra"
msgstr "Analyser davantage"
msgstr "Approfondir l'analyse"
msgid "analysis:equalize"
msgstr "Égaliser l'analyse des coups"
@@ -99,7 +99,7 @@ msgid "analysis:sweep"
msgstr "Analyser tous les possibles"
msgid "analysis:game"
msgstr "Analyser davantage la partie"
msgstr "Approfondir l'analyse de partie"
msgid "analysis:continuous"
msgstr "Lancer/Arrêter l'analyse continue"
@@ -302,7 +302,6 @@ msgstr "Coréennes"
msgid "aga"
msgstr "AGA"
#. TODO check
msgid "stone_scoring"
msgstr "Chinoises anciennes"
@@ -506,10 +505,7 @@ msgstr ""
"généraux"
msgid "Engine died unexpectedly"
msgstr ""
"Le moteur de jeu est tombé en panne, \n"
"possiblement pour cause de \n"
"mémoire insuffisante : {error}"
msgstr "Le moteur de jeu est tombé en panne: {error}"
msgid "teaching undo message"
msgstr ""
@@ -522,13 +518,13 @@ msgid "sweep analysis"
msgstr "Analyse de tous les possibles ({visits} visites/coup)."
msgid "extra analysis"
msgstr "Analyse supplémentaire de {visits} visites."
msgstr "Analyse approfondie ({visits} visites)."
msgid "equalizing analysis"
msgstr "Égalisation de l'analyse ({visits} visites/coup considéré)."
msgid "game re-analysis"
msgstr "Analyse supplémentaire de la partie ({visits} visites/coup)."
msgstr "Analyse de partie approfondie ({visits} visites/coup)."
#. AI names, help etc
msgid "strength:kyu"
@@ -899,10 +895,68 @@ msgstr ""
msgid "editgame"
msgstr "Partie en cours"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "Néo-zélandaises"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
msgid "analysis:report"
msgstr "Rapport de partie"
msgid "engine died popup opening message"
msgstr ""
"Le moteur de jeu KataGo ne fonctionne pas/plus.\n"
"\n"
"Rapport d'erreur : [color=#CC2222]{error_message}[/color]\n"
"\n"
"Code d'erreur : [color=#CC2222]{code}[/color]"
msgid "go to engine help page"
msgstr ""
"Si le problème persiste, vous pouvez consulter une page d'aide tenue à jour "
"en cliquant sur {link}."
msgid "change engine suggestion"
msgstr ""
"L'exécutable et le modèle KataGo utilisés doivent être compatibles. Vous "
"pouvez en télécharger et/ou en sélectionner d'autres dans les {link}. "
msgid "link_here"
msgstr "ce lien"
msgid "engine recovery"
msgstr "Aide au dépannage de KataGo"
msgid "reanalyze mistakes only"
msgstr "Restreindre l'analyse aux seules erreurs"
msgid "stat:accuracy"
msgstr "Acuité du jeu"
msgid "header:keystats"
msgstr "Statistiques des coups"
msgid "header:num moves"
msgstr "Nombre de coups"
msgid "Opening"
msgstr "Début de Partie"
msgid "Endgame"
msgstr "Fin de Partie"
msgid "Midgame"
msgstr "Milieu de Partie"
msgid "stat:meanpointloss"
msgstr "Perte moyenne"
msgid "stat:aitop5"
msgstr "L'un des 5 coups prioritaires"
msgid "stat:aitopmove"
msgstr "Ce que jouerait KataGo"
msgid "Entire Game"
msgstr "Partie entière"
Binary file not shown.
+76 -6
View File
@@ -336,7 +336,9 @@ msgid "point loss threshold"
msgstr "何目より損なら"
msgid "num undos"
msgstr "再考を促す回数"
msgstr ""
"再考を促す回数\n"
"(指導対局時)"
msgid "dot color"
msgstr "色"
@@ -450,7 +452,7 @@ msgstr ""
"クリップボードの内容:{contents}..."
msgid "Failed to load SGF"
msgstr "SGFのロードに失敗しました: {error}"
msgstr "SGFのロードに失敗しました:{error}"
msgid "Kata exe not found"
msgstr "KataGoの実行ファイル {exe} がありません。一般設定を確認してください。"
@@ -470,7 +472,7 @@ msgid "Starting Kata failed"
msgstr "コマンド '{command}' によるKataGoの起動に失敗しました。エラー {error}"
msgid "Engine died unexpectedly"
msgstr "出力なしで予期せずエンジンが止まりました。メモリ不足かもしれません:{error}"
msgstr "出力なしで予期せずエンジンが止まりました:{error}"
#. AI names, help etc
msgid "strength:kyu"
@@ -882,10 +884,78 @@ msgstr ""
msgid "editgame"
msgstr "対局情報を編集"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "ニュージーランド"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
#. View a player performance report
msgid "analysis:report"
msgstr "評価レポート"
msgid "engine died popup opening message"
msgstr ""
"おおっと!KataGoエンジンが起動に失敗したか、クラッシュしました。\n"
" エラー内容:{error_message}\n"
"エラーコード {code}"
#. Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr "それでもだめなら、{link}して最新のトラブルシューティングページへ。"
#. Link in tags should not be touched.
msgid "change engine suggestion"
msgstr "まずは、{link}から別バージョンのKataGoをダウンロードしてうまくいくか試してください。"
#. as in "click here" to go to this page,
msgid "link_here"
msgstr "ここをクリック"
#. popup title
msgid "engine recovery"
msgstr "KataGoセットアップのヘルプ"
#. label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "分析を悪手だけに限定"
#. statistic in performance report
msgid "stat:accuracy"
msgstr "正確度の評点"
#. header in performance report
msgid "header:keystats"
msgstr "基本統計"
#. header in performance report
msgid "header:num moves"
msgstr "手数"
#. opening of the game filter in performance report
msgid "Opening"
msgstr "序盤"
#. end of the game filter in performance report
msgid "Endgame"
msgstr "終盤"
#. middle of the game filter in performance report
msgid "Midgame"
msgstr "中盤"
#. statistic in performance report
msgid "stat:meanpointloss"
msgstr "平均損失目数"
#. statistic in performance report
msgid "stat:aitop5"
msgstr "AIの上位5候補手との一致率"
#. statistic in performance report
msgid "stat:aitopmove"
msgstr "AIの最善手との一致率"
#. game filter in performance report
msgid "Entire Game"
msgstr "全局"
Binary file not shown.
+58 -4
View File
@@ -446,7 +446,7 @@ msgstr ""
" 값이 올바른 KataGo 실행 파일 경로가 맞는지 확인해 주십시오."
msgid "Engine died unexpectedly"
msgstr "엔진이 출력을 보내지 않고 예상치 않게 종료되었습니다. 메모리 용량 초과일 가능성이 있습니다: {error}."
msgstr "엔진이 출력을 보내지 않고 예상치 않게 종료되었습니다: {error}."
#. AI names, help etc
msgid "strength:kyu"
@@ -837,10 +837,64 @@ msgstr ""
msgid "editgame"
msgstr "대국 정보 편집"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "뉴질랜드"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
msgid "analysis:report"
msgstr "기력 보고서"
msgid "engine died popup opening message"
msgstr ""
"KataGo 엔진을 시작할 수 없거나, 갑자기 멈추었습니다.\n"
"\n"
"오류: [color=#CC2222]{error_message}[/color]\n"
"\n"
"오류 코드:: [color=#CC2222]{code}[/color]."
msgid "go to engine help page"
msgstr "아직 문제를 겪고 있으시다면, {link}에서 최신 문제 해결법을 찾아보십시오."
msgid "change engine suggestion"
msgstr "우선, KataGo 버전들을 내려받아 보시고, {link}에서 다른 실행 파일로 바꾸면 괜찮아지는지 확인해 보십시오."
msgid "link_here"
msgstr "여기"
msgid "engine recovery"
msgstr "KataGo 설정 도움말"
msgid "reanalyze mistakes only"
msgstr "실수만 분석하기"
msgid "stat:accuracy"
msgstr "정확도 지표"
msgid "header:keystats"
msgstr "주요 통계"
msgid "header:num moves"
msgstr "착수 수"
msgid "Opening"
msgstr "포석"
msgid "Endgame"
msgstr "끝내기"
msgid "Midgame"
msgstr "중반"
msgid "stat:meanpointloss"
msgstr "평균 집 손해"
msgid "stat:aitop5"
msgstr "인공지능 최선 5수 일치율(%)"
msgid "stat:aitopmove"
msgstr "인공지능 최선수 일치율(%)"
msgid "Entire Game"
msgstr "모두"
Binary file not shown.
+65 -4
View File
@@ -856,10 +856,71 @@ msgstr ""
msgid "editgame"
msgstr "Редактировать игры"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "Новозеландские"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
msgstr "Тромп-Тейлор"
msgid "analysis:report"
msgstr "Отчёт о производительности"
msgid "engine died popup opening message"
msgstr ""
"Движок KataGo не смог запуститься или сломался\n"
"\n"
"Ошибка: [color=#CC2222]{error_message}[/color]\n"
"\n"
"Код ошибки: [color=#CC2222]{code}[/color]."
msgid "go to engine help page"
msgstr ""
"Если у вас до сих пор проблемы, вот ссылка {link} на страницу устранения "
"неполадок."
msgid "change engine suggestion"
msgstr ""
"Во-первых, попробуйте скачать и выбрать другую версию движка KataGo {link}"
msgid "link_here"
msgstr "Нажмите здесь"
msgid "engine recovery"
msgstr "Помощь в настройке KataGo"
msgid "reanalyze mistakes only"
msgstr "Анализировать только ошибки"
msgid "stat:accuracy"
msgstr "Рейтинг точности"
msgid "header:keystats"
msgstr "Ключевые статистики"
msgid "header:num moves"
msgstr "Количество ходов"
msgid "Opening"
msgstr "Начало игры"
msgid "Endgame"
msgstr "Окончание игры"
msgid "Midgame"
msgstr "Середина игры"
msgid "stat:meanpointloss"
msgstr "Главные потери очков"
msgid "stat:aitop5"
msgstr ""
"% совпадения с\n"
"топ-5 ходами ИИ"
msgid "stat:aitopmove"
msgstr ""
"% совпадения с\n"
"идеальными ходами ИИ"
msgid "Entire Game"
msgstr "Полная игра"
Binary file not shown.
+73 -3
View File
@@ -864,10 +864,80 @@ msgstr "這會產生部分的對局讓你接著下。例如:你可以使用它
msgid "editgame"
msgstr "編輯對局資訊"
#. TODO
msgid "new zealand"
msgstr "New Zealand"
msgstr "紐西蘭"
#. TODO
msgid "tromp-taylor"
msgstr "Tromp-Taylor"
#. View a player performance report
msgid "analysis:report"
msgstr "表現報告"
msgid "engine died popup opening message"
msgstr ""
"KataGo引擎啟動失敗或崩潰。\n"
"\n"
"錯誤:{error_message}\n"
"\n"
"錯誤碼:{code}."
#. Link in tags should not be touched (uses link_here msgid)
msgid "go to engine help page"
msgstr "如果你還有任何困難,請參考疑難排解頁面 {link}"
#. Link in tags should not be touched.
msgid "change engine suggestion"
msgstr "首先,在 {link} 中,嘗試下載且更換為不同版本的KataGo執行檔是否能解決問題。"
#. as in "click here" to go to this page,
msgid "link_here"
msgstr "點擊此處"
#. popup title
msgid "engine recovery"
msgstr "KataGo設定指南"
#. label in little popup for analyzing entire game
msgid "reanalyze mistakes only"
msgstr "僅分析錯誤操作"
#. statistic in performance report
msgid "stat:accuracy"
msgstr "準確度評分"
#. header in performance report
msgid "header:keystats"
msgstr "關鍵數據"
#. header in performance report
msgid "header:num moves"
msgstr "步數"
#. opening of the game filter in performance report
msgid "Opening"
msgstr "開局"
#. end of the game filter in performance report
msgid "Endgame"
msgstr "終盤"
#. middle of the game filter in performance report
msgid "Midgame"
msgstr "中盤"
#. statistic in performance report
msgid "stat:meanpointloss"
msgstr "平均損失目數"
#. statistic in performance report
msgid "stat:aitop5"
msgstr "AI前5佳選點重合度 %"
#. statistic in performance report
msgid "stat:aitopmove"
msgstr "AI最佳選點重合度 %"
#. game filter in performance report
msgid "Entire Game"
msgstr "全局"
+81 -10
View File
@@ -79,7 +79,6 @@
font_size: sp(Theme.DESC_FONT_SIZE) * 0.66
<ConfigPopup>:
configfile: configfile
model_path: model_path
katago_path: katago_path
model_files: model_files
@@ -201,11 +200,11 @@
input_property: "general/debug_level"
helper_text: i18n._("general:debug_level:hint")
helper_text_mode: "on_focus"
SmallDescriptionLabel:
text: i18n._('config file path')
SmallDescriptionLabel:
id: configfile
text: app.gui.config_file
DescriptionLabel:
text: 'Restore window size on startup'
AnchorLayout:
LabelledCheckBox:
input_property: "ui_state/restoresize"
BackgroundMixin:
background_color: LIGHT_GREY
size_hint: None, 1
@@ -255,7 +254,13 @@
height: sp(40)
text: i18n._("update settings")
on_press: root.update_config(True)
# BoxLayout:
# orientation: 'vertical'
# SmallDescriptionLabel:
# text: i18n._('config file path')
# SmallDescriptionLabel:
# id: configfile
# text:
<ContributePopup>:
katago_path: katago_path
@@ -789,6 +794,16 @@
text: i18n._("update ai settings")
on_press: root.update_config(True)
<EngineRecoveryPopup>:
Label:
padding: CP_PADDING, CP_PADDING*3
text: i18n._("engine died popup opening message").format(code=root.code,error_message=root.error_message) + "\n\n" + i18n._("change engine suggestion").format(link="[color=#CCCC11][u][ref=engine_settings]"+i18n._('menu:settings')+"[/ref][/u][/color]") + "\n\n" + i18n._("go to engine help page").format(link="[color=#CCCC11][u][ref=engine_help]"+i18n._('link_here')+"[/ref][/u][/color]")
text_size: self.size
font_size: sp(Theme.DESC_FONT_SIZE)
markup: True
valign: 'top'
on_ref_press: app.webbrowser('engine:help') if args[1]=='engine_help' else app.gui('config-popup')
<ReAnalyzeGamePopup>:
visits: visits
@@ -803,15 +818,23 @@
LabelledIntInput:
id: visits
text: '2500'
BoxLayout:
size_hint: 1, 1.5
orientation: 'horizontal'
DescriptionLabel:
text: i18n._('reanalyze mistakes only')
AnchorLayout:
LabelledCheckBox:
id: mistakes
AnchorLayout:
size_hint: 1, 1
AutoSizedRoundedRectangleButton:
id: button
padding_x: 15
size_hint: None,0.5
size_hint: None,0.66
text: i18n._("analysis:game") # re-use string
on_press:
root.katrain("analyze-extra", "game", visits=visits.input_value)
root.katrain("analyze-extra", "game", visits=visits.input_value, mistakes_only=mistakes.input_value)
root.popup.dismiss()
<LoadSGFPopup>:
@@ -858,4 +881,52 @@
file_must_exist: False
filters: ["*.sgf","*.gib","*.ngf"]
path: "."
size_hint: 1,7
size_hint: 1,7
<GameReportPopup>
player_infos: {'B': player_b, 'W': player_w}
stats: stats
orientation: 'vertical'
padding: Theme.CP_PADDING
BoxLayout:
size_hint_y: 0.8
spacing: CP_SPACING
padding: CP_PADDING
SizedRectangleToggleButton:
text: i18n._('Entire Game')
allow_no_selection: False
size_hint: 1,1
group: 'stats_depth'
state: 'down'
on_press: root.set_depth_filter(None)
SizedRectangleToggleButton:
text: i18n._('Opening')
allow_no_selection: False
size_hint: 1,1
group: 'stats_depth'
on_press: root.set_depth_filter((0,0.14))
SizedRectangleToggleButton:
text: i18n._('Midgame')
allow_no_selection: False
size_hint: 1,1
group: 'stats_depth'
on_press: root.set_depth_filter((0.14,0.4))
SizedRectangleToggleButton:
text: i18n._('Endgame')
group: 'stats_depth'
allow_no_selection: False
size_hint: 1,1
on_press: root.set_depth_filter((0.4,10))
BoxLayout:
size_hint_y: 1.33
PlayerInfo:
player: 'B'
id: player_b
PlayerInfo:
player: 'W'
id: player_w
alignment: 'left'
BoxLayout:
id: stats
size_hint_y: 10
+1
View File
@@ -43,6 +43,7 @@ setup(
"setuptools",
"kivy[full]>=2.0.0",
"kivymd>=0.104.1",
"ffpyplayer",
"importlib_resources ;python_version<'3.7'",
"urllib3",
"pygame;platform_system=='Darwin'", # some mac versions need this for kivy
+107
View File
@@ -0,0 +1,107 @@
import os
import time
import pandas as pd
import numpy as np
from unidecode import unidecode
from katrain.core.ai import game_report
from katrain.core.base_katrain import KaTrainBase
from katrain.core.engine import KataGoEngine
from katrain.core.game import KaTrainSGF, Game
import matplotlib.pyplot as plt
pd.set_option("display.max_rows", 5000)
settings = {
"fast_visits": 25,
"max_visits": 500,
"threads": 64,
"model": "C:\\Users\\sande\\.katrain\\kata1-b40c256-s7907049728-d1917596640.bin.gz",
}
# settings["model"] = "C:\\Users\\sande\\.katrain\\g170e-b20c256x2-s5303129600-d1228401921.bin.gz"
def dan(rank):
rank = rank.lower()
if rank[-1] in ["d", "p"]:
return int(rank[:-1])
elif rank == "?":
return np.nan
else:
assert rank[-1] == "k", f"unexpected rank {rank}"
return 1 - int(rank[:-1])
def polyfit(x, y, degree=1):
coeffs = np.polyfit(x, y, degree)
correlation = np.corrcoef(x, y)[0, 1]
results = {"coef": coeffs.tolist(), "r": correlation, "rsq": correlation ** 2}
return results
katrain = KaTrainBase(force_package_config=True, debug_level=0)
combined_settings = {**katrain.config("engine"), **settings}
engine = KataGoEngine(katrain, {**katrain.config("engine"), **settings})
thresholds = katrain.config("trainer/eval_thresholds")
games = []
n = 0
for sgf in os.listdir("sgftest/"):
if sgf.lower().endswith("sgf"):
print(sgf)
with open(os.path.join("sgftest", sgf)) as f:
move_tree = KaTrainSGF.parse_sgf(f.read())
games.append(Game(katrain, engine, move_tree=move_tree, analyze_fast=False))
n += 1
if n >= 30000: # small test=3
break
while not engine.is_idle():
print(f"waiting for engine to finish...{engine.queries_remaining()} queries left")
time.sleep(0.5)
engine.shutdown(finish=None)
reports = []
for game in games:
sum_stats, _, _ = game_report(game, thresholds=thresholds)
for bw in "BW":
oppbw = "B" if bw == "W" else "W"
info = {
f"name": game.root.get_property(f"P{bw}", "??"),
"rank": game.root.get_property(f"{bw}R", "9p"),
"opp_rank": game.root.get_property(f"{oppbw}R", "9p"),
**sum_stats[bw],
}
reports.append(info)
df = pd.DataFrame(reports).sort_values(by="accuracy", ascending=False).reset_index(drop=True)
df.name = [unidecode(n) for n in df.name]
print(df)
df["numrank"] = [dan(rank) for rank in df["rank"]]
def subplot(sp, ynames):
global df
plt.subplot(2, 2, sp)
legend = []
xfull = np.array(range(df["numrank"].min(), df["numrank"].max() + 1))
cols = "bgr"
for i, yname in enumerate(ynames):
plt.plot(df["numrank"], df[yname], cols[i] + "x")
for i, yname in enumerate(ynames):
fit = polyfit(df["numrank"], df[yname])
a, b = fit["coef"]
plt.plot(xfull, xfull * a + b, cols[i] + ":")
legend.append(f"{yname}: r^2 = {fit['rsq']:.3f}")
plt.xlabel("dan rank")
plt.legend(legend)
plt.subplots(2, 2)
subplot(1, ["accuracy"])
subplot(2, ["complexity"])
subplot(3, ["ai_top_move", "ai_top5_move"])
subplot(4, ["mean_ptloss", "weighted_ptloss"])
plt.show()
+11
View File
@@ -104,6 +104,17 @@ def test_old_server_style():
SGF.parse_sgf(input_sgf)
def test_old_server_style_again():
input_sgf = """(;
SZ[19]TM[600]KM[0.500000]LT[]
;B[fp]BL[500];
)"""
tree = SGF.parse_sgf(input_sgf)
assert 2 == len(tree.nodes_in_tree)
def test_ogs():
file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf")
SGF.parse_file(file)
Binary file not shown.
Binary file not shown.
Binary file not shown.