No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,69 +1,105 @@
|
||||
# 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.
|
||||
@@ -81,7 +117,8 @@ The above devices were found on a 2019 MacBook Pro with both an on-motherboard g
|
||||
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,7 +126,7 @@ 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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
# <a name="manual"></a> KaTrain v1.2
|
||||
# <a name="manual"></a> KaTrain
|
||||
|
||||
[](https://github.com/sanderland/katrain/releases)
|
||||
[](https://en.wikipedia.org/wiki/MIT_License)
|
||||
@@ -9,6 +8,19 @@
|
||||
[](https://liberapay.com/sanderbaduk/)
|
||||
[](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:
|
||||
|:-----------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:|
|
||||
| [](https://www.youtube.com/watch?v=wFl4Bab_eGM) | [](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
|
||||
|
||||
[](https://github.com/sanderland/katrain/issues)
|
||||
[](CONTRIBUTORS)
|
||||
[](CONTRIBUTIONS.md)
|
||||
[](https://liberapay.com/sanderbaduk/)
|
||||
[](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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +353,13 @@ 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
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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,6 +161,7 @@ class Game:
|
||||
except IllegalMoveException:
|
||||
self._calculate_groups()
|
||||
raise
|
||||
with self._lock:
|
||||
played_node = self.current_node.play(move)
|
||||
self.current_node = played_node
|
||||
if analyze:
|
||||
@@ -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":
|
||||
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))
|
||||
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()
|
||||
@@ -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"]
|
||||
|
||||
@@ -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')}"
|
||||
@@ -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:
|
||||
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 = "utf8" # ?
|
||||
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
|
||||
return cls.parse(decoded)
|
||||
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
|
||||
@@ -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:
|
||||
size_hint: 1,0.6
|
||||
text: i18n._(root.player_type)
|
||||
size_hint: 1,0.55
|
||||
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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
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.update_evaluation()
|
||||
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
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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))
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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>:
|
||||
<Graph>:
|
||||
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
|
||||
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)
|
||||
|
||||
<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')
|
||||
"""
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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 "每步棋计算深度"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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é."
|
||||
@@ -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 수"
|
||||
@@ -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 "Количество посещений на ход"
|
||||
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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")
|
||||