No files matched your search
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
ignore = E501, E203, W503, E402 # line length, space before binary op, line break before binary op, import not at top
|
||||
exclude = .git,__pycache__,build,dist
|
||||
@@ -11,6 +11,14 @@ This is not enforced, and contributions with incorrect formatting will be accept
|
||||
|
||||
## Translations
|
||||
|
||||
### Contributing to an existing translation
|
||||
|
||||
* Go [here](https://github.com/sanderland/katrain/blob/master/katrain/i18n/locales/) and locate the `.po` file for your language.
|
||||
* Alternatively, find the same file in the branch for the next version.
|
||||
* Correct the relevant `msgstr` entries.
|
||||
|
||||
### Adding a translation
|
||||
|
||||
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)
|
||||
@@ -19,19 +27,6 @@ Adding a translation requires making a new `.po` file with entries for that lang
|
||||
* 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
|
||||
|
||||
@@ -53,6 +48,8 @@ Many thanks to these additional authors:
|
||||
* "kaorahi" for bug fixes and SGF parser improvements.
|
||||
* "ajkenny84" for the red-green colourblind theme.
|
||||
* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
|
||||
* Carton He for a fix to handling empty komi values in sgfs.
|
||||
* "blamarche" for adding the board coordinates toggle
|
||||
|
||||
## Translators
|
||||
|
||||
@@ -60,7 +57,7 @@ Many thanks to the following contributors for translations.
|
||||
|
||||
* French: "Dontbtme" with contributions from "wonderingabout"
|
||||
* Korean: "isty2e"
|
||||
* German: "nimets123" with contributions from "trohde"
|
||||
* German: "nimets123", "trohde", and "Harleqin"
|
||||
* Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community
|
||||
* Russian: Dmitry Ivankov and Alexander Kiselev
|
||||
* Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin
|
||||
|
||||
@@ -44,7 +44,7 @@ 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).
|
||||
* Make sure you have a python installation, I will assume Anaconda (Python 3.7/3.8), 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 command prompt.
|
||||
@@ -53,7 +53,7 @@ Simply download and run, everything is included.
|
||||
|
||||
### <a name="LinuxQuick"></a>Quick install guide
|
||||
|
||||
If you have a working Python 3.6/3.7/3.8 available, you should be able to simply:
|
||||
If you have a working Python 3.6-3.8 available, you should be able to simply:
|
||||
|
||||
* Run `pip3 install -U katrain`
|
||||
* Run the program by executing `katrain` in a terminal.
|
||||
@@ -94,11 +94,12 @@ Then, try installing python package dependencies using:
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install screeninfo # Skip on MacOS, not working
|
||||
```
|
||||
In case the sound is not working, or there is no available wheel for your OS or Python version, try:
|
||||
In case the sound is not working, or there is no available wheel for your OS or Python version, try building kivy locally using:
|
||||
```bash
|
||||
pip3 uninstall kivy
|
||||
pip3 install --no-binary kivy kivy==2.0.0rc2
|
||||
pip3 install kivy --no-binary kivy
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -5,8 +5,8 @@ For on related licenses for these binaries and libraries see https://github.com/
|
||||
|
||||
2. Icons from www.flaticon.com, used with permission with the following attributions:
|
||||
- Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com
|
||||
- Local search icon: derived from work by Pixel Perfect from www.flaticon.com
|
||||
- Other Menu icons and Flag icons: derived from work by Freepik from www.flaticon.com
|
||||
- Other Menu icons, Finish, and Flag icons: derived from work by Freepik from www.flaticon.com
|
||||
- Collapse branch icon: derived from work by Kirill Kazachek from www.flaticon.com
|
||||
|
||||
3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# <a name="manual"></a> KaTrain
|
||||
|
||||
[](https://github.com/sanderland/katrain/releases)
|
||||
[](https://en.wikipedia.org/wiki/MIT_License)
|
||||
[](https://github.com/sanderland/katrain/releases)
|
||||
[](https://pepy.tech/project/katrain)
|
||||
[](https://github.com/sponsors/sanderland)
|
||||
[](https://discord.com/channels/417022162348802048/629446365688365067)
|
||||
[](http://github.com/sanderland/katrain/releases)
|
||||
[](http://en.wikipedia.org/wiki/MIT_License)
|
||||
[](http://github.com/sanderland/katrain/releases)
|
||||
[](http://pepy.tech/project/katrain)
|
||||
[](http://github.com/sponsors/sanderland)
|
||||
[](http://discord.com/channels/417022162348802048/629446365688365067)
|
||||
|
||||
<table>
|
||||
<td>
|
||||
@@ -24,15 +24,14 @@
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<a href="https://github.com/sanderland/katrain/blob/master/README.md"><img alt="English" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-uk.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=de&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="German" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-de.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=fr&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="French" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-fr.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=es&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Spanish" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-es.png" width=50></a>
|
||||
<a href="http://github.com/sanderland/katrain/blob/master/README.md"><img alt="English" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-uk.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=de&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="German" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-de.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=fr&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="French" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-fr.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ru&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Russian" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ru.png" width=50></a>
|
||||
<br/>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=ru&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Russian" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-ru.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=zh-CN&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Chinese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-cn.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=ko&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Korean" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-ko.png" width=50></a>
|
||||
<a href="https://translate.google.com/translate?sl=en&tl=ja&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Japanese" src="https://github.com/sanderland/katrain/blob/master/katrain/img/flaticon/flag-jp.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=zh-CN&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Chinese" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-cn.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ko&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Korean" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-ko.png" width=50></a>
|
||||
<a href="http://translate.google.com/translate?sl=en&tl=ja&u=https%3A%2F%2Fgithub.com%2Fsanderland%2Fkatrain%2Fblob%2Fmaster%2FREADME.md"><img alt="Japanese" src="http://github.com/sanderland/katrain/blob/master/katrain/img/flags/flag-jp.png" width=50></a>
|
||||
|
||||
</td>
|
||||
</table>
|
||||
@@ -50,26 +49,18 @@ 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.
|
||||
|
||||
## <a name="preview"></a> Previews and Youtube Videos
|
||||
## <a name="preview"></a> Preview and Youtube Videos
|
||||
|
||||
### Play against an AI Teacher
|
||||

|
||||
|
||||

|
||||
|
||||
### Analyze your games
|
||||
|
||||

|
||||
|
||||
### YouTube videos
|
||||
|
||||
| **New Features in v1.4** | **Analysis Tutorial** | **Teaching Game Tutorial** |
|
||||
| **Local Joseki Analysis** | **Analysis Tutorial** | **Teaching Game Tutorial** |
|
||||
|:-----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:|
|
||||
| [](https://www.youtube.com/watch?v=ujjRWGSZJrQ&feature=youtu.be) | [](https://www.youtube.com/watch?v=qjxkcKgrsbU) | [](https://www.youtube.com/watch?v=wFl4Bab_eGM) |
|
||||
| [](http://www.youtube.com/watch?v=ujjRWGSZJrQ&feature=youtu.be) | [](http://www.youtube.com/watch?v=qjxkcKgrsbU) | [](http://www.youtube.com/watch?v=wFl4Bab_eGM) |
|
||||
|
||||
|
||||
|
||||
## <a name="install"></a> Installation
|
||||
* See the [releases page](https://github.com/sanderland/katrain/releases) for downloadable executables for Windows.
|
||||
* See the [releases page](http://github.com/sanderland/katrain/releases) for downloadable executables for Windows.
|
||||
* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any 64-bit OS.
|
||||
* [This page](INSTALL.md) has detailed instructions for Window, Linux and MacOS,
|
||||
as well as troubleshooting and setting up KataGo to use multiple GPUs.
|
||||
@@ -119,7 +110,7 @@ This estimate should be reasonably accurate as long as you have not changed the
|
||||
* **KataGo** is full KataGo, above professional level. The analysis and feedback given is always based on this full strength KataGo AI.
|
||||
* **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).
|
||||
[here](http://github.com/sanderland/katrain/issues/44) and [here](http://github.com/sanderland/katrain/issues/74).
|
||||
* **Simple Style** Prefers moves that solidify both player's territory, leading to relatively simpler moves.
|
||||
* **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.
|
||||
@@ -137,7 +128,7 @@ This estimate should be reasonably accurate as long as you have not changed the
|
||||
The Engine based AIs (KataGo, ScoreLoss, KataJigo) are affected by both the model and choice of visits and maximum time,
|
||||
while the policy net based AIs are affected by the choice of model file, but work identically with 1 visit.
|
||||
|
||||
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.
|
||||
Further technical details and discussion on these AIs can be found on [this](http://lifein19x19.com/viewtopic.php?f=10&t=17488&sid=b11e42c005bb6f4f48c83771e6a27eff) thread at the life in 19x19 forums.
|
||||
|
||||
## <a name="analysis"></a> Analysis
|
||||
|
||||
@@ -153,8 +144,8 @@ Keyboard shortcuts are shown with **[key]**.
|
||||
* The checkboxes at the top of the screen:
|
||||
* **[q]**: Child moves are shown. On by default, can turn it off to avoid obscuring other information or when
|
||||
wanting to guess the next move.
|
||||
* **[w]**: Show all dots: Show all evaluation dots instead of the last few.
|
||||
* You can configure how many are shown with this setting off, and whether they are shown for AIs under 'Configure Teacher'.
|
||||
* **[w]**: Show all dots: Cycles through showing all evaluation dots, showing the last few, and showing none.
|
||||
* You can configure whether they are shown for AIs under 'Teaching/Analysis Settings'.
|
||||
* **[e]**: Top moves: Show the next moves KataGo considered, colored by their expected point loss.
|
||||
Small/faint dots indicate high uncertainty and never show text (lower than your 'fast visits' setting).
|
||||
Hover over any of them to see the principal variation.
|
||||
@@ -167,7 +158,11 @@ Keyboard shortcuts are shown with **[key]**.
|
||||
* **[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.
|
||||
* **[f]**: Find alternatives: Increases analysis of current candidate moves to at least the 'fast visits' level, and request a new query that excludes all current candidate moves.
|
||||
* **[g]**: Find local responses: Searches within 5 spaces of the last move only.
|
||||
* **[g]**: Select area of interest: set an area and search only for moves in this box.
|
||||
Good for solving tsumegos. Note that some results may appear outside the box due to establishing a baseline for the best move,
|
||||
and the opponent can tenuki in variations.
|
||||
* **[i]**: Start insertion mode. Allows you to insert moves, to improve analysis when both players ignore an important exchange or life and death situation. Press again to stop inserting and copy the rest of the branch.
|
||||
* **[l]**: Play out the game until the end and add as a collapsed branch, to visualize the potential effect of mistakes. This is done in the background, and can be started at several nodes at once when comparing the results at different starting positions.
|
||||
* **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on.
|
||||
* **[enter]** AI move. Makes the AI move for the current player regardless of current player selection.
|
||||
|
||||
@@ -175,15 +170,21 @@ Keyboard shortcuts are shown with **[key]**.
|
||||
|
||||
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.
|
||||
* **[Alt]**: Open the main menu.
|
||||
* **[~]** or **[ ` ]** or **[F12]**: Cycles through more minimalistic UI modes.
|
||||
* **[k]**: Toggle display of board coordinates.
|
||||
* **[p]**: Pass
|
||||
* **[b]**: Pause/Resume timer
|
||||
* **[arrow left]** or **[z]**: Undo move. Hold alt for 10 moves at a time, or ctrl to skip to the start.
|
||||
* **[arrow right]** or **[x]**: Redo move. Hold alt for 10 moves at a time, or ctrl to skip to the start.
|
||||
* **[pause]**: 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 end.
|
||||
* **[arrow up/down]** Switch branch, as would be expected from the move tree.
|
||||
* **[n]** As in clicking the red arrow, go to one move before the next mistake (orange or worse) by a human player.
|
||||
* **[home/end]** Go to the beginning/end of the game.
|
||||
* **[pageup]** Make the currently selected node the main branch
|
||||
* **[Ctrl-delete]** Delete current node.
|
||||
* **[c]** Collapse/Uncollapse the branch from the current node to the previous branching point.
|
||||
* **[b]** Go back to the previous branching point.
|
||||
* **[n]** As in clicking the forward red arrow, go to one move before the next mistake (orange or worse) by a human player.
|
||||
* **[Shift-n]** As in clicking the backward red arrow, go to one move before the previous mistake.
|
||||
* **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
|
||||
* **[scroll down]**: Redo move. Only works when hovering the cursor over the board.
|
||||
* **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
|
||||
@@ -193,25 +194,28 @@ In addition to shortcuts mentioned above and those shown in the main menu:
|
||||
|
||||
## <a name="faq"></a> FAQ
|
||||
|
||||
* How can I create themes?
|
||||
* See [these instructions](THEMES.md).
|
||||
* The program is running too slowly. How can I speed it up?
|
||||
* Adjust the number of visits or maximum time allowed in the settings.
|
||||
* KataGo crashes with out of memory errors, how can I prevent this?
|
||||
* Try using a lower number for `nnMaxBatchSize` in `KataGo/analysis_config.cfg`, and avoid using versions compiled with large board sizes.
|
||||
* If still encountering problems, please start KataGo by itself to check for any errors it gives.
|
||||
* Note that if you don't have a GPU, or your GPU does not support OpenCL, you should use the 'eigen' binaries which run on CPU only.
|
||||
* The font size is too small
|
||||
* On some ultra-high resolution monitors, font scale is not set correctly. Please see [these](http://github.com/sanderland/katrain/issues/250) instructions to adjust them.
|
||||
|
||||
## <a name="support"></a> Support / Contribute
|
||||
|
||||
[](https://github.com/sanderland/katrain/issues)
|
||||
[](CONTRIBUTIONS.md)
|
||||
[](https://github.com/sponsors/sanderland)
|
||||
[](http://github.com/sanderland/katrain/issues)
|
||||
[](CONTRIBUTIONS.md)
|
||||
[](http://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.
|
||||
* For suggestions and planned improvements, see [open issues](http://github.com/sanderland/katrain/issues) on github to check if the functionality is already planned.
|
||||
* I am looking for contributors of more translations of both this manual and the program itself. The best way to help with this is to contact me on discord.
|
||||
* You can contact me on [discord](https://discord.gg/AjTPFpN) (Sander#3278), [KakaoTalk](https://open.kakao.com/o/gTsMJCac)
|
||||
or [Reddit](http://reddit.com/u/sanderbaduk) to get help, discuss improvements, or simply show your appreciation.
|
||||
* You can also donate to the project through [Github Sponsors](https://github.com/sponsors/sanderland).
|
||||
* You can contact me on [discord](http://discord.gg/AjTPFpN) (Sander#3278) or [Reddit](http://reddit.com/u/sanderbaduk) to get help, discuss improvements, or simply show your appreciation.
|
||||
* You can also donate to the project through [Github Sponsors](http://github.com/sponsors/sanderland).
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Themes
|
||||
Version 1.7 brings basic support for themes.
|
||||
|
||||
* Look at the `Theme` class in `katrain/gui/theme.py`.
|
||||
* Make a `theme.json` file in your `<home dir>/.katrain` directory and specify any variables from the above class you want to override, e.g.
|
||||
```json
|
||||
{
|
||||
"BACKGROUND_COLOR": [1,0,0,1]
|
||||
}
|
||||
```
|
||||
* All resources (including icons which can not be renamed for now) will be looked up in `<home dir>/.katrain` first, so files with identical names there can be used to override sounds and images.
|
||||
@@ -1,7 +1,8 @@
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
import polib
|
||||
@@ -26,7 +27,7 @@ for lang in locales:
|
||||
pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po")
|
||||
po[lang] = polib.pofile(pofile[lang])
|
||||
for entry in po[lang].translated_entries():
|
||||
if "TODO" in entry.comment and not "DEPRECATED" in entry.comment:
|
||||
if "TODO" in entry.comment and "DEPRECATED" not in entry.comment:
|
||||
todos[lang].append(entry)
|
||||
strings_to_langs[entry.msgid][lang] = entry
|
||||
strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
|
||||
@@ -74,7 +75,7 @@ for lang in locales:
|
||||
|
||||
for msgid, lang_entries in strings_to_langs.items():
|
||||
if lang in lang_entries and "TODO" in lang_entries[lang].comment:
|
||||
if any(e.msgstr == lang_entries[lang].msgstr for l, e in lang_entries.items() if l != lang):
|
||||
if any(e.msgstr == lang_entries[lang].msgstr for ll, e in lang_entries.items() if ll != lang):
|
||||
todo_comment = (
|
||||
f"TODO - {lang_entries[DEFAULT_LANG].comment}" if lang_entries[DEFAULT_LANG].comment else "TODO"
|
||||
) # update todo
|
||||
@@ -100,4 +101,18 @@ for lang in locales:
|
||||
print("Fixed", pofile[lang], "and converted ->", mofile)
|
||||
|
||||
|
||||
for ext in ["py", "kv"]:
|
||||
lc = 0
|
||||
for file in glob.glob(f"katrain/*.{ext}") + glob.glob(f"katrain/**/*.{ext}"):
|
||||
with open(file, "r") as f:
|
||||
for i, line in enumerate(f.readlines()):
|
||||
if line.strip():
|
||||
lc += 1
|
||||
matches = [m.strip() for m in re.findall(r"i18n._\((.*?)\)", line)]
|
||||
for msgid in matches:
|
||||
stripped_msgid = msgid.strip("\"'")
|
||||
if stripped_msgid and msgid[0] in ['"', "'"] and stripped_msgid not in strings_to_langs: # not code
|
||||
print(f"Missing {msgid} used in code at \t{file}:{i} \t'{line.strip()}'")
|
||||
errors += 1
|
||||
print(f"Checked {lc} lines of {ext} code for missing i18n entries.")
|
||||
sys.exit(int(errors))
|
||||
@@ -1,6 +1,10 @@
|
||||
"""isort:skip_file"""
|
||||
# first, logging level lower
|
||||
import json
|
||||
import os
|
||||
import kivy
|
||||
|
||||
kivy.require("2.0.0")
|
||||
|
||||
os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning")
|
||||
if "KIVY_AUDIO" not in os.environ:
|
||||
@@ -46,9 +50,13 @@ from kivy.lang import Builder
|
||||
from kivy.resources import resource_add_path
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.screenmanager import Screen
|
||||
from katrain.core.ai import generate_ai_move
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import NumericProperty, ObjectProperty, StringProperty
|
||||
from kivy.clock import Clock
|
||||
from kivy.metrics import dp
|
||||
from katrain.core.ai import generate_ai_move
|
||||
|
||||
from katrain.core.lang import DEFAULT_LANGUAGE, i18n
|
||||
from katrain.core.constants import (
|
||||
@@ -57,7 +65,6 @@ from katrain.core.constants import (
|
||||
OUTPUT_INFO,
|
||||
OUTPUT_DEBUG,
|
||||
OUTPUT_EXTRA_DEBUG,
|
||||
MODE_PLAY,
|
||||
MODE_ANALYZE,
|
||||
HOMEPAGE,
|
||||
VERSION,
|
||||
@@ -65,18 +72,24 @@ from katrain.core.constants import (
|
||||
STATUS_INFO,
|
||||
PLAYING_NORMAL,
|
||||
PLAYER_HUMAN,
|
||||
SGF_INTERNAL_COMMENTS_MARKER,
|
||||
MODE_PLAY,
|
||||
DATA_FOLDER,
|
||||
)
|
||||
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup
|
||||
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup, I18NPopup, SaveSGFPopup
|
||||
from katrain.core.base_katrain import KaTrainBase
|
||||
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, ConfigAIPopup
|
||||
from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL, LIGHTGREY
|
||||
from katrain.gui.widgets import *
|
||||
from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget
|
||||
from katrain.gui.controlspanel import ControlsPanel
|
||||
from katrain.gui.theme import Theme
|
||||
from kivymd.app import MDApp
|
||||
|
||||
# used in kv
|
||||
from katrain.gui.kivyutils import *
|
||||
from katrain.gui.widgets import MoveTree, I18NFileBrowser, SelectionSlider, ScoreGraph # noqa F401
|
||||
from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget # noqa F401
|
||||
from katrain.gui.controlspanel import ControlsPanel # noqa F401
|
||||
|
||||
|
||||
class KaTrainGui(Screen, KaTrainBase):
|
||||
@@ -107,14 +120,14 @@ 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...", STATUS_INFO)
|
||||
self.controls.set_status("KataGo engine starting...", STATUS_INFO)
|
||||
if message.startswith("Tuning"):
|
||||
self.controls.set_status(
|
||||
f"KataGo is tuning settings for first startup, please wait." + message, STATUS_INFO
|
||||
"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.", STATUS_INFO)
|
||||
self.controls.set_status("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())
|
||||
@@ -164,11 +177,11 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
|
||||
# update engine status dot
|
||||
if not self.engine or not self.engine.katago_process or self.engine.katago_process.poll() is not None:
|
||||
self.board_controls.engine_status_col = ENGINE_DOWN_COL
|
||||
self.board_controls.engine_status_col = Theme.ENGINE_DOWN_COLOR
|
||||
elif len(self.engine.queries) == 0:
|
||||
self.board_controls.engine_status_col = ENGINE_READY_COL
|
||||
self.board_controls.engine_status_col = Theme.ENGINE_READY_COLOR
|
||||
else:
|
||||
self.board_controls.engine_status_col = ENGINE_BUSY_COL
|
||||
self.board_controls.engine_status_col = Theme.ENGINE_BUSY_COLOR
|
||||
self.board_controls.queries_remaining = len(self.engine.queries)
|
||||
|
||||
# redraw board/stones
|
||||
@@ -216,6 +229,9 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
|
||||
def update_player(self, bw, **kwargs):
|
||||
super().update_player(bw, **kwargs)
|
||||
if self.game:
|
||||
sgf_name = self.game.root.get_property("P" + bw)
|
||||
self.players_info[bw].name = None if not sgf_name or SGF_INTERNAL_COMMENTS_MARKER in sgf_name else sgf_name
|
||||
if self.controls:
|
||||
self.controls.update_players()
|
||||
self.update_state()
|
||||
@@ -225,6 +241,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
def set_note(self, note):
|
||||
self.game.current_node.note = note
|
||||
|
||||
# The message loop is here to make sure moves happen in the right order, and slow operations don't hang the GUI
|
||||
def _message_loop_thread(self):
|
||||
while True:
|
||||
game, msg, args, kwargs = self.message_queue.get()
|
||||
@@ -251,26 +268,29 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
else: # game related actions
|
||||
self.message_queue.put([self.game.game_id, message, args, kwargs])
|
||||
|
||||
def _do_new_game(self, move_tree=None, analyze_fast=False):
|
||||
def _do_new_game(self, move_tree=None, analyze_fast=False, sgf_filename=None):
|
||||
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)
|
||||
self.game = Game(self, self.engine, move_tree=move_tree, analyze_fast=analyze_fast, sgf_filename=sgf_filename)
|
||||
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_insert_mode(self, mode="toggle"):
|
||||
self.game.set_insert_mode(mode)
|
||||
if self.play_analyze_mode != MODE_ANALYZE:
|
||||
self.play_mode.switch_ui_mode()
|
||||
|
||||
def _do_ai_move(self, node=None):
|
||||
if node is None or self.game.current_node == node:
|
||||
mode = self.next_player_info.strategy
|
||||
@@ -295,9 +315,9 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.redo(n_times)
|
||||
|
||||
def _do_next_mistake(self):
|
||||
def _do_find_mistake(self, fn="redo"):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.redo(999, stop_on_mistake=self.config("trainer/eval_thresholds")[-4])
|
||||
getattr(self.game, fn)(9999, stop_on_mistake=self.config("trainer/eval_thresholds")[-4])
|
||||
|
||||
def _do_cycle_children(self, *args):
|
||||
self.board_gui.animating_pv = None
|
||||
@@ -317,6 +337,13 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
def _do_analyze_extra(self, mode, **kwargs):
|
||||
self.game.analyze_extra(mode, **kwargs)
|
||||
|
||||
def _do_play_to_end(self):
|
||||
self.game.play_to_end()
|
||||
|
||||
def _do_select_box(self):
|
||||
self.controls.set_status(i18n._("analysis:region:start"), STATUS_INFO)
|
||||
self.board_gui.selecting_region_of_interest = True
|
||||
|
||||
def _do_new_game_popup(self):
|
||||
self.controls.timer.paused = True
|
||||
if not self.new_game_popup:
|
||||
@@ -325,6 +352,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
).__self__
|
||||
self.new_game_popup.content.popup = self.new_game_popup
|
||||
self.new_game_popup.open()
|
||||
self.new_game_popup.content.update_from_current_game()
|
||||
|
||||
def _do_timer_popup(self):
|
||||
self.controls.timer.paused = True
|
||||
@@ -339,7 +367,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.controls.timer.paused = True
|
||||
if not self.teacher_settings_popup:
|
||||
self.teacher_settings_popup = I18NPopup(
|
||||
title_key="teacher settings", size=[dp(800), dp(750)], content=ConfigTeacherPopup(self)
|
||||
title_key="teacher settings", size=[dp(800), dp(800)], content=ConfigTeacherPopup(self)
|
||||
).__self__
|
||||
self.teacher_settings_popup.content.popup = self.teacher_settings_popup
|
||||
self.teacher_settings_popup.open()
|
||||
@@ -365,10 +393,10 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
def load_sgf_file(self, file, fast=False, rewind=True):
|
||||
try:
|
||||
move_tree = KaTrainSGF.parse_file(file)
|
||||
except ParseError as e:
|
||||
except (ParseError, FileNotFoundError) as e:
|
||||
self.log(i18n._("Failed to load SGF").format(error=e), OUTPUT_ERROR)
|
||||
return
|
||||
self._do_new_game(move_tree=move_tree, analyze_fast=fast)
|
||||
self._do_new_game(move_tree=move_tree, analyze_fast=fast, sgf_filename=file)
|
||||
if not rewind:
|
||||
self.game.redo(999)
|
||||
|
||||
@@ -384,9 +412,8 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
filename = popup_contents.filesel.filename
|
||||
self.fileselect_popup.dismiss()
|
||||
path, file = os.path.split(filename)
|
||||
settings_path = self.config("general/sgf_load")
|
||||
if path != settings_path:
|
||||
self.log(f"Updating sgf load path default to {path}", OUTPUT_INFO)
|
||||
if path != self.config("general/sgf_load"):
|
||||
self.log(f"Updating sgf load path default to {path}", OUTPUT_DEBUG)
|
||||
self._config["general"]["sgf_load"] = path
|
||||
self.save_config("general")
|
||||
self.load_sgf_file(filename, popup_contents.fast.active, popup_contents.rewind.active)
|
||||
@@ -396,15 +423,45 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.fileselect_popup.open()
|
||||
self.fileselect_popup.content.filesel.ids.list_view._trigger_update()
|
||||
|
||||
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, STATUS_INFO)
|
||||
def _do_save_game(self, filename=None):
|
||||
filename = filename or self.game.sgf_filename
|
||||
if not filename:
|
||||
return self("save-game-as-popup")
|
||||
try:
|
||||
msg = self.game.write_sgf(filename)
|
||||
self.log(msg, OUTPUT_INFO)
|
||||
self.controls.set_status(msg, STATUS_INFO, check_level=False)
|
||||
except Exception as e:
|
||||
self.log(f"Failed to save SGF to {filename}: {e}", OUTPUT_ERROR)
|
||||
|
||||
def _do_save_game_as_popup(self):
|
||||
popup_contents = SaveSGFPopup(suggested_filename=self.game.generate_filename())
|
||||
save_game_popup = I18NPopup(
|
||||
title_key="save sgf title", size=[dp(1200), dp(800)], content=popup_contents
|
||||
).__self__
|
||||
|
||||
def readfile(*_args):
|
||||
filename = popup_contents.filesel.filename
|
||||
if not filename.lower().endswith(".sgf"):
|
||||
filename += ".sgf"
|
||||
save_game_popup.dismiss()
|
||||
path, file = os.path.split(filename.strip())
|
||||
if not path:
|
||||
path = popup_contents.filesel.path # whatever dir is shown
|
||||
if path != self.config("general/sgf_save"):
|
||||
self.log(f"Updating sgf save path default to {path}", OUTPUT_DEBUG)
|
||||
self._config["general"]["sgf_save"] = path
|
||||
self.save_config("general")
|
||||
self._do_save_game(os.path.join(path, file))
|
||||
|
||||
popup_contents.filesel.on_success = readfile
|
||||
popup_contents.filesel.on_submit = readfile
|
||||
save_game_popup.open()
|
||||
|
||||
def load_sgf_from_clipboard(self):
|
||||
clipboard = Clipboard.paste()
|
||||
if not clipboard:
|
||||
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_INFO)
|
||||
self.controls.set_status("Ctrl-V pressed but clipboard is empty.", STATUS_INFO)
|
||||
return
|
||||
|
||||
url_match = re.match(r"(?P<url>https?://[^\s]+)", clipboard)
|
||||
@@ -425,7 +482,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.engine, analyze_fast=False
|
||||
) # speed up result for looking at end of game
|
||||
self._do_new_game(move_tree=move_tree, analyze_fast=True)
|
||||
self("redo", 999)
|
||||
self("redo", 9999)
|
||||
self.log("Imported game from clipboard.", OUTPUT_INFO)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
@@ -455,9 +512,11 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
"s": ("analyze-extra", "equalize"),
|
||||
"d": ("analyze-extra", "sweep"),
|
||||
"f": ("analyze-extra", "alternative"),
|
||||
"g": ("analyze-extra", "local"),
|
||||
"g": ("select-box",),
|
||||
"i": ("insert-mode",),
|
||||
"p": ("play", None),
|
||||
"n": ("next-mistake",),
|
||||
"l": ("play-to-end",),
|
||||
"b": ("undo", "branch"),
|
||||
"down": ("switch-branch", 1),
|
||||
"up": ("switch-branch", -1),
|
||||
"f5": ("timer-popup",),
|
||||
@@ -473,44 +532,59 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
return first_child if isinstance(first_child, Popup) else None
|
||||
|
||||
def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
|
||||
ctrl_pressed = "ctrl" in modifiers
|
||||
if self.controls.note.focus:
|
||||
return # when making notes, don't allow keyboard shortcuts
|
||||
|
||||
popup = self.popup_open
|
||||
if popup:
|
||||
if keycode[1] in ["f5", "f6", "f7", "f8"]: # switch between popups
|
||||
popup.dismiss()
|
||||
return
|
||||
elif keycode[1] in ["enter", "numpadenter"]:
|
||||
fn = getattr(popup.content, "on_submit", None)
|
||||
if fn:
|
||||
fn()
|
||||
return
|
||||
else:
|
||||
return
|
||||
ctrl_pressed = "ctrl" in modifiers
|
||||
alt_pressed = "alt" in modifiers
|
||||
shift_pressed = "shift" in modifiers
|
||||
shortcuts = self.shortcuts
|
||||
if keycode[1] == "tab":
|
||||
self.play_mode.switch_ui_mode()
|
||||
elif keycode[1] == "shift":
|
||||
elif keycode[1] == "alt":
|
||||
self.nav_drawer.set_state("toggle")
|
||||
elif keycode[1] == "spacebar":
|
||||
self.toggle_continuous_analysis()
|
||||
elif keycode[1] == "b" and ctrl_pressed:
|
||||
elif keycode[1] == "k":
|
||||
self.board_gui.toggle_coordinates()
|
||||
elif keycode[1] in ["pause", "break", "f15"] and not ctrl_pressed:
|
||||
self.controls.timer.paused = not self.controls.timer.paused
|
||||
elif keycode[1] in ["`", "~", "m"] and ctrl_pressed:
|
||||
elif keycode[1] in ["`", "~", "f12"]:
|
||||
self.zen = (self.zen + 1) % 3
|
||||
elif keycode[1] in ["left", "z"]:
|
||||
self("undo", 1 + (alt_pressed or shift_pressed) * 9 + (ctrl_pressed and not alt_pressed) * 999)
|
||||
self("undo", 1 + shift_pressed * 9 + ctrl_pressed * 9999)
|
||||
elif keycode[1] in ["right", "x"]:
|
||||
self("redo", 1 + (alt_pressed or shift_pressed) * 9 + (ctrl_pressed and not alt_pressed) * 999)
|
||||
self("redo", 1 + shift_pressed * 9 + ctrl_pressed * 9999)
|
||||
elif keycode[1] == "home":
|
||||
self("undo", 999)
|
||||
self("undo", 9999)
|
||||
elif keycode[1] == "end":
|
||||
self("redo", 999)
|
||||
self("redo", 9999)
|
||||
elif keycode[1] == "pageup":
|
||||
self.controls.move_tree.make_selected_node_main_branch()
|
||||
elif keycode[1] == "n" and not ctrl_pressed:
|
||||
self("find-mistake", "undo" if shift_pressed else "redo")
|
||||
elif keycode[1] == "delete" and ctrl_pressed:
|
||||
self.controls.move_tree.delete_selected_node()
|
||||
elif keycode[1] == "c" and not ctrl_pressed:
|
||||
self.controls.move_tree.toggle_selected_node_collapse()
|
||||
elif keycode[1] == "n" and ctrl_pressed:
|
||||
self("new-game-popup")
|
||||
elif keycode[1] == "l" and ctrl_pressed:
|
||||
self("analyze-sgf-popup")
|
||||
elif keycode[1] == "s" and ctrl_pressed:
|
||||
self("output-sgf")
|
||||
self("save-game")
|
||||
elif keycode[1] == "d" and ctrl_pressed:
|
||||
self("save-game-as-popup")
|
||||
elif keycode[1] == "c" and ctrl_pressed:
|
||||
Clipboard.copy(self.game.root.sgf())
|
||||
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
|
||||
@@ -522,6 +596,20 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
shortcut.trigger_action(duration=0)
|
||||
else:
|
||||
self(*shortcut)
|
||||
elif keycode[1] == "f9" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
|
||||
import yappi
|
||||
|
||||
yappi.set_clock_type("cpu")
|
||||
yappi.start()
|
||||
self.log("starting profiler", OUTPUT_ERROR)
|
||||
elif keycode[1] == "f10" and self.debug_level >= OUTPUT_EXTRA_DEBUG:
|
||||
import time
|
||||
import yappi
|
||||
|
||||
stats = yappi.get_func_stats()
|
||||
filename = f"callgrind.{int(time.time())}.prof"
|
||||
stats.save(filename, type="callgrind")
|
||||
self.log(f"wrote profiling results to {filename}", OUTPUT_ERROR)
|
||||
return True
|
||||
|
||||
|
||||
@@ -542,7 +630,23 @@ 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"])
|
||||
resource_add_path(PATHS["PACKAGE"] + "/fonts")
|
||||
resource_add_path(PATHS["PACKAGE"] + "/sounds")
|
||||
resource_add_path(PATHS["PACKAGE"] + "/img")
|
||||
resource_add_path(os.path.abspath(os.path.expanduser(DATA_FOLDER))) # prefer resources in .katrain
|
||||
|
||||
theme_file = resource_find("theme.json")
|
||||
if theme_file:
|
||||
try:
|
||||
with open(theme_file) as f:
|
||||
theme_overrides = json.load(f)
|
||||
for k, v in theme_overrides.items():
|
||||
setattr(Theme, k, v)
|
||||
print(f"[{theme_file}] Found theme override {k} = {v}")
|
||||
except Exception as e: # noqa E722
|
||||
print(f"Failed to load theme file {theme_file}: {e}")
|
||||
|
||||
Theme.DEFAULT_FONT = resource_find(Theme.DEFAULT_FONT)
|
||||
Builder.load_file(kv_file)
|
||||
|
||||
Window.bind(on_request_close=self.on_request_close)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"anim_pv_time": 0.5,
|
||||
"debug_level": 0,
|
||||
"lang": "en",
|
||||
"version": "1.6.1"
|
||||
"version": "1.7.0"
|
||||
},
|
||||
"timer": {
|
||||
"byo_length": 30,
|
||||
@@ -67,9 +67,11 @@
|
||||
true,
|
||||
true
|
||||
],
|
||||
"save_analysis": false,
|
||||
"low_visits": 25,
|
||||
"eval_off_show_last": 3,
|
||||
"text_point_loss": true,
|
||||
"top_moves_show": "top_move_delta_score",
|
||||
"top_moves_show_secondary": "top_move_visits",
|
||||
"eval_show_ai": true,
|
||||
"lock_ai": false
|
||||
},
|
||||
@@ -96,14 +98,6 @@
|
||||
"attach_penalty": 1,
|
||||
"tenuki_penalty": 0.5
|
||||
},
|
||||
"ai:settle": {
|
||||
"max_points_lost": 1.75,
|
||||
"settled_weight": 1.0,
|
||||
"opponent_fac": 0.5,
|
||||
"min_visits": 3,
|
||||
"attach_penalty": 1,
|
||||
"tenuki_penalty": 0.5
|
||||
},
|
||||
"ai:p:weighted": {
|
||||
"weaken_fac": 1.25,
|
||||
"pick_override": 1.0,
|
||||
|
||||
@@ -18,6 +18,7 @@ from katrain.core.constants import (
|
||||
AI_RANK,
|
||||
AI_SCORELOSS,
|
||||
AI_SCORELOSS_ELO,
|
||||
AI_SETTLE_STONES,
|
||||
AI_SIMPLE_OWNERSHIP,
|
||||
AI_STRATEGIES_PICK,
|
||||
AI_STRATEGIES_POLICY,
|
||||
@@ -32,7 +33,6 @@ from katrain.core.constants import (
|
||||
OUTPUT_DEBUG,
|
||||
OUTPUT_ERROR,
|
||||
OUTPUT_INFO,
|
||||
AI_SETTLE_STONES,
|
||||
)
|
||||
from katrain.core.game import Game, GameNode, Move
|
||||
from katrain.core.utils import var_to_grid
|
||||
@@ -125,11 +125,11 @@ def policy_weighted_move(policy_moves, lower_bound, weaken_fac):
|
||||
def generate_influence_territory_weights(ai_mode, ai_settings, policy_grid, size):
|
||||
thr_line = ai_settings["threshold"] - 1 # zero-based
|
||||
if ai_mode == AI_INFLUENCE:
|
||||
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (
|
||||
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** ( # noqa E731
|
||||
max(0, thr_line - min(size[0] - 1 - x, x)) + max(0, thr_line - min(size[1] - 1 - y, y))
|
||||
)
|
||||
) # noqa E731
|
||||
else:
|
||||
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (
|
||||
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** ( # noqa E731
|
||||
max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line)
|
||||
)
|
||||
weighted_coords = [
|
||||
@@ -203,7 +203,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
|
||||
game, cn, {"playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK"}
|
||||
)
|
||||
if not handicap_analysis:
|
||||
game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR)
|
||||
game.katrain.log("Error getting handicap-based move", OUTPUT_ERROR)
|
||||
ai_mode = AI_DEFAULT
|
||||
|
||||
while not cn.analysis_complete:
|
||||
@@ -225,7 +225,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
|
||||
ai_mode in [AI_LOCAL, AI_TENUKI] and not (cn.move and cn.move.coords)
|
||||
):
|
||||
ai_mode = AI_WEIGHTED
|
||||
ai_thoughts += f"Strategy override, using policy-weighted strategy instead. "
|
||||
ai_thoughts += "Strategy override, using policy-weighted strategy instead. "
|
||||
ai_settings = {"pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02}
|
||||
|
||||
if top_5_pass:
|
||||
@@ -336,7 +336,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
|
||||
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 += "Top move is pass, so passing regardless of strategy. "
|
||||
else:
|
||||
if ai_mode == AI_JIGO:
|
||||
sign = cn.player_sign(cn.next_player)
|
||||
|
||||
@@ -6,7 +6,18 @@ 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.constants import (
|
||||
PLAYER_HUMAN,
|
||||
PLAYER_AI,
|
||||
PLAYING_NORMAL,
|
||||
PLAYING_TEACHING,
|
||||
OUTPUT_INFO,
|
||||
OUTPUT_ERROR,
|
||||
OUTPUT_DEBUG,
|
||||
AI_DEFAULT,
|
||||
CONFIG_MIN_VERSION,
|
||||
DATA_FOLDER,
|
||||
)
|
||||
from katrain.core.utils import find_package_resource
|
||||
|
||||
|
||||
@@ -44,7 +55,7 @@ class Player:
|
||||
|
||||
|
||||
class KaTrainBase:
|
||||
USER_CONFIG_FILE = os.path.expanduser("~/.katrain/config.json")
|
||||
USER_CONFIG_FILE = os.path.expanduser(os.path.join(DATA_FOLDER, "config.json"))
|
||||
PACKAGE_CONFIG_FILE = "katrain/config.json"
|
||||
|
||||
"""Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI"""
|
||||
@@ -91,7 +102,7 @@ class KaTrainBase:
|
||||
else: # user file exists
|
||||
try:
|
||||
version = JsonStore(user_config_file).get("general")["version"]
|
||||
except Exception: # broken file etc
|
||||
except Exception: # noqa E722 broken file etc
|
||||
version = "0.0.0"
|
||||
if version < CONFIG_MIN_VERSION:
|
||||
backup = user_config_file + f".{version}.backup"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
VERSION = "1.6.1"
|
||||
PROGRAM_NAME = "KaTrain"
|
||||
VERSION = "1.7.0"
|
||||
HOMEPAGE = "https://github.com/sanderland/katrain"
|
||||
CONFIG_MIN_VERSION = "1.6.0" # keep config files from this version
|
||||
CONFIG_MIN_VERSION = "1.7.0" # keep config files from this version
|
||||
ANALYSIS_FORMAT_VERSION = "1.0"
|
||||
DATA_FOLDER = "~/.katrain"
|
||||
|
||||
OUTPUT_ERROR = -1
|
||||
OUTPUT_KATAGO_STDERR = -0.5
|
||||
@@ -220,3 +223,24 @@ AI_PICK_ELO_GRID = [
|
||||
[514.0, 670.0, 870.0, 1128.0, 1305.0, 1550.0, 1700.0, 1700.0],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
TOP_MOVE_DELTA_SCORE = "top_move_delta_score"
|
||||
TOP_MOVE_SCORE = "top_move_score"
|
||||
TOP_MOVE_DELTA_WINRATE = "top_move_delta_winrate"
|
||||
TOP_MOVE_WINRATE = "top_move_winrate"
|
||||
TOP_MOVE_VISITS = "top_move_visits"
|
||||
TOP_MOVE_NOTHING = "top_move_nothing"
|
||||
|
||||
TOP_MOVE_OPTIONS = [
|
||||
TOP_MOVE_SCORE,
|
||||
TOP_MOVE_DELTA_SCORE,
|
||||
TOP_MOVE_WINRATE,
|
||||
TOP_MOVE_DELTA_WINRATE,
|
||||
TOP_MOVE_VISITS,
|
||||
TOP_MOVE_NOTHING,
|
||||
]
|
||||
REPORT_DT = 0.25
|
||||
|
||||
SGF_INTERNAL_COMMENTS_MARKER = "\u3164\u200b"
|
||||
SGF_SEPARATOR_MARKER = "\u3164\u3164"
|
||||
@@ -6,7 +6,7 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import Callable, Dict, Optional
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from kivy.utils import platform
|
||||
|
||||
@@ -103,8 +103,10 @@ class KataGoEngine:
|
||||
i18n._("Starting Kata failed").format(command=self.command, error=e), OUTPUT_ERROR,
|
||||
)
|
||||
return # don't start
|
||||
self.analysis_thread = threading.Thread(target=self._analysis_read_thread, daemon=True).start()
|
||||
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True).start()
|
||||
self.analysis_thread = threading.Thread(target=self._analysis_read_thread, daemon=True)
|
||||
self.stderr_thread = threading.Thread(target=self._read_stderr_thread, daemon=True)
|
||||
self.analysis_thread.start()
|
||||
self.stderr_thread.start()
|
||||
|
||||
def on_new_game(self):
|
||||
self.base_priority += 1
|
||||
@@ -201,18 +203,21 @@ class KataGoEngine:
|
||||
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_ERROR)
|
||||
elif "warning" in analysis:
|
||||
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG)
|
||||
elif "terminateId" in analysis:
|
||||
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG)
|
||||
else:
|
||||
partial_result = analysis.get("isDuringSearch", False)
|
||||
if not partial_result:
|
||||
del self.queries[query_id]
|
||||
time_taken = time.time() - start_time
|
||||
results_exist = not analysis.get("noResults", False)
|
||||
self.katrain.log(
|
||||
f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] KataGo Analysis Received: {analysis.keys()}",
|
||||
f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] KataGo analysis received: {len(analysis.get('moveInfos',[]))} candidate moves, {analysis['rootInfo']['visits'] if results_exist else 'n/a'} visits",
|
||||
OUTPUT_DEBUG,
|
||||
)
|
||||
self.katrain.log(line, OUTPUT_EXTRA_DEBUG)
|
||||
try:
|
||||
if callback and not analysis.get("noResults", False):
|
||||
if callback and results_exist:
|
||||
callback(analysis, partial_result)
|
||||
except Exception as e:
|
||||
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR)
|
||||
@@ -228,13 +233,13 @@ class KataGoEngine:
|
||||
if "id" not in query:
|
||||
query["id"] = f"QUERY:{str(self.query_counter)}"
|
||||
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
|
||||
if self.katago_process:
|
||||
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
|
||||
try:
|
||||
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
|
||||
self.katago_process.stdin.flush()
|
||||
except OSError as e:
|
||||
self.check_alive(os_error=str(e), exception_if_dead=True)
|
||||
if self.katago_process:
|
||||
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
|
||||
try:
|
||||
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
|
||||
self.katago_process.stdin.flush()
|
||||
except OSError as e:
|
||||
self.check_alive(os_error=str(e), exception_if_dead=True)
|
||||
|
||||
def terminate_query(self, query_id):
|
||||
if query_id is not None:
|
||||
@@ -249,7 +254,7 @@ class KataGoEngine:
|
||||
analyze_fast: bool = False,
|
||||
time_limit=True,
|
||||
find_alternatives: bool = False,
|
||||
find_local: bool = False,
|
||||
region_of_interest: Optional[List] = None,
|
||||
priority: int = 0,
|
||||
ownership: Optional[bool] = None,
|
||||
next_move: Optional[GameNode] = None,
|
||||
@@ -278,22 +283,20 @@ class KataGoEngine:
|
||||
"untilDepth": 1,
|
||||
}
|
||||
]
|
||||
elif find_local:
|
||||
distance = 5
|
||||
last_move = analysis_node.move
|
||||
if last_move is None or last_move.is_pass:
|
||||
return
|
||||
elif region_of_interest:
|
||||
xmin, xmax, ymin, ymax = region_of_interest
|
||||
avoid = [
|
||||
{
|
||||
"moves": [
|
||||
Move((x, y)).gtp()
|
||||
for x in range(0, size_x)
|
||||
for y in range(0, size_y)
|
||||
if max(abs(x - last_move.coords[0]), abs(y - last_move.coords[1])) > distance
|
||||
if x < xmin or x > xmax or y < ymin or y > ymax
|
||||
],
|
||||
"player": analysis_node.next_player,
|
||||
"untilDepth": 1,
|
||||
"player": player,
|
||||
"untilDepth": 1, # tried a large number here, or 2, but this seems more natural
|
||||
}
|
||||
for player in "BW"
|
||||
]
|
||||
else:
|
||||
avoid = []
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
@@ -8,11 +9,15 @@ from typing import Dict, List, Optional, Union
|
||||
from kivy.clock import Clock
|
||||
|
||||
from katrain.core.constants import (
|
||||
HOMEPAGE,
|
||||
ANALYSIS_FORMAT_VERSION,
|
||||
OUTPUT_DEBUG,
|
||||
OUTPUT_INFO,
|
||||
PLAYER_AI,
|
||||
PLAYER_HUMAN,
|
||||
PROGRAM_NAME,
|
||||
SGF_INTERNAL_COMMENTS_MARKER,
|
||||
STATUS_ANALYSIS,
|
||||
STATUS_ERROR,
|
||||
STATUS_INFO,
|
||||
STATUS_TEACHING,
|
||||
)
|
||||
@@ -34,7 +39,7 @@ class KaTrainSGF(SGF):
|
||||
class Game:
|
||||
"""Represents a game of go, including an implementation of capture rules."""
|
||||
|
||||
DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "AP": f"KaTrain:{HOMEPAGE}", "CA": "UTF-8"}
|
||||
DEFAULT_PROPERTIES = {"GM": 1, "FF": 4}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -43,6 +48,7 @@ class Game:
|
||||
move_tree: GameNode = None,
|
||||
analyze_fast=False,
|
||||
game_properties: Optional[Dict] = None,
|
||||
sgf_filename=None,
|
||||
):
|
||||
self.katrain = katrain
|
||||
self._lock = threading.Lock()
|
||||
@@ -50,9 +56,16 @@ class Game:
|
||||
engine = {"B": engine, "W": engine}
|
||||
self.engines = engine
|
||||
self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S")
|
||||
self.sgf_filename = sgf_filename
|
||||
|
||||
self.insert_mode = False
|
||||
self.insert_after = None
|
||||
|
||||
self.region_of_interest = None
|
||||
self.external_game = False # not generated by katrain at some point
|
||||
if move_tree:
|
||||
self.root = move_tree
|
||||
self.external_game = PROGRAM_NAME not in self.root.get_property("AP", "")
|
||||
self.komi = self.root.komi
|
||||
handicap = int(self.root.get_property("HA", 0))
|
||||
if (
|
||||
@@ -63,11 +76,12 @@ class Game:
|
||||
self.root.place_handicap_stones(handicap)
|
||||
else:
|
||||
board_size = katrain.config("game/size")
|
||||
rules = katrain.config("game/rules")
|
||||
self.komi = katrain.config("game/komi")
|
||||
self.root = GameNode(
|
||||
properties={
|
||||
**Game.DEFAULT_PROPERTIES,
|
||||
**{"SZ": board_size, "KM": self.komi, "DT": self.game_id},
|
||||
**{"SZ": board_size, "KM": self.komi, "DT": self.game_id, "RU": rules},
|
||||
**(game_properties or {}),
|
||||
}
|
||||
)
|
||||
@@ -80,14 +94,24 @@ class Game:
|
||||
|
||||
self.set_current_node(self.root)
|
||||
self.main_time_used = 0
|
||||
|
||||
# restore shortcuts
|
||||
shortcut_id_to_node = {node.get_property("KTSID", None): node for node in self.root.nodes_in_tree}
|
||||
for node in self.root.nodes_in_tree:
|
||||
shortcut_id = node.get_property("KTSF", None)
|
||||
if shortcut_id and shortcut_id in shortcut_id_to_node:
|
||||
shortcut_id_to_node[shortcut_id].add_shortcut(node)
|
||||
|
||||
threading.Thread(
|
||||
target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True
|
||||
target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast, even_if_present=False),
|
||||
daemon=True,
|
||||
).start() # return faster, but bypass Kivy Clock
|
||||
|
||||
def analyze_all_nodes(self, priority=0, analyze_fast=False):
|
||||
def analyze_all_nodes(self, priority=0, analyze_fast=False, even_if_present=True):
|
||||
for node in self.root.nodes_in_tree:
|
||||
node.clear_analysis()
|
||||
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
|
||||
if even_if_present or not node.analysis_loaded:
|
||||
node.clear_analysis()
|
||||
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
|
||||
|
||||
# -- move tree functions --
|
||||
def _calculate_groups(self):
|
||||
@@ -157,6 +181,54 @@ class Game:
|
||||
if -1 not in neighbours(self.chains[this_chain]): # TODO: NZ rules?
|
||||
raise IllegalMoveException("Suicide")
|
||||
|
||||
def set_insert_mode(self, mode):
|
||||
if mode == "toggle":
|
||||
mode = not self.insert_mode
|
||||
if mode == self.insert_mode:
|
||||
return
|
||||
self.insert_mode = mode
|
||||
if mode:
|
||||
children = self.current_node.ordered_children
|
||||
if not children:
|
||||
self.insert_mode = False
|
||||
else:
|
||||
self.insert_after = self.current_node.ordered_children[0]
|
||||
self.katrain.controls.set_status(i18n._("starting insert mode"), STATUS_INFO)
|
||||
else:
|
||||
copy_from_node = self.insert_after
|
||||
copy_to_node = self.current_node
|
||||
num_copied = 0
|
||||
if copy_to_node != self.insert_after.parent:
|
||||
above_insertion_root = self.insert_after.parent.nodes_from_root
|
||||
already_inserted_moves = [
|
||||
n.move for n in copy_to_node.nodes_from_root if n not in above_insertion_root and n.move
|
||||
]
|
||||
try:
|
||||
while True:
|
||||
if copy_from_node.move not in already_inserted_moves:
|
||||
for m in copy_from_node.move_with_placements:
|
||||
self._validate_move_and_update_chains(m, True)
|
||||
# this inserts
|
||||
copy_to_node = GameNode(
|
||||
parent=copy_to_node, properties=copy.deepcopy(copy_from_node.properties)
|
||||
)
|
||||
num_copied += 1
|
||||
if not copy_from_node.children:
|
||||
break
|
||||
copy_from_node = copy_from_node.ordered_children[0]
|
||||
except IllegalMoveException:
|
||||
pass # illegal move = stop
|
||||
self._calculate_groups() # recalculate groups
|
||||
self.katrain.controls.set_status(
|
||||
i18n._("ending insert mode").format(num_copied=num_copied), STATUS_INFO
|
||||
)
|
||||
self.analyze_all_nodes(analyze_fast=True, even_if_present=False)
|
||||
else:
|
||||
self.katrain.controls.set_status("", STATUS_INFO)
|
||||
self.katrain.controls.move_tree.insert_node = self.insert_after if self.insert_mode else None
|
||||
self.katrain.controls.move_tree.redraw()
|
||||
self.katrain.update_state(redraw_board=True)
|
||||
|
||||
# Play a Move from the current position, raise IllegalMoveException if invalid.
|
||||
def play(self, move: Move, ignore_ko: bool = False, analyze=True):
|
||||
board_size_x, board_size_y = self.board_size
|
||||
@@ -171,25 +243,62 @@ class Game:
|
||||
played_node = self.current_node.play(move)
|
||||
self.current_node = played_node
|
||||
if analyze:
|
||||
played_node.analyze(self.engines[played_node.next_player])
|
||||
if self.region_of_interest:
|
||||
played_node.analyze(self.engines[played_node.next_player], analyze_fast=True)
|
||||
played_node.analyze(self.engines[played_node.next_player], region_of_interest=self.region_of_interest)
|
||||
else:
|
||||
played_node.analyze(self.engines[played_node.next_player])
|
||||
return played_node
|
||||
|
||||
def set_current_node(self, node):
|
||||
if self.insert_mode:
|
||||
self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR)
|
||||
return
|
||||
|
||||
self.current_node = node
|
||||
self._calculate_groups()
|
||||
|
||||
def undo(self, n_times=1):
|
||||
def undo(self, n_times=1, stop_on_mistake=None):
|
||||
# allow undo/delete only in insert mode
|
||||
cn = self.current_node # avoid race conditions
|
||||
for _ in range(n_times):
|
||||
if not cn.is_root:
|
||||
if self.insert_mode: # in insert mode, undo = delete
|
||||
if n_times == 1 and cn not in self.insert_after.nodes_from_root:
|
||||
cn.parent.children = [c for c in cn.parent.children if c != cn]
|
||||
self.current_node = cn.parent
|
||||
self._calculate_groups()
|
||||
return
|
||||
break_on_branch = False
|
||||
if n_times == "branch":
|
||||
n_times = 9999
|
||||
break_on_branch = True
|
||||
for move in range(n_times):
|
||||
if (
|
||||
stop_on_mistake is not None
|
||||
and cn.points_lost is not None
|
||||
and cn.points_lost >= stop_on_mistake
|
||||
and self.katrain.players_info[cn.player].player_type != PLAYER_AI
|
||||
):
|
||||
self.set_current_node(cn.parent)
|
||||
return
|
||||
if cn.shortcut_from:
|
||||
cn = cn.shortcut_from
|
||||
elif not cn.is_root:
|
||||
cn = cn.parent
|
||||
if break_on_branch and len(cn.children) > 1:
|
||||
break
|
||||
self.set_current_node(cn)
|
||||
|
||||
def redo(self, n_times=1, stop_on_mistake=None):
|
||||
if self.insert_mode:
|
||||
return
|
||||
cn = self.current_node # avoid race conditions
|
||||
for move in range(n_times):
|
||||
if cn.children:
|
||||
cn = cn.ordered_children[0]
|
||||
child = cn.ordered_children[0]
|
||||
shortcut_to = [m for m, v in cn.shortcuts_to if child == v] # are we about to go to a shortcut node?
|
||||
if shortcut_to:
|
||||
child = shortcut_to[0]
|
||||
cn = child
|
||||
if (
|
||||
move > 0
|
||||
and stop_on_mistake is not None
|
||||
@@ -287,43 +396,65 @@ class Game:
|
||||
+ f"\ncaptures: {self.prisoner_count}"
|
||||
)
|
||||
|
||||
def write_sgf(
|
||||
self, path: str, trainer_config: Optional[Dict] = None,
|
||||
):
|
||||
if trainer_config is None:
|
||||
trainer_config = self.katrain.config("trainer")
|
||||
save_feedback = trainer_config["save_feedback"]
|
||||
eval_thresholds = trainer_config["eval_thresholds"]
|
||||
|
||||
def generate_filename(self):
|
||||
def player_name(player_info):
|
||||
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})"
|
||||
if player_info.name and player_info.player_type == PLAYER_HUMAN:
|
||||
return player_info.name
|
||||
else:
|
||||
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})"
|
||||
|
||||
root_properties = self.root.properties
|
||||
x_properties = {}
|
||||
if "KaTrain" in self.root.get_property("AP", ""):
|
||||
for bw in "BW":
|
||||
x_properties["P" + bw] = player_name(self.katrain.players_info[bw])
|
||||
for bw in "BW":
|
||||
if not self.external_game:
|
||||
x_properties["P" + bw] = player_name(self.katrain.players_info[bw]) + SGF_INTERNAL_COMMENTS_MARKER
|
||||
player_info = self.katrain.players_info[bw]
|
||||
if player_info.player_type == PLAYER_AI:
|
||||
x_properties[bw + "R"] = rank_label(player_info.calculated_rank)
|
||||
if "+" in str(self.end_result):
|
||||
x_properties["RE"] = self.end_result
|
||||
x_properties["KTV"] = ANALYSIS_FORMAT_VERSION
|
||||
self.root.properties = {**root_properties, **{k: [v] for k, v in x_properties.items()}}
|
||||
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)
|
||||
player_names = {
|
||||
bw: re.sub(r"[\u200b\u3164'<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"
|
||||
}
|
||||
base_game_name = f"{PROGRAM_NAME}_{player_names['B']} vs {player_names['W']}"
|
||||
return f"{base_game_name} {self.game_id}.sgf"
|
||||
|
||||
def write_sgf(
|
||||
self, filename: str = None, trainer_config: Optional[Dict] = None,
|
||||
):
|
||||
if trainer_config is None:
|
||||
trainer_config = self.katrain.config("trainer", {})
|
||||
save_feedback = trainer_config.get("save_feedback", False)
|
||||
eval_thresholds = trainer_config["eval_thresholds"]
|
||||
save_analysis = trainer_config.get("save_analysis", False)
|
||||
|
||||
show_dots_for = {
|
||||
bw: trainer_config.get("eval_show_ai", True) or self.katrain.players_info[bw].human for bw in "BW"
|
||||
}
|
||||
sgf = self.root.sgf(
|
||||
save_comments_player=show_dots_for, save_comments_class=save_feedback, eval_thresholds=eval_thresholds
|
||||
save_comments_player=show_dots_for,
|
||||
save_comments_class=save_feedback,
|
||||
eval_thresholds=eval_thresholds,
|
||||
save_analysis=save_analysis,
|
||||
)
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
self.sgf_filename = filename
|
||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(sgf)
|
||||
self.root.properties = root_properties
|
||||
return i18n._("sgf written").format(file_name=file_name)
|
||||
return i18n._("sgf written").format(file_name=filename)
|
||||
|
||||
def set_region_of_interest(self, region_of_interest):
|
||||
x1, x2, y1, y2 = region_of_interest
|
||||
xmin, xmax = min(x1, x2), max(x1, x2)
|
||||
ymin, ymax = min(y1, y2), max(y1, y2)
|
||||
szx, szy = self.board_size
|
||||
if not (xmin == xmax and ymin == ymax) and not (xmax - xmin + 1 >= szx and ymax - ymin + 1 >= szy):
|
||||
self.region_of_interest = [xmin, xmax, ymin, ymax]
|
||||
else:
|
||||
self.region_of_interest = None
|
||||
self.katrain.controls.set_status("", OUTPUT_INFO)
|
||||
|
||||
def analyze_extra(self, mode, **kwargs):
|
||||
stones = {s.coords for s in self.stones}
|
||||
@@ -341,7 +472,9 @@ class Game:
|
||||
visits = cn.analysis_visits_requested + engine.config["max_visits"]
|
||||
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||
cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False)
|
||||
cn.analyze(
|
||||
engine, visits=visits, priority=-1_000, region_of_interest=self.region_of_interest, time_limit=False
|
||||
)
|
||||
return
|
||||
if mode == "game":
|
||||
nodes = self.root.nodes_in_tree
|
||||
@@ -382,13 +515,9 @@ class Game:
|
||||
visits = engine.config["fast_visits"]
|
||||
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
|
||||
priority = -1_000_000_000
|
||||
elif mode == "local": # also do a quick update on current candidates so it doesn't look too weird
|
||||
self.katrain.controls.set_status(i18n._("local analysis"), STATUS_ANALYSIS)
|
||||
cn.analyze(engine, priority=-500, time_limit=False, find_local="local")
|
||||
return
|
||||
elif mode in ["equalize", "alternative", "local"]:
|
||||
if not cn.analysis_complete and mode != "local":
|
||||
self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node)
|
||||
self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, self.current_node)
|
||||
return
|
||||
if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird
|
||||
self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS)
|
||||
@@ -404,9 +533,48 @@ class Game:
|
||||
for move in analyze_moves:
|
||||
if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits:
|
||||
cn.analyze(
|
||||
engine, priority=priority, visits=visits, refine_move=move, time_limit=False, report_every=None
|
||||
engine, priority=priority, visits=visits, refine_move=move, time_limit=False
|
||||
) # explicitly requested so take as long as you need
|
||||
|
||||
def play_to_end(self):
|
||||
cn = self.current_node
|
||||
count = 0
|
||||
if not cn.analysis_exists:
|
||||
self.katrain.controls.set_status(i18n._("wait-before-extra-analysis"), STATUS_INFO, cn)
|
||||
return
|
||||
|
||||
def analyze_and_play_policy(node):
|
||||
nonlocal count, cn
|
||||
cand = node.candidate_moves
|
||||
if self.katrain.game is not self:
|
||||
return # a new game happened
|
||||
if cand:
|
||||
move = Move.from_gtp(cand[0]["move"], player=node.next_player)
|
||||
else:
|
||||
polmoves = node.policy_ranking
|
||||
move = polmoves[0][1] if polmoves else Move(None)
|
||||
|
||||
if move.is_pass:
|
||||
if self.current_node == cn:
|
||||
self.set_current_node(node)
|
||||
return
|
||||
count += 1
|
||||
new_node = GameNode(parent=node, move=move)
|
||||
if node != cn:
|
||||
node.remove_shortcut()
|
||||
cn.add_shortcut(new_node)
|
||||
self.katrain.controls.move_tree.redraw_tree_trigger()
|
||||
|
||||
def set_analysis(result, _partial):
|
||||
new_node.set_analysis(result)
|
||||
analyze_and_play_policy(new_node)
|
||||
|
||||
self.engines[node.next_player].request_analysis(
|
||||
new_node, callback=set_analysis, priority=-1000, analyze_fast=True,
|
||||
)
|
||||
|
||||
analyze_and_play_policy(cn)
|
||||
|
||||
def analyze_undo(self, node):
|
||||
train_config = self.katrain.config("trainer")
|
||||
move = node.move
|
||||
|
||||
@@ -1,11 +1,49 @@
|
||||
import base64
|
||||
import copy
|
||||
import gzip
|
||||
import json
|
||||
import random
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from katrain.core.constants import (
|
||||
ANALYSIS_FORMAT_VERSION,
|
||||
PROGRAM_NAME,
|
||||
SGF_INTERNAL_COMMENTS_MARKER,
|
||||
SGF_SEPARATOR_MARKER,
|
||||
VERSION,
|
||||
REPORT_DT,
|
||||
)
|
||||
from katrain.core.lang import i18n
|
||||
from katrain.core.sgf_parser import Move, SGFNode
|
||||
from katrain.core.utils import evaluation_class, var_to_grid
|
||||
from katrain.gui.style import INFO_PV_COLOR
|
||||
from katrain.core.utils import evaluation_class, pack_floats, unpack_floats, var_to_grid
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
def analysis_dumps(analysis):
|
||||
analysis = copy.deepcopy(analysis)
|
||||
for movedict in analysis["moves"].values():
|
||||
if "ownership" in movedict: # per-move ownership rarely used
|
||||
del movedict["ownership"]
|
||||
ownership_data = pack_floats(analysis.pop("ownership"))
|
||||
policy_data = pack_floats(analysis.pop("policy"))
|
||||
main_data = json.dumps(analysis).encode("utf-8")
|
||||
return [
|
||||
base64.standard_b64encode(gzip.compress(data)).decode("utf-8")
|
||||
for data in [ownership_data, policy_data, main_data]
|
||||
]
|
||||
|
||||
|
||||
def analysis_loads(property_array, board_squares, version):
|
||||
if version > ANALYSIS_FORMAT_VERSION:
|
||||
raise ValueError(f"Can not decode analysis data with version {version}, please update {PROGRAM_NAME}")
|
||||
ownership_data, policy_data, main_data, *_ = [
|
||||
gzip.decompress(base64.standard_b64decode(data)) for data in property_array
|
||||
]
|
||||
return {
|
||||
**json.loads(main_data),
|
||||
"policy": unpack_floats(policy_data, board_squares + 1),
|
||||
"ownership": unpack_floats(ownership_data, board_squares),
|
||||
}
|
||||
|
||||
|
||||
class GameNode(SGFNode):
|
||||
@@ -13,8 +51,6 @@ class GameNode(SGFNode):
|
||||
|
||||
def __init__(self, parent=None, properties=None, move=None):
|
||||
super().__init__(parent=parent, properties=properties, move=move)
|
||||
self.ownership = None
|
||||
self.policy = None
|
||||
self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move)
|
||||
self.ai_thoughts = ""
|
||||
self.note = ""
|
||||
@@ -22,19 +58,65 @@ class GameNode(SGFNode):
|
||||
self.time_used = 0
|
||||
self.undo_threshold = random.random() # for fractional undos
|
||||
self.end_state = None
|
||||
self.shortcuts_to = []
|
||||
self.shortcut_from = None
|
||||
self.analysis_loaded = False
|
||||
self.clear_analysis()
|
||||
|
||||
def add_shortcut(self, to_node): # collapses the branch between them
|
||||
nodes = [to_node]
|
||||
while nodes[-1].parent and nodes[-1] != self: # ensure on path
|
||||
nodes.append(nodes[-1].parent)
|
||||
if nodes[-1] == self and len(nodes) > 2:
|
||||
via = nodes[-2]
|
||||
self.shortcuts_to.append((to_node, via)) # and first child
|
||||
to_node.shortcut_from = self
|
||||
|
||||
def remove_shortcut(self):
|
||||
from_node = self.shortcut_from
|
||||
if from_node:
|
||||
from_node.shortcuts_to = [(m, v) for m, v in from_node.shortcuts_to if m != self]
|
||||
self.shortcut_from = None
|
||||
|
||||
def add_list_property(self, property: str, values: List):
|
||||
if property == "KT":
|
||||
try:
|
||||
szx, szy = self.root.board_size
|
||||
version = self.root.get_property("KTV", "<unknown>")
|
||||
self.analysis = analysis_loads(values, szx * szy, version)
|
||||
self.analysis_loaded = True
|
||||
except Exception as e:
|
||||
print(f"Error in loading analysis: {e}")
|
||||
elif property == "C":
|
||||
comments = [ # strip out all previously auto generated comments
|
||||
c
|
||||
for v in values
|
||||
for c in v.split(SGF_SEPARATOR_MARKER)
|
||||
if c.strip() and SGF_INTERNAL_COMMENTS_MARKER not in c
|
||||
]
|
||||
self.note = "".join(comments) # no super call intended, just save as note to be editable
|
||||
else:
|
||||
return super().add_list_property(property, values)
|
||||
|
||||
def clear_analysis(self):
|
||||
self.analysis_visits_requested = 0
|
||||
self.analysis = {"moves": {}, "root": None, "completed": False}
|
||||
self.analysis = {"moves": {}, "root": None, "ownership": None, "policy": None, "completed": False}
|
||||
|
||||
def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None):
|
||||
def sgf_properties(
|
||||
self, save_comments_player=None, save_comments_class=None, eval_thresholds=None, save_analysis=False
|
||||
):
|
||||
properties = copy.copy(super().sgf_properties())
|
||||
note = self.note.strip()
|
||||
if save_analysis and self.analysis_complete:
|
||||
try:
|
||||
properties["KT"] = analysis_dumps(self.analysis)
|
||||
except Exception as e:
|
||||
print(f"Error in saving analysis: {e}")
|
||||
if self.points_lost and save_comments_class is not None and eval_thresholds is not None:
|
||||
show_class = save_comments_class[evaluation_class(self.points_lost, eval_thresholds)]
|
||||
else:
|
||||
show_class = False
|
||||
comments = properties.get("C", [])
|
||||
if (
|
||||
self.parent
|
||||
and self.parent.analysis_exists
|
||||
@@ -50,18 +132,29 @@ class GameNode(SGFNode):
|
||||
properties["SQ"] = best_sq
|
||||
if top_x and "MA" not in properties:
|
||||
properties["MA"] = [top_x]
|
||||
comment = self.comment(sgf=True, interactive=False)
|
||||
if comment:
|
||||
properties["C"] = ["\n".join(properties.get("C", "")) + comment]
|
||||
comments.append(self.comment(sgf=True, interactive=False) + SGF_INTERNAL_COMMENTS_MARKER)
|
||||
if self.is_root:
|
||||
properties["C"] = [
|
||||
i18n._("SGF start message")
|
||||
+ "\n"
|
||||
+ "\n".join(properties.get("C", ""))
|
||||
+ "\nSGF with review generated by KaTrain."
|
||||
comments = [
|
||||
i18n._("SGF start message") + SGF_INTERNAL_COMMENTS_MARKER + "\n",
|
||||
*comments,
|
||||
f"\nSGF generated by {PROGRAM_NAME} {VERSION}{SGF_INTERNAL_COMMENTS_MARKER}\n",
|
||||
]
|
||||
properties["CA"] = ["UTF-8"]
|
||||
properties["AP"] = [f"{PROGRAM_NAME}:{VERSION}"]
|
||||
if self.shortcut_from:
|
||||
properties["KTSF"] = [id(self.shortcut_from)]
|
||||
elif "KTSF" in properties:
|
||||
del properties["KTSF"]
|
||||
if self.shortcuts_to:
|
||||
properties["KTSID"] = [id(self)]
|
||||
elif "KTSID" in properties:
|
||||
del properties["KTSID"]
|
||||
if note:
|
||||
properties["C"] = ["\n".join(properties.get("C", "")) + f"\nNote: {self.note}"]
|
||||
comments.append(f"{self.note}")
|
||||
if comments:
|
||||
properties["C"] = [SGF_SEPARATOR_MARKER.join(comments)]
|
||||
elif "C" in properties:
|
||||
del properties["C"]
|
||||
return properties
|
||||
|
||||
@staticmethod
|
||||
@@ -80,13 +173,14 @@ class GameNode(SGFNode):
|
||||
refine_move=None,
|
||||
analyze_fast=False,
|
||||
find_alternatives=False,
|
||||
find_local=False,
|
||||
report_every=0.25,
|
||||
region_of_interest=None,
|
||||
report_every=REPORT_DT,
|
||||
):
|
||||
additional_moves = bool(find_alternatives or region_of_interest)
|
||||
engine.request_analysis(
|
||||
self,
|
||||
callback=lambda result, partial_result: self.set_analysis(
|
||||
result, refine_move, find_alternatives or find_local, partial_result
|
||||
result, refine_move, additional_moves, partial_result
|
||||
),
|
||||
priority=priority,
|
||||
visits=visits,
|
||||
@@ -94,7 +188,7 @@ class GameNode(SGFNode):
|
||||
time_limit=time_limit,
|
||||
next_move=refine_move,
|
||||
find_alternatives=find_alternatives,
|
||||
find_local=find_local,
|
||||
region_of_interest=region_of_interest,
|
||||
report_every=report_every,
|
||||
)
|
||||
|
||||
@@ -112,7 +206,11 @@ class GameNode(SGFNode):
|
||||
cur.update(move_analysis)
|
||||
|
||||
def set_analysis(
|
||||
self, analysis_json: Dict, refine_move: Optional[Move], additional_moves: bool, partial_result: bool = False
|
||||
self,
|
||||
analysis_json: Dict,
|
||||
refine_move: Optional[Move] = None,
|
||||
additional_moves: bool = False,
|
||||
partial_result: bool = False,
|
||||
):
|
||||
if refine_move:
|
||||
pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
|
||||
@@ -128,8 +226,8 @@ class GameNode(SGFNode):
|
||||
move_dict["order"] = 999 # old moves to end
|
||||
for move_analysis in analysis_json["moveInfos"]:
|
||||
self.update_move_analysis(move_analysis, move_analysis["move"])
|
||||
self.ownership = analysis_json.get("ownership")
|
||||
self.policy = analysis_json.get("policy")
|
||||
self.analysis["ownership"] = analysis_json.get("ownership")
|
||||
self.analysis["policy"] = analysis_json.get("policy")
|
||||
if not additional_moves:
|
||||
self.analysis["root"] = analysis_json["rootInfo"]
|
||||
if self.parent and self.move:
|
||||
@@ -142,6 +240,14 @@ class GameNode(SGFNode):
|
||||
is_normal_query = refine_move is None and not additional_moves
|
||||
self.analysis["completed"] = self.analysis["completed"] or (is_normal_query and not partial_result)
|
||||
|
||||
@property
|
||||
def ownership(self):
|
||||
return self.analysis.get("ownership")
|
||||
|
||||
@property
|
||||
def policy(self):
|
||||
return self.analysis.get("policy")
|
||||
|
||||
@property
|
||||
def analysis_exists(self):
|
||||
return self.analysis["root"] is not None
|
||||
@@ -182,7 +288,7 @@ class GameNode(SGFNode):
|
||||
def make_pv(self, player, pv, interactive):
|
||||
pvtext = f"{player}{' '.join(pv)}"
|
||||
if interactive:
|
||||
pvtext = f"[u][ref={pvtext}][color={INFO_PV_COLOR}]{pvtext}[/color][/ref][/u]"
|
||||
pvtext = f"[u][ref={pvtext}][color={Theme.INFO_PV_COLOR}]{pvtext}[/color][/ref][/u]"
|
||||
return pvtext
|
||||
|
||||
def comment(self, sgf=False, teach=False, details=False, interactive=True):
|
||||
@@ -276,6 +382,7 @@ class GameNode(SGFNode):
|
||||
{
|
||||
**self.analysis["root"],
|
||||
"pointsLost": 0,
|
||||
"winrateLost": 0,
|
||||
"order": 0,
|
||||
"move": top_polmove.gtp(),
|
||||
"pv": [top_polmove.gtp()],
|
||||
@@ -283,10 +390,15 @@ class GameNode(SGFNode):
|
||||
] # single visit -> go by policy/root
|
||||
|
||||
root_score = self.analysis["root"]["scoreLead"]
|
||||
root_winrate = self.analysis["root"]["winrate"]
|
||||
move_dicts = list(self.analysis["moves"].values()) # prevent incoming analysis from causing crash
|
||||
return sorted(
|
||||
[
|
||||
{"pointsLost": self.player_sign(self.next_player) * (root_score - d["scoreLead"]), **d}
|
||||
{
|
||||
"pointsLost": self.player_sign(self.next_player) * (root_score - d["scoreLead"]),
|
||||
"winrateLost": self.player_sign(self.next_player) * (root_winrate - d["winrate"]),
|
||||
**d,
|
||||
}
|
||||
for d in move_dicts
|
||||
],
|
||||
key=lambda d: (d["order"], d["pointsLost"]),
|
||||
|
||||
@@ -5,13 +5,13 @@ import sys
|
||||
from kivy._event import Observable
|
||||
|
||||
from katrain.core.utils import find_package_resource
|
||||
from katrain.gui.style import DEFAULT_FONT
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
class Lang(Observable):
|
||||
observers = []
|
||||
callbacks = []
|
||||
FONTS = {"jp": "fonts/NotoSansJP-Regular.otf"}
|
||||
FONTS = {"jp": "NotoSansJP-Regular.otf"}
|
||||
|
||||
def __init__(self, lang):
|
||||
super(Lang, self).__init__()
|
||||
@@ -53,7 +53,7 @@ class Lang(Observable):
|
||||
return
|
||||
# get the right locales directory, and instantiate a gettext
|
||||
self.lang = lang
|
||||
self.font_name = self.FONTS.get(lang) or DEFAULT_FONT
|
||||
self.font_name = self.FONTS.get(lang) or Theme.DEFAULT_FONT
|
||||
i18n_dir, _ = os.path.split(find_package_resource("katrain/i18n/__init__.py"))
|
||||
locale_dir = os.path.join(i18n_dir, "locales")
|
||||
locales = gettext.translation("katrain", locale_dir, languages=[lang, DEFAULT_LANGUAGE])
|
||||
|
||||
@@ -64,10 +64,15 @@ class Move:
|
||||
"""Returns True if the move is a pass"""
|
||||
return self.coords is None
|
||||
|
||||
@staticmethod
|
||||
def opponent_player(player):
|
||||
"""Returns the opposing player, i.e. W <-> B"""
|
||||
return "W" if player == "B" else "B"
|
||||
|
||||
@property
|
||||
def opponent(self):
|
||||
"""Returns the opposing player, i.e. W <-> B"""
|
||||
return "W" if self.player == "B" else "B"
|
||||
return self.opponent_player(self.player)
|
||||
|
||||
|
||||
class SGFNode:
|
||||
@@ -82,6 +87,10 @@ class SGFNode:
|
||||
self.parent.children.append(self)
|
||||
if parent and move:
|
||||
self.set_property(move.player, move.sgf(self.board_size))
|
||||
self._clear_cache()
|
||||
|
||||
def _clear_cache(self):
|
||||
self.moves_cache = None
|
||||
|
||||
def sgf_properties(self, **xargs) -> Dict:
|
||||
"""For hooking into in a subclass and overriding/formatting any additional properties to be output."""
|
||||
@@ -134,6 +143,7 @@ class SGFNode:
|
||||
"""Add some values to the property list."""
|
||||
# SiZe[19] ==> SZ[19] etc. for old SGF
|
||||
normalized_property = re.sub("[a-z]", "", property)
|
||||
self._clear_cache()
|
||||
self.properties[normalized_property] += values
|
||||
|
||||
def get_list_property(self, property, default=None) -> Any:
|
||||
@@ -144,6 +154,7 @@ class SGFNode:
|
||||
"""Add some values to the property. If not a list, it will be made into a single-value list."""
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
self._clear_cache()
|
||||
self.properties[property] = value
|
||||
|
||||
def get_property(self, property, default=None) -> Any:
|
||||
@@ -192,7 +203,12 @@ class SGFNode:
|
||||
@property
|
||||
def komi(self) -> float:
|
||||
"""Retrieves the root's KM property, or 6.5 if missing"""
|
||||
return float(self.root.get_property("KM", 6.5))
|
||||
try:
|
||||
km = float(self.root.get_property("KM", 6.5))
|
||||
except ValueError:
|
||||
km = 6.5
|
||||
|
||||
return km
|
||||
|
||||
@property
|
||||
def ruleset(self) -> str:
|
||||
@@ -202,11 +218,13 @@ class SGFNode:
|
||||
@property
|
||||
def moves(self) -> List[Move]:
|
||||
"""Returns all moves in the node - typically 'move' will be better."""
|
||||
return [
|
||||
Move.from_sgf(move, player=pl, board_size=self.board_size)
|
||||
for pl in Move.PLAYERS
|
||||
for move in self.get_list_property(pl, [])
|
||||
]
|
||||
if self.moves_cache is None:
|
||||
self.moves_cache = [
|
||||
Move.from_sgf(move, player=pl, board_size=self.board_size)
|
||||
for pl in Move.PLAYERS
|
||||
for move in self.get_list_property(pl, [])
|
||||
]
|
||||
return self.moves_cache
|
||||
|
||||
@property
|
||||
def placements(self) -> List[Move]:
|
||||
@@ -268,14 +286,18 @@ class SGFNode:
|
||||
def play(self, move) -> "SGFNode":
|
||||
"""Either find an existing child or create a new one with the given move."""
|
||||
for c in self.children:
|
||||
if c.move == move:
|
||||
if c.move and c.move == move:
|
||||
return c
|
||||
return self.__class__(parent=self, move=move)
|
||||
|
||||
@property
|
||||
def next_player(self):
|
||||
"""Returns player to move"""
|
||||
if "B" in self.properties or ("AB" in self.properties and not "W" in self.properties): # root or black moved
|
||||
if "PL" in self.properties: # explicit
|
||||
return "B" if self.get_property("PL").upper().strip() == "B" else "W"
|
||||
elif "B" in self.properties or (
|
||||
"AB" in self.properties and "W" not in self.properties and "AW" not in self.properties
|
||||
): # b move or setup with only black moves like root handicap
|
||||
return "W"
|
||||
else:
|
||||
return "B"
|
||||
@@ -283,7 +305,7 @@ class SGFNode:
|
||||
@property
|
||||
def player(self):
|
||||
"""Returns player that moved last. nb root is considered white played if no handicap stones are placed"""
|
||||
if "B" in self.properties or ("AB" in self.properties and not "W" in self.properties):
|
||||
if "B" in self.properties or ("AB" in self.properties and "W" not in self.properties):
|
||||
return "B"
|
||||
else:
|
||||
return "W"
|
||||
@@ -321,6 +343,7 @@ class SGFNode:
|
||||
|
||||
|
||||
class SGF:
|
||||
DEFAULT_ENCODING = "ISO-8859-1" # as specified by the standard
|
||||
|
||||
_NODE_CLASS = SGFNode # Class used for SGF Nodes, can change this to something that inherits from SGFNode
|
||||
# https://xkcd.com/1171/
|
||||
@@ -360,8 +383,11 @@ class SGF:
|
||||
if match:
|
||||
encoding = match[1].decode("ascii", errors="ignore")
|
||||
else:
|
||||
encoding = "ISO-8859-1" # default
|
||||
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
|
||||
encoding = cls.DEFAULT_ENCODING
|
||||
try:
|
||||
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
|
||||
except LookupError:
|
||||
decoded = bin_contents.decode(encoding=cls.DEFAULT_ENCODING, errors="ignore")
|
||||
if is_ngf:
|
||||
return cls.parse_ngf(decoded)
|
||||
if is_gib:
|
||||
@@ -434,7 +460,7 @@ class SGF:
|
||||
re = "W+"
|
||||
elif "lack win" in lines[10]:
|
||||
re = "B+"
|
||||
except:
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if handicap < 0 or handicap > 9:
|
||||
@@ -522,7 +548,7 @@ class SGF:
|
||||
try:
|
||||
grlt = int(re.search(grlt_regex, line).group(1))
|
||||
zipsu = int(re.search(zipsu_regex, line).group(1))
|
||||
except:
|
||||
except: # noqa E722
|
||||
return ""
|
||||
return gib_make_result(grlt, zipsu)
|
||||
|
||||
@@ -556,7 +582,7 @@ class SGF:
|
||||
komi = int(re.search(r"GONGJE:(\d+),", line).group(1)) / 10
|
||||
if komi:
|
||||
root.set_property("KM", komi)
|
||||
except:
|
||||
except: # noqa E722
|
||||
pass
|
||||
|
||||
if line.startswith("\\[GAMETAG="):
|
||||
@@ -565,7 +591,7 @@ class SGF:
|
||||
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:
|
||||
except: # noqa E722
|
||||
pass
|
||||
|
||||
if "RE" not in root.properties:
|
||||
@@ -578,7 +604,7 @@ class SGF:
|
||||
komi = int(re.search(r",G(\d+),", line).group(1)) / 10
|
||||
if komi:
|
||||
root.set_property("KM", komi)
|
||||
except:
|
||||
except: # noqa E722
|
||||
pass
|
||||
|
||||
if line[0:3] == "INI":
|
||||
@@ -587,7 +613,7 @@ class SGF:
|
||||
setup = line.split()
|
||||
try:
|
||||
handicap = int(setup[3])
|
||||
except IndexError:
|
||||
except ParseError:
|
||||
continue
|
||||
|
||||
if handicap < 0 or handicap > 9:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
from typing import List, Tuple, TypeVar
|
||||
|
||||
try:
|
||||
import importlib.resources as pkg_resources
|
||||
except:
|
||||
except ImportError:
|
||||
import importlib_resources as pkg_resources
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -53,3 +54,21 @@ def find_package_resource(path, silent_errors=False):
|
||||
return os.path.join(PATHS["PACKAGE"], path.replace("katrain\\", "katrain/").replace("katrain/", ""))
|
||||
else:
|
||||
return os.path.abspath(os.path.expanduser(path)) # absolute path
|
||||
|
||||
|
||||
def pack_floats(float_list):
|
||||
return struct.pack(f"{len(float_list)}e", *float_list)
|
||||
|
||||
|
||||
def unpack_floats(str, num):
|
||||
return struct.unpack(f"{num}e", str)
|
||||
|
||||
|
||||
def format_visits(n):
|
||||
if n < 1000:
|
||||
return str(n)
|
||||
if n < 1e5:
|
||||
return f"{n/1000:.1f}k"
|
||||
if n < 1e6:
|
||||
return f"{n/1000:.0f}k"
|
||||
return f"{n/1e6:.0f}k"
|
||||
@@ -10,29 +10,14 @@
|
||||
#:import MODE_ANALYZE katrain.core.constants.MODE_ANALYZE
|
||||
#:import STATUS_INFO katrain.core.constants.STATUS_INFO
|
||||
|
||||
#:import Theme katrain.gui.theme.Theme
|
||||
#:import BLACK katrain.gui.theme.BLACK
|
||||
#:import LIGHTER_GREY katrain.gui.theme.LIGHTER_GREY
|
||||
|
||||
#:import WHITE katrain.gui.style.WHITE
|
||||
#:import BLACK katrain.gui.style.BLACK
|
||||
#:import LIGHTGREY katrain.gui.style.LIGHTGREY
|
||||
#:import GREEN katrain.gui.style.GREEN
|
||||
#:import YELLOW katrain.gui.style.YELLOW
|
||||
#:import RED katrain.gui.style.RED
|
||||
#:import DARKRED katrain.gui.style.DARKRED
|
||||
#:import ORANGE katrain.gui.style.ORANGE
|
||||
|
||||
#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR
|
||||
#:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR
|
||||
#:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR
|
||||
#:import TEXT_COLOR katrain.gui.style.TEXT_COLOR
|
||||
#:import SCORE_COLOR katrain.gui.style.SCORE_COLOR
|
||||
#:import WINRATE_COLOR katrain.gui.style.WINRATE_COLOR
|
||||
#:import POINTLOSS_COLOR katrain.gui.style.POINTLOSS_COLOR
|
||||
#:set BUTTON_INACTIVE_COLOR LIGHTGREY
|
||||
|
||||
#:set CP_SPACING dp(6)
|
||||
#:set CP_SMALL_SPACING dp(3)
|
||||
#:set CP_PADDING dp(6)
|
||||
#:set NOTES_FONT_SIZE sp(18)
|
||||
#:set CP_SPACING Theme.CP_SPACING
|
||||
#:set CP_SMALL_SPACING Theme.CP_SMALL_SPACING
|
||||
#:set CP_PADDING Theme.CP_PADDING
|
||||
|
||||
#:set EPSILON 1e-9
|
||||
|
||||
@@ -67,8 +52,8 @@
|
||||
|
||||
<SizedButton>:
|
||||
ripple_duration_in_slow: 0.6
|
||||
text_color: TEXT_COLOR
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
text_color: Theme.TEXT_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
label: label
|
||||
text_size: root.size
|
||||
Label:
|
||||
@@ -84,27 +69,27 @@
|
||||
width: root.label.texture_size[0]
|
||||
|
||||
<ToggleButtonMixin>:
|
||||
inactive_background_color: BACKGROUND_COLOR
|
||||
active_background_color: BOX_BACKGROUND_COLOR
|
||||
inactive_background_color: Theme.BACKGROUND_COLOR
|
||||
active_background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
background_color: self.active_background_color if self.state=='down' else self.inactive_background_color
|
||||
outline_color: self.active_outline_color if self.state=='down' else self.inactive_outline_color
|
||||
|
||||
<SizedRectangleButton>:
|
||||
outline_color: WHITE
|
||||
outline_color: Theme.BUTTON_BORDER_COLOR
|
||||
text_color: self.outline_color
|
||||
|
||||
<AutoSizedRectangleButton>:
|
||||
outline_color: WHITE
|
||||
outline_color: Theme.BUTTON_BORDER_COLOR
|
||||
text_color: self.outline_color
|
||||
|
||||
<SizedRectangleToggleButton>:
|
||||
inactive_outline_color: BUTTON_INACTIVE_COLOR
|
||||
active_outline_color: WHITE
|
||||
inactive_outline_color: Theme.BUTTON_INACTIVE_COLOR
|
||||
active_outline_color: Theme.BUTTON_BORDER_COLOR
|
||||
text_color: self.outline_color
|
||||
|
||||
<AutoSizedRectangleToggleButton>:
|
||||
inactive_outline_color: BUTTON_INACTIVE_COLOR
|
||||
active_outline_color: WHITE
|
||||
inactive_outline_color: Theme.BUTTON_INACTIVE_COLOR
|
||||
active_outline_color: Theme.BUTTON_BORDER_COLOR
|
||||
text_color: self.outline_color
|
||||
|
||||
<SizedRoundedRectangleButton@SizedRectangleButton>:
|
||||
@@ -114,7 +99,7 @@
|
||||
background_radius: self.height/3.5
|
||||
|
||||
<BGBoxLayout>:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
|
||||
<TransparentIconButton>:
|
||||
background_normal: ''
|
||||
@@ -130,20 +115,20 @@
|
||||
|
||||
|
||||
<I18NSpinnerOption@SpinnerOption>:
|
||||
background_color: LIGHTER_BACKGROUND_COLOR
|
||||
background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
background_normal: ''
|
||||
lang_change_tracking: i18n._('') # for font
|
||||
color: WHITE
|
||||
color: Theme.INPUT_FONT_COLOR
|
||||
canvas.after:
|
||||
Color:
|
||||
rgba: LIGHTGREY
|
||||
rgba: LIGHT_GREY
|
||||
Line
|
||||
points: self.x,self.y,self.x+self.width,self.y
|
||||
width: 1
|
||||
|
||||
<Spinner>:
|
||||
-font_size: self.height * 0.5
|
||||
background_color: [*[c*255/88 for c in BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
background_color: [*[c*255/88 for c in Theme.BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
option_cls: 'I18NSpinnerOption'
|
||||
|
||||
<Button>:
|
||||
@@ -158,7 +143,7 @@
|
||||
id: image
|
||||
pos: (root.pos[0] + (root.width - root.min_size)/2,root.pos[1] + (root.height - root.min_size)/2)
|
||||
size: (root.min_size,root.min_size)
|
||||
source: 'img/white.png' if root.player=='W' else 'img/black.png'
|
||||
source: 'white_circle.png' if root.player=='W' else 'black_circle.png'
|
||||
mipmap: True
|
||||
Label:
|
||||
id: lbl
|
||||
@@ -166,7 +151,7 @@
|
||||
text_size: None, root.min_size
|
||||
bold: True
|
||||
valign: 'center'
|
||||
color: BLACK if root.player=='W' else WHITE
|
||||
color: Theme.CIRCLE_TEXT_COLORS[root.player]
|
||||
pos: (image.x, image.y)
|
||||
size: image.size
|
||||
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text))
|
||||
@@ -202,13 +187,17 @@
|
||||
Label:
|
||||
size_hint: 0.075, 0.5
|
||||
NavIconButton:
|
||||
icon: 'img/Previous-End.png'
|
||||
on_press: root.katrain('undo',999)
|
||||
icon: 'Previous-Mistake.png'
|
||||
color: Theme.MISTAKE_BUTTON_COLOR
|
||||
on_press: root.katrain('find-mistake','undo')
|
||||
NavIconButton:
|
||||
icon: 'img/Previous-5.png'
|
||||
icon: 'Previous-End.png'
|
||||
on_press: root.katrain('undo',9999)
|
||||
NavIconButton:
|
||||
icon: 'Previous-5.png'
|
||||
on_press: root.katrain('undo',10)
|
||||
NavIconButton:
|
||||
icon: 'img/Previous.png'
|
||||
icon: 'Previous.png'
|
||||
on_press: root.katrain('undo')
|
||||
MDFloatLayout:
|
||||
size_hint: 0.05, 1
|
||||
@@ -226,25 +215,25 @@
|
||||
player: 'W'
|
||||
text: ''
|
||||
NavIconButton:
|
||||
icon: 'img/Next.png'
|
||||
icon: 'Next.png'
|
||||
on_press: root.katrain('redo')
|
||||
NavIconButton:
|
||||
icon: 'img/Next-5.png'
|
||||
icon: 'Next-5.png'
|
||||
on_press: root.katrain('redo',10)
|
||||
NavIconButton:
|
||||
icon: 'img/Next-End.png'
|
||||
on_press: root.katrain('redo',999)
|
||||
icon: 'Next-End.png'
|
||||
on_press: root.katrain('redo',9999)
|
||||
NavIconButton:
|
||||
icon: 'img/Next-Mistake.png'
|
||||
color: [0.65,0.05,0.05,1]
|
||||
on_press: root.katrain('next-mistake')
|
||||
icon: 'Next-Mistake.png'
|
||||
color: Theme.MISTAKE_BUTTON_COLOR
|
||||
on_press: root.katrain('find-mistake','redo')
|
||||
ClickableLabel:
|
||||
pos_hint: {'center_x': 0.85,'center_y':0.5}
|
||||
size_hint: None, 1
|
||||
width: self.height
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: YELLOW
|
||||
rgba: Theme.ENGINE_PONDERING_COLOR
|
||||
Ellipse:
|
||||
pos: self.pos[0] + self.width/2 - self.width/6, self.pos[1] + self.height/2 - self.width/6
|
||||
size: self.width/3, self.width/3
|
||||
@@ -267,7 +256,7 @@
|
||||
|
||||
|
||||
<HamburgerButton@TransparentIconButton>:
|
||||
icon: 'img/hamburger.png'
|
||||
icon: 'hamburger.png'
|
||||
width: self.height
|
||||
icon_size: [self.height * 0.5, self.height * 0.5]
|
||||
|
||||
@@ -278,8 +267,8 @@
|
||||
player: 'B'
|
||||
player_type: 'player:human'
|
||||
player_subtype: 'game:normal'
|
||||
background_color: BOX_BACKGROUND_COLOR if self.active else BACKGROUND_COLOR
|
||||
outline_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR if self.active else Theme.BACKGROUND_COLOR
|
||||
outline_color: Theme.BOX_BACKGROUND_COLOR
|
||||
outline_width: 2
|
||||
padding: [2*CP_PADDING,CP_PADDING,CP_PADDING,CP_PADDING]
|
||||
CircleWithText:
|
||||
@@ -313,9 +302,9 @@
|
||||
height: self.width
|
||||
size_hint: 1, None
|
||||
min_size: min(self.height,self.width)
|
||||
inactive_line_color: WHITE
|
||||
inactive_line_color: Theme.BUTTON_BORDER_COLOR
|
||||
inactive_fill_color: [1,1,1,0]
|
||||
active_line_color: ORANGE
|
||||
active_line_color: Theme.PAUSE_ACTIVE_COLOR
|
||||
active_fill_color: [1,1,1,0]
|
||||
line_width: max(1.1,self.min_size/20)
|
||||
line_color: root.active_line_color if self.active else root.inactive_line_color
|
||||
@@ -344,13 +333,12 @@
|
||||
cap: 'square'
|
||||
|
||||
|
||||
|
||||
<TimerLabel@Label>
|
||||
timeout: False
|
||||
color: ORANGE if self.timeout else GREEN
|
||||
color: Theme.TIMER_TEXT_TIMEOUT_COLOR if self.timeout else Theme.TIMER_TEXT_COLOR
|
||||
markup: True
|
||||
max_lines: 1
|
||||
font_name: 'fonts/digital-7 (mono).ttf'
|
||||
font_name: 'digital-7 (mono).ttf'
|
||||
|
||||
<Timer>:
|
||||
spacing: CP_SPACING
|
||||
@@ -362,7 +350,7 @@
|
||||
MDFloatLayout:
|
||||
size_hint: 0.4,1
|
||||
TimerLabel:
|
||||
color: BACKGROUND_COLOR
|
||||
color: Theme.BACKGROUND_COLOR
|
||||
text: '{}:88'.format('188' if root.state[0] >= 6000 else '88')
|
||||
font_size: 0.85*self.height
|
||||
pos_hint: {'right':1,'center_y':0.5}
|
||||
@@ -440,30 +428,30 @@
|
||||
|
||||
<StatsBox>
|
||||
orientation: 'vertical'
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
labels: {'score':score_label,'winrate':winrate_label,'points':pointloss_label}
|
||||
StatsLabel:
|
||||
id: winrate_label
|
||||
label: i18n._('stats:winrate')
|
||||
text: root.winrate
|
||||
color: WINRATE_COLOR
|
||||
color: Theme.WINRATE_COLOR
|
||||
StatsLabel:
|
||||
id: score_label
|
||||
label: i18n._('stats:score')
|
||||
text: root.score
|
||||
color: SCORE_COLOR
|
||||
color: Theme.SCORE_COLOR
|
||||
StatsLabel:
|
||||
id: pointloss_label
|
||||
label: i18n._('stats:pointslost') if root.points_lost is None or root.points_lost > 0 else i18n._('stats:pointsgained')
|
||||
text: '{}: {:.1f}'.format(root.player,abs(root.points_lost)) if root.points_lost is not None else '...'
|
||||
color: POINTLOSS_COLOR
|
||||
color: Theme.POINTLOSS_COLOR
|
||||
|
||||
<ScrollableLabel>:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
do_scroll_x: False
|
||||
scroll_type: ['bars']
|
||||
bar_width: 5
|
||||
bar_color: LIGHTGREY
|
||||
bar_color: Theme.SCROLLBAR_COLOR
|
||||
label: label
|
||||
canvas.before:
|
||||
Color:
|
||||
@@ -476,9 +464,9 @@
|
||||
lang_change_tracking: i18n._('')
|
||||
markup: root.markup
|
||||
padding: 5, 5
|
||||
font_size: NOTES_FONT_SIZE
|
||||
font_size: sp(Theme.NOTES_FONT_SIZE)
|
||||
line_height: root.line_height
|
||||
color: TEXT_COLOR
|
||||
color: Theme.TEXT_COLOR
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
text_size: self.width, None
|
||||
@@ -496,7 +484,7 @@
|
||||
<CollapsablePanelHeader>:
|
||||
canvas.after:
|
||||
Color:
|
||||
rgba: BOX_BACKGROUND_COLOR
|
||||
rgba: Theme.BOX_BACKGROUND_COLOR
|
||||
Line:
|
||||
width: 1
|
||||
points: (self.x,self.y+1,self.x+self.width,self.y+1)
|
||||
@@ -534,7 +522,7 @@
|
||||
TimerOrMoveTree:
|
||||
id: timer_or_movetree
|
||||
size_hint_y: None
|
||||
height: root.timer_box_height
|
||||
height: root.timer_box_height + max(0,int(self.mode != MODE_PLAY) * (root.tab_option_height + root.graph_height - graph_panel.height))
|
||||
BoxLayout:
|
||||
size_hint_x: EPSILON if self.parent.mode != MODE_PLAY else 1
|
||||
opacity: 0 if self.parent.mode != MODE_PLAY else 1
|
||||
@@ -557,15 +545,16 @@
|
||||
on_left_release: root.katrain("resign")
|
||||
MoveTree:
|
||||
id: move_tree
|
||||
min_height: root.timer_box_height
|
||||
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
|
||||
content_height: graph.height # + rank_graph.height
|
||||
content_height: graph.height
|
||||
options_height: root.tab_option_height
|
||||
closed_label: 'closedlabel:scoregraph'
|
||||
options: ['score','winrate']
|
||||
option_colors: [SCORE_COLOR,WINRATE_COLOR]
|
||||
option_colors: [Theme.SCORE_COLOR,Theme.WINRATE_COLOR]
|
||||
option_active: [True,False,False]
|
||||
on_option_state:
|
||||
graph.show_graphs(args[1])
|
||||
@@ -574,7 +563,7 @@
|
||||
id: graph
|
||||
opacity: 0 if self.hidden else 1
|
||||
size_hint_y: None
|
||||
height: EPSILON if self.hidden else root.graph_height
|
||||
height: root.graph_height
|
||||
CollapsablePanel:
|
||||
id: stats_panel
|
||||
num_active: 3
|
||||
@@ -582,7 +571,7 @@
|
||||
options_height: root.tab_option_height
|
||||
closed_label: 'closedlabel:movestats'
|
||||
options: ['score','winrate','points']
|
||||
option_colors: [SCORE_COLOR,WINRATE_COLOR,POINTLOSS_COLOR]
|
||||
option_colors: [Theme.SCORE_COLOR,Theme.WINRATE_COLOR,Theme.POINTLOSS_COLOR]
|
||||
option_active: [True,True,True]
|
||||
on_option_state:
|
||||
for k, v in args[1].items(): stats.labels[k].hidden = not v
|
||||
@@ -594,26 +583,26 @@
|
||||
size_hint_y_open: 1
|
||||
closed_label: 'closedlabel:info¬es'
|
||||
options: ['info','info-details','notes']
|
||||
option_colors: [WHITE,WHITE,YELLOW]
|
||||
option_colors: [Theme.INFO_TAB_FONT_COLOR,Theme.INFO_TAB_FONT_COLOR,Theme.NOTES_TAB_FONT_COLOR]
|
||||
option_active: [True,False,False]
|
||||
on_option_state:
|
||||
info.detailed = args[1]['info-details']
|
||||
info.size_hint_y = int(args[1]['info'] or info.detailed)+1e-9
|
||||
info.size_hint_y = int(args[1]['info'] or info.detailed)+EPSILON
|
||||
info.opacity = int(args[1]['info'] or info.detailed)
|
||||
notebox.size_hint_y = int(args[1]['notes'])*0.66+1e-9
|
||||
notebox.size_hint_y = int(args[1]['notes'])*0.66+EPSILON
|
||||
notebox.opacity = int(args[1]['notes'])
|
||||
app.gui.update_state()
|
||||
options_height: root.tab_option_height
|
||||
BGBoxLayout:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
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 if self.error else WHITE
|
||||
background_color: LIGHTER_BACKGROUND_COLOR # DARKRED
|
||||
outline_color: Theme.ERROR_BORDER_COLOR if self.error else Theme.INFO_TAB_FONT_COLOR
|
||||
background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
ScrollableLabel:
|
||||
id: info
|
||||
detailed: False
|
||||
@@ -626,31 +615,32 @@
|
||||
padding: 0
|
||||
id: notebox
|
||||
size_hint: 1,0
|
||||
IMETextField:
|
||||
LabelledTextInput:
|
||||
id: note
|
||||
font_size: NOTES_FONT_SIZE
|
||||
font_size: sp(Theme.NOTES_FONT_SIZE)
|
||||
multiline: True
|
||||
on_text: root.katrain.set_note(self.text)
|
||||
hint_text: i18n._('SGF Notes Hint')
|
||||
color_mode: 'custom'
|
||||
line_color_focus: TEXT_COLOR
|
||||
line_color_focus: Theme.TEXT_COLOR
|
||||
size_hint: 1, 1
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: LIGHTGREY
|
||||
rgba: LIGHTER_GREY
|
||||
Line:
|
||||
points: self.x, self.y+self.height, self.x+self.width, self.y+self.height
|
||||
width: 1
|
||||
Label:
|
||||
size_hint_y: 1e-9 # takes up remaining space if info is closed
|
||||
size_hint_y: EPSILON # takes up remaining space if info is closed
|
||||
|
||||
|
||||
<MenuItem>
|
||||
font_size: self.height * 0.5
|
||||
size_hint_y: None
|
||||
height: 50
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
content_width: icon.width + label.texture_size[0] * 1.33
|
||||
height: dp(50)
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
width_margin: 1.33
|
||||
content_width: icon.width + label.texture_size[0] * self.width_margin
|
||||
MDFloatLayout:
|
||||
id: icon
|
||||
size_hint: None, 1
|
||||
@@ -671,7 +661,7 @@
|
||||
size_hint: None, None
|
||||
font_size: root.font_size
|
||||
font_name: root.font_name
|
||||
color: WHITE
|
||||
color: Theme.MENU_ITEM_FONT_COLOR
|
||||
Label:
|
||||
text: root.shortcut
|
||||
halign: 'left'
|
||||
@@ -679,7 +669,7 @@
|
||||
size_hint_x: 1/3
|
||||
font_size: root.font_size
|
||||
font_name: root.font_name
|
||||
color: LIGHTGREY
|
||||
color: Theme.MENU_ITEM_SHORTCUT_COLOR
|
||||
|
||||
|
||||
|
||||
@@ -713,15 +703,15 @@
|
||||
|
||||
<AnalysisMenuItem@MenuItem>:
|
||||
on_close: app.gui.analysis_controls.close_dropdown()
|
||||
background_color: LIGHTER_BACKGROUND_COLOR
|
||||
height: 45
|
||||
background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
height: dp(45)
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: WHITE
|
||||
rgba: Theme.MENU_ITEM_FONT_COLOR
|
||||
Line:
|
||||
points: self.x,self.y, self.x,self.y+self.height
|
||||
Color:
|
||||
rgba: LIGHTGREY
|
||||
rgba: Theme.MENU_ITEM_SHORTCUT_COLOR
|
||||
Line
|
||||
points: self.x,self.y,self.x+self.width,self.y
|
||||
width: 1
|
||||
@@ -732,42 +722,52 @@
|
||||
katrain: app.gui
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:extra")
|
||||
icon: 'img/Extra.png'
|
||||
icon: 'Extra.png'
|
||||
shortcut: 'a'
|
||||
on_action: root.katrain("analyze-extra", "extra")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:equalize")
|
||||
icon: 'img/Equalize.png'
|
||||
icon: 'Equalize.png'
|
||||
shortcut: 's'
|
||||
on_action: root.katrain("analyze-extra", "equalize")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:sweep")
|
||||
icon: 'img/Sweep.png'
|
||||
icon: 'Sweep.png'
|
||||
shortcut: 'd'
|
||||
on_action: root.katrain("analyze-extra", "sweep")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:alternatives")
|
||||
icon: 'img/Alternative.png'
|
||||
icon: 'Alternative.png'
|
||||
shortcut: 'f'
|
||||
on_action: root.katrain("analyze-extra", "alternative")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:local")
|
||||
icon: 'img/local.png'
|
||||
text: i18n._("analysis:region")
|
||||
icon: 'local.png'
|
||||
shortcut: 'g'
|
||||
on_action: root.katrain("analyze-extra", "local")
|
||||
on_action: root.katrain("select-box",)
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:insert")
|
||||
icon: 'Insert-Move.png'
|
||||
shortcut: 'i'
|
||||
on_action: root.katrain("insert-mode")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:playtoend")
|
||||
icon: 'Finish.png'
|
||||
shortcut: 'l'
|
||||
on_action: root.katrain("play-to-end")
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:game")
|
||||
icon: 'img/analysis.png'
|
||||
icon: 'analysis.png'
|
||||
shortcut: ''
|
||||
on_action: root.open_game_analysis_popup()
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:continuous")
|
||||
icon: 'img/play.png'
|
||||
icon: 'play.png'
|
||||
shortcut: 'space'
|
||||
on_action: root.katrain.toggle_continuous_analysis()
|
||||
AnalysisMenuItem:
|
||||
text: i18n._("analysis:aimove")
|
||||
icon: 'img/ai.png'
|
||||
icon: 'ai.png'
|
||||
shortcut: 'enter'
|
||||
on_action: root.katrain("ai-move")
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
width: analysis_button.width
|
||||
size_hint: None, 1
|
||||
AutoSizedRectangleButton:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
size_hint_y: 0.5
|
||||
pos_hint: {'center_y': 0.5, 'x': 0}
|
||||
id: analysis_button
|
||||
@@ -825,7 +825,7 @@
|
||||
x: self.parent.x + 2
|
||||
size_hint: None, 0.4
|
||||
width: self.height/2
|
||||
source: 'img/menu.png'
|
||||
source: 'menu.png'
|
||||
mipmap: True
|
||||
|
||||
|
||||
@@ -841,7 +841,7 @@
|
||||
|
||||
|
||||
<MenuLabel@BackgroundMixin+Label>:
|
||||
background_color: BACKGROUND_COLOR
|
||||
background_color: Theme.BACKGROUND_COLOR
|
||||
font_size: self.height * 0.5
|
||||
height: self.parent.item_height or 50
|
||||
size_hint: 1,None
|
||||
@@ -921,7 +921,7 @@
|
||||
<HamburgerMenuContents@MDBoxLayout+BackgroundMixin>:
|
||||
katrain: app.gui
|
||||
player_setup: player_setup
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
orientation: 'vertical'
|
||||
item_height: self.height/20
|
||||
MenuLabel:
|
||||
@@ -939,38 +939,43 @@
|
||||
item_height: root.item_height
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:newgame')
|
||||
icon: 'img/New-Game.png'
|
||||
icon: 'New-Game.png'
|
||||
shortcut: 'Ctrl-N'
|
||||
on_action: root.katrain("new-game-popup")
|
||||
height: root.item_height
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:save')
|
||||
icon: 'img/Save-Game.png'
|
||||
icon: 'Save-Game.png'
|
||||
shortcut: 'Ctrl-S'
|
||||
on_action: root.katrain("output-sgf")
|
||||
on_action: root.katrain("save-game")
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:save-as')
|
||||
icon: 'Save-Game-As.png'
|
||||
shortcut: 'Ctrl-D'
|
||||
on_action: root.katrain("save-game-as-popup")
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:load')
|
||||
icon: 'img/Load-Game.png'
|
||||
icon: 'Load-Game.png'
|
||||
shortcut: 'Ctrl-L'
|
||||
on_action: root.katrain("analyze-sgf-popup")
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:clocksettings')
|
||||
icon: 'img/Time-Settings.png'
|
||||
icon: 'Time-Settings.png'
|
||||
shortcut: 'F5'
|
||||
on_action: root.katrain('timer-popup')
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:teachsettings')
|
||||
icon: 'img/Teaching-Settings.png'
|
||||
icon: 'Teaching-Settings.png'
|
||||
shortcut: 'F6'
|
||||
on_action: root.katrain('teacher-popup')
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:aisettings')
|
||||
icon: 'img/AI-Settings.png'
|
||||
icon: 'AI-Settings.png'
|
||||
shortcut: 'F7'
|
||||
on_action: root.katrain('ai-popup')
|
||||
MainMenuItem:
|
||||
text: i18n._('menu:settings')
|
||||
icon: 'img/General-Settings.png'
|
||||
icon: 'General-Settings.png'
|
||||
shortcut: 'F8'
|
||||
on_action: root.katrain('config-popup')
|
||||
MenuLabel:
|
||||
@@ -980,28 +985,28 @@
|
||||
height: 1.5 * root.item_height
|
||||
padding: CP_PADDING, 0
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-uk.png'
|
||||
icon: 'flags/flag-uk.png'
|
||||
on_press: app.language = 'en'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-de.png'
|
||||
icon: 'flags/flag-de.png'
|
||||
on_press: app.language = 'de'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-fr.png'
|
||||
icon: 'flags/flag-fr.png'
|
||||
on_press: app.language = 'fr'
|
||||
# LangButton:
|
||||
# icon: 'flags/flag-es.png'
|
||||
# on_press: app.language = 'es'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-es.png'
|
||||
on_press: app.language = 'es'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-ru.png'
|
||||
icon: 'flags/flag-ru.png'
|
||||
on_press: app.language = 'ru'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-cn.png'
|
||||
icon: 'flags/flag-cn.png'
|
||||
on_press: app.language = 'cn'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-ko.png'
|
||||
icon: 'flags/flag-ko.png'
|
||||
on_press: app.language = 'ko'
|
||||
LangButton:
|
||||
icon: 'img/flaticon/flag-jp.png'
|
||||
icon: 'flags/flag-jp.png'
|
||||
on_press: app.language = 'jp'
|
||||
BoxLayout:
|
||||
size_hint: 1,None
|
||||
@@ -1019,10 +1024,10 @@
|
||||
|
||||
|
||||
<PlayAnalyzeButton@SizedToggleButton>:
|
||||
inactive_background_color: BOX_BACKGROUND_COLOR
|
||||
active_background_color: YELLOW
|
||||
ripple_color: BACKGROUND_COLOR
|
||||
text_color: self.inactive_background_color if self.state=='down' else self.active_background_color
|
||||
inactive_background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
active_background_color: Theme.PLAY_ANALYZE_TAB_COLOR
|
||||
ripple_color: Theme.BACKGROUND_COLOR
|
||||
text_color: self.inactive_background_color if self.state=='down' else self.active_background_color
|
||||
size_hint: 0.5,1
|
||||
_font_size: self.height * 0.5
|
||||
group: 'play_analyze'
|
||||
@@ -1033,7 +1038,6 @@
|
||||
play: play
|
||||
analyze: analyze
|
||||
OutlineBox:
|
||||
outline_color: WHITE
|
||||
size_hint: 0.7,0.7
|
||||
pos_hint: {'center_x':0.5,'center_y':0.45}
|
||||
PlayAnalyzeButton:
|
||||
@@ -1061,7 +1065,7 @@
|
||||
Screen:
|
||||
name: 'gui'
|
||||
BGBoxLayout:
|
||||
background_color: BACKGROUND_COLOR
|
||||
background_color: Theme.BACKGROUND_COLOR
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
AnalysisControls:
|
||||
@@ -1098,7 +1102,3 @@
|
||||
on_state: if self.state=='close': root.update_state() # ai move etc on nav drawer close
|
||||
HamburgerMenuContents:
|
||||
id: nav_drawer_contents
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import copy
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
@@ -17,34 +16,65 @@ from kivymd.app import MDApp
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
|
||||
from katrain.core.constants import MODE_PLAY, OUTPUT_DEBUG, STATUS_TEACHING, OUTPUT_EXTRA_DEBUG
|
||||
from katrain.core.constants import (
|
||||
MODE_PLAY,
|
||||
OUTPUT_DEBUG,
|
||||
OUTPUT_EXTRA_DEBUG,
|
||||
STATUS_TEACHING,
|
||||
TOP_MOVE_DELTA_SCORE,
|
||||
TOP_MOVE_DELTA_WINRATE,
|
||||
TOP_MOVE_NOTHING,
|
||||
TOP_MOVE_OPTIONS,
|
||||
TOP_MOVE_SCORE,
|
||||
TOP_MOVE_VISITS,
|
||||
TOP_MOVE_WINRATE,
|
||||
)
|
||||
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 BackgroundMixin, draw_circle, draw_text
|
||||
from katrain.core.utils import evaluation_class, format_visits, var_to_grid
|
||||
from katrain.gui.kivyutils import draw_circle, draw_text, cached_texture
|
||||
from katrain.gui.popups import I18NPopup, ReAnalyzeGamePopup
|
||||
from katrain.gui.style import *
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
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.stones_sounds = [SoundLoader.load(file) for file in Theme.STONE_SOUNDS]
|
||||
self.trainer_config = {}
|
||||
self.ghost_stone = []
|
||||
self.gridpos_x = []
|
||||
self.gridpos_y = []
|
||||
self.grid_size = 0
|
||||
self.stone_size = 0
|
||||
self.selecting_region_of_interest = False
|
||||
self.region_of_interest = []
|
||||
self.draw_coords_enabled = True
|
||||
|
||||
self.active_pv_moves = []
|
||||
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.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents, 0.05)
|
||||
self.redraw_trigger = Clock.create_trigger(self.redraw, 0.05)
|
||||
self.redraw_hover_contents_trigger = Clock.create_trigger(self.draw_hover_contents, 0.01)
|
||||
self.bind(size=self.redraw_trigger, pos=self.redraw_trigger)
|
||||
Clock.schedule_interval(self.animate_pv, 0.1)
|
||||
|
||||
def toggle_coordinates(self):
|
||||
self.draw_coords_enabled = not self.draw_coords_enabled
|
||||
self.redraw_trigger()
|
||||
return self.draw_coords_enabled
|
||||
|
||||
def get_enable_coordinates(self):
|
||||
return self.draw_coords_enabled
|
||||
|
||||
def play_stone_sound(self, *_args):
|
||||
if self.katrain.config("timer/sound"):
|
||||
sound = random.choice(self.stones_sounds)
|
||||
if sound:
|
||||
sound.play()
|
||||
|
||||
# stone placement functions
|
||||
def _find_closest(self, pos, gridpos):
|
||||
return sorted([(abs(p - pos), i) for i, p in enumerate(gridpos)])[0]
|
||||
@@ -60,29 +90,50 @@ class BadukPanWidget(Widget):
|
||||
else:
|
||||
self.ghost_stone = None
|
||||
if prev_ghost != self.ghost_stone:
|
||||
self.draw_hover_contents()
|
||||
self.redraw_hover_contents_trigger()
|
||||
|
||||
def update_box_selection(self, touch, second_point=True):
|
||||
if not self.gridpos_x:
|
||||
return
|
||||
_, xp = self._find_closest(touch.x, self.gridpos_x)
|
||||
_, yp = self._find_closest(touch.y, self.gridpos_y)
|
||||
if second_point and len(self.region_of_interest) == 4:
|
||||
self.region_of_interest[1] = xp
|
||||
self.region_of_interest[3] = yp
|
||||
else:
|
||||
self.region_of_interest = [xp, xp, yp, yp]
|
||||
self.redraw_hover_contents_trigger()
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
self.set_animating_pv(None, None) # any click kills PV from label/move
|
||||
if "button" in touch.profile and touch.button != "left":
|
||||
return
|
||||
self.check_next_move_ghost(touch)
|
||||
if self.selecting_region_of_interest:
|
||||
self.update_box_selection(touch, second_point=False)
|
||||
else:
|
||||
self.check_next_move_ghost(touch)
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if "button" in touch.profile and touch.button != "left":
|
||||
return
|
||||
return self.check_next_move_ghost(touch)
|
||||
if self.selecting_region_of_interest:
|
||||
return self.update_box_selection(touch)
|
||||
else:
|
||||
return self.check_next_move_ghost(touch)
|
||||
|
||||
def on_mouse_pos(self, *args): # https://gist.github.com/opqopq/15c707dc4cffc2b6455f
|
||||
if self.get_root_window(): # don't proceed if I'm not displayed <=> If have no parent
|
||||
pos = args[1]
|
||||
rel_pos = self.to_widget(*pos) # compensate for relative layout
|
||||
inside = self.collide_point(*rel_pos)
|
||||
if inside and self.active_pv_moves:
|
||||
|
||||
if inside and self.active_pv_moves and not self.selecting_region_of_interest:
|
||||
near_move = [
|
||||
(pv, node)
|
||||
for move, pv, node in self.active_pv_moves
|
||||
if abs(rel_pos[0] - self.gridpos_x[move[0]]) < self.grid_size / 2
|
||||
if move[0] < len(self.gridpos_x)
|
||||
and move[1] < len(self.gridpos_y)
|
||||
and abs(rel_pos[0] - self.gridpos_x[move[0]]) < self.grid_size / 2
|
||||
and abs(rel_pos[1] - self.gridpos_y[move[1]]) < self.grid_size / 2
|
||||
]
|
||||
if near_move:
|
||||
@@ -95,17 +146,17 @@ class BadukPanWidget(Widget):
|
||||
self.set_animating_pv(None, None) # any click kills PV from label/move
|
||||
self.last_mouse_pos = pos
|
||||
|
||||
def play_stone_sound(self, *_args):
|
||||
if self.katrain.config("timer/sound"):
|
||||
sound = random.choice(self.stones_sounds)
|
||||
if sound:
|
||||
sound.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"):
|
||||
if self.selecting_region_of_interest:
|
||||
if len(self.region_of_interest) == 4:
|
||||
self.katrain.game.set_region_of_interest(self.region_of_interest)
|
||||
self.region_of_interest = []
|
||||
self.selecting_region_of_interest = False
|
||||
|
||||
elif self.ghost_stone and ("button" not in touch.profile or touch.button == "left"):
|
||||
game = self.katrain and self.katrain.game
|
||||
current_node = game and self.katrain.game.current_node
|
||||
if (
|
||||
@@ -144,7 +195,7 @@ class BadukPanWidget(Widget):
|
||||
self.set_animating_pv(nodes_here[-1].parent.candidate_moves[0]["pv"], nodes_here[-1].parent)
|
||||
|
||||
self.ghost_stone = None
|
||||
self.draw_hover_contents() # remove ghost
|
||||
self.redraw_hover_contents_trigger() # remove ghost
|
||||
|
||||
# drawing functions
|
||||
def redraw(self, *_args):
|
||||
@@ -157,16 +208,18 @@ class BadukPanWidget(Widget):
|
||||
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",
|
||||
texture=cached_texture(Theme.STONE_TEXTURE[player]),
|
||||
)
|
||||
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))
|
||||
evalsize = self.stone_size * (
|
||||
Theme.EVAL_DOT_MIN_SIZE + eval_radius * (Theme.EVAL_DOT_MAX_SIZE - Theme.EVAL_DOT_MIN_SIZE)
|
||||
)
|
||||
Color(*evalcol)
|
||||
Rectangle(
|
||||
pos=(self.gridpos_x[x] - evalsize, self.gridpos_y[y] - evalsize),
|
||||
size=(2 * evalsize, 2 * evalsize),
|
||||
source=f"img/dot.png",
|
||||
texture=cached_texture(Theme.EVAL_DOT_TEXTURE),
|
||||
)
|
||||
if innercol:
|
||||
Color(*innercol)
|
||||
@@ -174,12 +227,12 @@ class BadukPanWidget(Widget):
|
||||
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",
|
||||
texture=cached_texture(Theme.LAST_MOVE_TEXTURE),
|
||||
)
|
||||
|
||||
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"])
|
||||
colors = EVAL_COLORS[self.trainer_config["theme"]]
|
||||
colors = Theme.EVAL_COLORS[self.trainer_config["theme"]]
|
||||
if show_dots_for_class is None or show_dots_for_class[i]:
|
||||
return colors[i]
|
||||
|
||||
@@ -192,8 +245,12 @@ class BadukPanWidget(Widget):
|
||||
with self.canvas.before:
|
||||
self.canvas.before.clear()
|
||||
# set up margins and grid lines
|
||||
grid_spaces_margin_x = [1.5, 0.75] # left, right
|
||||
grid_spaces_margin_y = [1.5, 0.75] # bottom, top
|
||||
if self.draw_coords_enabled:
|
||||
grid_spaces_margin_x = [1.5, 0.75] # left, right
|
||||
grid_spaces_margin_y = [1.5, 0.75] # bottom, top
|
||||
else: # no coordinates means remove the offset
|
||||
grid_spaces_margin_x = [0.75, 0.75] # left, right
|
||||
grid_spaces_margin_y = [0.75, 0.75] # bottom, top
|
||||
x_grid_spaces = board_size_x - 1 + sum(grid_spaces_margin_x)
|
||||
y_grid_spaces = board_size_y - 1 + sum(grid_spaces_margin_y)
|
||||
self.grid_size = min(self.width / x_grid_spaces, self.height / y_grid_spaces)
|
||||
@@ -201,7 +258,7 @@ class BadukPanWidget(Widget):
|
||||
board_height_with_margins = y_grid_spaces * self.grid_size
|
||||
extra_px_margin_x = (self.width - board_width_with_margins) / 2
|
||||
extra_px_margin_y = (self.height - board_height_with_margins) / 2
|
||||
self.stone_size = self.grid_size * STONE_SIZE
|
||||
self.stone_size = self.grid_size * Theme.STONE_SIZE
|
||||
|
||||
self.gridpos_x = [
|
||||
self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5)
|
||||
@@ -212,14 +269,20 @@ class BadukPanWidget(Widget):
|
||||
for i in range(board_size_y)
|
||||
]
|
||||
|
||||
Color(1, 0.95, 0.8, 1) # image is a bit too light
|
||||
if katrain.game.insert_mode:
|
||||
Color(*Theme.INSERT_BOARD_COLOR_TINT) # dreamy
|
||||
else:
|
||||
Color(*Theme.BOARD_COLOR_TINT) # 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),
|
||||
pos=(
|
||||
self.gridpos_x[0] - self.grid_size * grid_spaces_margin_x[0],
|
||||
self.gridpos_y[0] - self.grid_size * grid_spaces_margin_y[0],
|
||||
),
|
||||
size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces),
|
||||
source="img/board.png",
|
||||
texture=cached_texture(Theme.BOARD_TEXTURE),
|
||||
)
|
||||
|
||||
Color(*LINE_COLOR)
|
||||
Color(*Theme.LINE_COLOR)
|
||||
for i in range(board_size_x):
|
||||
Line(points=[(self.gridpos_x[i], self.gridpos_y[0]), (self.gridpos_x[i], self.gridpos_y[-1])])
|
||||
for i in range(board_size_y):
|
||||
@@ -234,28 +297,29 @@ class BadukPanWidget(Widget):
|
||||
[int(size / 2)] if size % 2 == 1 and size > 7 else []
|
||||
)
|
||||
|
||||
starpt_size = self.grid_size * STARPOINT_SIZE
|
||||
starpt_size = self.grid_size * Theme.STARPOINT_SIZE
|
||||
for x in star_point_coords(board_size_x):
|
||||
for y in star_point_coords(board_size_y):
|
||||
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, LINE_COLOR)
|
||||
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, Theme.LINE_COLOR)
|
||||
|
||||
# coordinates
|
||||
Color(0.25, 0.25, 0.25)
|
||||
coord_offset = self.grid_size * 1.5 / 2
|
||||
for i in range(board_size_x):
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset),
|
||||
text=Move.GTP_COORD[i],
|
||||
font_size=self.grid_size / 1.5,
|
||||
font_name="Roboto",
|
||||
)
|
||||
for i in range(board_size_y):
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]),
|
||||
text=str(i + 1),
|
||||
font_size=self.grid_size / 1.5,
|
||||
font_name="Roboto",
|
||||
)
|
||||
if self.draw_coords_enabled:
|
||||
Color(0.25, 0.25, 0.25)
|
||||
coord_offset = self.grid_size * 1.5 / 2
|
||||
for i in range(board_size_x):
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset),
|
||||
text=Move.GTP_COORD[i],
|
||||
font_size=self.grid_size / 1.5,
|
||||
font_name="Roboto",
|
||||
)
|
||||
for i in range(board_size_y):
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]),
|
||||
text=str(i + 1),
|
||||
font_size=self.grid_size / 1.5,
|
||||
font_name="Roboto",
|
||||
)
|
||||
|
||||
def draw_board_contents(self, *_args):
|
||||
if not (self.katrain and self.katrain.game):
|
||||
@@ -303,7 +367,7 @@ class BadukPanWidget(Widget):
|
||||
evalcol = self.eval_color(points_lost, show_dots_for_class)
|
||||
else:
|
||||
evalcol = None
|
||||
inner = STONE_COLORS[m.opponent] if i == 0 and not m in placements else None
|
||||
inner = Theme.STONE_COLORS[m.opponent] if i == 0 and m not in placements else None
|
||||
drawn_stone[m.coords] = m.player
|
||||
self.draw_stone(
|
||||
x=m.coords[0],
|
||||
@@ -319,9 +383,9 @@ class BadukPanWidget(Widget):
|
||||
for y in range(0, board_size_y):
|
||||
evalcol = self.eval_color(16 * y / board_size_y)
|
||||
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(1, y, "B", innercol=Theme.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(3, y, "W", innercol=Theme.STONE_COLORS["B"], evalcol=evalcol)
|
||||
|
||||
# ownership - allow one move out of date for smooth animation
|
||||
ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership)
|
||||
@@ -342,7 +406,7 @@ class BadukPanWidget(Widget):
|
||||
for x in range(board_size_x):
|
||||
loss = max(0, (-1 if current_node.children[-1].move.player == "B" else 1) * loss_grid[y][x])
|
||||
if loss > 0:
|
||||
Color(*EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss)
|
||||
Color(*Theme.EVAL_COLORS[self.trainer_config["theme"]][1][:3], loss)
|
||||
Rectangle(
|
||||
pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz)
|
||||
)
|
||||
@@ -352,7 +416,7 @@ class BadukPanWidget(Widget):
|
||||
for x in range(board_size_x):
|
||||
ix_owner = "B" if ownership_grid[y][x] > 0 else "W"
|
||||
if ix_owner != (has_stone.get((x, y), -1)):
|
||||
Color(*STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x]))
|
||||
Color(*Theme.STONE_COLORS[ix_owner][:3], abs(ownership_grid[y][x]))
|
||||
Rectangle(
|
||||
pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz)
|
||||
)
|
||||
@@ -374,24 +438,51 @@ class BadukPanWidget(Widget):
|
||||
if katrain.analysis_controls.policy.active and policy:
|
||||
policy_grid = var_to_grid(policy, (board_size_x, board_size_y))
|
||||
best_move_policy = max(*policy)
|
||||
colors = Theme.EVAL_COLORS[self.trainer_config["theme"]]
|
||||
text_lb = 0.01 * 0.01
|
||||
for y in range(board_size_y - 1, -1, -1):
|
||||
for x in range(board_size_x):
|
||||
if policy_grid[y][x] > 0:
|
||||
polsize = 1.1 * math.sqrt(policy_grid[y][x])
|
||||
policy_circle_color = (
|
||||
*POLICY_COLOR,
|
||||
POLICY_ALPHA + TOP_POLICY_ALPHA * (policy_grid[y][x] == best_move_policy),
|
||||
)
|
||||
move_policy = policy_grid[y][x]
|
||||
if move_policy < 0:
|
||||
continue
|
||||
pol_order = max(0, 5 + int(math.log10(max(1e-9, move_policy - 1e-9))))
|
||||
if move_policy > text_lb:
|
||||
draw_circle(
|
||||
(self.gridpos_x[x], self.gridpos_y[y]), polsize * self.stone_size, policy_circle_color
|
||||
(self.gridpos_x[x], self.gridpos_y[y]),
|
||||
self.stone_size * Theme.HINT_SCALE * 0.98,
|
||||
Theme.APPROX_BOARD_COLOR,
|
||||
)
|
||||
polsize = math.sqrt(policy[-1])
|
||||
scale = 0.95
|
||||
else:
|
||||
scale = 0.5
|
||||
draw_circle(
|
||||
(self.gridpos_x[x], self.gridpos_y[y]),
|
||||
Theme.HINT_SCALE * self.stone_size * scale,
|
||||
(*colors[pol_order][:3], Theme.POLICY_ALPHA),
|
||||
)
|
||||
if move_policy > text_lb:
|
||||
Color(*Theme.HINT_TEXT_COLOR)
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[x], self.gridpos_y[y]),
|
||||
text=f"{100 * move_policy :.2f}"[:4] + "%",
|
||||
font_name="Roboto",
|
||||
halign="center",
|
||||
)
|
||||
if move_policy == best_move_policy:
|
||||
Color(*Theme.TOP_MOVE_BORDER_COLOR[:3], Theme.POLICY_ALPHA)
|
||||
Line(
|
||||
circle=(self.gridpos_x[x], self.gridpos_y[y], self.stone_size - dp(1.2),), width=dp(2),
|
||||
)
|
||||
|
||||
with pass_btn.canvas.after:
|
||||
draw_circle(
|
||||
(pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2),
|
||||
polsize * pass_btn.height / 2,
|
||||
POLICY_COLOR,
|
||||
)
|
||||
move_policy = policy[-1]
|
||||
pol_order = 5 - int(-math.log10(max(1e-9, move_policy - 1e-9)))
|
||||
if pol_order >= 0:
|
||||
draw_circle(
|
||||
(pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2),
|
||||
pass_btn.height / 2,
|
||||
(*colors[pol_order][:3], Theme.GHOST_ALPHA),
|
||||
)
|
||||
|
||||
# pass circle
|
||||
passed = len(nodes) > 1 and current_node.is_pass
|
||||
@@ -401,23 +492,34 @@ class BadukPanWidget(Widget):
|
||||
katrain.controls.timer.paused = True
|
||||
else:
|
||||
text = i18n._("board-pass")
|
||||
Color(0.45, 0.05, 0.45, 0.7)
|
||||
Color(*Theme.PASS_CIRCLE_COLOR)
|
||||
center = (self.gridpos_x[int(board_size_x / 2)], self.gridpos_y[int(board_size_y / 2)])
|
||||
size = min(self.width, self.height) * 0.227
|
||||
Ellipse(pos=(center[0] - size / 2, center[1] - size / 2), size=(size, size))
|
||||
Color(0.85, 0.85, 0.85)
|
||||
draw_text(
|
||||
pos=center, text=text, font_size=size * 0.25, halign="center", outline_color=[0.95, 0.95, 0.95]
|
||||
)
|
||||
Color(*Theme.PASS_CIRCLE_TEXT_COLOR)
|
||||
draw_text(pos=center, text=text, font_size=size * 0.25, halign="center")
|
||||
|
||||
self.draw_hover_contents()
|
||||
self.redraw_hover_contents_trigger()
|
||||
|
||||
def draw_roi_box(self, region_of_interest, width=2):
|
||||
xmin, xmax, ymin, ymax = region_of_interest
|
||||
Color(*Theme.REGION_BORDER_COLOR)
|
||||
Line(
|
||||
rectangle=(
|
||||
self.gridpos_x[xmin] - self.grid_size / 3,
|
||||
self.gridpos_y[ymin] - self.grid_size / 3,
|
||||
(xmax - xmin + 2 / 3) * self.grid_size,
|
||||
(ymax - ymin + 2 / 3) * self.grid_size,
|
||||
),
|
||||
width=width,
|
||||
)
|
||||
|
||||
def draw_hover_contents(self, *_args):
|
||||
ghost_alpha = POLICY_ALPHA
|
||||
ghost_alpha = Theme.GHOST_ALPHA
|
||||
katrain = self.katrain
|
||||
game_ended = katrain.game.end_result
|
||||
current_node = katrain.game.current_node
|
||||
player, next_player = current_node.player, current_node.next_player
|
||||
next_player = current_node.next_player
|
||||
|
||||
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:
|
||||
@@ -425,8 +527,8 @@ class BadukPanWidget(Widget):
|
||||
|
||||
with self.canvas.after:
|
||||
self.canvas.after.clear()
|
||||
self.active_pv_moves = []
|
||||
|
||||
self.active_pv_moves = []
|
||||
# hints or PV
|
||||
hint_moves = []
|
||||
if (
|
||||
@@ -444,50 +546,87 @@ class BadukPanWidget(Widget):
|
||||
]
|
||||
|
||||
top_move_coords = None
|
||||
child_moves = {c.move.gtp() for c in current_node.children if c.move}
|
||||
if hint_moves:
|
||||
low_visits_threshold = katrain.config("trainer/low_visits", 25)
|
||||
top_moves_show = [
|
||||
opt
|
||||
for opt in [
|
||||
katrain.config("trainer/top_moves_show"),
|
||||
katrain.config("trainer/top_moves_show_secondary"),
|
||||
]
|
||||
if opt in TOP_MOVE_OPTIONS and opt != TOP_MOVE_NOTHING
|
||||
]
|
||||
for move_dict in hint_moves:
|
||||
move = Move.from_gtp(move_dict["move"])
|
||||
if move.coords is not None:
|
||||
engine_best_move = move_dict.get("order", 99) == 0
|
||||
scale = HINT_SCALE
|
||||
scale = Theme.HINT_SCALE
|
||||
text_on = True
|
||||
alpha = HINTS_ALPHA
|
||||
if move_dict["visits"] < low_visits_threshold and not engine_best_move:
|
||||
scale = UNCERTAIN_HINT_SCALE
|
||||
alpha = Theme.HINTS_ALPHA
|
||||
if (
|
||||
move_dict["visits"] < low_visits_threshold
|
||||
and not engine_best_move
|
||||
and not move_dict["move"] in child_moves
|
||||
):
|
||||
scale = Theme.UNCERTAIN_HINT_SCALE
|
||||
text_on = False
|
||||
alpha = HINTS_MIN_ALPHA + (HINTS_ALPHA - HINTS_MIN_ALPHA) * (
|
||||
move_dict["visits"] / low_visits_threshold
|
||||
)
|
||||
alpha = Theme.HINTS_LO_ALPHA
|
||||
|
||||
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)
|
||||
evalsize = self.stone_size * scale
|
||||
evalcol = self.eval_color(move_dict["pointsLost"])
|
||||
if text_on and top_moves_show: # remove grid lines using a board colored circle
|
||||
draw_circle(
|
||||
(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]),
|
||||
self.stone_size * scale * 0.98,
|
||||
Theme.APPROX_BOARD_COLOR,
|
||||
)
|
||||
|
||||
Color(*evalcol[:3], alpha)
|
||||
Rectangle(
|
||||
pos=(self.gridpos_x[move.coords[0]] - evalsize, self.gridpos_y[move.coords[1]] - evalsize),
|
||||
size=(2 * evalsize, 2 * evalsize),
|
||||
source="img/topmove.png",
|
||||
texture=cached_texture(Theme.TOP_MOVE_TEXTURE),
|
||||
)
|
||||
if self.trainer_config["text_point_loss"] and text_on:
|
||||
if -0.05 < move_dict["pointsLost"] < 0.05:
|
||||
ptloss_text = "0.0"
|
||||
if text_on and top_moves_show: # TODO: faster if not sized?
|
||||
keys = {"size": self.grid_size / 3, "smallsize": self.grid_size / 3.33}
|
||||
player_sign = current_node.player_sign(next_player)
|
||||
if len(top_moves_show) == 1:
|
||||
fmt = "[size={size:.0f}]{" + top_moves_show[0] + "}[/size]"
|
||||
else:
|
||||
ptloss_text = f"{-move_dict['pointsLost']:+.1f}"
|
||||
sizefac = 1
|
||||
Color(*BLACK)
|
||||
fmt = (
|
||||
"[size={size:.0f}]{"
|
||||
+ top_moves_show[0]
|
||||
+ "}[/size]\n[size={smallsize:.0f}]{"
|
||||
+ top_moves_show[1]
|
||||
+ "}[/size]"
|
||||
)
|
||||
|
||||
keys[TOP_MOVE_DELTA_SCORE] = (
|
||||
"0.0" if -0.05 < move_dict["pointsLost"] < 0.05 else f"{-move_dict['pointsLost']:+.1f}"
|
||||
)
|
||||
keys[TOP_MOVE_SCORE] = f"{player_sign * move_dict['scoreLead']:.1f}"
|
||||
winrate = move_dict["winrate"] if player_sign == 1 else 1 - move_dict["winrate"]
|
||||
keys[TOP_MOVE_WINRATE] = f"{winrate*100:.1f}"
|
||||
keys[TOP_MOVE_DELTA_WINRATE] = f"{-move_dict['winrateLost']:+.1%}"
|
||||
keys[TOP_MOVE_VISITS] = format_visits(move_dict["visits"])
|
||||
Color(*Theme.HINT_TEXT_COLOR)
|
||||
draw_text(
|
||||
pos=(self.gridpos_x[move.coords[0]], self.gridpos_y[move.coords[1]]),
|
||||
text=ptloss_text,
|
||||
font_size=self.grid_size * sizefac / 2.5,
|
||||
text=fmt.format(**keys),
|
||||
font_name="Roboto",
|
||||
markup=True,
|
||||
line_height=0.85,
|
||||
halign="center",
|
||||
)
|
||||
|
||||
if engine_best_move:
|
||||
top_move_coords = move.coords
|
||||
Color(*TOP_MOVE_BORDER_COLOR)
|
||||
Color(*Theme.TOP_MOVE_BORDER_COLOR)
|
||||
Line(
|
||||
circle=(
|
||||
self.gridpos_x[move.coords[0]],
|
||||
@@ -509,7 +648,7 @@ class BadukPanWidget(Widget):
|
||||
|
||||
if move.coords != top_move_coords: # for contrast
|
||||
dashed_width = 18
|
||||
Color(*STONE_CONTRAST_COLORS[child_node.player])
|
||||
Color(*Theme.NEXT_MOVE_DASH_CONTRAST_COLORS[child_node.player])
|
||||
Line(
|
||||
circle=(
|
||||
self.gridpos_x[move.coords[0]],
|
||||
@@ -520,7 +659,7 @@ class BadukPanWidget(Widget):
|
||||
)
|
||||
else:
|
||||
dashed_width = 10
|
||||
Color(*STONE_COLORS[child_node.player])
|
||||
Color(*Theme.STONE_COLORS[child_node.player])
|
||||
for s in range(0, 360, 30):
|
||||
Line(
|
||||
circle=(
|
||||
@@ -533,24 +672,31 @@ class BadukPanWidget(Widget):
|
||||
width=dp(1.2),
|
||||
)
|
||||
|
||||
# hover next move ghost stone
|
||||
if self.ghost_stone:
|
||||
self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha)
|
||||
if self.selecting_region_of_interest and len(self.region_of_interest) == 4:
|
||||
x1, x2, y1, y2 = self.region_of_interest
|
||||
self.draw_roi_box([min(x1, x2), max(x1, x2), min(y1, y2), max(y1, y2)], width=dp(2))
|
||||
else:
|
||||
# hover next move ghost stone
|
||||
if self.ghost_stone:
|
||||
self.draw_stone(*self.ghost_stone, next_player, alpha=ghost_alpha)
|
||||
|
||||
animating_pv = self.animating_pv
|
||||
if animating_pv:
|
||||
pv, node, start_time, _ = animating_pv
|
||||
delay = self.katrain.config("general/anim_pv_time", 0.5)
|
||||
up_to_move = (time.time() - start_time) / delay
|
||||
self.draw_pv(pv, node, up_to_move)
|
||||
animating_pv = self.animating_pv
|
||||
if animating_pv:
|
||||
pv, node, start_time, _ = animating_pv
|
||||
delay = self.katrain.config("general/anim_pv_time", 0.5)
|
||||
up_to_move = (time.time() - start_time) / delay
|
||||
self.draw_pv(pv, node, up_to_move)
|
||||
|
||||
if self.katrain.game.region_of_interest:
|
||||
self.draw_roi_box(self.katrain.game.region_of_interest, width=dp(1.25))
|
||||
|
||||
def animate_pv(self, _dt):
|
||||
if self.animating_pv:
|
||||
self.draw_hover_contents()
|
||||
self.redraw_hover_contents_trigger()
|
||||
|
||||
def draw_pv(self, pv, node, up_to_move):
|
||||
katrain = self.katrain
|
||||
next_last_player = [node.next_player, node.player]
|
||||
next_last_player = [node.next_player, Move.opponent_player(node.next_player)]
|
||||
cn = katrain.game.current_node
|
||||
if node != cn and node.parent != cn:
|
||||
hide_node = cn
|
||||
@@ -581,9 +727,9 @@ class BadukPanWidget(Widget):
|
||||
Rectangle( # not sure why the -1 here, but seems to center better
|
||||
pos=(board_coords[0] - stone_size - 1, board_coords[1] - stone_size),
|
||||
size=(2 * stone_size + 1, 2 * stone_size + 1),
|
||||
source=f"img/{move_player}_stone.png",
|
||||
texture=cached_texture(Theme.STONE_TEXTURE[move_player]),
|
||||
)
|
||||
Color(*STONE_TEXT_COLORS[move_player])
|
||||
Color(*Theme.PV_TEXT_COLORS[move_player])
|
||||
draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
|
||||
|
||||
def set_animating_pv(self, pv, node):
|
||||
@@ -593,7 +739,7 @@ class BadukPanWidget(Widget):
|
||||
not self.animating_pv or not (self.animating_pv[0] == pv and self.animating_pv[1] == node)
|
||||
):
|
||||
self.animating_pv = (pv, node, time.time(), self.last_mouse_pos)
|
||||
self.draw_hover_contents()
|
||||
self.redraw_hover_contents_trigger()
|
||||
|
||||
def show_pv_from_comments(self, pv_str):
|
||||
self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent)
|
||||
@@ -635,6 +781,6 @@ class AnalysisControls(MDBoxLayout):
|
||||
|
||||
|
||||
class BadukPanControls(MDFloatLayout):
|
||||
engine_status_col = ListProperty(ENGINE_DOWN_COL)
|
||||
engine_status_col = ListProperty(Theme.ENGINE_DOWN_COLOR)
|
||||
engine_status_pondering = NumericProperty(-1)
|
||||
queries_remaining = NumericProperty(0)
|
||||
@@ -2,13 +2,14 @@ import time
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.core.audio import SoundLoader
|
||||
from kivy.properties import ObjectProperty, OptionProperty, NumericProperty
|
||||
from kivy.properties import ObjectProperty, OptionProperty
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
|
||||
from katrain.core.constants import MODE_PLAY, MODE_ANALYZE, STATUS_ERROR, STATUS_ANALYSIS, PLAYER_HUMAN
|
||||
from katrain.core.constants import MODE_ANALYZE, MODE_PLAY, PLAYER_HUMAN, STATUS_ANALYSIS, STATUS_ERROR
|
||||
from katrain.core.lang import rank_label
|
||||
from katrain.gui.kivyutils import AnalysisToggle, CollapsablePanel
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
class PlayAnalyzeSelect(MDFloatLayout):
|
||||
@@ -70,8 +71,8 @@ class ControlsPanel(BoxLayout):
|
||||
self.status_state = (None, -1e9, None)
|
||||
self.active_comment_node = None
|
||||
self.last_timer_update = (None, 0, False)
|
||||
self.beep = SoundLoader.load("sounds/countdownbeep.wav")
|
||||
self.boing = SoundLoader.load("sounds/boing.wav")
|
||||
self.beep = SoundLoader.load(Theme.COUNTDOWN_SOUND)
|
||||
self.boing = SoundLoader.load(Theme.MINIMUM_TIME_PASSED_SOUND)
|
||||
if self.boing:
|
||||
self.boing.volume = 0.1
|
||||
self.beep_start = 5.2
|
||||
@@ -90,9 +91,14 @@ class ControlsPanel(BoxLayout):
|
||||
else rank_label(player_info.calculated_rank)
|
||||
)
|
||||
|
||||
def set_status(self, msg, status_type, at_node=None):
|
||||
def set_status(self, msg, status_type, at_node=None, check_level=True):
|
||||
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 == "":
|
||||
if (
|
||||
at_node != self.status_state[2]
|
||||
or not check_level
|
||||
or int(status_type) >= int(self.status_state[1])
|
||||
or msg == ""
|
||||
):
|
||||
if self.status_state != (msg, status_type, at_node): # prevent loop if error in update eval
|
||||
Clock.schedule_once(self.update_evaluation, 0)
|
||||
self.status_state = (msg, status_type, at_node)
|
||||
@@ -130,7 +136,7 @@ class ControlsPanel(BoxLayout):
|
||||
lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY
|
||||
details = self.info.detailed and not lock_ai
|
||||
info = ""
|
||||
if current_node.move or current_node.is_root:
|
||||
if move or current_node.is_root:
|
||||
info = self.active_comment_node.comment(
|
||||
teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details
|
||||
)
|
||||
@@ -147,7 +153,6 @@ 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
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from kivy.clock import Clock
|
||||
from kivy.core.image import Image
|
||||
from kivy.core.text import Label as CoreLabel
|
||||
from kivy.core.text.markup import MarkupLabel as CoreMarkupLabel
|
||||
from kivy.core.window import Window
|
||||
from kivy.graphics import *
|
||||
from kivy.graphics import Color, Ellipse, Rectangle
|
||||
from kivy.properties import (
|
||||
BooleanProperty,
|
||||
ListProperty,
|
||||
@@ -10,13 +12,13 @@ from kivy.properties import (
|
||||
OptionProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from kivy.uix.behaviors import ButtonBehavior, ToggleButtonBehavior, FocusBehavior
|
||||
from kivy.resources import resource_find
|
||||
from kivy.uix.behaviors import ButtonBehavior, ToggleButtonBehavior
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.spinner import Spinner
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.widget import Widget
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.behaviors import CircularRippleBehavior, RectangularRippleBehavior
|
||||
@@ -28,7 +30,7 @@ from kivymd.uix.textfield import MDTextField
|
||||
|
||||
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
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
class BackgroundMixin(Widget): # -- mixins
|
||||
@@ -67,14 +69,14 @@ class LeftButtonBehavior(ButtonBehavior): # stops buttons etc activating on rig
|
||||
# -- resizeable buttons / avoid baserectangular for sizing
|
||||
class SizedButton(LeftButtonBehavior, RectangularRippleBehavior, BasePressedButton, BaseFlatButton, BackgroundMixin):
|
||||
text = StringProperty("")
|
||||
text_color = ListProperty(WHITE)
|
||||
text_color = ListProperty(Theme.BUTTON_TEXT_COLOR)
|
||||
text_size = ListProperty([100, 100])
|
||||
halign = OptionProperty("center", options=["left", "center", "right", "justify", "auto"])
|
||||
label = ObjectProperty(None)
|
||||
padding_x = NumericProperty(6)
|
||||
padding_y = NumericProperty(0)
|
||||
_font_size = NumericProperty(None)
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
|
||||
|
||||
class AutoSizedButton(SizedButton):
|
||||
@@ -141,7 +143,7 @@ class StatsLabel(MDBoxLayout):
|
||||
label = StringProperty("")
|
||||
color = ListProperty([1, 1, 1, 1])
|
||||
hidden = BooleanProperty(False)
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
|
||||
|
||||
class MyNavigationDrawer(MDNavigationDrawer):
|
||||
@@ -212,13 +214,20 @@ class KeyValueSpinner(Spinner):
|
||||
sync_height_frac = NumericProperty(1.0)
|
||||
value_refs = ListProperty()
|
||||
selected_index = NumericProperty(0)
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.build_values()
|
||||
self.bind(size=self.update_dropdown_props, pos=self.update_dropdown_props, value_refs=self.build_values)
|
||||
|
||||
@property
|
||||
def input_value(self):
|
||||
try:
|
||||
return self.value_refs[self.selected_index]
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def selected(self):
|
||||
try:
|
||||
@@ -274,7 +283,7 @@ class I18NSpinner(KeyValueSpinner):
|
||||
sync_height_frac = NumericProperty(1.0)
|
||||
value_refs = ListProperty()
|
||||
selected_index = NumericProperty(0)
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -364,7 +373,7 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin):
|
||||
active = BooleanProperty(True)
|
||||
|
||||
|
||||
class TimerOrMoveTree(BoxLayout):
|
||||
class TimerOrMoveTree(MDBoxLayout):
|
||||
mode = StringProperty(MODE_PLAY)
|
||||
|
||||
|
||||
@@ -397,7 +406,7 @@ class TriStateMDCheckbox(MDCheckbox):
|
||||
class AnalysisToggle(MDBoxLayout):
|
||||
text = StringProperty("")
|
||||
default_active = BooleanProperty(False)
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
disabled = BooleanProperty(False)
|
||||
tri_state = BooleanProperty(False)
|
||||
|
||||
@@ -417,7 +426,7 @@ class MenuItem(RectangularRippleBehavior, LeftButtonBehavior, MDBoxLayout, Backg
|
||||
icon = StringProperty("")
|
||||
text = StringProperty("")
|
||||
shortcut = StringProperty("")
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
content_width = NumericProperty(100)
|
||||
|
||||
def on_left_release(self):
|
||||
@@ -457,8 +466,8 @@ class CollapsablePanel(MDBoxLayout):
|
||||
closed_label = StringProperty("Closed Panel")
|
||||
|
||||
state = OptionProperty("open", options=["open", "close"])
|
||||
close_icon = "img/Previous-5.png"
|
||||
open_icon = "img/Next-5.png"
|
||||
close_icon = "Previous-5.png"
|
||||
open_icon = "Next-5.png"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.open_close_button, self.header = None, None
|
||||
@@ -601,16 +610,33 @@ class ScrollableLabel(ScrollView, BackgroundMixin):
|
||||
pass
|
||||
|
||||
|
||||
def draw_text(pos, text, font_name=None, **kw):
|
||||
label = CoreLabel(text=text, bold=True, font_name=font_name or i18n.font_name, **kw) #
|
||||
def cached_text_texture(text, font_name, markup, _cache={}, **kwargs):
|
||||
args = (text, font_name, markup, *[(k, v) for k, v in kwargs.items()])
|
||||
texture = _cache.get(args)
|
||||
if texture:
|
||||
return texture
|
||||
label_cls = CoreMarkupLabel if markup else CoreLabel
|
||||
label = label_cls(text=text, bold=True, font_name=font_name or i18n.font_name, **kwargs)
|
||||
label.refresh()
|
||||
texture = _cache[args] = label.texture
|
||||
return texture
|
||||
|
||||
|
||||
def draw_text(pos, text, font_name=None, markup=False, **kwargs):
|
||||
texture = cached_text_texture(text, font_name, markup, **kwargs)
|
||||
Rectangle(
|
||||
texture=label.texture,
|
||||
pos=(pos[0] - label.texture.size[0] / 2, pos[1] - label.texture.size[1] / 2),
|
||||
size=label.texture.size,
|
||||
texture=texture, pos=(pos[0] - texture.size[0] / 2, pos[1] - texture.size[1] / 2), size=texture.size,
|
||||
)
|
||||
|
||||
|
||||
def draw_circle(pos, r, col):
|
||||
Color(*col)
|
||||
Ellipse(pos=(pos[0] - r, pos[1] - r), size=(2 * r, 2 * r))
|
||||
|
||||
|
||||
# direct cache to texture, bypassing resource_find
|
||||
def cached_texture(path, _cache={}):
|
||||
tex = _cache.get(path)
|
||||
if not tex:
|
||||
tex = _cache[path] = Image(resource_find(path)).texture
|
||||
return tex
|
||||
@@ -29,19 +29,19 @@ from katrain.core.constants import (
|
||||
OUTPUT_ERROR,
|
||||
OUTPUT_INFO,
|
||||
STATUS_INFO,
|
||||
DATA_FOLDER,
|
||||
)
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.lang import i18n, rank_label
|
||||
from katrain.core.utils import PATHS, find_package_resource
|
||||
from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner
|
||||
from katrain.gui.style import DEFAULT_FONT, EVAL_COLORS
|
||||
from katrain.gui.widgets import RankGraph
|
||||
from katrain.gui.theme import Theme
|
||||
from katrain.gui.widgets.progress_loader import ProgressLoader
|
||||
|
||||
|
||||
class I18NPopup(Popup):
|
||||
title_key = StringProperty("")
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -208,7 +208,7 @@ class QuickConfigGui(MDBoxLayout):
|
||||
selected = 0
|
||||
try:
|
||||
selected = widget.value_refs.index(value)
|
||||
except:
|
||||
except: # noqa: E722
|
||||
pass
|
||||
widget.text = widget.values[selected]
|
||||
else:
|
||||
@@ -249,6 +249,30 @@ class NewGamePopup(QuickConfigGui):
|
||||
self.player_setup.update_player_info(bw, info)
|
||||
|
||||
self.rules_spinner.value_refs = [name for abbr, name in katrain.engine.RULESETS_ABBR]
|
||||
Clock.schedule_once(self.update_from_current_game, 0.1)
|
||||
|
||||
def normalized_rules(self):
|
||||
rules = self.katrain.game.root.get_property("RU", "japanese").strip().lower()
|
||||
for abbr, name in self.katrain.engine.RULESETS_ABBR:
|
||||
if abbr == rules or name == rules:
|
||||
return name
|
||||
|
||||
def update_playerinfo(self, *args):
|
||||
for bw, player_setup in self.player_setup.players.items():
|
||||
name = self.player_name[bw].text
|
||||
if name:
|
||||
self.katrain.game.root.set_property("P" + bw, name)
|
||||
self.katrain.update_player(bw, **player_setup.player_type_dump)
|
||||
|
||||
def update_from_current_game(self, *args):
|
||||
for bw in "BW":
|
||||
name = self.katrain.game.root.get_property("P" + bw, None)
|
||||
if name:
|
||||
self.player_name[bw].text = name
|
||||
rules = self.normalized_rules()
|
||||
self.km.text = str(self.katrain.game.root.komi)
|
||||
if rules is not None:
|
||||
self.rules_spinner.select_key(rules.strip())
|
||||
|
||||
def update_config(self, save_to_file=True):
|
||||
super().update_config(save_to_file=save_to_file)
|
||||
@@ -256,19 +280,25 @@ class NewGamePopup(QuickConfigGui):
|
||||
if self.restart.active:
|
||||
self.katrain.log("Restarting Engine", OUTPUT_DEBUG)
|
||||
self.katrain.engine.restart()
|
||||
for bw, player_setup in self.player_setup.players.items():
|
||||
self.katrain.update_player(bw, **player_setup.player_type_dump)
|
||||
self.update_playerinfo()
|
||||
self.katrain("new-game")
|
||||
|
||||
def update_game(self):
|
||||
props = self.collect_properties(self)
|
||||
root = self.katrain.game.root
|
||||
changed = False
|
||||
for k, v in [("RU", props["game/rules"]), ("KM", props["game/komi"])]:
|
||||
current = root.get_property(k)
|
||||
if current != v:
|
||||
for k, currentval, newval in [
|
||||
("RU", self.normalized_rules(), props["game/rules"]),
|
||||
("KM", root.komi, props["game/komi"]),
|
||||
]:
|
||||
if currentval != newval:
|
||||
changed = True
|
||||
self.katrain.game.root.set_property(k, v)
|
||||
self.katrain.log(
|
||||
f"Property {k} changed from {currentval} to {newval}, triggering re-analysis of entire game.",
|
||||
OUTPUT_INFO,
|
||||
)
|
||||
self.katrain.game.root.set_property(k, newval)
|
||||
self.update_playerinfo()
|
||||
if changed:
|
||||
self.katrain.engine.on_new_game()
|
||||
self.katrain.game.analyze_all_nodes()
|
||||
@@ -297,14 +327,14 @@ class ConfigTeacherPopup(QuickConfigGui):
|
||||
savesgfs = self.katrain.config("trainer/save_feedback")
|
||||
show_dots = self.katrain.config("trainer/show_dots")
|
||||
|
||||
self.themes_spinner.value_refs = list(EVAL_COLORS.keys())
|
||||
self.themes_spinner.value_refs = list(Theme.EVAL_COLORS.keys())
|
||||
self.options_grid.clear_widgets()
|
||||
|
||||
for k in ["dot color", "point loss threshold", "num undos", "show dots", "save dots"]:
|
||||
self.options_grid.add_widget(DescriptionLabel(text=i18n._(k), font_name=i18n.font_name, font_size=dp(17)))
|
||||
|
||||
for i, (color, threshold, undo, show_dot, savesgf) in enumerate(
|
||||
zip(EVAL_COLORS[theme], thresholds, undos, show_dots, savesgfs)
|
||||
zip(Theme.EVAL_COLORS[theme], thresholds, undos, show_dots, savesgfs)
|
||||
):
|
||||
self.add_option_widgets(
|
||||
[
|
||||
@@ -391,8 +421,8 @@ class ConfigAIPopup(QuickConfigGui):
|
||||
class ConfigPopup(QuickConfigGui):
|
||||
def __init__(self, katrain):
|
||||
super().__init__(katrain)
|
||||
self.paths = [self.katrain.config("engine/model"), "katrain/models", "~/.katrain"]
|
||||
self.katago_paths = [self.katrain.config("engine/katago"), "~/.katrain"]
|
||||
self.paths = [self.katrain.config("engine/model"), "katrain/models", DATA_FOLDER]
|
||||
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
|
||||
Clock.schedule_once(self.check_katas)
|
||||
MDApp.get_running_app().bind(language=self.check_models)
|
||||
MDApp.get_running_app().bind(language=self.check_katas)
|
||||
@@ -531,7 +561,7 @@ class ConfigPopup(QuickConfigGui):
|
||||
for name, url in self.MODELS.items():
|
||||
filename = os.path.split(url)[1]
|
||||
if not any(os.path.split(f)[1] == filename for f in self.model_files.values):
|
||||
savepath = os.path.expanduser(os.path.join("~/.katrain", filename))
|
||||
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
|
||||
savepath_tmp = savepath + ".part"
|
||||
self.katrain.log(f"Downloading {name} model from {url} to {savepath_tmp}", OUTPUT_INFO)
|
||||
progress = ProgressLoader(
|
||||
@@ -598,8 +628,8 @@ class ConfigPopup(QuickConfigGui):
|
||||
filename = os.path.split(url)[1]
|
||||
exe_name = unzipped_name(filename)
|
||||
if not any(os.path.split(f)[1] == exe_name for f in self.katago_files.values):
|
||||
savepath_tmp = os.path.expanduser(os.path.join("~/.katrain", filename))
|
||||
exe_path_name = os.path.expanduser(os.path.join("~/.katrain", exe_name))
|
||||
savepath_tmp = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
|
||||
exe_path_name = os.path.expanduser(os.path.join(DATA_FOLDER, exe_name))
|
||||
self.katrain.log(f"Downloading binary {name} from {url} to {savepath_tmp}", OUTPUT_INFO)
|
||||
progress = ProgressLoader(
|
||||
download_url=url,
|
||||
@@ -659,13 +689,35 @@ class LoadSGFPopup(BoxLayout):
|
||||
super().__init__(**kwargs)
|
||||
app = MDApp.get_running_app()
|
||||
self.filesel.favorites = [
|
||||
(os.path.abspath(app.gui.config("general/sgf_load")), "Last Used Dir"),
|
||||
(os.path.abspath(app.gui.config("general/sgf_save")), "SGF Save Dir"),
|
||||
(os.path.abspath(app.gui.config("general/sgf_load")), "Last Load Dir"),
|
||||
(os.path.abspath(app.gui.config("general/sgf_save")), "Last Save Dir"),
|
||||
]
|
||||
self.filesel.path = os.path.abspath(os.path.expanduser(app.gui.config("general/sgf_load")))
|
||||
self.filesel.select_string = i18n._("Load File")
|
||||
|
||||
|
||||
class SaveSGFPopup(BoxLayout):
|
||||
def __init__(self, suggested_filename, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.suggested_filename = suggested_filename
|
||||
app = MDApp.get_running_app()
|
||||
self.filesel.favorites = [
|
||||
(os.path.abspath(app.gui.config("general/sgf_load")), "Last Load Dir"),
|
||||
(os.path.abspath(app.gui.config("general/sgf_save")), "Last Save Dir"),
|
||||
]
|
||||
save_path = os.path.expanduser(MDApp.get_running_app().gui.config("general/sgf_save") or ".")
|
||||
|
||||
def set_suggested(_widget, path):
|
||||
self.filesel.ids.file_text.text = os.path.join(path, self.suggested_filename)
|
||||
|
||||
self.filesel.ids.list_view.bind(path=set_suggested)
|
||||
self.filesel.path = os.path.abspath(save_path)
|
||||
self.filesel.select_string = i18n._("Save File")
|
||||
|
||||
def on_submit(self):
|
||||
self.filesel.dispatch("on_success")
|
||||
|
||||
|
||||
class ReAnalyzeGamePopup(BoxLayout):
|
||||
katrain = ObjectProperty(None)
|
||||
popup = ObjectProperty(None)
|
||||
@@ -1,82 +0,0 @@
|
||||
def to_hexcol(kivycol):
|
||||
return "#" + "".join(f"{round(c * 255):02x}" for c in kivycol[:3])
|
||||
|
||||
|
||||
DEFAULT_FONT = "fonts/NotoSansCJKsc-Regular.otf"
|
||||
|
||||
# basic color definitions
|
||||
WHITE = [0.95, 0.95, 0.95, 1]
|
||||
BLACK = [0.05, 0.05, 0.05, 1]
|
||||
LIGHTGREY = [0.7, 0.7, 0.7, 1]
|
||||
GREY = [0.5, 0.5, 0.5, 1]
|
||||
LIGHTERGREY = [0.85, 0.85, 0.85, 1]
|
||||
|
||||
RED = [0.8, 0.1, 0.1, 1]
|
||||
GREEN = [0.1, 0.8, 0.1, 1]
|
||||
YELLOW = [0.8, 0.8, 0.1, 1]
|
||||
DARKRED = [0.3, 0.1, 0.1, 1]
|
||||
ORANGE = [242 / 255, 96 / 255, 34 / 255, 1]
|
||||
|
||||
# gui colors
|
||||
BACKGROUND_COLOR = [36 / 255, 48 / 255, 62 / 255, 1]
|
||||
BOX_BACKGROUND_COLOR = [46 / 255, 65 / 255, 88 / 255, 1]
|
||||
LIGHTER_BACKGROUND_COLOR = [64 / 255, 85 / 255, 110 / 255, 1]
|
||||
TEXT_COLOR = WHITE
|
||||
|
||||
SCORE_COLOR = [0.3, 0.7, 0.9, 1]
|
||||
WINRATE_COLOR = GREEN
|
||||
POINTLOSS_COLOR = YELLOW
|
||||
|
||||
# eval dots
|
||||
EVAL_COLORS = {
|
||||
"theme:normal": [
|
||||
[0.447, 0.129, 0.42, 1],
|
||||
[0.8, 0, 0, 1],
|
||||
[0.9, 0.4, 0.1, 1],
|
||||
[0.95, 0.95, 0, 1],
|
||||
[0.67, 0.9, 0.18, 1],
|
||||
[0.117, 0.588, 0, 1],
|
||||
],
|
||||
"theme:red-green-colourblind": [
|
||||
[1, 0, 1, 1],
|
||||
[1, 0, 0, 1],
|
||||
[1, 0.5, 0, 1],
|
||||
[1, 1, 0, 1],
|
||||
[0, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
EVAL_DOT_MAX_SIZE = 0.5
|
||||
EVAL_DOT_MIN_SIZE = 0.25
|
||||
|
||||
STONE_COLORS = {"B": BLACK, "W": WHITE}
|
||||
STONE_CONTRAST_COLORS = {"B": LIGHTERGREY, "W": GREY}
|
||||
OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]}
|
||||
STONE_TEXT_COLORS = {"W": BLACK, "B": WHITE}
|
||||
|
||||
# board
|
||||
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.505 # texture edge is transparent
|
||||
|
||||
HINTS_MIN_ALPHA = 0.2 # single visit
|
||||
HINTS_ALPHA = 0.8
|
||||
POLICY_ALPHA = 0.6
|
||||
TOP_POLICY_ALPHA = 0.3
|
||||
TOP_MOVE_BORDER_COLOR = [10 / 255, 200 / 255, 250 / 255, 1.0]
|
||||
CHILD_SCALE = 0.95
|
||||
HINT_SCALE = 0.975
|
||||
UNCERTAIN_HINT_SCALE = 0.7
|
||||
|
||||
|
||||
# ponder light
|
||||
ENGINE_DOWN_COL = EVAL_COLORS["theme:normal"][1]
|
||||
ENGINE_BUSY_COL = EVAL_COLORS["theme:normal"][2]
|
||||
ENGINE_READY_COL = EVAL_COLORS["theme:normal"][-1]
|
||||
|
||||
# info
|
||||
INFO_PV_COLOR = to_hexcol(YELLOW)
|
||||
@@ -0,0 +1,145 @@
|
||||
def to_hexcol(kivycol):
|
||||
return "#" + "".join(f"{round(c * 255):02x}" for c in kivycol[:3])
|
||||
|
||||
|
||||
# basic colors
|
||||
WHITE = [0.95, 0.95, 0.95, 1]
|
||||
BLACK = [0.05, 0.05, 0.05, 1]
|
||||
LIGHT_GREY = [0.7, 0.7, 0.7, 1]
|
||||
GREY = [0.5, 0.5, 0.5, 1]
|
||||
LIGHTER_GREY = [0.85, 0.85, 0.85, 1]
|
||||
RED = [0.8, 0.1, 0.1, 1]
|
||||
GREEN = [0.1, 0.8, 0.1, 1]
|
||||
YELLOW = [0.8, 0.8, 0.1, 1]
|
||||
ORANGE = [242 / 255, 96 / 255, 34 / 255, 1]
|
||||
LIGHT_ORANGE = [1, 0.5, 0.25, 1]
|
||||
BLUE = [0.3, 0.7, 0.9, 1]
|
||||
|
||||
|
||||
class Theme:
|
||||
# font
|
||||
DEFAULT_FONT = "NotoSansCJKsc-Regular.otf"
|
||||
INPUT_FONT_SIZE = 25 # sp
|
||||
DESC_FONT_SIZE = 20 # sp
|
||||
NOTES_FONT_SIZE = 18 # sp
|
||||
|
||||
# gui colors
|
||||
BACKGROUND_COLOR = [36 / 255, 48 / 255, 62 / 255, 1]
|
||||
BOX_BACKGROUND_COLOR = [46 / 255, 65 / 255, 88 / 255, 1]
|
||||
LIGHTER_BACKGROUND_COLOR = [64 / 255, 85 / 255, 110 / 255, 1]
|
||||
SCROLLBAR_COLOR = LIGHT_GREY
|
||||
TEXT_COLOR = WHITE
|
||||
SCORE_COLOR = BLUE # blue
|
||||
WINRATE_COLOR = GREEN # green
|
||||
POINTLOSS_COLOR = YELLOW # yellow
|
||||
BUTTON_INACTIVE_COLOR = LIGHT_GREY
|
||||
BUTTON_BORDER_COLOR = WHITE
|
||||
BUTTON_TEXT_COLOR = WHITE
|
||||
PAUSE_ACTIVE_COLOR = ORANGE
|
||||
TIMER_TEXT_COLOR = GREEN
|
||||
TIMER_TEXT_TIMEOUT_COLOR = ORANGE
|
||||
CIRCLE_TEXT_COLORS = {"W": BLACK, "B": WHITE}
|
||||
NOTES_TAB_FONT_COLOR = YELLOW
|
||||
INFO_TAB_FONT_COLOR = WHITE
|
||||
ERROR_BORDER_COLOR = RED
|
||||
MENU_ITEM_FONT_COLOR = WHITE
|
||||
MENU_ITEM_SHORTCUT_COLOR = LIGHT_GREY
|
||||
PLAY_ANALYZE_TAB_COLOR = YELLOW
|
||||
INPUT_FONT_COLOR = WHITE
|
||||
MISTAKE_BUTTON_COLOR = [0.79, 0.06, 0.06, 1]
|
||||
|
||||
# gui spacing
|
||||
CP_SPACING = 6
|
||||
CP_SMALL_SPACING = 3
|
||||
CP_PADDING = 6
|
||||
|
||||
# textures
|
||||
STONE_TEXTURE = {"B": "B_stone.png", "W": "W_stone.png"}
|
||||
EVAL_DOT_TEXTURE = "dot.png"
|
||||
LAST_MOVE_TEXTURE = "inner.png"
|
||||
TOP_MOVE_TEXTURE = "topmove.png"
|
||||
BOARD_TEXTURE = "board.png"
|
||||
GRAPH_TEXTURE = "graph_bg.png"
|
||||
# sounds
|
||||
STONE_SOUNDS = [f"stone{i}.wav" for i in [1, 2, 3, 4, 5]]
|
||||
COUNTDOWN_SOUND = "countdownbeep.wav"
|
||||
MINIMUM_TIME_PASSED_SOUND = "boing.wav"
|
||||
|
||||
# eval dots
|
||||
EVAL_COLORS = {
|
||||
"theme:normal": [
|
||||
[0.447, 0.129, 0.42, 1],
|
||||
[0.8, 0, 0, 1],
|
||||
[0.9, 0.4, 0.1, 1],
|
||||
[0.95, 0.95, 0, 1],
|
||||
[0.67, 0.9, 0.18, 1],
|
||||
[0.117, 0.588, 0, 1],
|
||||
],
|
||||
"theme:red-green-colourblind": [
|
||||
[1, 0, 1, 1],
|
||||
[1, 0, 0, 1],
|
||||
[1, 0.5, 0, 1],
|
||||
[1, 1, 0, 1],
|
||||
[0, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
],
|
||||
}
|
||||
|
||||
EVAL_DOT_MAX_SIZE = 0.5
|
||||
EVAL_DOT_MIN_SIZE = 0.25
|
||||
|
||||
# board theme
|
||||
APPROX_BOARD_COLOR = [0.95, 0.75, 0.47, 1] # for drawing on top of / hiding what's under it
|
||||
BOARD_COLOR_TINT = [1, 0.95, 0.8, 1] # multiplied by texture
|
||||
|
||||
HINT_TEXT_COLOR = BLACK
|
||||
|
||||
REGION_BORDER_COLOR = LIGHTER_BACKGROUND_COLOR
|
||||
INSERT_BOARD_COLOR_TINT = [1, 1, 1, 0.75]
|
||||
PASS_CIRCLE_COLOR = [0.45, 0.05, 0.45, 0.7]
|
||||
PASS_CIRCLE_TEXT_COLOR = [0.85, 0.85, 0.85]
|
||||
|
||||
STONE_COLORS = {"B": BLACK, "W": WHITE}
|
||||
NEXT_MOVE_DASH_CONTRAST_COLORS = {"B": LIGHTER_GREY, "W": GREY}
|
||||
OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]}
|
||||
PV_TEXT_COLORS = {"W": BLACK, "B": WHITE} # numbers in PV
|
||||
|
||||
# board
|
||||
LINE_COLOR = [0, 0, 0]
|
||||
STARPOINT_SIZE = 0.1
|
||||
BOARD_COLOR = [0.85, 0.68, 0.40, 1]
|
||||
STONE_SIZE = 0.505 # texture edge is transparent
|
||||
|
||||
GHOST_ALPHA = 0.6
|
||||
POLICY_ALPHA = 0.5
|
||||
|
||||
HINTS_LO_ALPHA = 0.6
|
||||
HINTS_ALPHA = 0.8
|
||||
TOP_MOVE_BORDER_COLOR = [10 / 255, 200 / 255, 250 / 255, 1.0]
|
||||
CHILD_SCALE = 0.95
|
||||
HINT_SCALE = 0.98
|
||||
UNCERTAIN_HINT_SCALE = 0.7
|
||||
|
||||
# ponder light
|
||||
ENGINE_DOWN_COLOR = EVAL_COLORS["theme:normal"][1]
|
||||
ENGINE_BUSY_COLOR = EVAL_COLORS["theme:normal"][2]
|
||||
ENGINE_READY_COLOR = EVAL_COLORS["theme:normal"][-1]
|
||||
ENGINE_PONDERING_COLOR = YELLOW
|
||||
|
||||
# info PV link
|
||||
INFO_PV_COLOR = to_hexcol(YELLOW)
|
||||
|
||||
# graph
|
||||
GRAPH_DOT_COLOR = [0.85, 0.3, 0.3, 1]
|
||||
WINRATE_MARKER_COLOR = [0.05, 0.7, 0.05, 1]
|
||||
SCORE_MARKER_COLOR = [0.2, 0.6, 0.8, 1]
|
||||
|
||||
# move tree
|
||||
MOVE_TREE_LINE = LIGHT_GREY
|
||||
MOVE_TREE_CURRENT = YELLOW
|
||||
MOVE_TREE_SELECTED = RED
|
||||
MOVE_TREE_INSERT_NODE_PARENT = GREEN
|
||||
MOVE_TREE_INSERT_CURRENT = ORANGE
|
||||
MOVE_TREE_INSERT_OTHER = LIGHT_ORANGE
|
||||
MOVE_TREE_COLLAPSED = LIGHT_GREY
|
||||
MOVE_TREE_STONE_OUTLINE_COLORS = {"W": BLACK, "B": WHITE}
|
||||
@@ -1,6 +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.graph import ScoreGraph
|
||||
from katrain.gui.widgets.movetree import MoveTree
|
||||
from katrain.gui.widgets.selection_slider import SelectionSlider
|
||||
|
||||
__all__ = ["ScoreGraph", "RankGraph", "MoveTree", "I18NFileBrowser", "SelectionSlider"]
|
||||
__all__ = ["ScoreGraph", "MoveTree", "I18NFileBrowser", "SelectionSlider"]
|
||||
@@ -38,7 +38,6 @@ a shortcut to the Documents directory added to the favorites bar::
|
||||
:align: right
|
||||
"""
|
||||
import string
|
||||
import warnings
|
||||
from functools import partial
|
||||
from os import walk
|
||||
from os.path import dirname, expanduser, getmtime, isdir, join, sep
|
||||
@@ -52,7 +51,7 @@ from kivy.uix.filechooser import FileChooserListLayout, FileChooserListView
|
||||
from kivy.uix.treeview import TreeView, TreeViewLabel
|
||||
from kivy.utils import platform
|
||||
|
||||
from katrain.gui.style import DEFAULT_FONT
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
if platform == "win":
|
||||
from ctypes import windll, create_unicode_buffer
|
||||
@@ -81,27 +80,25 @@ def get_drives():
|
||||
drives = []
|
||||
if platform == "win":
|
||||
bitmask = windll.kernel32.GetLogicalDrives()
|
||||
GetVolumeInformationW = windll.kernel32.GetVolumeInformationW
|
||||
for letter in string.ascii_uppercase:
|
||||
if bitmask & 1:
|
||||
name = create_unicode_buffer(64)
|
||||
# get name of the drive
|
||||
drive = letter + u":"
|
||||
res = GetVolumeInformationW(drive + sep, name, 64, None, None, None, None, 0)
|
||||
drive = letter + ":"
|
||||
if isdir(drive):
|
||||
drives.append((drive, name.value))
|
||||
bitmask >>= 1
|
||||
elif platform == "linux":
|
||||
drives.append((sep, sep))
|
||||
drives.append((expanduser(u"~"), "~/"))
|
||||
places = (sep + u"mnt", sep + u"media")
|
||||
drives.append((expanduser("~"), "~/"))
|
||||
places = (sep + "mnt", sep + "media")
|
||||
for place in places:
|
||||
if isdir(place):
|
||||
for directory in next(walk(place))[1]:
|
||||
drives.append((place + sep + directory, directory))
|
||||
elif platform == "macosx" or platform == "ios":
|
||||
drives.append((expanduser(u"~"), "~/"))
|
||||
vol = sep + u"Volume"
|
||||
drives.append((expanduser("~"), "~/"))
|
||||
vol = sep + "Volume"
|
||||
if isdir(vol):
|
||||
for drive in next(walk(vol))[1]:
|
||||
drives.append((vol + sep + drive, drive))
|
||||
@@ -109,7 +106,7 @@ def get_drives():
|
||||
|
||||
|
||||
class I18NFileChooserListView(FileChooserListView):
|
||||
font_name = StringProperty(DEFAULT_FONT)
|
||||
font_name = StringProperty(Theme.DEFAULT_FONT)
|
||||
show_hidden = BooleanProperty(True) # avoid errors
|
||||
|
||||
|
||||
@@ -154,7 +151,7 @@ Builder.load_string(
|
||||
layout: layout
|
||||
I18NFileChooserListLayout:
|
||||
id: layout
|
||||
controller: root
|
||||
controller: root
|
||||
"""
|
||||
)
|
||||
Config.set("kivy", "log_level", log_level)
|
||||
@@ -178,6 +175,7 @@ Builder.load_string(
|
||||
filename: file_text.text
|
||||
browser: list_view
|
||||
on_favorites: link_tree.reload_favs(self.favorites)
|
||||
on_selection: file_text.text = root.selection[0] if root.selection else ""
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
spacing: 5
|
||||
@@ -225,7 +223,6 @@ Builder.load_string(
|
||||
spacing: [5]
|
||||
TextInput:
|
||||
id: file_text
|
||||
text: (root.selection and (root._shorten_filenames(root.selection) if root.multiselect else root.selection[0])) or ''
|
||||
hint_text: i18n._('Filename')
|
||||
multiline: False
|
||||
height: '40dp'
|
||||
|
||||
@@ -7,13 +7,12 @@ from kivy.properties import BooleanProperty, Clock, ListProperty, NumericPropert
|
||||
from kivy.uix.widget import Widget
|
||||
from kivymd.app import MDApp
|
||||
|
||||
from katrain.core.constants import OUTPUT_ERROR
|
||||
from katrain.core.lang import rank_label
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
class Graph(Widget):
|
||||
marker_font_size = NumericProperty(0)
|
||||
background_image = StringProperty("img/graph_bg.png")
|
||||
background_image = StringProperty(Theme.GRAPH_TEXTURE)
|
||||
background_color = ListProperty([1, 1, 1, 1])
|
||||
highlighted_index = NumericProperty(0)
|
||||
nodes = ListProperty([])
|
||||
@@ -77,7 +76,7 @@ class ScoreGraph(Graph):
|
||||
navigate_move = ListProperty([None, 0, 0, 0])
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.collide_point(*touch.pos) and "scroll" not in getattr(touch, "button", ""):
|
||||
ix, _ = min(enumerate(self.score_points[::2]), key=lambda ix_v: abs(ix_v[1] - touch.x))
|
||||
self.navigate_move = [
|
||||
self.nodes[ix],
|
||||
@@ -92,7 +91,7 @@ class ScoreGraph(Graph):
|
||||
return self.on_touch_down(touch)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self.collide_point(*touch.pos) and self.navigate_move[0]:
|
||||
if self.collide_point(*touch.pos) and self.navigate_move[0] and "scroll" not in getattr(touch, "button", ""):
|
||||
katrain = MDApp.get_running_app().gui
|
||||
if katrain and katrain.game:
|
||||
katrain.game.set_current_node(self.navigate_move[0])
|
||||
@@ -128,11 +127,11 @@ class ScoreGraph(Graph):
|
||||
xscale = self.width / max(len(score_values) - 1, 15)
|
||||
available_height = self.height
|
||||
score_line_points = [
|
||||
[self.x + i * xscale, self.y + self.height / 2 + available_height / 2 * (val / self.score_scale),]
|
||||
[self.x + i * xscale, self.y + self.height / 2 + available_height / 2 * (val / self.score_scale)]
|
||||
for i, val in enumerate(score_values)
|
||||
]
|
||||
winrate_line_points = [
|
||||
[self.x + i * xscale, self.y + self.height / 2 + available_height / 2 * (val / self.winrate_scale),]
|
||||
[self.x + i * xscale, self.y + self.height / 2 + available_height / 2 * (val / self.winrate_scale)]
|
||||
for i, val in enumerate(winrate_values)
|
||||
]
|
||||
self.score_points = sum(score_line_points, [])
|
||||
@@ -158,161 +157,12 @@ class ScoreGraph(Graph):
|
||||
self.winrate_dot_pos = winrate_dot_point
|
||||
|
||||
|
||||
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 == node.move
|
||||
][0]
|
||||
if policy_ranking and node.move
|
||||
else (None, None, None, None)
|
||||
for node, 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 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
|
||||
#:import Theme katrain.gui.theme.Theme
|
||||
|
||||
<Graph>:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
marker_font_size: 0.1 * self.height
|
||||
canvas.before:
|
||||
Color:
|
||||
@@ -330,12 +180,12 @@ Builder.load_string(
|
||||
<ScoreGraph>:
|
||||
canvas:
|
||||
Color:
|
||||
rgba: SCORE_COLOR
|
||||
rgba: Theme.SCORE_COLOR
|
||||
Line:
|
||||
points: root.score_points if root.show_score else []
|
||||
width: dp(1.1)
|
||||
Color:
|
||||
rgba: WINRATE_COLOR
|
||||
rgba: Theme.WINRATE_COLOR
|
||||
Line:
|
||||
points: root.winrate_points if root.show_winrate else []
|
||||
width: dp(1.1)
|
||||
@@ -345,13 +195,13 @@ Builder.load_string(
|
||||
points: root.navigate_move[1], root.y, root.navigate_move[1], root.y+root.height
|
||||
width: 1
|
||||
Color:
|
||||
rgba: GRAPH_DOT_COLOR
|
||||
rgba: Theme.GRAPH_DOT_COLOR
|
||||
Ellipse:
|
||||
id: score_dot
|
||||
pos: [c - self.highlight_size / 2 for c in (self.score_dot_pos if not self.navigate_move[0] else [self.navigate_move[1],self.navigate_move[2]] ) ]
|
||||
size: (self.highlight_size,self.highlight_size) if root.show_score else (0.0001,0.0001)
|
||||
Color:
|
||||
rgba: GRAPH_DOT_COLOR
|
||||
rgba: Theme.GRAPH_DOT_COLOR
|
||||
Ellipse:
|
||||
id: winrate_dot
|
||||
pos: [c - self.highlight_size / 2 for c in (self.winrate_dot_pos if not self.navigate_move[0] else [self.navigate_move[1],self.navigate_move[3]] ) ]
|
||||
@@ -359,67 +209,34 @@ Builder.load_string(
|
||||
# score ticks
|
||||
GraphMarkerLabel:
|
||||
font_size: root.marker_font_size
|
||||
color: SCORE_MARKER_COLOR
|
||||
color: Theme.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_MARKER_COLOR
|
||||
color: Theme.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_MARKER_COLOR
|
||||
color: Theme.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_MARKER_COLOR
|
||||
color: Theme.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_MARKER_COLOR
|
||||
color: Theme.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')
|
||||
opacity: int(root.show_winrate)
|
||||
"""
|
||||
)
|
||||
@@ -3,6 +3,7 @@ from collections import defaultdict
|
||||
from kivy.graphics.context_instructions import Color
|
||||
from kivy.graphics.vertex_instructions import Line, Rectangle
|
||||
from kivy.lang import Builder
|
||||
from kivy.metrics import dp
|
||||
from kivy.properties import BooleanProperty, Clock, NumericProperty, ObjectProperty
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
@@ -10,17 +11,7 @@ from kivy.uix.widget import Widget
|
||||
from kivymd.app import MDApp
|
||||
|
||||
from katrain.gui.kivyutils import BackgroundMixin, draw_circle, draw_text
|
||||
from katrain.gui.style import (
|
||||
BACKGROUND_COLOR,
|
||||
LIGHTER_BACKGROUND_COLOR,
|
||||
LIGHTGREY,
|
||||
OUTLINE_COLORS,
|
||||
RED,
|
||||
STONE_COLORS,
|
||||
STONE_TEXT_COLORS,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
)
|
||||
from katrain.gui.theme import Theme
|
||||
|
||||
|
||||
class MoveTreeDropdown(DropDown):
|
||||
@@ -79,23 +70,43 @@ class MoveTreeCanvas(Widget):
|
||||
self.menu_selected_node = selected_node
|
||||
|
||||
def delete_selected_node(self):
|
||||
if self.menu_selected_node and self.menu_selected_node.parent:
|
||||
parent = self.menu_selected_node.parent
|
||||
parent.children.remove(self.menu_selected_node)
|
||||
selected_node = self.menu_selected_node or self.scroll_view_widget.current_node
|
||||
if selected_node and selected_node.parent:
|
||||
if selected_node.shortcut_from:
|
||||
parent = selected_node.shortcut_from
|
||||
via = [v for m, v in parent.shortcuts_to if m == selected_node]
|
||||
selected_node.remove_shortcut()
|
||||
if via: # should always be
|
||||
parent.children.remove(via[0])
|
||||
else:
|
||||
parent = selected_node.parent
|
||||
parent.children.remove(selected_node)
|
||||
self.set_game_node(parent)
|
||||
self.is_open = False
|
||||
|
||||
def make_selected_node_main_branch(self):
|
||||
|
||||
if self.menu_selected_node and self.menu_selected_node.parent:
|
||||
node = self.menu_selected_node
|
||||
selected_node = self.menu_selected_node or self.scroll_view_widget.current_node
|
||||
if selected_node and selected_node.parent:
|
||||
node = selected_node
|
||||
while node.parent is not None:
|
||||
node.parent.children.remove(node)
|
||||
node.parent.children.insert(0, node)
|
||||
node = node.parent
|
||||
self.set_game_node(self.menu_selected_node)
|
||||
self.set_game_node(selected_node)
|
||||
self.is_open = False
|
||||
|
||||
def toggle_selected_node_collapse(self):
|
||||
selected_node = self.menu_selected_node or self.scroll_view_widget.current_node
|
||||
if selected_node and selected_node.parent:
|
||||
if selected_node.shortcut_from:
|
||||
selected_node.remove_shortcut()
|
||||
else: # find branching point
|
||||
node = selected_node.parent
|
||||
while len(node.children) == 1 and not node.is_root and not node.shortcut_from:
|
||||
node = node.parent
|
||||
node.add_shortcut(selected_node)
|
||||
self.scroll_view_widget.redraw_tree_trigger()
|
||||
|
||||
def switch_branch(self, direction=1):
|
||||
pos = self.move_pos.get(self.scroll_view_widget.current_node)
|
||||
if not self.scroll_view_widget:
|
||||
@@ -106,35 +117,46 @@ class MoveTreeCanvas(Widget):
|
||||
return
|
||||
self.set_game_node(same_x_moves[new_index][1])
|
||||
|
||||
def draw_move_tree(self, current_node):
|
||||
def draw_move_tree(self, current_node, insert_node):
|
||||
if not self.scroll_view_widget or not current_node:
|
||||
return
|
||||
|
||||
spacing = 5
|
||||
moves_vert = 3
|
||||
self.move_size = (self.scroll_view_widget.height - (moves_vert + 1) * spacing) / moves_vert
|
||||
self.move_size = (self.scroll_view_widget.min_height - (moves_vert + 1) * spacing) / moves_vert
|
||||
|
||||
root = current_node.root
|
||||
|
||||
def children_with_shortcuts(move):
|
||||
shortcuts = move.shortcuts_to
|
||||
via = {v: m for m, v in shortcuts} # children that are shortcut
|
||||
return [m if m not in via else via[m] for m in move.ordered_children]
|
||||
|
||||
self.move_pos = {root: (0, 0)}
|
||||
stack = root.ordered_children[::-1]
|
||||
stack = children_with_shortcuts(root)[::-1]
|
||||
next_y_pos = defaultdict(int) # x pos -> max y pos
|
||||
children = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw!
|
||||
children[root] = root.ordered_children
|
||||
children[root] = [*stack]
|
||||
while stack:
|
||||
move = stack.pop()
|
||||
x = move.depth
|
||||
y = max(next_y_pos[x], self.move_pos[move.parent][1])
|
||||
if move.shortcut_from:
|
||||
parent = move.shortcut_from
|
||||
else:
|
||||
parent = move.parent
|
||||
|
||||
if parent:
|
||||
x = self.move_pos[parent][0] + 1
|
||||
else:
|
||||
x = 0
|
||||
y = max(next_y_pos[x], self.move_pos[parent][1])
|
||||
next_y_pos[x] = y + 1
|
||||
next_y_pos[x - 1] = max(next_y_pos[x], next_y_pos[x - 1])
|
||||
self.move_pos[move] = (x, y)
|
||||
children[move] = move.ordered_children
|
||||
for c in children[move][::-1]: # stack, so push top child last to process first
|
||||
stack.append(c)
|
||||
children[move] = children_with_shortcuts(move)
|
||||
stack += children[move][::-1] # stack, so push top child last to process first
|
||||
|
||||
def draw_stone(pos, player):
|
||||
draw_circle(pos, self.move_size / 2 - 0.5, STONE_COLORS[player])
|
||||
Color(*STONE_TEXT_COLORS[player])
|
||||
def draw_stone(pos, player, special_color=None):
|
||||
draw_circle(pos, self.move_size / 2 - 0.5, (special_color or Theme.STONE_COLORS[player]))
|
||||
Color(*Theme.MOVE_TREE_STONE_OUTLINE_COLORS[player])
|
||||
Line(circle=(*pos, self.move_size / 2), width=1)
|
||||
|
||||
def coord_pos(coord):
|
||||
@@ -148,24 +170,36 @@ class MoveTreeCanvas(Widget):
|
||||
|
||||
self.move_xy_pos = {n: xy_pos(x, y) for n, (x, y) in self.move_pos.items()}
|
||||
|
||||
special_nodes = {current_node: Theme.MOVE_TREE_CURRENT, self.menu_selected_node: Theme.MOVE_TREE_SELECTED}
|
||||
|
||||
if insert_node:
|
||||
special_nodes[insert_node.parent] = Theme.MOVE_TREE_INSERT_NODE_PARENT
|
||||
insert_path = current_node
|
||||
while insert_path != insert_node.parent and insert_path.parent:
|
||||
special_nodes[insert_path] = (
|
||||
Theme.MOVE_TREE_INSERT_CURRENT if insert_path == current_node else Theme.MOVE_TREE_INSERT_OTHER
|
||||
)
|
||||
insert_path = insert_path.parent
|
||||
|
||||
with self.canvas:
|
||||
self.canvas.clear()
|
||||
Color(*LIGHTGREY)
|
||||
Color(*Theme.MOVE_TREE_LINE)
|
||||
for node, (x, y) in self.move_xy_pos.items():
|
||||
for ci, c in enumerate(children[node]):
|
||||
cx, cy = self.move_xy_pos[c]
|
||||
Line(points=[x, y, x, cy, cx, cy], width=1)
|
||||
|
||||
for node, pos in self.move_xy_pos.items():
|
||||
if node == self.menu_selected_node or node == current_node:
|
||||
Color(*(RED if node == self.menu_selected_node else YELLOW))
|
||||
if node in special_nodes:
|
||||
Color(*special_nodes[node])
|
||||
Rectangle(
|
||||
pos=[c - self.move_size / 2 - spacing / 2 for c in self.move_xy_pos[node]],
|
||||
size=(self.move_size + spacing, self.move_size + spacing),
|
||||
)
|
||||
draw_stone(pos, node.player)
|
||||
collapsed_color = Theme.MOVE_TREE_COLLAPSED if node.shortcut_from else None
|
||||
draw_stone(pos, node.player, collapsed_color)
|
||||
text = str(node.depth)
|
||||
Color(*STONE_COLORS["W" if node.player == "B" else "B"])
|
||||
Color(*Theme.STONE_COLORS["W" if node.player == "B" and not node.shortcut_from else "B"])
|
||||
draw_text(pos=pos, text=text, font_size=self.move_size * 1.75 / (1 + 1 * len(text)), font_name="Roboto")
|
||||
|
||||
if current_node in self.move_xy_pos:
|
||||
@@ -174,22 +208,33 @@ class MoveTreeCanvas(Widget):
|
||||
|
||||
class MoveTree(ScrollView, BackgroundMixin):
|
||||
current_node = ObjectProperty(None)
|
||||
min_height = NumericProperty(dp(50)) # non-expanded height, to determine the node size
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.insert_node = None
|
||||
self.redraw_tree_trigger = Clock.create_trigger(
|
||||
lambda _dt: self.move_tree_canvas.draw_move_tree(self.current_node)
|
||||
lambda _dt: self.move_tree_canvas.draw_move_tree(self.current_node, self.insert_node), 0.1
|
||||
)
|
||||
self.bind(current_node=self.redraw_tree_trigger, size=self.redraw_tree_trigger)
|
||||
|
||||
def redraw(self):
|
||||
self.redraw_tree_trigger()
|
||||
|
||||
def switch_branch(self, direction):
|
||||
self.move_tree_canvas.switch_branch(direction)
|
||||
|
||||
def delete_selected_node(self):
|
||||
self.move_tree_canvas.delete_selected_node()
|
||||
self.redraw_tree_trigger()
|
||||
|
||||
def make_selected_node_main_branch(self):
|
||||
self.move_tree_canvas.make_selected_node_main_branch()
|
||||
self.redraw_tree_trigger()
|
||||
|
||||
def toggle_selected_node_collapse(self):
|
||||
self.move_tree_canvas.toggle_selected_node_collapse()
|
||||
self.redraw_tree_trigger()
|
||||
|
||||
def scroll_to_pixel(self, x, y):
|
||||
if not self._viewport:
|
||||
@@ -202,18 +247,26 @@ class MoveTree(ScrollView, BackgroundMixin):
|
||||
sy = (y - self.height / 2) / (vp.height - self.height)
|
||||
self.scroll_y = max(0, min(1, sy))
|
||||
|
||||
# disable mousewheel
|
||||
def on_scroll_start(self, touch, check_children=True):
|
||||
if "button" in touch.profile and touch.button.startswith("scroll"):
|
||||
return False
|
||||
return super().on_scroll_start(touch, check_children)
|
||||
|
||||
|
||||
Builder.load_string(
|
||||
"""
|
||||
#:import Theme katrain.gui.theme.Theme
|
||||
#:import WHITE katrain.gui.theme.WHITE
|
||||
#:import LIGHT_GREY katrain.gui.theme.LIGHT_GREY
|
||||
<MoveTree>:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
background_color: Theme.BOX_BACKGROUND_COLOR
|
||||
move_tree_canvas: move_tree_canvas
|
||||
scroll_distance: 0 # scroll wheel is for forward/backward
|
||||
MoveTreeCanvas:
|
||||
scroll_view_widget: root
|
||||
id: move_tree_canvas
|
||||
size_hint: None, None
|
||||
|
||||
|
||||
<MoveTreeDropdownItem@MenuItem>:
|
||||
canvas.before:
|
||||
Color:
|
||||
@@ -221,7 +274,7 @@ Builder.load_string(
|
||||
Line:
|
||||
points: self.x, self.y, self.x, self.y+self.height
|
||||
Color:
|
||||
rgba: LIGHTGREY
|
||||
rgba: LIGHT_GREY
|
||||
Line
|
||||
points: self.x,self.y,self.x+self.width,self.y
|
||||
width: 1
|
||||
@@ -230,15 +283,27 @@ Builder.load_string(
|
||||
katrain: app.gui
|
||||
MoveTreeDropdownItem:
|
||||
text: i18n._("Delete Node")
|
||||
icon: 'img/delete.png'
|
||||
shortcut: ''
|
||||
icon: 'delete.png'
|
||||
shortcut: 'Ctrl+Del'
|
||||
on_action: root.katrain.controls.move_tree.delete_selected_node()
|
||||
-background_color: LIGHTER_BACKGROUND_COLOR
|
||||
-background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
-height: dp(45)
|
||||
-width_margin: 1.6
|
||||
MoveTreeDropdownItem:
|
||||
text: i18n._("Make Main Branch")
|
||||
icon: 'img/Branch.png'
|
||||
shortcut: ''
|
||||
icon: 'Branch.png'
|
||||
shortcut: 'PgUp'
|
||||
on_action: root.katrain.controls.move_tree.make_selected_node_main_branch()
|
||||
-background_color: LIGHTER_BACKGROUND_COLOR
|
||||
"""
|
||||
-background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
-height: dp(45)
|
||||
-width_margin: 1.6
|
||||
MoveTreeDropdownItem:
|
||||
text: i18n._("Toggle Collapse Branch")
|
||||
icon: 'Collapse.png'
|
||||
shortcut: 'c'
|
||||
on_action: root.katrain.controls.move_tree.toggle_selected_node_collapse()
|
||||
-background_color: Theme.LIGHTER_BACKGROUND_COLOR
|
||||
-height: dp(45)
|
||||
-width_margin: 1.6
|
||||
"""
|
||||
)
|
||||
@@ -1,8 +1,6 @@
|
||||
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):
|
||||
@@ -122,7 +120,7 @@ KV = """
|
||||
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))
|
||||
(slider.px_pos - dp(9), slider.center_y - hint_box.height / 2 + dp(30))
|
||||
|
||||
Label:
|
||||
id: label
|
||||
|
||||
@@ -415,7 +415,7 @@ msgstr "保存AI设置"
|
||||
msgid "sgf written"
|
||||
msgstr "带分析的SGF文件保存到 {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "优化前等待初始分析完成"
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -613,9 +613,6 @@ msgstr "覆盖引擎命令"
|
||||
msgid "download katago button"
|
||||
msgstr "下载KataGo 版本"
|
||||
|
||||
msgid "show point loss as text"
|
||||
msgstr "在棋子上显示目数损失"
|
||||
|
||||
msgid "katago binaries available"
|
||||
msgstr "{num} 个exe文件可用"
|
||||
|
||||
@@ -660,9 +657,6 @@ msgstr ""
|
||||
msgid "ai:simple"
|
||||
msgstr "简明风格"
|
||||
|
||||
msgid "analysis:local"
|
||||
msgstr "寻找局部走法"
|
||||
|
||||
msgid "local analysis"
|
||||
msgstr "正在寻找局部走法"
|
||||
|
||||
@@ -674,19 +668,99 @@ msgstr ""
|
||||
msgid "Make Main Branch"
|
||||
msgstr "设为实战变化"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Plays moves that lead to clarity on stones' status, favouring solid moves "
|
||||
"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
|
||||
"points and with at least 'min_visits', prefer moves that lose fewer points, "
|
||||
"settle own stones more (with importance settled_weight), settle opponent's "
|
||||
"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
|
||||
"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
|
||||
" attachment_penalty). Weights can be negative to make it complicate the game"
|
||||
" instead, and wide root noise (in general settings) can be used to further "
|
||||
"play according to the style at the cost of strength. "
|
||||
#. save as
|
||||
msgid "menu:save-as"
|
||||
msgstr "保存为..."
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Style"
|
||||
#. sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "载入"
|
||||
|
||||
#. sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "保存SGF"
|
||||
|
||||
#. sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "Save File"
|
||||
|
||||
#. status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "嵌入模式,按 i 结束嵌入"
|
||||
|
||||
#. status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "嵌入完毕, 复制{num_copied}手棋"
|
||||
|
||||
#. when a user tries to go to a different move/node while in insert
|
||||
#. mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr "此功能在嵌入模式无法使用, 按 i 结束嵌入 "
|
||||
|
||||
#. in new game options
|
||||
msgid "player names"
|
||||
msgstr "棋手信息"
|
||||
|
||||
#. Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "嵌入"
|
||||
|
||||
#. Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr "拖放选择局部。点击一次取消局部"
|
||||
|
||||
#. Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "选择局部"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "放大/缩小"
|
||||
|
||||
#. Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "自动下完"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "黑方"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "白方"
|
||||
|
||||
#. setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "最佳选点统计"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "胜率波动"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "计算深度"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "分数"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(无)"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "胜率"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "分数波动"
|
||||
|
||||
#. top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "计算深度...以上时显示"
|
||||
|
||||
#. analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "分析缓存到SGF"
|
||||
@@ -8,16 +8,16 @@ msgstr ""
|
||||
|
||||
#. main hamburger menu
|
||||
msgid "menu:playersetup"
|
||||
msgstr "Spieler Setup"
|
||||
msgstr "Spieler-Setup"
|
||||
|
||||
msgid "menu:newgame"
|
||||
msgstr "Neue Partie"
|
||||
|
||||
msgid "menu:save"
|
||||
msgstr "Partie Speichern"
|
||||
msgstr "Partie speichern"
|
||||
|
||||
msgid "menu:load"
|
||||
msgstr "Partie Laden"
|
||||
msgstr "Partie laden"
|
||||
|
||||
msgid "menu:settings"
|
||||
msgstr "Allgemeine Einstellungen"
|
||||
@@ -29,7 +29,7 @@ msgid "menu:teachsettings"
|
||||
msgstr "Einstellungen Übungsspiel"
|
||||
|
||||
msgid "menu:aisettings"
|
||||
msgstr "AI Einstellungen"
|
||||
msgstr "AI-Einstellungen"
|
||||
|
||||
msgid "menu:lang"
|
||||
msgstr "Spracheinstellungen"
|
||||
@@ -51,7 +51,7 @@ msgid "player:ai"
|
||||
msgstr "AI"
|
||||
|
||||
msgid "aistrategy"
|
||||
msgstr "AI Strategie"
|
||||
msgstr "AI-Strategie"
|
||||
|
||||
msgid "gametype"
|
||||
msgstr "Partieart"
|
||||
@@ -99,7 +99,7 @@ msgid "analysis:sweep"
|
||||
msgstr "Schnelle Analyse aller Züge"
|
||||
|
||||
msgid "analysis:aimove"
|
||||
msgstr "AI Zug erzwingen"
|
||||
msgstr "AI-Zug erzwingen"
|
||||
|
||||
#. right panel buttons and below board
|
||||
msgid "pass-button-text"
|
||||
@@ -107,8 +107,8 @@ msgstr "Passen"
|
||||
|
||||
msgid "ai-ponder"
|
||||
msgstr ""
|
||||
"Zeigt den Enginestatus an von grün (Leerlauf) bis orange(Zugberechnung). Ist"
|
||||
" rot, wenn die Engine abgestürzt ist oder nicht starten konnte."
|
||||
"Zeigt den Enginestatus an von grün (Leerlauf) bis orange (Zugberechnung). "
|
||||
"Ist rot, wenn die Engine abgestürzt ist oder nicht starten konnte."
|
||||
|
||||
#. undo button on the right
|
||||
msgid "undo-button-text"
|
||||
@@ -131,17 +131,17 @@ msgid "tab:notes"
|
||||
msgstr "Anmerkungen"
|
||||
|
||||
msgid "closedlabel:scoregraph"
|
||||
msgstr "Punkte Graph"
|
||||
msgstr "Punkte-Graph"
|
||||
|
||||
#. on the graph
|
||||
msgid "Jigo"
|
||||
msgstr "Jigo"
|
||||
|
||||
msgid "closedlabel:movestats"
|
||||
msgstr "Zug Statistik"
|
||||
msgstr "Zug-Statistik"
|
||||
|
||||
msgid "closedlabel:info¬es"
|
||||
msgstr "Zug Info & Anmerkungen"
|
||||
msgstr "Zug-Info & Anmerkungen"
|
||||
|
||||
#. on the board after passing once/twice
|
||||
msgid "board-pass"
|
||||
@@ -149,7 +149,7 @@ msgstr "Passen"
|
||||
|
||||
msgid "board-game-end"
|
||||
msgstr ""
|
||||
"Partie\n"
|
||||
"Partie-\n"
|
||||
"Ende"
|
||||
|
||||
#. Select mode
|
||||
@@ -178,8 +178,8 @@ msgstr "Punkte gewonnen"
|
||||
#. SGF and move comment messages
|
||||
msgid "SGF start message"
|
||||
msgstr ""
|
||||
"Züge, die mit 'X' markiert sind, sind die besten Züge für KataGo, die "
|
||||
"miteinem Quadrat sind Züge, die wenige als 0.5 Punkte verlieren."
|
||||
"Züge, die mit 'X' markiert sind, sind die besten Züge für KataGo; die mit "
|
||||
"einem Quadrat sind Züge, die weniger als 0.5 Punkte verlieren."
|
||||
|
||||
msgid "Info:score"
|
||||
msgstr "Ergebnis: {score}"
|
||||
@@ -195,7 +195,7 @@ msgid "move"
|
||||
msgstr "Zug {number}"
|
||||
|
||||
msgid "Info:top move"
|
||||
msgstr "Bestvorhergesagte Zug war {top_move} ({score})."
|
||||
msgstr "Bestvorhergesagter Zug war {top_move} ({score})."
|
||||
|
||||
msgid "Info:best move"
|
||||
msgstr "Zug war der bestvorhergesagte Zug"
|
||||
@@ -204,7 +204,7 @@ msgid "Info:PV"
|
||||
msgstr "PV: {pv}"
|
||||
|
||||
msgid "Info:policy rank"
|
||||
msgstr "Zug war #{rank} gemäss Policy ({probability:.2%})."
|
||||
msgstr "Zug war #{rank} gemäß Policy ({probability:.2%})."
|
||||
|
||||
msgid "Info:policy best"
|
||||
msgstr "Bester Policy-Zug war {move} ({probability:.1%})."
|
||||
@@ -225,11 +225,11 @@ msgid "Analyzing move..."
|
||||
msgstr "Zug wird analysiert..."
|
||||
|
||||
msgid "SGF Notes Hint"
|
||||
msgstr "Deine SGF Anmerkungen für diese Position."
|
||||
msgstr "Deine SGF-Anmerkungen für diese Position."
|
||||
|
||||
# sgf load popup
|
||||
msgid "load sgf title"
|
||||
msgstr "SGF File für Analyse laden"
|
||||
msgstr "SGF-Datei für Analyse laden"
|
||||
|
||||
msgid "load sgf fast analysis"
|
||||
msgstr "Schnelle Analyse verwenden"
|
||||
@@ -239,10 +239,10 @@ msgstr "Am Anfang beginnen"
|
||||
|
||||
# timer settings from here
|
||||
msgid "timer settings"
|
||||
msgstr "Timereinstellungen Anpassen"
|
||||
msgstr "Timereinstellungen anpassen"
|
||||
|
||||
msgid "byoyomi length"
|
||||
msgstr "Zeit pro Byo-yomi Periode"
|
||||
msgstr "Zeit pro Byo-yomi-Periode"
|
||||
|
||||
msgid "byoyomi periods"
|
||||
msgstr "Anzahl Perioden"
|
||||
@@ -256,10 +256,10 @@ msgstr "Timer aktualisieren"
|
||||
|
||||
# new game settings from here
|
||||
msgid "New Game title"
|
||||
msgstr "Neues Spiel Einstellen"
|
||||
msgstr "Neues Spiel erstellen"
|
||||
|
||||
msgid "board size"
|
||||
msgstr "Brettgrösse"
|
||||
msgstr "Brettgröße"
|
||||
|
||||
msgid "handicap"
|
||||
msgstr "Vorgabe"
|
||||
@@ -284,12 +284,12 @@ msgid "aga"
|
||||
msgstr "AGA"
|
||||
|
||||
msgid "clear cache"
|
||||
msgstr "Cache Leeren"
|
||||
msgstr "Cache leeren"
|
||||
|
||||
#. clear cache help
|
||||
msgid "avoids replaying"
|
||||
msgstr ""
|
||||
"Vermeidet Identische\n"
|
||||
"Vermeidet, identische\n"
|
||||
"Partien zu spielen"
|
||||
|
||||
#. hints on new game
|
||||
@@ -298,14 +298,14 @@ msgstr "Benutze x:y (z.B. 19:9) für nichtquadratische Grössen"
|
||||
|
||||
#. submit new game
|
||||
msgid "new game"
|
||||
msgstr "Neues Spiel Starten"
|
||||
msgstr "Neues Spiel starten"
|
||||
|
||||
# teacher settings from here
|
||||
msgid "teacher settings"
|
||||
msgstr "Übungsspieleinstellungen Bearbeiten"
|
||||
msgstr "Übungsspieleinstellungen bearbeiten"
|
||||
|
||||
msgid "point loss threshold"
|
||||
msgstr "Punktverlust grösser als"
|
||||
msgstr "Punktverlust größer als"
|
||||
|
||||
msgid "num undos"
|
||||
msgstr "Anzahl der Undos"
|
||||
@@ -326,7 +326,7 @@ msgstr ""
|
||||
|
||||
msgid "show ai dots"
|
||||
msgstr ""
|
||||
"Dots/SGF Kommentare \n"
|
||||
"Dots/SGF-Kommentare \n"
|
||||
" für AI Spieler zeigen"
|
||||
|
||||
msgid "lock ai when playing"
|
||||
@@ -340,11 +340,11 @@ msgstr "Feedbackeinstellungen aktualisieren"
|
||||
|
||||
# main config settings
|
||||
msgid "restarting engine"
|
||||
msgstr "Engine nach Änderunge der Einstellungen neustarten."
|
||||
msgstr "Engine nach Änderungen der Einstellungen neustarten."
|
||||
|
||||
# title
|
||||
msgid "general settings title"
|
||||
msgstr "Allgemeine und Engine Einstellungen"
|
||||
msgstr "Allgemeine und Engine-Einstellungen"
|
||||
|
||||
msgid "file not found"
|
||||
msgstr "Pfad existiert nicht"
|
||||
@@ -353,25 +353,25 @@ msgid "general settings"
|
||||
msgstr "Allgemeine Einstellungen"
|
||||
|
||||
msgid "engine settings"
|
||||
msgstr "Engine Einstellungen"
|
||||
msgstr "Engine-Einstellungen"
|
||||
|
||||
msgid "katago settings"
|
||||
msgstr "KataGo Einstellungen"
|
||||
msgstr "KataGo-Einstellungen"
|
||||
|
||||
msgid "config file path"
|
||||
msgstr "Alle Optionen sind gespeichert in:"
|
||||
|
||||
msgid "engine:katago"
|
||||
msgstr "Pfad zur KataGo Datei"
|
||||
msgstr "Pfad zur KataGo-Datei"
|
||||
|
||||
msgid "engine:katago:hint"
|
||||
msgstr "Leer = beigefügte Datei verwenden."
|
||||
msgstr "Leer = beigefügte Datei verwenden"
|
||||
|
||||
msgid "engine:model"
|
||||
msgstr "Pfad zur KataGo Model Datei"
|
||||
msgstr "Pfad zur KataGo-Model-Datei"
|
||||
|
||||
msgid "engine:config"
|
||||
msgstr "Pfad zur KataGo Config Datei"
|
||||
msgstr "Pfad zur KataGo-Config-Datei"
|
||||
|
||||
msgid "engine:max_visits"
|
||||
msgstr "Maximale Anzahl der Visits in Analyse"
|
||||
@@ -394,16 +394,16 @@ msgid "engine:time:hint"
|
||||
msgstr "Zeit in Sekunden"
|
||||
|
||||
msgid "general:sgf_load"
|
||||
msgstr "Pfad um SGF Dateien zu laden"
|
||||
msgstr "Pfad um SGF-Dateien zu laden"
|
||||
|
||||
msgid "general:sgf_save"
|
||||
msgstr "Pfad um SGF Dateien zu speichern"
|
||||
msgstr "Pfad um SGF-Dateien zu speichern"
|
||||
|
||||
msgid "general:anim_pv_time"
|
||||
msgstr "Zeit zwischen Zügen, wenn Sequenz animiert wird"
|
||||
|
||||
msgid "general:debug_level"
|
||||
msgstr "Debug level in der Konsole"
|
||||
msgstr "Debuglevel in der Konsole"
|
||||
|
||||
msgid "general:debug_level:hint"
|
||||
msgstr "Auf 1 setzen, wenn Fehler gemeldet werden sollen"
|
||||
@@ -413,19 +413,19 @@ msgstr "Einstellungen aktualisieren"
|
||||
|
||||
# ai settings from here
|
||||
msgid "ai settings"
|
||||
msgstr "Erweiterte AI Einstellungen"
|
||||
msgstr "Erweiterte AI-Einstellungen"
|
||||
|
||||
msgid "Select AI"
|
||||
msgstr "AI Strategie zum Konfigurieren auswählen"
|
||||
msgstr "AI-Strategie zum Konfigurieren auswählen"
|
||||
|
||||
msgid "update ai settings"
|
||||
msgstr "AI Einstellungen aktualisieren"
|
||||
msgstr "AI-Einstellungen aktualisieren"
|
||||
|
||||
#. misc errors and info messages
|
||||
msgid "sgf written"
|
||||
msgstr "SGF mit Analyse gespeichert unter {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr ""
|
||||
"Auf Abschluss der Initialanalyse warten, bevor eine Verfeinerung erfolgt."
|
||||
|
||||
@@ -438,18 +438,18 @@ msgstr ""
|
||||
"Clipboard contents: {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "SGF konnte nicht geladen werden. Parse Error: {error}"
|
||||
msgstr "SGF konnte nicht geladen werde: {error}"
|
||||
|
||||
msgid "Starting Kata failed"
|
||||
msgstr ""
|
||||
"Der Start von KataGo mit Befehl '{command}' ist fehlgeschlagen mit Fehler "
|
||||
"{error}.Bitte überprüfe, dass der 'katago' Wert in 'engine' bei den "
|
||||
"Einstellungen aufdas richtige KataGo File zeigt."
|
||||
"{error}. Bitte überprüfe, dass der Wert 'katago' in 'engine' bei den "
|
||||
"Einstellungen auf die richtige KataGo-Datei zeigt."
|
||||
|
||||
msgid "Engine died unexpectedly"
|
||||
msgstr ""
|
||||
"Engine ist unerwartet abgestürzt ohne Ausgabe zu senden,wahrscheinlich wegen"
|
||||
" zu wenig Speicher: {error}"
|
||||
"Engine ist unerwartet abgestürzt, ohne Ausgabe zu senden, wahrscheinlich "
|
||||
"wegen zu wenig Speicher: {error}"
|
||||
|
||||
#. AI names, help etc
|
||||
msgid "strength:kyu"
|
||||
@@ -463,8 +463,8 @@ msgstr "KataGo"
|
||||
|
||||
msgid "aihelp:default"
|
||||
msgstr ""
|
||||
"Spielstärkste KataGo AI. Spielstärke wird von `max visits` und `model`in den"
|
||||
" allgemeinen Einstellungen im Bereich `engine` und dem Engine-"
|
||||
"Spielstärkste KataGo-AI. Spielstärke wird von `max visits` und `model` in "
|
||||
"den allgemeinen Einstellungen im Bereich `engine` und dem Engine-"
|
||||
"Konfigurationsfile bestimmt. Hier gibt es sonst keine Optionen."
|
||||
|
||||
msgid "ai:jigo"
|
||||
@@ -498,9 +498,9 @@ msgstr "Policy Gewichtet"
|
||||
|
||||
msgid "aihelp:p:weighted"
|
||||
msgstr ""
|
||||
"Wählt den Zug gemäss der Policy-Wahrscheinlichkeit, beeinflusst durch "
|
||||
"Wählt den Zug gemäß der Policy-Wahrscheinlichkeit, beeinflusst durch "
|
||||
"`weaken_fac` wie wahrscheinlich schlechtere Züge gespielt werden. Mit einem "
|
||||
"kleinen Wert spielt wie die `Policy` AI. `pick_override` legt fest, wann der"
|
||||
"kleinen Wert spielt wie die AI `Policy`. `pick_override` legt fest, wann der"
|
||||
" beste Zug gewählt wird ohne Zufall und `lower_bound` bestimmt die untere "
|
||||
"Grenze des erlaubten Policy-Wertes."
|
||||
|
||||
@@ -510,50 +510,50 @@ msgstr "Blinde Policy"
|
||||
msgid "aihelp:p:pick"
|
||||
msgstr ""
|
||||
"Wählt `pick_n + pick_frac * <number of legal moves>` gewichtete Züge "
|
||||
"zufälligund spielt den Besten. Spielt wie die Policy AI, wenn `pick_frac=1`."
|
||||
" Spielt besten Zug, wenn Policy-Wert höher als `pick_override` ist, um "
|
||||
"offensichtliche Fehler zu vermeiden."
|
||||
"zufällig und spielt den besten. Spielt wie die AI `Policy`, wenn "
|
||||
"`pick_frac=1`. Spielt besten Zug, wenn Policy-Wert höher als `pick_override`"
|
||||
" ist, um offensichtliche Fehler zu vermeiden."
|
||||
|
||||
msgid "ai:p:local"
|
||||
msgstr "Lokaler Stil"
|
||||
|
||||
msgid "aihelp:p:local"
|
||||
msgstr ""
|
||||
"Wählt `pick_n + pick_frac * <number of legal moves>` gewichtete Züge nahedem"
|
||||
" letzten Zug und spielt den Besten. Spielt besten Zug, wenn Policy-Wert "
|
||||
"Wählt `pick_n + pick_frac * <number of legal moves>` gewichtete Züge nahe "
|
||||
"dem letzten Zug und spielt den besten. Spielt besten Zug, wenn Policy-Wert "
|
||||
"höher als`pick_override` ist, um offensichtliche Fehler zu vermeiden."
|
||||
|
||||
msgid "ai:p:tenuki"
|
||||
msgstr "Tenuki Stil"
|
||||
msgstr "Tenuki-Stil"
|
||||
|
||||
msgid "aihelp:p:tenuki"
|
||||
msgstr ""
|
||||
"Wählt `pick_n + pick_frac * <number of legal moves>` gewichtete Züge "
|
||||
"fernvomletzten Zug und spielt den Besten. Spielt besten Zug, wenn Policy-"
|
||||
"Wert höher als`pick_override` ist, um offensichtliche Fehler zu vermeiden. "
|
||||
"Eine höhere `stddev`führt zu weiter entfernten Zügen."
|
||||
"Wählt `pick_n + pick_frac * <number of legal moves>` gewichtete Züge fern "
|
||||
"vom letzten Zug und spielt den besten. Spielt besten Zug, wenn Policy-Wert "
|
||||
"höher als`pick_override` ist, um offensichtliche Fehler zu vermeiden. Eine "
|
||||
"höhere `stddev` führt zu weiter entfernten Zügen."
|
||||
|
||||
msgid "ai:p:influence"
|
||||
msgstr "Einflussstil"
|
||||
|
||||
msgid "aihelp:p:influence"
|
||||
msgstr ""
|
||||
"Wählt Züge oberhalb des Grenzwertes und spielt den Besten.Erhöhe "
|
||||
"`line_weight`,um Züge nahe der Ecken mehr zu bestrafen.Stoppt die Strategie "
|
||||
"nach dem der 'endgame' Anteil des Brettes gefüllt ist."
|
||||
"Wählt Züge oberhalb des Grenzwertes und spielt den Besten. Erhöhe "
|
||||
"`line_weight`, um Züge nahe der Ecken mehr zu bestrafen. Stoppt die "
|
||||
"Strategie nachdem der 'endgame'-Anteil des Brettes gefüllt ist."
|
||||
|
||||
msgid "ai:p:territory"
|
||||
msgstr "Gebietsstil"
|
||||
|
||||
msgid "aihelp:p:territory"
|
||||
msgstr ""
|
||||
"Wählt Züge unterhalb des Grenzwertes und spielt den Besten.Erhöhe "
|
||||
"`line_weight`,um Züge nahe der Mitte mehr zu bestrafen.Stoppt die Strategie "
|
||||
"nach dem der 'endgame' Anteil des Brettes gefüllt ist."
|
||||
"Wählt Züge unterhalb des Grenzwertes und spielt den besten. Erhöhe "
|
||||
"`line_weight`, um Züge nahe der Mitte mehr zu bestrafen. Stoppt die "
|
||||
"Strategie nachdem der 'endgame'-Anteil des Brettes gefüllt ist."
|
||||
|
||||
msgid "aihelp:p:rank"
|
||||
msgstr ""
|
||||
"Wählt eine begrenzte Anzahl zufälliger Züge und spielt den Besten. Stärkere "
|
||||
"Wählt eine begrenzte Anzahl zufälliger Züge und spielt den besten. Stärkere "
|
||||
"Einstellungen wählen den besten Zug aus einer grösseren Auswahl. Da es "
|
||||
"keinen 0 Kyu/Dan gibt, ist 3 Dan = -2 Kyu."
|
||||
|
||||
@@ -567,23 +567,23 @@ msgstr "Details"
|
||||
|
||||
msgid "Kata exe not found"
|
||||
msgstr ""
|
||||
"KataGo binary {exe} nicht gefunden, bitte prüfe Allgemeine Einstellungen."
|
||||
"KataGo-Binary {exe} nicht gefunden, bitte prüfe allgemeine Einstellungen."
|
||||
|
||||
msgid "Kata config not found"
|
||||
msgstr ""
|
||||
"KataGo config Datei {config} nicht gefunden, bitte prüfe Allgemeine "
|
||||
"KataGo-Konfigurationsdatei {config} nicht gefunden, bitte prüfe allgemeine "
|
||||
"Einstellungen."
|
||||
|
||||
msgid "Kata exe not found in path"
|
||||
msgstr ""
|
||||
"KataGo binary {exe} nicht im PATH gefunden, prüfe deine Umgebungs- variablen"
|
||||
" oder benutze den vollen Pfad in den Allgemeinen Einstellungen. Wenn unter "
|
||||
"MacOS, konsultiere das Handbuch wie brew verwendet werden soll, um KataGo zu"
|
||||
" installieren."
|
||||
"KataGo-Binary {exe} nicht im PATH gefunden, prüfe deine Umgebungsvariablen "
|
||||
"oder benutze den vollen Pfad in den allgemeinen Einstellungen. Wenn unter "
|
||||
"MacOS, konsultiere das Handbuch, wie brew verwendet werden soll, um KataGo "
|
||||
"zu installieren."
|
||||
|
||||
msgid "Kata model not found"
|
||||
msgstr ""
|
||||
"KataGo model {model} nicht gefunden, bitte prüfe Allgemeine Einstellungen."
|
||||
"KataGo-Modell {model} nicht gefunden, bitte prüfe allgemeine Einstellungen."
|
||||
|
||||
msgid "extra analysis"
|
||||
msgstr "Erweiterte Analyse wird ausgeführt mit {visits} Visits"
|
||||
@@ -616,13 +616,14 @@ msgid "analysis:continuous"
|
||||
msgstr "Kontinuierliche Analyse ein/aus"
|
||||
|
||||
msgid "ai:handicap"
|
||||
msgstr "KataHandicap"
|
||||
msgstr "Kata-Vorgabesteine"
|
||||
|
||||
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."
|
||||
"Optimiertes KataGo für höhere Vorgabe-Spiele. Die Einstellung `pda` "
|
||||
"entspricht `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"
|
||||
@@ -644,7 +645,7 @@ msgstr "Farbschema"
|
||||
|
||||
msgid "engine:altcommand:hint"
|
||||
msgstr ""
|
||||
"Kompletter Befehl um die Engine zu starten. Alle anderen Modelloptionen "
|
||||
"Kompletter Befehl, um die Engine zu starten. Alle anderen Modelloptionen "
|
||||
"werden ignoriert."
|
||||
|
||||
msgid "engine:altcommand"
|
||||
@@ -664,13 +665,10 @@ msgstr "Alle verfügbaren Programme wurden bereits heruntergeladen"
|
||||
|
||||
msgid "Engine missing DLL"
|
||||
msgstr ""
|
||||
"Konnte KataGo wegen fehlender DLL nicht starten. Überprüf die Anforderungen "
|
||||
"in der KataGo-Dokumentation oder versuch, KataGo alleine zu starten, um "
|
||||
"Konnte KataGo wegen fehlender DLL nicht starten. Überprüfe die Anforderungen"
|
||||
" in der KataGo-Dokumentation oder versuche, KataGo alleine zu starten, um "
|
||||
"Einzelheiten zu erfahren."
|
||||
|
||||
msgid "show point loss as text"
|
||||
msgstr "Zeig den Punkteverlust auf Beste Züge"
|
||||
|
||||
msgid "teaching undo message"
|
||||
msgstr ""
|
||||
"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bitte "
|
||||
@@ -690,41 +688,26 @@ msgstr "Finde alternative Züge"
|
||||
msgid "alternative analysis"
|
||||
msgstr "Suche nach alternativen Zügen"
|
||||
|
||||
#. TODO - ignore moves if timer is on and less than this is used
|
||||
#. ignore moves if timer is on and less than this is used
|
||||
msgid "minimal time use"
|
||||
msgstr "Minimal time use in byo-yomi (s)"
|
||||
msgstr "Mindest-Zeitnutzung im Byo-Yomi (s)"
|
||||
|
||||
msgid "move too fast"
|
||||
msgstr "Denke mindestens {num} Sekunden nach, bevor Du setzt."
|
||||
|
||||
msgid "main time"
|
||||
msgstr "Hauptbedenkzeit (Min.)"
|
||||
msgstr "Hauptbedenkzeit (min)"
|
||||
|
||||
msgid "aihelp:simple"
|
||||
msgstr ""
|
||||
"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von "
|
||||
"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens "
|
||||
"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren und"
|
||||
"Zügen, die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens "
|
||||
"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren und "
|
||||
"eigenes Gebiet mehr sichern."
|
||||
|
||||
msgid "ai:simple"
|
||||
msgstr "Einfacher Stil"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von "
|
||||
"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens "
|
||||
"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren, "
|
||||
"eigenes Gebiet mehr sichern"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Style"
|
||||
|
||||
msgid "analysis:local"
|
||||
msgstr "Finde lokale Antworten"
|
||||
|
||||
msgid "local analysis"
|
||||
msgstr "Suche nach lokalen Antworten"
|
||||
|
||||
@@ -735,3 +718,107 @@ msgstr ""
|
||||
|
||||
msgid "Make Main Branch"
|
||||
msgstr "Zum Hauptzweig machen"
|
||||
|
||||
#. setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "Anzeige für den besten Zug"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "Änderung der Gewinnrate"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "Visits"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "Score"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(nichts)"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "Gewinnrate"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "Punktänderung"
|
||||
|
||||
#. top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "Zeige Bewertung wenn mehr als ... Visits"
|
||||
|
||||
#. analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "Cache-Analyse in SGF"
|
||||
|
||||
#. Speichern als
|
||||
msgid "menu:save-as"
|
||||
msgstr "Spiel speichern als..."
|
||||
|
||||
#. Laden
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "Spiel laden"
|
||||
|
||||
#. Save SGF (Popup)
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "SGF-Datei speichern"
|
||||
|
||||
#. Save button
|
||||
# sgf save button
|
||||
msgid "Save File"
|
||||
msgstr "Datei speichern"
|
||||
|
||||
#. Status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "Starte Einfüge-Modus; drücke 'i' zum beenden"
|
||||
|
||||
#. Status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "Einfügen beendet, {num_copied} Züge kopiert"
|
||||
|
||||
#. When a user tries to go to a different move/node while in insert
|
||||
#. mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr ""
|
||||
"Navigation ist im Einfüge-Modus deaktiviert, beende erst Einfüge-Modus durch"
|
||||
" Drücken von 'i'"
|
||||
|
||||
#. In new game options
|
||||
msgid "player names"
|
||||
msgstr "Spielernamen"
|
||||
|
||||
#. Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "Züge einfügen"
|
||||
|
||||
#. Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr ""
|
||||
"Ziehen, um einen Bereich zu markieren. Wähle einen einzelnen Punkt, um die "
|
||||
"Bereichsauswahl abzubrechen."
|
||||
|
||||
#. Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "Bereich auswählen"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "Aus-/Einklappen"
|
||||
|
||||
#. Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "Schnelles Ausspielen bis Spielende"
|
||||
|
||||
#. Hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "Name von Schwarz"
|
||||
|
||||
#. Hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "Name von Weiß"
|
||||
@@ -11,11 +11,14 @@ msgid "menu:playersetup"
|
||||
msgstr "Player Setup"
|
||||
|
||||
msgid "menu:newgame"
|
||||
msgstr "New Game"
|
||||
msgstr "New Game/Change Rules"
|
||||
|
||||
msgid "menu:save"
|
||||
msgstr "Save Game"
|
||||
|
||||
msgid "menu:save-as"
|
||||
msgstr "Save Game As..."
|
||||
|
||||
msgid "menu:load"
|
||||
msgstr "Load Game"
|
||||
|
||||
@@ -106,9 +109,19 @@ msgstr "Deeper full game analysis"
|
||||
msgid "analysis:alternatives"
|
||||
msgstr "Find alternative moves"
|
||||
|
||||
#. Analyze again but allow only nearby moves
|
||||
msgid "analysis:local"
|
||||
msgstr "Find local responses"
|
||||
#. Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "Set region of interest"
|
||||
|
||||
#. Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr ""
|
||||
"Drag to select the region of interest. Select a single point to cancel the "
|
||||
"region."
|
||||
|
||||
#. Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "Fast playout to game end"
|
||||
|
||||
#. Keep improving current position analysis when there is nothing else to do
|
||||
msgid "analysis:continuous"
|
||||
@@ -117,6 +130,10 @@ msgstr "Toggle continuous analysis"
|
||||
msgid "analysis:aimove"
|
||||
msgstr "Force AI Move"
|
||||
|
||||
#. Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "Insert moves"
|
||||
|
||||
#. status message
|
||||
msgid "extra analysis"
|
||||
msgstr "Performing additional analysis to {visits} visits"
|
||||
@@ -290,6 +307,18 @@ msgstr "Your SGF notes for this position here."
|
||||
msgid "load sgf title"
|
||||
msgstr "Load SGF File to Analyze"
|
||||
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "Load File"
|
||||
|
||||
# sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "Save File"
|
||||
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "Save SGF File"
|
||||
|
||||
msgid "load sgf fast analysis"
|
||||
msgstr "Use fast analysis"
|
||||
|
||||
@@ -328,6 +357,18 @@ msgstr "Update Timer"
|
||||
msgid "New Game title"
|
||||
msgstr "Setup New Game"
|
||||
|
||||
#. in new game options
|
||||
msgid "player names"
|
||||
msgstr "Player Names"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "Black player's name"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "White player's name"
|
||||
|
||||
msgid "board size"
|
||||
msgstr "Board size"
|
||||
|
||||
@@ -354,7 +395,7 @@ msgid "aga"
|
||||
msgstr "AGA"
|
||||
|
||||
msgid "clear cache"
|
||||
msgstr "Clear Cache"
|
||||
msgstr "Clear cache"
|
||||
|
||||
#. clear cache help
|
||||
msgid "avoids replaying"
|
||||
@@ -378,7 +419,7 @@ msgstr ""
|
||||
|
||||
# teacher settings from here
|
||||
msgid "teacher settings"
|
||||
msgstr "Edit Teacher Settings"
|
||||
msgstr "Edit Teaching/Analysis Settings"
|
||||
|
||||
msgid "point loss threshold"
|
||||
msgstr "Point loss greater than"
|
||||
@@ -405,9 +446,9 @@ msgstr ""
|
||||
"Show dots/SGF comments\n"
|
||||
" for AI players"
|
||||
|
||||
#. in addition to colour, show point loss as -0.1 etc on hints/top moves
|
||||
msgid "show point loss as text"
|
||||
msgstr "Show point loss on top moves"
|
||||
#. analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "Cache analysis in SGF"
|
||||
|
||||
#. removed as an option / can ignore
|
||||
msgid "lock ai when playing"
|
||||
@@ -515,7 +556,7 @@ msgstr "Update AI Settings"
|
||||
msgid "sgf written"
|
||||
msgstr "SGF with analysis written to {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "Wait for initial analysis to complete before requesting a refinement."
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -527,7 +568,7 @@ msgstr ""
|
||||
"Clipboard contents: {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "Failed to load SGF. Parse Error: {error}"
|
||||
msgstr "Failed to load SGF: {error}"
|
||||
|
||||
msgid "Kata exe not found"
|
||||
msgstr "KataGo binary {exe} does not exist, please check general settings."
|
||||
@@ -600,23 +641,6 @@ msgstr ""
|
||||
"root noise (in general settings) can be used to increase play according to "
|
||||
"the style at the cost of strength. "
|
||||
|
||||
#. DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Style"
|
||||
|
||||
#. DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Plays moves that lead to clarity on stones' status, favouring solid moves "
|
||||
"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
|
||||
"points and with at least 'min_visits', prefer moves that lose fewer points, "
|
||||
"settle own stones more (with importance settled_weight), settle opponent's "
|
||||
"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
|
||||
"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
|
||||
" attachment_penalty). Weights can be negative to make it complicate the game"
|
||||
" instead, and wide root noise (in general settings) can be used to further "
|
||||
"play according to the style at the cost of strength. "
|
||||
|
||||
msgid "ai:jigo"
|
||||
msgstr "KataJigo"
|
||||
|
||||
@@ -748,6 +772,10 @@ msgstr "Number of visits per move"
|
||||
msgid "Delete Node"
|
||||
msgstr "Delete Node"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "Expand/Collapse"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Make Main Branch"
|
||||
msgstr "Make Main Branch"
|
||||
@@ -763,3 +791,48 @@ msgstr "Default Theme"
|
||||
#. theme name
|
||||
msgid "theme:red-green-colourblind"
|
||||
msgstr "Red-Green Colourblind"
|
||||
|
||||
#. setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "Stats on top moves"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "Change in score"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "Score"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "Change in win rate"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "Win rate"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "Visits"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(nothing)"
|
||||
|
||||
#. top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "Show stats if more than ... visits"
|
||||
|
||||
#. status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "Starting insert mode, press 'i' to finish inserting."
|
||||
|
||||
#. status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "Inserting exchange(s) done, copied {num_copied} following moves."
|
||||
|
||||
#. when a user tries to go to a different move/node while in insert mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr ""
|
||||
"Navigation is disabled in insert mode, finish insert mode by pressing 'i'. "
|
||||
@@ -430,7 +430,7 @@ msgstr "Actualizar conf IA"
|
||||
msgid "sgf written"
|
||||
msgstr "SGF con análisis escrito en {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr ""
|
||||
"Esperar completar análisis inicial antes de solicitar un refinamiento."
|
||||
|
||||
@@ -443,7 +443,7 @@ msgstr ""
|
||||
"Contenido portapapeles: {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "Error al cargar SGF. Error de parseo: {error}"
|
||||
msgstr "Error al cargar SGF: {error}"
|
||||
|
||||
msgid "Starting Kata failed"
|
||||
msgstr ""
|
||||
@@ -681,11 +681,6 @@ msgstr ""
|
||||
"Failed to start KataGo due to missing DLL. Check the requirements, or try "
|
||||
"running the binary by itself for details. "
|
||||
|
||||
#. TODO - in addition to colour, show point loss as -0.1 etc on hints/top
|
||||
#. moves
|
||||
msgid "show point loss as text"
|
||||
msgstr "Show point loss on top moves"
|
||||
|
||||
#. TODO - resign the game
|
||||
msgid "resign"
|
||||
msgstr "Resign"
|
||||
@@ -718,27 +713,6 @@ msgstr "Plays moves that lead to simplifying the board state."
|
||||
msgid "ai:simple"
|
||||
msgstr "Simple Style"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Plays moves that lead to clarity on stones' status, favouring solid moves "
|
||||
"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
|
||||
"points and with at least 'min_visits', prefer moves that lose fewer points, "
|
||||
"settle own stones more (with importance settled_weight), settle opponent's "
|
||||
"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
|
||||
"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
|
||||
" attachment_penalty). Weights can be negative to make it complicate the game"
|
||||
" instead, and wide root noise (in general settings) can be used to further "
|
||||
"play according to the style at the cost of strength. "
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Style"
|
||||
|
||||
#. TODO - Analyze again but allow only nearby moves
|
||||
msgid "analysis:local"
|
||||
msgstr "Find local responses"
|
||||
|
||||
#. TODO - status message
|
||||
msgid "local analysis"
|
||||
msgstr "Searching for local responses"
|
||||
@@ -752,3 +726,106 @@ msgstr ""
|
||||
#. TODO - on move tree editing
|
||||
msgid "Make Main Branch"
|
||||
msgstr "Make Main Branch"
|
||||
|
||||
#. TODO - setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "Stats on top moves"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "Change in win rate"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "Visits"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "Score"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(nothing)"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "Win rate"
|
||||
|
||||
#. TODO - Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "Change in score"
|
||||
|
||||
#. TODO - top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "Show stats if more than ... visits"
|
||||
|
||||
#. TODO - analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "Cache analysis in SGF"
|
||||
|
||||
#. TODO
|
||||
msgid "menu:save-as"
|
||||
msgstr "Save Game As..."
|
||||
|
||||
#. TODO
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "Load File"
|
||||
|
||||
#. TODO
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "Save SGF File"
|
||||
|
||||
#. TODO
|
||||
# sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "Save File"
|
||||
|
||||
#. TODO - status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "Starting insert mode, press 'i' to finish inserting."
|
||||
|
||||
#. TODO - status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "Inserting exchange(s) done, copied {num_copied} following moves."
|
||||
|
||||
#. TODO - when a user tries to go to a different move/node while in insert
|
||||
#. mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr ""
|
||||
"Navigation is disabled in insert mode, finish insert mode by pressing 'i'. "
|
||||
|
||||
#. TODO - in new game options
|
||||
msgid "player names"
|
||||
msgstr "Player Names"
|
||||
|
||||
#. TODO - Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "Insert moves"
|
||||
|
||||
#. TODO - Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr ""
|
||||
"Drag to select the region of interest. Select a single point to remove the "
|
||||
"region."
|
||||
|
||||
#. TODO - Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "Set region of interest"
|
||||
|
||||
#. TODO - on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "Expand/Collapse"
|
||||
|
||||
#. TODO - Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "Fast playout to game end"
|
||||
|
||||
#. TODO - hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "Black player's name"
|
||||
|
||||
#. TODO - hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "White player's name"
|
||||
@@ -105,7 +105,7 @@ msgid "analysis:continuous"
|
||||
msgstr "Lancer/Arrêter l'analyse continue"
|
||||
|
||||
msgid "analysis:aimove"
|
||||
msgstr "Jouer le coup de l'IA"
|
||||
msgstr "Jouer le coup du joueur artificiel"
|
||||
|
||||
#. label in little popup for analyzing entire game
|
||||
msgid "reanalyze max visits"
|
||||
@@ -476,7 +476,7 @@ msgstr "Mettre à jour"
|
||||
msgid "sgf written"
|
||||
msgstr "Analyse SGF rédigée dans {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "Attendez la fin de l'analyse en cours avant de l'approfondir."
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -488,7 +488,7 @@ msgstr ""
|
||||
"Contenu du presse-papier : {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "Impossible de charger le SGF. Erreur d'analyse : {error}"
|
||||
msgstr "Impossible de charger le SGF: {error}"
|
||||
|
||||
msgid "Kata model not found"
|
||||
msgstr ""
|
||||
@@ -522,7 +522,6 @@ msgstr ""
|
||||
"possiblement pour cause de \n"
|
||||
"mémoire insuffisante : {error}"
|
||||
|
||||
#. TODO: updated
|
||||
msgid "teaching undo message"
|
||||
msgstr ""
|
||||
"Le coup {move} a été annulé car il perdait {points_lost:.1f} points.Vous "
|
||||
@@ -711,9 +710,6 @@ msgstr ""
|
||||
"votre configuration système, ou bien essayez l'exécutable seul pour plus de "
|
||||
"détails."
|
||||
|
||||
msgid "show point loss as text"
|
||||
msgstr "Indiquer les points perdus"
|
||||
|
||||
msgid "resign"
|
||||
msgstr "Abandon"
|
||||
|
||||
@@ -746,26 +742,6 @@ msgstr ""
|
||||
msgid "ai:simple"
|
||||
msgstr "Modéré"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Plays moves that lead to clarity on stones' status, favouring solid moves "
|
||||
"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
|
||||
"points and with at least 'min_visits', prefer moves that lose fewer points, "
|
||||
"settle own stones more (with importance settled_weight), settle opponent's "
|
||||
"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
|
||||
"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
|
||||
" attachment_penalty). Weights can be negative to make it complicate the game"
|
||||
" instead, and wide root noise (in general settings) can be used to further "
|
||||
"play according to the style at the cost of strength. "
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Stones Style"
|
||||
|
||||
msgid "analysis:local"
|
||||
msgstr "Proposer des coups locaux"
|
||||
|
||||
msgid "local analysis"
|
||||
msgstr "Recherche de coups locaux."
|
||||
|
||||
@@ -776,3 +752,81 @@ msgstr ""
|
||||
|
||||
msgid "Make Main Branch"
|
||||
msgstr "Mettre en branche principale"
|
||||
|
||||
msgid "stats on top move"
|
||||
msgstr "Données des coups recommandés :"
|
||||
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "Variation pronostiq."
|
||||
|
||||
msgid "top_move_visits"
|
||||
msgstr "Nombre de Visites"
|
||||
|
||||
msgid "top_move_score"
|
||||
msgstr "Score"
|
||||
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(Néant)"
|
||||
|
||||
msgid "top_move_winrate"
|
||||
msgstr "Pronostic"
|
||||
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "Variation du score"
|
||||
|
||||
msgid "show stats if"
|
||||
msgstr "Nombre de visites minimum requis pour afficher les données :"
|
||||
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "Sauvegarder un cache SGF de l'analyse"
|
||||
|
||||
msgid "menu:save-as"
|
||||
msgstr "Enregistrer sous..."
|
||||
|
||||
msgid "Load File"
|
||||
msgstr "Charger"
|
||||
|
||||
msgid "save sgf title"
|
||||
msgstr "Sauvegarder un fichier SGF"
|
||||
|
||||
msgid "Save File"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
msgid "starting insert mode"
|
||||
msgstr "Pour clore/valider l'insertion des coups, veuillez appuyer sur 'i'."
|
||||
|
||||
msgid "ending insert mode"
|
||||
msgstr ""
|
||||
"L'insertion a été effectuée dans une nouvelle branche complétée par "
|
||||
"{num_copied} coup(s) d'origine."
|
||||
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr ""
|
||||
"Il est impossible de parcourir l'arbre de jeu tant que l'insertion des coups"
|
||||
" n'est pas close/validée (en appuyant sur 'i')."
|
||||
|
||||
msgid "player names"
|
||||
msgstr "Nom des joueurs"
|
||||
|
||||
msgid "analysis:insert"
|
||||
msgstr "Insérer des coups"
|
||||
|
||||
msgid "analysis:region:start"
|
||||
msgstr ""
|
||||
"Cliquez et glissez pour sélectionner la région à analyser en priorité. \n"
|
||||
"Appuyez sur 'g' puis sélectionnez une intersection pour tout annuler."
|
||||
|
||||
msgid "analysis:region"
|
||||
msgstr "Définir la zone d'intérêt"
|
||||
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "Nouer/Dénouer la branche"
|
||||
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "Simuler la fin de partie"
|
||||
|
||||
msgid "black player name hint"
|
||||
msgstr "Noir"
|
||||
|
||||
msgid "white player name hint"
|
||||
msgstr "Blanc"
|
||||
@@ -11,7 +11,7 @@ msgid "menu:playersetup"
|
||||
msgstr "対局者設定"
|
||||
|
||||
msgid "menu:newgame"
|
||||
msgstr "新規対局"
|
||||
msgstr "新規対局・ルール変更"
|
||||
|
||||
msgid "menu:save"
|
||||
msgstr "棋譜保存"
|
||||
@@ -452,7 +452,7 @@ msgstr "AI設定を更新"
|
||||
msgid "sgf written"
|
||||
msgstr "分析結果入りSGFを{file_name}に保存"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "初期分析が完了しないと均等分析はできません"
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -464,7 +464,7 @@ msgstr ""
|
||||
"クリップボードの内容:{contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "SGFのロードに失敗しました。Parse Error: {error}"
|
||||
msgstr "SGFのロードに失敗しました: {error}"
|
||||
|
||||
msgid "Kata exe not found"
|
||||
msgstr "KataGoの実行ファイル {exe} がありません。一般設定を確認してください。"
|
||||
@@ -658,11 +658,6 @@ msgstr ""
|
||||
"KataGoの起動に失敗しました. DLLがみつかりません. 必要なものをKataGoの説明書で確認するか, "
|
||||
"KataGo実行ファイル自体を直接実行して詳細を調べてください."
|
||||
|
||||
#. in addition to colour, show point loss as -0.1 etc on hints/top
|
||||
#. moves
|
||||
msgid "show point loss as text"
|
||||
msgstr "損失目数を候補手上に表示"
|
||||
|
||||
#. resign the game
|
||||
msgid "resign"
|
||||
msgstr "投了"
|
||||
@@ -703,22 +698,6 @@ msgstr ""
|
||||
msgid "ai:simple"
|
||||
msgstr "簡明派"
|
||||
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"複雑なサバキよりも, 石の生死をはっきりさせる堅実な手を好みます. 具体的には, 次の基準で手を選びます (括弧内は重視度): 損する目数が少ない, "
|
||||
"自分の生死が明確 (settled_weight), 相手の生死も明確 (settled_weight × opponent fac), "
|
||||
"相手が打った手の近辺…縦横とも3間以内 (tenuki_penalty), 相手の石に接触しない (attachment_penalty). ただし, "
|
||||
"損が大きすぎたり (max_points_lost目以上) 探索手数が少なすぎたり (min_visits手未満) する候補手は除きます. "
|
||||
"重視度を負にすれば, 逆に盤面の複雑化をめざします. また, 一般設定のwide root noiseを大きくすれば, 強さは犠牲になりますが, "
|
||||
"より前述の基準に忠実な打ち方をします."
|
||||
|
||||
msgid "ai:settle"
|
||||
msgstr "堅実派"
|
||||
|
||||
#. Analyze again but allow only nearby moves
|
||||
msgid "analysis:local"
|
||||
msgstr "近辺の応手に絞って分析"
|
||||
|
||||
#. status message
|
||||
msgid "local analysis"
|
||||
msgstr "近辺の応手を分析中"
|
||||
@@ -731,3 +710,100 @@ msgstr ""
|
||||
#. on move tree editing
|
||||
msgid "Make Main Branch"
|
||||
msgstr "こちらを主分岐に"
|
||||
|
||||
#. setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "候補手上の表示"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "勝率の損失"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "探索手数"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "目差"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "なし"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "勝率"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "損失目数"
|
||||
|
||||
#. top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "表示候補の最低探索数"
|
||||
|
||||
#. analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "分析結果をSGFに保存"
|
||||
|
||||
msgid "menu:save-as"
|
||||
msgstr "名前をつけて棋譜保存"
|
||||
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "読込"
|
||||
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "SGFファイルを保存"
|
||||
|
||||
# sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "保存"
|
||||
|
||||
#. status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "挿入モードを開始します。済んだらiを押してください。"
|
||||
|
||||
#. status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "入力された手順を挿入した上で、後続の{num_copied}手をコピーしました。"
|
||||
|
||||
#. when a user tries to go to a different move/node while in insert
|
||||
#. mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr "挿入モード中は他の手に移動できません。iを押して挿入モードを終了してください。"
|
||||
|
||||
#. in new game options
|
||||
msgid "player names"
|
||||
msgstr "対局者名"
|
||||
|
||||
#. Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "棋譜の途中に手を追加"
|
||||
|
||||
#. Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr ""
|
||||
"マウスをドラッグして分析する領域を設定してください。領域を解除したいときは、このメッセージが表示されている状態で碁盤上のどこか一点だけを選んでください。"
|
||||
|
||||
#. Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "領域を絞って分析"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "折畳み/展開"
|
||||
|
||||
#. Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "終局まで簡易自動対局"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "黒番"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "白番"
|
||||
@@ -424,7 +424,7 @@ msgstr "인공지능 설정 갱신"
|
||||
msgid "sgf written"
|
||||
msgstr "SGF와 분석 결과를 {file_name}에 기록했습니다."
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "추가적인 분석을 요청하기 전에, 먼저 일차적인 분석이 끝날 때까지 기다려 주십시오."
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -436,7 +436,7 @@ msgstr ""
|
||||
"클립보드 내용: {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "SGF 파일을 불러올 수 없습니다. Parse Error: {error}"
|
||||
msgstr "SGF 파일을 불러올 수 없습니다: {error}"
|
||||
|
||||
msgid "Kata exe not found"
|
||||
msgstr "KataGo 실행 파일 {exe}이 존재하지 않습니다. 일반 설정을 확인해 주십시오."
|
||||
@@ -638,9 +638,6 @@ msgid "Engine missing DLL"
|
||||
msgstr ""
|
||||
"없는 DLL 파일이 있어 KataGo가 실행되지 않습니다. 요구사항을 점검하시거나, 실행 파일을 따로 실행해 세부사항을 알아보십시오."
|
||||
|
||||
msgid "show point loss as text"
|
||||
msgstr "돌 위에 집 손해 표시하기"
|
||||
|
||||
msgid "resign"
|
||||
msgstr "기권"
|
||||
|
||||
@@ -674,9 +671,6 @@ msgstr ""
|
||||
msgid "ai:simple"
|
||||
msgstr "간명한 기풍"
|
||||
|
||||
msgid "analysis:local"
|
||||
msgstr "국지적인 응수 찾기"
|
||||
|
||||
msgid "local analysis"
|
||||
msgstr "국지적인 응수를 찾고 있습니다"
|
||||
|
||||
@@ -685,22 +679,101 @@ msgstr ""
|
||||
"이 판에 대해\n"
|
||||
"덤과 규칙 등 조절"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "ai:settle"
|
||||
msgstr "Settle Style"
|
||||
|
||||
#. TODO - DEPRECATED
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Plays moves that lead to clarity on stones' status, favouring solid moves "
|
||||
"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
|
||||
"points and with at least 'min_visits', prefer moves that lose fewer points, "
|
||||
"settle own stones more (with importance settled_weight), settle opponent's "
|
||||
"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
|
||||
"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
|
||||
" attachment_penalty). Weights can be negative to make it complicate the game"
|
||||
" instead, and wide root noise (in general settings) can be used to further "
|
||||
"play according to the style at the cost of strength. "
|
||||
|
||||
msgid "Make Main Branch"
|
||||
msgstr "주 변화로 만들기"
|
||||
|
||||
#. setting: what to show on top move (e.g. point loss, visits, ...)
|
||||
msgid "stats on top move"
|
||||
msgstr "후보수 위에 표시할 정보"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "승률 변동"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_visits"
|
||||
msgstr "Visit수"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_score"
|
||||
msgstr "집 차이"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_nothing"
|
||||
msgstr "(없음)"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_winrate"
|
||||
msgstr "승률"
|
||||
|
||||
#. Dropdown menu option
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "집 변동"
|
||||
|
||||
#. top move stats show threshold description
|
||||
msgid "show stats if"
|
||||
msgstr "... visit 이상일 때만 정보 보이기"
|
||||
|
||||
#. analysis option
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "분석 결과 SGF 파일에 캐싱"
|
||||
|
||||
msgid "menu:save-as"
|
||||
msgstr "이 판을 다음으로 저장..."
|
||||
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "파일 불러오기"
|
||||
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "SGF 파일 저장하기"
|
||||
|
||||
# sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "파일 저장하기"
|
||||
|
||||
#. status message on starting insert mode
|
||||
msgid "starting insert mode"
|
||||
msgstr "삽입 모드를 시작하고 있습니다. 삽입을 끝내려면 'i'를 누르십시오."
|
||||
|
||||
#. status message on ending insert mode and copying the moves
|
||||
msgid "ending insert mode"
|
||||
msgstr "수순을 삽입하고, {num_copied}개의 수를 복사했습니다."
|
||||
|
||||
#. when a user tries to go to a different move/node while in insert
|
||||
#. mode
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr "삽입 모드에서는 탐색할 수 없습니다. 삽입을 끝내려면 'i'를 누르십시오. "
|
||||
|
||||
#. in new game options
|
||||
msgid "player names"
|
||||
msgstr "대국자"
|
||||
|
||||
#. Option in dropdown to insert some moves between the current and next
|
||||
msgid "analysis:insert"
|
||||
msgstr "기보 중간에 수 추가"
|
||||
|
||||
#. Status message
|
||||
msgid "analysis:region:start"
|
||||
msgstr "드래그해서 분석할 범위를 지정하십시오."
|
||||
|
||||
#. Restrict analysis to a specific region
|
||||
msgid "analysis:region"
|
||||
msgstr "분석할 범위 지정"
|
||||
|
||||
#. on move tree editing
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "펼치기/접기"
|
||||
|
||||
#. Menu option
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "종국까지 빠르게 알아서 두기"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "black player name hint"
|
||||
msgstr "흑 대국자"
|
||||
|
||||
#. hint on player name input
|
||||
msgid "white player name hint"
|
||||
msgstr "백 대국자"
|
||||
@@ -26,7 +26,7 @@ msgid "menu:clocksettings"
|
||||
msgstr "Контроль времени"
|
||||
|
||||
msgid "menu:teachsettings"
|
||||
msgstr "Настройки обучающей игры"
|
||||
msgstr "Настройки анализа"
|
||||
|
||||
msgid "menu:aisettings"
|
||||
msgstr "Настройки ИИ"
|
||||
@@ -325,7 +325,7 @@ msgstr "Начать новую игру"
|
||||
|
||||
# teacher settings from here
|
||||
msgid "teacher settings"
|
||||
msgstr "Редактировать настройки учителя"
|
||||
msgstr "Редактировать настройки анализа"
|
||||
|
||||
msgid "point loss threshold"
|
||||
msgstr "Потеря очков более"
|
||||
@@ -360,7 +360,7 @@ msgstr ""
|
||||
|
||||
#. submit button
|
||||
msgid "update teacher"
|
||||
msgstr "Сохранить настройки обратной связи"
|
||||
msgstr "Сохранить настройки анализа"
|
||||
|
||||
# main config settings
|
||||
msgid "restarting engine"
|
||||
@@ -450,7 +450,7 @@ msgstr "Сохранить настройки ИИ"
|
||||
msgid "sgf written"
|
||||
msgstr "SGF с анализом сохранён в {file_name}"
|
||||
|
||||
msgid "wait-before-equalize"
|
||||
msgid "wait-before-extra-analysis"
|
||||
msgstr "Дождитесь начального анализа перед запросом уточнений."
|
||||
|
||||
msgid "Copied SGF to clipboard."
|
||||
@@ -462,7 +462,7 @@ msgstr ""
|
||||
"Содержимое буфера: {contents}..."
|
||||
|
||||
msgid "Failed to load SGF"
|
||||
msgstr "Ошибка загрузки SGF. Ошибка парсинга: {error}"
|
||||
msgstr "Ошибка загрузки SGF: {error}"
|
||||
|
||||
msgid "Kata exe not found"
|
||||
msgstr ""
|
||||
@@ -673,9 +673,6 @@ msgstr ""
|
||||
"Не удалось запустить KataGo из-за отсутствующих DLL. Проверьте требования "
|
||||
"или запустите бинарный файл сам по себе для получения подробностей."
|
||||
|
||||
msgid "show point loss as text"
|
||||
msgstr "Показывать потерянные очки текстом"
|
||||
|
||||
msgid "resign"
|
||||
msgstr "Сдаться"
|
||||
|
||||
@@ -709,25 +706,6 @@ msgstr ""
|
||||
msgid "ai:simple"
|
||||
msgstr "Простой стиль"
|
||||
|
||||
msgid "aihelp:settle"
|
||||
msgstr ""
|
||||
"Делает ходы, ведущие к ясности в статусе камней, предпочитая надёжные ходы, "
|
||||
"нежели сложное сабаки. Из ходов, теряющих не более чем 'max_points_lost' "
|
||||
"очков и имеющих как минимум 'min_visits', предпочитает ходы, укрепляющие "
|
||||
"собственную территорию (коэффициент settled_weight), укрепляющие территорию "
|
||||
"оппонента (коэффициент settled_weight * opponent_fac), избегает тэнуки "
|
||||
"(более 5 пунктов, коэффициент tenuki_penalty), и избегает прилипаний "
|
||||
"(коэффициент attachment_penalty). Коэффициенты могут быть отрицательными, "
|
||||
"чтобы напротив сделать игру сложнее. 'Уровень шума' (в 'Настройках системы')"
|
||||
" может быть использован для увеличения вариативности игры за счёт небольшой "
|
||||
"потери силы. "
|
||||
|
||||
msgid "ai:settle"
|
||||
msgstr "Надёжный стиль"
|
||||
|
||||
msgid "analysis:local"
|
||||
msgstr "Найти локальные ответы"
|
||||
|
||||
msgid "local analysis"
|
||||
msgstr "Поиск локальных ответов"
|
||||
|
||||
@@ -738,3 +716,80 @@ msgstr ""
|
||||
|
||||
msgid "Make Main Branch"
|
||||
msgstr "Сделать главной веткой"
|
||||
|
||||
msgid "stats on top move"
|
||||
msgstr "Показывать числа поверх ходов"
|
||||
|
||||
msgid "top_move_delta_winrate"
|
||||
msgstr "Изменение процента"
|
||||
|
||||
msgid "top_move_visits"
|
||||
msgstr "Посещения"
|
||||
|
||||
msgid "top_move_score"
|
||||
msgstr "Счёт"
|
||||
|
||||
msgid "top_move_nothing"
|
||||
msgstr "-"
|
||||
|
||||
msgid "top_move_winrate"
|
||||
msgstr "Процент побед"
|
||||
|
||||
msgid "top_move_delta_score"
|
||||
msgstr "Изменение счёта"
|
||||
|
||||
msgid "show stats if"
|
||||
msgstr "Числа только если посещений больше чем"
|
||||
|
||||
msgid "cache analysis to sgf"
|
||||
msgstr "Кэшировать анализ в SGF"
|
||||
|
||||
msgid "menu:save-as"
|
||||
msgstr "Сохранить игру как..."
|
||||
|
||||
# sgf load button
|
||||
msgid "Load File"
|
||||
msgstr "Загрузить файл"
|
||||
|
||||
# sgf save popup
|
||||
msgid "save sgf title"
|
||||
msgstr "Сохранить SGF файл"
|
||||
|
||||
# sgf save as button
|
||||
msgid "Save File"
|
||||
msgstr "Сохранить файл"
|
||||
|
||||
msgid "starting insert mode"
|
||||
msgstr "Активен режим вставки, нажмите 'i' для завершения."
|
||||
|
||||
msgid "ending insert mode"
|
||||
msgstr "Режим вставки завершён, скопировано {num_copied} следующих ходов."
|
||||
|
||||
msgid "finish inserting before navigating"
|
||||
msgstr ""
|
||||
"Навигация отключена в режиме вставки, сначала завершите режим вставки нажав "
|
||||
"'i'."
|
||||
|
||||
msgid "player names"
|
||||
msgstr "Имена игроков"
|
||||
|
||||
msgid "analysis:insert"
|
||||
msgstr "Вставить ходы"
|
||||
|
||||
msgid "analysis:region:start"
|
||||
msgstr "Выберите регион интереса. Выберите одну точку для сброса региона."
|
||||
|
||||
msgid "analysis:region"
|
||||
msgstr "Выбрать регион интереса"
|
||||
|
||||
msgid "Toggle Collapse Branch"
|
||||
msgstr "Развернуть/Свернуть"
|
||||
|
||||
msgid "analysis:playtoend"
|
||||
msgstr "Быстро доиграть до конца"
|
||||
|
||||
msgid "black player name hint"
|
||||
msgstr "Имя игрока чёрными"
|
||||
|
||||
msgid "white player name hint"
|
||||
msgstr "Имя игрока белыми"
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -4,32 +4,29 @@
|
||||
#:import abspath os.path.abspath
|
||||
#:import kivy_version kivy.__version__
|
||||
|
||||
#:set CP_SPACING 6
|
||||
#:set CP_SMALL_SPACING 3
|
||||
#:set CP_PADDING 6
|
||||
#:import Theme katrain.gui.theme.Theme
|
||||
#:import LIGHT_GREY katrain.gui.theme.LIGHT_GREY
|
||||
|
||||
#:set INPUT_FONT_SIZE sp(25)
|
||||
#:set DESC_FONT_SIZE sp(20)
|
||||
#:set CP_SPACING Theme.CP_SPACING
|
||||
#:set CP_SMALL_SPACING Theme.CP_SMALL_SPACING
|
||||
#:set CP_PADDING Theme.CP_PADDING
|
||||
|
||||
#: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
|
||||
#:import TOP_MOVE_OPTIONS katrain.core.constants.TOP_MOVE_OPTIONS
|
||||
|
||||
|
||||
# MDTextField:
|
||||
# hint_text: "Helper text on focus"
|
||||
# helper_text: "This will disappear when you click off"
|
||||
# helper_text_mode: "on_focus"
|
||||
# font_size: INPUT_FONT_SIZE
|
||||
# font_size: sp(Theme.INPUT_FONT_SIZE)
|
||||
|
||||
<LabelledTextInput>:
|
||||
font_size: INPUT_FONT_SIZE
|
||||
font_size: sp(Theme.INPUT_FONT_SIZE)
|
||||
helper_text_mode: "none"
|
||||
hint_text: ""
|
||||
helper_text: ""
|
||||
color_mode: 'custom'
|
||||
line_color_focus: TEXT_COLOR
|
||||
line_color_focus: Theme.TEXT_COLOR
|
||||
|
||||
<LabelledPathInput>:
|
||||
font_size: sp(16)
|
||||
@@ -38,27 +35,27 @@
|
||||
helper_text: i18n._("file not found")
|
||||
|
||||
<LabelledIntInput>:
|
||||
font_size: INPUT_FONT_SIZE
|
||||
font_size: sp(Theme.INPUT_FONT_SIZE)
|
||||
size_hint: 0.5, None
|
||||
|
||||
<LabelledFloatInput>:
|
||||
font_size: INPUT_FONT_SIZE
|
||||
font_size: sp(Theme.INPUT_FONT_SIZE)
|
||||
size_hint: 0.5, None
|
||||
|
||||
<LabelledCheckBox>:
|
||||
size: DESC_FONT_SIZE*2,DESC_FONT_SIZE*2
|
||||
size: sp(Theme.DESC_FONT_SIZE)*2,sp(Theme.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
|
||||
font_size: DESC_FONT_SIZE
|
||||
size: sp(Theme.DESC_FONT_SIZE) * 2, sp(Theme.DESC_FONT_SIZE) * 2
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
|
||||
<I18NPopup>:
|
||||
background_color: BACKGROUND_COLOR
|
||||
background_color: Theme.BACKGROUND_COLOR
|
||||
background: self.background if kivy_version < '2.0' else ''
|
||||
title: i18n._(self.title_key)
|
||||
title_size: DESC_FONT_SIZE
|
||||
title_size: sp(Theme.DESC_FONT_SIZE)
|
||||
title_font: self.font_name
|
||||
size_hint: None, None
|
||||
|
||||
@@ -68,14 +65,14 @@
|
||||
padding: 4*CP_PADDING,CP_PADDING, 4*CP_PADDING, CP_PADDING
|
||||
|
||||
<DescriptionLabel>:
|
||||
font_size: DESC_FONT_SIZE
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
multiline: True
|
||||
text_size: self.width, None
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
|
||||
<SmallDescriptionLabel@DescriptionLabel>
|
||||
font_size: DESC_FONT_SIZE * 0.66
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.66
|
||||
|
||||
<ConfigPopup>:
|
||||
configfile: configfile
|
||||
@@ -87,7 +84,7 @@
|
||||
katago_download_progress_box: katago_download_progress_box
|
||||
DescriptionLabel:
|
||||
text: i18n._('katago settings')
|
||||
font_size: DESC_FONT_SIZE * 1.5
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 1.5
|
||||
GridLayout:
|
||||
cols: 2
|
||||
rows: 6
|
||||
@@ -122,7 +119,7 @@
|
||||
size_hint_y: 0.7
|
||||
sync_height_frac: 1.0
|
||||
-font_size: self.height * 0.5
|
||||
-background_color: [*[c*255/88 for c in BOX_BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
-background_color: [*[c*255/88 for c in Theme.BOX_BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
DescriptionLabel:
|
||||
text: i18n._("engine:config")
|
||||
size_hint: 0.33, 1
|
||||
@@ -157,7 +154,7 @@
|
||||
size_hint_y: 0.7
|
||||
sync_height_frac: 1.0
|
||||
-font_size: self.height * 0.5
|
||||
-background_color: [*[c*255/88 for c in BOX_BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
-background_color: [*[c*255/88 for c in Theme.BOX_BACKGROUND_COLOR[:3]], 1] # compensate for texture
|
||||
DescriptionLabel:
|
||||
text: i18n._("engine:altcommand")
|
||||
size_hint: 0.33, 1
|
||||
@@ -170,22 +167,17 @@
|
||||
orientation: 'horizontal'
|
||||
DescriptionLabel:
|
||||
text: i18n._('general settings')
|
||||
font_size: DESC_FONT_SIZE * 1.5
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 1.5
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE * 1.5
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 1.5
|
||||
text: i18n._('engine settings')
|
||||
BoxLayout:
|
||||
size_hint: 1,5
|
||||
orientation: 'horizontal'
|
||||
GridLayout:
|
||||
cols: 2
|
||||
rows: 5
|
||||
rows: 4
|
||||
spacing: CP_SPACING
|
||||
DescriptionLabel:
|
||||
text: i18n._("general:sgf_save")
|
||||
AnchorLayout:
|
||||
LabelledPathInput:
|
||||
input_property: "general/sgf_save"
|
||||
DescriptionLabel:
|
||||
text: i18n._('count down sound')
|
||||
AnchorLayout:
|
||||
@@ -210,7 +202,7 @@
|
||||
id: configfile
|
||||
text: app.gui.config_file
|
||||
BackgroundMixin:
|
||||
background_color: LIGHTGREY
|
||||
background_color: LIGHT_GREY
|
||||
size_hint: None, 1
|
||||
width: 2
|
||||
GridLayout:
|
||||
@@ -278,40 +270,56 @@
|
||||
DescriptionLabel:
|
||||
text: i18n._("save dots")
|
||||
AnchorLayout:
|
||||
size_hint: 1, 3
|
||||
size_hint: 1, 4
|
||||
GridLayout:
|
||||
cols: 2
|
||||
rows: 4
|
||||
rows: 5
|
||||
padding: 4
|
||||
size_hint: 0.9, 1
|
||||
size_hint: 0.95, 1
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE * 0.8
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
|
||||
text: i18n._("theme")
|
||||
AnchorLayout:
|
||||
LabelledSpinner:
|
||||
size_hint: 0.95,0.8
|
||||
input_property: 'trainer/theme'
|
||||
font_size: DESC_FONT_SIZE * 0.8
|
||||
id: themes_spinner
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE * 0.8
|
||||
text: i18n._("show last n dots")
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
|
||||
text: i18n._("stats on top move")
|
||||
AnchorLayout:
|
||||
BoxLayout:
|
||||
size_hint: 1, 0.8
|
||||
orientation: 'horizontal'
|
||||
LabelledSpinner:
|
||||
id: top_moves_show
|
||||
value_refs: TOP_MOVE_OPTIONS
|
||||
input_property: "trainer/top_moves_show"
|
||||
Label:
|
||||
size_hint: 0.1, 1
|
||||
text: '&'
|
||||
LabelledSpinner:
|
||||
id: top_moves_show_secondary
|
||||
value_refs: TOP_MOVE_OPTIONS
|
||||
input_property: "trainer/top_moves_show_secondary"
|
||||
DescriptionLabel:
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
|
||||
text: i18n._("show stats if")
|
||||
AnchorLayout:
|
||||
LabelledIntInput:
|
||||
size_hint_x: 0.15
|
||||
input_property: "trainer/eval_off_show_last"
|
||||
input_property: "trainer/low_visits"
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE * 0.8
|
||||
text: i18n._("show point loss as text")
|
||||
AnchorLayout:
|
||||
LabelledCheckBox:
|
||||
input_property: "trainer/text_point_loss"
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE * 0.8
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
|
||||
text: i18n._("show ai dots")
|
||||
AnchorLayout:
|
||||
LabelledCheckBox:
|
||||
input_property: "trainer/eval_show_ai"
|
||||
DescriptionLabel:
|
||||
font_size: sp(Theme.DESC_FONT_SIZE) * 0.8
|
||||
text: i18n._("cache analysis to sgf")
|
||||
AnchorLayout:
|
||||
LabelledCheckBox:
|
||||
input_property: "trainer/save_analysis"
|
||||
AnchorLayout:
|
||||
size_hint: 1,1.5
|
||||
AutoSizedRoundedRectangleButton:
|
||||
@@ -363,16 +371,34 @@
|
||||
<NewGamePopup>:
|
||||
restart: restart
|
||||
rules_spinner: rules_spinner
|
||||
km: km
|
||||
player_setup: player_setup
|
||||
player_name: {"W": white_player_name, "B": black_player_name}
|
||||
PlayerSetupBlock:
|
||||
size_hint: 1, 2
|
||||
id: player_setup
|
||||
GridLayout:
|
||||
rows: 6
|
||||
rows: 7
|
||||
cols: 3
|
||||
size_hint: 1, 2.5
|
||||
size_hint: 1, 3
|
||||
spacing: CP_SPACING
|
||||
padding: CP_PADDING
|
||||
DescriptionLabel:
|
||||
text: i18n._("player names")
|
||||
AnchorLayout:
|
||||
LabelledTextInput:
|
||||
text: ''
|
||||
id: black_player_name
|
||||
write_tab: False
|
||||
hint_text: i18n._("black player name hint")
|
||||
size_hint: 0.75, None
|
||||
AnchorLayout:
|
||||
LabelledTextInput:
|
||||
text: ''
|
||||
id: white_player_name
|
||||
write_tab: False
|
||||
hint_text: i18n._("white player name hint")
|
||||
size_hint: 0.75, None
|
||||
DescriptionLabel:
|
||||
text: i18n._("board size")
|
||||
AnchorLayout:
|
||||
@@ -380,6 +406,7 @@
|
||||
text: '19'
|
||||
id: boardsize
|
||||
input_property: 'game/size'
|
||||
write_tab: False
|
||||
size_hint: 0.5, None
|
||||
hint_text: i18n._("non square board hint")
|
||||
AnchorLayout:
|
||||
@@ -401,6 +428,7 @@
|
||||
LabelledIntInput:
|
||||
text: '0'
|
||||
input_property: 'game/handicap'
|
||||
write_tab: False
|
||||
id: handicap
|
||||
AnchorLayout:
|
||||
MDBoxLayout:
|
||||
@@ -444,7 +472,7 @@
|
||||
LabelledSpinner:
|
||||
size_hint: 0.8,0.8
|
||||
input_property: 'game/rules'
|
||||
font_size: DESC_FONT_SIZE
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
id: rules_spinner
|
||||
Label:
|
||||
DescriptionLabel:
|
||||
@@ -481,7 +509,7 @@
|
||||
size_hint: 2.25,1
|
||||
values: root.values
|
||||
on_change: textbox.text = str(slider.value)
|
||||
track_color: LIGHTGREY
|
||||
track_color: LIGHT_GREY
|
||||
thumb_color: (0.6, 0.7, 0.9, 1) if root.key_option else (0.3, 0.4, 0.6, 1)
|
||||
LabelledFloatInput:
|
||||
id: textbox
|
||||
@@ -494,7 +522,7 @@
|
||||
BoxLayout:
|
||||
size_hint: 1,1
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
text: i18n._('Select AI')
|
||||
AnchorLayout:
|
||||
I18NSpinner:
|
||||
@@ -503,12 +531,12 @@
|
||||
BoxLayout:
|
||||
size_hint: 1,1
|
||||
DescriptionLabel:
|
||||
font_size: DESC_FONT_SIZE
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
size_hint: 1, 1
|
||||
text: i18n._('estimated strength')
|
||||
DescriptionLabel:
|
||||
size_hint: 0.8, 1
|
||||
font_size: DESC_FONT_SIZE
|
||||
font_size: sp(Theme.DESC_FONT_SIZE)
|
||||
id: estimated_rank_label
|
||||
text: '?' + i18n._('strength:kyu')
|
||||
SmallDescriptionLabel:
|
||||
@@ -583,3 +611,13 @@
|
||||
filters: ["*.sgf","*.gib","*.ngf"]
|
||||
path: "."
|
||||
size_hint: 1,7
|
||||
|
||||
<SaveSGFPopup>:
|
||||
filesel: filesel
|
||||
orientation: 'vertical'
|
||||
I18NFileBrowser:
|
||||
id: filesel
|
||||
multiselect: False
|
||||
filters: ["*.sgf","*.gib","*.ngf"]
|
||||
path: "."
|
||||
size_hint: 1,7
|
||||
@@ -1,14 +1,12 @@
|
||||
certifi==2020.6.20
|
||||
certifi==2020.12.5
|
||||
chardet==3.0.4
|
||||
Cython==0.29.14
|
||||
dataclasses==0.6
|
||||
docutils==0.16
|
||||
ffpyplayer==4.3.2
|
||||
idna==2.10
|
||||
Kivy==2.0.0rc2
|
||||
Kivy==2.0.0
|
||||
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
|
||||
Pillow==8.0.1
|
||||
Pygments==2.7.3
|
||||
requests==2.25.0
|
||||
urllib3==1.26.2
|
||||
|
Before Width: | Height: | Size: 688 KiB After Width: | Height: | Size: 864 KiB |
|
Before Width: | Height: | Size: 747 KiB |
@@ -1,11 +1,11 @@
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
package_data = {"": ["*.json", "*.kv", "*.wav"], "katrain": [], "tests": []}
|
||||
packages = find_packages(exclude=["tests"])
|
||||
version = re.search('^VERSION\s*=\s*"(.*)"', open("katrain/core/constants.py").read(), re.M).group(1)
|
||||
version = re.search(r'^VERSION\s*=\s*"(.*)"', open("katrain/core/constants.py").read(), re.M).group(1)
|
||||
|
||||
|
||||
def include_data_files(directory):
|
||||
@@ -21,7 +21,7 @@ include_data_files("katrain/models")
|
||||
include_data_files("katrain/fonts")
|
||||
include_data_files("katrain/sounds")
|
||||
include_data_files("katrain/img/")
|
||||
include_data_files("katrain/img/flaticon")
|
||||
include_data_files("katrain/img/flags")
|
||||
include_data_files("katrain/i18n")
|
||||
|
||||
print(packages, package_data)
|
||||
@@ -41,26 +41,22 @@ setup(
|
||||
install_requires=[
|
||||
"wheel",
|
||||
"setuptools",
|
||||
"urllib3",
|
||||
"importlib_resources ;python_version<'3.7'",
|
||||
"cython>=0.24,<=0.29.14,!=0.27,!=0.27.2", # kivy wants this
|
||||
"kivy==2.0.0rc2;platform_system=='Darwin'", # rc3 failing on mac
|
||||
"kivy>=2.0.0rc2;platform_system!='Darwin'", # just use the latest on linux?
|
||||
"pygame;platform_system=='Darwin'", # some mac versions need this for kivy
|
||||
"kivy_deps.glew;platform_system=='Windows'",
|
||||
"kivy_deps.sdl2;platform_system=='Windows'",
|
||||
"kivy_deps.gstreamer;platform_system=='Windows'",
|
||||
"kivy[full]>=2.0.0",
|
||||
"kivymd>=0.104.1",
|
||||
"importlib_resources ;python_version<'3.7'",
|
||||
"urllib3",
|
||||
"pygame;platform_system=='Darwin'", # some mac versions need this for kivy
|
||||
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
|
||||
],
|
||||
dependency_links=["https://kivy.org/downloads/simple/"],
|
||||
python_requires=">=3.6, <4",
|
||||
entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Games/Entertainment :: Board Games",
|
||||
],
|
||||
packages=packages,
|
||||
package_data=package_data,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import pytest
|
||||
import os
|
||||
from katrain.core.ai import generate_ai_move, ai_rank_estimation
|
||||
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, AI_STRATEGIES, OUTPUT_INFO
|
||||
|
||||
import pytest
|
||||
|
||||
from katrain.core.ai import ai_rank_estimation, generate_ai_move
|
||||
from katrain.core.base_katrain import KaTrainBase
|
||||
from katrain.core.constants import AI_STRATEGIES, AI_STRATEGIES_RECOMMENDED_ORDER, OUTPUT_INFO
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.game import Game
|
||||
from katrain.core.constants import AI_STRATEGIES
|
||||
|
||||
|
||||
class TestAI:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from katrain.core.base_katrain import KaTrainBase
|
||||
from katrain.core.game import Game, IllegalMoveException, Move
|
||||
from katrain.core.base_katrain import KaTrainBase, OUTPUT_INFO
|
||||
from katrain.core.game_node import GameNode
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from katrain.core.base_katrain import KaTrainBase
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.game import Game, KaTrainSGF
|
||||
from katrain.core.sgf_parser import SGF, SGFNode
|
||||
|
||||
@@ -107,7 +106,7 @@ def test_old_server_style():
|
||||
|
||||
def test_ogs():
|
||||
file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf")
|
||||
tree = SGF.parse_file(file)
|
||||
SGF.parse_file(file)
|
||||
|
||||
|
||||
def test_gibo():
|
||||
@@ -153,3 +152,16 @@ def test_foxwq():
|
||||
while game.current_node.children:
|
||||
assert 1 == len(game.current_node.children)
|
||||
game.redo(1)
|
||||
|
||||
|
||||
def test_next_player():
|
||||
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb])"
|
||||
assert "B" == SGF.parse_sgf(input_sgf).next_player
|
||||
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb]PL[B])"
|
||||
assert "B" == SGF.parse_sgf(input_sgf).next_player
|
||||
input_sgf = "(;GM[1]FF[4]AB[aa]AW[bb]PL[W])"
|
||||
assert "W" == SGF.parse_sgf(input_sgf).next_player
|
||||
input_sgf = "(;GM[1]FF[4]AB[aa])"
|
||||
assert "W" == SGF.parse_sgf(input_sgf).next_player
|
||||
input_sgf = "(;GM[1]FF[4]AB[aa]PL[B])"
|
||||
assert "B" == SGF.parse_sgf(input_sgf).next_player
|
||||