* distributed option

* i18n, pause, etc

* fixes galore

* max buffer etc

* test override config

* maxgame

* visit

* contribute menu

* restore vars

* i18n

* actually start

* retry distr and errors

* pw eye / move speed

* cfg/no log

* shutdown fix

* shutdown fix

* download fix

* wut

* stats and desc

* esc fix

* Omit terminate queries from KataGoEngine.queries. (#353)

* press key h to clear current node analysis. (#352)

* clear current node all analysis

* add reset key binding

* reset all analysis off.

* code refactor

* code change according to review.

* remove useless variable

* code review changes

* clean up reset node

* oops

* Setup position feature (#354)

* go for zebras

* setup working!

* uhh

* tweak

* img

* fix i18n

* AEEEEEEEEEE and stop

* link in readme

* new game options for edit/new/setup (#357)

* new game options for edit/new/setup

* feedback

* fix

* ai thoughts

* no tristate cb

* branch fixing (#358)

* new game options for edit/new/setup

* feedback

* fix

* ai thoughts

* no tristate cb

* fix comma etc

* normal katago

* kata

* dll

* save window pos/size

* BAD comma

* new linux binary + allow default engine

* fix

* unused icon, build mac

* exe

* player new game fix

* fix i18n

* test test

* just print

* 한국어

* –활성화

* Add new Traditional Chinese translation (#362)

Improve some translations as well.

* cn (#363)

* cn

* rm old

* rm old

* informal you

* mo

* jp (#364)

* config and readme

* mo

* kgtraining

* fix score

* lower stddev a bit

* tweak weights

* 1.8 French Po (#365)

* fr and getattr fix

* -DBUILD_DISTRIBUTED=1

* openssl

* openssl

* openssl

* openssl

* config

* jp (num undos) (#366)

* clarify undo in en, po->mo

* fix player name

* new kata

* new kata

* Load game option save config (#367)

* in the load dialog,  "load fast analysis" and "rewind to the start" option ,save to config file

* in the load dialog,  "load fast analysis" and "rewind to the start" option ,save to config file

* default option

* remove useless override

* save check state when file dialog's submit event ,

* save config in callback

* readme

* active games

* de

* log games as json always

* black

* Russian localization for 1.8.0 (#375)

* ru

Co-authored-by: pdeblanc <peter@spaceandgames.com>
Co-authored-by: xiaoyifang <105986+xiaoyifang@users.noreply.github.com>
Co-authored-by: Liou, Guan-Cheng <Tony-Liou@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: Alexander Kiselev <satoshi.yoda.register.264@gmail.com>
This commit is contained in:
authored and GitHub committed 2021-03-05 20:40:48 +01:00
1 parent 34b3f92d20
commit c5587bf6e1
58 files changed
+5808 -658

No files matched your search

+9 -3
View File
@@ -17,13 +17,19 @@ jobs:
python-version: 3.8
- name: Build KataGo
run: |
export OPENSSL_ROOT_DIR=$(brew --prefix openssl)
export OPENSSL_LIBRARIES="${OPENSSL_ROOT_DIR}/lib"
echo $OPENSSL_ROOT_DIR
ls $OPENSSL_ROOT_DIR
echo $OPENSSL_LIBRARIES
ls $OPENSSL_LIBRARIES
rm katrain/KataGo/katago
rm katrain/KataGo/*.dll
rm katrain/KataGo/katago.exe
cd ..
git clone https://github.com/lightvector/KataGo.git
git clone --branch stable https://github.com/lightvector/KataGo.git
pushd KataGo/cpp
cmake . -DUSE_BACKEND=OPENCL
cmake . -DUSE_BACKEND=OPENCL -DBUILD_DISTRIBUTED=1
make
cp katago ../../katrain/katrain/KataGo/katago-osx
- name: Get Kivy dependencies
@@ -46,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.7.2" -a "Sander Land" -o "org.katrain.KaTrain" -i "../../katrain/katrain/img/icon.ico"
./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"
./cleanup-app.sh KaTrain.app -g 1
./relocate.sh KaTrain.app
- name: Create dmg
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
run: pytest tests
- name: Run I18N conversion
run: python i18n.py
run: python i18n.py -todo
- name: Build
run: |
+1
View File
@@ -19,6 +19,7 @@ logs
callgrind.*
cpp/out
.vs
dist_sgf
# debug
outdated_log.txt
+6 -5
View File
@@ -26,7 +26,7 @@ Adding a translation requires making a new `.po` file with entries for that lang
* Note that anything between `{}` should be left as-is.
* The information at the top of the file should also not be translated.
You can send me the resulting `.po` file and I will integrate it into the program.
You can send me the resulting `.po` file, and I will integrate it into the program.
# Contributors
@@ -38,7 +38,7 @@ You can send me the resulting `.po` file and I will integrate it into the progra
Many thanks to these additional authors:
* Matthew Allred ("Kameone") for design of the v1.1 UI, MacOS installation instructions, and working on promotion and YouTube videos.
* Matthew Allred ("Kameone") for design of the v1.1 UI, macOS installation instructions, and working on promotion and YouTube videos.
* "bale-go" for development and continued work on the 'calibrated rank' AI and rank estimation algorithm.
* "Dontbtme" for detailed feedback and early testing of v1.0+.
* "nowoowoo" for a fix to the parser for SGF files with extra line breaks.
@@ -50,8 +50,9 @@ Many thanks to these additional authors:
* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
* Carton He for contributions to sgf parsing and handling.
* "blamarche" for adding the board coordinates toggle.
* "pdeblanc" for adding the ancient chinese scoring option.
* "LiamHz" for adding the undo to main branch keyboard shortcut.
* "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.
## Translators
@@ -59,7 +60,7 @@ Many thanks to the following contributors for translations.
* French: "Dontbtme" with contributions from "wonderingabout"
* Korean: "isty2e"
* German: "nimets123", "trohde", and "Harleqin"
* German: "nimets123", "trohde", "Harleqin" and "Sovereign"
* Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community
* Russian: Dmitry Ivankov and Alexander Kiselev
* Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin
+4 -4
View File
@@ -7,10 +7,10 @@
* [Quick install guide for Linux](#LinuxQuick)
* [Troubleshooting and installation from sources](#LinuxSources)
## <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="MacOs" height="35"/> Installation for MacOS users
## <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="macOs" height="35"/> Installation for macOS users
### <a name="MacQuick"></a>Quick install guide
You can find downloadable .app files for MacOSX [here](https://github.com/sanderland/katrain/releases).
You can 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.
Users with the last generation M1 macs with different architecture should download this bundle and then `brew install katago` and point KaTrain to their own binary in general settings, as the built-in binary is not compatible.
@@ -67,7 +67,7 @@ If you have a working Python 3.6-3.8 available, you should be able to simply:
### <a name="LinuxSources"></a>Installation from sources
This section describes how to install KaTrain from source,
This section describes how to install KaTrain from sources,
in case you want to run it in a local directory or have more control over the process.
It assumes you have a working Python 3.6+ installation.
@@ -133,7 +133,7 @@ As you can see it scores about twice as high as the Intel UHD chip and KataGo ha
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.
From the example above, we would want to use devices 1 and 2, for the Intel and AMD GPU's, but not device 0 (the CPU). In our case, the lines should read:
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
openclDeviceToUseThread1 = 2
+2 -1
View File
@@ -5,8 +5,9 @@ For on related licenses for these binaries and libraries see https://github.com/
2. Icons from www.flaticon.com, used with permission with the following attributions:
- Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com
- Other Menu icons, Finish, and Flag icons: derived from work by Freepik from www.flaticon.com
- Other Menu icons, Finish, Collaboration, and Flag icons: derived from work by Freepik from www.flaticon.com
- Collapse branch icon: derived from work by Kirill Kazachek from www.flaticon.com
- Reset icon: derived from work by Pixel Perfect from www.flaticon.com
3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use.
+22 -8
View File
@@ -18,6 +18,7 @@
* [Play against AI](#ai)
* [Analyzing your Games](#analysis)
* [Keyboard shortcuts](#keyboard)
* [Distributed training](#distributed)
* [Themes](#themes)
* [FAQ and Troubleshooting](#faq)
* [Contributing](#support)
@@ -64,12 +65,12 @@ but has since grown to include a wide range of features, including:
## <a name="install"></a> Installation
* See the [releases page](http://github.com/sanderland/katrain/releases) for downloadable executables for Windows and Mac.
* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any 64-bit OS.
* [This page](INSTALL.md) has detailed instructions for Window, Linux and MacOS,
* [This page](https://github.com/sanderland/katrain/blob/master/INSTALL.md) has detailed instructions for Window, Linux and macOS,
as well as troubleshooting and setting up KataGo to use multiple GPUs.
## <a name="kata"></a> Configuring KataGo
KaTrain comes pre-packaged with a working KataGo (OpenCL version) for Windows, Linux, and pre-M1 Mac operating systems, and the 15 block neural network.
KaTrain comes pre-packaged with a working KataGo (OpenCL version) for Windows, Linux, and pre-M1 Mac operating systems, and the 15 block model.
To change the model, open 'General and Engine settings' in the application and 'Download models'. You can then select the model you want from the dropdown menu.
@@ -85,7 +86,7 @@ Finally, you can override the entire command used to start the analysis engine,
* Select the players in the main menu, or under 'New Game'.
* In a teaching game, KaTrain will analyze your moves and automatically undo those that are sufficiently bad.
* When playing against AI, note that the "Undo" button will undo both the AI's last move as well as yours.
* When playing against AI, note that the "Undo" button will undo both the AI's last move and yours.
### Instant feedback
@@ -162,6 +163,7 @@ Keyboard shortcuts are shown with **[key]**.
* **[g]**: Select area of interest: set an area and search only for moves in this box.
Good for solving tsumegos. Note that some results may appear outside the box due to establishing a baseline for the best move,
and the opponent can tenuki in variations.
* **[h]**: Reset analysis. This reverts the analysis to what the engine returns after a normal query, removing any additional exploration.
* **[i]**: Start insertion mode. Allows you to insert moves, to improve analysis when both players ignore an important exchange or life and death situation. Press again to stop inserting and copy the rest of the branch.
* **[l]**: Play out the game until the end and add as a collapsed branch, to visualize the potential effect of mistakes. This is done in the background, and can be started at several nodes at once when comparing the results at different starting positions.
* **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on.
@@ -184,30 +186,42 @@ In addition to shortcuts mentioned above and those shown in the main menu:
* **[Ctrl-delete]** Delete current node.
* **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point.
* **[b]** Go back to the previous branching point.
* **[Shift-b]** Go back the the main branch.
* **[Shift-b]** Go back the main branch.
* **[n]** As in clicking the forward red arrow, go to one move before the next mistake (orange or worse) by a human player.
* **[Shift-n]** As in clicking the backward red arrow, go to one move before the previous mistake.
* **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
* **[scroll down]**: Redo move. Only works when hovering the cursor over the board.
* **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
* **[double-click on a move]**: Navigate directly to just before that point in the game.
* **[Ctrl-V]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
* **[Ctrl-V]**: Load SGF from the clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
* **[Ctrl-C]**: Save SGF to clipboard.
* **[Escape]**: Stop all analysis.
## <a name="distributed"></a> Contributing to distributed training
Starting in December 2020, KataGo started [distributed training](https://katagotraining.org/).
This allows people to all help generate self-play games to increase KataGo's strength and train bigger models.
KaTrain 1.8.0+ makes it easy to contribute to distributed training: simply select the option from the main menu, register an account, and click run.
During this mode you can do little more than watch games. Toggling continuous analysis (spacebar) will allow you to switch between manually navigating the current game, and automatically advancing it.
Keep in mind that partial games are not uploaded,
so it is best to plan to keep it running for at least an hour, if not several, for the most effective contribution.
## <a name="themes"></a> Themes
* See [these instructions](THEMES.md) for how to modify the look of any graphics or colours, and creating or instal themes.
* See [these instructions](THEMES.md) for how to modify the look of any graphics or colours, and creating or install themes.
## <a name="faq"></a> FAQ
* The program is running too slowly. How can I speed it up?
* Adjust the number of visits or maximum time allowed in the settings.
* KataGo crashes with out of memory errors, how can I prevent this?
* KataGo crashes with "out of memory" errors, how can I prevent this?
* Try using a lower number for `nnMaxBatchSize` in `KataGo/analysis_config.cfg`, and avoid using versions compiled with large board sizes.
* If still encountering problems, please start KataGo by itself to check for any errors it gives.
* Note that if you don't have a GPU, or your GPU does not support OpenCL, you should use the 'eigen' binaries which run on CPU only.
* The font size is too small
* On some ultra-high resolution monitors, font scale is not set correctly. Please see [these](http://github.com/sanderland/katrain/issues/250) instructions to adjust them.
* On some ultra-high resolution monitors, dialogs and other elements with text can appear too small. Please see [these](https://github.com/sanderland/katrain/issues/359#issuecomment-784096271) instructions to adjust them.
## <a name="support"></a> Support / Contribute
Binary file not shown.
File diff suppressed because it is too large. Load diff
+94
View File
@@ -0,0 +1,94 @@
serverUrl = https://katagotraining.org/
username = x
password = x
# How many games should KataGo play at once? For good GPUs, putting too small a number here will be *very* inefficient.
# For absolute-top-tier GPUs, try numbers like 32, 40, 64, etc.
# For modern good middle-tier GPUs, you can try numbers like 8 or 16.
# For very old or weak GPUs, or if you want KataGo to put less load on your system, try 4.
maxSimultaneousGames = 4
# Set to true if you want one of KataGo's games to be streamed to a text file, so you can watch.
# Follow in a separate shell with a command like the following:
# Linux: "tail -f watchgame.txt"
# Windows Powershell: "Get-Content .\watchgame.txt -Tail 50 -Wait"
watchOngoingGameInFile = false
watchOngoingGameInFileName = watchgame.txt
logGamesAsJson = true
includeOwnership = false
# KataGo will only use one GPU by default. You can edit the below if you have multiple GPUs and want to use them all.
# TO USE MULTIPLE GPUS:
# Set this to the number of GPUs you have and/or would like to use.
# **AND** if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
# numNNServerThreadsPerModel = 1
# CUDA GPU settings--------------------------------------
# These only apply when using the CUDA version of KataGo.
# IF USING ONE GPU: optionally uncomment and change this if the GPU you want to use turns out to be not device 0
# cudaDeviceToUse = 0
# IF USING TWO GPUS: Uncomment these two lines (AND set numNNServerThreadsPerModel above):
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
# IF USING THREE GPUS: Uncomment these three lines (AND set numNNServerThreadsPerModel above):
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
# cudaDeviceToUseThread2 = 2 # change this if the third GPU you want to use turns out to be not device 2
# You can probably guess the pattern if you have four, five, etc. GPUs.
# KataGo will automatically use FP16 or not based on the compute capability of your NVIDIA GPU. If you
# want to try to force a particular behavior though you can uncomment these lines and change them
# to "true" or "false". E.g. it's using FP16 but on your card that's giving an error, or it's not using
# FP16 but you think it should.
# cudaUseFP16 = auto
# cudaUseNHWC = auto
# OpenCL GPU settings--------------------------------------
# These only apply when using the OpenCL version of KataGo.
# Uncomment to tune OpenCL for every board size separately, rather than only the largest possible size
# openclReTunePerBoardSize = true
# IF USING ONE GPU: optionally uncomment and change this if the best device to use is guessed incorrectly.
# The default behavior tries to guess the 'best' GPU or device on your system to use, usually it will be a good guess.
# openclDeviceToUse = 0
# IF USING TWO GPUS: Uncomment these two lines and replace X and Y with the device ids of the devices you want to use.
# It might NOT be 0 and 1, some computers will have many OpenCL devices. You can see what the devices are when
# KataGo starts up - it should print or log all the devices it finds.
# (AND also set numNNServerThreadsPerModel above)
# openclDeviceToUseThread0 = X
# openclDeviceToUseThread1 = Y
# IF USING THREE GPUS: Uncomment these three lines and replace X and Y and Z with the device ids of the devices you want to use.
# It might NOT be 0 and 1 and 2, some computers will have many OpenCL devices. You can see what the devices are when
# KataGo starts up - it should print or log all the devices it finds.
# (AND also set numNNServerThreadsPerModel above)
# openclDeviceToUseThread0 = X
# openclDeviceToUseThread1 = Y
# openclDeviceToUseThread2 = Z
# You can probably guess the pattern if you have four, five, etc. GPUs.
# KataGo will automatically use FP16 or not based on testing your GPU during tuning. If you
# want to try to force a particular behavior though you can uncomment this lines and change it
# to "true" or "false". This is a fairly blunt setting - more detailed settings are testable
# by rerunning the tuner with various arguments.
# openclUseFP16 = auto
# Eigen-specific settings--------------------------------------
# These only apply when using the Eigen (pure CPU) version of KataGo.
# This is the number of CPU threads for evaluating the neural net on the Eigen backend.
# It defaults to numSearchThreads.
# numEigenThreadsPerModel = X
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+114 -39
View File
@@ -2,6 +2,8 @@
# first, logging level lower and force audio framework
import os
from katrain.core.contribute_engine import KataGoContributeEngine
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
@@ -18,20 +20,6 @@ from kivy.utils import platform
ICON = find_package_resource("katrain/img/icon.ico")
Config.set("kivy", "window_icon", ICON)
# finally, window size
WINDOW_SCALE_FAC, WINDOW_X, WINDOW_Y = 1, 1300, 1000
try:
from screeninfo import get_monitors
for m in get_monitors():
WINDOW_SCALE_FAC = min(WINDOW_SCALE_FAC, (m.height - 100) / WINDOW_Y, (m.width - 100) / WINDOW_X)
except Exception as e:
if platform != "macosx":
print(f"Exception {e} while getting screen resolution.")
WINDOW_SCALE_FAC = 0.85
Config.set("graphics", "width", max(400, int(WINDOW_X * WINDOW_SCALE_FAC)))
Config.set("graphics", "height", max(400, int(WINDOW_Y * WINDOW_SCALE_FAC)))
Config.set("input", "mouse", "mouse,multitouch_on_demand")
import re
@@ -77,11 +65,12 @@ from katrain.core.constants import (
SGF_INTERNAL_COMMENTS_MARKER,
MODE_PLAY,
DATA_FOLDER,
AI_DEFAULT,
)
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup, SaveSGFPopup
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup, SaveSGFPopup, ContributePopup
from katrain.core.base_katrain import KaTrainBase
from katrain.core.engine import KataGoEngine
from katrain.core.game import Game, IllegalMoveException, KaTrainSGF
from katrain.core.game import Game, IllegalMoveException, KaTrainSGF, BaseGame
from katrain.core.sgf_parser import Move, ParseError
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, ConfigAIPopup
from katrain.gui.theme import Theme
@@ -103,6 +92,7 @@ class KaTrainGui(Screen, KaTrainBase):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.engine = None
self.contributing = False
self.new_game_popup = None
self.fileselect_popup = None
@@ -110,8 +100,10 @@ class KaTrainGui(Screen, KaTrainBase):
self.ai_settings_popup = None
self.teacher_settings_popup = None
self.timer_settings_popup = None
self.contribute_popup = None
self.idle_analysis = False
self.animate_contributing = False
self.message_queue = Queue()
self.last_key_down = None
@@ -120,14 +112,16 @@ class KaTrainGui(Screen, KaTrainBase):
def log(self, message, level=OUTPUT_INFO):
super().log(message, level)
if level == OUTPUT_KATAGO_STDERR and "ERROR" not in self.controls.status.text:
if "starting" in message.lower():
if self.contributing:
self.controls.set_status(message, STATUS_INFO)
elif "starting" in message.lower():
self.controls.set_status("KataGo engine starting...", STATUS_INFO)
if message.startswith("Tuning"):
elif message.startswith("Tuning"):
self.controls.set_status(
"KataGo is tuning settings for first startup, please wait." + message, STATUS_INFO
)
return
if "ready" in message.lower():
elif "ready" in message.lower():
self.controls.set_status("KataGo engine ready.", STATUS_INFO)
if (
level == OUTPUT_ERROR
@@ -135,17 +129,22 @@ class KaTrainGui(Screen, KaTrainBase):
) and getattr(self, "controls", None):
self.controls.set_status(f"ERROR: {message}", STATUS_ERROR)
def animate_pondering(self, *_args):
if not self.idle_analysis:
self.board_controls.engine_status_pondering = -1
else:
def handle_animations(self, *_args):
if self.contributing and self.animate_contributing:
self.engine.advance_showing_game()
if (self.contributing and self.animate_contributing) or self.idle_analysis:
self.board_controls.engine_status_pondering += 5
else:
self.board_controls.engine_status_pondering = -1
@property
def play_analyze_mode(self):
return self.play_mode.mode
def toggle_continuous_analysis(self):
if self.contributing:
self.animate_contributing = not self.animate_contributing
else:
if self.idle_analysis:
self.controls.set_status("", STATUS_INFO)
self.idle_analysis = not self.idle_analysis
@@ -167,7 +166,7 @@ class KaTrainGui(Screen, KaTrainBase):
else:
self._do_new_game()
Clock.schedule_interval(self.animate_pondering, 0.1)
Clock.schedule_interval(self.handle_animations, 0.1)
Window.request_keyboard(None, self, "").bind(on_key_down=self._on_keyboard_down, on_key_up=self._on_keyboard_up)
def set_focus_event(*args):
@@ -220,6 +219,7 @@ class KaTrainGui(Screen, KaTrainBase):
if not self.game or not self.game.current_node:
return
cn = self.game.current_node
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:
teaching_undo = cn.player and last_player.being_taught and cn.parent
@@ -269,7 +269,19 @@ class KaTrainGui(Screen, KaTrainBase):
f"Message skipped as it is outdated (current game is {self.game.game_id}", OUTPUT_EXTRA_DEBUG
)
continue
fn = getattr(self, f"_do_{msg.replace('-','_')}")
msg = msg.replace("-", "_")
if self.contributing:
if msg not in [
"katago_contribute",
"redo",
"undo",
"update_state",
"save_game",
"find_mistake",
]:
self.log(i18n._("gui-locked").format(action=msg), OUTPUT_ERROR)
continue
fn = getattr(self, f"_do_{msg}")
fn(*args, **kwargs)
if msg != "update_state":
self._do_update_state()
@@ -280,6 +292,9 @@ class KaTrainGui(Screen, KaTrainBase):
def __call__(self, message, *args, **kwargs):
if self.game:
if message.endswith("popup"): # gui code needs to run in main kivy thread.
if self.contributing and "save" not in message and message != "contribute-popup":
self.log(i18n._("gui-locked").format(action=message), OUTPUT_ERROR)
return
fn = getattr(self, f"_do_{message.replace('-', '_')}")
Clock.schedule_once(lambda _dt: fn(*args, **kwargs), -1)
else: # game related actions
@@ -299,16 +314,30 @@ class KaTrainGui(Screen, KaTrainBase):
analyze_fast=analyze_fast or not move_tree,
sgf_filename=sgf_filename,
)
if move_tree:
for bw, player_info in self.players_info.items():
player_info.sgf_rank = self.game.root.get_property(bw + "R")
player_info.calculated_rank = None
if sgf_filename is not None: # load game->no ai player
player_info.player_type = PLAYER_HUMAN
player_info.player_subtype = PLAYING_NORMAL
player_info.sgf_rank = move_tree.root.get_property(bw + "R")
player_info.calculated_rank = None
self.update_player(bw)
self.update_player(bw, player_type=player_info.player_type, player_subtype=player_info.player_subtype)
self.controls.graph.initialize_from_game(self.game.root)
self.update_state(redraw_board=True)
def _do_katago_contribute(self):
if self.contributing and not self.engine.server_error and self.engine.katago_process is not None:
return
self.contributing = self.animate_contributing = True # special mode
if self.play_analyze_mode == MODE_PLAY: # switch to analysis view
self.play_mode.switch_ui_mode()
self.idle_analysis = False
self.board_gui.animating_pv = None
for bw, player_info in self.players_info.items():
self.update_player(bw, player_type=PLAYER_AI, player_subtype=AI_DEFAULT)
self.engine.shutdown(finish=False)
self.engine = KataGoContributeEngine(self)
self.game = BaseGame(self)
def _do_insert_mode(self, mode="toggle"):
self.game.set_insert_mode(mode)
if self.play_analyze_mode != MODE_ANALYZE:
@@ -331,6 +360,9 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_gui.animating_pv = None
self.game.undo(n_times)
def _do_reset_analysis(self):
self.game.reset_current_analysis()
def _do_resign(self):
self.game.current_node.end_state = f"{self.game.current_node.player}+R"
@@ -360,8 +392,8 @@ class KaTrainGui(Screen, KaTrainBase):
def _do_analyze_extra(self, mode, **kwargs):
self.game.analyze_extra(mode, **kwargs)
def _do_play_to_end(self):
self.game.play_to_end()
def _do_selfplay_setup(self, until_move, target_b_advantage=None):
self.game.selfplay(int(until_move) if isinstance(until_move, float) else until_move, target_b_advantage)
def _do_select_box(self):
self.controls.set_status(i18n._("analysis:region:start"), STATUS_INFO)
@@ -371,7 +403,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True
if not self.new_game_popup:
self.new_game_popup = I18NPopup(
title_key="New Game title", size=[dp(800), dp(800)], content=NewGamePopup(self)
title_key="New Game title", size=[dp(800), dp(900)], content=NewGamePopup(self)
).__self__
self.new_game_popup.content.popup = self.new_game_popup
self.new_game_popup.open()
@@ -404,6 +436,14 @@ class KaTrainGui(Screen, KaTrainBase):
self.config_popup.content.popup = self.config_popup
self.config_popup.open()
def _do_contribute_popup(self):
if not self.contribute_popup:
self.contribute_popup = I18NPopup(
title_key="contribute settings title", size=[dp(1100), dp(800)], content=ContributePopup(self)
).__self__
self.contribute_popup.content.popup = self.contribute_popup
self.contribute_popup.open()
def _do_ai_popup(self):
self.controls.timer.paused = True
if not self.ai_settings_popup:
@@ -414,6 +454,8 @@ class KaTrainGui(Screen, KaTrainBase):
self.ai_settings_popup.open()
def load_sgf_file(self, file, fast=False, rewind=True):
if self.contributing:
return
try:
move_tree = KaTrainSGF.parse_file(file)
except (ParseError, FileNotFoundError) as e:
@@ -425,7 +467,7 @@ class KaTrainGui(Screen, KaTrainBase):
def _do_analyze_sgf_popup(self):
if not self.fileselect_popup:
popup_contents = LoadSGFPopup()
popup_contents = LoadSGFPopup(self)
popup_contents.filesel.path = os.path.abspath(os.path.expanduser(self.config("general/sgf_load", ".")))
self.fileselect_popup = I18NPopup(
title_key="load sgf title", size=[dp(1200), dp(800)], content=popup_contents
@@ -438,6 +480,7 @@ class KaTrainGui(Screen, KaTrainBase):
if path != self.config("general/sgf_load"):
self.log(f"Updating sgf load path default to {path}", OUTPUT_DEBUG)
self._config["general"]["sgf_load"] = path
popup_contents.update_config(False)
self.save_config("general")
self.load_sgf_file(filename, popup_contents.fast.active, popup_contents.rewind.active)
@@ -536,9 +579,10 @@ class KaTrainGui(Screen, KaTrainBase):
"d": ("analyze-extra", "sweep"),
"f": ("analyze-extra", "alternative"),
"g": ("select-box",),
"h": ("reset-analysis",),
"i": ("insert-mode",),
"p": ("play", None),
"l": ("play-to-end",),
"l": ("selfplay-setup", "end", None),
"b": ("undo", "branch"),
"down": ("switch-branch", 1),
"up": ("switch-branch", -1),
@@ -546,6 +590,8 @@ class KaTrainGui(Screen, KaTrainBase):
"f6": ("teacher-popup",),
"f7": ("ai-popup",),
"f8": ("config-popup",),
"f9": ("contribute-popup",),
"escape": ("analyze-extra", "stop"),
}
@property
@@ -562,7 +608,7 @@ class KaTrainGui(Screen, KaTrainBase):
return # when making notes, don't allow keyboard shortcuts
popup = self.popup_open
if popup:
if keycode[1] in ["f5", "f6", "f7", "f8"]: # switch between popups
if keycode[1] in ["f5", "f6", "f7", "f8", "f9"]: # switch between popups
popup.dismiss()
return
elif keycode[1] in ["enter", "numpadenter"]:
@@ -619,13 +665,13 @@ class KaTrainGui(Screen, KaTrainBase):
shortcut.trigger_action(duration=0)
else:
self(*shortcut)
elif keycode[1] == "f9" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
elif keycode[1] == "f10" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
import yappi
yappi.set_clock_type("cpu")
yappi.start()
self.log("starting profiler", OUTPUT_ERROR)
elif keycode[1] == "f10" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
elif keycode[1] == "f11" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
import time
import yappi
@@ -692,6 +738,25 @@ class KaTrainApp(MDApp):
Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file.decode("utf8")))
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)
if not win_size:
window_scale_fac = 1
try:
from screeninfo import get_monitors
for m in get_monitors():
window_scale_fac = min(window_scale_fac, (m.height - 100) / 1000, (m.width - 100) / 1300)
except Exception as e:
window_scale_fac = 0.85
win_size = [1300 * window_scale_fac, 1000 * window_scale_fac]
Window.size = (win_size[0], win_size[1])
if win_left is not None and win_top is not None:
Window.left = win_left
Window.top = win_top
return self.gui
def on_language(self, _instance, language):
@@ -704,7 +769,11 @@ class KaTrainApp(MDApp):
self.gui.controls.set_status("", STATUS_INFO)
def webbrowser(self, site_key):
websites = {"homepage": HOMEPAGE + "#manual", "support": HOMEPAGE + "#support"}
websites = {
"homepage": HOMEPAGE + "#manual",
"support": HOMEPAGE + "#support",
"contribute:signup": "http://katagotraining.org/accounts/signup/",
}
if site_key in websites:
webbrowser.open(websites[site_key])
@@ -712,9 +781,15 @@ class KaTrainApp(MDApp):
self.language = self.gui.config("general/lang")
self.gui.start()
def on_request_close(self, *_args):
def on_request_close(self, *_args, source=None):
if source == "keyboard":
return True # do not close on esc
if getattr(self, "gui", None):
self.gui.play_mode.save_ui_state()
self.gui._config["ui_state"]["size"] = list(Window.size)
self.gui._config["ui_state"]["top"] = Window.top
self.gui._config["ui_state"]["left"] = Window.left
self.gui.save_config("ui_state")
if self.gui.engine:
self.gui.engine.shutdown(finish=None)
+21 -4
View File
@@ -11,13 +11,26 @@
"wide_root_noise": 0.0,
"_enable_ownership": true
},
"contribute": {
"katago": "",
"config": "katrain/KataGo/contribute_config.cfg",
"ownership": false,
"maxgames": 6,
"movespeed": 2,
"username": "",
"password": "",
"savepath": "./dist_sgf/",
"savesgf": false
},
"general": {
"sgf_load": "~/Downloads",
"sgf_save": "./sgfout",
"anim_pv_time": 0.5,
"debug_level": 0,
"lang": "en",
"version": "1.7.1"
"version": "1.8.0",
"load_fast_analysis": false,
"load_sgf_rewind": true
},
"timer": {
"byo_length": 30,
@@ -31,7 +44,9 @@
"komi": 6.5,
"handicap": 0,
"rules": "japanese",
"clear_cache": false
"clear_cache": false,
"setup_move":100,
"setup_advantage":20
},
"trainer": {
"theme": "theme:normal",
@@ -68,8 +83,9 @@
true
],
"save_analysis": false,
"save_marks": false,
"low_visits": 25,
"eval_off_show_last": 3,
"eval_on_show_last": 3,
"top_moves_show": "top_move_delta_score",
"top_moves_show_secondary": "top_move_visits",
"eval_show_ai": true,
@@ -143,6 +159,7 @@
}
},
"ui_state": {
"size": [],
"play": {
"analysis_controls": {
"show_children": true,
@@ -180,7 +197,7 @@
"analyze": {
"analysis_controls": {
"show_children": true,
"eval": false,
"eval": true,
"hints": true,
"policy": false,
"ownership": true
+1 -7
View File
@@ -35,7 +35,7 @@ from katrain.core.constants import (
OUTPUT_INFO,
)
from katrain.core.game import Game, GameNode, Move
from katrain.core.utils import var_to_grid
from katrain.core.utils import var_to_grid, weighted_selection_without_replacement
def interp_ix(lst, x):
@@ -91,12 +91,6 @@ def ai_rank_estimation(strategy, settings) -> int:
return AI_STRENGTH[strategy]
def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]:
"""For a list of tuples where the second element is a weight, returns random items with those weights, without replacement."""
elt = [(math.log(random.random()) / (item[1] + 1e-18), item) for item in items] # magic
return [e[1] for e in heapq.nlargest(pick_n, elt)] # NB fine if too small
def dirichlet_noise(num, dir_alpha=0.3):
sample = [random.gammavariate(dir_alpha, 1) for _ in range(num)]
sum_sample = sum(sample)
+2
View File
@@ -154,6 +154,8 @@ class KaTrainBase:
if player_info.player_type == PLAYER_AI:
settings = self.config(f"ai/{player_info.strategy}")
player_info.calculated_rank = ai_rank_estimation(player_info.player_subtype, settings)
else:
player_info.calculated_rank = None
def reset_players(self):
self.update_player("B")
+9 -2
View File
@@ -1,7 +1,7 @@
PROGRAM_NAME = "KaTrain"
VERSION = "1.7.6"
VERSION = "1.8.0"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version
CONFIG_MIN_VERSION = "1.8.0" # keep config files from this version
ANALYSIS_FORMAT_VERSION = "1.0"
DATA_FOLDER = "~/.katrain"
@@ -230,8 +230,12 @@ TOP_MOVE_SCORE = "top_move_score"
TOP_MOVE_DELTA_WINRATE = "top_move_delta_winrate"
TOP_MOVE_WINRATE = "top_move_winrate"
TOP_MOVE_VISITS = "top_move_visits"
# TOP_MOVE_UTILITY = "top_move_utility"
# TOP_MOVE_UTILITYLCB = "top_move_utiltiy_lcb"
# TOP_MOVE_SCORE_STDDEV = "top_move_score_stddev"
TOP_MOVE_NOTHING = "top_move_nothing"
TOP_MOVE_OPTIONS = [
TOP_MOVE_SCORE,
TOP_MOVE_DELTA_SCORE,
@@ -239,6 +243,9 @@ TOP_MOVE_OPTIONS = [
TOP_MOVE_DELTA_WINRATE,
TOP_MOVE_VISITS,
TOP_MOVE_NOTHING,
# TOP_MOVE_SCORE_STDDEV,
# TOP_MOVE_UTILITY,
# TOP_MOVE_UTILITYLCB
]
REPORT_DT = 0.25
+268
View File
@@ -0,0 +1,268 @@
import json
import os
import random
import shlex
import subprocess
import threading
import time
import traceback
from collections import defaultdict
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_KATAGO_STDERR
from katrain.core.engine import EngineDiedException, BaseEngine
from katrain.core.game import BaseGame
from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move
from katrain.core.utils import find_package_resource
class KataGoContributeEngine(BaseEngine):
"""Starts and communicates with the KataGo contribute program"""
DEFAULT_MAX_GAMES = 8
SHOW_RESULT_TIME = 5
GIVE_UP_AFTER = 120
def __init__(self, katrain):
super().__init__(katrain, katrain.config("contribute"))
self.katrain = katrain
base_dir = os.path.expanduser("~/.katrain/katago_contribute")
self.katago_process = None
self.stdout_thread = None
self.stderr_thread = None
self.shell = False
self.active_games = {}
self.finished_games = set()
self.showing_game = None
self.last_advance = 0
self.move_count = 0
self.uploaded_games_count = 0
self.last_move_for_game = defaultdict(int)
self.visits_count = 0
self.start_time = 0
self.server_error = None
self.save_sgf = self.config.get("savesgf", False)
self.save_path = self.config.get("savepath", "./dist_sgf/")
self.move_speed = self.config.get("movespeed", 2.0)
exe = self.get_engine_path(self.config.get("katago"))
cfg = find_package_resource(self.config.get("config"))
settings_dict = {
"username": self.config.get("username"),
"password": self.config.get("password"),
"maxSimultaneousGames": self.config.get("maxgames") or self.DEFAULT_MAX_GAMES,
"includeOwnership": self.config.get("ownership") or False,
"logGamesAsJson": True,
}
self.max_buffer_games = 2 * settings_dict["maxSimultaneousGames"]
settings = {f"{k}={v}" for k, v in settings_dict.items()}
self.command = shlex.split(
f'"{exe}" contribute -config "{cfg}" -base-dir "{base_dir}" -override-config "{",".join(settings)}"'
)
self.start()
@staticmethod
def game_ended(game):
cn = game.current_node
if cn.is_pass and cn.analysis_exists:
moves = cn.candidate_moves
if moves and moves[0]["move"] == "pass":
game.play(Move(None, player=game.current_node.next_player)) # play pass
return game.end_result
def advance_showing_game(self):
current_game = self.active_games.get(self.showing_game)
if current_game:
end_result = self.game_ended(current_game)
if end_result is not None:
self.finished_games.add(self.showing_game)
if time.time() - self.last_advance > self.SHOW_RESULT_TIME:
del self.active_games[self.showing_game]
if self.save_sgf:
filename = os.path.join(self.save_path, f"{self.showing_game}.sgf")
self.katrain.log(current_game.write_sgf(filename, self.katrain.config("trainer")), OUTPUT_INFO)
self.katrain.log(f"Game {self.showing_game} finished, finding a new one", OUTPUT_INFO)
self.showing_game = None
elif time.time() - self.last_advance > self.move_speed or len(self.active_games) > self.max_buffer_games:
if current_game.current_node.children:
current_game.redo(1)
self.last_advance = time.time()
self.katrain("update-state")
elif time.time() - self.last_advance > self.GIVE_UP_AFTER:
self.katrain.log(
f"Giving up on game {self.showing_game} which appears stuck, finding a new one", OUTPUT_INFO
)
self.showing_game = None
else:
if self.active_games:
self.showing_game = None
best_count = -1
for game_id, game in self.active_games.items(): # find game with most moves left to show
count = 0
node = game.current_node
while node.children:
node = node.children[0]
count += 1
if count > best_count:
best_count = count
self.showing_game = game_id
self.last_advance = time.time()
self.katrain.log(f"Showing game {self.showing_game}, {best_count} moves left to show.", OUTPUT_INFO)
self.katrain.game = self.active_games[self.showing_game]
self.katrain("update-state", redraw_board=True)
def status(self):
return f"Contributing to distributed training\nGames: {self.uploaded_games_count} uploaded, {len(self.active_games)} in buffer, {len(self.finished_games)} shown\n{self.move_count} moves played ({60*self.move_count/(time.time()-self.start_time):.1f}/min, {self.visits_count / (time.time() - self.start_time):.1f} visits/s)\n"
def is_idle(self):
return False
def queries_remaining(self):
return 1
def start(self):
try:
self.katrain.log(f"Starting Distributed KataGo with {self.command}", OUTPUT_INFO)
startupinfo = None
if hasattr(subprocess, "STARTUPINFO"):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller
self.katago_process = subprocess.Popen(
self.command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
shell=self.shell,
)
except (FileNotFoundError, PermissionError, OSError) as e:
self.katrain.log(
i18n._("Starting Kata failed").format(command=self.command, error=e),
OUTPUT_ERROR,
)
return # don't start
self.stdout_thread = threading.Thread(target=self._read_stdout_thread, daemon=True)
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
self.stdout_thread.start()
self.stderr_thread.start()
def check_alive(self, os_error="", exception_if_dead=False):
ok = self.katago_process and self.katago_process.poll() is None
if not ok and exception_if_dead:
if self.katago_process:
code = self.katago_process and self.katago_process.poll()
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 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):
process = self.katago_process
if process:
self.katago_process = None
process.terminate()
if finish is not None:
for t in [self.stderr_thread, self.stdout_thread]:
if t:
t.join()
def _read_stderr_thread(self):
while self.katago_process is not None:
try:
line = self.katago_process.stderr.readline()
if line:
try:
message = line.decode(errors="ignore").strip()
if any(
s in message
for s in ["not status code 200 OK", "Server returned error", "Uncaught exception:"]
):
message = message.replace("what():", "").replace("Uncaught exception:", "").strip()
self.server_error = message # don't be surprised by engine dying
self.katrain.log(message, OUTPUT_ERROR)
return
else:
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)
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
return
def _read_stdout_thread(self):
while self.katago_process is not None:
try:
line = self.katago_process.stdout.readline()
if line:
line = line.decode(errors="ignore").strip()
if line.startswith("{"):
try:
analysis = json.loads(line)
if "gameId" in analysis:
game_id = analysis["gameId"]
if game_id in self.finished_games:
continue
current_game = self.active_games.get(game_id)
new_game = current_game is None
if new_game:
board_size = [analysis["boardXSize"], analysis["boardYSize"]]
placements = {
f"A{bw}": [
Move.from_gtp(move, pl).sgf(board_size)
for pl, move in analysis["initialStones"]
if pl == bw
]
for bw in "BW"
}
game_properties = {k: v for k, v in placements.items() if v}
game_properties["SZ"] = f"{board_size[0]}:{board_size[1]}"
game_properties["KM"] = analysis["rules"]["komi"]
game_properties["RU"] = json.dumps(analysis["rules"])
game_properties["PB"] = analysis["blackPlayer"]
game_properties["PW"] = analysis["whitePlayer"]
current_game = BaseGame(self.katrain, game_properties=game_properties)
self.active_games[game_id] = current_game
last_node = current_game.sync_branch(
[Move.from_gtp(coord, pl) for pl, coord in analysis["moves"]]
)
last_node.set_analysis(analysis)
if new_game:
current_game.set_current_node(last_node)
self.start_time = self.start_time or time.time() - 1
self.move_count += 1
self.visits_count += analysis["rootInfo"]["visits"]
last_move = self.last_move_for_game[game_id]
self.last_move_for_game[game_id] = time.time()
dt = self.last_move_for_game[game_id] - last_move if last_move else 0
self.katrain.log(
f"[{time.time()-self.start_time:.1f}] Game {game_id} Move {analysis['turnNumber']}: {' '.join(analysis['move'])} Visits {analysis['rootInfo']['visits']} Time {dt:.1f}s\t Moves/min {60*self.move_count/(time.time()-self.start_time):.1f} Visits/s {self.visits_count/(time.time()-self.start_time):.1f}",
OUTPUT_DEBUG,
)
self.katrain("update-state")
except Exception as e:
traceback.print_exc()
self.katrain.log(f"Exception {e} in parsing or processing JSON: {line}", OUTPUT_ERROR)
elif "uploaded sgf" in line:
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
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}", OUTPUT_DEBUG)
return
+91 -48
View File
@@ -24,8 +24,7 @@ class EngineDiedException(Exception):
pass
class KataGoEngine:
"""Starts and communicates with the KataGO analysis engine"""
class BaseEngine: # some common elements between analysis and contribute engine
# TODO: we don't support suicide in game.py, so no "tt": "tromp-taylor", "nz": "new-zealand"
RULESETS_ABBR = [
@@ -37,14 +36,62 @@ class KataGoEngine:
]
RULESETS = {fromkey: name for abbr, name in RULESETS_ABBR for fromkey in [abbr, name]}
@staticmethod
def get_rules(node):
return KataGoEngine.RULESETS.get(str(node.ruleset).lower(), "japanese")
def __init__(self, katrain, config):
self.katrain = katrain
self.queries = {} # outstanding query id -> start time and callback
self.config = config
@staticmethod
def get_rules(node):
ruleset = node.ruleset
if ruleset.strip().startswith("{"):
try:
ruleset = json.loads(ruleset)
except:
pass
if isinstance(ruleset, dict):
return ruleset
return KataGoEngine.RULESETS.get(str(ruleset).lower(), "japanese")
def advance_showing_game(self):
pass # avoid transitional error
def status(self):
return "" # avoid transitional error
def get_engine_path(self, exe):
if not exe:
if kivy_platform == "win":
exe = "katrain/KataGo/katago.exe"
elif kivy_platform == "linux":
exe = "katrain/KataGo/katago"
else:
exe = find_package_resource("katrain/KataGo/katago-osx") # github actions built
if not os.path.isfile(exe) or "arm64" in platform.version().lower():
exe = "katago" # e.g. MacOS after brewing
if exe.startswith("katrain"):
exe = find_package_resource(exe)
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)
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)
return None
exe = exe_with_paths[0]
return exe
class KataGoEngine(BaseEngine):
"""Starts and communicates with the KataGO analysis engine"""
def __init__(self, katrain, config):
super().__init__(katrain, config)
self.queries = {} # outstanding query id -> start time and callback
self.query_counter = 0
self.katago_process = None
self.base_priority = 0
@@ -60,36 +107,15 @@ class KataGoEngine:
self.command = config["altcommand"]
self.shell = True
else:
if not exe:
if kivy_platform == "win":
exe = "katrain/KataGo/katago.exe"
elif kivy_platform == "linux":
exe = "katrain/KataGo/katago"
else:
exe = find_package_resource("katrain/KataGo/katago-osx") # github actions built
if not os.path.isfile(exe) or "arm64" in platform.version().lower():
exe = "katago" # e.g. MacOS after brewing
model = find_package_resource(config["model"])
cfg = find_package_resource(config["config"])
if exe.startswith("katrain"):
exe = find_package_resource(exe)
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)
return # don't start
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)
return # don't start
exe = exe_with_paths[0]
elif not os.path.isfile(model):
exe = self.get_engine_path(config["katago"])
if not exe:
return
if not os.path.isfile(model):
self.katrain.log(i18n._("Kata model not found").format(model=model), OUTPUT_ERROR)
return # don't start
elif not os.path.isfile(cfg):
if not os.path.isfile(cfg):
self.katrain.log(i18n._("Kata config not found").format(config=cfg), OUTPUT_ERROR)
return # don't start
self.command = shlex.split(
@@ -98,6 +124,7 @@ class KataGoEngine:
self.start()
def start(self):
with self.thread_lock:
self.write_queue = queue.Queue()
try:
@@ -131,10 +158,22 @@ class KataGoEngine:
self.base_priority += 1
if not self.is_idle():
with self.thread_lock:
for query_id in list(self.queries.keys()):
self.terminate_query(query_id)
self.queries = {}
self.write_queue = queue.Queue()
self.terminate_queries(only_for_node=None, lock=False)
self.queries = {}
def terminate_queries(self, only_for_node=None, lock=True):
if lock:
with self.thread_lock:
return self.terminate_queries(only_for_node=only_for_node, lock=False)
for query_id, (_, _, _, _, node) in list(self.queries.items()):
if only_for_node is None or only_for_node is node:
self.terminate_query(query_id)
def terminate_query(self, query_id):
if query_id is not None:
self.send_query({"action": "terminate", "terminateId": query_id}, None, None)
self.queries.pop(query_id, None)
def restart(self):
self.queries = {}
@@ -220,9 +259,11 @@ class KataGoEngine:
continue
query_id = analysis["id"]
if query_id not in self.queries:
self.katrain.log(f"Query result {query_id} discarded -- recent new game?", OUTPUT_DEBUG)
self.katrain.log(
f"Query result {query_id} discarded -- recent new game or node reset?", OUTPUT_DEBUG
)
continue
callback, error_callback, start_time, next_move = self.queries[query_id]
callback, error_callback, start_time, next_move, _ = self.queries[query_id]
if "error" in analysis:
del self.queries[query_id]
if error_callback:
@@ -249,6 +290,7 @@ class KataGoEngine:
callback(analysis, partial_result)
except Exception as e:
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR)
traceback.print_exc()
if getattr(self.katrain, "update_state", None): # easier mocking etc
self.katrain.update_state()
except Exception as e:
@@ -258,14 +300,15 @@ class KataGoEngine:
def _write_stdin_thread(self): # flush only in a thread since it returns only when the other program reads
while self.katago_process is not None:
try:
query, callback, error_callback, next_move = self.write_queue.get(block=True, timeout=0.1)
query, callback, error_callback, next_move, node = self.write_queue.get(block=True, timeout=0.1)
except queue.Empty:
continue
with self.thread_lock:
if "id" not in query:
self.query_counter += 1
query["id"] = f"QUERY:{str(self.query_counter)}"
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
if query.get("action") != "terminate":
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move, node)
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
try:
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
@@ -273,12 +316,8 @@ class KataGoEngine:
except OSError as e:
self.check_alive(os_error=str(e), exception_if_dead=False)
def send_query(self, query, callback, error_callback, next_move=None):
self.write_queue.put((query, callback, error_callback, next_move))
def terminate_query(self, query_id):
if query_id is not None:
self.send_query({"action": "terminate", "terminateId": query_id}, None, None)
def send_query(self, query, callback, error_callback, next_move=None, node=None):
self.write_queue.put((query, callback, error_callback, next_move, node))
def request_analysis(
self,
@@ -299,6 +338,11 @@ class KataGoEngine:
nodes = analysis_node.nodes_from_root
moves = [m for node in nodes for m in node.moves]
initial_stones = [m for node in nodes for m in node.placements]
clear_placements = [m for node in nodes for m in node.clear_placements]
if clear_placements: # TODO: support these
self.katrain.log(f"Not analyzing node {analysis_node} as there are AE commands in the path", OUTPUT_DEBUG)
return
if next_move:
moves.append(next_move)
if ownership is None:
@@ -341,7 +385,6 @@ class KataGoEngine:
settings["maxTime"] = self.config["max_time"]
if self.config.get("wide_root_noise", 0.0) > 0.0: # don't send if 0.0, so older versions don't error
settings["wideRootNoise"] = self.config["wide_root_noise"]
query = {
"rules": self.get_rules(analysis_node),
"priority": self.base_priority + priority,
@@ -362,5 +405,5 @@ class KataGoEngine:
query["reportDuringSearchEvery"] = report_every
if avoid:
query["avoidMoves"] = avoid
self.send_query(query, callback, error_callback, next_move)
self.send_query(query, callback, error_callback, next_move, analysis_node)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits)
+390 -266
View File
@@ -1,6 +1,7 @@
import copy
import math
import os
import random
import re
import threading
from datetime import datetime
@@ -9,8 +10,8 @@ from typing import Dict, List, Optional, Union
from kivy.clock import Clock
from katrain.core.constants import (
ANALYSIS_FORMAT_VERSION,
OUTPUT_DEBUG,
OUTPUT_EXTRA_DEBUG,
OUTPUT_INFO,
PLAYER_AI,
PLAYER_HUMAN,
@@ -25,7 +26,7 @@ from katrain.core.engine import KataGoEngine
from katrain.core.game_node import GameNode
from katrain.core.lang import i18n, rank_label
from katrain.core.sgf_parser import SGF, Move
from katrain.core.utils import var_to_grid
from katrain.core.utils import var_to_grid, weighted_selection_without_replacement
class IllegalMoveException(Exception):
@@ -36,7 +37,7 @@ class KaTrainSGF(SGF):
_NODE_CLASS = GameNode
class Game:
class BaseGame:
"""Represents a game of go, including an implementation of capture rules."""
DEFAULT_PROPERTIES = {"GM": 1, "FF": 4}
@@ -44,24 +45,16 @@ class Game:
def __init__(
self,
katrain,
engine: Union[Dict, KataGoEngine],
move_tree: GameNode = None,
analyze_fast=False,
game_properties: Optional[Dict] = None,
sgf_filename=None,
):
self.katrain = katrain
self._lock = threading.Lock()
if not isinstance(engine, Dict):
engine = {"B": engine, "W": engine}
self.engines = engine
self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S")
self.sgf_filename = sgf_filename
self.insert_mode = False
self.insert_after = None
self.region_of_interest = None
self.external_game = False # not generated by katrain at some point
if move_tree:
self.root = move_tree
@@ -112,34 +105,31 @@ class Game:
if shortcut_id and shortcut_id in shortcut_id_to_node:
shortcut_id_to_node[shortcut_id].add_shortcut(node)
threading.Thread(
target=lambda: self.analyze_all_nodes(-1_000_000, 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):
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():
node.clear_analysis()
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
# -- move tree functions --
def _calculate_groups(self):
def _init_state(self):
board_size_x, board_size_y = self.board_size
with self._lock:
self.board = [
[-1 for _x in range(board_size_x)] for _y in range(board_size_y)
] # type: List[List[int]] # board pos -> chain id
self.chains = [] # type: List[List[Move]] # chain id -> chain
self.prisoners = [] # type: List[Move]
self.last_capture = [] # type: List[Move]
def _calculate_groups(self):
with self._lock:
self._init_state()
try:
for node in self.current_node.nodes_from_root:
for m in node.move_with_placements:
self._validate_move_and_update_chains(
m, True
) # ignore ko since we didn't know if it was forced
if node.clear_placements: # handle AE by playing all moves left from empty board
clear_coords = {c.coords for c in node.clear_placements}
stones = [m for c in self.chains for m in c if m.coords not in clear_coords]
self._init_state()
for m in stones:
self._validate_move_and_update_chains(m, True)
except IllegalMoveException as e:
raise Exception(f"Unexpected illegal move ({str(e)})")
@@ -192,6 +182,293 @@ class Game:
if -1 not in neighbours(self.chains[this_chain]): # TODO: NZ rules?
raise IllegalMoveException("Suicide")
# Play a Move from the current position, raise IllegalMoveException if invalid.
def play(self, move: Move, ignore_ko: bool = False):
board_size_x, board_size_y = self.board_size
if not move.is_pass and not (0 <= move.coords[0] < board_size_x and 0 <= move.coords[1] < board_size_y):
raise IllegalMoveException(f"Move {move} outside of board coordinates")
try:
self._validate_move_and_update_chains(move, ignore_ko)
except IllegalMoveException:
self._calculate_groups()
raise
with self._lock:
played_node = self.current_node.play(move)
self.current_node = played_node
return played_node
# Insert a list of moves from root, often just adding one.
def sync_branch(self, moves: List[Move]):
node = self.root
with self._lock:
for move in moves:
node = node.play(move)
return node
def set_current_node(self, node):
self.current_node = node
self._calculate_groups()
def undo(self, n_times=1, stop_on_mistake=None):
break_on_branch = False
cn = self.current_node # avoid race conditions
break_on_main_branch = False
last_branching_node = cn
if n_times == "branch":
n_times = 9999
break_on_branch = True
elif n_times == "main-branch":
n_times = 9999
break_on_main_branch = True
for move in range(n_times):
if (
stop_on_mistake is not None
and cn.points_lost is not None
and cn.points_lost >= stop_on_mistake
and self.katrain.players_info[cn.player].player_type != PLAYER_AI
):
self.set_current_node(cn.parent)
return
previous_cn = cn
if cn.shortcut_from:
cn = cn.shortcut_from
elif not cn.is_root:
cn = cn.parent
else:
break # root
if break_on_branch and len(cn.children) > 1:
break
elif break_on_main_branch and cn.ordered_children[0] != previous_cn: # implies > 1 child
last_branching_node = cn
if break_on_main_branch:
cn = last_branching_node
if cn is not self.current_node:
self.set_current_node(cn)
def redo(self, n_times=1, stop_on_mistake=None):
cn = self.current_node # avoid race conditions
for move in range(n_times):
if cn.children:
child = cn.ordered_children[0]
shortcut_to = [m for m, v in cn.shortcuts_to if child == v] # are we about to go to a shortcut node?
if shortcut_to:
child = shortcut_to[0]
cn = child
if (
move > 0
and stop_on_mistake is not None
and cn.points_lost is not None
and cn.points_lost >= stop_on_mistake
and self.katrain.players_info[cn.player].player_type != PLAYER_AI
):
self.set_current_node(cn.parent)
return
if stop_on_mistake is None:
self.set_current_node(cn)
@property
def board_size(self):
return self.root.board_size
@property
def stones(self):
with self._lock:
return sum(self.chains, [])
@property
def end_result(self):
if self.current_node.end_state:
return self.current_node.end_state
if self.current_node.parent and self.current_node.is_pass and self.current_node.parent.is_pass:
return self.manual_score or i18n._("board-game-end")
@property
def prisoner_count(
self,
) -> Dict: # returns prisoners that are of a certain colour as {B: black stones captures, W: white stones captures}
return {player: sum([m.player == player for m in self.prisoners]) for player in Move.PLAYERS}
@property
def rules(self):
return self.root.ruleset
@property
def manual_score(self):
rules = self.rules
if (
not self.current_node.ownership
or str(rules).lower() not in ["jp", "japanese"]
or not self.current_node.parent
or not self.current_node.parent.ownership
):
if not self.current_node.score:
return None
return self.current_node.format_score(round(2 * self.current_node.score) / 2) + "?"
board_size_x, board_size_y = self.board_size
mean_ownership = [(c + p) / 2 for c, p in zip(self.current_node.ownership, self.current_node.parent.ownership)]
ownership_grid = var_to_grid(mean_ownership, (board_size_x, board_size_y))
stones = {m.coords: m.player for m in self.stones}
lo_threshold = 0.15
hi_threshold = 0.85
max_unknown = 10
max_dame = 4 * (board_size_x + board_size_y)
def japanese_score_square(square, owner):
player = stones.get(square, None)
if (
(player == "B" and owner > hi_threshold)
or (player == "W" and owner < -hi_threshold)
or abs(owner) < lo_threshold
):
return 0 # dame or own stones
if player is None and abs(owner) >= hi_threshold:
return round(owner) # surrounded empty intersection
if (player == "B" and owner < -hi_threshold) or (player == "W" and owner > hi_threshold):
return 2 * round(owner) # captured stone
return math.nan # unknown!
scored_squares = [
japanese_score_square((x, y), ownership_grid[y][x])
for y in range(board_size_y)
for x in range(board_size_x)
]
num_sq = {t: sum([s == t for s in scored_squares]) for t in [-2, -1, 0, 1, 2]}
num_unkn = sum(math.isnan(s) for s in scored_squares)
prisoners = self.prisoner_count
score = sum([t * n for t, n in num_sq.items()]) + prisoners["W"] - prisoners["B"] - self.komi
self.katrain.log(
f"Manual Scoring: {num_sq} score by square with {num_unkn} unknown, {prisoners} captures, and {self.komi} komi -> score = {score}",
OUTPUT_DEBUG,
)
if num_unkn > max_unknown or (num_sq[0] - len(stones)) > max_dame:
return None
return self.current_node.format_score(score)
def __repr__(self):
return (
"\n".join("".join(self.chains[c][0].player if c >= 0 else "-" for c in line) for line in self.board)
+ f"\ncaptures: {self.prisoner_count}"
)
def update_root_properties(self):
def player_name(player_info):
if player_info.name and player_info.player_type == PLAYER_HUMAN:
return player_info.name
else:
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)}){SGF_INTERNAL_COMMENTS_MARKER}"
root_properties = self.root.properties
x_properties = {}
for bw in "BW":
if not self.external_game:
x_properties["P" + bw] = player_name(self.katrain.players_info[bw])
player_info = self.katrain.players_info[bw]
if player_info.player_type == PLAYER_AI:
x_properties[bw + "R"] = rank_label(player_info.calculated_rank)
if "+" in str(self.end_result):
x_properties["RE"] = self.end_result
self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}}
def generate_filename(self):
self.update_root_properties()
player_names = {
bw: re.sub(r"[\u200b\u3164'<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"
}
base_game_name = f"{PROGRAM_NAME}_{player_names['B']} vs {player_names['W']}"
return f"{base_game_name} {self.game_id}.sgf"
def write_sgf(self, filename: str, trainer_config: Optional[Dict] = None):
if trainer_config is None:
trainer_config = self.katrain.config("trainer", {})
save_feedback = trainer_config.get("save_feedback", False)
eval_thresholds = trainer_config["eval_thresholds"]
save_analysis = trainer_config.get("save_analysis", False)
save_marks = trainer_config.get("save_marks", False)
self.update_root_properties()
show_dots_for = {
bw: trainer_config.get("eval_show_ai", True) or self.katrain.players_info[bw].human for bw in "BW"
}
sgf = self.root.sgf(
save_comments_player=show_dots_for,
save_comments_class=save_feedback,
eval_thresholds=eval_thresholds,
save_analysis=save_analysis,
save_marks=save_marks,
)
self.sgf_filename = filename
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w", encoding="utf-8") as f:
f.write(sgf)
return i18n._("sgf written").format(file_name=filename)
class Game(BaseGame):
"""Extensions related to analysis etc."""
def __init__(
self,
katrain,
engine: Union[Dict, KataGoEngine],
move_tree: GameNode = None,
analyze_fast=False,
game_properties: Optional[Dict] = None,
sgf_filename=None,
):
super().__init__(
katrain=katrain, move_tree=move_tree, game_properties=game_properties, sgf_filename=sgf_filename
)
if not isinstance(engine, Dict):
engine = {"B": engine, "W": engine}
self.engines = engine
self.insert_mode = False
self.insert_after = None
self.region_of_interest = None
threading.Thread(
target=lambda: self.analyze_all_nodes(-1_000_000, 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):
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():
node.clear_analysis()
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
@property
def rules(self): # maybe not needed
return self.engines["B"].get_rules(self.root)
def set_current_node(self, node):
if self.insert_mode:
self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR)
return
super().set_current_node(node)
def undo(self, n_times=1, stop_on_mistake=None):
if self.insert_mode: # in insert mode, undo = delete
cn = self.current_node # avoid race conditions
if n_times == 1 and cn not in self.insert_after.nodes_from_root:
cn.parent.children = [c for c in cn.parent.children if c != cn]
self.current_node = cn.parent
self._calculate_groups()
return
super().undo(n_times=n_times, stop_on_mistake=stop_on_mistake)
def reset_current_analysis(self):
cn = self.current_node
engine = self.engines[cn.next_player]
engine.terminate_queries(cn)
cn.clear_analysis()
cn.analyze(engine)
def redo(self, n_times=1, stop_on_mistake=None):
if self.insert_mode:
return
super().redo(n_times=n_times, stop_on_mistake=stop_on_mistake)
def set_insert_mode(self, mode):
if mode == "toggle":
mode = not self.insert_mode
@@ -242,17 +519,7 @@ class Game:
# Play a Move from the current position, raise IllegalMoveException if invalid.
def play(self, move: Move, ignore_ko: bool = False, analyze=True):
board_size_x, board_size_y = self.board_size
if not move.is_pass and not (0 <= move.coords[0] < board_size_x and 0 <= move.coords[1] < board_size_y):
raise IllegalMoveException(f"Move {move} outside of board coordinates")
try:
self._validate_move_and_update_chains(move, ignore_ko)
except IllegalMoveException:
self._calculate_groups()
raise
with self._lock:
played_node = self.current_node.play(move)
self.current_node = played_node
played_node = super().play(move, ignore_ko)
if analyze:
if self.region_of_interest:
played_node.analyze(self.engines[played_node.next_player], analyze_fast=True)
@@ -261,214 +528,6 @@ class Game:
played_node.analyze(self.engines[played_node.next_player])
return played_node
def set_current_node(self, node):
if self.insert_mode:
self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR)
return
self.current_node = node
self._calculate_groups()
def undo(self, n_times=1, stop_on_mistake=None):
# allow undo/delete only in insert mode
cn = self.current_node # avoid race conditions
if self.insert_mode: # in insert mode, undo = delete
if n_times == 1 and cn not in self.insert_after.nodes_from_root:
cn.parent.children = [c for c in cn.parent.children if c != cn]
self.current_node = cn.parent
self._calculate_groups()
return
break_on_branch = False
break_on_main_branch = False
last_branching_node = cn
if n_times == "branch":
n_times = 9999
break_on_branch = True
elif n_times == "main-branch":
n_times = 9999
break_on_main_branch = True
for move in range(n_times):
if (
stop_on_mistake is not None
and cn.points_lost is not None
and cn.points_lost >= stop_on_mistake
and self.katrain.players_info[cn.player].player_type != PLAYER_AI
):
self.set_current_node(cn.parent)
return
previous_cn = cn
if cn.shortcut_from:
cn = cn.shortcut_from
elif not cn.is_root:
cn = cn.parent
else:
break # root
if break_on_branch and len(cn.children) > 1:
break
elif break_on_main_branch and cn.ordered_children[0] != previous_cn: # implies > 1 child
last_branching_node = cn
if break_on_main_branch:
cn = last_branching_node
if cn is not self.current_node:
self.set_current_node(cn)
def redo(self, n_times=1, stop_on_mistake=None):
if self.insert_mode:
return
cn = self.current_node # avoid race conditions
for move in range(n_times):
if cn.children:
child = cn.ordered_children[0]
shortcut_to = [m for m, v in cn.shortcuts_to if child == v] # are we about to go to a shortcut node?
if shortcut_to:
child = shortcut_to[0]
cn = child
if (
move > 0
and stop_on_mistake is not None
and cn.points_lost is not None
and cn.points_lost >= stop_on_mistake
and self.katrain.players_info[cn.player].player_type != PLAYER_AI
):
self.set_current_node(cn.parent)
return
if stop_on_mistake is None:
self.set_current_node(cn)
def cycle_children(self, direction):
cn = self.current_node # avoid race conditions
if cn.parent and len(cn.parent.children) > 1:
ordered_children = cn.parent.ordered_children
ix = (ordered_children.index(cn) + len(ordered_children) + direction) % len(ordered_children)
self.set_current_node(ordered_children[ix])
@property
def board_size(self):
return self.root.board_size
@property
def stones(self):
with self._lock:
return sum(self.chains, [])
@property
def end_result(self):
if self.current_node.end_state:
return self.current_node.end_state
if self.current_node.parent and self.current_node.is_pass and self.current_node.parent.is_pass:
return self.manual_score or i18n._("board-game-end")
@property
def prisoner_count(
self,
) -> Dict: # returns prisoners that are of a certain colour as {B: black stones captures, W: white stones captures}
return {player: sum([m.player == player for m in self.prisoners]) for player in Move.PLAYERS}
@property
def manual_score(self):
rules = self.engines["B"].get_rules(self.root)
if not self.current_node.ownership or rules != "japanese":
if not self.current_node.score:
return None
self.katrain.log(
f"rules '{rules}' are not japanese, or no ownership available ({not self.current_node.ownership}) -> no manual score available",
OUTPUT_DEBUG,
)
return self.current_node.format_score(round(2 * self.current_node.score) / 2) + "?"
board_size_x, board_size_y = self.board_size
ownership_grid = var_to_grid(self.current_node.ownership, (board_size_x, board_size_y))
stones = {m.coords: m.player for m in self.stones}
lo_threshold = 0.15
hi_threshold = 0.85
max_unknown = 10
max_dame = 4 * (board_size_x + board_size_y)
def japanese_score_square(square, owner):
player = stones.get(square, None)
if (
(player == "B" and owner > hi_threshold)
or (player == "W" and owner < -hi_threshold)
or abs(owner) < lo_threshold
):
return 0 # dame or own stones
if player is None and abs(owner) >= hi_threshold:
return round(owner) # surrounded empty intersection
if (player == "B" and owner < -hi_threshold) or (player == "W" and owner > hi_threshold):
return 2 * round(owner) # captured stone
return math.nan # unknown!
scored_squares = [
japanese_score_square((x, y), ownership_grid[y][x])
for y in range(board_size_y)
for x in range(board_size_x)
]
num_sq = {t: sum([s == t for s in scored_squares]) for t in [-2, -1, 0, 1, 2]}
num_unkn = sum(math.isnan(s) for s in scored_squares)
prisoners = self.prisoner_count
score = sum([t * n for t, n in num_sq.items()]) + prisoners["W"] - prisoners["B"] - self.komi
self.katrain.log(
f"Manual Scoring: {num_sq} score by square with {num_unkn} unknown, {prisoners} captures, and {self.komi} komi -> score = {score}",
OUTPUT_DEBUG,
)
if num_unkn > max_unknown or (num_sq[0] - len(stones)) > max_dame:
return None
return self.current_node.format_score(score)
def __repr__(self):
return (
"\n".join("".join(self.chains[c][0].player if c >= 0 else "-" for c in line) for line in self.board)
+ f"\ncaptures: {self.prisoner_count}"
)
def update_root_properties(self):
def player_name(player_info):
if player_info.name and player_info.player_type == PLAYER_HUMAN:
return player_info.name
else:
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})"
root_properties = self.root.properties
x_properties = {}
for bw in "BW":
if not self.external_game:
x_properties["P" + bw] = player_name(self.katrain.players_info[bw]) + SGF_INTERNAL_COMMENTS_MARKER
player_info = self.katrain.players_info[bw]
if player_info.player_type == PLAYER_AI:
x_properties[bw + "R"] = rank_label(player_info.calculated_rank)
if "+" in str(self.end_result):
x_properties["RE"] = self.end_result
self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}}
def generate_filename(self):
self.update_root_properties()
player_names = {
bw: re.sub(r"[\u200b\u3164'<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"
}
base_game_name = f"{PROGRAM_NAME}_{player_names['B']} vs {player_names['W']}"
return f"{base_game_name} {self.game_id}.sgf"
def write_sgf(self, filename: str, trainer_config: Optional[Dict] = None):
if trainer_config is None:
trainer_config = self.katrain.config("trainer", {})
save_feedback = trainer_config.get("save_feedback", False)
eval_thresholds = trainer_config["eval_thresholds"]
save_analysis = trainer_config.get("save_analysis", False)
self.update_root_properties()
show_dots_for = {
bw: trainer_config.get("eval_show_ai", True) or self.katrain.players_info[bw].human for bw in "BW"
}
sgf = self.root.sgf(
save_comments_player=show_dots_for,
save_comments_class=save_feedback,
eval_thresholds=eval_thresholds,
save_analysis=save_analysis,
)
self.sgf_filename = filename
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w", encoding="utf-8") as f:
f.write(sgf)
return i18n._("sgf written").format(file_name=filename)
def set_region_of_interest(self, region_of_interest):
x1, x2, y1, y2 = region_of_interest
xmin, xmax = min(x1, x2), max(x1, x2)
@@ -484,6 +543,12 @@ class Game:
stones = {s.coords for s in self.stones}
cn = self.current_node
if mode == "stop":
for e in set(self.engines.values()):
e.terminate_queries()
self.katrain.idle_analysis = False
return
engine = self.engines[cn.next_player]
Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0)
@@ -514,6 +579,7 @@ class Game:
elif mode == "sweep":
board_size_x, board_size_y = self.board_size
if cn.analysis_exists:
policy_grid = (
var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y))
@@ -554,50 +620,108 @@ class Game:
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
else:
raise ValueError("Invalid analysis mode")
for move in analyze_moves:
if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits:
cn.analyze(
engine, priority=priority, visits=visits, refine_move=move, time_limit=False
) # explicitly requested so take as long as you need
def play_to_end(self):
def selfplay(self, until_move, target_b_advantage=None):
cn = self.current_node
count = 0
if not cn.analysis_exists:
self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, cn)
return
def analyze_and_play_policy(node):
nonlocal count, cn
cand = node.candidate_moves
if target_b_advantage is not None:
analysis_kwargs = {"visits": max(25, self.katrain.config("engine/fast_visits"))}
engine_settings = {"wideRootNoise": 0.03}
else:
analysis_kwargs = engine_settings = {}
def set_analysis(node, result):
node.set_analysis(result)
analyze_and_play(node)
def request_analysis_for_node(node):
self.engines[node.player].request_analysis(
node,
callback=lambda result, _partial: set_analysis(node, result),
priority=-1000,
analyze_fast=True,
extra_settings=engine_settings,
**analysis_kwargs,
)
def analyze_and_play(node):
nonlocal cn, engine_settings
candidates = node.candidate_moves
if self.katrain.game is not self:
return # a new game happened
if cand:
move = Move.from_gtp(cand[0]["move"], player=node.next_player)
else:
ai_thoughts = "Move generated by AI self-play\n"
if until_move != "end" and target_b_advantage is not None: # setup pos
if node.depth >= until_move or candidates[0]["move"] == "pass":
self.set_current_node(node)
return
target_score = cn.score + (node.depth - cn.depth + 1) * (target_b_advantage - cn.score) / (
until_move - cn.depth
)
max_loss = 5
stddev = min(3, 0.5 + (until_move - node.depth) * 0.15)
ai_thoughts += f"Selecting moves aiming at score {target_score:.1f} +/- {stddev:.2f} with < {max_loss} points lost\n"
if abs(node.score - target_score) < 3 * stddev:
weighted_cands = [
(
move,
math.exp(-0.5 * (abs(move["scoreLead"] - target_score) / stddev) ** 2)
* math.exp(-0.5 * (min(0, move["pointsLost"]) / max_loss) ** 2),
)
for i, move in enumerate(candidates)
if move["pointsLost"] < max_loss or i == 0
]
move_info = weighted_selection_without_replacement(weighted_cands, 1)[0][0]
for move, wt in weighted_cands:
self.katrain.log(
f"{'* ' if move_info == move else ' '} {move['move']} {move['scoreLead']} {wt}",
OUTPUT_EXTRA_DEBUG,
)
ai_thoughts += f"Move option: {move['move']} score {move['scoreLead']:.2f} loss {move['pointsLost']:.2f} weight {wt:.3e}\n"
else: # we're a bit lost, far away from target, just push it closer
move_info = min(candidates, key=lambda move: abs(move["scoreLead"] - target_score))
self.katrain.log(
f"* Played {move_info['move']} {move_info['scoreLead']} because score deviation between current score {node.score} and target score {target_score} > {3*stddev}",
OUTPUT_EXTRA_DEBUG,
)
ai_thoughts += f"Move played to close difference between score {node.score:.1f} and target {target_score:.1f} quickly."
self.katrain.log(
f"Self-play until {until_move} target {target_b_advantage}: {len(candidates)} candidates -> move {move_info['move']} score {move_info['scoreLead']} point loss {move_info['pointsLost']}",
OUTPUT_DEBUG,
)
move = Move.from_gtp(move_info["move"], player=node.next_player)
elif candidates: # just selfplay to end
move = Move.from_gtp(candidates[0]["move"], player=node.next_player)
else: # 1 visit etc
polmoves = node.policy_ranking
move = polmoves[0][1] if polmoves else Move(None)
if move.is_pass:
if self.current_node == cn:
self.set_current_node(node)
return
count += 1
new_node = GameNode(parent=node, move=move)
new_node.ai_thoughts = ai_thoughts
if until_move != "end" and target_b_advantage is not None:
self.set_current_node(new_node)
self.katrain.controls.set_status(
i18n._("setup game status message").format(move=new_node.depth, until_move=until_move),
STATUS_INFO,
)
else:
if node != cn:
node.remove_shortcut()
cn.add_shortcut(new_node)
self.katrain.controls.move_tree.redraw_tree_trigger()
request_analysis_for_node(new_node)
def set_analysis(result, _partial):
new_node.set_analysis(result)
analyze_and_play_policy(new_node)
self.engines[node.next_player].request_analysis(
new_node, callback=set_analysis, priority=-1000, analyze_fast=True
)
analyze_and_play_policy(cn)
request_analysis_for_node(cn)
def analyze_undo(self, node):
train_config = self.katrain.config("trainer")
+19 -3
View File
@@ -106,7 +106,12 @@ class GameNode(SGFNode):
self.analysis = {"moves": {}, "root": None, "ownership": None, "policy": None, "completed": False}
def sgf_properties(
self, save_comments_player=None, save_comments_class=None, eval_thresholds=None, save_analysis=False
self,
save_comments_player=None,
save_comments_class=None,
eval_thresholds=None,
save_analysis=False,
save_marks=False,
):
properties = copy.copy(super().sgf_properties())
note = self.note.strip()
@@ -126,6 +131,7 @@ class GameNode(SGFNode):
and self.analysis_exists
and (note or ((save_comments_player or {}).get(self.player, False) and show_class))
):
if save_marks:
candidate_moves = self.parent.candidate_moves
top_x = Move.from_gtp(candidate_moves[0]["move"]).sgf(self.board_size)
best_sq = [
@@ -286,6 +292,7 @@ class GameNode(SGFNode):
single_move = self.move
if single_move and self.parent:
policy_ranking = self.parent.policy_ranking
if policy_ranking:
for ix, (p, m) in enumerate(policy_ranking):
if m == single_move:
return ix + 1, p, policy_ranking
@@ -301,7 +308,10 @@ class GameNode(SGFNode):
single_move = self.move
if not self.parent or not single_move: # root
if self.root:
return f"{i18n._('komi')}: {self.komi:.1f}\n{i18n._('ruleset')}: {i18n._(self.get_property('RU','Japanese').lower())}\n"
rules = self.get_property("RU", "Japanese")
if isinstance(rules, str): # else katago dict
rules = i18n._(rules.lower())
return f"{i18n._('komi')}: {self.komi:.1f}\n{i18n._('ruleset')}: {rules}\n"
return ""
text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n"
@@ -319,7 +329,13 @@ class GameNode(SGFNode):
text += i18n._("Info:point loss").format(points_lost=points_lost) + "\n"
top_move = previous_top_move["move"]
score = self.format_score(previous_top_move["scoreLead"])
text += i18n._("Info:top move").format(top_move=top_move, score=score) + "\n"
text += (
i18n._("Info:top move").format(
top_move=top_move,
score=score,
)
+ "\n"
)
else:
text += i18n._("Info:best move") + "\n"
if previous_top_move.get("pv") and (sgf or details):
+21 -2
View File
@@ -50,6 +50,9 @@ class Move:
def __eq__(self, other):
return self.coords == other.coords and self.player == other.player
def __hash__(self):
return hash((self.coords, self.player))
def gtp(self):
"""Returns GTP coordinates of the move"""
if self.is_pass:
@@ -95,6 +98,9 @@ class SGFNode:
def _clear_cache(self):
self.moves_cache = None
def __repr__(self):
return f"SGFNode({dict(self.properties)})"
def sgf_properties(self, **xargs) -> Dict:
"""For hooking into in a subclass and overriding/formatting any additional properties to be output."""
return copy.deepcopy(self.properties)
@@ -164,6 +170,10 @@ class SGFNode:
"""Get the first value of the property, typically when exactly one is expected."""
return self.properties.get(property, [default])[0]
def clear_property(self, property) -> Any:
"""Removes property if it exists."""
return self.properties.pop(property, None)
@property
def parent(self) -> Optional["SGFNode"]:
"""Returns the parent node"""
@@ -186,10 +196,11 @@ class SGFNode:
def depth(self) -> int:
"""Returns the depth of this node, where root is 0, cached for speed"""
if self._depth is None:
moves = self.moves
if self.is_root:
self._depth = 0
else:
self._depth = self.parent.depth + 1
else: # no increase on placements etc
self._depth = self.parent.depth + len(moves)
return self._depth
@property
@@ -245,6 +256,14 @@ class SGFNode:
for sgf_coords in self.get_list_property("A" + pl, [])
]
@property
def clear_placements(self) -> List[Move]:
"""Returns all AE clear square commends in the node."""
return [
Move.from_sgf(sgf_coords, player=None, board_size=self.board_size)
for sgf_coords in self.get_list_property("AE", [])
]
@property
def move_with_placements(self) -> List[Move]:
"""Returns all moves (B/W) and placements (AB/AW) in the node."""
+9
View File
@@ -1,4 +1,7 @@
import heapq
import math
import os
import random
import struct
import sys
from typing import List, Tuple, TypeVar
@@ -89,3 +92,9 @@ def json_truncate_arrays(data, lim=20):
return {k: json_truncate_arrays(v) for k, v in data.items()}
else:
return data
def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]:
"""For a list of tuples where the second element is a weight, returns random items with those weights, without replacement."""
elt = [(math.log(random.random()) / (item[1] + 1e-18), item) for item in items] # magic
return [e[1] for e in heapq.nlargest(pick_n, elt)] # NB fine if too small
+13 -4
View File
@@ -691,16 +691,14 @@
size_hint_x: None
width: self.texture_size[0] + 4
on_left_press: checkbox.trigger_action()
TriStateMDCheckbox:
MDCheckbox:
id: checkbox
disabled: root.disabled
tri_state: root.tri_state
size_hint: None,0.5
pos_hint: {'center_y': 0.5}
width: self.height
active: root.default_active
on_active: if app.gui: app.gui.update_state()
on_slashed: if app.gui: app.gui.update_state()
<AnalysisMenuItem@MenuItem>:
@@ -747,6 +745,11 @@
icon: 'local.png'
shortcut: 'g'
on_action: root.katrain("select-box",)
AnalysisMenuItem:
text: i18n._("analysis:reset")
icon: 'reset.png'
shortcut: 'h'
on_action: root.katrain("reset-analysis",)
AnalysisMenuItem:
text: i18n._("analysis:insert")
icon: 'Insert-Move.png'
@@ -756,7 +759,7 @@
text: i18n._("analysis:playtoend")
icon: 'Finish.png'
shortcut: 'l'
on_action: root.katrain("play-to-end")
on_action: root.katrain("selfplay-setup","end")
AnalysisMenuItem:
text: i18n._("analysis:game")
icon: 'analysis.png'
@@ -979,6 +982,12 @@
icon: 'General-Settings.png'
shortcut: 'F8'
on_action: root.katrain('config-popup')
MainMenuItem:
text: i18n._('menu:distributed')
shortcut: 'F9'
icon: 'collaboration.png'
on_action: root.katrain('contribute-popup')
# on_action: root.katrain('katago-contribute')
MenuLabel:
text: i18n._('menu:lang')
MDBoxLayout:
+35 -25
View File
@@ -40,7 +40,7 @@ from katrain.gui.theme import Theme
class BadukPanWidget(Widget):
def __init__(self, **kwargs):
super(BadukPanWidget, self).__init__(**kwargs)
self.stones_sounds = [SoundLoader.load(file) for file in Theme.STONE_SOUNDS]
self.stones_sounds = []
self.trainer_config = {}
self.ghost_stone = []
self.gridpos_x = []
@@ -71,6 +71,8 @@ class BadukPanWidget(Widget):
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()
@@ -335,15 +337,13 @@ class BadukPanWidget(Widget):
board_size_x, board_size_y = katrain.game.board_size
if len(self.gridpos_x) < board_size_x or len(self.gridpos_y) < board_size_y:
return # race condition
show_n_eval = self.trainer_config["eval_off_show_last"]
show_n_eval = self.trainer_config.get("eval_on_show_last", 3)
with self.canvas:
self.canvas.clear()
# stones
current_node = katrain.game.current_node
game_ended = katrain.game.end_result
full_eval_on = katrain.analysis_controls.eval.active
all_dots_off = katrain.analysis_controls.eval.checkbox.slashed
all_dots_off = not katrain.analysis_controls.eval.active
has_stone = {}
drawn_stone = {}
for m in katrain.game.stones:
@@ -367,9 +367,7 @@ class BadukPanWidget(Widget):
placements = node.placements
for m in node.moves + placements:
if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for
move_eval_on = (
not all_dots_off and show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on)
)
move_eval_on = not all_dots_off and show_dots_for.get(m.player) and i < show_n_eval
if move_eval_on and points_lost is not None:
evalcol = self.eval_color(points_lost, show_dots_for_class)
else:
@@ -478,7 +476,14 @@ class BadukPanWidget(Widget):
)
if move_policy == best_move_policy:
Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA)
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], self.stone_size - dp(1.2)), width=dp(2))
Line(
circle=(
self.gridpos_x[x],
self.gridpos_y[y],
self.stone_size - dp(1.2),
),
width=dp(2),
)
with pass_btn.canvas.after:
move_policy = policy[-1]
@@ -490,21 +495,6 @@ class BadukPanWidget(Widget):
(*colors[pol_order][:3], Theme.GHOST_ALPHA),
)
# pass circle
passed = len(nodes) > 1 and current_node.is_pass
if passed or game_ended:
if game_ended:
text = game_ended
katrain.controls.timer.paused = True
else:
text = i18n._("board-pass")
Color(*Theme.PASS_CIRCLE_COLOR)
center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)])
size = min(self.width, self.height) * 0.227
Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size))
Color(*Theme.PASS_CIRCLE_TEXT_COLOR)
draw_text(pos=center, text=text, font_size=size * 0.25, halign="center")
self.redraw_hover_contents_trigger()
def draw_roi_box(self, region_of_interest, width=2):
@@ -615,11 +605,17 @@ class BadukPanWidget(Widget):
keys[TOP_MOVE_DELTA_SCORE] = (
"0.0" if -0.05 < move_dict["pointsLost"] < 0.05 else f"{-move_dict['pointsLost']:+.1f}"
)
# def fmt_maybe_missing(arg,sign,digits=1):
# return str(round(sign*arg,digits)) if arg is not None else "N/A"
keys[TOP_MOVE_SCORE] = f"{player_sign * move_dict['scoreLead']:.1f}"
winrate = move_dict["winrate"] if player_sign == 1 else 1 - move_dict["winrate"]
keys[TOP_MOVE_WINRATE] = f"{winrate*100:.1f}"
keys[TOP_MOVE_DELTA_WINRATE] = f"{-move_dict['winrateLost']:+.1%}"
keys[TOP_MOVE_VISITS] = format_visits(move_dict["visits"])
# keys[TOP_MOVE_UTILITY] = fmt_maybe_missing( move_dict.get('utility'),player_sign,2)
# keys[TOP_MOVE_UTILITYLCB] = fmt_maybe_missing(move_dict.get('utilityLcb'),player_sign,2)
# keys[TOP_MOVE_SCORE_STDDEV] =fmt_maybe_missing(move_dict.get('scoreStdev'),1)
Color(*Theme.HINT_TEXT_COLOR)
draw_text(
pos=(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]),
@@ -693,9 +689,23 @@ class BadukPanWidget(Widget):
up_to_move = (time.time() - start_time) / delay
self.draw_pv(pv, node, up_to_move)
if self.katrain.game.region_of_interest:
if getattr(self.katrain.game, "region_of_interest", None):
self.draw_roi_box(self.katrain.game.region_of_interest, width=dp(1.25))
# pass circle
if current_node.is_pass or game_ended:
if game_ended:
text = game_ended
katrain.controls.timer.paused = True
else:
text = i18n._("board-pass")
Color(*Theme.PASS_CIRCLE_COLOR)
center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)])
size = min(self.width, self.height) * 0.227
Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size))
Color(*Theme.PASS_CIRCLE_TEXT_COLOR)
draw_text(pos=center, text=text, font_size=size * 0.25, halign="center")
def animate_pv(self, _dt):
if self.animating_pv:
self.redraw_hover_contents_trigger()
+29 -6
View File
@@ -6,7 +6,15 @@ from kivy.properties import ObjectProperty, OptionProperty
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from katrain.core.constants import MODE_ANALYZE, MODE_PLAY, PLAYER_HUMAN, STATUS_ANALYSIS, STATUS_ERROR
from katrain.core.constants import (
MODE_ANALYZE,
MODE_PLAY,
PLAYER_HUMAN,
STATUS_ANALYSIS,
STATUS_ERROR,
AI_DEFAULT,
PLAYER_AI,
)
from katrain.core.lang import rank_label
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
from katrain.gui.theme import Theme
@@ -24,7 +32,7 @@ class PlayAnalyzeSelect(MDFloatLayout):
self.katrain._config["ui_state"] = self.katrain._config.get("ui_state", {})
self.katrain._config["ui_state"][self.mode] = {
"analysis_controls": {
id: toggle.active if not toggle.checkbox.slashed else None # troolean ftw
id: toggle.active
for id, toggle in self.katrain.analysis_controls.ids.items()
if isinstance(toggle, AnalysisToggle)
},
@@ -41,8 +49,6 @@ class PlayAnalyzeSelect(MDFloatLayout):
for id, active in state.get("analysis_controls", {}).items():
cb = self.katrain.analysis_controls.ids[id].checkbox
cb.active = bool(active)
if cb.tri_state:
cb.slashed = active is None
for id, (panel_state, button_state) in state.get("panels", {}).items():
self.katrain.controls.ids[id].set_option_state(button_state)
self.katrain.controls.ids[id].state = panel_state
@@ -119,7 +125,7 @@ class ControlsPanel(BoxLayout):
game.current_node is not self.status_state[2]
and not (self.status_state[1] == STATUS_ERROR and self.status_state[2] is None)
) or (
len(game.engines["B"].queries) == 0 and self.status_state[1] == STATUS_ANALYSIS
self.katrain.engine.is_idle() and self.status_state[1] == STATUS_ANALYSIS
): # clear status if node changes, except startup errors on root. also clear analysis message when no queries
self.status.text = ""
self.status_state = (None, -1e9, None)
@@ -139,8 +145,25 @@ class ControlsPanel(BoxLayout):
lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY
details = self.info.detailed and not lock_ai
info = ""
if katrain.contributing:
info += katrain.engine.status()
game_id = getattr(katrain.engine, "showing_game", None)
game = getattr(katrain.engine, "active_games", {}).get(game_id)
if game is not None:
info += f"Showing game {game_id}\n"
for bw in "BW":
self.players[bw].rank = None
network = game.root.get_property(f"P{bw}", AI_DEFAULT)
parts = network.split("-")
if len(parts) == 4:
self.players[bw].player_type = parts[1]
self.players[bw].player_subtype = parts[2]
else:
self.players[bw].player_type = PLAYER_AI
self.players[bw].player_subtype = network
if move or current_node.is_root:
info = self.active_comment_node.comment(
info += self.active_comment_node.comment(
teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details
)
-32
View File
@@ -410,38 +410,6 @@ class Timer(BGBoxLayout):
timeout = BooleanProperty(False)
class TriStateMDCheckbox(MDCheckbox):
tri_state = BooleanProperty(False)
slashed = BooleanProperty(False)
checkbox_icon_slashed = StringProperty("checkbox-blank-off-outline")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(slashed=self.update_icon)
def _do_press(self):
if not self.tri_state:
return super()._do_press()
if self.slashed:
self.state = "normal"
self.slashed = False
elif self.state == "down":
self.state = "normal"
self.slashed = True
else:
self.state = "down"
self.slashed = False
self.update_icon()
def update_icon(self, *args):
if self.tri_state and self.slashed:
self.icon = self.checkbox_icon_slashed
elif self.state == "down":
self.icon = self.checkbox_icon_down
else:
self.icon = self.checkbox_icon_normal
class AnalysisToggle(MDBoxLayout):
text = StringProperty("")
default_active = BooleanProperty(False)
+49 -17
View File
@@ -49,8 +49,10 @@ class I18NPopup(Popup):
font_name = StringProperty(Theme.DEFAULT_FONT)
def __init__(self, size=None, **kwargs):
if size: # do not exceed window height
size[1] = min(MDApp.get_running_app().gui.height, size[1])
if size: # do not exceed window size
app = MDApp.get_running_app()
size[0] = min(app.gui.width, size[0])
size[1] = min(app.gui.height, size[1])
super().__init__(size=size, **kwargs)
self.bind(on_dismiss=Clock.schedule_once(lambda _dt: MDApp.get_running_app().gui.update_state(), 1))
@@ -252,12 +254,15 @@ class ConfigTimerPopup(QuickConfigGui):
class NewGamePopup(QuickConfigGui):
mode = StringProperty("newgame")
def __init__(self, katrain):
super().__init__(katrain)
for bw, info in katrain.players_info.items():
self.player_setup.update_player_info(bw, info)
self.rules_spinner.value_refs = [name for abbr, name in katrain.engine.RULESETS_ABBR]
self.bind(mode=self.update_playername)
Clock.schedule_once(self.update_from_current_game, 0.1)
def normalized_rules(self):
@@ -271,13 +276,17 @@ class NewGamePopup(QuickConfigGui):
name = self.player_name[bw].text
if name:
self.katrain.game.root.set_property("P" + bw, name)
else:
self.katrain.game.root.clear_property("P" + bw)
self.katrain.update_player(bw, **player_setup.player_type_dump)
def update_from_current_game(self, *args):
def update_playername(self, *args):
for bw in "BW":
name = self.katrain.game.root.get_property("P" + bw, None)
if name and SGF_INTERNAL_COMMENTS_MARKER not in name:
self.player_name[bw].text = name
self.player_name[bw].text = name if self.mode == "editgame" else ""
def update_from_current_game(self, *args): # set rules and komi
rules = self.normalized_rules()
self.km.text = str(self.katrain.game.root.komi)
if rules is not None:
@@ -285,15 +294,15 @@ class NewGamePopup(QuickConfigGui):
def update_config(self, save_to_file=True, close_popup=True):
super().update_config(save_to_file=save_to_file, close_popup=close_popup)
self.katrain.log(f"New game settings: {self.katrain.config('game')}", OUTPUT_DEBUG)
props = self.collect_properties(self)
self.katrain.log(f"Mode: {self.mode}, settings: {self.katrain.config('game')}", OUTPUT_DEBUG)
self.update_playerinfo() # type
if self.mode == "newgame":
if self.restart.active:
self.katrain.log("Restarting Engine", OUTPUT_DEBUG)
self.katrain.engine.restart()
self.update_playerinfo()
self.katrain("new-game")
def update_game(self):
props = self.collect_properties(self)
self.katrain._do_new_game()
elif self.mode == "editgame":
root = self.katrain.game.root
changed = False
for k, currentval, newval in [
@@ -307,11 +316,13 @@ class NewGamePopup(QuickConfigGui):
OUTPUT_INFO,
)
self.katrain.game.root.set_property(k, newval)
self.update_playerinfo()
if changed:
self.katrain.engine.on_new_game()
self.katrain.game.analyze_all_nodes(analyze_fast=True)
self.popup.dismiss()
else: # setup position
self.katrain._do_new_game()
self.katrain("selfplay-setup", props["game/setup_move"], props["game/setup_advantage"])
self.update_playerinfo() # name
def wrap_anchor(widget):
@@ -404,7 +415,7 @@ class ConfigAIPopup(QuickConfigGui):
widget.bind(active=self.estimate_rank_from_options)
else:
if isinstance(values[0], Tuple): # with descriptions, possibly language-specific
fixed_values = [(v, re.sub(r"\[(.*?)\]", lambda m: i18n._(m[1]), l)) for v, l in values]
fixed_values = [(v, re.sub(r"\[(.*?)]", lambda m: i18n._(m[1]), l)) for v, l in values]
else: # just numbers
fixed_values = [(v, str(v)) for v in values]
widget = LabelledSelectionSlider(
@@ -502,7 +513,7 @@ class BaseConfigPopup(QuickConfigGui):
f.replace("/", os.path.sep).replace(PATHS["PACKAGE"], "katrain")
for ftype in ["*.bin.gz", "*.txt.gz"]
for f in glob.glob(slashpath + "/" + ftype)
if '.tmp.' not in f
if ".tmp." not in f
]
if files and path not in self.paths:
self.paths.append(path) # persistent on paths with models found
@@ -743,9 +754,27 @@ class ConfigPopup(BaseConfigPopup):
Clock.schedule_once(restart_engine, 0)
class LoadSGFPopup(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
class ContributePopup(BaseConfigPopup):
def __init__(self, katrain):
super().__init__(katrain)
MDApp.get_running_app().bind(language=self.check_katas)
Clock.schedule_once(self.check_katas)
def start_contributing(self):
self.update_config(True, close_popup=False)
self.error.text = ""
log_settings = {**self.katrain.config("contribute"), "password": "***"}
self.katrain.log(f"Updating contribution settings {log_settings}", OUTPUT_DEBUG)
if not self.katrain.config("contribute/username") or not self.katrain.config("contribute/password"):
self.error.text = "Please enter your username and password for katagotraining.org"
else:
self.popup.dismiss()
self.katrain("katago-contribute")
class LoadSGFPopup(BaseConfigPopup):
def __init__(self, katrain):
super().__init__(katrain)
app = MDApp.get_running_app()
self.filesel.favorites = [
(os.path.abspath(app.gui.config("general/sgf_load")), "Last Load Dir"),
@@ -783,3 +812,6 @@ class SaveSGFPopup(BoxLayout):
class ReAnalyzeGamePopup(BoxLayout):
katrain = ObjectProperty(None)
popup = ObjectProperty(None)
def on_submit(self):
self.button.trigger_action(duration=0)
+4 -3
View File
@@ -196,9 +196,10 @@ class MoveTreeCanvas(Widget):
pos=[c - self.move_size / 2 - spacing / 2 for c in self.move_xy_pos[node]],
size=(self.move_size + spacing, self.move_size + spacing),
)
collapsed_color = Theme.MOVE_TREE_COLLAPSED if node.shortcut_from else None
draw_stone(pos, node.player, collapsed_color)
text = str(node.depth)
placements = node.placements + node.clear_placements
special_node = Theme.MOVE_TREE_COLLAPSED if node.shortcut_from or placements else None
draw_stone(pos, node.player, special_node)
text = "+" if placements else str(node.depth)
Color(*Theme.STONE_COLORS["W" if node.player == "B" and not node.shortcut_from else "B"])
draw_text(pos=pos, text=text, font_size=self.move_size * 1.75 / (1 + 1 * len(text)), font_name="Roboto")
+1
View File
@@ -36,6 +36,7 @@ class SelectionSlider(Widget):
return self.values[self.index][0]
def set_value(self, set_value): # set to closest value
if isinstance(set_value, (float, int)):
eq_value = sorted([(abs(v - set_value), i) for i, (v, l) in enumerate(self.values)])
self.index = eq_value[0][1]
Binary file not shown.
+97 -17
View File
@@ -276,10 +276,6 @@ msgstr "韩国规则"
msgid "aga"
msgstr "AGA规则"
#. TODO check
msgid "stone_scoring"
msgstr "古代中国规则"
msgid "clear cache"
msgstr "清除缓存"
@@ -294,7 +290,7 @@ msgid "non square board hint"
msgstr "非方形棋盘使用x:y(例如19:9)"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "开始新对局"
# teacher settings from here
@@ -317,14 +313,10 @@ msgid "save dots"
msgstr "保存为SGF"
msgid "show last n dots"
msgstr ""
"显示最后\n"
"<this many> 手棋的选点"
msgstr "显示最后几手棋的选点"
msgid "show ai dots"
msgstr ""
"AI的\n"
"显示选点/SGF评价"
msgstr "AI的显示选点/SGF评价"
#. submit button
msgid "update teacher"
@@ -645,7 +637,6 @@ msgstr ""
"所有损失最高'max_points_lost'目和至少'min_visits'计算深度的着法当中,偏向于损失较小,乙方空定型(偏向值settled_weight),对方空定型(偏向值settled_weight"
" * opponent "
"fac),避免脱先(5路以上,偏向值tenuki_penalty),避免碰(偏向值attachment_penalty)的手法。偏向值可以设为复数捣乱局面,宽根噪声(通用设置)可以棋力代价更加符合风格。"
" "
msgid "ai:simple"
msgstr "简明风格"
@@ -653,11 +644,6 @@ msgstr "简明风格"
msgid "local analysis"
msgstr "正在寻找局部走法"
msgid "change current game"
msgstr ""
"更新贴目\n"
"规则在当前对局"
msgid "Make Main Branch"
msgstr "设为实战变化"
@@ -757,3 +743,97 @@ msgstr "计算深度...以上时显示"
#. analysis option
msgid "cache analysis to sgf"
msgstr "分析缓存到SGF"
msgid "stone_scoring"
msgstr "古代中国规则"
#. in self-play mode
msgid "gui-locked"
msgstr "在分散式自我对弈训练模式中,{action} 动作是不允许的。重新启动KaTrain来离开此模式。"
msgid "contribute:username"
msgstr "帐号"
#. hint for ownership
msgid "contribute:ownership:hint"
msgstr "稍微降低训练速度,但允许所有权可视化。"
#. contribute settings popup
msgid "contribute settings title"
msgstr "贡献至KataGo分散式训练"
msgid "contribute:password"
msgstr "密码"
msgid "contribute:start"
msgstr "开始贡献!"
msgid "contribute:register"
msgstr "注册"
#. menu item
msgid "menu:distributed"
msgstr "分散式自我对弈训练"
#. title
msgid "contribute:login"
msgstr "登入资讯 (katagotraining.org)"
#. hint for max games
msgid "contribute:maxgames:hint"
msgstr "最好等于你的最佳化numSearchThreads,中阶GPU约为8。可以稍微设的低一点,来缩短执行时间,因为未完成的对局将不会被上传。"
#. description for input
msgid "contribute:maxgames"
msgstr "同时对局数"
msgid "contribute:ownership"
msgstr "启用所有权"
#. description
msgid "contribute:movespeed"
msgstr "下棋速度"
#. hint for move speed
msgid "contribute:movespeed:hint"
msgstr "每步延迟(秒/步)"
#. description
msgid "contribute:savesgf"
msgstr "储存SGF"
#. hint for save
msgid "contribute:savesgf:hint"
msgstr "储存显示于 ./dist_sgf,已结束的对局SGF档"
#. title
msgid "contribute:viewer settings"
msgstr "观察者设定"
#. Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "重设分析"
#. Description for black target score
msgid "setup position black score"
msgstr "黑棋目标目数:"
#. Description for how many moves
msgid "setup position move number"
msgstr "产生对局到第几手:"
#. Progress message
msgid "setup game status message"
msgstr "由AI自我对弈产生对局:{move}/{until_move}手完成。"
#. generate position alongside starting new game
msgid "setupposition"
msgstr "设定位置"
#. Popup for generating position: top explanation
msgid "setup position explanation"
msgstr "这会产生部分的对局让你接著下。例如:你可以使用它从各种不同优势的中盘或终局阶段开始,与更强的AI对弈。"
#. change komi etc in current game
msgid "editgame"
msgstr "编辑对局资讯"
Binary file not shown.
+106 -17
View File
@@ -26,7 +26,7 @@ msgid "menu:clocksettings"
msgstr "Bedenkzeit"
msgid "menu:teachsettings"
msgstr "Einstellungen Übungsspiel"
msgstr "Analyse-Einstellungen"
msgid "menu:aisettings"
msgstr "AI-Einstellungen"
@@ -283,9 +283,8 @@ msgstr "Koreanisch"
msgid "aga"
msgstr "AGA"
#. TODO check
msgid "stone_scoring"
msgstr "Alte Chinesisch"
msgstr "Alte chinesische"
msgid "clear cache"
msgstr "Cache leeren"
@@ -301,7 +300,7 @@ msgid "non square board hint"
msgstr "Benutze x:y (z.B. 19:9) für nichtquadratische Grössen"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "Neues Spiel starten"
# teacher settings from here
@@ -324,14 +323,10 @@ msgid "save dots"
msgstr "In SGF speichern"
msgid "show last n dots"
msgstr ""
"Dots für die letzten\n"
"<this many> Züge zeigen"
msgstr "Dots für die letzten <n> Züge zeigen"
msgid "show ai dots"
msgstr ""
"Dots/SGF-Kommentare \n"
" für AI Spieler zeigen"
msgstr "Dots/SGF-Kommentare für AI Spieler zeigen"
#. submit button
msgid "update teacher"
@@ -645,7 +640,7 @@ msgid "engine:altcommand"
msgstr "Engine-Befehl überschreiben"
msgid "download katago button"
msgstr "KataGo-Versionen herunterladen"
msgstr "KataGo-Prog. herunterladen"
msgid "katago binaries available"
msgstr "{num} KataGo-Programme verfügbar"
@@ -704,11 +699,6 @@ msgstr "Einfacher Stil"
msgid "local analysis"
msgstr "Suche nach lokalen Antworten"
msgid "change current game"
msgstr ""
"Aktualisiere Komi/Regeln\n"
" im aktuellen Spiel"
msgid "Make Main Branch"
msgstr "Zum Hauptzweig machen"
@@ -718,7 +708,7 @@ msgstr "Anzeige für den besten Zug"
#. Dropdown menu option
msgid "top_move_delta_winrate"
msgstr "Änderung der Gewinnrate"
msgstr "Gewinnrate Änderung"
#. Dropdown menu option
msgid "top_move_visits"
@@ -815,3 +805,102 @@ msgstr "Name von Schwarz"
#. Hint on player name input
msgid "white player name hint"
msgstr "Name von Weiß"
#. in self-play mode
msgid "gui-locked"
msgstr ""
"Aktion {action} nicht erlaubt im verteilten Selbstspieltraining Modus. Neu "
"starten, damit Katrain diesen Modus verlässt."
#. menu item
msgid "menu:distributed"
msgstr "Verteiltes Selbstspieltraining"
msgid "contribute:register"
msgstr "Registrieren"
msgid "contribute:start"
msgstr "Anfangen beizutragen!"
#. contribute settings popup
msgid "contribute settings title"
msgstr "Zu KataGo's verteiltem Training beitragen"
msgid "contribute:username"
msgstr "Benutzername"
#. description for input
msgid "contribute:maxgames"
msgstr "Gleichzeitige Spiele"
msgid "contribute:password"
msgstr "Passwort"
#. hint for max games
msgid "contribute:maxgames:hint"
msgstr ""
"Am besten gleich zu eurem optimalen numSearchThreads, ungefähr 8 für "
"mittlemäßige GPUs. setzt es leicht kleiner für kürzere Beiträge, da "
"unbeendete Spiele nicht hochgeladen werden."
#. hint for ownership
msgid "contribute:ownership:hint"
msgstr "Macht Training leicht langsamer, aber erlaubt Eigentum visualisierung"
msgid "contribute:ownership"
msgstr "Eigentum aktivieren"
#. title
msgid "contribute:login"
msgstr "Anmeldung Information (katagotraining.org)"
#. description
msgid "contribute:movespeed"
msgstr "Zuggeschwindigkeit"
#. hint for move speed
msgid "contribute:movespeed:hint"
msgstr "Zuggeschwindigkeit in Sekunden/Zug."
#. description
msgid "contribute:savesgf"
msgstr "SGFs speichern"
#. hint for save
msgid "contribute:savesgf:hint"
msgstr "SGF Dateien von beendete Spiele in ./dist_sgf speichern"
#. title
msgid "contribute:viewer settings"
msgstr "Beobachtereinstellungen"
#. Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "Analysis zurücksetzen"
#. Description for black target score
msgid "setup position black score"
msgstr "Zielergebnis für Schwarz:"
#. Description for how many moves
msgid "setup position move number"
msgstr "Spiel generieren bis zum Zug:"
#. Progress message
msgid "setup game status message"
msgstr "Spiel durch KI generieren: Zug {move}/{until_move} generiert."
#. generate position alongside starting new game
msgid "setupposition"
msgstr "Position aufsetzen"
#. Popup for generating position: top explanation
msgid "setup position explanation"
msgstr ""
"Dies generiert ein Teil eines Spieles, von dem ihr anfangen könnt, zu "
"spielen. Zum Beispiel könnt ihr es benutzen um im Vorteil gegen stärkere KIs"
" in verschiedene Mittel- oder Endspielsituationen zu Spielen."
#. change komi etc in current game
msgid "editgame"
msgstr "Spiel editieren"
Binary file not shown.
+104 -13
View File
@@ -109,6 +109,10 @@ msgstr "Deeper full game analysis"
msgid "analysis:alternatives"
msgstr "Find alternative moves"
#. Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "Reset analysis"
#. Restrict analysis to a specific region
msgid "analysis:region"
msgstr "Set region of interest"
@@ -355,7 +359,7 @@ msgstr "Update Timer"
# new game settings from here
msgid "New Game title"
msgstr "Setup New Game"
msgstr "Set up New Game / Edit Game Info and Rules"
#. in new game options
msgid "player names"
@@ -411,14 +415,16 @@ msgid "non square board hint"
msgstr "Use x:y (e.g. 19:9) for non-square size"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "Start New Game"
#. change komi etc in current game
msgid "change current game"
msgstr ""
"Update komi/rules\n"
"in active game"
msgid "editgame"
msgstr "Edit Game Info"
#. generate position alongside starting new game
msgid "setupposition"
msgstr "Set up Position"
# teacher settings from here
msgid "teacher settings"
@@ -428,7 +434,9 @@ msgid "point loss threshold"
msgstr "Point loss greater than"
msgid "num undos"
msgstr "Number of undos"
msgstr ""
"Number of undos\n"
"(teaching games)"
msgid "dot color"
msgstr "Colour"
@@ -440,14 +448,10 @@ msgid "save dots"
msgstr "Save in SGF"
msgid "show last n dots"
msgstr ""
"Show dots for the last\n"
"<this many> moves"
msgstr "Show dots for the last <this many> moves"
msgid "show ai dots"
msgstr ""
"Show dots/SGF comments\n"
" for AI players"
msgstr "Show dots/SGF comments for AI players"
#. analysis option
msgid "cache analysis to sgf"
@@ -461,6 +465,64 @@ msgstr "Update Feedback Settings"
msgid "restarting engine"
msgstr "Restarting Engine after Settings Change."
#. contribute settings popup
msgid "contribute settings title"
msgstr "Contribute To KataGo Distributed Training"
#. title
msgid "contribute:viewer settings"
msgstr "Viewer Settings"
#. title
msgid "contribute:login"
msgstr "Login Information for katagotraining.org"
#. description for input
msgid "contribute:maxgames"
msgstr "Simultaneous Games"
#. hint for max games
msgid "contribute:maxgames:hint"
msgstr ""
"Best equal to your optimal numSearchThreads, around 8 for mid-tier GPUs. Can"
" set it slightly lower for shorter runs, as half-finished games are not "
"uploaded."
msgid "contribute:ownership"
msgstr "Enable Ownership"
#. hint for ownership
msgid "contribute:ownership:hint"
msgstr "Slightly slows down training, but allows ownership visualization."
#. description
msgid "contribute:movespeed"
msgstr "Move speed"
#. hint for move speed
msgid "contribute:movespeed:hint"
msgstr "Move playing delay in seconds/move."
#. description
msgid "contribute:savesgf"
msgstr "Save SGFs"
#. hint for save
msgid "contribute:savesgf:hint"
msgstr "Save SGF files of games that are finished showing in ./dist_sgf"
msgid "contribute:username"
msgstr "Username"
msgid "contribute:password"
msgstr "Password"
msgid "contribute:register"
msgstr "Register"
msgid "contribute:start"
msgstr "Start Contributing!"
# title
msgid "general settings title"
msgstr "General and Engine Settings"
@@ -826,3 +888,32 @@ msgstr "Inserting exchange(s) done, copied {num_copied} following moves."
msgid "finish inserting before navigating"
msgstr ""
"Navigation is disabled in insert mode, finish insert mode by pressing 'i'. "
#. menu item
msgid "menu:distributed"
msgstr "Distributed Self-play Training"
#. in self-play mode
msgid "gui-locked"
msgstr ""
"Action {action} not allowed in distributed self-play training mode. Restart "
"KaTrain to exit this mode."
#. Progress message
msgid "setup game status message"
msgstr "Generating game via AI self-play: Move {move}/{until_move} done."
#. Popup for generating position: top explanation
msgid "setup position explanation"
msgstr ""
"This generates a partial game for you to start playing from. For example, "
"you can use this to play against stronger AIs starting from an advantage in "
"a variety of middle- or end-game positions."
#. Description for black target score
msgid "setup position black score"
msgstr "Target score for Black:"
#. Description for how many moves
msgid "setup position move number"
msgstr "Generate game until move:"
Binary file not shown.
+107 -13
View File
@@ -305,7 +305,7 @@ msgid "non square board hint"
msgstr "Usa x:y (i.e. 19:9) para tableros no cuadrados"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "Empezar Nuevo Juego"
# teacher settings from here
@@ -328,14 +328,10 @@ msgid "save dots"
msgstr "Guardar en SGF"
msgid "show last n dots"
msgstr ""
"Mostar puntos de las últimas \n"
"número de jugadas moves"
msgstr "Mostar puntos de las últimas número de jugadas moves"
msgid "show ai dots"
msgstr ""
"Mostrar puntos/comentarios\n"
" para motores de IA"
msgstr "Mostrar puntos/comentarios para motores de IA"
#. submit button
msgid "update teacher"
@@ -710,12 +706,6 @@ msgstr "Simple Style"
msgid "local analysis"
msgstr "Searching for local responses"
#. TODO - change komi etc in current game
msgid "change current game"
msgstr ""
"Update komi/rules\n"
"in active game"
#. TODO - on move tree editing
msgid "Make Main Branch"
msgstr "Make Main Branch"
@@ -822,3 +812,107 @@ msgstr "Black player's name"
#. TODO - hint on player name input
msgid "white player name hint"
msgstr "White player's name"
#. TODO - in self-play mode
msgid "gui-locked"
msgstr ""
"Action {action} not allowed in distributed training mode. Restart KaTrain to"
" exit this mode."
#. TODO - menu item
msgid "menu:distributed"
msgstr "Distributed Self-play Training"
#. TODO
msgid "contribute:register"
msgstr "Register"
#. TODO
msgid "contribute:start"
msgstr "Start Contributing!"
#. TODO - contribute settings popup
msgid "contribute settings title"
msgstr "Contribute To KataGo Distributed Training"
#. TODO
msgid "contribute:username"
msgstr "Username"
#. TODO - description for input
msgid "contribute:maxgames"
msgstr "Simultaneous Games"
#. TODO
msgid "contribute:password"
msgstr "Password"
#. TODO - hint for max games
msgid "contribute:maxgames:hint"
msgstr ""
"Best equal to your optimal numSearchThreads, around 8 for mid-tier GPUs. Can"
" set it slightly lower for shorter runs, as half-finished games are not "
"uploaded."
#. TODO - hint for ownership
msgid "contribute:ownership:hint"
msgstr "Slightly slows down training, but allows ownership visualization."
#. TODO
msgid "contribute:ownership"
msgstr "Enable Ownership"
#. TODO - title
msgid "contribute:login"
msgstr "Login Information for katagotraining.org"
#. TODO - description
msgid "contribute:movespeed"
msgstr "Move speed"
#. TODO - hint for move speed
msgid "contribute:movespeed:hint"
msgstr "Move playing delay in seconds/move."
#. TODO - description
msgid "contribute:savesgf"
msgstr "Save SGFs"
#. TODO - hint for save
msgid "contribute:savesgf:hint"
msgstr "Save SGF files of games that are finished showing in ./dist_sgf"
#. TODO - title
msgid "contribute:viewer settings"
msgstr "Viewer Settings"
#. TODO - Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "Reset analysis"
#. TODO - Description for black target score
msgid "setup position black score"
msgstr "Target score for Black (Can be negative):"
#. TODO - Description for how many moves
msgid "setup position move number"
msgstr "Generate game until move:"
#. TODO - Progress message
msgid "setup game status message"
msgstr "Generating game via AI self-play: Move {move}/{until_move} done."
#. TODO - generate position alongside starting new game
msgid "setupposition"
msgstr "Set up Position"
#. TODO - Popup for generating position: top explanation
msgid "setup position explanation"
msgstr ""
"This generates a partial game for you to start playing from. For example, "
"you can use this to play against stronger AIs starting from an advantage in "
"a variety of middle- or end-game positions."
#. TODO - change komi etc in current game
msgid "editgame"
msgstr "Edit Game Info"
Binary file not shown.
+92 -13
View File
@@ -11,7 +11,7 @@ msgid "menu:playersetup"
msgstr "Paramètres des joueurs"
msgid "menu:newgame"
msgstr "Nouvelle partie"
msgstr "Lancer/Éditer une partie"
msgid "menu:save"
msgstr "Enregistrer"
@@ -194,8 +194,8 @@ msgstr "Gain"
#. SGF and move comment messages
msgid "SGF start message"
msgstr ""
"Les coup préférés de KataGo sont marqué d’un X. En comparaison, ceux perdant"
" moins de 0.5 points sont marqués d’un carré"
"Les coup préférés de KataGo sont marqué d'un X. En comparaison, ceux perdant"
" moins de 0.5 points sont marqués d'un carré"
msgid "Info:score"
msgstr "Score : {score}"
@@ -235,10 +235,10 @@ msgid "Info:undo predicted PV"
msgstr "Continuation attendue : {pv}"
msgid "Info:AI thoughts"
msgstr "Raisonnement de l’IA : {thoughts}"
msgstr "Raisonnement de he l'IA : {thoughts}"
msgid "No analysis available"
msgstr "Pas d’analyse disponible"
msgstr "Pas d'analyse disponible"
msgid "Analyzing move..."
msgstr "Analyse en cours..."
@@ -320,8 +320,8 @@ msgid "non square board hint"
msgstr "Noter x:y (p. ex. 19:9) pour les non-carrés"
#. submit new game
msgid "new game"
msgstr "Commencer la partie"
msgid "newgame"
msgstr "Nouvelle partie"
# teacher settings from here
msgid "teacher settings"
@@ -361,7 +361,7 @@ msgid "general settings title"
msgstr "Paramètres généraux"
msgid "file not found"
msgstr "Chemin d’accès est introuvable"
msgstr "Chemin d'accès est introuvable"
msgid "general settings"
msgstr "Divers"
@@ -734,11 +734,6 @@ msgstr "Modéré"
msgid "local analysis"
msgstr "Recherche de coups locaux."
msgid "change current game"
msgstr ""
"Mettre à jour\n"
"la partie en cours"
msgid "Make Main Branch"
msgstr "Mettre en branche principale"
@@ -819,3 +814,87 @@ msgstr "Noir"
msgid "white player name hint"
msgstr "Blanc"
msgid "gui-locked"
msgstr ""
"L'action {action} est interdite lors de l'entraînement distribué de KataGo. "
"Vous pouvez quitter ce mode en redémarrant KaTrain."
msgid "menu:distributed"
msgstr "Entraîner Katago"
msgid "contribute:register"
msgstr "Créer le compte"
msgid "contribute:start"
msgstr "Je participe !"
msgid "contribute settings title"
msgstr "Participer à l'entraînement de KataGo"
msgid "contribute:username"
msgstr "Identifiant"
msgid "contribute:maxgames"
msgstr "Parties simultanées"
msgid "contribute:password"
msgstr "Mot de passe"
msgid "contribute:maxgames:hint"
msgstr ""
"Idéalement égal à numSearchThread (autour de 8 pour un GPU ordinaire), mais "
"préférer moins lors de courtes sessions, car les parties inachevées sont "
"oubliées."
msgid "contribute:ownership:hint"
msgstr ""
"Cette option permet de visualiser les prévisions territoriales, mais "
"ralentit légèrement l'entrainement."
msgid "contribute:ownership"
msgstr "Prévisions territoriales"
msgid "contribute:login"
msgstr "Informations de connexion"
msgid "contribute:movespeed"
msgstr "Cadence des coups"
msgid "contribute:movespeed:hint"
msgstr "Délai d'affichage des coups en secondes."
msgid "contribute:savesgf"
msgstr "Sauvegarder les parties"
msgid "contribute:savesgf:hint"
msgstr ""
"Sauvegarder les fichiers SGF des parties complétées dans le dossier "
"./dist_sgf"
msgid "contribute:viewer settings"
msgstr "Visionneuse"
msgid "analysis:reset"
msgstr "Recommencer l'analyse"
msgid "setup position black score"
msgstr " Différence de points (pour Noir) :"
msgid "setup position move number"
msgstr "Nombre de coups simulés :"
msgid "setup game status message"
msgstr "Simulation de partie en cours : Coup {move}/{until_move}."
msgid "setupposition"
msgstr "Nouvelle position"
msgid "setup position explanation"
msgstr ""
"Vous voulez gagner de l'expérience dans les milieux ou les fins de parties ?"
" Apprendre à grappiller les points, maintenir votre avantage ou renverser la"
" situation ? Alors jouez des positions fictives composées selon vos besoins."
msgid "editgame"
msgstr "Partie en cours"
Binary file not shown.
+97 -13
View File
@@ -325,7 +325,7 @@ msgid "non square board hint"
msgstr "長方形碁盤は x:y と記入(例 19:9)"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "新規対局を開始"
# teacher settings from here
@@ -336,7 +336,7 @@ msgid "point loss threshold"
msgstr "何目より損なら"
msgid "num undos"
msgstr "待ったの回数"
msgstr "再考を促す回数"
msgid "dot color"
msgstr "色"
@@ -348,14 +348,10 @@ msgid "save dots"
msgstr "SGFに保存"
msgid "show last n dots"
msgstr ""
"直近何手の\n"
"ドットを表示"
msgstr "直近何手のドットを表示"
msgid "show ai dots"
msgstr ""
"AIの手にもドットや\n"
"SGFコメントをつける"
msgstr "AIの手にもドットやSGFコメントをつける"
#. submit button
msgid "update teacher"
@@ -692,11 +688,6 @@ msgstr "簡明派"
msgid "local analysis"
msgstr "近辺の応手を分析中"
msgid "change current game"
msgstr ""
"現対局のコミ・\n"
"ルールを変更"
#. on move tree editing
msgid "Make Main Branch"
msgstr "こちらを主分岐に"
@@ -797,3 +788,96 @@ msgstr "黒番"
#. hint on player name input
msgid "white player name hint"
msgstr "白番"
#. in self-play mode
msgid "gui-locked"
msgstr "自己対局モードではこの動作 {action} はできません。分散訓練モードを抜けるにはKaTrainを再起動してください。"
#. menu item
msgid "menu:distributed"
msgstr "自己対局による分散訓練"
msgid "contribute:register"
msgstr "登録"
msgid "contribute:start"
msgstr "貢献を開始!"
#. contribute settings popup
msgid "contribute settings title"
msgstr "KataGoの分散訓練に協力"
msgid "contribute:username"
msgstr "ユーザ名"
#. description for input
msgid "contribute:maxgames"
msgstr "最大同時対局数"
msgid "contribute:password"
msgstr "パスワード"
#. hint for max games
msgid "contribute:maxgames:hint"
msgstr ""
"あなたの環境での最適なnumSearchThreads(探索スレッド数、中級品のGPUなら8程度)に合わせるのが最良です。短時間だけ稼働する場合にはそれよりやや小さい値にするのもよいでしょう(未完了の対局はアップロードされないので)。"
#. hint for ownership
msgid "contribute:ownership:hint"
msgstr "訓練速度は若干落ちてしまいますが、黒地か白地かを可視化します"
msgid "contribute:ownership"
msgstr "黒地か白地かを予測"
#. title
msgid "contribute:login"
msgstr "ログイン情報 (katagotraining.org)"
#. description
msgid "contribute:movespeed"
msgstr "着手の速さ"
#. hint for move speed
msgid "contribute:movespeed:hint"
msgstr "何秒ごとに一手進めるか"
#. description
msgid "contribute:savesgf"
msgstr "SGFを保存"
#. hint for save
msgid "contribute:savesgf:hint"
msgstr "表示しおわった対局のSGFファイルを./dist_sgfに保存"
#. title
msgid "contribute:viewer settings"
msgstr "表示設定"
#. Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "分析をリセット"
#. Description for black target score
msgid "setup position black score"
msgstr "黒の何目リードを目標に:"
#. Description for how many moves
msgid "setup position move number"
msgstr "何手目まで生成:"
#. Progress message
msgid "setup game status message"
msgstr "AIの自己対局により局面を生成:{move}手/{until_move}手まで"
#. generate position alongside starting new game
msgid "setupposition"
msgstr "局面を生成"
#. Popup for generating position: top explanation
msgid "setup position explanation"
msgstr ""
"設定に応じた途中局面を自動生成して、そこから打ち継ぎます。たとえば、形勢の良い中盤や終盤の色々な局面から、強いAIを相手に続きを打つといった使い方ができます。"
#. change komi etc in current game
msgid "editgame"
msgstr "対局情報を編集"
Binary file not shown.
+79 -13
View File
@@ -280,7 +280,6 @@ msgstr "한국"
msgid "aga"
msgstr "AGA"
#. TODO check
msgid "stone_scoring"
msgstr "고대 중국"
@@ -298,7 +297,7 @@ msgid "non square board hint"
msgstr "정사각형이 아닌 판에서 두려면 x:y(예: 19:9)를 사용하십시오."
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "새 대국 시작"
# teacher settings from here
@@ -321,14 +320,10 @@ msgid "save dots"
msgstr "SGF 파일에 저장"
msgid "show last n dots"
msgstr ""
"마지막 몇 수에 대해\n"
"인공지능 평가를 색으로 표시할지"
msgstr "마지막 몇 수에 대해 인공지능 평가를 색으로 표시할지"
msgid "show ai dots"
msgstr ""
"인공지능 대국자에 대해서도\n"
"평가를 색으로 표시"
msgstr "인공지능 대국자에 대해서도 평가를 색으로 표시"
#. submit button
msgid "update teacher"
@@ -667,11 +662,6 @@ msgstr "간명한 기풍"
msgid "local analysis"
msgstr "국지적인 응수를 찾고 있습니다"
msgid "change current game"
msgstr ""
"이 판에 대해\n"
"덤과 규칙 등 조절"
msgid "Make Main Branch"
msgstr "주 변화로 만들기"
@@ -770,3 +760,79 @@ msgstr "흑 대국자"
#. hint on player name input
msgid "white player name hint"
msgstr "백 대국자"
msgid "gui-locked"
msgstr "{action} 행동은 분산 자가대국 학습 모드에서 허용되지 않습니다. 이 모드를 벗어나려면 KaTrain을 재시작하십시오."
msgid "menu:distributed"
msgstr "분산 자가대국 학습"
msgid "contribute:register"
msgstr "회원 등록"
msgid "contribute:start"
msgstr "기여 시작하기!"
msgid "contribute settings title"
msgstr "KataGo 분산 학습에 기여하기"
msgid "contribute:username"
msgstr "사용자 이름"
msgid "contribute:maxgames"
msgstr "동시 대국 수"
msgid "contribute:password"
msgstr "비밀번호"
msgid "contribute:maxgames:hint"
msgstr ""
"최적의 numSearchThreads 값과 같게 두는 것이 좋습니다(중급 GPU는 8 정도). 완료되지 않은 판은 업로드되지 않으므로, "
"오래 실행하지 않는다면 조금 낮추어도 무방합니다."
msgid "contribute:ownership:hint"
msgstr "자가대국이 약간 느려지지만, 집 모양을 표시할 수 있습니다."
msgid "contribute:ownership"
msgstr "집 모양 활성화"
msgid "contribute:login"
msgstr "katagotraining.org 로그인 정보 "
msgid "contribute:movespeed"
msgstr "착수 속도"
msgid "contribute:movespeed:hint"
msgstr "착수 시 지연 시간(초)"
msgid "contribute:savesgf"
msgstr "SGF 파일 저장하기"
msgid "contribute:savesgf:hint"
msgstr "다 보여준 판을 ./dist_sgf에 SGF 파일로 저장합니다."
msgid "contribute:viewer settings"
msgstr "뷰어 설정"
msgid "analysis:reset"
msgstr "분석 결과 지우기"
msgid "setup position black score"
msgstr "목표로 하는 집 차이(흑 시점):"
msgid "setup position move number"
msgstr "생성할 대국 수수:"
msgid "setup game status message"
msgstr "AI 자가대국으로 대국 생성중: {move}/{until_move}수 완료됨."
msgid "setupposition"
msgstr "판 깔아두기"
msgid "setup position explanation"
msgstr ""
"이어 둘 수 있는 미완료 대국을 생성합니다. 예를 들어, 강한 AI를 상대로 둘 때 중반전이나 끝내기에서 앞서 있는 상태로 대국을 시작할"
" 수 있습니다."
msgid "editgame"
msgstr "대국 정보 편집"
Binary file not shown.
+84 -13
View File
@@ -306,7 +306,6 @@ msgstr "Корейские"
msgid "aga"
msgstr "AGA"
#. TODO check
msgid "stone_scoring"
msgstr "Древние китайские"
@@ -324,7 +323,7 @@ msgid "non square board hint"
msgstr "Используйте x:y (e.g. 19:9) для прямоугольных размеров"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "Начать новую игру"
# teacher settings from here
@@ -347,14 +346,10 @@ msgid "save dots"
msgstr "Сохранить в SGF"
msgid "show last n dots"
msgstr ""
"Показать точки для\n"
"<этого количества> последних ходов"
msgstr "Показать точки для <этого количества> последних ходов"
msgid "show ai dots"
msgstr ""
"Показать точки и SGF комментарии\n"
" для ИИ игроков"
msgstr "Показать точки и SGF комментарии для ИИ игроков"
#. submit button
msgid "update teacher"
@@ -700,11 +695,6 @@ msgstr "Простой стиль"
msgid "local analysis"
msgstr "Поиск локальных ответов"
msgid "change current game"
msgstr ""
"Изменить коми/правила\n"
"в текущей игре"
msgid "Make Main Branch"
msgstr "Сделать главной веткой"
@@ -784,3 +774,84 @@ msgstr "Имя игрока чёрными"
msgid "white player name hint"
msgstr "Имя игрока белыми"
msgid "gui-locked"
msgstr ""
"Действие {action} запрещено в режиме распределённой тренировки. "
"Перезапустите KaTrain для выхода из этого режима."
msgid "menu:distributed"
msgstr "Распределённая тренировка"
msgid "contribute:register"
msgstr "Регистрация"
msgid "contribute:start"
msgstr "Начать помогать тренировке!"
msgid "contribute settings title"
msgstr "Помощь в распределённой тренировке KataGo"
msgid "contribute:username"
msgstr "Имя пользователя"
msgid "contribute:maxgames"
msgstr "Одновременные игры"
msgid "contribute:password"
msgstr "Пароль"
msgid "contribute:maxgames:hint"
msgstr ""
"Оптимальное значение numSearchThreads, около 8 для GPU среднего уровня. Для "
"коротких сессий обучения лучше установить поменьше, т.к. наполовину "
"сыгранные игры не представляют интереса."
msgid "contribute:ownership:hint"
msgstr "Слегка замедляет тренировку, но позволяет визуализацию."
msgid "contribute:ownership"
msgstr "Включить визуализацию"
msgid "contribute:login"
msgstr "Информация для авторизации на katagotraining.org"
msgid "contribute:movespeed"
msgstr "Скорость хода"
msgid "contribute:movespeed:hint"
msgstr "Длительность хода в секундах/ход."
msgid "contribute:savesgf"
msgstr "Сохранить SGF файлы"
msgid "contribute:savesgf:hint"
msgstr "Сохраняет SGF файлы законченных игр в ./dist_sgf"
msgid "contribute:viewer settings"
msgstr "Настройки отображения"
msgid "analysis:reset"
msgstr "Перезапустить анализ"
msgid "setup position black score"
msgstr "Целевой счёт для чёрных:"
msgid "setup position move number"
msgstr "Генерировать игру до хода:"
msgid "setupposition"
msgstr "Установить позицию"
msgid "setup position explanation"
msgstr ""
"Генерирует частичную игру. Например, вы можете использовать это для игры "
"против сильного AI начиная с перевесом в середине или ближе к окончанию "
"игры."
msgid "setup game status message"
msgstr ""
"Генерация позиции с помощью игры AI с самим собой: Ход {move}/{until_move}."
msgid "editgame"
msgstr "Редактировать игры"
Binary file not shown.
+98 -18
View File
@@ -115,7 +115,7 @@ msgstr "設定欲分析區域"
#. Status message
msgid "analysis:region:start"
msgstr "拖移以選取區域。選擇其他點來取消已選取的區域。"
msgstr "拖移以選取區域。點選其他位置來取消已選取的區域。"
#. Menu option
msgid "analysis:playtoend"
@@ -175,7 +175,7 @@ msgstr "AI引擎狀態指示燈,從綠色(閒置)變為橘色(處理中
#. undo button on the right
msgid "undo-button-text"
msgstr "復原"
msgstr "悔棋"
#. tabs and labels on the right panel
msgid "tab:score"
@@ -400,15 +400,9 @@ msgid "non square board hint"
msgstr "使用 x:y 來設定非正方形棋盤(例如19:9)"
#. submit new game
msgid "new game"
msgid "newgame"
msgstr "開始新對局"
#. change komi etc in current game
msgid "change current game"
msgstr ""
"在進行中對局\n"
"更新貼目/規則"
# teacher settings from here
msgid "teacher settings"
msgstr "指導棋設定"
@@ -429,14 +423,10 @@ msgid "save dots"
msgstr "存入SGF"
msgid "show last n dots"
msgstr ""
"顯示最後\n"
"<this many> 手棋的選點"
msgstr "顯示最後幾手棋的選點"
msgid "show ai dots"
msgstr ""
"顯示AI的\n"
"選點/SGF評論"
msgstr "顯示AI的選點/SGF評論"
#. analysis option
msgid "cache analysis to sgf"
@@ -717,7 +707,7 @@ msgstr "刪除模式"
#. on move tree editing
msgid "Toggle Collapse Branch"
msgstr "擴展/摺疊"
msgstr "展開/摺疊"
#. on move tree editing
msgid "Make Main Branch"
@@ -780,6 +770,96 @@ msgstr "插入完畢,複製 {num_copied} 手棋。"
msgid "finish inserting before navigating"
msgstr "此功能在插入模式無法使用,按 i 結束插入。"
#. TODO
msgid "stone_scoring"
msgstr "Ancient Chinese"
msgstr "古代中國規則"
#. in self-play mode
msgid "gui-locked"
msgstr "在分散式自我對弈訓練模式中,{action} 動作是不允許的。重新啟動KaTrain來離開此模式。"
msgid "contribute:username"
msgstr "帳號"
#. hint for ownership
msgid "contribute:ownership:hint"
msgstr "稍微降低訓練速度,但允許所有權可視化。"
#. contribute settings popup
msgid "contribute settings title"
msgstr "貢獻至KataGo分散式訓練"
msgid "contribute:password"
msgstr "密碼"
msgid "contribute:start"
msgstr "開始貢獻!"
msgid "contribute:register"
msgstr "註冊"
#. menu item
msgid "menu:distributed"
msgstr "分散式自我對弈訓練"
#. title
msgid "contribute:login"
msgstr "登入資訊 (katagotraining.org)"
#. hint for max games
msgid "contribute:maxgames:hint"
msgstr "最好等於您的最佳化numSearchThreads,中階GPU約為8。可以稍微設的低一點,來縮短執行時間,因為未完成的對局將不會被上傳。"
#. description for input
msgid "contribute:maxgames"
msgstr "同時對局數"
msgid "contribute:ownership"
msgstr "啟用所有權"
#. description
msgid "contribute:movespeed"
msgstr "下棋速度"
#. hint for move speed
msgid "contribute:movespeed:hint"
msgstr "每步延遲(秒/步)"
#. description
msgid "contribute:savesgf"
msgstr "儲存SGF"
#. hint for save
msgid "contribute:savesgf:hint"
msgstr "儲存顯示於 ./dist_sgf,已結束的對局SGF檔"
#. title
msgid "contribute:viewer settings"
msgstr "觀察者設定"
#. Clear all results and reset to normal query
msgid "analysis:reset"
msgstr "重設分析"
#. Description for black target score
msgid "setup position black score"
msgstr "黑棋目標目數:"
#. Description for how many moves
msgid "setup position move number"
msgstr "產生對局到第幾手:"
#. Progress message
msgid "setup game status message"
msgstr "由AI自我對弈產生對局:{move}/{until_move}手完成。"
#. generate position alongside starting new game
msgid "setupposition"
msgstr "設定位置"
#. Popup for generating position: top explanation
msgid "setup position explanation"
msgstr "這會產生部分的對局讓你接著下。例如:你可以使用它從各種不同優勢的中盤或終局階段開始,與更強的AI對弈。"
#. change komi etc in current game
msgid "editgame"
msgstr "編輯對局資訊"
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+282 -48
View File
@@ -10,6 +10,7 @@
#:set CP_SPACING Theme.CP_SPACING
#:set CP_SMALL_SPACING Theme.CP_SMALL_SPACING
#:set CP_PADDING Theme.CP_PADDING
#:set EPSILON 1e-9
#:import TOP_MOVE_OPTIONS katrain.core.constants.TOP_MOVE_OPTIONS
@@ -31,15 +32,18 @@
<LabelledPathInput>:
font_size: sp(16)
multiline: False
write_tab: False
helper_text_mode: "on_error"
helper_text: i18n._("file not found")
<LabelledIntInput>:
font_size: sp(Theme.INPUT_FONT_SIZE)
write_tab: False
size_hint: 0.5, None
<LabelledFloatInput>:
font_size: sp(Theme.INPUT_FONT_SIZE)
write_tab: False
size_hint: 0.5, None
<LabelledCheckBox>:
@@ -252,13 +256,172 @@
text: i18n._("update settings")
on_press: root.update_config(True)
<ContributePopup>:
katago_path: katago_path
katago_files: katago_files
katago_download_progress_box: katago_download_progress_box
error: error
DescriptionLabel:
text: i18n._('katago settings')
font_size: sp(Theme.DESC_FONT_SIZE) * 1.25
GridLayout:
cols: 2
rows: 5
size_hint: 1, 6
spacing: CP_SPACING
DescriptionLabel:
text: i18n._("engine:katago")
size_hint: 0.33, 1
AnchorLayout:
LabelledPathInput:
id: katago_path
input_property: "contribute/katago"
hint_text: i18n._("engine:katago:hint")
on_text: root.check_katas()
AnchorLayout:
size_hint: 0.33, 1
AutoSizedRoundedRectangleButton:
text: i18n._("download katago button")
on_press: root.download_katas()
size_hint_y: 0.66
AnchorLayout:
KeyValueSpinner:
id: katago_files
text: ''
text_autoupdate: True
-text_size: self.width, None
-halign: 'center'
-shorten: True
-shorten_from: 'right'
on_select:
if self.selected_index != 0: katago_path.text = self.value_keys[self.selected_index]
size_hint_y: 0.7
sync_height_frac: 1.0
-font_size: self.height * 0.5
-background_color: [*[c*255/88 for c in Theme.BOX_BACKGROUND_COLOR[:3]], 1] # compensate for texture
DescriptionLabel:
text: i18n._("engine:config")
size_hint: 0.33, 1
AnchorLayout:
LabelledPathInput:
input_property: "contribute/config"
DescriptionLabel:
size_hint: 0.2, 1
text: i18n._("contribute:maxgames")
BoxLayout:
AnchorLayout:
size_hint: 0.5, 1
LabelledIntInput:
input_property: "contribute/maxgames"
SmallDescriptionLabel:
text: i18n._("contribute:maxgames:hint")
DescriptionLabel:
size_hint: 0.2, 1
text: i18n._("contribute:ownership")
BoxLayout:
AnchorLayout:
size_hint: 0.5, 1
LabelledCheckBox:
input_property: "contribute/ownership"
SmallDescriptionLabel:
text: i18n._("contribute:ownership:hint")
DescriptionLabel:
text: i18n._('contribute:viewer settings')
font_size: sp(Theme.DESC_FONT_SIZE) * 1.25
GridLayout:
cols: 3
rows: 2
size_hint: 1, 2
spacing: CP_SPACING
DescriptionLabel:
size_hint: 0.5, 1
text: i18n._("contribute:savesgf")
AnchorLayout:
size_hint: 0.5, 1
LabelledCheckBox:
input_property: "contribute/savesgf"
SmallDescriptionLabel:
text: i18n._("contribute:savesgf:hint")
DescriptionLabel:
size_hint: 0.5, 1
text: i18n._("contribute:movespeed")
AnchorLayout:
size_hint: 0.5, 1
LabelledFloatInput:
input_property: "contribute/movespeed"
SmallDescriptionLabel:
text: i18n._("contribute:movespeed:hint")
DescriptionLabel:
text: i18n._('contribute:login')
font_size: sp(Theme.DESC_FONT_SIZE) * 1.25
AnchorLayout:
size_hint: 1,2
GridLayout:
cols: 3
rows: 2
size_hint: 0.85,1
DescriptionLabel:
size_hint: 0.35, 1
text: i18n._("contribute:username")
AnchorLayout:
size_hint: 0.65, 1
LabelledTextInput:
input_property: "contribute/username"
write_tab: False
AnchorLayout:
size_hint: 0.5, 1
AutoSizedRoundedRectangleButton:
text: i18n._("contribute:register")
size_hint: 0.8, 0.7
on_press: app.webbrowser('contribute:signup')
DescriptionLabel:
size_hint: 0.35, 1
text: i18n._("contribute:password")
AnchorLayout:
size_hint: 0.65, 1
LabelledTextInput:
input_property: "contribute/password"
write_tab: False
password: True
id: password
AnchorLayout:
anchor_x: 'left'
size_hint: 0.5, 1
MDIconButton:
icon: "eye"
pos_hint: {"center_x": 0, "center_y": .5}
on_press: password.password = not password.password
DescriptionLabel:
text: ''
id: error
font_size: sp(Theme.DESC_FONT_SIZE)
color: Theme.ERROR_BORDER_COLOR
size_hint: 1, 0.5
MDBoxLayout:
orientation: 'horizontal'
size_hint: 1, 1
MDBoxLayout:
size_hint_x: 2.5 if self.children else 0.0001
orientation: 'vertical'
id: katago_download_progress_box
AnchorLayout:
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,None
height: sp(40)
text: i18n._("contribute:start")
on_press: root.start_contributing()
<ConfigTeacherPopup>
options_grid: options_grid
themes_spinner: themes_spinner
GridLayout:
cols: 5
rows: 7
size_hint: 1, 7
size_hint: 1, 6.5
id: options_grid
DescriptionLabel:
text: i18n._("dot color")
@@ -271,10 +434,10 @@
DescriptionLabel:
text: i18n._("save dots")
AnchorLayout:
size_hint: 1, 4
size_hint: 1, 4.25
GridLayout:
cols: 2
rows: 5
rows: 6
padding: 4
size_hint: 0.95, 1
DescriptionLabel:
@@ -290,7 +453,7 @@
text: i18n._("stats on top move")
AnchorLayout:
BoxLayout:
size_hint: 1, 0.8
size_hint: 1, 0.75
orientation: 'horizontal'
LabelledSpinner:
id: top_moves_show
@@ -307,12 +470,21 @@
DescriptionLabel:
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
text: i18n._("show stats if")
line_height: 0.75
AnchorLayout:
LabelledIntInput:
input_property: "trainer/low_visits"
DescriptionLabel:
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
text: i18n._("show last n dots")
line_height: 0.75
AnchorLayout:
LabelledIntInput:
input_property: "trainer/eval_on_show_last"
DescriptionLabel:
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
text: i18n._("show ai dots")
line_height: 0.75
AnchorLayout:
LabelledCheckBox:
input_property: "trainer/eval_show_ai"
@@ -323,10 +495,10 @@
LabelledCheckBox:
input_property: "trainer/save_analysis"
AnchorLayout:
size_hint: 1,1.5
size_hint: 1,1
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,0.5
size_hint: None,0.75
text: i18n._("update teacher")
on_press: root.update_config(True)
@@ -370,19 +542,50 @@
on_press: root.update_config(True)
<NewGameModeButton@SizedToggleButton>:
inactive_background_color: Theme.LIGHTER_BACKGROUND_COLOR
active_background_color: Theme.PLAY_ANALYZE_TAB_COLOR
ripple_color: Theme.LIGHTER_BACKGROUND_COLOR
text_color: self.inactive_background_color if self.state=='down' else self.active_background_color
size_hint: 0.33,1
_font_size: self.height * 0.45
group: 'new_game_mode'
allow_no_selection: False
<HideyBoxLayout@BoxLayout>:
opacity: 1 if self.size_hint_y > EPSILON else 0
<NewGamePopup>:
restart: restart
rules_spinner: rules_spinner
km: km
player_setup: player_setup
player_name: {"W": white_player_name, "B": black_player_name}
BoxLayout:
size_hint: 1, 0.66
BoxLayout:
size_hint: 0.7,0.7
pos_hint: {'center_x':0.5,'center_y':0.5}
NewGameModeButton:
text: i18n._('newgame')
id: play
state: 'down'
on_state: if self.state=='down': root.mode = 'newgame'
NewGameModeButton:
text: i18n._('setupposition')
id: analyze
on_state: if self.state=='down': root.mode = 'setupposition'
NewGameModeButton:
text: i18n._('editgame')
id: analyze
on_state: if self.state=='down': root.mode = 'editgame'
PlayerSetupBlock:
size_hint: 1, 2
id: player_setup
GridLayout:
rows: 7
rows: 3
cols: 3
size_hint: 1, 3
size_hint: 1, 1.5
spacing: CP_SPACING
padding: CP_PADDING
DescriptionLabel:
@@ -401,6 +604,41 @@
write_tab: False
hint_text: i18n._("white player name hint")
size_hint: 0.75, None
DescriptionLabel:
text: i18n._("ruleset")
AnchorLayout:
LabelledSpinner:
size_hint: 0.95,0.8
input_property: 'game/rules'
font_size: sp(Theme.DESC_FONT_SIZE)
id: rules_spinner
Label:
DescriptionLabel:
text: i18n._("komi")
AnchorLayout:
LabelledFloatInput:
text: '6.5'
id: km
input_property: 'game/komi'
AnchorLayout:
MDBoxLayout:
adaptive_size: True
spacing: CP_SPACING
QuickInputButton:
text: '0.5'
on_left_press: km.text=self.text
QuickInputButton:
text: '6.5'
on_left_press: km.text=self.text
QuickInputButton:
text: '7.5'
on_left_press: km.text=self.text
HideyBoxLayout:
size_hint_y: 1 if root.mode != 'editgame' else EPSILON
id: board_setup_section
GridLayout:
cols: 3
rows: 2
DescriptionLabel:
text: i18n._("board size")
AnchorLayout:
@@ -448,35 +686,9 @@
text: '9'
on_left_press: km.text='0.5'
target: handicap
DescriptionLabel:
text: i18n._("komi")
AnchorLayout:
LabelledFloatInput:
text: '6.5'
id: km
input_property: 'game/komi'
AnchorLayout:
MDBoxLayout:
adaptive_size: True
spacing: CP_SPACING
QuickInputButton:
text: '0.5'
on_left_press: km.text=self.text
QuickInputButton:
text: '6.5'
on_left_press: km.text=self.text
QuickInputButton:
text: '7.5'
on_left_press: km.text=self.text
DescriptionLabel:
text: i18n._("ruleset")
AnchorLayout:
LabelledSpinner:
size_hint: 0.95,0.8
input_property: 'game/rules'
font_size: sp(Theme.DESC_FONT_SIZE)
id: rules_spinner
Label:
HideyBoxLayout:
id: new_game_section
size_hint_y: 1.5 if root.mode == 'newgame' else EPSILON
DescriptionLabel:
text: i18n._('clear cache')
AnchorLayout:
@@ -485,21 +697,40 @@
input_property: 'game/clear_cache'
SmallDescriptionLabel:
text: i18n._('avoids replaying')
BoxLayout:
size_hint: 1,0.75
HideyBoxLayout:
id: setup_game_section
size_hint_y: 1.5 if root.mode == 'setupposition' else EPSILON
orientation: 'vertical'
SmallDescriptionLabel:
text: i18n._('setup position explanation')
size_hint: 1, 1.5
GridLayout:
cols: 2
rows: 2
size_hint: 1,2
DescriptionLabel:
text: i18n._('setup position black score')
AnchorLayout:
size_hint: 0.5, 1
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,0.5
_font_size: self.height*0.35
text: i18n._("change current game")
on_press: root.update_game()
LabelledSelectionSlider:
input_property: 'game/setup_advantage'
key_option: True
values: [(pt,f"{pt}") for pt in range(-150,151)]
DescriptionLabel:
text: i18n._('setup position move number')
AnchorLayout:
LabelledSelectionSlider:
input_property: 'game/setup_move'
key_option: True
values: [(mv,str(mv)) for mv in range(50,251)]
BoxLayout:
size_hint_y: 2.5 if root.mode == 'editgame' else EPSILON
HideyBoxLayout:
size_hint: 1,0.75
AnchorLayout:
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,0.66
text: i18n._("new game")
text: i18n._(root.mode)
on_press: root.update_config(True)
@@ -562,6 +793,7 @@
<ReAnalyzeGamePopup>:
visits: visits
orientation: 'vertical'
button: button
BoxLayout:
size_hint: 1, 1.5
orientation: 'horizontal'
@@ -574,6 +806,7 @@
AnchorLayout:
size_hint: 1, 1
AutoSizedRoundedRectangleButton:
id: button
padding_x: 15
size_hint: None,0.5
text: i18n._("analysis:game") # re-use string
@@ -581,7 +814,6 @@
root.katrain("analyze-extra", "game", visits=visits.input_value)
root.popup.dismiss()
<LoadSGFPopup>:
fast: fast
rewind: rewind
@@ -599,6 +831,7 @@
LabelledCheckBox:
id: fast
size_hint: 0.05,1
input_property: "general/load_fast_analysis"
DescriptionLabel:
text: i18n._("load sgf rewind")
halign: 'right'
@@ -607,6 +840,7 @@
id: rewind
active: True
size_hint: 0.05,1
input_property: "general/load_sgf_rewind"
I18NFileBrowser:
id: filesel
multiselect: False