Merge pull request #242 from sanderland/1.7

1.7
This commit is contained in:
sanderland authored and GitHub committed 2020-12-19 04:04:36 +09:00
commit 3bec7ac53e
71 files changed
+2528 -1253

No files matched your search

+3
View File
@@ -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 -14
View File
@@ -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)
@@ -20,19 +28,6 @@ Adding a translation requires making a new `.po` file with entries for that lang
You can send me the resulting `.po` file and I will integrate it into the program.
For those who have some python experience as well, you can:
* Replicate the directory structure `katrain/i18n/<lang>/LC_MESSAGES/katrain.po`.
* Run `python i18.py` which will generate the `.mo` files and check for any errors.
* Find `gui.kv` and add a button near the others:
```
LangButton:
icon: 'img/flaticon/flag-<lang>.png'
on_press: app.language = '<lang>'
```
* The language should now show up in the app.
* Pull request your changes to the latest minor version branch.
# Contributors
## Primary author and project maintainer:
@@ -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
+5 -4
View File
@@ -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.
+2 -2
View File
@@ -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.
+49 -45
View File
@@ -1,11 +1,11 @@
# <a name="manual"></a> KaTrain
[![Latest Release](https://img.shields.io/github/release/sanderland/katrain?label=download)](https://github.com/sanderland/katrain/releases)
[![License:MIT](https://img.shields.io/pypi/l/katrain)](https://en.wikipedia.org/wiki/MIT_License)
[![GitHub Downloads](https://img.shields.io/github/downloads/sanderland/katrain/total?color=%23336699&label=github%20downloads)](https://github.com/sanderland/katrain/releases)
[![PyPI Downloads](https://pepy.tech/badge/katrain)](https://pepy.tech/project/katrain)
[![Github sponsors](https://img.shields.io/static/v1?label=sponsor&message=7&logo=GitHub&color=dcb424&link=https://github.com/sponsors/sanderland/)](https://github.com/sponsors/sanderland)
[![Discord](https://img.shields.io/discord/417022162348802048?logo=discord)](https://discord.com/channels/417022162348802048/629446365688365067)
[![Latest Release](http://img.shields.io/github/release/sanderland/katrain?label=download)](http://github.com/sanderland/katrain/releases)
[![License:MIT](http://img.shields.io/pypi/l/katrain)](http://en.wikipedia.org/wiki/MIT_License)
[![GitHub Downloads](http://img.shields.io/github/downloads/sanderland/katrain/total?color=%23336699&label=github%20downloads)](http://github.com/sanderland/katrain/releases)
[![PyPI Downloads](http://pepy.tech/badge/katrain)](http://pepy.tech/project/katrain)
[![Github sponsors](http://img.shields.io/static/v1?label=sponsor&message=%E2%9D%A4&logo=GitHub&color=dcb424&link=http://github.com/sponsors/sanderland/)](http://github.com/sponsors/sanderland)
[![Discord](http://img.shields.io/discord/417022162348802048?logo=discord)](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
![screenshot](http://raw.githubusercontent.com/sanderland/katrain/master/screenshots/analysis.png)
![screenshot](https://raw.githubusercontent.com/sanderland/katrain/master/screenshots/teaching.gif)
### Analyze your games
![screenshot](https://raw.githubusercontent.com/sanderland/katrain/master/screenshots/analysis.png)
### YouTube videos
| **New Features in v1.4** | **Analysis Tutorial** | **Teaching Game Tutorial** |
| **Local Joseki Analysis** | **Analysis Tutorial** | **Teaching Game Tutorial** |
|:-----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------:|
| [![New Features Video](https://i.imgur.com/IXNwTHL.png)](https://www.youtube.com/watch?v=ujjRWGSZJrQ&feature=youtu.be) | [![Analysis Tutorial](https://i.imgur.com/3EP4IEr.png)](https://www.youtube.com/watch?v=qjxkcKgrsbU) | [![ Teaching Game Tutorial](https://i.imgur.com/jAdcSL5.png)](https://www.youtube.com/watch?v=wFl4Bab_eGM) |
| [![Local Joseki Analysi Video](http://i.imgur.com/YcpmSBx.png)](http://www.youtube.com/watch?v=ujjRWGSZJrQ&feature=youtu.be) | [![Analysis Tutorial](http://i.imgur.com/3EP4IEr.png)](http://www.youtube.com/watch?v=qjxkcKgrsbU) | [![ Teaching Game Tutorial](http://i.imgur.com/jAdcSL5.png)](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
[![GitHub issues](https://img.shields.io/github/issues/sanderland/katrain)](https://github.com/sanderland/katrain/issues)
[![Contributors](https://img.shields.io/static/v1?label=contributors&message=19&color=dcb424)](CONTRIBUTIONS.md)
[![Github sponsors](https://img.shields.io/static/v1?label=sponsor&message=7&logo=GitHub&color=dcb424&link=https://github.com/sponsors/sanderland/)](https://github.com/sponsors/sanderland)
[![GitHub issues](http://img.shields.io/github/issues/sanderland/katrain)](http://github.com/sanderland/katrain/issues)
[![Contributors](http://img.shields.io/static/v1?label=contributors&message=24&color=dcb424)](CONTRIBUTIONS.md)
[![Github sponsors](http://img.shields.io/static/v1?label=sponsor&message=%E2%9D%A4&logo=GitHub&color=dcb424&link=http://github.com/sponsors/sanderland/)](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).
+11
View File
@@ -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.
+18 -3
View File
@@ -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))
+150 -46
View File
@@ -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)
+4 -10
View File
@@ -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,
+7 -7
View File
@@ -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)
+14 -3
View File
@@ -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"
+26 -2
View File
@@ -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"
+24 -21
View File
@@ -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 = []
+207 -39
View File
@@ -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
+136 -24
View File
@@ -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"]),
+3 -3
View File
@@ -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])
+44 -18
View File
@@ -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:
+20 -1
View File
@@ -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"
+135 -135
View File
@@ -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&notes'
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
+258 -112
View File
@@ -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)
+13 -8
View File
@@ -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
+45 -19
View File
@@ -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
+71 -19
View File
@@ -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)
-82
View File
@@ -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)
+145
View File
@@ -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}
+3 -3
View File
@@ -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"]
+8 -11
View File
@@ -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
@@ -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'
+17 -200
View File
@@ -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')
"""
)
+112 -47
View File
@@ -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,13 +247,21 @@ 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
@@ -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
"""
)
-2
View File
@@ -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):
Binary file not shown.
+96 -22
View File
@@ -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"
Binary file not shown.
+193 -106
View File
@@ -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&notes"
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ß"
Binary file not shown.
+101 -28
View File
@@ -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'. "
Binary file not shown.
+105 -28
View File
@@ -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"
Binary file not shown.
+81 -27
View File
@@ -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"
Binary file not shown.
+100 -24
View File
@@ -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 "白番"
Binary file not shown.
+98 -25
View File
@@ -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 "백 대국자"
Binary file not shown.
+82 -27
View File
@@ -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 "Имя игрока белыми"
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

File renamed without changes.
+94 -56
View File
@@ -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
+7 -9
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 KiB

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 KiB

+9 -13
View File
@@ -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,
+5 -4
View File
@@ -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 -1
View File
@@ -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
+15 -3
View File
@@ -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