1.8.0 (#356)
* 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:
58 files changed
+5799
-649
No files matched your search
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
run: pytest tests
|
||||
|
||||
- name: Run I18N conversion
|
||||
run: python i18n.py
|
||||
run: python i18n.py -todo
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
@@ -19,6 +19,7 @@ logs
|
||||
callgrind.*
|
||||
cpp/out
|
||||
.vs
|
||||
dist_sgf
|
||||
|
||||
# debug
|
||||
outdated_log.txt
|
||||
|
||||
+6
-5
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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.
@@ -276,10 +276,6 @@ msgstr "韩国规则"
|
||||
msgid "aga"
|
||||
msgstr "AGA规则"
|
||||
|
||||
#. TODO check
|
||||
msgid "stone_scoring"
|
||||
msgstr "古代中国规则"
|
||||
|
||||
msgid "clear cache"
|
||||
msgstr "清除缓存"
|
||||
|
||||
@@ -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.
@@ -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"
|
||||
@@ -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.
@@ -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"
|
||||
@@ -415,10 +419,12 @@ msgid "new game"
|
||||
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.
@@ -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.
@@ -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..."
|
||||
@@ -321,7 +321,7 @@ msgstr "Noter x:y (p. ex. 19:9) pour les non-carrés"
|
||||
|
||||
#. submit new game
|
||||
msgid "newgame"
|
||||
msgstr "Commencer la partie"
|
||||
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.
@@ -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.
@@ -280,7 +280,6 @@ msgstr "한국"
|
||||
msgid "aga"
|
||||
msgstr "AGA"
|
||||
|
||||
#. TODO check
|
||||
msgid "stone_scoring"
|
||||
msgstr "고대 중국"
|
||||
|
||||
@@ -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.
@@ -306,7 +306,6 @@ msgstr "Корейские"
|
||||
msgid "aga"
|
||||
msgstr "AGA"
|
||||
|
||||
#. TODO check
|
||||
msgid "stone_scoring"
|
||||
msgstr "Древние китайские"
|
||||
|
||||
@@ -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.
@@ -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"
|
||||
@@ -403,12 +403,6 @@ msgstr "使用 x:y 來設定非正方形棋盤(例如19:9)"
|
||||
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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user