Merge pull request #86 from sanderland/v1.3

V1.3
This commit is contained in:
sanderland authored and GitHub committed 2020-07-07 18:06:17 +02:00
commit 64df8f6e62
61 files changed
+2304 -613

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
# Contributing
If you are a new contributor, please first discuss the change you wish to make via
an issue, reddit or discord before making a change.
## Python contributions
Python code is formatted using [black](https://github.com/psf/black) with the settings `-l 120`.
This is not enforced, and contributions with incorrect formatting will be accepted, but formatting this way is appreciated.
## Translations
Adding a translation requires making a new `.po` file with entries for that languages.
* Copy the [English .po file](https://github.com/sanderland/katrain/blob/master/katrain/i18n/locales/en/LC_MESSAGES/katrain.po)
* Change all the `msgstr` entries to your target language.
* 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.
For those who have some python experience as well, you can:
* Replicate the directory structure `katrain/i18n/<lang>/LC_MESSAGES/katrain.po`.
* Run `python i18.py` which will generate the `.mo` files and check for any errors.
* Find `gui.kv` and add a button near the others:
```
LangButton:
icon: 'img/flaticon/flag-<lang>.png'
on_press: app.language = '<lang>'
```
* The language should now show up in the app.
* Pull request your changes to the latest minor version branch.
# Contributors
## Primary author and project maintainer:
[Sander Land](https://github.com/sanderland/)
## Contributors
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.
* "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.
* "nimets123" for the timer sound effects and board/stone graphics.
* Jordan Seaward for the stone sound effects.
* "fohristiwhirl" for the Gibo format parsing code.
## Translators
Many thanks to the following contributors for translations.
* French: "Dontbtme" with contributions from "wonderingabout"
* Korean: "isty2e"
* German: "nimets123"
* 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"
## Additional thanks to
* David Wu ("lightvector") for creating KataGo and providing assistance with making the most of KataGo's amazing capabilities.
* "세븐틴" for including KaTrain in the Baduk Megapack and making explanatory YouTube videos in Korean.
-23
View File
@@ -1,23 +0,0 @@
Primary author and project maintainer (https://github.com/sanderland/katrain):
Sander Land
Many thanks to these additional authors:
Matthew Allred ("Kameone") for design of the v1.1 UI, many suggestions, providing MacOS installation instructions, and working on promotion and youtube videos.
"isty2e" for the Korean translation.
"Dontbtme" for detailed feedback and early testing of v1.0, and the French translation.
"wonderingabout" for helping with the French translation.
"bale-go" for implementing and calibrating the 'calibrated rank' AI.
"nimets123" for the German translation and sound effects.
"nowoowoo" for a fix to the parser for SGF files with extra line breaks.
Sergio Villegas ("serpiente") for the Spanish translation.
Dmitry Ivankov and Alexander Kiselev for the Russian translation.
Qing Mu for the simplified Chinese translation.
"Medwin" for checking technical terms and suggesting improvements for the simplified Chinese translation.
"fohristiwhirl" for the Gibo format parsing code.
Additional thanks to:
"Seventeen" for explanatory youtube videos in Korean.
+89 -52
View File
@@ -1,87 +1,124 @@
# KaTrain Installation
# Installation for MacOS users
* [Quick install guide for MacOS](#MacQuick)
* [Troubleshooting and installation from sources](#MacSources)
* [Quick install guide for Windows](#WindowsQuick)
* [Troubleshooting and installation from sources](#WindowsSources)
* [Quick install guide for Linux](#LinuxQuick)
* [Troubleshooting and installation from sources](#LinuxSources)
## <a name="MacPrereq"></a>Installation pre-requisites
* Install [Homebrew](https://brew.sh) by running the following command in terminal:
* ```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
```
* Install Python using [Homebrew](https://brew.sh/) by executing `brew install python3`
* Alternatively download and install [Python 3.7.5](https://www.python.org/downloads/release/python-375/)
* Install Katago using [Homebrew](https://brew.sh/) by executing `brew install katago`
* You can also follow instructions [here](https://github.com/lightvector/KataGo) to compile KataGo yourself.
* If you encounter SSL errors on downloading model files, you may need to follow [these](https://stackoverflow.com/questions/52805115/certificate-verify-failed-unable-to-get-local-issuer-certificate) instructions to fix your certificates.
## <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Apple_Logo.svg" alt="MacOs" height="35"/> Installation for MacOS users
## Installation and running KaTrain from PyPi
* Run `pip3 install katrain`
* Run the program by executing `katrain` in a terminal.
* If you see an error about initializing KataGo:
* Open the settings dialog by clicking on the gear icon at the bottom right of the window and change the path of the 'katago'
setting under 'engine' to `/usr/local/bin/katago`, or the path where you compiled KataGo.
### <a name="MacQuick"></a>Quick install guide
# Installation for Windows users
[Open a terminal](https://support.apple.com/guide/terminal/open-or-quit-terminal-apd5265185d-f365-44cb-8b09-71a064a42125/mac) and enter the following commands:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install python3
brew install katago
pip3 install katrain
```
Now you can start KaTrain by simply typing `katrain` in a terminal.
## Downloadable .exe files
These commands install [Homebrew](https://brew.sh), which simplifies installing packages,
followed by the programming language Python, the KataGo AI, and KaTrain itself.
You can find downloadable .exe files for windows [here](https://github.com/sanderland/katrain/releases). Simply download and run, everything is included.
### <a name="MacSources"></a>Troubleshooting and Installation from sources
## Installation from sources for Windows users
Installation from sources is essentially the same as for Linux, see [here](#LinuxSources),
note that you will still need to install your own KataGo, using brew or otherwise.
If you encounter SSL errors on downloading model files, you may need to follow [these](https://stackoverflow.com/questions/52805115/certificate-verify-failed-unable-to-get-local-issuer-certificate) instructions to fix your certificates.
## <img src="https://upload.wikimedia.org/wikipedia/commons/5/5f/Windows_logo_-_2012.svg" alt="Windows" height="35"/> Installation for Windows users
### <a name="WindowsQuick"></a>Quick install guide
You can find downloadable .exe files for windows [here](https://github.com/sanderland/katrain/releases).
Simply download and run, everything is included.
### <a name="WindowsSources"></a>Installation from sources
* Download the repository by clicking the green *Clone or download* on this page and *Download zip*. Extract the contents.
* Make sure you have a python installation, I will assume Anaconda (Python 3.7), available [here](https://www.anaconda.com/products/individual#download-section).
* Open 'Anaconda prompt' from the start menu and navigate to where you extracted the zip file using the `cd <folder>` command.
* Execute the command `pip3 install .`
* Start the app by running `katrain` in the directory where you downloaded the scripts.
* Start the app by running `katrain` in the command prompt.
# <a name="LinuxSources"></a>Installation from sources for Linux users
## <img src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Linux_Logo_in_Linux_Libertine_Font.svg" alt="Linux" height="35"/> Installation for Linux users
### <a name="LinuxQuick"></a>Quick install guide
If you have a working Python 3.6/3.7 available, you should be able to simply:
* Run `pip3 install katrain`
* Run the program by executing `katrain` in a terminal.
### <a name="LinuxSources"></a>Installation from sources
This section describes how to install KaTrain from source,
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.
* This assumed you have a working Python 3.6/3.7 installation as a default. If your default is python 2, use pip3/python3.
Kivy currently does not have a release for Python 3.8.
* Open a terminal.
* Run the command `git clone https://github.com/sanderland/katrain.git` to download the repository.
* Changing directory using `cd katrain`
* Run the command `pip3 install .` to install the package globally, or use `--user` to install locally, then run the program by typing `katrain` in the terminal.
* If you prefer not to install, run without installing using `python3 -m katrain` after installing the kivy dependencies mentioned in the next section.
* A binary for KataGo is included, but if you have compiled your own, point the 'engine/katago' setting to the relevant KataGo v1.4+ binary.
* Run the command `git clone https://github.com/sanderland/katrain.git` to download the repository and
change directory using `cd katrain`
* Run the command `pip3 install .` to install the package globally, or use `--user` to install locally.
* Run the program by typing `katrain` in the terminal.
* If you prefer not to install, run without installing using `python3 -m katrain` after installing the
dependencies from `requirements.txt`.
## Troubleshooting
A binary for KataGo is included, but if you have compiled your own, press F8 to open general settings and change the
KataGo executable path to the relevant KataGo v1.4+ binary.
Older linux machines may have trouble installing, you can try to manually install dependencies to resolve some issues.
### Troubleshooting and advanced installation from sources
The following packages may help resolve missing OS packages for Kivy or KataGo.
```
You can try to manually install dependencies to resolve some issues relating to missing dependencies,
e.g. the binary 'wheel' is not provided, KataGo is not starting, or sounds are not working.
You can also follow these instructions if you don't want to install KaTrain, and just run it locally.
First install the following packages, which are either required for building Kivy,
or may help resolve missing dependencies for Kivy or KataGo.
```bash
sudo apt-get install python3-pip build-essential git python3 python3-dev ffmpeg libsdl2-dev libsdl2-image-dev\
libsdl2-mixer-dev libsdl2-ttf-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev\
libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good\
pkg-config libgl-dev opencl-headers ocl-icd-opencl-dev python3-pygame
libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good libpulse\
pkg-config libgl-dev opencl-headers ocl-icd-opencl-dev
```
Then, try installing python package dependencies using:
```bash
pip3 install -r requirements.txt
pip3 install screeninfo # Skip on MacOS, not working
```
pip3 install -U cython wheel setuptools
pip3 install kivy==2.0.0rc2 kivymd==0.104.1
In case the sound is not working, or there is no available wheel for your OS or Python version, try:
```bash
pip3 uninstall kivy
pip3 install --no-binary kivy kivy==2.0.0rc2
```
You can now start KaTrain by running `python3 -m katrain`
In case KataGo does not start, an alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
# Configuring the GPU(s) KataGo uses
## Configuring the GPU(s) KataGo uses
In most cases KataGo detects your configuration correctly, automatically searching for OpenCL devices and select the highest scoring device.
However, if you have multiple GPUs or want to force a specific device you will need to edit the 'analysis_config.cfg' file in the KataGo folder.
To see what devices are available and which one KataGo is using. Look for the following lines in the terminal after starting KaTrain:
```
Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
```
The above devices were found on a 2019 MacBook Pro with both an on-motherboard graphics chip, and a separate AMD Radeon Pro video card.
As you can see it scores about twice as high as the Intel UHD chip and KataGo has selected
it as it's sole device. You can configure KataGo to use *both* the AMD and the Intel devices to get the best performance out of the system.
* Open the 'analysis_config.cfg' file in the KataGo folder.
* Open the 'analysis_config.cfg' file in the `katrain/KataGo` folder in your python packages, or local sources.
If you can't find it, turn on `debug_level=1` in general settings and look for the command that is used to start KataGo.
* Search for `numNNServerThreadsPerModel` (~line 75), uncomment the line by deleting the # and set the value to 2. The line should read `numNNServerThreadsPerModel = 2`.
* Search for `openclDeviceToUseThread` (~line 117), uncomment by deleting the # and set the values to the device ID numbers identified in the terminal.
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:
@@ -89,13 +126,13 @@ As you can see it scores about twice as high as the Intel UHD chip and KataGo ha
openclDeviceToUseThread0 = 1
openclDeviceToUseThread1 = 2
```
* Run `python3 -m katrain` and confirm that KataGo is now using both devices, by
* Run `katrain` and confirm that KataGo is now using both devices, by
checking the output from the terminal, which should indicate two devices being used. For example:
```
Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
Using OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) OpenCL 1.2
Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
Found 3 device(s) on platform 0 with type CPU or GPU or Accelerator
Found OpenCL Device 0: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (Intel) (score 102)
Found OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) (score 6000102)
Found OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) (score 11000102)
Using OpenCL Device 1: Intel(R) UHD Graphics 630 (Intel Inc.) OpenCL 1.2
Using OpenCL Device 2: AMD Radeon Pro 5500M Compute Engine (AMD) OpenCL 1.2
```
+45 -21
View File
@@ -1,5 +1,4 @@
# <a name="manual"></a> KaTrain v1.2
# <a name="manual"></a> KaTrain
[![Latest Release](https://img.shields.io/github/release/sanderland/katrain?label=download)](https://github.com/sanderland/katrain/releases)
[![License:MIT](https://img.shields.io/pypi/l/katrain)](https://en.wikipedia.org/wiki/MIT_License)
@@ -9,6 +8,19 @@
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/sanderbaduk)](https://liberapay.com/sanderbaduk/)
[![Discord](https://img.shields.io/discord/417022162348802048?logo=discord)](https://discord.com/channels/417022162348802048/629446365688365067)
* [Introduction](#intro)
* [Previews and YouTube tutorials](#preview)
* [Installation](#install)
* [Manual](#ai)
* [Play against AI](#ai)
* [Analyzing your Games](#analysis)
* [Keyboard shortcuts](#keyboard)
* [FAQ and Troubleshooting](#faq)
* [Contributing](#support)
## <a name="intro"></a> Introduction
KaTrain is a tool for analyzing and playing go with AI feedback from KataGo.
The original idea was to give immediate feedback on the many large mistakes we make in terms of inefficient moves,
@@ -20,7 +32,7 @@ but has since grown to include a wide range of features, including:
* Play against a stronger player and use the retry option instead of handicap stones.
* Automatically generate focused SGF reviews which show your biggest mistakes.
## Previews and Youtube Videos
## <a name="preview"></a> Previews and Youtube Videos
### Play against an AI Teacher
@@ -36,16 +48,13 @@ but has since grown to include a wide range of features, including:
|:-----------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:|
| [![New Features Video](https://i.imgur.com/gCY6hMH.png)](https://www.youtube.com/watch?v=wFl4Bab_eGM) | [![ Teaching Game Tutorial](https://i.imgur.com/jAdcSL5.png)](https://www.youtube.com/watch?v=wFl4Bab_eGM) |
## Installation
## <a name="install"></a> Installation
* See the [releases tab](https://github.com/sanderland/katrain/releases) for pre-built installers for windows.
* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any 64-bit OS.
* Note that on MacOS you will need to set up KataGo using brew, as described [here](INSTALL.md).
* See [here](INSTALL.md#MacPrereq) for detailed instructions for running from source files on Window, Linux and MacOS,
as well as setting up KataGo to use multiple GPUs.
* See [here](INSTALL.md) for detailed instructions for Window, Linux and MacOS,
as well as troubleshooting and setting up KataGo to use multiple GPUs.
## Play against AI
## <a name="ai"></a> Play against AI
* 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.
@@ -67,13 +76,19 @@ on the board, or not output details for them in SGFs,you can do so under 'Config
This section describes the available AIs, with strength based on their current OGS rankings using the default settings.
In the 'AI settings', settings which have been tested and calibrated are at the top and have a lighter color,
changing these will show an estimate of rank.
This estimate should be reasonably accurate as long as you have not changed the other settings.
* Recommended options for serious play include:
* **[9p+]** **KataGo** is full KataGo, above professional level. The analysis and feedback given is always based on this full strength KataGo AI.
* **[15k - 3d]** **Calibrated Rank Bot** was calibrated on various bots (e.g. GnuGo and Pachi at different strength settings) to play a balanced game from the opening to the endgame without making serious (DDK) blunders. Further discussion can be found on [this](https://github.com/sanderland/katrain/issues/44) thread.
* **[~5k]** **ScoreLoss** is KataGo analyzing as usual, but
* **[15k - 3d]** **Calibrated Rank Bot** was calibrated on various bots (e.g. GnuGo and Pachi at different strength settings) to play a balanced
game from the opening to the endgame without making serious (DDK) blunders. Further discussion can be found
[here](https://github.com/sanderland/katrain/issues/44) and [here](https://github.com/sanderland/katrain/issues/74).
* **[8k - 4d+]** **ScoreLoss** is KataGo analyzing as usual, but
choosing from potential moves depending on the expected score loss, leading to a varied style with mostly small mistakes.
* **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading).
* **[~4k]** **Policy Weighted** picks a random move weighted by the policy, leading to a varied style with mostly small mistakes, and occasional blunders due to a lack of reading.
* **[~5d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading).
* **[12k - 2d]** **Policy Weighted** picks a random move weighted by the policy, leading to a varied style with mostly small mistakes, and occasional blunders due to a lack of reading.
* **[~8k]** **Blinded Policy** picks a number of moves at random and play the best move among them, being effectively 'blind' to part of the board each turn.
* Options that are more on the 'fun and experimental' side include:
* Variants of **Blinded Policy**, which use the same basic strategy, but with a twist.:
@@ -88,7 +103,9 @@ The Engine based AIs (KataGo, ScoreLoss, KataJigo) are affected by both the mode
Further technical details and discussion on these AIs can be found on [this](https://lifein19x19.com/viewtopic.php?f=10&t=17488&sid=b11e42c005bb6f4f48c83771e6a27eff) thread at the life in 19x19 forums.
## Analysis
## <a name="analysis"></a> Analysis
Analysis options in KaTrain allow you to explore variations and request more in-depth analysis from the engine at any point in the game.
Keyboard shortcuts are shown with **[key]**.
@@ -110,27 +127,34 @@ Keyboard shortcuts are shown with **[key]**.
* **[a]**: Deeper analysis: Re-evaluate the position using more visits, usually resulting in a more accurate evaluation.
* **[s]**: Equalize visits: Re-evaluate all currently shown next moves with the same visits as the current top move. Useful to increase confidence in the suggestions with high uncertainty.
* **[d]**: Analyze all moves: Evaluate all possible next moves. This can take a bit of time even though 'fast_visits' is used, but can be useful to see how many reasonable next moves are available.
* **[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.
* **[enter]** AI move. Makes the AI move for the current player regardless of current player selection.
## Keyboard and mouse shortcuts
### Rank Estimation
A new feature in v1.3 is the rank estimation panel. This adds an extra graph which uses a statistical model to estimate the playing strength
of both players for every 80 move segment. It can be used for determining which part of your game needs extra attention,
but keep in mind the estimation is based purely on how well moves correspond to the 'shape sense' of KataGo and can be very inaccurate at times.
## <a name="keyboard"></a> Keyboard and mouse shortcuts
In addition to shortcuts mentioned above and those shown in the main menu:
* **[Shift]**: Open the main menu.
* **[~]** or **[ ` ]** or **[m]**: Cycles through more minimalistic UI modes.
* **[p]**: Pass
* **[spacebar]**: Pause/Resume timer
* **[b]**: Pause/Resume timer
* **[arrow left]** or **[z]**: Undo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
* **[arrow right]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
* **[arrow up/down]** Switch branch, as would be expected from the move tree.
* **[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 that point in the game.
* **[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-C]**: Save SGF to clipboard.
## FAQ
## <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.
@@ -145,9 +169,9 @@ In addition to shortcuts mentioned above and those shown in the main menu:
## <a name="support"></a> Support / Contribute
[![GitHub issues](https://img.shields.io/github/issues/sanderland/katrain)](https://github.com/sanderland/katrain/issues)
[![Contributors](https://img.shields.io/static/v1?label=contributors&message=12&color=dcb424)](CONTRIBUTORS)
[![Contributors](https://img.shields.io/static/v1?label=contributors&message=16&color=dcb424)](CONTRIBUTIONS.md)
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/sanderbaduk)](https://liberapay.com/sanderbaduk/)
[![Github sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=dcb424&link=https://github.com/sponsors/sanderland/)](https://github.com/sponsors/sanderland)
[![Github sponsors](https://img.shields.io/static/v1?label=Sponsor&message=2&logo=GitHub&color=dcb424&link=https://github.com/sponsors/sanderland/)](https://github.com/sponsors/sanderland)
* Ideas, feedback, and contributions to code or translations are all very welcome.
* For suggestions and planned improvements, see [open issues](https://github.com/sanderland/katrain/issues) on github to check if the functionality is already planned.
+32 -6
View File
@@ -1,6 +1,7 @@
import os
import re
import sys
import copy
from collections import defaultdict
import polib
@@ -27,14 +28,13 @@ for lang in locales:
for entry in po[lang].translated_entries():
if "TODO" in entry.comment:
todos[lang].append(entry)
else:
strings_to_langs[entry.msgid][lang] = entry.msgstr
strings_to_langs[entry.msgid][lang] = entry
strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
if entry.msgid in lang_to_strings[lang]:
print("duplicate", entry.msgid, "in", lang)
errors = True
lang_to_strings[lang].add(entry.msgid)
if todos[lang]:
if todos[lang] and any("todo" in a for a in sys.argv):
print(f"========== {lang} has {len(todos[lang])} TODO entries ========== ")
for item in todos[lang]:
print(item)
@@ -60,14 +60,40 @@ for lang in locales:
print("Message id", msgid, "found as ", strings_to_langs[msgid], "but missing in default", DEFAULT_LANG)
errors = True
elif DEFAULT_LANG in strings_to_langs[msgid]:
copied_msg = strings_to_langs[msgid][DEFAULT_LANG]
copied_entry = copy.copy(strings_to_langs[msgid][DEFAULT_LANG])
print("Message id", msgid, "missing in ", lang, "-> Adding it from", DEFAULT_LANG)
entry = polib.POEntry(msgid=msgid, msgstr=copied_msg, comment="TODO")
po[lang].append(entry)
if copied_entry.comment:
copied_entry.comment = f"TODO - {copied_entry.comment}"
else:
copied_entry.comment = "TODO"
po[lang].append(copied_entry)
errors = True
else:
print(f"MISSING IN DEFAULT AND {lang}", strings_to_langs[msgid])
errors = True
for msgid, lang_entries in strings_to_langs.items():
if lang in lang_entries and "TODO" in lang_entries[lang].comment:
if any(e.msgstr == lang_entries[lang].msgstr for l, e in lang_entries.items() if l != lang):
todo_comment = (
f"TODO - {lang_entries[DEFAULT_LANG].comment}" if lang_entries[DEFAULT_LANG].comment else "TODO"
) # update todo
if (
lang_entries[lang].msgstr != lang_entries[DEFAULT_LANG].msgstr
or lang_entries[lang].comment.replace("\n", " ") != todo_comment
):
print(
[
lang_entries[lang].msgstr,
lang_entries[DEFAULT_LANG].msgstr,
lang_entries[lang].comment,
todo_comment,
]
)
lang_entries[lang].msgstr = lang_entries[DEFAULT_LANG].msgstr # update
lang_entries[lang].comment = todo_comment
print(f"{lang}/{msgid} todo entry updated")
po[lang].save(pofile[lang])
mofile = pofile[lang].replace(".po", ".mo")
po[lang].save_as_mofile(mofile)
+63 -32
View File
@@ -3,7 +3,8 @@
import os
os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning")
os.environ["KIVY_AUDIO"] = "sdl2" # force working audio
if "KIVY_AUDIO" not in os.environ:
os.environ["KIVY_AUDIO"] = "sdl2" # seems to be most stable / some players hard crash
# next, icon
from katrain.core.utils import find_package_resource, PATHS
@@ -58,6 +59,10 @@ from katrain.core.constants import (
MODE_ANALYZE,
HOMEPAGE,
VERSION,
STATUS_ERROR,
STATUS_INFO,
PLAYING_NORMAL,
PLAYER_HUMAN,
)
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup
from katrain.core.base_katrain import KaTrainBase
@@ -65,11 +70,9 @@ from katrain.core.engine import KataGoEngine
from katrain.core.game import Game, IllegalMoveException, KaTrainSGF
from katrain.core.sgf_parser import Move, ParseError
from katrain.gui.kivyutils import *
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, AIPopup
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, ConfigAIPopup
from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL, LIGHTGREY
from katrain.gui.widgets.graph import ScoreGraph
from katrain.gui.widgets.movetree import MoveTree
from katrain.gui.widgets.filebrowser import I18NFileBrowser
from katrain.gui.widgets import *
from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget
from katrain.gui.controlspanel import ControlsPanel
@@ -91,6 +94,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.teacher_settings_popup = None
self.timer_settings_popup = None
self.idle_analysis = False
self.message_queue = Queue()
self._keyboard = Window.request_keyboard(None, self, "")
@@ -100,21 +104,30 @@ class KaTrainGui(Screen, KaTrainBase):
super().log(message, level)
if level == OUTPUT_KATAGO_STDERR and "ERROR" not in self.controls.status.text:
if "starting" in message.lower():
self.controls.set_status(f"KataGo engine starting...")
self.controls.set_status(f"KataGo engine starting...", STATUS_INFO)
if message.startswith("Tuning"):
self.controls.set_status(f"KataGo is tuning settings for first startup, please wait." + message)
self.controls.set_status(
f"KataGo is tuning settings for first startup, please wait." + message, STATUS_INFO
)
return
if "ready" in message.lower():
self.controls.set_status(f"KataGo engine ready.")
if (level == OUTPUT_ERROR or (level == OUTPUT_KATAGO_STDERR and "error" in message.lower())) and getattr(
self, "controls", None
):
self.controls.set_status(f"ERROR: {message}")
self.controls.set_status(f"KataGo engine ready.", STATUS_INFO)
if (
level == OUTPUT_ERROR
or (level == OUTPUT_KATAGO_STDERR and "error" in message.lower() and "tuning" not in message.lower())
) and getattr(self, "controls", None):
self.controls.set_status(f"ERROR: {message}", STATUS_ERROR)
@property
def play_analyze_mode(self):
return self.play_mode.mode
def toggle_continuous_analysis(self):
self.idle_analysis = not self.idle_analysis
if not self.idle_analysis:
self.controls.set_status("", STATUS_INFO)
self.update_state()
def start(self):
if self.engine:
return
@@ -158,7 +171,10 @@ class KaTrainGui(Screen, KaTrainBase):
# update move tree
self.controls.move_tree.current_node = self.game.current_node
def update_state(
def update_state(self, redraw_board=False): # redirect to message queue thread
self("update_state", redraw_board=redraw_board)
def _do_update_state(
self, redraw_board=False
): # is called after every message and on receiving analyses and config changes
# AI and Trainer/auto-undo handlers
@@ -184,7 +200,10 @@ class KaTrainGui(Screen, KaTrainBase):
and not (teaching_undo and cn.auto_undo is None)
): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
self._do_ai_move(cn)
Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1)
Clock.schedule_once(self.board_gui.play_stone_sound, 0)
if len(self.engine.queries) == 0 and self.idle_analysis:
self("analyze-extra", "extra", continuous=True)
Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger?
def update_player(self, bw, **kwargs):
super().update_player(bw, **kwargs)
@@ -199,7 +218,7 @@ class KaTrainGui(Screen, KaTrainBase):
def _message_loop_thread(self):
while True:
game, msg, *args = self.message_queue.get()
game, msg, args, kwargs = self.message_queue.get()
try:
self.log(f"Message Loop Received {msg}: {args} for Game {game}", OUTPUT_EXTRA_DEBUG)
if game != self.game.game_id:
@@ -208,28 +227,39 @@ class KaTrainGui(Screen, KaTrainBase):
)
continue
fn = getattr(self, f"_do_{msg.replace('-','_')}")
fn(*args)
self.update_state()
fn(*args, **kwargs)
if msg != "update_state":
self._do_update_state()
except Exception as exc:
self.log(f"Exception in processing message {msg} {args}: {exc}", OUTPUT_ERROR)
traceback.print_exc()
def __call__(self, message, *args):
def __call__(self, message, *args, **kwargs):
if self.game:
if message.endswith("popup"): # gui code needs to run in main kivy thread.
fn = getattr(self, f"_do_{message.replace('-', '_')}")
Clock.schedule_once(lambda _dt: fn(*args), -1)
Clock.schedule_once(lambda _dt: fn(*args, **kwargs), -1)
else: # game related actions
self.message_queue.put([self.game.game_id, message, *args])
self.message_queue.put([self.game.game_id, message, args, kwargs])
def _do_new_game(self, move_tree=None, analyze_fast=False):
self.idle_analysis = False
mode = self.play_analyze_mode
if (move_tree is not None and mode == MODE_PLAY) or (move_tree is None and mode == MODE_ANALYZE):
self.play_mode.switch_ui_mode() # for new game, go to play, for loaded, analyze
self.board_gui.animating_pv = None
self.engine.on_new_game() # clear queries
self.game = Game(self, self.engine, move_tree=move_tree, analyze_fast=analyze_fast)
if move_tree:
for bw, player_info in self.players_info.items():
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
player_info.name = move_tree.root.get_property("P" + bw)
self.update_player(bw)
self.controls.graph.initialize_from_game(self.game.root)
self.controls.rank_graph.initialize_from_game(self.game.root)
self.update_state(redraw_board=True)
def _do_ai_move(self, node=None):
@@ -266,10 +296,10 @@ class KaTrainGui(Screen, KaTrainBase):
try:
self.game.play(Move(coords, player=self.next_player_info.player))
except IllegalMoveException as e:
self.controls.set_status(f"Illegal Move: {str(e)}")
self.controls.set_status(f"Illegal Move: {str(e)}", STATUS_ERROR)
def _do_analyze_extra(self, mode):
self.game.analyze_extra(mode)
def _do_analyze_extra(self, mode, **kwargs):
self.game.analyze_extra(mode, **kwargs)
def _do_new_game_popup(self):
self.controls.timer.paused = True
@@ -311,7 +341,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True
if not self.ai_settings_popup:
self.ai_settings_popup = I18NPopup(
title_key="ai settings", size=[dp(600), dp(600)], content=AIPopup(self)
title_key="ai settings", size=[dp(600), dp(650)], content=ConfigAIPopup(self)
).__self__
self.ai_settings_popup.content.popup = self.ai_settings_popup
self.ai_settings_popup.open()
@@ -352,15 +382,15 @@ class KaTrainGui(Screen, KaTrainBase):
def _do_output_sgf(self):
msg = self.game.write_sgf(self.config("general/sgf_save"))
self.log(msg, OUTPUT_INFO)
self.controls.set_status(msg)
self.controls.set_status(msg, OUTPUT_INFO)
def load_sgf_from_clipboard(self):
clipboard = Clipboard.paste()
if not clipboard:
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.")
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_ERROR)
return
try:
move_tree = KaTrainSGF.parse(clipboard)
move_tree = KaTrainSGF.parse_sgf(clipboard)
except Exception as exc:
self.controls.set_status(
i18n._("Failed to import from clipboard").format(error=exc, contents=clipboard[:50])
@@ -431,8 +461,10 @@ class KaTrainGui(Screen, KaTrainBase):
elif keycode[1] == "shift":
self.nav_drawer.set_state("toggle")
elif keycode[1] == "spacebar":
self.toggle_continuous_analysis()
elif keycode[1] == "b" and "ctrl" not in modifiers:
self.controls.timer.paused = not self.controls.timer.paused
elif keycode[1] in ["`", "~", "m"]:
elif keycode[1] in ["`", "~", "m"] and "ctrl" not in modifiers:
self.zen = (self.zen + 1) % 3
elif keycode[1] in ["left", "z"]:
self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
@@ -446,7 +478,7 @@ class KaTrainGui(Screen, KaTrainBase):
self("output-sgf")
elif keycode[1] == "c" and "ctrl" in modifiers:
Clipboard.copy(self.game.root.sgf())
self.controls.set_status(i18n._("Copied SGF to clipboard."))
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
elif keycode[1] == "v" and "ctrl" in modifiers:
self.load_sgf_from_clipboard()
elif keycode[1] in shortcuts.keys() and "ctrl" not in modifiers:
@@ -476,11 +508,10 @@ class KaTrainApp(MDApp):
kv_file = find_package_resource("katrain/gui.kv")
popup_kv_file = find_package_resource("katrain/popups.kv")
resource_add_path(PATHS["PACKAGE"])
Builder.load_file(kv_file)
Window.bind(on_request_close=self.on_request_close)
Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file))
Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file.decode("utf8")))
self.gui = KaTrainGui()
Builder.load_file(popup_kv_file)
@@ -493,7 +524,7 @@ class KaTrainApp(MDApp):
self.gui.save_config()
if self.gui.game:
self.gui.update_state()
self.gui.controls.set_status("")
self.gui.controls.set_status("", STATUS_INFO)
def webbrowser(self, site_key):
websites = {"homepage": HOMEPAGE + "#manual", "support": HOMEPAGE + "#support"}
+12 -8
View File
@@ -16,7 +16,7 @@
"anim_pv_time": 0.5,
"debug_level": 0,
"lang": "en",
"version": "1.2.2"
"version": "1.3.0"
},
"timer": {
"byo_length": 30,
@@ -69,24 +69,28 @@
},
"ai": {
"ai:default": {},
"ai:handicap": {
"automatic": true,
"pda": 0
},
"ai:jigo": {
"target_score": 0.5
},
"ai:scoreloss": {
"strength": 0.5
"strength": 0.2
},
"ai:policy": {
"opening_moves": 22.0
},
"ai:p:weighted": {
"weaken_fac": 1.25,
"pick_override": 1.0,
"lower_bound": 0.001,
"weaken_fac": 1.25
"lower_bound": 0.001
},
"ai:p:pick": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.33
"pick_frac": 0.35
},
"ai:p:local": {
"pick_override": 0.95,
@@ -99,13 +103,13 @@
"pick_override": 0.85,
"stddev": 7.5,
"pick_n": 5,
"pick_frac": 0.5,
"pick_frac": 0.4,
"endgame": 0.45
},
"ai:p:influence": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.4,
"pick_frac": 0.3,
"threshold": 3.5,
"line_weight": 10,
"endgame": 0.4
@@ -113,7 +117,7 @@
"ai:p:territory": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.4,
"pick_frac": 0.3,
"threshold": 3.5,
"line_weight": 2,
"endgame": 0.4
+164 -22
View File
@@ -2,27 +2,91 @@ import heapq
import math
import random
import time
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
from katrain.core.utils import var_to_grid
from katrain.core.constants import (
OUTPUT_INFO,
OUTPUT_DEBUG,
AI_STRATEGIES_POLICY,
AI_POLICY,
AI_WEIGHTED,
AI_STRATEGIES_PICK,
AI_JIGO,
AI_SCORELOSS,
AI_DEFAULT,
AI_HANDICAP,
AI_INFLUENCE,
AI_JIGO,
AI_LOCAL,
AI_PICK,
AI_POLICY,
AI_RANK,
AI_SCORELOSS,
AI_STRATEGIES_PICK,
AI_STRATEGIES_POLICY,
AI_STRENGTH,
AI_TENUKI,
AI_TERRITORY,
AI_PICK,
AI_RANK,
AI_WEIGHTED,
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_INFO,
AI_WEIGHTED_ELO,
AI_SCORELOSS_ELO,
CALIBRATED_RANK_ELO,
AI_LOCAL_ELO_GRID,
AI_TENUKI_ELO_GRID,
AI_TERRITORY_ELO_GRID,
AI_INFLUENCE_ELO_GRID,
AI_PICK_ELO_GRID,
)
from katrain.core.game import Game, GameNode, Move
from katrain.core.utils import var_to_grid
def interp_ix(lst, x):
i = 0
while i + 1 < len(lst) - 1 and lst[i + 1] < x:
i += 1
t = max(0, min(1, (x - lst[i]) / (lst[i + 1] - lst[i])))
return i, t
def interp1d(lst, x):
xs, ys = zip(*lst)
i, t = interp_ix(xs, x)
return (1 - t) * ys[i] + t * ys[i + 1]
def interp2d(gridspec, x, y):
xs, ys, matrix = gridspec
i, t = interp_ix(xs, x)
j, s = interp_ix(ys, y)
return (
matrix[j][i] * (1 - t) * (1 - s)
+ matrix[j][i + 1] * t * (1 - s)
+ matrix[j + 1][i] * (1 - t) * s
+ matrix[j + 1][i + 1] * t * s
)
def ai_rank_estimation(strategy, settings) -> int:
if strategy in [AI_DEFAULT, AI_HANDICAP, AI_JIGO]:
return 9
if strategy == AI_RANK:
return 1 - settings["kyu_rank"]
if strategy in [AI_WEIGHTED, AI_SCORELOSS, AI_LOCAL, AI_TENUKI, AI_TERRITORY, AI_INFLUENCE, AI_PICK]:
if strategy == AI_WEIGHTED:
elo = interp1d(AI_WEIGHTED_ELO, settings["weaken_fac"])
if strategy == AI_SCORELOSS:
elo = interp1d(AI_SCORELOSS_ELO, settings["strength"])
if strategy == AI_PICK:
elo = interp2d(AI_PICK_ELO_GRID, settings["pick_frac"], settings["pick_n"])
if strategy == AI_LOCAL:
elo = interp2d(AI_LOCAL_ELO_GRID, settings["pick_frac"], settings["pick_n"])
if strategy == AI_TENUKI:
elo = interp2d(AI_TENUKI_ELO_GRID, settings["pick_frac"], settings["pick_n"])
if strategy == AI_TERRITORY:
elo = interp2d(AI_TERRITORY_ELO_GRID, settings["pick_frac"], settings["pick_n"])
if strategy == AI_INFLUENCE:
elo = interp2d(AI_INFLUENCE_ELO_GRID, settings["pick_frac"], settings["pick_n"])
kyu = interp1d(CALIBRATED_RANK_ELO, elo)
return 1 - kyu
else:
return AI_STRENGTH[strategy]
def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]:
@@ -48,11 +112,12 @@ def policy_weighted_move(policy_moves, lower_bound, weaken_fac):
]
if weighted_coords:
top = weighted_selection_without_replacement(weighted_coords, 1)[0]
ai_thoughts = f"Playing policy-weighted random move {top[2].gtp()} ({top[0]:.1%}) from {len(weighted_coords)} moves above lower_bound of {lower_bound:.1%}."
move = top[2]
ai_thoughts = f"Playing policy-weighted random move {move.gtp()} ({top[0]:.1%}) from {len(weighted_coords)} moves above lower_bound of {lower_bound:.1%}."
else:
top = policy_moves[0]
move = policy_moves[0][1]
ai_thoughts = f"Playing top policy move because no non-pass move > above lower_bound of {lower_bound:.1%}."
return top[2], ai_thoughts
return move, ai_thoughts
def generate_influence_territory_weights(ai_mode, ai_settings, policy_grid, size):
@@ -93,8 +158,51 @@ def generate_local_tenuki_weights(ai_mode, ai_settings, policy_grid, cn, size):
return weighted_coords, ai_thoughts
def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optional[Dict]:
error = False
analysis = None
def set_analysis(a):
nonlocal analysis
analysis = a
def set_error(a):
nonlocal error
game.katrain.log("Error in PDA-based analysis", a)
error = True
engine = game.engines[cn.player]
engine.request_analysis(
cn,
callback=set_analysis,
error_callback=set_error,
priority=1_000,
ownership=False,
extra_settings=extra_settings,
)
while not (error or analysis):
time.sleep(0.01)
engine.check_alive(exception_if_dead=True)
return analysis
def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]:
cn = game.current_node
if ai_mode == AI_HANDICAP:
pda = ai_settings["pda"]
if ai_settings["automatic"]:
n_handicaps = len(game.root.get_list_property("AB", []))
MOVE_VALUE = 14 # could be rules dependent
b_stones_advantage = max(n_handicaps - 1, 0) - (cn.komi - MOVE_VALUE / 2) / MOVE_VALUE
pda = min(3, max(-3, -b_stones_advantage * (3 / 8))) # max PDA at 8 stone adv, normal 9 stone game is 8.46
handicap_analysis = request_ai_analysis(
game, cn, {"playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK"}
)
if not handicap_analysis:
game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT
while not cn.analysis_ready:
time.sleep(0.01)
game.engines[cn.next_player].check_alive(exception_if_dead=True)
@@ -128,12 +236,19 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
board_squares = size[0] * size[1]
if ai_mode == AI_RANK: # calibrated, override from 0.8 at start to ~0.4 at full board
override = 0.8 * (1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares)
overridetwo = 0.85 + max(0, 0.02 * (ai_settings["kyu_rank"] - 8))
else:
override = ai_settings["pick_override"]
overridetwo = 1.0
if policy_moves[0][0] > override:
aimove = top_policy_move
ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies."
elif policy_moves[0][0] + policy_moves[1][0] > overridetwo:
aimove = top_policy_move
ai_thoughts += (
f"Top two policy moves have cumulative weight > {overridetwo:.1%}, so overriding other strategies."
)
elif ai_mode == AI_WEIGHTED:
aimove, ai_thoughts = policy_weighted_move(
policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"]
@@ -141,9 +256,30 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
elif ai_mode in AI_STRATEGIES_PICK:
if ai_mode != AI_RANK:
n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])
n_moves = max(1, int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"]))
else:
n_moves = int(round(board_squares / 361 * 10 ** (-0.05737 * ai_settings["kyu_rank"] + 1.9482)))
orig_calib_avemodrank = 0.063015 + 0.7624 * board_squares / (
10 ** (-0.05737 * ai_settings["kyu_rank"] + 1.9482)
)
norm_leg_moves = len(legal_policy_moves) / board_squares
modified_calib_avemodrank = (
0.3931
+ 0.6559
* norm_leg_moves
* math.exp(
-1
* (
3.002 * norm_leg_moves * norm_leg_moves
- norm_leg_moves
- 0.034889 * ai_settings["kyu_rank"]
- 0.5097
)
** 2
)
- 0.01093 * ai_settings["kyu_rank"]
) * orig_calib_avemodrank
n_moves = board_squares * norm_leg_moves / (1.31165 * (modified_calib_avemodrank + 1) - 0.082653)
n_moves = max(1, round(n_moves))
if ai_mode in [AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI]:
if cn.depth > ai_settings["endgame"] * board_squares:
@@ -151,7 +287,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
x_ai_thoughts = (
f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. "
)
n_moves = int(max(n_moves, 0.5 * len(legal_policy_moves)))
n_moves = int(max(n_moves, len(legal_policy_moves) // 2))
elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]:
weighted_coords, x_ai_thoughts = generate_influence_territory_weights(
ai_mode, ai_settings, policy_grid, size
@@ -188,10 +324,13 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
raise ValueError(f"Unknown Policy-based AI mode {ai_mode}")
else: # Engine based move
candidate_ai_moves = cn.candidate_moves
if ai_mode == AI_HANDICAP:
candidate_ai_moves = handicap_analysis["moveInfos"]
top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
if top_cand.is_pass: # don't play suicidal to balance score - pass when it's best
if top_cand.is_pass and ai_mode not in [AI_DEFAULT, AI_HANDICAP]: # don't play suicidal to balance score
aimove = top_cand
ai_thoughts += f"Top move is pass, so passing regardless of strategy."
ai_thoughts += f"Top move is pass, so passing regardless of strategy. "
else:
if ai_mode == AI_JIGO:
sign = cn.player_sign(cn.next_player)
@@ -214,11 +353,14 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
aimove = topmove[2]
ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights."
else:
if ai_mode != AI_DEFAULT:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP]:
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
aimove = top_cand
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
if ai_mode == AI_HANDICAP:
ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}"
else:
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG)
played_node = game.play(aimove)
played_node.ai_thoughts = ai_thoughts
+11
View File
@@ -5,6 +5,7 @@ import sys
from kivy import Config
from kivy.storage.jsonstore import JsonStore
from katrain.core.ai import ai_rank_estimation
from katrain.core.constants import *
from katrain.core.utils import find_package_resource
@@ -12,6 +13,9 @@ from katrain.core.utils import find_package_resource
class Player:
def __init__(self, player="B", player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL, periods_used=0):
self.player = player
self.sgf_rank = None
self.calculated_rank = None
self.name = ""
self.update(player_type, player_subtype)
self.periods_used = periods_used
@@ -132,6 +136,13 @@ class KaTrainBase:
def update_player(self, bw, **kwargs):
self.players_info[bw].update(**kwargs)
self.update_calculated_ranks()
def update_calculated_ranks(self):
for bw, player_info in self.players_info.items():
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)
def reset_players(self):
self.update_player("B")
+157 -21
View File
@@ -1,6 +1,17 @@
VERSION = "1.2.2"
VERSION = "1.3.0"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.2.0"
CONFIG_MIN_VERSION = "1.3.0" # keep config files from this version
OUTPUT_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5
OUTPUT_INFO = 0
OUTPUT_DEBUG = 1
OUTPUT_EXTRA_DEBUG = 2
STATUS_ANALYSIS = 1.0 # same priority for analysis/info
STATUS_INFO = 1.1
STATUS_TEACHING = 2.0
STATUS_ERROR = 1000.0
PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai"
PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI]
@@ -11,6 +22,7 @@ GAME_TYPES = [PLAYING_NORMAL, PLAYING_TEACHING]
MODE_PLAY, MODE_ANALYZE = "play", "analyze"
AI_DEFAULT = "ai:default"
AI_HANDICAP = "ai:handicap"
AI_SCORELOSS = "ai:scoreloss"
AI_WEIGHTED = "ai:p:weighted"
AI_JIGO = "ai:jigo"
@@ -24,13 +36,14 @@ AI_RANK = "ai:p:rank"
AI_CONFIG_DEFAULT = AI_SCORELOSS
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_SCORELOSS, AI_JIGO]
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_JIGO]
AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK]
AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK
AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY
AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_DEFAULT,
AI_RANK,
AI_HANDICAP,
AI_SCORELOSS,
AI_POLICY,
AI_WEIGHTED,
@@ -42,24 +55,147 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_JIGO,
]
AI_STRENGTH = {
AI_DEFAULT: "9d",
AI_POLICY: "4d",
AI_JIGO: "?d",
AI_SCORELOSS: "5k",
AI_WEIGHTED: "5k",
AI_PICK: "8k",
AI_LOCAL: "5k",
AI_TENUKI: "8k",
AI_INFLUENCE: "8k",
AI_TERRITORY: "5k",
AI_RANK: "15k - 3d",
AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some?
AI_DEFAULT: 9,
AI_POLICY: 5,
AI_JIGO: float("nan"),
AI_SCORELOSS: -4,
AI_WEIGHTED: -4,
AI_PICK: -7,
AI_LOCAL: -4,
AI_TENUKI: -7,
AI_INFLUENCE: -7,
AI_TERRITORY: -7,
AI_RANK: float("nan"),
}
AI_OPTION_VALUES = {
"kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)]
+ [(k, f"{1-k}[strength:dan]") for k in range(0, -3, -1)],
"strength": [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 1],
"opening_moves": range(0, 51),
"pick_override": [0, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 0.99, 1],
"lower_bound": [(v, f"{v:.2%}") for v in [0, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05]],
"weaken_fac": [x / 20 for x in range(10, 3 * 20 + 1)],
"endgame": [x / 100 for x in range(10, 80, 5)],
"pick_frac": [x / 100 for x in range(0, 101, 5)],
"pick_n": range(0, 26),
"stddev": [x / 2 for x in range(21)],
"line_weight": range(0, 11),
"threshold": [2, 2.5, 3, 3.5, 4, 4.5],
"automatic": "bool",
"pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)],
}
AI_KEY_PROPERTIES = {"kyu_rank", "strength", "weaken_fac", "pick_frac", "pick_n", "automatic"}
OUTPUT_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5
OUTPUT_INFO = 0
OUTPUT_DEBUG = 1
OUTPUT_EXTRA_DEBUG = 2
CALIBRATED_RANK_ELO = [
(-21.679482223451032, 18),
(42.60243194422105, 17),
(106.88434611189314, 16),
(171.16626027956522, 15),
(235.44817444723742, 14),
(299.7300886149095, 13),
(364.0120027825817, 12),
(428.2939169502538, 11),
(492.5758311179259, 10),
(556.8577452855981, 9),
(621.1396594532702, 8),
(685.4215736209424, 7),
(749.7034877886144, 6),
(813.9854019562865, 5),
(878.2673161239586, 4),
(942.5492302916308, 3),
(1006.8311444593029, 2),
(1071.113058626975, 1),
(1135.3949727946472, 0),
(1199.6768869623193, -1),
(1263.9588011299913, -2),
(1700, -4),
]
AI_WEIGHTED_ELO = [
(0.5, 1591.5718897531551),
(1.0, 1269.9896556526198),
(1.25, 1042.25179764667),
(1.5, 848.9410084463602),
(1.75, 630.1483212024823),
(2, 575.3637091858013),
(2.5, 410.9747543504796),
(3.0, 219.8667371799533),
]
AI_SCORELOSS_ELO = [
(0.0, 539),
(0.05, 625),
(0.1, 859),
(0.2, 1035),
(0.3, 1201),
(0.4, 1299),
(0.5, 1346),
(0.75, 1374),
(1.0, 1386),
]
AI_LOCAL_ELO_GRID = [
[0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
[0, 5, 10, 15, 25, 50],
[
[-204.0, 791.0, 1154.0, 1372.0, 1402.0, 1473.0, 1700.0, 1700.0],
[174.0, 1094.0, 1191.0, 1384.0, 1435.0, 1522.0, 1700.0, 1700.0],
[619.0, 1155.0, 1323.0, 1390.0, 1450.0, 1558.0, 1700.0, 1700.0],
[975.0, 1289.0, 1332.0, 1401.0, 1461.0, 1575.0, 1700.0, 1700.0],
[1344.0, 1348.0, 1358.0, 1467.0, 1477.0, 1616.0, 1700.0, 1700.0],
[1425.0, 1474.0, 1489.0, 1524.0, 1571.0, 1700.0, 1700.0, 1700.0],
],
]
AI_TENUKI_ELO_GRID = [
[0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
[0, 5, 10, 15, 25, 50],
[
[47.0, 335.0, 530.0, 678.0, 830.0, 1070.0, 1376.0, 1700.0],
[99.0, 469.0, 546.0, 707.0, 855.0, 1090.0, 1413.0, 1700.0],
[327.0, 513.0, 605.0, 745.0, 875.0, 1110.0, 1424.0, 1700.0],
[429.0, 519.0, 620.0, 754.0, 900.0, 1130.0, 1435.0, 1700.0],
[492.0, 607.0, 682.0, 797.0, 1000.0, 1208.0, 1454.0, 1700.0],
[778.0, 830.0, 909.0, 949.0, 1169.0, 1461.0, 1483.0, 1700.0],
],
]
AI_TERRITORY_ELO_GRID = [
[0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
[0, 5, 10, 15, 25, 50],
[
[34.0, 383.0, 566.0, 748.0, 980.0, 1264.0, 1527.0, 1700.0],
[131.0, 450.0, 586.0, 826.0, 995.0, 1280.0, 1537.0, 1700.0],
[291.0, 517.0, 627.0, 850.0, 1010.0, 1310.0, 1547.0, 1700.0],
[454.0, 526.0, 696.0, 870.0, 1038.0, 1340.0, 1590.0, 1700.0],
[491.0, 603.0, 747.0, 890.0, 1050.0, 1390.0, 1635.0, 1700.0],
[718.0, 841.0, 1039.0, 1076.0, 1332.0, 1523.0, 1700.0, 1700.0],
],
]
AI_INFLUENCE_ELO_GRID = [
[0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
[0, 5, 10, 15, 25, 50],
[
[217.0, 439.0, 572.0, 768.0, 960.0, 1227.0, 1449.0, 1521.0],
[302.0, 551.0, 580.0, 800.0, 1028.0, 1257.0, 1470.0, 1529.0],
[388.0, 572.0, 619.0, 839.0, 1077.0, 1305.0, 1490.0, 1561.0],
[467.0, 591.0, 764.0, 878.0, 1097.0, 1390.0, 1530.0, 1591.0],
[539.0, 622.0, 815.0, 953.0, 1120.0, 1420.0, 1560.0, 1601.0],
[772.0, 912.0, 958.0, 1145.0, 1318.0, 1511.0, 1577.0, 1623.0],
],
]
AI_PICK_ELO_GRID = [
[0.0, 0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0],
[0, 5, 10, 15, 25, 50],
[
[-533.0, -515.0, -355.0, 234.0, 650.0, 1147.0, 1546.0, 1700.0],
[-531.0, -450.0, -69.0, 347.0, 670.0, 1182.0, 1550.0, 1700.0],
[-450.0, -311.0, 140.0, 459.0, 693.0, 1252.0, 1555.0, 1700.0],
[-365.0, -82.0, 265.0, 508.0, 864.0, 1301.0, 1619.0, 1700.0],
[-113.0, 273.0, 363.0, 641.0, 983.0, 1486.0, 1700.0, 1700.0],
[514.0, 670.0, 870.0, 1128.0, 1305.0, 1550.0, 1700.0, 1700.0],
],
]
+29 -13
View File
@@ -5,7 +5,7 @@ import subprocess
import threading
import time
import traceback
from typing import Callable, Optional
from typing import Callable, Optional, Dict
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
from katrain.core.game_node import GameNode
@@ -101,12 +101,17 @@ class KataGoEngine:
self.shutdown(finish=False)
self.start()
def check_alive(self, exception_if_dead=False):
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:
raise EngineDiedException(
f"Engine died (process {self.katago_process}, poll {self.katago_process and self.katago_process.poll()}) config {self.config}"
)
if self.katago_process:
os_error += f"status {self.katago_process and self.katago_process.poll()}"
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
self.katrain.log(died_msg, OUTPUT_ERROR)
self.katago_process = None
else:
died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
raise EngineDiedException(died_msg)
return ok
def shutdown(self, finish=False):
@@ -115,8 +120,8 @@ class KataGoEngine:
while self.queries and process.poll() is None:
time.sleep(0.1)
if process:
process.terminate()
self.katago_process = None
process.terminate()
if self.stderr_thread:
self.stderr_thread.join()
if self.analysis_thread:
@@ -134,15 +139,22 @@ class KataGoEngine:
self.katrain.log(line.decode(errors="ignore").strip(), OUTPUT_KATAGO_STDERR)
except Exception as e:
print("ERROR in processing KataGo stderr:", line, "Exception", e)
except:
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 _analysis_read_thread(self):
while self.katago_process is not None:
try:
line = self.katago_process.stdout.readline()
if self.katago_process and not line:
self.check_alive(exception_if_dead=True)
except OSError as e:
raise EngineDiedException(i18n("Engine died unexpectedly").format(error=e))
self.check_alive(os_error=str(e), exception_if_dead=True)
return
if b"Uncaught exception" in line:
self.katrain.log(f"KataGo Engine Failed: {line.decode(errors='ignore')}", OUTPUT_ERROR)
return
@@ -193,7 +205,7 @@ class KataGoEngine:
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
self.katago_process.stdin.flush()
except OSError as e:
self.katrain.log(i18n._("Engine died unexpectedly").format(error=e), OUTPUT_ERROR)
self.check_alive(os_error=str(e), exception_if_dead=True)
return # do not raise, since there's nothing to catch it
def request_analysis(
@@ -206,9 +218,11 @@ class KataGoEngine:
time_limit=True,
priority: int = 0,
ownership: Optional[bool] = None,
next_move=None,
next_move: Optional[GameNode] = None,
extra_settings: Optional[Dict] = None,
):
moves = [m for node in analysis_node.nodes_from_root for m in node.move_with_placements]
moves = [m for node in analysis_node.nodes_from_root for m in node.moves]
initial_stones = analysis_node.root.placements
if next_move:
moves.append(next_move)
if ownership is None:
@@ -233,9 +247,11 @@ class KataGoEngine:
"komi": analysis_node.komi,
"boardXSize": size_x,
"boardYSize": size_y,
"includeOwnership": ownership,
"includeOwnership": ownership and not next_move,
"includePolicy": not next_move,
"initialStones": [[m.player, m.gtp()] for m in initial_stones],
"moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": settings,
"overrideSettings": {**settings, **(extra_settings or {})},
}
self.send_query(query, callback, error_callback, next_move)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits)
+52 -49
View File
@@ -5,10 +5,20 @@ import threading
from datetime import datetime
from typing import Dict, List, Optional, Union
from katrain.core.constants import HOMEPAGE, OUTPUT_DEBUG, OUTPUT_INFO
from kivy.clock import Clock
from katrain.core.constants import (
HOMEPAGE,
OUTPUT_DEBUG,
OUTPUT_INFO,
STATUS_ANALYSIS,
STATUS_INFO,
STATUS_TEACHING,
PLAYER_AI,
)
from katrain.core.engine import KataGoEngine
from katrain.core.game_node import GameNode
from katrain.core.lang import i18n
from katrain.core.lang import i18n, rank_label
from katrain.core.sgf_parser import SGF, Move
from katrain.core.utils import var_to_grid
@@ -46,7 +56,7 @@ class Game:
self.komi = self.root.komi
handicap = int(self.root.get_property("HA", 0))
if handicap and not self.root.placements:
self.place_handicap_stones(handicap)
self.root.place_handicap_stones(handicap)
else:
board_size = katrain.config("game/size")
self.komi = katrain.config("game/komi")
@@ -59,7 +69,7 @@ class Game:
)
handicap = katrain.config("game/handicap")
if handicap:
self.place_handicap_stones(handicap)
self.root.place_handicap_stones(handicap)
if not self.root.get_property("RU"):
self.root.set_property("RU", katrain.config("game/rules"))
@@ -151,8 +161,9 @@ class Game:
except IllegalMoveException:
self._calculate_groups()
raise
played_node = self.current_node.play(move)
self.current_node = played_node
with self._lock:
played_node = self.current_node.play(move)
self.current_node = played_node
if analyze:
played_node.analyze(self.engines[played_node.next_player])
return played_node
@@ -182,35 +193,6 @@ class Game:
ix = (ordered_children.index(cn) + len(ordered_children) + direction) % len(ordered_children)
self.set_current_node(ordered_children[ix])
def place_handicap_stones(self, n_handicaps):
board_size_x, board_size_y = self.board_size
near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1)
near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1)
far_x = board_size_x - 1 - near_x
far_y = board_size_y - 1 - near_y
middle_x = board_size_x // 2 # what for even sizes?
middle_y = board_size_y // 2
if n_handicaps > 9 and board_size_x == board_size_y:
stones_per_row = math.ceil(math.sqrt(n_handicaps))
spacing = (far_x - near_x) / (stones_per_row - 1)
if spacing < near_x:
far_x += 1
near_x -= 1
spacing = (far_x - near_x) / (stones_per_row - 1)
coords = list({math.floor(0.5 + near_x + i * spacing) for i in range(stones_per_row)})
stones = sorted(
[(x, y) for x in coords for y in coords],
key=lambda xy: -((xy[0] - (board_size_x - 1) / 2) ** 2 + (xy[1] - (board_size_y - 1) / 2) ** 2),
)
else: # max 9
stones = [(far_x, far_y), (near_x, near_y), (far_x, near_y), (near_x, far_y)]
if n_handicaps % 2 == 1:
stones.append((middle_x, middle_y))
stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)]
self.root.set_property(
"AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]})
)
@property
def board_size(self):
return self.root.board_size
@@ -297,12 +279,14 @@ class Game:
def player_name(player_info):
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})"
player_names = {
bw: re.sub(
r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw) or player_name(self.katrain.players_info[bw])
)
for bw in "BW"
}
if "KaTrain" in self.root.get_property("AP", ""):
for bw in "BW":
self.root.set_property("P" + bw, player_name(self.katrain.players_info[bw]))
player_info = self.katrain.players_info[bw]
if player_info.player_type == PLAYER_AI:
self.root.set_property(bw + "R", rank_label(player_info.calculated_rank))
player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"}
game_name = f"katrain_{player_names['B']} vs {player_names['W']} {self.game_id}"
file_name = os.path.abspath(os.path.join(path, f"{game_name}.sgf"))
os.makedirs(os.path.dirname(file_name), exist_ok=True)
@@ -317,16 +301,33 @@ class Game:
f.write(sgf)
return i18n._("sgf written").format(file_name=file_name)
def analyze_extra(self, mode):
def analyze_extra(self, mode, **kwargs):
stones = {s.coords for s in self.stones}
cn = self.current_node
engine = self.engines[cn.next_player]
Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0)
if mode == "extra":
visits = cn.analysis_visits_requested + engine.config["max_visits"]
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits))
if kwargs.get("continuous", False):
visits = max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25))
else:
visits = cn.analysis_visits_requested + engine.config["max_visits"]
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False)
return
if mode == "game":
nodes = self.root.nodes_in_tree
if "visits" in kwargs:
visits = kwargs["visits"]
else:
min_visits = min(node.analysis_visits_requested for node in nodes)
visits = min_visits + engine.config["max_visits"]
for node in nodes:
node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False)
self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
return
elif mode == "sweep":
board_size_x, board_size_y = self.board_size
if cn.analysis_ready:
@@ -352,17 +353,19 @@ class Game:
if (x, y) not in stones
]
visits = engine.config["fast_visits"]
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits))
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000_000_000
else: # mode=='equalize':
elif mode == "equalize":
if not cn.analysis_ready:
self.katrain.controls.set_status(i18n._("wait-before-equalize"), self.current_node)
self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node)
return
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
visits = max(d["visits"] for d in cn.analysis["moves"].values())
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits))
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000
else:
raise ValueError("Invalid analysis mode")
for move in analyze_moves:
cn.analyze(
engine, priority, visits=visits, refine_move=move, time_limit=False
@@ -391,6 +394,6 @@ class Game:
if undo:
self.undo(1)
self.katrain.controls.set_status(
i18n._("teaching undo message").format(move=move.gtp(), points_lost=points_lost)
i18n._("teaching undo message").format(move=move.gtp(), points_lost=points_lost), STATUS_TEACHING
)
self.katrain.update_state()
+29 -36
View File
@@ -69,8 +69,6 @@ class GameNode(SGFNode):
# various analysis functions
def analyze(self, engine, priority=0, visits=None, time_limit=True, refine_move=None, analyze_fast=False):
if visits and not refine_move:
self.analysis_visits_requested = max(visits, engine.config["max_visits"])
engine.request_analysis(
self,
lambda result: self.set_analysis(result, refine_move),
@@ -136,6 +134,15 @@ class GameNode(SGFNode):
if win_rate is not None:
return f"{'B' if win_rate > 0.5 else 'W'} {max(win_rate,1-win_rate):.1%}"
def move_policy_stats(self) -> Tuple[Optional[int], float, List]:
single_move = self.move
if single_move and self.parent:
policy_ranking = self.parent.policy_ranking
for ix, (p, m) in enumerate(policy_ranking):
if m == single_move:
return ix + 1, p, policy_ranking
return None, 0.0, []
def make_pv(self, player, pv, interactive):
pvtext = f"{player}{' '.join(pv)}"
if interactive:
@@ -160,44 +167,24 @@ class GameNode(SGFNode):
points_lost = self.points_lost
if sgf and points_lost > 0.5:
text += i18n._("Info:point loss").format(points_lost=points_lost) + "\n"
text += (
i18n._("Info:top move").format(
top_move=previous_top_move["move"],
score=self.format_score(previous_top_move["scoreLead"]),
)
+ "\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"
else:
text += i18n._("Info:best move") + "\n"
if previous_top_move.get("pv") and (sgf or details):
text += (
i18n._("Info:PV").format(
pv=self.make_pv(single_move.player, previous_top_move["pv"], interactive)
)
+ "\n"
)
pv = self.make_pv(single_move.player, previous_top_move["pv"], interactive)
text += i18n._("Info:PV").format(pv=pv) + "\n"
if sgf or details or teach:
policy_ranking = self.parent.policy_ranking
currmove_policy_with_ix = [
(ix + 1, p)
for (p, m), ix in zip(policy_ranking, range(len(policy_ranking)))
if m == single_move
]
if currmove_policy_with_ix:
text += (
i18n._("Info:policy rank").format(
rank=currmove_policy_with_ix[0][0], probability=currmove_policy_with_ix[0][1]
)
+ "\n"
)
if not currmove_policy_with_ix or currmove_policy_with_ix[0] != 1 and (sgf or details):
text += (
i18n._("Info:policy best").format(
move=policy_ranking[0][1].gtp(), probability=policy_ranking[0][0]
)
+ "\n"
)
currmove_pol_rank, currmove_pol_prob, policy_ranking = self.move_policy_stats()
if currmove_pol_rank is not None:
policy_rank_msg = i18n._("Info:policy rank")
text += policy_rank_msg.format(rank=currmove_pol_rank, probability=currmove_pol_prob) + "\n"
if currmove_pol_rank is None or currmove_pol_rank != 1 and (sgf or details):
policy_best_msg = i18n._("Info:policy best")
pol_move, pol_prob = policy_ranking[0][1].gtp(), policy_ranking[0][0]
text += policy_best_msg.format(move=pol_move, probability=pol_prob) + "\n"
if self.auto_undo and sgf:
text += i18n._("Info:teaching undo") + "\n"
top_pv = self.analysis_ready and self.candidate_moves[0].get("pv")
@@ -243,7 +230,13 @@ class GameNode(SGFNode):
polmoves = self.policy_ranking
top_polmove = polmoves[0][1] if polmoves else Move(None) # if no info at all, pass
return [
{**self.analysis["root"], "pointsLost": 0, "order": 0, "move": top_polmove.gtp()}
{
**self.analysis["root"],
"pointsLost": 0,
"order": 0,
"move": top_polmove.gtp(),
"pv": [top_polmove.gtp()],
}
] # single visit -> go by policy/root
root_score = self.analysis["root"]["scoreLead"]
+10
View File
@@ -77,3 +77,13 @@ class Lang(Observable):
DEFAULT_LANGUAGE = "en"
i18n = Lang(DEFAULT_LANGUAGE)
def rank_label(rank):
if rank is None:
return "??k"
if rank >= 0.5:
return f"{rank:.0f}{i18n._('strength:dan')}"
else:
return f"{1-rank:.0f}{i18n._('strength:kyu')}"
+170 -7
View File
@@ -1,4 +1,5 @@
import copy
import math
import re
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple
@@ -285,6 +286,37 @@ class SGFNode:
else:
return "W"
def place_handicap_stones(self, n_handicaps, tygem=False):
board_size_x, board_size_y = self.board_size
near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1)
near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1)
far_x = board_size_x - 1 - near_x
far_y = board_size_y - 1 - near_y
middle_x = board_size_x // 2 # what for even sizes?
middle_y = board_size_y // 2
if n_handicaps > 9 and board_size_x == board_size_y:
stones_per_row = math.ceil(math.sqrt(n_handicaps))
spacing = (far_x - near_x) / (stones_per_row - 1)
if spacing < near_x:
far_x += 1
near_x -= 1
spacing = (far_x - near_x) / (stones_per_row - 1)
coords = list({math.floor(0.5 + near_x + i * spacing) for i in range(stones_per_row)})
stones = sorted(
[(x, y) for x in coords for y in coords],
key=lambda xy: -((xy[0] - (board_size_x - 1) / 2) ** 2 + (xy[1] - (board_size_y - 1) / 2) ** 2),
)
else: # max 9
stones = [(far_x, far_y), (near_x, near_y), (far_x, near_y), (near_x, far_y)]
if n_handicaps % 2 == 1:
stones.append((middle_x, middle_y))
stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)]
if tygem:
stones[2], stones[3] = stones[3], stones[2]
self.set_property(
"AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]})
)
class SGF:
@@ -293,23 +325,31 @@ class SGF:
SGFPROP_PAT = re.compile(r"\s*(?:\(|\)|;|(\w+)((\s*\[([^\]\\]|\\.)*\])+))", flags=re.DOTALL)
@classmethod
def parse(cls, input_str) -> SGFNode:
def parse_sgf(cls, input_str) -> SGFNode:
"""Parse a string as SGF."""
return cls(input_str).root
@classmethod
def parse_file(cls, filename, encoding=None) -> SGFNode:
is_gib = filename.lower().endswith(".gib")
"""Parse a file as SGF, encoding will be detected if not given."""
with open(filename, "rb") as f:
bin_contents = f.read()
if not encoding:
match = re.search(rb"CA\[(.*?)\]", bin_contents)
if match:
encoding = match[1].decode("ascii", errors="ignore")
if not is_gib:
match = re.search(rb"CA\[(.*?)\]", bin_contents)
if match:
encoding = match[1].decode("ascii", errors="ignore")
else:
encoding = "ISO-8859-1" # default
else:
encoding = "ISO-8859-1" # default
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
return cls.parse(decoded)
encoding = "utf8" # ?
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
if is_gib:
return cls.parse_gib(decoded)
else: # sgf
return cls.parse_sgf(decoded)
def __init__(self, contents):
self.contents = contents
@@ -341,3 +381,126 @@ class SGF:
if self.ix < len(self.contents):
raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}")
raise ParseError("Parse Error: expected ')' at end of input.")
# GIB parser adapted from https://github.com/fohristiwhirl/gofish/
@classmethod
def parse_gib(cls, gib):
def parse_player_name(raw):
name = raw
rank = ""
foo = raw.split("(")
if len(foo) == 2:
if foo[1][-1] == ")":
name = foo[0].strip()
rank = foo[1][0:-1]
return name, rank
def gib_make_result(grlt, zipsu):
easycases = {3: "B+R", 4: "W+R", 7: "B+T", 8: "W+T"}
if grlt in easycases:
return easycases[grlt]
if grlt in [0, 1]:
return "{}+{}".format("B" if grlt == 0 else "W", zipsu / 10)
return ""
def gib_get_result(line, grlt_regex, zipsu_regex):
try:
grlt = int(re.search(grlt_regex, line).group(1))
zipsu = int(re.search(zipsu_regex, line).group(1))
except:
return ""
return gib_make_result(grlt, zipsu)
root = cls._NODE_CLASS()
node = root
lines = gib.split("\n")
for line in lines:
line = line.strip()
if line.startswith("\\[GAMEBLACKNAME=") and line.endswith("\\]"):
s = line[16:-2]
name, rank = parse_player_name(s)
if name:
root.set_property("PB", name)
if rank:
root.set_property("BR", rank)
if line.startswith("\\[GAMEWHITENAME=") and line.endswith("\\]"):
s = line[16:-2]
name, rank = parse_player_name(s)
if name:
root.set_property("PW", name)
if rank:
root.set_property("WR", rank)
if line.startswith("\\[GAMEINFOMAIN="):
result = gib_get_result(line, r"GRLT:(\d+),", r"ZIPSU:(\d+),")
if result:
root.set_property("RE", result)
try:
komi = int(re.search(r"GONGJE:(\d+),", line).group(1)) / 10
if komi:
root.set_property("KM", komi)
except:
pass
if line.startswith("\\[GAMETAG="):
if "DT" not in root.properties:
try:
match = re.search(r"C(\d\d\d\d):(\d\d):(\d\d)", line)
date = "{}-{}-{}".format(match.group(1), match.group(2), match.group(3))
root.set_property("DT", date)
except:
pass
if "RE" not in root.properties:
result = gib_get_result(line, r",W(\d+),", r",Z(\d+),")
if result:
root.set_property("RE", result)
if "KM" not in root.properties:
try:
komi = int(re.search(r",G(\d+),", line).group(1)) / 10
if komi:
root.set_property("KM", komi)
except:
pass
if line[0:3] == "INI":
if node is not root:
raise ParseError("Node is not root")
setup = line.split()
try:
handicap = int(setup[3])
except IndexError:
continue
if handicap < 0 or handicap > 9:
raise ParseError(f"Handicap {handicap} out of range")
if handicap >= 2:
root.set_property("HA", handicap)
root.place_handicap_stones(handicap, tygem=True)
if line[0:3] == "STO":
move = line.split()
key = "B" if move[3] == "1" else "W"
try:
x = int(move[4])
y = 18 - int(move[5])
if not (0 <= x < 19 and 0 <= y < 19):
raise ParseError(f"Coordinates for move ({x},{y}) out of range on line {line}")
value = Move(coords=(x, y)).sgf(board_size=(19, 19))
except IndexError:
continue
node = cls._NODE_CLASS(parent=node)
node.set_property(key, value)
if len(root.children) == 0: # We'll assume we failed in this case
raise ParseError("No valid nodes found")
return root
+87 -46
View File
@@ -4,8 +4,12 @@
#:import PLAYER_TYPES katrain.core.constants.PLAYER_TYPES
#:import PLAYER_HUMAN katrain.core.constants.PLAYER_HUMAN
#:import PLAYER_AI katrain.core.constants.PLAYER_AI
#:import PLAYING_NORMAL katrain.core.constants.PLAYING_NORMAL
#:import PLAYING_TEACHING katrain.core.constants.PLAYING_TEACHING
#:import MODE_PLAY katrain.core.constants.MODE_PLAY
#:import MODE_ANALYZE katrain.core.constants.MODE_ANALYZE
#:import STATUS_INFO katrain.core.constants.STATUS_INFO
#:import WHITE katrain.gui.style.WHITE
#:import BLACK katrain.gui.style.BLACK
@@ -30,6 +34,8 @@
#:set CP_PADDING dp(6)
#:set NOTES_FONT_SIZE dp(18)
#:set EPSILON 1e-9
# for sizing help
#<Label>:
# canvas.before:
@@ -51,7 +57,7 @@
Color:
rgba: root.outline_color
Line:
rounded_rectangle: (*self.pos,self.width,self.height,root.background_radius,root.background_radius,root.background_radius,root.background_radius,max(1,root.height))
rounded_rectangle: (self.x + root.outline_width, self.y + root.outline_width, self.width - 2 * root.outline_width, self.height - 2*root.outline_width, root.background_radius,root.background_radius,root.background_radius,root.background_radius,max(1,root.height))
width: root.outline_width or 1
@@ -162,11 +168,6 @@
size: image.size
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text))
<ClickableLabel>:
color: WHITE
halign: 'center'
valign: 'middle'
size: self.texture_size
# UI components
@@ -253,7 +254,7 @@
Line:
circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, self.width/6
width: 1
on_press: app.gui.controls.set_status(i18n._('ai-ponder'))
on_press: app.gui.controls.set_status(i18n._('ai-ponder'),STATUS_INFO)
<HamburgerButton@TransparentIconButton>:
@@ -275,20 +276,29 @@
CircleWithText:
player: root.player
text: str(root.captures)
size_hint: 0.25, 1
MDBoxLayout:
size_hint: 0.32, 1
BoxLayout:
size_hint: 0.75, 1
pos_hint: {'x':0,'y':0}
size_hint: 1,1
orientation: 'vertical'
spacing: CP_SPACING
Label:
text: i18n._(root.player_type)
size_hint: 1,0.55
size_hint: 1,0.6
text: i18n._(root.player_type)
font_size: 0.8 * self.height
Label:
text: i18n._(root.player_subtype)
size_hint: 1,0.45
font_size: 0.7 * self.height
size: self.texture_size
max_lines: 1
halign: 'center'
valign: 'middle'
Label: # todo: proper scaling/shortening label instead of this jank
text: (root.name if root.name and root.player_type==PLAYER_HUMAN and root.player_subtype == PLAYING_NORMAL else i18n._(root.player_subtype) ) + (" ({})".format(root.rank) if root.rank and root.player_subtype != PLAYING_TEACHING else "")
size_hint: 1,0.4
font_size: self.height * 0.7 if root.player_type==PLAYER_HUMAN else self.height * 0.7 * min(1,18/len(self.text))
text_size: self.size if root.player_type==PLAYER_HUMAN else (None,self.height)
halign: 'center'
valign: 'middle'
shorten: root.player_type==PLAYER_HUMAN # overflow/cut rank off AI names
max_lines: 1
<PauseButton>:
height: self.width
@@ -330,7 +340,7 @@
timeout: False
color: ORANGE if self.timeout else GREEN
markup: True
multiline: False
max_lines: 1
font_name: 'fonts/digital-7 (mono).ttf'
<Timer>:
@@ -373,7 +383,7 @@
valign: 'top'
id: periods
text: str(root.state[1])
multiline: False
max_lines: 1
timeout: root.timeout
MDBoxLayout:
size_hint: 0.2,1
@@ -394,28 +404,30 @@
<StatsLabel>:
value : value
orientation: 'horizontal'
size_hint_y: EPSILON if root.hidden else 1
opacity: 0 if root.hidden else 1
Label:
size_hint: 0.6,1
id: desc
text: root.label if not root.hidden else ''
font_size: self.height * 0.7
font_name: root.font_name
text_size: self.size
valign: 'middle'
halign: 'center'
text: root.label
max_lines: 1
Label:
size_hint: 0.4,1
text: root.text if not root.hidden else ''
color: root.color
font_size: desc.font_size
font_name: root.font_name
text_size: self.size
valign: 'middle'
halign: 'center'
id: value
bold: True
text: root.text
max_lines: 1
<StatsBox>
orientation: 'vertical'
@@ -478,7 +490,7 @@
rgba: BOX_BACKGROUND_COLOR
Line:
width: 1
points: (*self.pos,self.x+self.width,self.y)
points: (self.x,self.y+1,self.x+self.width,self.y+1)
<ControlsPanel>:
@@ -488,19 +500,21 @@
engine_status_col: [1,1,1,1]
timer:timer
graph: graph
rank_graph: rank_graph
stats: stats
info: info
note: note
status: status
timer_or_movetree: timer_or_movetree
move_tree: move_tree
padding: CP_PADDING
padding: CP_PADDING,CP_PADDING,CP_PADDING,0
spacing: CP_SPACING
player_box_height: max(50, 0.075 * root.height)
timer_box_height: max(50, 0.1 * root.height)
graph_box_height: max(100, 0.2 * root.height)
stats_box_height: max(50, 0.15 * root.height)
tab_option_height: max(15,root.height / 30)
available_height: root.height - (3 * self.tab_option_height + 2*CP_PADDING - 4*CP_SPACING) # minus tabs fixed, spacing etc
player_box_height: max(50, 0.08 * root.available_height)
timer_box_height: max(50, 0.11 * root.available_height)
graph_height: max(75, 0.17 * root.height)
stat_height: max(20, 0.035 * root.height)
BoxLayout: # -- Players
size_hint_y: None
height: root.player_box_height
@@ -515,7 +529,7 @@
size_hint_y: None
height: root.timer_box_height
BoxLayout:
size_hint_x: 0.0001 if self.parent.mode != MODE_PLAY else 1
size_hint_x: EPSILON if self.parent.mode != MODE_PLAY else 1
opacity: 0 if self.parent.mode != MODE_PLAY else 1
spacing: CP_SPACING
Timer:
@@ -536,28 +550,43 @@
on_left_release: root.katrain("undo","smart")
MoveTree:
id: move_tree
size_hint_x: 0.0001 if self.parent.mode == MODE_PLAY else 1
size_hint_x: EPSILON if self.parent.mode == MODE_PLAY else 1
opacity: 0 if self.parent.mode == MODE_PLAY else 1
CollapsablePanel:
id: graph_panel
height_open: root.graph_box_height
closed_label: 'closedlabel:scoregraph'
options: ['score','winrate']
option_colors: [SCORE_COLOR,WINRATE_COLOR]
option_active: [True,False]
content_height: graph.height + rank_graph.height
options_height: root.tab_option_height
on_option_state: graph.show_graphs(args[1])
closed_label: 'closedlabel:scoregraph'
options: ['score','winrate','rank_est']
option_colors: [SCORE_COLOR,WINRATE_COLOR,POINTLOSS_COLOR]
option_active: [True,False,False]
on_option_state:
graph.show_graphs(args[1])
graph.hidden = not (args[1]['score'] or args[1]['winrate'])
rank_graph.hidden = not args[1]['rank_est']
rank_graph.trigger_calculate()
ScoreGraph:
id: graph
opacity: 0 if self.hidden else 1
size_hint_y: None
height: EPSILON if self.hidden else root.graph_height
RankGraph:
id: rank_graph
opacity: 0 if self.hidden else 1
size_hint_y: None
height: EPSILON if self.hidden else root.graph_height
CollapsablePanel:
id: stats_panel
height_open: root.stats_box_height
num_active: 3
content_height: self.num_active * root.stat_height + EPSILON
options_height: root.tab_option_height
closed_label: 'closedlabel:movestats'
options: ['score','winrate','points']
option_colors: [SCORE_COLOR,WINRATE_COLOR,POINTLOSS_COLOR]
option_active: [True,True,True]
options_height: root.tab_option_height
on_option_state: for k, v in args[1].items(): stats.labels[k].hidden = not v
on_option_state:
for k, v in args[1].items(): stats.labels[k].hidden = not v
self.num_active = sum(args[1].values(),0)
StatsBox:
id: stats
CollapsablePanel:
@@ -580,10 +609,11 @@
orientation: 'vertical'
ScrollableLabel:
id: status
error: False
size_hint: 1, None
height: min(self.parent.height*0.66,self.label.texture_size[1])
outline_color: RED
background_color: DARKRED
outline_color: RED if self.error else WHITE
background_color: LIGHTER_BACKGROUND_COLOR # DARKRED
ScrollableLabel:
id: info
detailed: False
@@ -658,7 +688,7 @@
size_hint: None, 0.7
pos_hint: {'center_y':0.5}
checkbox: checkbox
Label:
ClickableLabel:
id: label
text: root.text
halign: 'center'
@@ -667,6 +697,7 @@
color: checkbox.selected_color if checkbox.active else checkbox.unselected_color
size_hint_x: None
width: self.texture_size[0] + 4
on_left_press: checkbox.active = not checkbox.active
MDCheckbox:
id: checkbox
size_hint: None,0.5
@@ -709,6 +740,16 @@
icon: 'img/Sweep.png'
shortcut: 'd'
on_action: root.katrain("analyze-extra", "sweep")
AnalysisMenuItem:
text: i18n._("analysis:game")
icon: 'img/analysis.png'
shortcut: ''
on_action: root.open_game_analysis_popup()
AnalysisMenuItem:
text: i18n._("analysis:continuous")
icon: 'img/play.png'
shortcut: 'space'
on_action: root.katrain.toggle_continuous_analysis()
AnalysisMenuItem:
text: i18n._("analysis:aimove")
icon: 'img/ai.png'
@@ -782,7 +823,7 @@
icon_size: self.height * 0.75, self.height * 0.75
<MenuLabel@Label+BackgroundMixin>:
<MenuLabel@BackgroundMixin+Label>:
background_color: BACKGROUND_COLOR
font_size: self.height * 0.5
height: self.parent.item_height or 50
@@ -823,7 +864,7 @@
MDBoxLayout:
orientation: 'vertical'
spacing: CP_SPACING
size_hint_x: 1 if root.mode != PLAYER_AI else 0.0001
size_hint_x: 1 if root.mode != PLAYER_AI else EPSILON
opacity:1 if root.mode != PLAYER_AI else 0
Label:
text: i18n._("gametype")
@@ -836,7 +877,7 @@
MDBoxLayout:
orientation: 'vertical'
spacing: CP_SPACING
size_hint_x: 1 if root.mode == PLAYER_AI else 0.0001
size_hint_x: 1 if root.mode == PLAYER_AI else EPSILON
opacity: 1 if root.mode == PLAYER_AI else 0
Label:
text: i18n._("aistrategy")
+91 -49
View File
@@ -1,15 +1,19 @@
import copy
import math
import random
import time
from typing import List, Optional
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.core.window import Window
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Ellipse, Line, Rectangle
from kivy.properties import ListProperty, ObjectProperty, BooleanProperty
from kivy.metrics import dp
from kivy.properties import BooleanProperty, ListProperty, ObjectProperty
from kivy.uix.dropdown import DropDown
from kivy.uix.widget import Widget
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
@@ -17,13 +21,15 @@ from katrain.core.constants import MODE_PLAY, OUTPUT_DEBUG
from katrain.core.game import Move
from katrain.core.lang import i18n
from katrain.core.utils import evaluation_class, var_to_grid
from katrain.gui.kivyutils import draw_circle, draw_text, BackgroundMixin
from katrain.gui.kivyutils import BackgroundMixin, draw_circle, draw_text
from katrain.gui.popups import I18NPopup, ReAnalyzeGamePopup
from katrain.gui.style import *
class BadukPanWidget(Widget):
def __init__(self, **kwargs):
super(BadukPanWidget, self).__init__(**kwargs)
self.stones_sounds = [SoundLoader.load(f"sounds/stone{i}.wav") for i in [1, 2, 3, 4, 5]]
self.trainer_config = {}
self.ghost_stone = []
self.gridpos_x = []
@@ -34,7 +40,6 @@ class BadukPanWidget(Widget):
self.animating_pv = None
self.last_mouse_pos = (0, 0)
Window.bind(mouse_pos=self.on_mouse_pos)
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents)
self.redraw_trigger = Clock.create_trigger(self.redraw)
self.bind(size=self.redraw_trigger)
@@ -93,12 +98,17 @@ class BadukPanWidget(Widget):
self.draw_hover_contents()
self.last_mouse_pos = pos
def play_stone_sound(self, *_args):
if self.katrain.config("timer/sound"):
random.choice(self.stones_sounds).play()
def on_touch_up(self, touch):
if ("button" in touch.profile and touch.button != "left") or not self.gridpos_x:
return
katrain = self.katrain
if self.ghost_stone and ("button" not in touch.profile or touch.button == "left"):
katrain("play", self.ghost_stone)
self.play_stone_sound()
elif not self.ghost_stone:
xd, xp = self._find_closest(touch.x, self.gridpos_x)
yd, yp = self._find_closest(touch.y, self.gridpos_y)
@@ -108,7 +118,7 @@ class BadukPanWidget(Widget):
]
if nodes_here and max(yd, xd) < self.grid_size / 2: # load old comment
if touch.is_double_tap: # navigate to move
katrain.game.set_current_node(nodes_here[-1])
katrain.game.set_current_node(nodes_here[-1].parent)
katrain.update_state()
else: # load comments & pv
katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_DEBUG)
@@ -126,20 +136,31 @@ class BadukPanWidget(Widget):
self.draw_board()
self.draw_board_contents()
def draw_stone(self, x, y, col, outline_col=None, innercol=None, evalcol=None, evalscale=1.0, scale=1.0):
def draw_stone(self, x, y, player, alpha=1, innercol=None, evalcol=None, evalscale=1.0, scale=1.0):
stone_size = self.stone_size * scale
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), stone_size, col)
if outline_col:
Color(*outline_col)
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=min(2, 0.035 * stone_size))
Color(1, 1, 1, alpha)
Rectangle(
pos=(self.gridpos_x[x] - stone_size, self.gridpos_y[y] - stone_size),
size=(2 * stone_size, 2 * stone_size),
source=f"img/{player}_stone.png",
)
if evalcol:
eval_radius = math.sqrt(evalscale) # scale area by evalscale
evalsize = self.stone_size * (EVAL_DOT_MIN_SIZE + eval_radius * (EVAL_DOT_MAX_SIZE - EVAL_DOT_MIN_SIZE))
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), evalsize, evalcol)
Color(*evalcol)
Rectangle(
pos=(self.gridpos_x[x] - evalsize, self.gridpos_y[y] - evalsize),
size=(2 * evalsize, 2 * evalsize),
source=f"img/dot.png",
)
if innercol:
Color(*innercol)
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size * 0.475 / 0.85), width=0.1 * stone_size)
inner_size = stone_size * 0.8
Rectangle(
pos=(self.gridpos_x[x] - inner_size, self.gridpos_y[y] - inner_size),
size=(2 * inner_size, 2 * inner_size),
source=f"img/inner.png",
)
def eval_color(self, points_lost, show_dots_for_class: List[bool] = None) -> Optional[List[float]]:
i = evaluation_class(points_lost, self.trainer_config["eval_thresholds"])
@@ -175,10 +196,11 @@ class BadukPanWidget(Widget):
for i in range(board_size_y)
]
Color(*BOARD_COLOR)
Color(1, 0.95, 0.8, 1) # image is a bit too light
Rectangle(
pos=(self.gridpos_x[0] - self.grid_size * 1.5, self.gridpos_y[0] - self.grid_size * 1.5),
size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces),
source="img/board.png",
)
Color(*LINE_COLOR)
@@ -224,6 +246,8 @@ class BadukPanWidget(Widget):
return
katrain = self.katrain
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"]
with self.canvas:
@@ -247,40 +271,40 @@ class BadukPanWidget(Widget):
katrain.config("trainer/show_dots")
for i, node in enumerate(nodes[::-1]): # reverse order!
points_lost = node.points_lost
evalsize = 1
evalscale = 1
if points_lost and realized_points_lost:
if points_lost <= 0.5 and realized_points_lost <= 1.5:
evalsize = 0
evalscale = 0
else:
evalsize = min(1, max(0, realized_points_lost / points_lost))
for m in node.move_with_placements:
evalscale = min(1, max(0, realized_points_lost / points_lost))
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 = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on)
if move_eval_on and points_lost is not None:
evalcol = self.eval_color(points_lost, show_dots_for_class)
else:
evalcol = None
inner = STONE_COLORS[m.opponent] if i == 0 else None
inner = STONE_COLORS[m.opponent] if i == 0 and not m in placements else None
drawn_stone[m.coords] = m.player
self.draw_stone(
m.coords[0],
m.coords[1],
STONE_COLORS[m.player],
OUTLINE_COLORS[m.player],
inner,
evalcol,
evalsize,
x=m.coords[0],
y=m.coords[1],
player=m.player,
innercol=inner,
evalcol=evalcol,
evalscale=evalscale,
)
realized_points_lost = node.parent_realized_points_lost
if katrain.game.current_node.is_root and katrain.debug_level >= 3: # secret ;)
for y in range(0, board_size_y):
evalcol = self.eval_color(16 * y / board_size_y)
self.draw_stone(0, y, STONE_COLORS["B"], OUTLINE_COLORS["B"], None, evalcol, y / (board_size_y - 1))
self.draw_stone(1, y, STONE_COLORS["B"], OUTLINE_COLORS["B"], STONE_COLORS["W"], evalcol, 1)
self.draw_stone(2, y, STONE_COLORS["W"], OUTLINE_COLORS["W"], None, evalcol, y / (board_size_y - 1))
self.draw_stone(3, y, STONE_COLORS["W"], OUTLINE_COLORS["W"], STONE_COLORS["B"], evalcol, 1)
self.draw_stone(4, y, [*evalcol[:3], 0.5], scale=0.8)
self.draw_stone(0, y, "B", evalcol=evalcol, evalscale=y / (board_size_y - 1))
self.draw_stone(1, y, "B", innercol=STONE_COLORS["W"], evalcol=evalcol)
self.draw_stone(2, y, "W", evalcol=evalcol, evalscale=y / (board_size_y - 1))
self.draw_stone(3, y, "W", innercol=STONE_COLORS["B"], evalcol=evalcol)
# self.draw_stone(4, y, evalcol=[*evalcol[:3], 0.5], scale=0.8)
# ownership - allow one move out of date for smooth animation
ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership)
@@ -319,7 +343,9 @@ class BadukPanWidget(Widget):
*POLICY_COLOR,
GHOST_ALPHA + TOP_MOVE_ALPHA * (policy_grid[y][x] == best_move_policy),
)
self.draw_stone(x, y, policy_circle_color, scale=polsize)
draw_circle(
(self.gridpos_x[x], self.gridpos_y[y]), polsize * self.stone_size, policy_circle_color
)
polsize = math.sqrt(policy[-1])
with pass_btn.canvas.after:
draw_circle(
@@ -333,6 +359,7 @@ class BadukPanWidget(Widget):
if passed:
if game_ended:
text = katrain.game.manual_score or i18n._("board-game-end")
katrain.controls.timer.paused = True
else:
text = i18n._("board-pass")
Color(0.45, 0.05, 0.45, 0.7)
@@ -352,7 +379,6 @@ class BadukPanWidget(Widget):
game_ended = katrain.game.ended
current_node = katrain.game.current_node
player, next_player = current_node.player, current_node.next_player
stone_color = STONE_COLORS
with self.canvas.after:
self.canvas.after.clear()
@@ -378,10 +404,9 @@ class BadukPanWidget(Widget):
self.draw_stone(
move.coords[0],
move.coords[1],
(*stone_color[move.player][:3], alpha),
None,
None,
evalcol,
move.player,
alpha=alpha,
evalcol=evalcol,
evalscale=scale,
scale=scale,
)
@@ -393,24 +418,31 @@ class BadukPanWidget(Widget):
move = Move.from_gtp(move_dict["move"])
if move.coords is not None:
alpha, scale = GHOST_ALPHA, 1.0
if i == 0:
alpha += TOP_MOVE_ALPHA
elif move_dict["visits"] < VISITS_FRAC_SMALL * hint_moves[0]["visits"]:
if move_dict["visits"] < VISITS_FRAC_SMALL * hint_moves[0]["visits"]:
scale = 0.8
if "pv" in move_dict:
self.active_pv_moves.append((move.coords, move_dict["pv"], current_node))
else:
katrain.log(f"PV missing for move_dict {move_dict}", OUTPUT_DEBUG)
self.draw_stone(
move.coords[0],
move.coords[1],
[*self.eval_color(move_dict["pointsLost"])[:3], alpha],
scale=scale,
draw_circle(
(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]),
col=[*self.eval_color(move_dict["pointsLost"])[:3], alpha],
r=self.stone_size * scale,
)
if i == 0:
Color(*TOP_MOVE_BORDER_COLOR)
Line(
circle=(
self.gridpos_x[move.coords[0]],
self.gridpos_y[move.coords[1]],
self.stone_size * scale - 1.2,
),
width=dp(1.2),
)
# hover next move ghost stone
if self.ghost_stone:
self.draw_stone(*self.ghost_stone, (*stone_color[next_player][:3], ghost_alpha))
self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha)
animating_pv = self.animating_pv
if animating_pv:
@@ -426,13 +458,13 @@ class BadukPanWidget(Widget):
def draw_pv(self, pv, node, up_to_move):
katrain = self.katrain
next_last_player = [node.next_player, node.player]
stone_color = STONE_COLORS
cn = katrain.game.current_node
if node != cn and node.parent != cn:
hide_node = cn
while hide_node and hide_node.move and hide_node != node:
if not hide_node.move.is_pass:
self.draw_stone(*hide_node.move.coords, [0.85, 0.68, 0.40, 0.8]) # board coloured dot
pos = (self.gridpos_x[hide_node.move.coords[0]], self.gridpos_y[hide_node.move.coords[1]])
draw_circle(pos, self.stone_size, [0.85, 0.68, 0.40, 0.8])
hide_node = hide_node.parent
for i, gtpmove in enumerate(pv):
if i > up_to_move:
@@ -451,7 +483,13 @@ class BadukPanWidget(Widget):
board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]])
sizefac = 1
draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player])
stone_size = self.stone_size * sizefac
Color(1, 1, 1, 1)
Rectangle(
pos=(board_coords[0] - stone_size, board_coords[1] - stone_size),
size=(2 * stone_size, 2 * stone_size),
source=f"img/{move_player}_stone.png",
)
Color(*STONE_TEXT_COLORS[move_player])
draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
@@ -466,7 +504,11 @@ class BadukPanWidget(Widget):
class AnalysisDropDown(DropDown):
pass
def open_game_analysis_popup(self, *_args):
analysis_popup = I18NPopup(title_key="analysis:game", size=[dp(500), dp(300)], content=ReAnalyzeGamePopup())
analysis_popup.content.popup = analysis_popup
analysis_popup.content.katrain = MDApp.get_running_app().gui
analysis_popup.open()
class AnalysisControls(MDBoxLayout):
+27 -15
View File
@@ -2,11 +2,12 @@ import time
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.properties import ObjectProperty, OptionProperty
from kivy.properties import ObjectProperty, OptionProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from katrain.core.constants import MODE_PLAY, MODE_ANALYZE
from katrain.core.constants import MODE_PLAY, MODE_ANALYZE, STATUS_ERROR, STATUS_ANALYSIS, PLAYER_HUMAN
from katrain.core.lang import rank_label
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
@@ -66,11 +67,10 @@ class ControlsPanel(BoxLayout):
def __init__(self, **kwargs):
super(ControlsPanel, self).__init__(**kwargs)
self.status_msg = None
self.status_node = None
self.status_state = (None, -1e9, None)
self.active_comment_node = None
self.last_timer_update = (None, 0, False)
self.beep = SoundLoader.load("beep.wav")
self.beep = SoundLoader.load("sounds/countdownbeep.wav")
self.beep_start = 5.2
self.timer_interval = 0.07
@@ -80,25 +80,36 @@ class ControlsPanel(BoxLayout):
for bw, player_info in self.katrain.players_info.items():
self.players[bw].player_type = player_info.player_type
self.players[bw].player_subtype = player_info.player_subtype
self.players[bw].name = player_info.name
self.players[bw].rank = (
player_info.sgf_rank
if player_info.player_type == PLAYER_HUMAN
else rank_label(player_info.calculated_rank)
)
def set_status(self, msg, at_node=None):
self.status_msg = msg
self.status_node = at_node or self.katrain and self.katrain.game and self.katrain.game.current_node
self.status.text = msg
self.update_evaluation()
def set_status(self, msg, status_type, at_node=None):
at_node = at_node or self.katrain and self.katrain.game and self.katrain.game.current_node
if at_node != self.status_state[2] or int(status_type) >= int(self.status_state[1]) or msg == "":
self.status_state = (msg, status_type, at_node)
self.status.text = msg
self.status.error = status_type == STATUS_ERROR
Clock.schedule_once(self.update_evaluation, 0)
# handles showing completed analysis and score graph
def update_evaluation(self):
def update_evaluation(self, *_args):
katrain = self.katrain
game = katrain and katrain.game
if not game:
return
current_node, move = game.current_node, game.current_node.move
if game.current_node is not self.status_node and not (
self.status is not None and self.status_node is None and game.current_node.is_root
): # startup errors on root
if (
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
): # clear status if node changes, except startup errors on root. also clear analysis message when no queries
self.status.text = ""
self.status_node = None
self.status_state = (None, -1e9, None)
last_player_was_ai_playing_human = katrain.last_player_info.ai and katrain.next_player_info.human
both_players_are_robots = katrain.last_player_info.ai and katrain.next_player_info.ai
@@ -132,6 +143,7 @@ class ControlsPanel(BoxLayout):
self.stats.player = ""
self.graph.update_value(current_node)
self.rank_graph.update_value(current_node)
self.note.text = current_node.note
self.info.text = info
+33 -21
View File
@@ -22,7 +22,7 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import BaseFlatButton, BasePressedButton
from kivymd.uix.navigationdrawer import MDNavigationDrawer
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, PLAYER_AI, MODE_PLAY
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, MODE_PLAY, PLAYER_AI
from katrain.core.lang import i18n
from katrain.gui.style import DEFAULT_FONT, WHITE
@@ -297,6 +297,8 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin):
player = OptionProperty("B", options=["B", "W"])
player_type = StringProperty("Player")
player_subtype = StringProperty("")
name = StringProperty("", allownone=True)
rank = StringProperty("", allownone=True)
active = BooleanProperty(True)
@@ -317,6 +319,9 @@ class AnalysisToggle(MDBoxLayout):
def trigger_action(self, *args, **kwargs):
return self.checkbox.trigger_action(*args, **kwargs)
def activate(self, *_args):
self.checkbox.active = True
@property
def active(self):
return self.checkbox.active
@@ -355,22 +360,23 @@ class CollapsablePanel(MDBoxLayout):
options = ListProperty([])
options_height = NumericProperty(25)
content_height = NumericProperty(100)
size_hint_y_open = NumericProperty(None) # total height inc tabs, overrides content_height
options_spacing = NumericProperty(6)
option_labels = ListProperty([])
option_active = ListProperty([])
option_colors = ListProperty([])
closed_label = StringProperty("Closed Panel")
contents = ListProperty([])
size_hint_y_open = NumericProperty(1)
height_open = NumericProperty(None)
closed_label = StringProperty("Closed Panel")
state = OptionProperty("open", options=["open", "close"])
close_icon = "img/Previous-5.png"
open_icon = "img/Next-5.png"
def __init__(self, **kwargs):
self.header, self.contents, self.open_close_button = None, None, None
self.open_close_button, self.header = None, None
self.option_buttons = []
super().__init__(**kwargs)
self.orientation = "vertical"
@@ -381,10 +387,25 @@ class CollapsablePanel(MDBoxLayout):
option_active=self.build_options,
options_spacing=self.build_options,
)
self.bind(state=self.build, size_hint_y_open=self.build, height_open=self.build)
self.bind(state=self._on_state, content_height=self._on_size, options_height=self._on_size)
MDApp.get_running_app().bind(language=lambda *_: Clock.schedule_once(self.build_options, 0))
self.build_options()
def _on_state(self, *_args):
self.build()
self.trigger_select(ix=None)
def _on_size(self, *_args):
height, size_hint_y = 1, None
if self.state == "open" and self.contents:
if self.size_hint_y_open is not None:
size_hint_y = self.size_hint_y_open
else:
height = self.content_height + self.options_height
else:
height = self.header.height
self.height, self.size_hint_y = height, size_hint_y
@property
def option_state(self):
return {option: active for option, active in zip(self.options, self.option_active)}
@@ -396,7 +417,7 @@ class CollapsablePanel(MDBoxLayout):
button.state = "down" if state_dict[option] else "normal"
self.trigger_select(ix=None)
def build_options(self, *args, **kwargs):
def build_options(self, *args):
self.header = CollapsablePanelHeader(
height=self.options_height, size_hint_y=None, spacing=self.options_spacing, padding=[1, 0, 0, 0]
)
@@ -422,13 +443,12 @@ class CollapsablePanel(MDBoxLayout):
self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None, self.open_close_icon()))
self.build()
def build(self, *args, **kwargs):
def build(self, *args):
self.header.clear_widgets()
if self.state == "open":
for button in self.option_buttons:
self.header.add_widget(button)
self.header.add_widget(Label()) # spacer
self.trigger_select(ix=None)
else:
self.header.add_widget(
Label(
@@ -439,24 +459,16 @@ class CollapsablePanel(MDBoxLayout):
super().clear_widgets()
super().add_widget(self.header)
height, size_hint_y = 1, None
if self.state == "open" and self.contents:
super().add_widget(self.contents)
if self.height_open:
height = self.height_open
else:
size_hint_y = self.size_hint_y_open
else:
height = self.header.height
self.height, self.size_hint_y = height, size_hint_y
for w in self.contents:
super().add_widget(w)
self._on_size()
def open_close_icon(self):
return self.open_icon if self.state == "open" else self.close_icon
def add_widget(self, widget, index=0, **_kwargs):
if self.contents:
raise ValueError("CollapsablePanel can only have one child")
self.contents = widget
self.contents.append(widget)
self.build()
def set_state(self, state="toggle"):
+87 -19
View File
@@ -5,7 +5,7 @@ from typing import Any, Dict, List, Tuple, Union
from kivy.clock import Clock
from kivy.metrics import dp
from kivy.properties import BooleanProperty, NumericProperty, StringProperty
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
@@ -15,19 +15,24 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField
from katrain.core.ai import ai_rank_estimation
from katrain.core.constants import (
AI_CONFIG_DEFAULT,
AI_DEFAULT,
AI_OPTION_VALUES,
AI_STRATEGIES_RECOMMENDED_ORDER,
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_INFO,
STATUS_INFO,
AI_KEY_PROPERTIES,
)
from katrain.core.engine import KataGoEngine
from katrain.core.lang import i18n
from katrain.core.lang import i18n, rank_label
from katrain.core.utils import PATHS, find_package_resource
from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner
from katrain.gui.style import DEFAULT_FONT, EVAL_COLORS
from katrain.gui.widgets import RankGraph
from katrain.gui.widgets.progress_loader import ProgressLoader
@@ -131,6 +136,26 @@ class LabelledIntInput(LabelledTextInput):
return int(self.text or "0")
class LabelledSelectionSlider(BoxLayout):
input_property = StringProperty("")
values = ListProperty([(0, "")]) # (value:numeric,label:string) pairs
key_option = BooleanProperty(False)
def set_value(self, v):
self.slider.set_value(v)
self.textbox.text = str(v)
@property
def input_value(self):
if self.textbox.text:
return float(self.textbox.text)
return self.slider.values[self.slider.index][0]
@property
def raw_input_value(self):
return self.textbox.text
class InputParseError(Exception):
pass
@@ -143,9 +168,9 @@ class QuickConfigGui(MDBoxLayout):
Clock.schedule_once(self.build_and_set_properties, 0)
def collect_properties(self, widget) -> Dict:
if isinstance(widget, (LabelledTextInput, LabelledSpinner, LabelledCheckBox)) and getattr(
widget, "input_property", None
):
if isinstance(
widget, (LabelledTextInput, LabelledSpinner, LabelledCheckBox, LabelledSelectionSlider)
) and getattr(widget, "input_property", None):
try:
ret = {widget.input_property: widget.input_value}
except Exception as e: # TODO : on widget?
@@ -185,12 +210,14 @@ class QuickConfigGui(MDBoxLayout):
return self._set_properties_subtree(self)
def _set_properties_subtree(self, widget):
if isinstance(widget, (LabelledTextInput, LabelledSpinner, LabelledCheckBox)) and getattr(
widget, "input_property", None
):
if isinstance(
widget, (LabelledTextInput, LabelledSpinner, LabelledCheckBox, LabelledSelectionSlider)
) and getattr(widget, "input_property", None):
value = self.get_setting(widget.input_property)[0]
if isinstance(widget, LabelledCheckBox):
widget.active = value is True
elif isinstance(widget, LabelledSelectionSlider):
widget.set_value(value)
elif isinstance(widget, LabelledSpinner):
selected = 0
try:
@@ -283,7 +310,7 @@ class DescriptionLabel(Label):
pass
class AIPopup(QuickConfigGui):
class ConfigAIPopup(QuickConfigGui):
max_options = NumericProperty(6)
def __init__(self, katrain):
@@ -295,18 +322,54 @@ class AIPopup(QuickConfigGui):
self.build_ai_options()
self.ai_select.bind(text=self.build_ai_options)
def estimate_rank_from_options(self, *_args):
strategy = self.ai_select.selected[1]
try:
options = self.collect_properties(self) # [strategy]
except InputParseError:
self.estimated_rank_label.text = "??"
return
prefix = f"ai/{strategy}/"
options = {k[len(prefix) :]: v for k, v in options.items() if k.startswith(prefix)}
dan_rank = ai_rank_estimation(strategy, options)
self.estimated_rank_label.text = rank_label(dan_rank)
def build_ai_options(self, *_args):
strategy = self.ai_select.selected[1]
mode_settings = self.katrain.config(f"ai/{strategy}")
self.options_grid.clear_widgets()
self.help_label.text = i18n._(strategy.replace("ai:", "aihelp:"))
for k, v in sorted(mode_settings.items(), key=lambda kv: kv[0]):
self.options_grid.add_widget(DescriptionLabel(text=k))
self.options_grid.add_widget(
wrap_anchor(LabelledFloatInput(text=str(v), input_property=f"ai/{strategy}/{k}"))
)
for k, v in sorted(mode_settings.items(), key=lambda kv: (kv[0] not in AI_KEY_PROPERTIES, kv[0])):
self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.25))
if k in AI_OPTION_VALUES:
values = AI_OPTION_VALUES[k]
if values == "bool":
widget = LabelledCheckBox(input_property=f"ai/{strategy}/{k}")
widget.active = v
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]
else: # just numbers
fixed_values = [(v, str(v)) for v in values]
widget = LabelledSelectionSlider(
values=fixed_values, input_property=f"ai/{strategy}/{k}", key_option=(k in AI_KEY_PROPERTIES)
)
widget.set_value(v)
widget.textbox.bind(text=self.estimate_rank_from_options)
self.options_grid.add_widget(wrap_anchor(widget))
else:
self.options_grid.add_widget(
wrap_anchor(LabelledFloatInput(text=str(v), input_property=f"ai/{strategy}/{k}"))
)
for _ in range((self.max_options - len(mode_settings)) * 2):
self.options_grid.add_widget(Label())
self.options_grid.add_widget(Label(size_hint_x=None))
Clock.schedule_once(self.estimate_rank_from_options)
def update_config(self, save_to_file=True):
super().update_config(save_to_file=save_to_file)
self.katrain.update_calculated_ranks()
Clock.schedule_once(self.katrain.controls.update_players, 0)
class ConfigPopup(QuickConfigGui):
@@ -316,7 +379,6 @@ class ConfigPopup(QuickConfigGui):
def build_and_set_properties(self, *_args):
super().build_and_set_properties()
# self.check_models()
def check_models(self, *args): # WIP
done = set()
@@ -388,8 +450,8 @@ class ConfigPopup(QuickConfigGui):
progress.start(self.download_progress_box)
downloading = True
if not downloading:
self.download_progress_box.add_widget(Label(text=i18n._("All models downloaded"),text_size=(None,dp(50) )))
print('x')
self.download_progress_box.add_widget(Label(text=i18n._("All models downloaded"), text_size=(None, dp(50))))
print("x")
def update_config(self, save_to_file=True):
updated = super().update_config(save_to_file=save_to_file)
@@ -400,8 +462,9 @@ class ConfigPopup(QuickConfigGui):
if detected_restart:
def restart_engine(_dt):
self.katrain.controls.set_status("", STATUS_INFO)
self.katrain.log(f"Restarting Engine after {detected_restart} settings change")
self.katrain.controls.set_status(i18n._("restarting engine"))
self.katrain.controls.set_status(i18n._("restarting engine"), STATUS_INFO)
old_engine = self.katrain.engine # type: KataGoEngine
old_proc = old_engine.katago_process
@@ -426,3 +489,8 @@ class LoadSGFPopup(BoxLayout):
]
self.filesel.path = os.path.abspath(os.path.expanduser(app.gui.config("general/sgf_load")))
self.filesel.select_string = i18n._("Load File")
class ReAnalyzeGamePopup(BoxLayout):
katrain = ObjectProperty(None)
popup = ObjectProperty(None)
+5 -4
View File
@@ -31,8 +31,8 @@ EVAL_COLORS = [
[0.8, 0, 0, 1],
[0.9, 0.4, 0.1, 1],
[0.85, 0.89, 0.3, 1],
[0.67, 0.9, 0.18, 1.0],
[0.117, 0.588, 0, 1.0],
[0.67, 0.9, 0.18, 1],
[0.117, 0.588, 0, 1],
]
EVAL_DOT_MAX_SIZE = 0.5
EVAL_DOT_MIN_SIZE = 0.25
@@ -46,11 +46,12 @@ LINE_COLOR = [0, 0, 0]
POLICY_COLOR = [0.9, 0.2, 0.8]
STARPOINT_SIZE = 0.1
BOARD_COLOR = [0.85, 0.68, 0.40, 1]
STONE_SIZE = 0.475
STONE_SIZE = 0.505 # texture edge is transparent
VISITS_FRAC_SMALL = 0.1
GHOST_ALPHA = 0.5
TOP_MOVE_ALPHA = 0.4
TOP_MOVE_BORDER_COLOR = [46 / 255, 65 / 255, 88 / 255, 0.9]
CHILD_SCALE = 0.95
# ponder light
@@ -59,4 +60,4 @@ ENGINE_BUSY_COL = EVAL_COLORS[2]
ENGINE_READY_COL = EVAL_COLORS[-1]
# info
INFO_PV_COLOR = to_hexcol(RED)
INFO_PV_COLOR = to_hexcol(YELLOW)
+6
View File
@@ -0,0 +1,6 @@
from katrain.gui.widgets.graph import ScoreGraph, RankGraph
from katrain.gui.widgets.movetree import MoveTree
from katrain.gui.widgets.filebrowser import I18NFileBrowser
from katrain.gui.widgets.selection_slider import SelectionSlider
__all__ = ["ScoreGraph", "RankGraph", "MoveTree", "I18NFileBrowser", "SelectionSlider"]
+265 -74
View File
@@ -1,39 +1,80 @@
import math
from kivy.lang import Builder
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, Clock
from kivymd.app import MDApp
from katrain.gui.kivyutils import BackgroundMixin
import threading
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import BooleanProperty, Clock, ListProperty, NumericProperty, StringProperty
from kivy.uix.widget import Widget
from kivymd.app import MDApp
class ScoreGraph(BackgroundMixin):
whalf = NumericProperty(0)
bhalf = NumericProperty(0)
mid = NumericProperty(0)
from katrain.core.constants import OUTPUT_ERROR
from katrain.core.lang import rank_label
show_score = BooleanProperty(True)
show_winrate = BooleanProperty(True)
class Graph(Widget):
marker_font_size = NumericProperty(0)
background_image = StringProperty("img/graph_bg.png")
background_color = ListProperty([1, 1, 1, 1])
highlighted_index = NumericProperty(0)
nodes = ListProperty([])
score_points = ListProperty([])
winrate_points = ListProperty([])
score_dot_pos = ListProperty([0, 0])
winrate_dot_pos = ListProperty([0, 0])
highlighted_index = NumericProperty(None)
highlight_size = NumericProperty(6)
score_scale = NumericProperty(5)
winrate_scale = NumericProperty(5)
navigate_move = ListProperty([None, 0, 0, 0])
hidden = BooleanProperty(False)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._lock = threading.Lock()
self.bind(pos=self.update_graph, size=self.update_graph)
self.redraw_trigger = Clock.create_trigger(self.update_graph, 0.1)
def initialize_from_game(self, root):
self.nodes = [root]
node = root
while node.children:
node = node.ordered_children[0]
self.nodes.append(node)
self.highlighted_index = 0
self.redraw_trigger()
def update_graph(self, *args):
pass
def update_value(self, node):
with self._lock:
self.highlighted_index = index = node.depth
self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1)))
self.nodes[index] = node
if index > 1 and node.parent: # sometimes there are gaps
backfill, bfnode = index - 1, node.parent
while bfnode is not None and self.nodes[backfill] != bfnode:
self.nodes[backfill] = bfnode
backfill -= 1
bfnode = bfnode.parent
if index + 1 < len(self.nodes) and (
node is None or not node.children or self.nodes[index + 1] != node.ordered_children[0]
):
self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch
if index == len(self.nodes) - 1: # possibly just switched branch or the line above triggered
while node.children: # add children back
node = node.ordered_children[0]
self.nodes.append(node)
self.redraw_trigger()
class ScoreGraph(Graph):
show_score = BooleanProperty(True)
show_winrate = BooleanProperty(True)
score_points = ListProperty([])
winrate_points = ListProperty([])
score_dot_pos = ListProperty([0, 0])
winrate_dot_pos = ListProperty([0, 0])
highlight_size = NumericProperty(dp(6))
score_scale = NumericProperty(5)
winrate_scale = NumericProperty(5)
navigate_move = ListProperty([None, 0, 0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
@@ -58,14 +99,6 @@ class ScoreGraph(BackgroundMixin):
katrain.update_state()
self.navigate_move = [None, 0, 0, 0]
def initialize_from_game(self, root):
self.nodes = [root]
node = root
while node.children:
node = node.ordered_children[0]
self.nodes.append(node)
self.highlighted_index = 0
def show_graphs(self, keys):
self.show_score = keys["score"]
self.show_winrate = keys["winrate"]
@@ -124,63 +157,188 @@ class ScoreGraph(BackgroundMixin):
)
self.winrate_dot_pos = winrate_dot_point
def update_value(self, node):
with self._lock:
self.highlighted_index = index = node.depth
self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1)))
self.nodes[index] = node
if index > 1 and node.parent: # sometimes things go so fast
backfill, bfnode = index - 1, node.parent
while bfnode is not None and self.nodes[backfill] != bfnode:
self.nodes[backfill] = bfnode
backfill -= 1
bfnode = bfnode.parent
if index + 1 < len(self.nodes) and (
node is None or not node.children or self.nodes[index + 1] != node.ordered_children[0]
):
self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch
if index == len(self.nodes) - 1: # possibly just switched branch or the line above triggered
while node.children: # add children back
node = node.ordered_children[0]
self.nodes.append(node)
Clock.schedule_once(self.update_graph, 0)
def averagemod(data):
sorteddata = sorted(data)
lendata = len(data)
return sum(sorteddata[int(lendata * 0.2) : int(lendata * 0.8) + 1]) / (
(int(lendata * 0.8) + 1) - int(lendata * 0.2)
) # average without the best and worst 20% of ranks
def gauss(data):
return math.exp(-1 * (data) ** 2)
class RankGraph(Graph):
black_rank_points = ListProperty([])
white_rank_points = ListProperty([])
segment_length = NumericProperty(80)
RANK_CAP = 5
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.calculate_trigger = Clock.create_trigger(lambda *args: self.rank_game(), 0.25)
self.rank_by_player = {}
@staticmethod
def calculate_rank_for_player(segment_stats, num_intersec, player):
non_obvious_moves = [
(nl, r, val)
for nl, r, val, pl in segment_stats
if nl is not None and val < (0.8 * (1 - (num_intersec - nl) / num_intersec * 0.5)) and pl == player
]
if not non_obvious_moves:
return None
num_legal, rank, value = zip(*non_obvious_moves)
rank = [min(r, nl * 0.09) for r, nl in zip(rank, num_legal)]
averagemod_rank = averagemod(rank)
averagemod_len_legal = averagemod(num_legal)
norm_avemod_len_legal = averagemod_len_legal / num_intersec
if averagemod_rank > 0.1:
rank_kyu = (
-0.97222
* math.log(averagemod_rank)
/ (0.24634 + averagemod_rank * gauss(3.3208 * (norm_avemod_len_legal)))
+ 12.703 * (norm_avemod_len_legal)
+ 11.198 * math.log(averagemod_rank)
+ 12.28 * gauss(2.379 * (norm_avemod_len_legal))
- 16.544
)
else:
rank_kyu = -4
return min(RankGraph.RANK_CAP, 1 - rank_kyu) # dan rank
@staticmethod
def calculate_ranks(segment_stats, num_intersec):
return {pl: RankGraph.calculate_rank_for_player(segment_stats, num_intersec, pl) for pl in "BW"}
def rank_game(self):
try:
nodes = self.nodes
parent_policy_per_move = [node.parent.policy_ranking if node.parent else None for node in nodes]
num_legal_moves = [
sum(pv >= 0 for pv, _ in policy_ranking) if policy_ranking else 0
for policy_ranking in parent_policy_per_move
]
policy_stats = [
[
(num_mv, rank, value, mv.player)
for rank, (value, mv) in enumerate(policy_ranking)
if mv == move.move
][0]
if policy_ranking
else (None, None, None, None)
for move, policy_ranking, num_mv in zip(nodes, parent_policy_per_move, num_legal_moves)
]
size = self.nodes[0].board_size
num_intersec = size[0] * size[1]
half_seg = self.segment_length // 2
ranks = {"B": [], "W": []}
dx = self.segment_length // 4
for segment_mid in range(dx, len(nodes), dx):
bounds = (max(0, segment_mid - half_seg), min(segment_mid + half_seg, len(nodes)))
num_analyzed = sum(num_mv is not None for num_mv, _, _, _ in policy_stats[bounds[0] : bounds[1] + 1])
if num_analyzed >= self.segment_length * 0.75:
for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items():
ranks[pl].append((segment_mid, rank))
self.rank_by_player = ranks
except Exception as e:
MDApp.get_running_app().gui.log(f"Exception while calculating rank: {e}", OUTPUT_ERROR)
self.redraw_trigger()
def update_value(self, node):
super().update_value(node)
self.trigger_calculate()
def trigger_calculate(self):
if self.opacity != 0: # recalc here on trigger and only if visible for speed
self.calculate_trigger()
def update_graph(self, *args):
if self.rank_by_player:
xscale = self.width / max(len(self.nodes) - 1, 15)
available_height = self.height
all_ranks = [rank for lst in self.rank_by_player.values() for seg, rank in lst if rank is not None]
if not all_ranks:
return
min_rank = math.floor(min(all_ranks))
max_rank = math.ceil(max(all_ranks))
if max_rank == min_rank:
min_rank -= 1
if (max_rank - min_rank) % 2 != 0: # make midpoint whole integer
if abs(max_rank - max(all_ranks)) < abs(min(all_ranks) - min_rank) and max_rank < self.RANK_CAP:
max_rank += 1
else:
min_rank -= 1
rank_range = max_rank - min_rank
self.ids.mid_marker.text = rank_label((max_rank + min_rank) / 2)
self.ids.top_marker.text = rank_label(max_rank) + ("+" if max_rank == self.RANK_CAP else "")
self.ids.bottom_marker.text = rank_label(min_rank)
graph_points = {}
for pl, rank_points in self.rank_by_player.items():
graph_points[pl] = [
[
self.x + i * xscale,
self.y + available_height * (val - min_rank) / rank_range if val is not None else math.nan,
]
for i, val in rank_points
]
self.black_rank_points = sum(graph_points["B"], [])
self.white_rank_points = sum(graph_points["W"], [])
else:
self.black_rank_points = []
self.white_rank_points = []
Builder.load_string(
"""
#:set GRAPH_CENTER_COLOR [0.5,0.5,0.5]
#:set GRAPH_DOT_COLOR [0.85, 0.3, 0.3,1]
#:set GRAPH_DOT_COLOR [0.85,0.3,0.3,1]
#:set WINRATE_MARKER_COLOR [0.05, 0.7, 0.05, 1]
#:set SCORE_MARKER_COLOR [0.2, 0.6, 0.8, 1]
#:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR
#:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR
#:import SCORE_COLOR katrain.gui.style.SCORE_COLOR
#:import WINRATE_COLOR katrain.gui.style.WINRATE_COLOR
#:import BLACK katrain.gui.style.BLACK
#:import WHITE katrain.gui.style.WHITE
#:import YELLOW katrain.gui.style.YELLOW
<ScoreGraph>:
background_color: BOX_BACKGROUND_COLOR
whalf: self.y + self.height*0.25
bhalf: self.y + self.height*0.75
mid: self.y + self.height*0.5
right_edge: self.x + self.width
<Graph>:
background_color: BOX_BACKGROUND_COLOR
marker_font_size: 0.1 * self.height
canvas:
canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
Color:
rgba: [1,1,1,1]
Rectangle:
pos: self.pos
size: self.size
source: 'img/graph_bg.png'
canvas.after:
source: root.background_image
<ScoreGraph>:
canvas:
Color:
rgba: SCORE_COLOR
Line:
points: root.score_points if root.show_score else []
width: 1.1
width: dp(1.1)
Color:
rgba: WINRATE_COLOR
Line:
points: root.winrate_points if root.show_winrate else []
width: 1.1
width: dp(1.1)
Color:
rgba: [0.5,0.5,0.5,1] if root.navigate_move[0] else [0,0,0,0]
Line:
@@ -201,34 +359,67 @@ Builder.load_string(
# score ticks
GraphMarkerLabel:
font_size: root.marker_font_size
color: SCORE_COLOR
pos: root.right_edge - self.width-1, root.pos[1]+root.height - self.font_size - 1
color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.pos[1]+root.height - self.font_size - 1
text: 'B+{}'.format(root.score_scale)
opacity: int(root.show_score)
GraphMarkerLabel:
font_size: root.marker_font_size
color: SCORE_COLOR
pos: root.right_edge - self.width-1, root.mid - self.height/2 + 2
color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.y + root.height*0.5 - self.height/2 + 2
text: i18n._('Jigo')
opacity: int(root.show_score)
GraphMarkerLabel:
font_size: root.marker_font_size
color: SCORE_COLOR
pos: root.right_edge - self.width-1, root.pos[1]
color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.pos[1]
text: 'W+' + str(int(root.score_scale))
opacity: int(root.show_score)
# wr ticks
GraphMarkerLabel:
font_size: root.marker_font_size
color: WINRATE_COLOR
pos: root.pos[0]+1, root.pos[1]+root.height - self.font_size - 1
color: WINRATE_MARKER_COLOR
pos: root.pos[0]+1, root.pos[1] + root.height - self.font_size - 1
text: "{}%".format(50 + root.winrate_scale)
opacity: int(root.show_winrate)
GraphMarkerLabel:
font_size: root.marker_font_size
color: WINRATE_COLOR
color: WINRATE_MARKER_COLOR
pos:root.pos[0]+1, root.pos[1]
text: "{}%".format(50 - root.winrate_scale)
opacity: int(root.show_winrate)
opacity: int(root.show_winrate)
<RankGraph>:
background_color: LIGHTER_BACKGROUND_COLOR
canvas:
Color:
rgba: WHITE
Line:
points: root.white_rank_points
width: dp(1.1)
Color:
rgba: BLACK
Line:
points: root.black_rank_points
width: dp(1.1)
# rank ticks
GraphMarkerLabel:
id: mid_marker
font_size: root.marker_font_size
color: YELLOW
pos: root.x + root.width - self.width-1, root.y + root.height*0.5 - self.height/2 + 2
text: '?' + i18n._('strength:kyu')
GraphMarkerLabel:
id: top_marker
font_size: root.marker_font_size
color: YELLOW
pos: root.x + root.width - self.width-1, root.pos[1]+root.height - self.font_size - 1
text: '?' + i18n._('strength:kyu')
GraphMarkerLabel:
id: bottom_marker
font_size: root.marker_font_size
color: YELLOW
pos: root.x + root.width - self.width-1, root.pos[1]
text: '?' + i18n._('strength:kyu')
"""
)
+136
View File
@@ -0,0 +1,136 @@
from kivy.lang import Builder
from kivy.properties import BooleanProperty, ListProperty, NumericProperty
from kivy.uix.widget import Widget
from kivymd.uix.label import MDLabel
from kivymd.uix.selectioncontrol import Thumb
class SelectionSlider(Widget):
__events__ = ["on_select", "on_change"]
active = BooleanProperty(False)
hint = BooleanProperty(True)
index = NumericProperty(0) # selected index
values = ListProperty([(0, "")]) # (value:numeric,label:string) pairs
normalized_pos = NumericProperty(0) # slider relative pos from 0-1
px_pos = NumericProperty(0) # actual px pos
padding = NumericProperty("16sp")
track_color = ListProperty([1, 1, 1, 0.3])
thumb_color = ListProperty([0.5, 0.5, 0.5, 1])
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
size=self.set_index_and_positions,
pos=self.set_index_and_positions,
values=self.set_index_and_positions,
index=self.set_index_and_positions,
)
def set_index_and_positions(self, *_args):
self.index = max(0, min(self.index, len(self.values) - 1))
self.normalized_pos = self.index / (len(self.values) - 1)
self.px_pos = self.x + self.padding + self.normalized_pos * (self.width - 2 * self.padding)
@property
def value(self):
return self.values[self.index][0]
def set_value(self, set_value): # set to closest value
eq_value = sorted([(abs(v - set_value), i) for i, (v, l) in enumerate(self.values)])
self.index = eq_value[0][1]
def set_from_pos(self, pos):
norm_value = (pos[0] - self.x - self.padding) / (self.width - 2 * self.padding)
self.index = round(norm_value * (len(self.values) - 1))
self.dispatch("on_change", self.value)
def on_touch_down(self, touch):
if self.disabled or not self.collide_point(*touch.pos):
return
if touch.is_mouse_scrolling:
if "down" in touch.button or "left" in touch.button:
self.index += 1
if "up" in touch.button or "right" in touch.button:
self.index -= 1
else:
touch.grab(self)
self.active = True
self.set_from_pos(touch.pos)
return True
def on_touch_move(self, touch):
if touch.grab_current == self:
self.set_from_pos(touch.pos)
return True
def on_touch_up(self, touch):
if touch.grab_current == self:
self.set_from_pos(touch.pos)
self.active = False
self.dispatch("on_select", self.value)
return True
def on_select(self, value):
pass
def on_change(self, value):
pass
KV = """
#:import i18n katrain.core.lang.i18n
<SelectionSlider>:
id: slider
canvas:
Clear
Color:
rgba:
self.track_color
Rectangle:
size:
(self.width - self.padding*2, dp(4))
pos:
(self.x + self.padding, self.center_y - dp(4))
Color:
rgba:
self.thumb_color
Rectangle:
size:
((self.width-self.padding*2)*self.normalized_pos, sp(4))
pos:
(self.x + self.padding, self.center_y - dp(4))
Thumb:
id: thumb
size_hint: None, None
size:
((dp(24), dp(24)) if root.active else (dp(16), dp(16)))
pos:
(slider.px_pos - dp(8), slider.center_y - thumb.height/2 - dp(2))
color:
root.thumb_color
elevation:
4 if root.active else 2
MDCard:
id: hint_box
size_hint: None, None
md_bg_color: [1, 1, 1, 1] if root.active else [0, 0, 0, 0]
elevation: 4 if root.active else 0
size:
(max(dp(28), label.texture_size[0]+4) , dp(28))
pos:
(slider.px_pos - dp(9), slider.center_y - hint_box.height / 2 + dp(30))
Label:
id: label
text: slider.values[slider.index][1]
font_size: dp(12)
lang_change_tracking: i18n._('') # for font
halign: "center"
color: root.thumb_color if root.active else [0, 0, 0, 0]
"""
Builder.load_string(KV)
Binary file not shown.
+29 -6
View File
@@ -557,14 +557,37 @@ msgid "Starting Kata failed"
msgstr ""
"用'{command}'命令启动katago出错 {error}. 请确认引擎设置里'katago'的值指向了 正确的KataGo运行文件."
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgstr "找到{num} 个可用模型"
#. TODO
msgid "download models button"
msgstr "Download Models"
msgstr "下载模型"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgstr "所有可用模型均已下载"
msgid "tab:rank_est"
msgstr "估计段位"
msgid "game re-analysis"
msgstr "使用 {visits}深度全局再分析"
msgid "analysis:game"
msgstr "深入全盘分析"
msgid "analysis:continuous"
msgstr "开启继续分析"
msgid "ai:handicap"
msgstr "Kata让子棋"
msgid "aihelp:handicap"
msgstr ""
"KataGo 让子棋优化版. pda 设置 对应KataGo里黑棋视角的 playoutDoublingAdvantage. 当设置了 自动, "
"KaTrain 会自动找到合适的值"
msgid "estimated strength"
msgstr "预计棋力"
msgid "reanalyze max visits"
msgstr "每步棋计算深度"
Binary file not shown.
+30 -6
View File
@@ -600,14 +600,38 @@ msgstr "Analyse der Kandidatenzüge wird abgeglichen mit {visits} Visits"
msgid "sweep analysis"
msgstr "Analysiere gesamtes Brett mit {visits} Visits/Zug"
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgstr "{num} verfügbare Modelle gefunden"
#. TODO
msgid "download models button"
msgstr "Download Models"
msgstr "Modelle herunterladen"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgstr "Alle verfügbaren Modelle bereits heruntergeladen"
msgid "tab:rank_est"
msgstr "Rangschätzung"
msgid "game re-analysis"
msgstr "Neuanalyse des gesamten Spiels mit {visits} Visits"
msgid "analysis:game"
msgstr "Tiefere Gesamtspielanalyse"
msgid "analysis:continuous"
msgstr "Kontinuierliche Analyse ein/aus"
msgid "ai:handicap"
msgstr "KataHandicap"
msgid "aihelp:handicap"
msgstr ""
"Optimiertes KataGo für höhere Handicap Spiele.Die pda Einstellungentspricht "
"playoutDoublingAdvantage in KataGo, aus Sicht von Schwarz.Wenn automatic "
"eingestellt ist, wird KaTrain automatischeinen geeigneten Wert finden."
msgid "estimated strength"
msgstr "Geschätzte Spielstärke"
msgid "reanalyze max visits"
msgstr "Anzahl der Visits pro Zug"
Binary file not shown.
@@ -98,6 +98,14 @@ msgstr "Equalize visits of potential moves"
msgid "analysis:sweep"
msgstr "Fast analysis of all moves"
#. Analyze every node in the game more deeply
msgid "analysis:game"
msgstr "Deeper full game analysis"
#. Keep improving current position analysis when there is nothing else to do
msgid "analysis:continuous"
msgstr "Toggle continuous analysis"
msgid "analysis:aimove"
msgstr "Force AI Move"
@@ -113,6 +121,10 @@ msgstr "Analyzing entire board using {visits} visits/move"
msgid "equalizing analysis"
msgstr "Equalizing analysis of candidate moves to {visits} visits"
#. status message
msgid "game re-analysis"
msgstr "Re-analyzing entire game using {visits} visits"
#. right panel buttons and below board
msgid "pass-button-text"
msgstr "Pass"
@@ -139,6 +151,10 @@ msgstr "Score"
msgid "tab:winrate"
msgstr "Win Rate"
#. shown on a graph tab
msgid "tab:rank_est"
msgstr "Rank Estimate"
msgid "tab:points"
msgstr "Point Loss"
@@ -502,6 +518,17 @@ msgstr ""
"in the general settings `engine` section and engine configuration file. No "
"options are available here."
#. ai which handles handicap games better
msgid "ai:handicap"
msgstr "KataHandicap"
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective. When `automatic` is set, KaTrain will find a suitable value "
"automatically."
msgid "ai:jigo"
msgstr "KataJigo"
@@ -593,11 +620,22 @@ msgstr ""
"one. Stronger settings select the best move from a larger selection. Since "
"there is no 0 kyu/dan, 3 dan = -2 kyu."
#. in AI settings
msgid "estimated strength"
msgstr "Estimated Strength"
#. button in general settings for downloading models
msgid "download models button"
msgstr "Download Models"
#. shown on on the model select dropdown
msgid "models available"
msgstr "{num} available model(s) found"
#. error message on trying to download models when already done
msgid "All models downloaded"
msgstr "All available models already downloaded"
#. label in little popup for analyzing entire game
msgid "reanalyze max visits"
msgstr "Number of visits per move"
Binary file not shown.
+53 -28
View File
@@ -23,7 +23,7 @@ msgid "menu:settings"
msgstr "Conf General y Motores"
msgid "menu:clocksettings"
msgstr "Conf de Reloj"
msgstr "Conf Reloj"
msgid "menu:teachsettings"
msgstr "Conf Juego Enseñanza"
@@ -93,10 +93,10 @@ msgid "analysis:extra"
msgstr "Profundizar Análisis"
msgid "analysis:equalize"
msgstr "Igualar visitas de potenciales movimientos"
msgstr "Igualar visitas potenciales movimientos"
msgid "analysis:sweep"
msgstr "Análisis rápido de todos los movimientos."
msgstr "Análisis rápido todos los movimientos"
msgid "analysis:aimove"
msgstr "Forzar jugada IA"
@@ -297,7 +297,7 @@ msgstr ""
#. hints on new game
msgid "non square board hint"
msgstr "Usa x:y (e.g. 19:9) para tableros no cuadrados"
msgstr "Usa x:y (i.e. 19:9) para tableros no cuadrados"
#. submit new game
msgid "new game"
@@ -305,16 +305,16 @@ msgstr "Empezar Nuevo Juego"
# teacher settings from here
msgid "teacher settings"
msgstr "Editar Conf de Profesor"
msgstr "Editar Conf Enseñanza"
msgid "point loss threshold"
msgstr "Pérdidas de puntaje mayores a"
msgstr "Pérdida de puntaje mayor a"
msgid "num undos"
msgstr "Número de jugadas desechas"
msgid "dot color"
msgstr "Color (corregido)"
msgstr "Color"
msgid "show dots"
msgstr "Mostrar Puntos"
@@ -339,7 +339,7 @@ msgstr ""
#. submit button
msgid "update teacher"
msgstr "Actualizar conf de retroalimentación"
msgstr "Actualizar conf"
# main config settings
msgid "restarting engine"
@@ -469,7 +469,7 @@ msgid "aihelp:default"
msgstr ""
"Fuerza máxima de KataGo IA. La fuerza es afectada por el número `máximo "
"visitas` y `modelo` en conf general del `motor` y el archivo de conf del "
"motor. No hay opciones disponibles acá."
"motor. No hay opciones disponibles acá."
msgid "ai:jigo"
msgstr "KataJigo"
@@ -479,12 +479,12 @@ msgstr ""
"Trata de ganar por una cantidad de puntos (default 0.5), sin restriciones."
msgid "ai:scoreloss"
msgstr "Perdia de puntos"
msgstr "Perdida de puntos"
msgid "aihelp:scoreloss"
msgstr ""
"Jugadas que conllevan a mayor pérdida de puntos con una menor probabilidad. "
"Probablemente lleva a jugada mas variadasçdebiles con un mayor número de "
"Las jugadas que conllevan a mayor pérdida de puntos se juegan con una menor "
"probabilidad. Probablemente juega mas variado/débil con un mayor número de "
"visitas."
msgid "ai:policy"
@@ -492,9 +492,9 @@ msgstr "Jugadas intuitivas"
msgid "aihelp:policy"
msgstr ""
"Juega la jugada que se considera la más intuivida sin leer. La fuerza esta "
"dada por el `model` en la conf del motor. El parámetro opening_moves afecta"
" cuantas jugadas en la apertura son al azar."
"Juega la jugada que se considera la más intuitvda sin leer. La fuerza esta "
"dada por el `model` en la conf del motor. El parámetro `opening_moves` "
"afecta cuantas jugadas en la apertura son al azar."
msgid "ai:p:weighted"
msgstr "Parcial"
@@ -512,10 +512,10 @@ msgstr "Politica Ciega"
msgid "aihelp:p:pick"
msgstr ""
"Escoge `pick_n + pick_frac * <#jugadas legales>` al azar y juega la "
"mejor.For `pick_frac=1` juega como la politica intuitia del IA. Juega la "
"mejor jugada si la probabilidad de la politica es por encima de "
"`pick_override`. Esto con el fin de prevenir errores obvios."
"Escoge `pick_n + pick_frac * <#jugadas legales>` al azar y juega la mejor.El"
" `pick_frac=1` juega como la politica intuitiva del IA. Juega la mejor "
"jugada si la probabilidad del IA es por encima de `pick_override`. Esto con "
"el fin de prevenir errores obvios."
msgid "ai:p:local"
msgstr "Estilo Local"
@@ -557,9 +557,9 @@ msgstr ""
msgid "aihelp:p:rank"
msgstr ""
"Seleciona jugadas al azar de una limitada selección y juega la "
"mejorConfiguraciones más fuertes amplia su selección y juega la mejor "
"jugada. Como no hay 0 kyu/dan, 3 dan = -2 kyu."
"Seleciona jugadas al azar de una limitada selección y juega la mejor. "
"Configuraciones más fuertes toma la mejor jugada de una selección más amplia"
" de jugadas. Como no hay 0 kyu/dan, 3 dan = -2 kyu."
msgid "Kata model not found"
msgstr "El modelo KataGo {model} no existe, revisar conf general."
@@ -595,14 +595,39 @@ msgstr ""
msgid "extra analysis"
msgstr "Realizar análisis adicional a {visits} visitas"
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgstr "{num} de modelo(s) disponibles"
#. TODO
msgid "download models button"
msgstr "Download Models"
msgstr "Descargar Modelos"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgstr "Todos los modelos fueron descargados"
msgid "tab:rank_est"
msgstr "Estimación fuerza"
msgid "game re-analysis"
msgstr "Re-analizar juego usando {visits} visitas"
msgid "analysis:game"
msgstr "Re-analizar juego"
msgid "analysis:continuous"
msgstr "Analizar continuamente "
msgid "ai:handicap"
msgstr "KataVentaja"
msgid "aihelp:handicap"
msgstr ""
"KataGo optimizado para jugar con ventaja al oponente. El parámetro `pda` "
"corresponde a `playoutDoublingAdvantage` en KataGo, desde la perspectiva de "
"negro.Cuando `automatic` está establecido, KaTrain encontrará un valor "
"adecuado automáticamente."
msgid "estimated strength"
msgstr "Fuerza Estimada"
msgid "reanalyze max visits"
msgstr "Número de visitas por jugada"
Binary file not shown.
+37 -10
View File
@@ -98,9 +98,19 @@ msgstr "Égaliser l’analyse des coups"
msgid "analysis:sweep"
msgstr "Analyser tous les possibles"
msgid "analysis:game"
msgstr "Analyser davantage la partie"
msgid "analysis:continuous"
msgstr "Lancer/Arrêter l'analyse continue"
msgid "analysis:aimove"
msgstr "Jouer le coup de l’IA"
#. label in little popup for analyzing entire game
msgid "reanalyze max visits"
msgstr "Nombre de visites par coup"
#. right panel buttons and below board
msgid "pass-button-text"
msgstr "Passer"
@@ -121,6 +131,9 @@ msgstr "Score"
msgid "tab:winrate"
msgstr "Pronostic"
msgid "tab:rank_est"
msgstr "Niveau"
msgid "tab:points"
msgstr "Pertes"
@@ -382,6 +395,15 @@ msgstr ""
"Chemin d'accès du \n"
"fichier de configuration"
msgid "models available"
msgstr "{num} modèle(s) disponible(s) pour le réseau neuronal"
msgid "download models button"
msgstr "Télécharger des modèles"
msgid "All models downloaded"
msgstr "Pas de nouveaux modèles disponibles pour le réseau neuronal"
msgid "engine:max_visits"
msgstr ""
"Maximum de visites \n"
@@ -448,6 +470,9 @@ msgstr "Joueurs artificiels"
msgid "Select AI"
msgstr "Style de jeu :"
msgid "estimated strength"
msgstr "Niveau estimé"
msgid "update ai settings"
msgstr "Mettre à jour"
@@ -516,6 +541,9 @@ msgstr "Analyse supplémentaire de {visits} visites."
msgid "equalizing analysis"
msgstr "Égalisation de l'analyse ({visits} visites/coup considéré)."
msgid "game re-analysis"
msgstr "Analyse supplémentaire de la partie ({visits} visites/coup)."
#. AI names, help etc
msgid "strength:kyu"
msgstr "k"
@@ -634,14 +662,13 @@ msgstr ""
"Une valeur positive équivaudra à un niveau en kyu, 0 à 1er dan, -1 à 2e dan, etc. \n"
"(Niveaux estimés sur OGS avec le réseau neuronal utilisé PAR DÉFAUT)"
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgid "ai:handicap"
msgstr "Par handicap"
#. TODO
msgid "download models button"
msgstr "Download Models"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgid "aihelp:handicap"
msgstr ""
"KataGo recherchera/évitera les complications \n"
"en fonction de la différence de niveau supposée des joueurs. \n"
"Tout à gauche, un PDA de -3 (W+3) supposera que Blanc est bien plus fort que Noir. \n"
"Tout à droite, un PDA de 3 (B+3) supposera que Noir est bien plus fort que Blanc. \n"
"Cocher l'option 'automatic' adaptera la valeur PDA au handicap utilisé."
Binary file not shown.
+30 -6
View File
@@ -572,14 +572,38 @@ msgstr "모든 후보수를 균일하게 {visits} visits으로 분석합니다."
msgid "extra analysis"
msgstr "{visits} visits까지 추가적으로 분석하고 있습니다."
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgstr "사용할 수 있는 {num}개의 가중치가 있습니다."
#. TODO
msgid "download models button"
msgstr "Download Models"
msgstr "가중치 내려받기"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgstr "사용할 수 있는 가중치를 이미 모두 내려받았습니다."
msgid "tab:rank_est"
msgstr "추정 기력"
msgid "game re-analysis"
msgstr "대국 전체를 {visits} visits로 다시 분석하고 있습니다."
msgid "analysis:game"
msgstr "대국 전체를 다시 분석하기"
msgid "analysis:continuous"
msgstr "분석 계속하기/멈추기"
msgid "ai:handicap"
msgstr "접바둑 KataGo"
msgid "aihelp:handicap"
msgstr ""
"치석이 많은 접바둑에 더 적합한 KataGo입니다. `pda` 설정은 흑 시점에서 KataGo의 "
"`playoutDoublingAdvantage`에 해당합니다. `automatic`으로 설정하면, KaTrain이 알맞는 값을 자동으로 "
"설정할 것입니다."
msgid "estimated strength"
msgstr "추정 기력"
msgid "reanalyze max visits"
msgstr "한 수당 visit 수"
Binary file not shown.
+30 -6
View File
@@ -597,14 +597,38 @@ msgstr ""
"Более сильные настройки расширяют множество для выбора. Т.к. на шкале "
"отсутствуют 0 кю/дан, то 3 дан = -2 кю."
#. TODO
msgid "models available"
msgstr "{num} available model(s) found"
msgstr "{num} доступных моделей найдено"
#. TODO
msgid "download models button"
msgstr "Download Models"
msgstr "Скачать модели"
#. TODO
msgid "All models downloaded"
msgstr "All available models already downloaded"
msgstr "Все доступные модели уже загружены"
msgid "tab:rank_est"
msgstr "Оценка ранга"
msgid "game re-analysis"
msgstr "Повторить анализ всей игры, используя {visits} посещений"
msgid "analysis:game"
msgstr "Более глубокий анализ всей игры"
msgid "analysis:continuous"
msgstr "Непрерывный анализ"
msgid "ai:handicap"
msgstr "KataHandicap"
msgid "aihelp:handicap"
msgstr ""
"KataGo оптимизированный под игры с большой форой. Настройка `pda` "
"соответствует `playoutDoublingAdvantage` в KataGo, с точки зрения чёрных. "
"При установке `automatic`, KaTrain выберет значение автоматически."
msgid "estimated strength"
msgstr "Оценка силы"
msgid "reanalyze max visits"
msgstr "Количество посещений на ход"
Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+55 -8
View File
@@ -13,6 +13,7 @@
#:set LIGHTGREY [0.7,0.7,0.7,1]
#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR
#:import LIGHTGREY katrain.gui.style.LIGHTGREY
#:import TEXT_COLOR katrain.gui.style.TEXT_COLOR
@@ -48,7 +49,6 @@
size: DESC_FONT_SIZE*2,DESC_FONT_SIZE*2
size_hint: None,None
<QuickInputButton@SizedRectangleButton>
on_left_press: if self.target: self.target.text = self.text
size: DESC_FONT_SIZE * 2, DESC_FONT_SIZE * 2
@@ -62,7 +62,6 @@
title_font: self.font_name
size_hint: None, None
<QuickConfigGui>:
orientation: 'vertical'
spacing: CP_SPACING
@@ -88,8 +87,6 @@
font_size: DESC_FONT_SIZE * 1.5
GridLayout:
cols: 2
rows: 3
size_hint: 1,3
rows: 4
size_hint: 1,4
spacing: CP_SPACING
@@ -365,10 +362,13 @@
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:
@@ -395,10 +395,23 @@
on_press: root.update_config(True)
<LabelledSelectionSlider>:
slider: slider
textbox: textbox
SelectionSlider:
id: slider
size_hint: 2,1
values: root.values
on_change: textbox.text = str(slider.value)
track_color: LIGHTGREY
thumb_color: (0.6, 0.7, 0.9, 1) if root.key_option else (0.3, 0.4, 0.6, 1)
LabelledFloatInput:
id: textbox
<AIPopup>
<ConfigAIPopup>:
options_grid: options_grid
help_label: help_label
estimated_rank_label: estimated_rank_label
ai_select: ai_select
BoxLayout:
size_hint: 1,1
@@ -409,17 +422,28 @@
I18NSpinner:
size_hint: 0.8, 0.5
id: ai_select
BoxLayout:
size_hint: 1,1
DescriptionLabel:
font_size: DESC_FONT_SIZE
size_hint: 1, 1
text: i18n._('estimated strength')
DescriptionLabel:
size_hint: 0.8, 1
font_size: DESC_FONT_SIZE
id: estimated_rank_label
text: '?' + i18n._('strength:kyu')
SmallDescriptionLabel:
id: help_label
size_hint: 1,1.25
text: i18n._('')
GridLayout:
size_hint: 1,4
size_hint: 1, 4
id: options_grid
rows: root.max_options
cols: 2
AnchorLayout:
size_hint: 1,1.5
size_hint: 1, 1.5
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,0.5
@@ -427,6 +451,29 @@
on_press: root.update_config(True)
<ReAnalyzeGamePopup>:
visits: visits
orientation: 'vertical'
BoxLayout:
size_hint: 1, 1.5
orientation: 'horizontal'
DescriptionLabel:
text: i18n._('reanalyze max visits')
AnchorLayout:
LabelledIntInput:
id: visits
text: '2500'
AnchorLayout:
size_hint: 1, 1
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,0.5
text: i18n._("analysis:game") # re-use string
on_press:
root.katrain("analyze-extra", "game", visits=visits.input_value)
root.popup.dismiss()
<LoadSGFPopup>:
fast: fast
rewind: rewind
@@ -455,6 +502,6 @@
I18NFileBrowser:
id: filesel
multiselect: False
filters: ["*.sgf"]
filters: ["*.sgf","*.gib"]
path: "."
size_hint: 1,7
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
certifi==2020.6.20
chardet==3.0.4
Cython==0.29.14
dataclasses==0.6
docutils==0.16
idna==2.10
Kivy==2.0.0rc2
Kivy-Garden==0.1.4
kivymd==0.104.1
Pillow==7.2.0
pygame==1.9.6
Pygments==2.6.1
requests==2.24.0
urllib3==1.25.9
+2 -2
View File
@@ -41,12 +41,12 @@ setup(
"wheel",
"setuptools",
"importlib_resources ;python_version<'3.7'",
"pygame", # some versions need this for kivy
"pygame", # some mac versions need this for kivy
"cython>=0.24,<=0.29.14,!=0.27,!=0.27.2", # kivy wants this
"kivy_deps.glew;platform_system=='Windows'",
"kivy_deps.sdl2;platform_system=='Windows'",
"kivy_deps.gstreamer;platform_system=='Windows'",
"kivy>=2.0.0rc2",
"kivy==2.0.0rc2", # rc3 failing on mac
"kivymd>=0.104.1",
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
],
+3 -3
View File
@@ -14,13 +14,13 @@ a = Analysis(
datas=[
("..\\katrain\\gui.kv", "katrain"),
("..\\katrain\\popups.kv", "katrain"),
("..\\katrain\\beep.wav", "katrain"),
("..\\katrain\\config.json", "katrain"),
("..\\katrain\\KataGo", "katrain\\KataGo"),
("..\\katrain\img", "katrain\\img"),
("..\\katrain\\models", "katrain\\models"),
("..\\katrain\\i18n", "katrain\\i18n"),
("..\\katrain\\sounds", "katrain\\sounds"),
("..\\katrain\img", "katrain\\img"),
("..\\katrain\\fonts", "katrain\\fonts"),
("..\\katrain\\i18n", "katrain\\i18n"),
],
hiddenimports=["win32file", "win32timezone"], # FileChooser in kivy loads this conditionally
hookspath=[kivymd_hooks_path],
+271
View File
@@ -0,0 +1,271 @@
\HS
\[GIBOKIND=Global\]
\[TYPE=0\]
\[SZAUDIO=0\]
\[GAMECONDITION=Even : Black 6.5 Dum\]
\[GAMETIME=Time limit 10minute : 30 second countdown 3 time\]
\[GAMERESULT=white wins by time\]
\[GAMEZIPSU=0\]
\[GAMEDUM=0\]
\[GAMEGONGJE=65\]
\[GAMETOTALNUM=232\]
\[GAMENAME=�°��� �뱹\]
\[GAMEDATE=2020- 6-14- 1-26-14\]
\[GAMEPLACE=Ÿ���� �ٵ�\]
\[GAMELECNAME=\]
\[GAMEWHITENAME=wildsim1 (2D)\]
\[GAMEWHITELEVEL=19\]
\[GAMEWHITENICK=wildsim1\]
\[GAMEWHITECOUNTRY=0\]
\[GAMEWAVATA=60011\]
\[GAMEWIMAGE=\]
\[GAMEBLACKNAME=kim (2D)\]
\[GAMEBLACKLEVEL=19\]
\[GAMEBLACKNICK=kim\]
\[GAMEBLACKCOUNTRY=3\]
\[GAMEBAVATA=114\]
\[GAMEBIMAGE=\]
\[GAMECOMMENT=\]
\[GAMEINFOMAIN=GBKIND:3,GTYPE:0,GCDT:0,GTIME:600-30-3,GRLT:8,ZIPSU:0,DUM:0,GONGJE:65,TCNT:232,AUSZ:0\]
\[GAMEINFOSUB=GNAMEF:0,GPLCF:0,GNAME:�°��� �뱹,GDATE:2020- 6-14- 1-26-14,GPLC:Ÿ���� �ٵ�,GCMT:\]
\[WUSERINFO=WID:楷摬楳ㅭ,WLV:19,WNICK:楷摬楳ㅭ,WNCD:0,WAID:60011,WIMG:\]
\[BUSERINFO=BID:楫獭慳浥,BLV:19,BNICK:楫獭慳浥,BNCD:3,BAID:114,BIMG:\]
\[GAMETAG=S0,R0,D0,G65,W8,Z0,T30-3-600, C2020:06:14:01:26, I:楷摬楳ㅭ,L:19,M:楫獭慳浥,N:19,A:楷摬楳ㅭ,B:楫獭慳浥,J:0,K:3\]
\HE
\GS
2 5 0
233 0 &4
INI 0 1 0 &4
STO 0 2 1 15 3
STO 0 3 2 3 15
STO 0 4 1 16 15
STO 0 5 2 13 2
STO 0 6 1 3 3
STO 0 7 2 2 5
STO 0 8 1 5 2
STO 0 9 2 1 3
STO 0 10 1 2 2
STO 0 11 2 15 1
STO 0 12 1 14 2
STO 0 13 2 14 1
STO 0 14 1 13 3
STO 0 15 2 12 2
STO 0 16 1 12 3
STO 0 17 2 11 2
STO 0 18 1 13 16
STO 0 19 2 16 9
STO 0 20 1 16 7
STO 0 21 2 16 12
STO 0 22 1 2 13
STO 0 23 2 2 11
STO 0 24 1 2 16
STO 0 25 2 3 16
STO 0 26 1 2 15
STO 0 27 2 3 14
STO 0 28 1 2 14
STO 0 29 2 4 12
STO 0 30 1 9 15
STO 0 31 2 8 16
STO 0 32 1 8 15
STO 0 33 2 9 16
STO 0 34 1 10 16
STO 0 35 2 10 17
STO 0 36 1 11 16
STO 0 37 2 11 17
STO 0 38 1 7 16
STO 0 39 2 7 17
STO 0 40 1 6 17
STO 0 41 2 8 17
STO 0 42 1 6 15
STO 0 43 2 5 16
STO 0 44 1 6 16
STO 0 45 2 12 16
STO 0 46 1 12 15
STO 0 47 2 12 17
STO 0 48 1 14 15
STO 0 49 2 13 17
STO 0 50 1 14 17
STO 0 51 2 13 14
STO 0 52 1 13 15
STO 0 53 2 2 17
STO 0 54 1 1 17
STO 0 55 2 3 17
STO 0 56 1 7 12
STO 0 57 2 1 18
STO 0 58 1 1 12
STO 0 59 2 1 11
STO 0 60 1 0 17
STO 0 61 2 2 12
STO 0 62 1 0 13
STO 0 63 2 6 13
STO 0 64 1 14 12
STO 0 65 2 17 14
STO 0 66 1 7 13
STO 0 67 2 17 15
STO 0 68 1 16 16
STO 0 69 2 15 8
STO 0 70 1 15 11
STO 0 71 2 16 11
STO 0 72 1 6 12
STO 0 73 2 6 14
STO 0 74 1 7 14
STO 0 75 2 5 15
STO 0 76 1 4 11
STO 0 77 2 5 12
STO 0 78 1 5 11
STO 0 79 2 3 11
STO 0 80 1 3 13
STO 0 81 2 4 13
STO 0 82 1 3 12
STO 0 83 2 5 14
STO 0 84 1 15 7
STO 0 85 2 2 8
STO 0 86 1 14 8
STO 0 87 2 15 10
STO 0 88 1 14 10
STO 0 89 2 14 9
STO 0 90 1 17 10
STO 0 91 2 13 10
STO 0 92 1 14 11
STO 0 93 2 13 8
STO 0 94 1 16 10
STO 0 95 2 15 9
STO 0 96 1 17 9
STO 0 97 2 17 16
STO 0 98 1 16 13
STO 0 99 2 17 13
STO 0 100 1 16 14
STO 0 101 2 17 11
STO 0 102 1 14 7
STO 0 103 2 13 7
STO 0 104 1 13 6
STO 0 105 2 13 9
STO 0 106 1 16 8
STO 0 107 2 11 7
STO 0 108 1 12 6
STO 0 109 2 10 10
STO 0 110 1 12 7
STO 0 111 2 11 8
STO 0 112 1 12 8
STO 0 113 2 11 9
STO 0 114 1 12 11
STO 0 115 2 12 10
STO 0 116 1 11 11
STO 0 117 2 16 2
STO 0 118 1 16 3
STO 0 119 2 17 2
STO 0 120 1 17 3
STO 0 121 2 8 10
STO 0 122 1 11 10
STO 0 123 2 12 9
STO 0 124 1 10 9
STO 0 125 2 9 9
STO 0 126 1 10 8
STO 0 127 2 9 7
STO 0 128 1 10 11
STO 0 129 2 9 10
STO 0 130 1 3 10
STO 0 131 2 2 10
STO 0 132 1 3 9
STO 0 133 2 2 9
STO 0 134 1 8 8
STO 0 135 2 9 8
STO 0 136 1 8 2
STO 0 137 2 1 2
STO 0 138 1 1 1
STO 0 139 2 2 3
STO 0 140 1 3 2
STO 0 141 2 3 4
STO 0 142 1 4 4
STO 0 143 2 4 5
STO 0 144 1 5 4
STO 0 145 2 5 5
STO 0 146 1 6 4
STO 0 147 2 6 5
STO 0 148 1 7 4
STO 0 149 2 6 11
STO 0 150 1 7 11
STO 0 151 2 6 10
STO 0 152 1 7 10
STO 0 153 2 7 9
STO 0 154 1 6 9
STO 0 155 2 5 10
STO 0 156 1 8 9
STO 0 157 2 8 7
STO 0 158 1 7 8
STO 0 159 2 4 10
STO 0 160 1 7 7
STO 0 161 2 10 5
STO 0 162 1 10 2
STO 0 163 2 11 3
STO 0 164 1 11 6
STO 0 165 2 10 6
STO 0 166 1 11 4
STO 0 167 2 10 3
STO 0 168 1 10 4
STO 0 169 2 9 4
STO 0 170 1 9 3
STO 0 171 2 11 5
STO 0 172 1 12 4
STO 0 173 2 8 5
STO 0 174 1 8 4
STO 0 175 2 9 5
STO 0 176 1 7 5
STO 0 177 2 8 6
STO 0 178 1 10 1
STO 0 179 2 15 2
STO 0 180 1 14 3
STO 0 181 2 11 1
STO 0 182 1 17 17
STO 0 183 2 18 11
STO 0 184 1 18 16
STO 0 185 2 18 15
STO 0 186 1 18 13
STO 0 187 2 18 17
STO 0 188 1 17 18
STO 0 189 2 18 12
STO 0 190 1 18 2
STO 0 191 2 18 1
STO 0 192 1 18 3
STO 0 193 2 10 0
STO 0 194 1 9 1
STO 0 195 2 17 0
STO 0 196 1 13 18
STO 0 197 2 12 18
STO 0 198 1 14 18
STO 0 199 2 5 17
STO 0 200 1 7 18
STO 0 201 2 8 18
STO 0 202 1 6 18
STO 0 203 2 5 18
STO 0 204 1 7 15
STO 0 205 2 9 18
STO 0 206 1 12 0
STO 0 207 2 13 0
STO 0 208 1 9 0
STO 0 209 2 0 1
STO 0 210 1 11 0
STO 0 211 2 13 1
STO 0 212 1 1 0
STO 0 213 2 5 7
STO 0 214 1 5 8
STO 0 215 2 4 8
STO 0 216 1 5 9
STO 0 217 2 4 9
STO 0 218 1 9 11
STO 0 219 2 8 11
STO 0 220 1 8 12
STO 0 221 2 12 5
STO 0 222 1 13 5
STO 0 223 2 18 9
STO 0 224 1 18 8
STO 0 225 2 18 10
STO 0 226 1 17 8
STO 0 227 2 13 11
STO 0 228 1 13 12
STO 0 229 2 10 7
STO 0 230 1 15 12
STO 0 231 2 18 14
STO 0 232 1 6 7
STO 0 233 2 6 6
\GE
+8 -1
View File
@@ -1,6 +1,6 @@
import pytest
import os
from katrain.core.ai import generate_ai_move
from katrain.core.ai import generate_ai_move, ai_rank_estimation
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, AI_STRATEGIES, OUTPUT_INFO
from katrain.core.base_katrain import KaTrainBase
from katrain.core.engine import KataGoEngine
@@ -35,3 +35,10 @@ class TestAI:
move, played_node = generate_ai_move(game, strategy, settings)
katrain.log(f"Testing strategy on first move {strategy} -> {move}", OUTPUT_INFO)
assert game.current_node.depth == 1
def test_ai_rank_estimation(self):
katrain = KaTrainBase(force_package_config=True, debug_level=0)
for strategy in AI_STRATEGIES:
settings = katrain.config(f"ai/{strategy}")
rank = ai_rank_estimation(strategy, settings)
assert -20 <= rank <= 9
+16 -14
View File
@@ -2,27 +2,29 @@ import pytest
from katrain.core.game import Game, IllegalMoveException, Move
from katrain.core.base_katrain import KaTrainBase, OUTPUT_INFO
from katrain.core.game_node import GameNode
class MockKaTrain(KaTrainBase):
pass
# def log(self, message, level=OUTPUT_INFO):
# pass
class MockEngine:
def request_analysis(self, *args, **kwargs):
pass
@pytest.fixture
def new_game():
return GameNode(properties={"SZ": 19})
class TestBoard:
def nonempty_chains(self, b):
return [c for c in b.chains if c]
def test_merge(self):
b = Game(MockKaTrain(), MockEngine())
def test_merge(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("B9", player="B"))
b.play(Move.from_gtp("A3", player="B"))
b.play(Move.from_gtp("A9", player="B"))
@@ -30,8 +32,8 @@ class TestBoard:
assert 3 == len(b.stones)
assert 0 == len(b.prisoners)
def test_collide(self):
b = Game(MockKaTrain(), MockEngine())
def test_collide(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("B9", player="B"))
with pytest.raises(IllegalMoveException):
b.play(Move.from_gtp("B9", player="W"))
@@ -39,8 +41,8 @@ class TestBoard:
assert 1 == len(b.stones)
assert 0 == len(b.prisoners)
def test_capture(self):
b = Game(MockKaTrain(), MockEngine())
def test_capture(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("A2", player="B"))
b.play(Move.from_gtp("B1", player="W"))
b.play(Move.from_gtp("A1", player="W"))
@@ -60,8 +62,8 @@ class TestBoard:
assert 4 == len(b.stones)
assert 2 == len(b.prisoners)
def test_snapback(self):
b = Game(MockKaTrain(), MockEngine())
def test_snapback(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
for move in ["C1", "D1", "E1", "C2", "D3", "E4", "F2", "F3", "F4"]:
b.play(Move.from_gtp(move, player="B"))
for move in ["D2", "E2", "C3", "D4", "C4"]:
@@ -78,8 +80,8 @@ class TestBoard:
assert 12 == len(b.stones)
assert 4 == len(b.prisoners)
def test_ko(self):
b = Game(MockKaTrain(), MockEngine())
def test_ko(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
for move in ["A2", "B1"]:
b.play(Move.from_gtp(move, player="B"))
+20 -5
View File
@@ -5,7 +5,7 @@ from katrain.core.sgf_parser import SGF, SGFNode
def test_simple():
input_sgf = "(;GM[1]FF[4]SZ[19]DT[2020-04-12]AB[dd][dj];B[dp];W[pp];B[pj])"
root = SGF.parse(input_sgf)
root = SGF.parse_sgf(input_sgf)
assert "4" == root.get_property("FF")
assert root.get_property("XYZ") is None
assert "dp" == root.children[0].get_property("B")
@@ -14,13 +14,13 @@ def test_simple():
def test_branch():
input_sgf = "(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]AB[dd][dj](;B[dp];W[pp](;B[pj])(;PL[B]AW[jp]C[sdfdsfdsf]))(;B[pd]))"
root = SGF.parse(input_sgf)
root = SGF.parse_sgf(input_sgf)
assert input_sgf == root.sgf()
def test_dragon_weirdness(): # dragon go server has weird line breaks
input_sgf = "\n(\n\n;\nGM[1]\nFF[4]\nCA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]AB[dd]\n[dj]\n(\n;\nB[dp]\n;\nW[pp]\n(\n;\nB[pj]\n)\n(\n;\nPL[B]\nAW[jp]\nC[sdfdsfdsf]\n)\n)\n(\n;\nB[pd]\n)\n)\n"
root = SGF.parse(input_sgf)
root = SGF.parse_sgf(input_sgf)
assert input_sgf.replace("\n", "") == root.sgf()
@@ -29,7 +29,7 @@ def test_weird_escape():
[
or \\]
])"""
root = SGF.parse(input_sgf)
root = SGF.parse_sgf(input_sgf)
assert input_sgf == root.sgf()
@@ -44,7 +44,7 @@ def test_backslash_escape():
node = SGFNode(properties={"C1": nasty_string})
node.set_property("C2", c2)
assert "(;C1[[\\]\\]\\\\]C2[\\]][\\\\])" == node.sgf()
assert {"C1": [nasty_string], "C2": c2} == SGF.parse(node.sgf()).properties
assert {"C1": [nasty_string], "C2": c2} == SGF.parse_sgf(node.sgf()).properties
def test_alphago():
@@ -94,3 +94,18 @@ def test_pandanet():
def test_ogs():
file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf")
tree = SGF.parse_file(file)
def test_gibo():
file = os.path.join(os.path.dirname(__file__), "data/test.gib")
root = SGF.parse_file(file)
assert {
"PW": ["wildsim1"],
"WR": ["2D"],
"PB": ["kim"],
"BR": ["2D"],
"RE": ["W+T"],
"KM": [6.5],
"DT": ["2020-06-14"],
} == root.properties
assert "pd" == root.children[0].get_property("B")