74 files changed
+1659
-572
No files matched your search
@@ -0,0 +1,35 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.6, 3.7]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip3 install -e .
|
||||
pip3 install pytest wheel
|
||||
|
||||
- name: Run tests
|
||||
run: pytest tests
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
python3 setup.py sdist
|
||||
python3 setup.py bdist_wheel
|
||||
+3
-2
@@ -1,12 +1,13 @@
|
||||
# additions
|
||||
KataGoData
|
||||
KataGo/KataGoData
|
||||
katrain/KataGo/KataGoData
|
||||
experiments
|
||||
.idea
|
||||
.buildozer
|
||||
bin
|
||||
gtp.log
|
||||
*.zip
|
||||
log.txt
|
||||
*.sgf
|
||||
sgfout
|
||||
sgf_selfplay
|
||||
sgf_ogs
|
||||
|
||||
+30
-42
@@ -1,55 +1,43 @@
|
||||
# Installation from source for Windows users
|
||||
|
||||
* Download the repository by clicking the green *Clone or download* on this page and *Download zip*. Extract the contents.
|
||||
* Make sure you have a python installation, I will assume Anaconda (Python 3.7), available [here](https://www.anaconda.com/products/individual#download-section).
|
||||
* Open 'Anaconda prompt' from the start menu and navigate to where you extracted the zip file using the `cd <folder>` command.
|
||||
* Execute the command `pip install kivy_deps.glew kivy_deps.sdl2 kivy_deps.gstreamer kivy`
|
||||
* Start the app by running `python katrain.py` in the directory where you downloaded the scripts.
|
||||
* Note that the program can be slow to initialize the first time, due to KataGo's gpu tuning.
|
||||
|
||||
# Installation for Linux users
|
||||
|
||||
* This assumed you have a working Python 3.6/3.7 installation as a default. If your default is python 2, use pip3/python3.
|
||||
Kivy currently does not have a release for Python 3.8.
|
||||
* Open a terminal.
|
||||
* Run the command `git clone https://github.com/sanderland/katrain.git` to download the repository.
|
||||
* Run the command `pip install kivy`.
|
||||
* A binary for KataGo is included, but if you have compiled your own, point the 'engine/katago' setting to the relevant KataGo v1.4+ binary.
|
||||
* Start the app by changing directory using `cd katrain` and running `python katrain.py`.
|
||||
* Note that the program can be slow to initialize the first time, due to KataGo's GPU tuning.
|
||||
|
||||
# Installation for MacOS users
|
||||
|
||||
## Installation pre-requisites
|
||||
|
||||
## <a name="MacPrereq"></a>Installation pre-requisites
|
||||
* Download and install [Python 3.7.5](https://www.python.org/downloads/release/python-375/)
|
||||
* Install [Homebrew](https://brew.sh) by running the following command in terminal:
|
||||
* ```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
```
|
||||
* Run the command `pip3 install kivy` in the terminal.
|
||||
* Install Katago using [Homebrew](https://brew.sh/)
|
||||
* Note that the version required for KaTrain is currently too new so we need to update the Homebrew script.
|
||||
* Run the command `brew edit katago` and replace lines 4-5 with
|
||||
* ```
|
||||
url "https://github.com/lightvector/KataGo/archive/v1.4.1.tar.gz"
|
||||
sha256 "b408086c7c973ddc6144e16156907556ae5f42921b9f29dc13e6909a9e9a4787"
|
||||
```
|
||||
* You can also follow instructions [here](https://github.com/lightvector/KataGo) to compile KataGo yourself.
|
||||
* Install Katago using [Homebrew](https://brew.sh/) by executing `brew install katago`
|
||||
* You can also follow instructions [here](https://github.com/lightvector/KataGo) to compile KataGo yourself.
|
||||
|
||||
## Installation and running KaTrain
|
||||
|
||||
* Now that the dependencies are installed its time to Git clone or download the KaTrain repository
|
||||
* Run the command `git clone https://github.com/sanderland/katrain.git` this will clone KaTrain to your home folder.
|
||||
* To run Katrain you need to first access the KaTrain folder.
|
||||
* If you used the 'git clone' command to download the repository then its located in your home folder.
|
||||
You can access it by typing `cd katrain` in the terminal.
|
||||
* If you've moved the folder to another location the easiest way to navigate to it in terminal is to type `cd` and drag
|
||||
the KaTrain folder from the finder window into terminal. This will copy its full path to the command line.
|
||||
* Now that we're in the KaTrain folder run the following command. `python3 katrain.py`
|
||||
* The first time you run KaTrain you will see an error about initializing KataGo.
|
||||
## Installation and running KaTrain from PyPi
|
||||
* Run `pip3 install katrain`
|
||||
* Run the program by executing `katrain` in a terminal.
|
||||
* If you see an error about initializing KataGo:
|
||||
* Open the settings dialog by clicking on the gear icon at the bottom right of the window and change the path of the 'katago'
|
||||
setting to `/usr/local/bin/katago` (or the path where you compiled KataGo) then click 'Apply and Save'.
|
||||
setting under 'engine' to `/usr/local/bin/katago`, or the path where you compiled KataGo.
|
||||
|
||||
## Installation from sources
|
||||
* This is largely the same as for linux, see [here](#LinuxSources).
|
||||
|
||||
# Installation from sources for Windows users
|
||||
* Download the repository by clicking the green *Clone or download* on this page and *Download zip*. Extract the contents.
|
||||
* Make sure you have a python installation, I will assume Anaconda (Python 3.7), available [here](https://www.anaconda.com/products/individual#download-section).
|
||||
* Open 'Anaconda prompt' from the start menu and navigate to where you extracted the zip file using the `cd <folder>` command.
|
||||
* Execute the command `python setup.py install`
|
||||
* Start the app by running `katrain` in the directory where you downloaded the scripts.
|
||||
|
||||
# <a name="LinuxSources"></a>Installation from sources for Linux users
|
||||
|
||||
* This assumed you have a working Python 3.6/3.7 installation as a default. If your default is python 2, use pip3/python3.
|
||||
Kivy currently does not have a release for Python 3.8.
|
||||
* Open a terminal.
|
||||
* Run the command `git clone https://github.com/sanderland/katrain.git` to download the repository.
|
||||
* Changing directory using `cd katrain`.
|
||||
* Run the command `python3 setup.py install`.
|
||||
* A binary for KataGo is included, but if you have compiled your own, point the 'engine/katago' setting to the relevant KataGo v1.4+ binary.
|
||||
* Run the program by typing `katrain` in the terminal.
|
||||
|
||||
|
||||
# Configuring the GPU(s) KataGo uses
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
|
||||
[packages]
|
||||
kivy = ">=1.11"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
Generated
+178
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "63ca72eff43ccb8467fc921871c0438382996aa6fc1642e955cf1432c90ce520"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
|
||||
"sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
],
|
||||
"version": "==2020.4.5.1"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
||||
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
||||
],
|
||||
"version": "==0.16"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
],
|
||||
"version": "==2.9"
|
||||
},
|
||||
"kivy": {
|
||||
"hashes": [
|
||||
"sha256:090d3ded9835a17477cd93fbdaf0a7c42ff2218981cf198ded5ad8795bc74391",
|
||||
"sha256:11e85eaf6efbfa2362a3334ffdad179a1b0ca8d255cca79eaa6a2765560d4982",
|
||||
"sha256:1a1ff32f8a95f1e175198cbab81fcd2596783b180d4eafe63e87d171aa7fdb5e",
|
||||
"sha256:1d28b198a64c30db8d94a0488e85f3037af60d514ab0d7ad5ab45add3ab77090",
|
||||
"sha256:4a5480cbf837d3780c77a4f61b32b56d22ae9f03845e7a89dd3eaef1ae5fd037",
|
||||
"sha256:4d0e596f74271e901b551f77661dde238df4765484fce9f5d1c72e8022984e84",
|
||||
"sha256:5c3d0f2749522d62e9cce09cd54b2d823bf1b6b644ff1f627be49de6f3e3cba0",
|
||||
"sha256:815a5c0b3b72fcd81ca7b2aa0744087163ed03e4cf9ab4e7c9733cea99fc1571",
|
||||
"sha256:8819a27a09871af451760cb69486ced52e830c8a0a37480f22ef5e692f12c05b",
|
||||
"sha256:a687602d90c4629dd036f577ca39acb76ba581370f9d915f3cab99be818ba8ad",
|
||||
"sha256:b7ef6aad43a86d8df3fb865db864e354f2155a748019f8517f69f65c1a29cb64",
|
||||
"sha256:b85ccf165050cbf2ee8447671eebbc222b369b40f0e0038dd9547d49a5e37373",
|
||||
"sha256:c36652caa7f6c327dee834cfc699d5962d346b7a53e54bd81abc17c314226d89",
|
||||
"sha256:ece170514db3f49844a41e4c910ad9ce9bc46da6f47a49158e11266bdcc6e479",
|
||||
"sha256:f3bea6e4a21991827885d04127fc6d09a0e974ecfa12da7bf5faae93562ea102",
|
||||
"sha256:f835462dd9aa491272552ef079b948a088598e2e95d68bb1d885d2c3f3d4e2c3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"kivy-garden": {
|
||||
"hashes": [
|
||||
"sha256:c256f42788421273a08fbb0a228f0fb0e80dd86b629fb8c0920507f645be6c72"
|
||||
],
|
||||
"version": "==0.1.4"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
|
||||
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
|
||||
],
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
],
|
||||
"version": "==2.23.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
],
|
||||
"version": "==1.25.9"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
|
||||
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||
],
|
||||
"version": "==8.2.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
|
||||
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
|
||||
],
|
||||
"version": "==20.3"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3",
|
||||
"sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.4.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
|
||||
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
|
||||
],
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,11 @@ 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.
|
||||
|
||||
## Screenshots
|
||||
## Animated Screenshots
|
||||
|
||||
| Analyze games | Play against an AI Teacher |
|
||||
| ------------- | ------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -26,7 +26,9 @@ but has since grown to include a wide range of features, including:
|
||||
|
||||
## Installation
|
||||
* See the [releases tab](https://github.com/sanderland/katrain/releases) for pre-built installers for windows.
|
||||
* See [here](INSTALL.md) for detailed instructions for running from source files on Window, Linux and MacOS,
|
||||
* Alternatively use `pip3 install -U katrain` to install the latest version from PyPI on any OS.
|
||||
* Note that on MacOS you will need to set up KataGo using brew, as described [here](INSTALL.md).
|
||||
* See [here](INSTALL.md#MacPrereq) for detailed instructions for running from source files on Window, Linux and MacOS,
|
||||
as well as setting up KataGo to use multiple GPUs.
|
||||
|
||||
## Manual
|
||||
@@ -48,30 +50,34 @@ If you do not want to see 'Points lost' or other feedback for your moves,
|
||||
|
||||
The dots indicate how many points were lost by that move.
|
||||
|
||||
* The colour indicates the size of the mistake according to kata
|
||||
* The colour indicates the size of the mistake according to KataGo
|
||||
* The size indicates if the mistake was actually punished. Going from fully punished at maximal size,
|
||||
to no actual effect on the score at minimal size.
|
||||
|
||||
In short, if you are a weaker player you should mostly on large dots that are red or purple,
|
||||
while stronger players can pay more attention to smaller mistakes.
|
||||
while stronger players can pay more attention to smaller mistakes. If you want to hide some colours, you
|
||||
can do so under 'Configure Teacher'.
|
||||
|
||||
#### AIs
|
||||
|
||||
Available AIs, with strength indicating an estimate for the default settings, are:
|
||||
Available AIs, with strength indicating an estimate for the default settings based on their current OGS rankings, are:
|
||||
|
||||
* **[9p+]** **Default** is full KataGo, above professional level.
|
||||
* **[~1k?]** **ScoreLoss** is KataGo making moves with probability `~ e^(-strength * points lost)`, playing a varied style with small mistakes.
|
||||
* **[~5k]** **ScoreLoss** is KataGo making moves with probability `~ e^(-strength * points lost)`, playing a varied style with small mistakes.
|
||||
* **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points.
|
||||
* **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points.
|
||||
* **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used. There is a setting to increase variety in the opening, but otherwise it plays deterministically.
|
||||
* **[~2k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves.
|
||||
* **[~5k]**: **P:Pick** picks `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
|
||||
* **[~3k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves.
|
||||
* **[~7k]**: **P:Pick** picks `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
|
||||
The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders.
|
||||
This, along with 'Weighted' are probably the best choice for kyu players who want a chance of winning without playing the sillier bots below. Variants of this strategy include:
|
||||
* **[~2k]**: **P:Local** will pick such moves biased towards the last move with probability related to `local_stddev`.
|
||||
* **[~10k]**: **P:Tenuki** is biased in the opposite way as P:Local, using the same setting.
|
||||
* **[~10k]**: **P:Influence** is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight.
|
||||
* **[~10k]**: **P:Territory** is biased in the opposite way, towards 1-3rd line moves, using the same setting.
|
||||
* **[~3k]**: **P:Local** will pick such moves biased towards the last move with probability related to `local_stddev`.
|
||||
* **[~5k]**: **P:Tenuki** is biased in the opposite way as P:Local, using the same setting. After about half the board is filled, it stops and plays like P:Pick.
|
||||
* **[~6k]**: **P:Influence** is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight.
|
||||
* **[~8k]**: **P:Territory** is biased in the opposite way, towards 1-3rd line moves, using the same setting. Both of these also stop the strategy in endgame and revert to P:Pick.
|
||||
|
||||
The Engine based AIs (Default, ScoreLoss, Balance, Jigo) are affected by both the model and choice of max_visits/max_time,
|
||||
while the Policy net based AIs (Policy, P:...) are affected by the choice of model file, but work identically with 'max_visits' set to 1.
|
||||
|
||||
### Analysis
|
||||
|
||||
@@ -101,7 +107,7 @@ In addition to shortcuts mentioned above, there are:
|
||||
* **[arrow down]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
|
||||
* **[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.
|
||||
* **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
|
||||
* **[double-click on a move]**: Navigate directly to that point in the game.
|
||||
* **[Ctrl-v]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
|
||||
* **[Ctrl-c]**: Save SGF to clipboard.
|
||||
@@ -156,5 +162,5 @@ If you ever need to reset to the original settings, simply re-download the `conf
|
||||
|
||||
* Feedback and pull requests are both very welcome. I would also be happy to host translations of this manual into languages where English fluency is typically lower.
|
||||
* For suggestions and planned improvements, see the 'issues' tab on github.
|
||||
* You can also contact me on discord (Sander#3278), [KakaoTalk](https://open.kakao.com/o/gTsMJCac) or [Reddit](http://reddit.com/u/sanderbaduk) to give feedback, or simply show your appreciation.
|
||||
* You can also 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 give feedback, or simply show your appreciation.
|
||||
* Some people have also asked me how to donate. Something go-related such as a book or teaching time is highly appreciated.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6
-6
@@ -4,12 +4,12 @@ import sys
|
||||
import time
|
||||
import random
|
||||
|
||||
from core.ai import ai_move
|
||||
from core.common import OUTPUT_ERROR, OUTPUT_INFO
|
||||
from katrain.core.ai import ai_move
|
||||
from katrain.core.common import OUTPUT_ERROR, OUTPUT_INFO
|
||||
from bots.settings import bot_strategy_names
|
||||
from core.engine import EngineDiedException, KataGoEngine
|
||||
from core.game import Game
|
||||
from core.sgf_parser import Move
|
||||
from katrain.core.engine import EngineDiedException, KataGoEngine
|
||||
from katrain.core.game import Game
|
||||
from katrain.core.sgf_parser import Move
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
bot = "dev"
|
||||
@@ -42,7 +42,7 @@ ENGINE_SETTINGS = {
|
||||
|
||||
engine = KataGoEngine(logger, ENGINE_SETTINGS)
|
||||
|
||||
with open("config.json") as f:
|
||||
with open("katrain/config.json") as f:
|
||||
settings = json.load(f)
|
||||
all_ai_settings = settings["ai"]
|
||||
|
||||
|
||||
Binary file not shown.
@@ -6,8 +6,8 @@ import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from core.common import OUTPUT_INFO
|
||||
from core.engine import KataGoEngine
|
||||
from katrain.core.common import OUTPUT_INFO
|
||||
from katrain.core.engine import KataGoEngine
|
||||
|
||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8587
|
||||
|
||||
@@ -18,7 +18,7 @@ ENGINE_SETTINGS = {
|
||||
"config": "KataGo/analysis_config.cfg",
|
||||
"max_visits": 50,
|
||||
"max_time": 1.0,
|
||||
"enable_ownership": False,
|
||||
"_enable_ownership": False,
|
||||
"threads": 32,
|
||||
}
|
||||
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
# Example config for C++ (non-python) gtp bot
|
||||
|
||||
# SEE NOTES ABOUT PERFORMANCE AND MEMORY USAGE IN gtp_example.cfg
|
||||
|
||||
# Logs------------------------------------------------------------------------------------
|
||||
|
||||
# Where to output log?
|
||||
logFile = gtp.log
|
||||
|
||||
# Controls the number of moves after the first move in a variation.
|
||||
# analysisPVLen = 15
|
||||
|
||||
# Report winrates for analysis as (BLACK|WHITE|SIDETOMOVE).
|
||||
reportAnalysisWinratesAs = BLACK
|
||||
|
||||
# Bot behavior---------------------------------------------------------------------------------------
|
||||
|
||||
# Handicap -------------
|
||||
|
||||
# Assume that if black makes many moves in a row right at the start of the game, then the game is a handicap game.
|
||||
# This is necessary on some servers and for some GUIs and also when initializing from many SGF files, which may
|
||||
# set up a handicap games using repeated GTP "play" commands for black rather than GTP "place_free_handicap" commands.
|
||||
# However, it may also lead to incorrect undersanding of komi if whiteBonusPerHandicapStone = 1 and a server does NOT
|
||||
# have such a practice.
|
||||
# Defaults to true! Uncomment and set to false to disable this behavior.
|
||||
# assumeMultipleStartingBlackMovesAreHandicap = true
|
||||
|
||||
# Passing and cleanup -------------
|
||||
|
||||
# Make the bot never assume that its pass will end the game, even if passing would end and "win" under Tromp-Taylor rules.
|
||||
# Usually this is a good idea when using it for analysis or playing on servers where scoring may be implemented non-tromp-taylorly.
|
||||
# Defaults to true! Uncomment and set to false to disable this.
|
||||
conservativePass = true
|
||||
|
||||
# When using territory scoring, self-play games continue beyond two passes with special cleanup
|
||||
# rules that may be confusing for human players. This option prevents the special cleanup phases from being
|
||||
# reachable when using the bot for GTP play.
|
||||
# Defaults to true! Uncomment and set to false if you want KataGo to be able to enter special cleanup.
|
||||
# For example, if you are testing it against itself, or against another bot that has precisely implemented the rules
|
||||
# documented at https://lightvector.github.io/KataGo/rules.html
|
||||
# preventCleanupPhase = true
|
||||
|
||||
# Search limits-----------------------------------------------------------------------------------
|
||||
|
||||
# By default, if NOT specified in an individual request, limit maximum number of root visits per search to this much
|
||||
maxVisits = 500
|
||||
# If provided, cap search time at this many seconds
|
||||
# maxTime = 60
|
||||
|
||||
# Number of threads to use in each search in parallel for any SINGLE position.
|
||||
# NOTE: Analysis engine can specify number of POSITIONS to be able to search in parallel via command line argument
|
||||
# so this number does not necessarily need to be larger than 1, although you can still set it larger if you prefer
|
||||
# to analyze fewer positions in parallel but spend more threads on each position.
|
||||
# Generally, having more threads on a single position will worsen the quality of search slightly, holding fixed the
|
||||
# number of visits, and thread contention will reduce efficiency, so cross-position parallelization is preferable
|
||||
# to numSearchThreads, but numSearchThreads is preferable if you want to reduce latency, and have individual
|
||||
# searches complete faster by doing fewer of them at a time.
|
||||
numSearchThreads = 2
|
||||
|
||||
# GPU Settings-------------------------------------------------------------------------------
|
||||
|
||||
# Maximum number of positions to send to GPU at once.
|
||||
nnMaxBatchSize = 32
|
||||
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
|
||||
nnCacheSizePowerOfTwo = 14
|
||||
# Size of mutex pool for nnCache is 2 ** this
|
||||
nnMutexPoolSizePowerOfTwo = 14
|
||||
# Randomize board orientation when running neural net evals?
|
||||
nnRandomize = true
|
||||
|
||||
|
||||
# TO USE MULTIPLE GPUS:
|
||||
# Set this to the number of GPUs you have and/or would like to use...
|
||||
# AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
|
||||
# numNNServerThreadsPerModel = 1
|
||||
|
||||
|
||||
# CUDA GPU settings--------------------------------------
|
||||
# These only apply when using the CUDA version of KataGo.
|
||||
|
||||
# IF USING ONE GPU: optionally uncomment and change this if the GPU you want to use turns out to be not device 0
|
||||
# cudaDeviceToUse = 0
|
||||
|
||||
# IF USING TWO GPUS: Uncomment these two lines (AND set numNNServerThreadsPerModel above):
|
||||
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
|
||||
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
|
||||
|
||||
# IF USING THREE GPUS: Uncomment these three lines (AND set numNNServerThreadsPerModel above):
|
||||
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
|
||||
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
|
||||
# cudaDeviceToUseThread2 = 2 # change this if the third GPU you want to use turns out to be not device 2
|
||||
|
||||
# You can probably guess the pattern if you have four, five, etc. GPUs.
|
||||
|
||||
# KataGo will automatically use FP16 or not based on the compute capability of your NVIDIA GPU. If you
|
||||
# want to try to force a particular behavior though you can uncomment these lines and change them
|
||||
# to "true" or "false". E.g. it's using FP16 but on your card that's giving an error, or it's not using
|
||||
# FP16 but you think it should.
|
||||
# cudaUseFP16 = auto
|
||||
# cudaUseNHWC = auto
|
||||
|
||||
|
||||
# OpenCL GPU settings--------------------------------------
|
||||
# These only apply when using the OpenCL version of KataGo.
|
||||
|
||||
# Uncomment to tune OpenCL for every board size separately, rather than only the largest possible size
|
||||
# openclReTunePerBoardSize = true
|
||||
|
||||
# IF USING ONE GPU: optionally uncomment and change this if the best device to use is guessed incorrectly.
|
||||
# The default behavior tries to guess the 'best' GPU or device on your system to use, usually it will be a good guess.
|
||||
# openclDeviceToUse = 0
|
||||
|
||||
# IF USING TWO GPUS: Uncomment these two lines and replace X and Y with the device ids of the devices you want to use.
|
||||
# It might NOT be 0 and 1, some computers will have many OpenCL devices. You can see what the devices are when
|
||||
# KataGo starts up - it should print or log all the devices it finds.
|
||||
# (AND also set numNNServerThreadsPerModel above)
|
||||
# openclDeviceToUseThread0 = X
|
||||
# openclDeviceToUseThread1 = Y
|
||||
|
||||
# IF USING THREE GPUS: Uncomment these three lines and replace X and Y and Z with the device ids of the devices you want to use.
|
||||
# It might NOT be 0 and 1 and 2, some computers will have many OpenCL devices. You can see what the devices are when
|
||||
# KataGo starts up - it should print or log all the devices it finds.
|
||||
# (AND also set numNNServerThreadsPerModel above)
|
||||
# openclDeviceToUseThread0 = X
|
||||
# openclDeviceToUseThread1 = Y
|
||||
# openclDeviceToUseThread2 = Z
|
||||
|
||||
# You can probably guess the pattern if you have four, five, etc. GPUs.
|
||||
|
||||
|
||||
# Root move selection and biases------------------------------------------------------------------------------
|
||||
# Uncomment and edit any of the below values to change them from their default.
|
||||
# Not all of these parameters are applicable to analysis, some are only used for actual play
|
||||
|
||||
# Temperature for the early game, randomize between chosen moves with this temperature
|
||||
# chosenMoveTemperatureEarly = 0.5
|
||||
# Decay temperature for the early game by 0.5 every this many moves, scaled with board size.
|
||||
# chosenMoveTemperatureHalflife = 19
|
||||
# At the end of search after the early game, randomize between chosen moves with this temperature
|
||||
# chosenMoveTemperature = 0.10
|
||||
# Subtract this many visits from each move prior to applying chosenMoveTemperature
|
||||
# (unless all moves have too few visits) to downweight unlikely moves
|
||||
# chosenMoveSubtract = 0
|
||||
# The same as chosenMoveSubtract but only prunes moves that fall below the threshold, does not affect moves above
|
||||
# chosenMovePrune = 1
|
||||
|
||||
# Number of symmetries to sample (WITH replacement) and average at the root
|
||||
# rootNumSymmetriesToSample = 1
|
||||
|
||||
# Using LCB for move selection?
|
||||
# useLcbForSelection = true
|
||||
# How many stdevs a move needs to be better than another for LCB selection
|
||||
# lcbStdevs = 5.0
|
||||
# Only use LCB override when a move has this proportion of visits as the top move
|
||||
# minVisitPropForLCB = 0.15
|
||||
|
||||
# Internal params------------------------------------------------------------------------------
|
||||
# Uncomment and edit any of the below values to change them from their default.
|
||||
|
||||
# Scales the utility of winning/losing
|
||||
# winLossUtilityFactor = 1.0
|
||||
# Scales the utility for trying to maximize score
|
||||
# staticScoreUtilityFactor = 0.10
|
||||
# dynamicScoreUtilityFactor = 0.30
|
||||
# Adjust dynamic score center this proportion of the way towards zero, capped at a reasonable amount.
|
||||
# dynamicScoreCenterZeroWeight = 0.20
|
||||
# dynamicScoreCenterScale = 0.75
|
||||
# The utility of getting a "no result" due to triple ko or other long cycle in non-superko rulesets (-1 to 1)
|
||||
# noResultUtilityForWhite = 0.0
|
||||
# The number of wins that a draw counts as, for white. (0 to 1)
|
||||
# drawEquivalentWinsForWhite = 0.5
|
||||
|
||||
# Exploration constant for mcts
|
||||
# cpuctExploration = 0.9
|
||||
# cpuctExplorationLog = 0.4
|
||||
# FPU reduction constant for mcts
|
||||
# fpuReductionMax = 0.2
|
||||
# rootFpuReductionMax = 0.1
|
||||
# Use parent average value for fpu base point instead of point value net estimate
|
||||
# fpuUseParentAverage = true
|
||||
# Amount to apply a downweighting of children with very bad values relative to good ones
|
||||
# valueWeightExponent = 0.5
|
||||
# Slight incentive for the bot to behave human-like with regard to passing at the end, filling the dame,
|
||||
# not wasting time playing in its own territory, etc, and not play moves that are equivalent in terms of
|
||||
# points but a bit more unfriendly to humans.
|
||||
# rootEndingBonusPoints = 0.5
|
||||
# Make the bot prune useless moves that are just prolonging the game to avoid losing yet
|
||||
# rootPruneUselessMoves = true
|
||||
|
||||
# How big to make the mutex pool for search synchronization
|
||||
# mutexPoolSize = 8192
|
||||
# How many virtual losses to add when a thread descends through a node
|
||||
# numVirtualLossesPerThread = 1
|
||||
+30
-21
@@ -7,11 +7,11 @@ import traceback
|
||||
from collections import defaultdict
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
|
||||
from core.ai import ai_move
|
||||
from core.common import OUTPUT_ERROR, OUTPUT_INFO
|
||||
from katrain.core.ai import ai_move
|
||||
from katrain.core.common import OUTPUT_ERROR, OUTPUT_INFO
|
||||
from elote import EloCompetitor
|
||||
from core.engine import KataGoEngine
|
||||
from core.game import Game
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.game import Game
|
||||
import json
|
||||
|
||||
DB_FILENAME = "bots/ai_performance.pickle"
|
||||
@@ -36,7 +36,7 @@ class AI:
|
||||
DEFAULT_ENGINE_SETTINGS = {
|
||||
"katago": "KataGo/katago",
|
||||
"model": "KataGo/models/b15-1.3.2.txt.gz",
|
||||
"config": "KataGo/analysis_config.cfg",
|
||||
"config": "bots/lowmem.cfg",
|
||||
"max_visits": 1,
|
||||
"max_time": 300.0,
|
||||
"_enable_ownership": False,
|
||||
@@ -75,9 +75,14 @@ class AI:
|
||||
|
||||
try:
|
||||
with open(DB_FILENAME, "rb") as f:
|
||||
ai_database, all_results = pickle.load(f)
|
||||
for ai in ai_database:
|
||||
ai.fix_settings() # update as required
|
||||
ai_database_loaded, all_results = pickle.load(f)
|
||||
ai_database = []
|
||||
for ai in ai_database_loaded:
|
||||
try:
|
||||
ai.fix_settings() # update as required
|
||||
ai_database.append(ai)
|
||||
except:
|
||||
print("Error loading AI", ai.strategy)
|
||||
except FileNotFoundError:
|
||||
ai_database = []
|
||||
all_results = []
|
||||
@@ -96,25 +101,26 @@ def retrieve_ais(selected_ais):
|
||||
|
||||
|
||||
test_ais = [
|
||||
# AI("Jigo", {}, {"max_visits": 100}),
|
||||
AI("Policy", {}, {"model": "my/model.bin.gz"}),
|
||||
AI("Policy", {}, {"model": "KataGo/models/b10-1.3.txt.gz"}),
|
||||
AI("Default", {}, {"model": "bots/6b.bin.gz", "max_visits": 500}),
|
||||
AI("Default", {}, {"model": "bots/6b104-s22347264.txt.gz", "max_visits": 500}),
|
||||
AI("Default", {}, {"model": "bots/6b104-s42364928.txt.gz", "max_visits": 500}),
|
||||
# AI("Default", {}, {"model": "KataGo/models/b10-1.3.txt.gz", "max_visits": 500}),
|
||||
AI("Policy", {}),
|
||||
AI("P:Local", {}),
|
||||
AI("P:Pick", {}),
|
||||
AI("P:Noise", {}),
|
||||
AI("P:Tenuki", {}),
|
||||
AI("P:Local", {}),
|
||||
AI("P:Influence", {}),
|
||||
AI("P:Territory", {}),
|
||||
AI("P:Weighted", {}),
|
||||
AI("P:Pick", {}),
|
||||
AI("ScoreLoss", {"max_visits": 500}),
|
||||
# AI("P:Tenuki", {}),
|
||||
# AI("P:Local", {}),
|
||||
AI("P:Influence", {}),
|
||||
# AI("P:Territory", {}),
|
||||
]
|
||||
|
||||
|
||||
for ai in test_ais:
|
||||
add_ai(ai)
|
||||
|
||||
N_GAMES = 5
|
||||
N_GAMES = 1
|
||||
BOARDSIZE = 19
|
||||
|
||||
ais_to_test = retrieve_ais(test_ais)
|
||||
@@ -131,13 +137,16 @@ def play_games(black: AI, white: AI):
|
||||
game.root.add_list_property("PW", [white.name])
|
||||
game.root.add_list_property("PB", [black.name])
|
||||
start_time = time.time()
|
||||
while not game.ended:
|
||||
while not game.ended and game.current_node.depth < 300:
|
||||
p = game.current_node.next_player
|
||||
move = ai_move(game, players[p].strategy, players[p].ai_settings)
|
||||
move, node = ai_move(game, players[p].strategy, players[p].ai_settings)
|
||||
while not game.current_node.analysis_ready:
|
||||
time.sleep(0.001)
|
||||
game.game_id += f"_{game.current_node.format_score()}"
|
||||
print(f"{tag}\tGame finished in {time.time()-start_time:.1f}s {game.current_node.format_score()} -> {game.write_sgf('sgf_selfplay/')}", file=sys.stderr)
|
||||
print(
|
||||
f"{tag}\tGame finished in {time.time()-start_time:.1f}s @ move {game.current_node.depth} {game.current_node.format_score()} -> {game.write_sgf('sgf_selfplay/')}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
score = game.current_node.score
|
||||
if score > 0.3:
|
||||
black.elo_comp.beat(white.elo_comp)
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ else:
|
||||
|
||||
username = f"katrain-{bot}"
|
||||
|
||||
with open("config.json") as f:
|
||||
with open("katrain/config.json") as f:
|
||||
settings = json.load(f)
|
||||
all_ai_settings = settings["ai"]
|
||||
|
||||
@@ -38,10 +38,10 @@ if bot not in greetings or username not in apikeys:
|
||||
APIKEY = apikeys[username]
|
||||
settings_dump = ", ".join(f"{k}={v}" for k, v in ai_settings.items() if not k.startswith("_"))
|
||||
print(settings_dump)
|
||||
GREETING = f"Hello, welcome to an experimental version of KaTrain AIs - These are based on weakened policy nets of KataGo. Current mode is: {greetings[bot]}"
|
||||
GREETING = f"Hello, play with these bots at any time by downloading KaTrain at github.com/sanderland/katrain - Current mode is {bot_strategy_names[bot]} ({greetings[bot]})"
|
||||
if settings:
|
||||
GREETING += f" Settings: {settings_dump}."
|
||||
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin! Play with these bots at any time by downloading KaTrain at github.com/sanderland/katrain"
|
||||
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin! "
|
||||
|
||||
cmd = f'{GTP2OGS} --debug --apikey {APIKEY} --rejectnewfile ~/shutdown_bots --username {username} --greeting "{GREETING}" --farewell "{BYEMSG}" {BOT_SETTINGS} --farewellscore --aichat --noclock --nopause --speeds blitz,live --persist --minrank 25k -- python bots/ai2gtp.py {bot} {port}'
|
||||
print(f"starting bot {username} using server port {port} --> {cmd}")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
OUTPUT_ERROR = -1
|
||||
OUTPUT_KATAGO_STDERR = -0.5
|
||||
OUTPUT_INFO = 0
|
||||
OUTPUT_DEBUG = 1
|
||||
OUTPUT_EXTRA_DEBUG = 2
|
||||
|
||||
|
||||
def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]:
|
||||
"""convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y"""
|
||||
ix = 0
|
||||
grid = [[]] * size[1]
|
||||
for y in range(size[1] - 1, -1, -1):
|
||||
grid[y] = array_var[ix : ix + size[0]]
|
||||
ix += size[0]
|
||||
return grid
|
||||
|
||||
|
||||
def evaluation_class(points_lost: float, eval_thresholds: List[float]):
|
||||
i = 0
|
||||
while i < len(eval_thresholds) - 1 and points_lost < eval_thresholds[i]:
|
||||
i += 1
|
||||
return i
|
||||
@@ -1,4 +0,0 @@
|
||||
from gui.badukpan import BadukPanControls, BadukPanWidget
|
||||
from gui.controls import Controls
|
||||
from gui.kivyutils import *
|
||||
from gui.popups import LoadSGFPopup, NewGamePopup, ConfigAIPopup, ConfigTeacherPopup, ConfigPopup
|
||||
+3
-342
@@ -1,343 +1,4 @@
|
||||
from kivy.config import Config # isort:skip
|
||||
# for backward compatibility
|
||||
from katrain.__main__ import run_app
|
||||
|
||||
Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click
|
||||
ICON = "img/icon.png"
|
||||
Config.set("kivy", "window_icon", ICON) # isort:skip # set icon before Window is imported
|
||||
|
||||
import signal
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from queue import Queue
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.core.clipboard import Clipboard
|
||||
from kivy.storage.jsonstore import JsonStore
|
||||
from kivy.uix.popup import Popup
|
||||
|
||||
from core.ai import ai_move
|
||||
from core.common import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
|
||||
from core.engine import KataGoEngine
|
||||
from core.game import Game, IllegalMoveException, KaTrainSGF
|
||||
from core.sgf_parser import Move, ParseError
|
||||
from gui import *
|
||||
|
||||
|
||||
class KaTrainGui(BoxLayout):
|
||||
"""Top level class responsible for tying everything together"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(KaTrainGui, self).__init__(**kwargs)
|
||||
self.debug_level = 0
|
||||
self.engine = None
|
||||
self.game = None
|
||||
self.new_game_popup = None
|
||||
self.fileselect_popup = None
|
||||
self.config_popup = None
|
||||
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
|
||||
|
||||
self._load_config()
|
||||
|
||||
self.debug_level = self.config("debug/level", OUTPUT_INFO)
|
||||
self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = list(self.config("ai").keys())
|
||||
self.message_queue = Queue()
|
||||
|
||||
self._keyboard = Window.request_keyboard(None, self, "")
|
||||
self._keyboard.bind(on_key_down=self._on_keyboard_down)
|
||||
|
||||
def log(self, message, level=OUTPUT_INFO):
|
||||
if level == OUTPUT_KATAGO_STDERR:
|
||||
if "starting" in message.lower():
|
||||
self.controls.set_status(f"KataGo engine starting...")
|
||||
if message.startswith("Tuning"):
|
||||
self.controls.set_status(f"KataGo is tuning settings for first startup, please wait." + message)
|
||||
if "ready" in message.lower():
|
||||
self.controls.set_status(f"KataGo engine ready.")
|
||||
print(f"[KG:STDERR]{message.strip()}")
|
||||
elif level == OUTPUT_ERROR:
|
||||
self.controls.set_status(f"ERROR: {message}")
|
||||
print(f"ERROR: {message}")
|
||||
elif self.debug_level >= level:
|
||||
print(message)
|
||||
|
||||
def _load_config(self):
|
||||
base_path = getattr(sys, "_MEIPASS", os.getcwd()) # for pyinstaller
|
||||
config_file = sys.argv[1] if len(sys.argv) > 1 else os.path.join(base_path, "config.json")
|
||||
try:
|
||||
self.log(f"Using config file {config_file}", OUTPUT_INFO)
|
||||
self._config_store = JsonStore(config_file, indent=4)
|
||||
self._config = dict(self._config_store)
|
||||
except Exception as e:
|
||||
self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR)
|
||||
sys.exit(1)
|
||||
|
||||
def save_config(self):
|
||||
for k, v in self._config.items():
|
||||
self._config_store.put(k, **v)
|
||||
|
||||
def config(self, setting, default=None):
|
||||
try:
|
||||
if "/" in setting:
|
||||
cat, key = setting.split("/")
|
||||
return self._config[cat].get(key, default)
|
||||
else:
|
||||
return self._config[setting]
|
||||
except KeyError:
|
||||
self.log(f"Missing configuration option {setting}", OUTPUT_ERROR)
|
||||
|
||||
def start(self):
|
||||
if self.engine:
|
||||
return
|
||||
self.board_gui.trainer_config = self.config("trainer")
|
||||
self.board_gui.ui_config = self.config("board_ui")
|
||||
self.engine = KataGoEngine(self, self.config("engine"))
|
||||
threading.Thread(target=self._message_loop_thread, daemon=True).start()
|
||||
self._do_new_game()
|
||||
|
||||
def update_state(self, redraw_board=False): # is called after every message and on receiving analyses and config changes
|
||||
# AI and Trainer/auto-undo handlers
|
||||
cn = self.game.current_node
|
||||
auto_undo = cn.player and "undo" in self.controls.player_mode(cn.player)
|
||||
if auto_undo and cn.analysis_ready and cn.parent and cn.parent.analysis_ready and not cn.children and not self.game.ended:
|
||||
self.game.analyze_undo(cn, self.config("trainer")) # not via message loop
|
||||
if cn.analysis_ready and "ai" in self.controls.player_mode(cn.next_player).lower() and not cn.children and not self.game.ended and not (auto_undo and cn.auto_undo is None):
|
||||
self._do_ai_move(cn) # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
|
||||
|
||||
# Handle prisoners and next player display
|
||||
prisoners = self.game.prisoner_count
|
||||
top, bot = self.board_controls.black_prisoners.__self__, self.board_controls.white_prisoners.__self__ # no weakref
|
||||
if self.game.next_player == "W":
|
||||
top, bot = bot, top
|
||||
self.board_controls.mid_circles_container.clear_widgets()
|
||||
self.board_controls.mid_circles_container.add_widget(bot)
|
||||
self.board_controls.mid_circles_container.add_widget(top)
|
||||
self.board_controls.black_prisoners.text = str(prisoners["W"])
|
||||
self.board_controls.white_prisoners.text = str(prisoners["B"])
|
||||
|
||||
# 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 = self.config("board_ui/engine_down_col")
|
||||
elif len(self.engine.queries) >= 4:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_busy_col")
|
||||
elif len(self.engine.queries) >= 2:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_little_busy_col")
|
||||
elif len(self.engine.queries) == 0:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_ready_col")
|
||||
else:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_almost_done_col")
|
||||
# redraw
|
||||
if redraw_board:
|
||||
Clock.schedule_once(self.board_gui.draw_board, -1)
|
||||
self.board_gui.redraw_board_contents_trigger()
|
||||
self.controls.update_evaluation()
|
||||
|
||||
def _message_loop_thread(self):
|
||||
while True:
|
||||
game, msg, *args = self.message_queue.get()
|
||||
try:
|
||||
self.log(f"Message Loop Received {msg}: {args} for Game {game}", OUTPUT_EXTRA_DEBUG)
|
||||
if game != self.game.game_id:
|
||||
self.log(f"Message skipped as it is outdated (current game is {self.game.game_id}", OUTPUT_EXTRA_DEBUG)
|
||||
continue
|
||||
getattr(self, f"_do_{msg.replace('-','_')}")(*args)
|
||||
self.update_state()
|
||||
except Exception as e:
|
||||
self.log(f"Exception in processing message {msg} {args}: {e}", OUTPUT_ERROR)
|
||||
traceback.print_exc()
|
||||
|
||||
def __call__(self, message, *args):
|
||||
if self.game:
|
||||
self.message_queue.put([self.game.game_id, message, *args])
|
||||
|
||||
def _do_new_game(self, move_tree=None, analyze_fast=False):
|
||||
self.engine.on_new_game() # clear queries
|
||||
self.game = Game(self, self.engine, self.config("game"), move_tree=move_tree, analyze_fast=analyze_fast)
|
||||
self.controls.select_mode("analyze" if move_tree and len(move_tree.nodes_in_tree) > 1 else "play")
|
||||
self.controls.graph.initialize_from_game(self.game.root)
|
||||
self.update_state(redraw_board=True)
|
||||
|
||||
def _do_ai_move(self, node=None):
|
||||
if node is None or self.game.current_node == node:
|
||||
mode = self.controls.ai_mode(self.game.current_node.next_player)
|
||||
settings = self.config(f"ai/{mode}")
|
||||
if settings:
|
||||
ai_move(self.game, mode, settings)
|
||||
|
||||
def _do_undo(self, n_times=1):
|
||||
self.game.undo(n_times)
|
||||
|
||||
def _do_redo(self, n_times=1):
|
||||
self.game.redo(n_times)
|
||||
|
||||
def _do_switch_branch(self, direction):
|
||||
self.game.switch_branch(direction)
|
||||
|
||||
def _do_play(self, coords):
|
||||
try:
|
||||
self.game.play(Move(coords, player=self.game.next_player))
|
||||
except IllegalMoveException as e:
|
||||
self.controls.set_status(f"Illegal Move: {str(e)}")
|
||||
|
||||
def _do_analyze_extra(self, mode):
|
||||
self.game.analyze_extra(mode)
|
||||
|
||||
def _do_analyze_sgf_popup(self):
|
||||
if not self.fileselect_popup:
|
||||
self.fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)).__self__
|
||||
popup_contents = LoadSGFPopup()
|
||||
self.fileselect_popup.add_widget(popup_contents)
|
||||
popup_contents.filesel.path = os.path.abspath(os.path.expanduser(self.config("sgf/sgf_load")))
|
||||
|
||||
def readfile(files, _mouse):
|
||||
self.fileselect_popup.dismiss()
|
||||
try:
|
||||
move_tree = KaTrainSGF.parse_file(files[0])
|
||||
except ParseError as e:
|
||||
self.log(f"Failed to load SGF. Parse Error: {e}", OUTPUT_ERROR)
|
||||
return
|
||||
self._do_new_game(move_tree=move_tree, analyze_fast=popup_contents.fast.active)
|
||||
if not popup_contents.rewind.active:
|
||||
self.game.redo(999)
|
||||
|
||||
popup_contents.filesel.on_submit = readfile
|
||||
self.fileselect_popup.open()
|
||||
|
||||
def _do_new_game_popup(self):
|
||||
if not self.new_game_popup:
|
||||
self.new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6)).__self__
|
||||
popup_contents = NewGamePopup(self, self.new_game_popup, {k: v[0] for k, v in self.game.root.properties.items() if len(v) == 1})
|
||||
self.new_game_popup.add_widget(popup_contents)
|
||||
self.new_game_popup.open()
|
||||
|
||||
def _do_config_popup(self):
|
||||
if not self.config_popup:
|
||||
self.config_popup = Popup(title="Edit Settings", size_hint=(0.9, 0.9)).__self__
|
||||
popup_contents = ConfigPopup(self, self.config_popup, dict(self._config), ignore_cats=("trainer", "ai"))
|
||||
self.config_popup.add_widget(popup_contents)
|
||||
self.config_popup.open()
|
||||
|
||||
def _do_output_sgf(self):
|
||||
for pl in Move.PLAYERS:
|
||||
if not self.game.root.get_property(f"P{pl}"):
|
||||
_, model_file = os.path.split(self.engine.config["model"])
|
||||
self.game.root.set_property(
|
||||
f"P{pl}", f"AI {self.controls.ai_mode(pl)} (KataGo { os.path.splitext(model_file)[0]})" if "ai" in self.controls.player_mode(pl) else "Player"
|
||||
)
|
||||
msg = self.game.write_sgf(
|
||||
self.config("sgf/sgf_save"),
|
||||
trainer_config=self.config("trainer"),
|
||||
save_feedback=self.config("sgf/save_feedback"),
|
||||
eval_thresholds=self.config("trainer/eval_thresholds"),
|
||||
)
|
||||
self.log(msg, OUTPUT_INFO)
|
||||
self.controls.set_status(msg)
|
||||
|
||||
def load_sgf_from_clipboard(self):
|
||||
clipboard = Clipboard.paste()
|
||||
if not clipboard:
|
||||
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.")
|
||||
return
|
||||
try:
|
||||
move_tree = KaTrainSGF.parse(clipboard)
|
||||
except Exception as e:
|
||||
self.controls.set_status(f"Failed to imported game from clipboard: {e}\nClipboard contents: {clipboard[:50]}...")
|
||||
return
|
||||
move_tree.nodes_in_tree[-1].analyze(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.log("Imported game from clipboard.", OUTPUT_INFO)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos):
|
||||
if touch.button == "scrollup":
|
||||
self("redo")
|
||||
elif touch.button == "scrolldown":
|
||||
self("undo")
|
||||
return super().on_touch_up(touch)
|
||||
|
||||
def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
|
||||
if isinstance(App.get_running_app().root_window.children[0], Popup):
|
||||
return # if in new game or load, don't allow keyboard shortcuts
|
||||
|
||||
shortcuts = {
|
||||
"q": self.controls.show_children,
|
||||
"w": self.controls.eval,
|
||||
"e": self.controls.hints,
|
||||
"r": self.controls.ownership,
|
||||
"t": self.controls.policy,
|
||||
"enter": ("ai-move",),
|
||||
"a": self.controls.analyze_extra,
|
||||
"s": self.controls.analyze_equalize,
|
||||
"d": self.controls.analyze_sweep,
|
||||
"right": ("switch-branch", 1),
|
||||
"left": ("switch-branch", -1),
|
||||
}
|
||||
if keycode[1] in shortcuts.keys():
|
||||
shortcut = shortcuts[keycode[1]]
|
||||
if isinstance(shortcut, Widget):
|
||||
shortcut.trigger_action(duration=0)
|
||||
else:
|
||||
self(*shortcut)
|
||||
elif keycode[1] == "tab":
|
||||
self.controls.switch_mode()
|
||||
elif keycode[1] == "spacebar":
|
||||
self("play", None) # pass
|
||||
elif keycode[1] in ["`", "~", "p"]:
|
||||
self.controls_box.hidden = not self.controls_box.hidden
|
||||
elif keycode[1] in ["up", "z"]:
|
||||
self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] in ["down", "x"]:
|
||||
self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] == "n" and "ctrl" in modifiers:
|
||||
self("new-game-popup")
|
||||
elif keycode[1] == "l" and "ctrl" in modifiers:
|
||||
self("analyze-sgf-popup")
|
||||
elif keycode[1] == "s" and "ctrl" in modifiers:
|
||||
self("output-sgf")
|
||||
elif keycode[1] == "c" and "ctrl" in modifiers:
|
||||
Clipboard.copy(self.game.root.sgf())
|
||||
self.controls.set_status("Copied SGF to clipboard.")
|
||||
elif keycode[1] == "v" and "ctrl" in modifiers:
|
||||
self.load_sgf_from_clipboard()
|
||||
return True
|
||||
|
||||
|
||||
class KaTrainApp(App):
|
||||
gui = ObjectProperty(None)
|
||||
|
||||
def build(self):
|
||||
self.icon = ICON # how you're supposed to set an icon
|
||||
self.gui = KaTrainGui()
|
||||
print(self.get_application_icon())
|
||||
Window.bind(on_request_close=self.on_request_close)
|
||||
return self.gui
|
||||
|
||||
def on_start(self):
|
||||
self.gui.start()
|
||||
|
||||
def on_request_close(self, *args):
|
||||
if getattr(self, "gui", None) and self.gui.engine:
|
||||
self.gui.engine.shutdown()
|
||||
|
||||
def signal_handler(self, *args):
|
||||
if self.gui.debug_level >= OUTPUT_DEBUG:
|
||||
print("TRACEBACKS")
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
print(f"\n# ThreadID: {threadId}")
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
print(f"\tFile: {filename}, line {lineno}, in {name}")
|
||||
if line:
|
||||
print(f"\t\t{line.strip()}")
|
||||
self.on_request_close()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = KaTrainApp()
|
||||
signal.signal(signal.SIGINT, app.signal_handler)
|
||||
try:
|
||||
app.run()
|
||||
except Exception:
|
||||
app.on_request_close()
|
||||
raise
|
||||
run_app()
|
||||
File renamed without changes.
File renamed without changes.
@@ -62,9 +62,9 @@ numSearchThreads = 6
|
||||
# Maximum number of positions to send to GPU at once.
|
||||
nnMaxBatchSize = 64
|
||||
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
|
||||
nnCacheSizePowerOfTwo = 21
|
||||
nnCacheSizePowerOfTwo = 19
|
||||
# Size of mutex pool for nnCache is 2 ** this
|
||||
nnMutexPoolSizePowerOfTwo = 17
|
||||
nnMutexPoolSizePowerOfTwo = 16
|
||||
# Randomize board orientation when running neural net evals?
|
||||
nnRandomize = true
|
||||
|
||||
File renamed without changes.
Regular → Executable
BIN
Binary file not shown.
File renamed without changes.
Binary file not shown.
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.
@@ -0,0 +1,363 @@
|
||||
from kivy.config import Config # isort:skip
|
||||
from kivy.lang import Builder
|
||||
from kivy.resources import resource_add_path
|
||||
|
||||
Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click
|
||||
ICON = "img/icon.png"
|
||||
Config.set("kivy", "window_icon", ICON) # isort:skip # set icon before Window is imported
|
||||
|
||||
import signal
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from queue import Queue
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.core.clipboard import Clipboard
|
||||
from kivy.storage.jsonstore import JsonStore
|
||||
from kivy.uix.popup import Popup
|
||||
|
||||
from katrain.core.ai import ai_move
|
||||
from katrain.core.common import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, find_package_resource, clean_temp
|
||||
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.badukpan import BadukPanWidget
|
||||
from katrain.gui.controls import Controls
|
||||
from katrain.gui.popups import NewGamePopup, ConfigPopup, LoadSGFPopup
|
||||
|
||||
__version__ = "1.0.5"
|
||||
|
||||
|
||||
class KaTrainGui(BoxLayout):
|
||||
"""Top level class responsible for tying everything together"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(KaTrainGui, self).__init__(**kwargs)
|
||||
self.debug_level = 0
|
||||
self.engine = None
|
||||
self.game = None
|
||||
self.new_game_popup = None
|
||||
self.fileselect_popup = None
|
||||
self.config_popup = None
|
||||
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
|
||||
self.config_file = self._load_config()
|
||||
|
||||
self.debug_level = self.config("debug/level", OUTPUT_INFO)
|
||||
self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = list(self.config("ai").keys())
|
||||
self.message_queue = Queue()
|
||||
|
||||
self._keyboard = Window.request_keyboard(None, self, "")
|
||||
self._keyboard.bind(on_key_down=self._on_keyboard_down)
|
||||
|
||||
def log(self, message, level=OUTPUT_INFO):
|
||||
if level == OUTPUT_KATAGO_STDERR and "ERROR" not in self.controls.status_label.text:
|
||||
if "starting" in message.lower():
|
||||
self.controls.set_status(f"KataGo engine starting...")
|
||||
if message.startswith("Tuning"):
|
||||
self.controls.set_status(f"KataGo is tuning settings for first startup, please wait." + message)
|
||||
return
|
||||
if "ready" in message.lower():
|
||||
self.controls.set_status(f"KataGo engine ready.")
|
||||
if "ready" in message.lower():
|
||||
self.controls.set_status(f"KataGo engine ready.")
|
||||
if level == OUTPUT_ERROR or (level == OUTPUT_KATAGO_STDERR and "error" in message.lower()):
|
||||
self.controls.set_status(f"ERROR: {message}")
|
||||
print(f"ERROR: {message}")
|
||||
elif self.debug_level >= level:
|
||||
print(message)
|
||||
|
||||
def _load_config(self):
|
||||
config_file = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else find_package_resource("katrain/config.json"))
|
||||
try:
|
||||
self.log(f"Using config file {config_file}", OUTPUT_INFO)
|
||||
self._config_store = JsonStore(config_file, indent=4)
|
||||
self._config = dict(self._config_store)
|
||||
return config_file
|
||||
except Exception as e:
|
||||
self.log(f"Failed to load config {config_file}: {e}", OUTPUT_ERROR)
|
||||
sys.exit(1)
|
||||
|
||||
def save_config(self):
|
||||
for k, v in self._config.items():
|
||||
self._config_store.put(k, **v)
|
||||
|
||||
def config(self, setting, default=None):
|
||||
try:
|
||||
if "/" in setting:
|
||||
cat, key = setting.split("/")
|
||||
return self._config[cat].get(key, default)
|
||||
else:
|
||||
return self._config[setting]
|
||||
except KeyError:
|
||||
self.log(f"Missing configuration option {setting}", OUTPUT_ERROR)
|
||||
|
||||
def start(self):
|
||||
if self.engine:
|
||||
return
|
||||
self.board_gui.trainer_config = self.config("trainer")
|
||||
self.board_gui.ui_config = self.config("board_ui")
|
||||
self.engine = KataGoEngine(self, self.config("engine"))
|
||||
threading.Thread(target=self._message_loop_thread, daemon=True).start()
|
||||
self._do_new_game()
|
||||
|
||||
def update_state(self, redraw_board=False): # is called after every message and on receiving analyses and config changes
|
||||
# AI and Trainer/auto-undo handlers
|
||||
cn = self.game.current_node
|
||||
auto_undo = cn.player and "undo" in self.controls.player_mode(cn.player)
|
||||
if auto_undo and cn.analysis_ready and cn.parent and cn.parent.analysis_ready and not cn.children and not self.game.ended:
|
||||
self.game.analyze_undo(cn, self.config("trainer")) # not via message loop
|
||||
if cn.analysis_ready and "ai" in self.controls.player_mode(cn.next_player).lower() and not cn.children and not self.game.ended and not (auto_undo and cn.auto_undo is None):
|
||||
self._do_ai_move(cn) # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
|
||||
|
||||
# Handle prisoners and next player display
|
||||
prisoners = self.game.prisoner_count
|
||||
top, bot = self.board_controls.black_prisoners.__self__, self.board_controls.white_prisoners.__self__ # no weakref
|
||||
if self.game.next_player == "W":
|
||||
top, bot = bot, top
|
||||
self.board_controls.mid_circles_container.clear_widgets()
|
||||
self.board_controls.mid_circles_container.add_widget(bot)
|
||||
self.board_controls.mid_circles_container.add_widget(top)
|
||||
self.board_controls.black_prisoners.text = str(prisoners["W"])
|
||||
self.board_controls.white_prisoners.text = str(prisoners["B"])
|
||||
|
||||
# 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 = self.config("board_ui/engine_down_col")
|
||||
elif len(self.engine.queries) >= 4:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_busy_col")
|
||||
elif len(self.engine.queries) >= 2:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_little_busy_col")
|
||||
elif len(self.engine.queries) == 0:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_ready_col")
|
||||
else:
|
||||
self.board_controls.engine_status_col = self.config("board_ui/engine_almost_done_col")
|
||||
# redraw
|
||||
if redraw_board:
|
||||
Clock.schedule_once(self.board_gui.draw_board, -1)
|
||||
self.board_gui.redraw_board_contents_trigger()
|
||||
self.controls.update_evaluation()
|
||||
|
||||
def _message_loop_thread(self):
|
||||
while True:
|
||||
game, msg, *args = self.message_queue.get()
|
||||
try:
|
||||
self.log(f"Message Loop Received {msg}: {args} for Game {game}", OUTPUT_EXTRA_DEBUG)
|
||||
if game != self.game.game_id:
|
||||
self.log(f"Message skipped as it is outdated (current game is {self.game.game_id}", OUTPUT_EXTRA_DEBUG)
|
||||
continue
|
||||
getattr(self, f"_do_{msg.replace('-','_')}")(*args)
|
||||
self.update_state()
|
||||
except Exception as e:
|
||||
self.log(f"Exception in processing message {msg} {args}: {e}", OUTPUT_ERROR)
|
||||
traceback.print_exc()
|
||||
|
||||
def __call__(self, message, *args):
|
||||
if self.game:
|
||||
self.message_queue.put([self.game.game_id, message, *args])
|
||||
|
||||
def _do_new_game(self, move_tree=None, analyze_fast=False):
|
||||
self.board_gui.animating_pv = None
|
||||
self.engine.on_new_game() # clear queries
|
||||
self.game = Game(self, self.engine, self.config("game"), move_tree=move_tree, analyze_fast=analyze_fast)
|
||||
self.controls.select_mode("analyze" if move_tree and len(move_tree.nodes_in_tree) > 1 else "play")
|
||||
self.controls.graph.initialize_from_game(self.game.root)
|
||||
self.update_state(redraw_board=True)
|
||||
|
||||
def _do_ai_move(self, node=None):
|
||||
if node is None or self.game.current_node == node:
|
||||
mode = self.controls.ai_mode(self.game.current_node.next_player)
|
||||
settings = self.config(f"ai/{mode}")
|
||||
if settings:
|
||||
ai_move(self.game, mode, settings)
|
||||
|
||||
def _do_undo(self, n_times=1):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.undo(n_times)
|
||||
|
||||
def _do_redo(self, n_times=1):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.redo(n_times)
|
||||
|
||||
def _do_switch_branch(self, direction):
|
||||
self.game.switch_branch(direction)
|
||||
|
||||
def _do_play(self, coords):
|
||||
self.board_gui.animating_pv = None
|
||||
try:
|
||||
self.game.play(Move(coords, player=self.game.next_player))
|
||||
except IllegalMoveException as e:
|
||||
self.controls.set_status(f"Illegal Move: {str(e)}")
|
||||
|
||||
def _do_analyze_extra(self, mode):
|
||||
self.game.analyze_extra(mode)
|
||||
|
||||
def _do_analyze_sgf_popup(self):
|
||||
if not self.fileselect_popup:
|
||||
self.fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)).__self__
|
||||
popup_contents = LoadSGFPopup()
|
||||
self.fileselect_popup.add_widget(popup_contents)
|
||||
popup_contents.filesel.path = os.path.abspath(os.path.expanduser(self.config("sgf/sgf_load")))
|
||||
|
||||
def readfile(files, _mouse):
|
||||
self.fileselect_popup.dismiss()
|
||||
try:
|
||||
move_tree = KaTrainSGF.parse_file(files[0])
|
||||
except ParseError as e:
|
||||
self.log(f"Failed to load SGF. Parse Error: {e}", OUTPUT_ERROR)
|
||||
return
|
||||
self._do_new_game(move_tree=move_tree, analyze_fast=popup_contents.fast.active)
|
||||
if not popup_contents.rewind.active:
|
||||
self.game.redo(999)
|
||||
|
||||
popup_contents.filesel.on_submit = readfile
|
||||
self.fileselect_popup.open()
|
||||
|
||||
def _do_new_game_popup(self):
|
||||
if not self.new_game_popup:
|
||||
self.new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6)).__self__
|
||||
popup_contents = NewGamePopup(self, self.new_game_popup, {k: v[0] for k, v in self.game.root.properties.items() if len(v) == 1})
|
||||
self.new_game_popup.add_widget(popup_contents)
|
||||
self.new_game_popup.open()
|
||||
|
||||
def _do_config_popup(self):
|
||||
if not self.config_popup:
|
||||
self.config_popup = Popup(title=f"Edit Settings - {self.config_file}", size_hint=(0.9, 0.9)).__self__
|
||||
popup_contents = ConfigPopup(self, self.config_popup, dict(self._config), ignore_cats=("trainer", "ai"))
|
||||
self.config_popup.add_widget(popup_contents)
|
||||
self.config_popup.open()
|
||||
|
||||
def _do_output_sgf(self):
|
||||
for pl in Move.PLAYERS:
|
||||
if not self.game.root.get_property(f"P{pl}"):
|
||||
_, model_file = os.path.split(self.engine.config["model"])
|
||||
self.game.root.set_property(
|
||||
f"P{pl}", f"AI {self.controls.ai_mode(pl)} (KataGo { os.path.splitext(model_file)[0]})" if "ai" in self.controls.player_mode(pl) else "Player"
|
||||
)
|
||||
msg = self.game.write_sgf(
|
||||
self.config("sgf/sgf_save"),
|
||||
trainer_config=self.config("trainer"),
|
||||
save_feedback=self.config("sgf/save_feedback"),
|
||||
eval_thresholds=self.config("trainer/eval_thresholds"),
|
||||
)
|
||||
self.log(msg, OUTPUT_INFO)
|
||||
self.controls.set_status(msg)
|
||||
|
||||
def load_sgf_from_clipboard(self):
|
||||
clipboard = Clipboard.paste()
|
||||
if not clipboard:
|
||||
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.")
|
||||
return
|
||||
try:
|
||||
move_tree = KaTrainSGF.parse(clipboard)
|
||||
except Exception as e:
|
||||
self.controls.set_status(f"Failed to imported game from clipboard: {e}\nClipboard contents: {clipboard[:50]}...")
|
||||
return
|
||||
move_tree.nodes_in_tree[-1].analyze(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.log("Imported game from clipboard.", OUTPUT_INFO)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos):
|
||||
if touch.button == "scrollup":
|
||||
self("redo")
|
||||
elif touch.button == "scrolldown":
|
||||
self("undo")
|
||||
return super().on_touch_up(touch)
|
||||
|
||||
def _on_keyboard_down(self, _keyboard, keycode, _text, modifiers):
|
||||
if isinstance(App.get_running_app().root_window.children[0], Popup):
|
||||
return # if in new game or load, don't allow keyboard shortcuts
|
||||
|
||||
shortcuts = {
|
||||
"q": self.controls.show_children,
|
||||
"w": self.controls.eval,
|
||||
"e": self.controls.hints,
|
||||
"r": self.controls.ownership,
|
||||
"t": self.controls.policy,
|
||||
"enter": ("ai-move",),
|
||||
"a": self.controls.analyze_extra,
|
||||
"s": self.controls.analyze_equalize,
|
||||
"d": self.controls.analyze_sweep,
|
||||
"right": ("switch-branch", 1),
|
||||
"left": ("switch-branch", -1),
|
||||
}
|
||||
if keycode[1] in shortcuts.keys():
|
||||
shortcut = shortcuts[keycode[1]]
|
||||
if isinstance(shortcut, Widget):
|
||||
shortcut.trigger_action(duration=0)
|
||||
else:
|
||||
self(*shortcut)
|
||||
elif keycode[1] == "tab":
|
||||
self.controls.switch_mode()
|
||||
elif keycode[1] == "spacebar":
|
||||
self("play", None) # pass
|
||||
elif keycode[1] in ["`", "~", "p"]:
|
||||
self.controls_box.hidden = not self.controls_box.hidden
|
||||
elif keycode[1] in ["up", "z"]:
|
||||
self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] in ["down", "x"]:
|
||||
self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] == "n" and "ctrl" in modifiers:
|
||||
self("new-game-popup")
|
||||
elif keycode[1] == "l" and "ctrl" in modifiers:
|
||||
self("analyze-sgf-popup")
|
||||
elif keycode[1] == "s" and "ctrl" in modifiers:
|
||||
self("output-sgf")
|
||||
elif keycode[1] == "c" and "ctrl" in modifiers:
|
||||
Clipboard.copy(self.game.root.sgf())
|
||||
self.controls.set_status("Copied SGF to clipboard.")
|
||||
elif keycode[1] == "v" and "ctrl" in modifiers:
|
||||
self.load_sgf_from_clipboard()
|
||||
return True
|
||||
|
||||
|
||||
class KaTrainApp(App):
|
||||
gui = ObjectProperty(None)
|
||||
|
||||
def build(self):
|
||||
self.icon = ICON # how you're supposed to set an icon
|
||||
self.gui = KaTrainGui()
|
||||
self.title = f"KaTrain v{__version__}"
|
||||
Window.bind(on_request_close=self.on_request_close)
|
||||
return self.gui
|
||||
|
||||
def on_start(self):
|
||||
self.gui.start()
|
||||
|
||||
def on_request_close(self, *args):
|
||||
if getattr(self, "gui", None) and self.gui.engine:
|
||||
self.gui.engine.shutdown()
|
||||
clean_temp()
|
||||
|
||||
def signal_handler(self, *args):
|
||||
if self.gui.debug_level >= OUTPUT_DEBUG:
|
||||
print("TRACEBACKS")
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
print(f"\n# ThreadID: {threadId}")
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
print(f"\tFile: {filename}, line {lineno}, in {name}")
|
||||
if line:
|
||||
print(f"\t\t{line.strip()}")
|
||||
self.on_request_close()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def run_app():
|
||||
kv_file = find_package_resource("katrain/gui.kv")
|
||||
resource_add_path(os.path.split(kv_file)[0])
|
||||
Builder.load_file(kv_file)
|
||||
app = KaTrainApp()
|
||||
signal.signal(signal.SIGINT, app.signal_handler)
|
||||
try:
|
||||
app.run()
|
||||
except Exception:
|
||||
app.on_request_close()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_app()
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"engine": {
|
||||
"katago": "KataGo/katago",
|
||||
"model": "KataGo/models/b15-1.3.2.txt.gz",
|
||||
"config": "KataGo/analysis_config.cfg",
|
||||
"katago": "",
|
||||
"model": "katrain/models/b15-1.3.2.txt.gz",
|
||||
"config": "katrain/KataGo/analysis_config.cfg",
|
||||
"threads": 16,
|
||||
"max_visits": 500,
|
||||
"fast_visits": 50,
|
||||
@@ -128,6 +128,7 @@
|
||||
}
|
||||
},
|
||||
"board_ui": {
|
||||
"anim_pv_time": 0.5,
|
||||
"engine_down_col": [
|
||||
0.8,
|
||||
0,
|
||||
Whitespace-only changes.
@@ -4,9 +4,9 @@ import random
|
||||
import time
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from core.common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, var_to_grid
|
||||
from core.engine import EngineDiedException
|
||||
from core.game import Game, GameNode, IllegalMoveException, Move
|
||||
from katrain.core.common import OUTPUT_DEBUG, OUTPUT_INFO, var_to_grid
|
||||
from katrain.core.engine import EngineDiedException
|
||||
from katrain.core.game import Game, GameNode, Move
|
||||
|
||||
|
||||
def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]:
|
||||
@@ -0,0 +1,52 @@
|
||||
from typing import Any, List, Tuple
|
||||
import os
|
||||
|
||||
try:
|
||||
import importlib.resources as pkg_resources
|
||||
except:
|
||||
import importlib_resources as pkg_resources
|
||||
|
||||
OUTPUT_ERROR = -1
|
||||
OUTPUT_KATAGO_STDERR = -0.5
|
||||
OUTPUT_INFO = 0
|
||||
OUTPUT_DEBUG = 1
|
||||
OUTPUT_EXTRA_DEBUG = 2
|
||||
|
||||
|
||||
def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]:
|
||||
"""convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y"""
|
||||
ix = 0
|
||||
grid = [[]] * size[1]
|
||||
for y in range(size[1] - 1, -1, -1):
|
||||
grid[y] = array_var[ix : ix + size[0]]
|
||||
ix += size[0]
|
||||
return grid
|
||||
|
||||
|
||||
def evaluation_class(points_lost: float, eval_thresholds: List[float]):
|
||||
i = 0
|
||||
while i < len(eval_thresholds) - 1 and points_lost < eval_thresholds[i]:
|
||||
i += 1
|
||||
return i
|
||||
|
||||
|
||||
resource_scopes = []
|
||||
|
||||
|
||||
def clean_temp():
|
||||
for s in resource_scopes:
|
||||
s.__exit__(None, None, None)
|
||||
|
||||
|
||||
def find_package_resource(path):
|
||||
if path.startswith("katrain"):
|
||||
parts = path.replace("\\", "/").split("/")
|
||||
try:
|
||||
path_obj = pkg_resources.path(".".join(parts[:-1]), parts[-1]).__enter__()
|
||||
resource_scopes.append(path_obj)
|
||||
return str(path_obj)
|
||||
except (ModuleNotFoundError, FileNotFoundError) as e:
|
||||
print(f"File {path} not found, installation possibly broken")
|
||||
return f"FILENOTFOUND::{path}"
|
||||
else:
|
||||
return path # absolute path
|
||||
@@ -6,8 +6,8 @@ import threading
|
||||
import time
|
||||
from typing import Callable, Optional
|
||||
|
||||
from core.common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
|
||||
from core.game_node import GameNode
|
||||
from katrain.core.common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, find_package_resource
|
||||
from katrain.core.game_node import GameNode
|
||||
|
||||
|
||||
class EngineDiedException(Exception):
|
||||
@@ -27,7 +27,17 @@ class KataGoEngine:
|
||||
|
||||
def __init__(self, katrain, config):
|
||||
self.katrain = katrain
|
||||
self.command = f"{config['katago']} analysis -model {config['model']} -config {config['config']} -analysis-threads {config['threads']}"
|
||||
executable = config["katago"].strip()
|
||||
if not executable:
|
||||
if sys.platform.startswith("win"):
|
||||
executable = "katrain/KataGo/katago.exe"
|
||||
elif sys.platform.startswith("linux"):
|
||||
executable = "katrain/KataGo/katago"
|
||||
else: # e.g. MacOS after brewing
|
||||
executable = "katago"
|
||||
|
||||
modelfile, configfile = find_package_resource(config["model"]), find_package_resource(config["config"])
|
||||
self.command = f'{find_package_resource(executable)} analysis -model "{modelfile}" -config "{configfile}" -analysis-threads {config["threads"]}'
|
||||
if not sys.platform.startswith("win"):
|
||||
self.command = shlex.split(self.command)
|
||||
self.queries = {} # outstanding query id -> start time and callback
|
||||
@@ -37,19 +47,27 @@ class KataGoEngine:
|
||||
self.base_priority = 0
|
||||
self.override_settings = {} # mainly for bot scripts to hook into
|
||||
self._lock = threading.Lock()
|
||||
self.analysis_thread = None
|
||||
self.stderr_thread = None
|
||||
self.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()
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
|
||||
self.katago_process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except FileNotFoundError as e:
|
||||
self.katrain.log(
|
||||
f"Starting kata with command '{self.command}' failed with error {e}. Please make sure the 'katago' value under 'engine' in settings points to the correct KataGo executable.",
|
||||
OUTPUT_ERROR,
|
||||
)
|
||||
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||
if self.config["katago"].strip():
|
||||
self.katrain.log(
|
||||
f"Starting kata with command '{self.command}' failed with error {e}. If on MacOS, see the manual on how to use brew to install katago first, and add it to your path or 'engine/katago' setting.",
|
||||
OUTPUT_ERROR,
|
||||
)
|
||||
else:
|
||||
self.katrain.log(
|
||||
f"Starting kata with command '{self.command}' failed with error {e}. Please make sure the 'katago' value under 'engine' in settings points to the correct KataGo executable.",
|
||||
OUTPUT_ERROR,
|
||||
)
|
||||
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()
|
||||
|
||||
def on_new_game(self):
|
||||
self.base_priority += 1
|
||||
@@ -68,6 +86,10 @@ class KataGoEngine:
|
||||
if process:
|
||||
process.terminate()
|
||||
self.katago_process = None
|
||||
if self.stderr_thread:
|
||||
self.stderr_thread.join()
|
||||
if self.analysis_thread:
|
||||
self.analysis_thread.join()
|
||||
|
||||
def is_idle(self):
|
||||
return not self.queries
|
||||
@@ -77,7 +99,7 @@ class KataGoEngine:
|
||||
try:
|
||||
line = self.katago_process.stderr.readline()
|
||||
if line:
|
||||
self.katrain.log(line.decode(), OUTPUT_KATAGO_STDERR)
|
||||
self.katrain.log(line.decode().strip(), OUTPUT_KATAGO_STDERR)
|
||||
except:
|
||||
return
|
||||
|
||||
@@ -89,6 +111,7 @@ class KataGoEngine:
|
||||
raise EngineDiedException(f"Engine died unexpectedly without sending output, possibly due to out of memory: {e}")
|
||||
if b"Uncaught exception" in line:
|
||||
self.katrain.log(f"KataGo Engine Failed: {line.decode()}", OUTPUT_ERROR)
|
||||
return
|
||||
if not line:
|
||||
continue
|
||||
analysis = json.loads(line)
|
||||
@@ -97,13 +120,16 @@ class KataGoEngine:
|
||||
continue
|
||||
query_id = analysis["id"]
|
||||
callback, error_callback, start_time, next_move = self.queries[query_id]
|
||||
del self.queries[query_id]
|
||||
if "error" in analysis:
|
||||
del self.queries[query_id]
|
||||
if error_callback:
|
||||
error_callback(analysis)
|
||||
elif not (next_move and "Illegal move" in analysis["error"]): # sweep
|
||||
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_ERROR)
|
||||
elif "warning" in analysis:
|
||||
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG)
|
||||
else:
|
||||
del self.queries[query_id]
|
||||
time_taken = time.time() - start_time
|
||||
self.katrain.log(f"[{time_taken:.1f}][{analysis['id']}] KataGo Analysis Received: {analysis.keys()}", OUTPUT_DEBUG)
|
||||
self.katrain.log(line, OUTPUT_EXTRA_DEBUG)
|
||||
@@ -127,7 +153,7 @@ class KataGoEngine:
|
||||
self.katago_process.stdin.flush()
|
||||
except OSError as e:
|
||||
self.katrain.log(f"Engine died unexpectedly, possibly due to out of memory: {e}", OUTPUT_ERROR)
|
||||
return # do not raise, since
|
||||
return # do not raise, since there's nothing to catch it
|
||||
|
||||
def request_analysis(
|
||||
self,
|
||||
@@ -5,10 +5,10 @@ import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from core.common import var_to_grid, OUTPUT_INFO, OUTPUT_DEBUG
|
||||
from core.engine import KataGoEngine
|
||||
from core.game_node import GameNode
|
||||
from core.sgf_parser import SGF, Move
|
||||
from katrain.core.common import var_to_grid, OUTPUT_INFO, OUTPUT_DEBUG
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.game_node import GameNode
|
||||
from katrain.core.sgf_parser import SGF, Move
|
||||
|
||||
|
||||
class IllegalMoveException(Exception):
|
||||
@@ -26,7 +26,7 @@ class Game:
|
||||
|
||||
def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, move_tree: GameNode = None, analyze_fast=False):
|
||||
self.katrain = katrain
|
||||
if isinstance(engine, KataGoEngine):
|
||||
if not isinstance(engine, Dict):
|
||||
engine = {"B": engine, "W": engine}
|
||||
self.engines = engine
|
||||
self.config = config
|
||||
@@ -136,6 +136,7 @@ class Game:
|
||||
cn = self.current_node # avoid race conditions
|
||||
for _ in range(n_times):
|
||||
if not cn.is_root:
|
||||
cn.parent.set_favourite_child(cn)
|
||||
cn = cn.parent
|
||||
self.set_current_node(cn)
|
||||
|
||||
@@ -143,7 +144,7 @@ class Game:
|
||||
cn = self.current_node # avoid race conditions
|
||||
for _ in range(n_times):
|
||||
if cn.children:
|
||||
cn = cn.children[-1]
|
||||
cn = cn.favourite_child
|
||||
self.set_current_node(cn)
|
||||
|
||||
def switch_branch(self, direction):
|
||||
@@ -296,16 +297,16 @@ class Game:
|
||||
while i < len(thresholds) and points_lost < thresholds[i]:
|
||||
i += 1
|
||||
num_undos = num_undo_prompts[i] if i < len(num_undo_prompts) else 0
|
||||
xmsg = ". Please try again."
|
||||
xmsg = ". Please try once more"
|
||||
if num_undos == 0:
|
||||
undo = False
|
||||
elif num_undos < 1: # probability
|
||||
undo = int(node.undo_threshold < num_undos) and len(node.parent.children) == 1
|
||||
xmsg = f" (with {num_undos:.0%} probability at this level of mistake)" + xmsg
|
||||
else:
|
||||
undo = len(node.parent.children) <= num_undos
|
||||
if len(node.parent.children) == num_undos:
|
||||
xmsg = xmsg[:-1] + ", but note that this is your last try at this level of mistake."
|
||||
if len(node.parent.children) < num_undos:
|
||||
xmsg = ". Please try again (multiple tries remaining)."
|
||||
|
||||
node.auto_undo = undo
|
||||
if undo:
|
||||
self.undo(1)
|
||||
@@ -2,8 +2,8 @@ import copy
|
||||
import random
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from core.common import evaluation_class, var_to_grid
|
||||
from core.sgf_parser import Move, SGFNode
|
||||
from katrain.core.common import evaluation_class, var_to_grid
|
||||
from katrain.core.sgf_parser import Move, SGFNode
|
||||
|
||||
|
||||
class GameNode(SGFNode):
|
||||
@@ -18,6 +18,7 @@ class GameNode(SGFNode):
|
||||
self.ai_thoughts = ""
|
||||
self.move_number = 0
|
||||
self.undo_threshold = random.random() # for fractional undos, store the random threshold in the move itself for consistency
|
||||
self._favourite_child = None
|
||||
|
||||
def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None):
|
||||
properties = copy.copy(super().sgf_properties())
|
||||
@@ -33,7 +34,7 @@ class GameNode(SGFNode):
|
||||
properties["SQ"] = best_sq
|
||||
if top_x and "MA" not in properties:
|
||||
properties["MA"] = [top_x]
|
||||
comment = self.comment(sgf=True)
|
||||
comment = self.comment(sgf=True, interactive=False)
|
||||
if comment:
|
||||
properties["C"] = [properties.get("C", "") + comment]
|
||||
if self.is_root:
|
||||
@@ -48,6 +49,16 @@ class GameNode(SGFNode):
|
||||
def order_children(children):
|
||||
return sorted(children, key=lambda c: 0.5 if c.auto_undo is None else int(c.auto_undo)) # analyzed/not undone main, non-teach second, undone last
|
||||
|
||||
def set_favourite_child(self, c):
|
||||
self._favourite_child = c
|
||||
|
||||
@property
|
||||
def favourite_child(self) -> Optional["GameNode"]:
|
||||
if self._favourite_child:
|
||||
return self._favourite_child
|
||||
elif self.children:
|
||||
return self.children[0]
|
||||
|
||||
# various analysis functions
|
||||
def analyze(self, engine, priority=0, visits=None, time_limit=True, refine_move=None, analyze_fast=False):
|
||||
engine.request_analysis(
|
||||
@@ -88,11 +99,11 @@ class GameNode(SGFNode):
|
||||
|
||||
def make_pv(self, player, pv, interactive):
|
||||
pvtext = f"{player}{' '.join(pv)}"
|
||||
# if interactive:
|
||||
# pvtext = f"[u][ref={pvtext}][color=#334466]{pvtext}[/color][/ref][/u]"
|
||||
if interactive:
|
||||
pvtext = f"[u][ref={pvtext}][color=#334466]{pvtext}[/color][/ref][/u]"
|
||||
return pvtext
|
||||
|
||||
def comment(self, sgf=False, teach=False, hints=False, interactive=False):
|
||||
def comment(self, sgf=False, teach=False, hints=False, interactive=True):
|
||||
single_move = self.move
|
||||
if not self.parent or not single_move: # root
|
||||
return ""
|
||||
@@ -113,9 +124,8 @@ class GameNode(SGFNode):
|
||||
text += f"Predicted top move was {previous_top_move['move']} ({self.format_score(previous_top_move['scoreLead'])}).\n"
|
||||
else:
|
||||
text += f"Move was predicted best move.\n"
|
||||
if sgf:
|
||||
if previous_top_move.get("pv") and (sgf or hints):
|
||||
text += f"PV: {self.make_pv(single_move.player,previous_top_move['pv'],interactive)}\n"
|
||||
if previous_top_move.get("pv") and (sgf or hints):
|
||||
text += f"PV: {self.make_pv(single_move.player,previous_top_move['pv'],interactive)}\n"
|
||||
|
||||
if sgf or hints or teach:
|
||||
policy_ranking = self.parent.policy_ranking
|
||||
@@ -88,19 +88,23 @@ class SGFNode:
|
||||
|
||||
def sgf(self, **xargs) -> str:
|
||||
"""Generates an SGF, calling sgf_properties on each node with the given xargs, so it can filter relevant properties if needed."""
|
||||
if self.is_root:
|
||||
import sys
|
||||
|
||||
bszx, bszy = self.board_size
|
||||
sys.setrecursionlimit(max(sys.getrecursionlimit(), 4 * bszx * bszy))
|
||||
sgf_str = "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in self.sgf_properties(**xargs).items() if values])
|
||||
if self.children:
|
||||
children = [c.sgf(**xargs) for c in self.order_children(self.children)]
|
||||
if len(children) == 1:
|
||||
sgf_str += ";" + children[0]
|
||||
def node_sgf_str(node):
|
||||
return ";" + "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in node.sgf_properties(**xargs).items() if values])
|
||||
|
||||
stack = [")", self, "("]
|
||||
sgf_str = ""
|
||||
while stack:
|
||||
item = stack.pop()
|
||||
if isinstance(item, str):
|
||||
sgf_str += item
|
||||
else:
|
||||
sgf_str += "(;" + ")(;".join(children) + ")"
|
||||
return f"(;{sgf_str})" if self.is_root else sgf_str
|
||||
sgf_str += node_sgf_str(item)
|
||||
if len(item.children) == 1:
|
||||
stack.append(item.children[0])
|
||||
elif item.children:
|
||||
stack += sum([[")", c, "("] for c in self.order_children(item.children)[::-1]], [])
|
||||
return sgf_str
|
||||
|
||||
def add_list_property(self, property: str, values: List):
|
||||
"""Add some values to the property list."""
|
||||
@@ -210,7 +214,13 @@ class SGFNode:
|
||||
@property
|
||||
def nodes_in_tree(self) -> List:
|
||||
"""Returns all nodes in the tree rooted at this node"""
|
||||
return [self] + sum([c.nodes_in_tree for c in self.children], [])
|
||||
stack = [self]
|
||||
nodes = []
|
||||
while stack:
|
||||
item = stack.pop(0)
|
||||
nodes.append(item)
|
||||
stack += item.children
|
||||
return nodes
|
||||
|
||||
@property
|
||||
def nodes_from_root(self) -> List:
|
||||
@@ -620,8 +620,8 @@
|
||||
height: 0.00001 if not self.text else min(0.66*self.parent.height,self.label.texture_size[1])
|
||||
ScrollableLabel:
|
||||
id: info
|
||||
# markup: True
|
||||
# on_ref_press: root.katrain.board_gui.show_pv_from_label(args[1])
|
||||
markup: True
|
||||
on_ref_press: root.katrain.board_gui.show_pv_from_comments(args[1])
|
||||
size_hint: 1, None
|
||||
height: self.parent.height - status_label.height - 1
|
||||
BoxLayout:
|
||||
Whitespace-only changes.
@@ -1,5 +1,6 @@
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics.context_instructions import Color
|
||||
@@ -7,10 +8,10 @@ from kivy.graphics.vertex_instructions import Ellipse, Line, Rectangle
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
from core.common import OUTPUT_DEBUG, evaluation_class
|
||||
from core.game import Move
|
||||
from gui.kivyutils import draw_circle, draw_text
|
||||
from core.common import var_to_grid
|
||||
from katrain.core.common import OUTPUT_DEBUG, evaluation_class
|
||||
from katrain.core.game import Move
|
||||
from katrain.gui.kivyutils import draw_circle, draw_text
|
||||
from katrain.core.common import var_to_grid
|
||||
from kivy.core.window import Window
|
||||
|
||||
|
||||
@@ -24,20 +25,18 @@ class BadukPanWidget(Widget):
|
||||
self.gridpos_y = []
|
||||
self.grid_size = 0
|
||||
self.stone_size = 0
|
||||
self.last_eval = 0
|
||||
self.active_pv_moves = []
|
||||
self.show_pv_for = None
|
||||
self.animating_pv = None
|
||||
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents)
|
||||
self.last_mouse_pos = (0, 0)
|
||||
Window.bind(mouse_pos=self.on_mouse_pos)
|
||||
Clock.schedule_interval(self.animate_pv, 0.1)
|
||||
|
||||
# stone placement functions
|
||||
def _find_closest(self, pos, gridpos):
|
||||
return sorted([(abs(p - pos), i) for i, p in enumerate(gridpos)])[0]
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if touch.button != "left":
|
||||
return
|
||||
|
||||
def check_next_move_ghost(self, touch):
|
||||
if not self.gridpos_x:
|
||||
return
|
||||
xd, xp = self._find_closest(touch.x, self.gridpos_x)
|
||||
@@ -50,26 +49,38 @@ class BadukPanWidget(Widget):
|
||||
if prev_ghost != self.ghost_stone:
|
||||
self.draw_hover_contents()
|
||||
|
||||
def on_touch_move(self, touch): # on_motion on_touch_move
|
||||
return self.on_touch_down(touch)
|
||||
def on_touch_down(self, touch):
|
||||
self.animating_pv = None # any click kills PV from label/move
|
||||
self.draw_hover_contents()
|
||||
if touch.button != "left":
|
||||
return
|
||||
self.check_next_move_ghost(touch)
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
return self.check_next_move_ghost(touch)
|
||||
|
||||
def on_mouse_pos(self, *args): # https://gist.github.com/opqopq/15c707dc4cffc2b6455f
|
||||
last_show_pv = self.show_pv_for
|
||||
self.show_pv_for = None
|
||||
if self.get_root_window(): # do proceed if I'm not displayed <=> If have no parent
|
||||
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:
|
||||
near_move = [
|
||||
move
|
||||
for move in self.active_pv_moves
|
||||
(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 and abs(rel_pos[1] - self.gridpos_y[move[1]]) < self.grid_size / 2
|
||||
]
|
||||
if near_move:
|
||||
self.show_pv_for = near_move[0]
|
||||
if self.show_pv_for != last_show_pv:
|
||||
self.draw_hover_contents()
|
||||
self.set_animating_pv(near_move[0][0], near_move[0][1])
|
||||
else:
|
||||
self.animating_pv = None
|
||||
self.draw_hover_contents()
|
||||
if inside and self.animating_pv is not None:
|
||||
d_sq = (pos[0] - self.animating_pv[3][0]) ** 2 + (pos[1] - self.animating_pv[3][1])
|
||||
if d_sq > 2 * self.stone_size ** 2: # move too far from where it was activated
|
||||
self.animating_pv = None
|
||||
self.draw_hover_contents()
|
||||
self.last_mouse_pos = pos
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.button != "left" or not self.gridpos_x:
|
||||
@@ -86,10 +97,13 @@ class BadukPanWidget(Widget):
|
||||
if touch.is_double_tap: # navigate to move
|
||||
katrain.game.set_current_node(nodes_here[-1])
|
||||
katrain.update_state()
|
||||
else: # load comments
|
||||
else: # load comments & pv
|
||||
katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_DEBUG)
|
||||
katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_DEBUG)
|
||||
katrain.controls.info.text = nodes_here[-1].comment(sgf=True, interactive=True)
|
||||
katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
|
||||
katrain.controls.active_comment_node = nodes_here[-1].parent
|
||||
if nodes_here[-1].parent.analysis_ready:
|
||||
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
|
||||
@@ -297,12 +311,10 @@ class BadukPanWidget(Widget):
|
||||
else:
|
||||
evalcol = copy.copy(self.eval_color(points_lost))
|
||||
evalcol[3] = alpha
|
||||
self.active_pv_moves.append(move.coords)
|
||||
if teaching and move.coords == self.show_pv_for and child_node.analysis_ready:
|
||||
self.draw_pv(katrain, [move.gtp()] + child_node.candidate_moves[0].get("pv", []), [next_player, player])
|
||||
else:
|
||||
scale = self.ui_config["child_scale"]
|
||||
self.draw_stone(move.coords[0], move.coords[1], (*stone_color[move.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale)
|
||||
if teaching and child_node.auto_undo and child_node.analysis_ready:
|
||||
self.active_pv_moves.append((move.coords, child_node.candidate_moves[0]["pv"], child_node))
|
||||
scale = self.ui_config["child_scale"]
|
||||
self.draw_stone(move.coords[0], move.coords[1], (*stone_color[move.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale)
|
||||
|
||||
# hints or PV
|
||||
if katrain.controls.hints.active and not game_ended and not lock_ai:
|
||||
@@ -315,20 +327,38 @@ class BadukPanWidget(Widget):
|
||||
alpha += self.ui_config["top_move_x_alpha"]
|
||||
elif move_dict["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]:
|
||||
scale = 0.8
|
||||
self.active_pv_moves.append(move.coords)
|
||||
if move.coords == self.show_pv_for:
|
||||
self.draw_pv(katrain, move_dict.get("pv", [move.gtp()]), [next_player, player]) # if empty, show current move at least
|
||||
elif not self.show_pv_for:
|
||||
self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(move_dict["pointsLost"])[:3], alpha], scale=scale)
|
||||
self.active_pv_moves.append((move.coords, move_dict["pv"], current_node))
|
||||
self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(move_dict["pointsLost"])[:3], alpha], scale=scale)
|
||||
|
||||
# hover next move ghost stone
|
||||
if self.ghost_stone:
|
||||
self.draw_stone(*self.ghost_stone, (*stone_color[next_player], ghost_alpha))
|
||||
|
||||
def draw_pv(self, katrain, pv, next_last_player):
|
||||
# TODO: overlapping moves
|
||||
animating_pv = self.animating_pv
|
||||
if animating_pv:
|
||||
pv, node, start_time, _ = animating_pv
|
||||
delay = self.ui_config.get("anim_pv_time", 0.5)
|
||||
up_to_move = (time.time() - start_time) / delay
|
||||
self.draw_pv(pv, node, up_to_move)
|
||||
|
||||
def animate_pv(self, _dt):
|
||||
if self.animating_pv:
|
||||
self.draw_hover_contents()
|
||||
|
||||
def draw_pv(self, pv, node, up_to_move):
|
||||
katrain = self.katrain
|
||||
next_last_player = [node.next_player, node.player]
|
||||
stone_color = self.ui_config["stones"]
|
||||
cn = katrain.game.current_node
|
||||
if node != cn and node.parent != cn:
|
||||
hide_node = cn
|
||||
while hide_node and hide_node.move and hide_node != node:
|
||||
if not hide_node.move.is_pass:
|
||||
self.draw_stone(*hide_node.move.coords, [0.85, 0.68, 0.40, 0.8]) # board coloured dot
|
||||
hide_node = hide_node.parent
|
||||
for i, gtpmove in enumerate(pv):
|
||||
if i > up_to_move:
|
||||
return
|
||||
move_player = next_last_player[i % 2]
|
||||
opp_player = next_last_player[1 - i % 2]
|
||||
coords = Move.from_gtp(gtpmove).coords
|
||||
@@ -340,10 +370,17 @@ class BadukPanWidget(Widget):
|
||||
]
|
||||
else:
|
||||
board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]])
|
||||
sizefac = 0.95
|
||||
draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player])
|
||||
|
||||
draw_circle(board_coords, self.stone_size, stone_color[move_player])
|
||||
Color(*stone_color[opp_player])
|
||||
draw_text(pos=board_coords, text=str(i + 1), font_size=sizefac * self.grid_size / 1.45)
|
||||
draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size / 1.45)
|
||||
|
||||
def set_animating_pv(self, pv, node):
|
||||
if node is not None and (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)
|
||||
|
||||
def show_pv_from_comments(self, pv_str):
|
||||
self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent)
|
||||
|
||||
|
||||
class BadukPanControls(BoxLayout):
|
||||
@@ -1,7 +1,7 @@
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.popup import Popup
|
||||
|
||||
from gui.popups import ConfigAIPopup, ConfigTeacherPopup
|
||||
from katrain.gui.popups import ConfigAIPopup, ConfigTeacherPopup
|
||||
|
||||
|
||||
class Controls(BoxLayout):
|
||||
@@ -11,6 +11,7 @@ class Controls(BoxLayout):
|
||||
self.status_node = None
|
||||
self.ai_settings_popup = None
|
||||
self.teacher_settings_popup = None
|
||||
self.active_comment_node = None
|
||||
|
||||
def set_status(self, msg, at_node=None):
|
||||
self.status = msg
|
||||
@@ -60,8 +61,10 @@ class Controls(BoxLayout):
|
||||
current_player_is_ai_playing_human = current_node.player and "ai" in self.player_mode(current_node.player) and "ai" not in self.player_mode(current_node.next_player)
|
||||
if next_player_is_human_or_both_robots and not current_node.is_root and move:
|
||||
info += current_node.comment(teach="undo" in self.player_mode(current_node.player), hints=self.hints.active)
|
||||
self.active_comment_node = current_node
|
||||
elif current_player_is_ai_playing_human and current_node.parent:
|
||||
info += current_node.parent.comment(teach="undo" in self.player_mode(current_node.next_player), hints=self.hints.active)
|
||||
self.active_comment_node = current_node.parent
|
||||
|
||||
if current_node.analysis_ready:
|
||||
self.score.text = current_node.format_score()
|
||||
@@ -265,7 +265,7 @@ class ScoreGraph(Label):
|
||||
line_points = ListProperty([])
|
||||
dot_pos = ListProperty([0, 0])
|
||||
highlighted_index = NumericProperty(None)
|
||||
y_scale = NumericProperty(4)
|
||||
y_scale = NumericProperty(5)
|
||||
marginx = NumericProperty(0.015)
|
||||
marginy = NumericProperty(0.01)
|
||||
highlight_size = NumericProperty(5)
|
||||
@@ -278,7 +278,7 @@ class ScoreGraph(Label):
|
||||
self.nodes = [root]
|
||||
node = root
|
||||
while node.children:
|
||||
node = node.children[0]
|
||||
node = node.favourite_child
|
||||
self.nodes.append(node)
|
||||
self.highlighted_index = 0
|
||||
|
||||
@@ -289,7 +289,7 @@ class ScoreGraph(Label):
|
||||
nn_values = [n.score for n in nodes if n and n.score]
|
||||
val_range = min(nn_values or [0]), max(nn_values or [0])
|
||||
|
||||
self.y_scale = math.ceil(max(4, max(-val_range[0], val_range[1])) / 2) * 2
|
||||
self.y_scale = math.ceil(max(5, max(-val_range[0], val_range[1])) / 5) * 5
|
||||
|
||||
xscale = self.width * (1 - 2 * self.marginx) / max(len(values) - 1, 15)
|
||||
available_height = self.height * (1 - 2 * self.marginy)
|
||||
@@ -7,10 +7,10 @@ from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.popup import Popup
|
||||
|
||||
from core.common import OUTPUT_DEBUG, OUTPUT_ERROR
|
||||
from core.engine import KataGoEngine
|
||||
from core.game import Game, GameNode
|
||||
from gui.kivyutils import (
|
||||
from katrain.core.common import OUTPUT_DEBUG, OUTPUT_ERROR
|
||||
from katrain.core.engine import KataGoEngine
|
||||
from katrain.core.game import Game, GameNode
|
||||
from katrain.gui.kivyutils import (
|
||||
BackgroundLabel,
|
||||
LabelledCheckBox,
|
||||
LabelledFloatInput,
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 567 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 184 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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Whitespace-only changes.
File renamed without changes.
@@ -0,0 +1,51 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
package_data = {"": ["*.json", "*.kv"], "katrain": [], "tests": []}
|
||||
packages = find_packages(exclude=["bots", "tests"])
|
||||
version = re.search('^__version__\s*=\s*"(.*)"', open("katrain/__main__.py").read(), re.M).group(1)
|
||||
|
||||
|
||||
def include_data_files(directory):
|
||||
for root, subfolders, files in os.walk(directory):
|
||||
for fn in files:
|
||||
filename = os.path.join(root.replace("/", os.path.sep), fn)
|
||||
parts = filename.split(os.path.sep)
|
||||
package_data[parts[0]].append(os.path.join(*parts[1:]))
|
||||
|
||||
|
||||
include_data_files("katrain/KataGo")
|
||||
include_data_files("katrain/models")
|
||||
include_data_files("katrain/img/")
|
||||
include_data_files("katrain/img/flaticon")
|
||||
|
||||
print(packages, package_data)
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name="KaTrain",
|
||||
version=version,
|
||||
description="Go/Baduk/Weiqi playing and teaching app with a variety of AIs",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author="Sander Land",
|
||||
author_email="sander.land@gmail.com",
|
||||
url="https://github.com/sanderland/katrain",
|
||||
license="MIT",
|
||||
install_requires=[
|
||||
"kivy",
|
||||
"kivy_deps.glew;platform_system=='Windows'",
|
||||
"kivy_deps.sdl2;platform_system=='Windows'",
|
||||
"kivy_deps.gstreamer;platform_system=='Windows'",
|
||||
"importlib_resources ;python_version<'3.7'",
|
||||
],
|
||||
python_requires=">=3.6, <3.8",
|
||||
entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]},
|
||||
classifiers=["Development Status :: 4 - Beta", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3"],
|
||||
packages=packages,
|
||||
package_data=package_data,
|
||||
)
|
||||
+6
-4
@@ -6,10 +6,12 @@ block_cipher = None
|
||||
# pyinstaller spec/katrain.spec --noconfirm
|
||||
# --upx-dir my
|
||||
|
||||
a = Analysis(['..\\katrain.py'],
|
||||
a = Analysis(['..\\katrain\\__main__.py'],
|
||||
pathex=['C:\\Users\\sande\\Desktop\\katrain\\spec'],
|
||||
binaries=[],
|
||||
datas=[('..\\katrain.kv','.'),('..\\config.json','.'),('..\\KataGo','KataGo'),('..\\img','img')],
|
||||
datas=[('..\\katrain\\gui.kv','katrain'),('..\\katrain\\config.json','katrain'),
|
||||
('..\\katrain\\KataGo','katrain\KataGo'),('..\\katrain\img','katrain\img'),
|
||||
('..\\katrain\\models','katrain\models')],
|
||||
hiddenimports=["win32file","win32timezone"], # FileChooser in kivy loads this conditionally
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
@@ -34,8 +36,8 @@ exe = EXE(pyz,
|
||||
upx=True,
|
||||
console=True) # , icon='..\\icon.png'
|
||||
|
||||
EXCLUDE_SUFFIX = ['katago','b10-1.3.txt.gz','screenshot_analyze.png','screenshot_play.png']
|
||||
EXCLUDE = ['KataGoData']
|
||||
EXCLUDE_SUFFIX = ['katago']
|
||||
EXCLUDE = ['KataGoData','anim_','screenshot_','__pycache__']
|
||||
a.datas = [(ff,ft,tp) for ff,ft,tp in a.datas if not any(ff.endswith(suffix) for suffix in EXCLUDE_SUFFIX) and not any(kw in ff for kw in EXCLUDE)]
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.43.3]ST[2]RU[Chinese]SZ[19]KM[7.50]TM[7200]OT[3x60 byo-yomi]PW[Lee Sedol]PB[AlphaGo]WR[9p]DT[2016-03-13]C[Game 4 - Endurance
|
||||
|
||||
Commentary by Fan Hui 2p
|
||||
Expert Go analysis by Gu Li 9p and Zhou Ruiyang 9p
|
||||
Translated by Lucas Baker, Thomas Hubert, and Thore Graepel
|
||||
|
||||
When I arrived in the playing room on the morning of the fourth game, everyone appeared more relaxed than before. Whatever happened today, the match had already been decided. Nonetheless, there were still two games to play, and a true professional like Lee must give his all each time he sits down at the board.
|
||||
|
||||
When Lee arrived in the playing room, he looked serene, the burden of expectation lifted. Perhaps he would finally recover his composure, forget his surroundings, and simply play his best Go. One thing was certain: it was not in Lee's nature to surrender.
|
||||
|
||||
There were many fewer reporters in the press room this time. It seemed the media thought the interesting part was finished, and the match was headed for a final score of 5-0. But in a game of Go, although the information is open for all to see, often the results defy our expectations. Until the final stone is played, anything is possible.]RE[W+Resign];B[pd];W[dp];B[cd];W[qp];B[op];W[oq];B[nq];W[pq];B[cn];W[fq];B[mp]C[For the fourth game, Lee took White. Up to move 11, the opening was the same as the second game. AlphaGo is an extremely consistent player, and once it thinks a move is good, its opinion will not change.
|
||||
];W[po]LB[ed:B][qn:A]C[During the commentary for game 2, I mentioned that AlphaGo prefers to tenuki with 12 and approach at B. Previously, Lee chose to finish the joseki normally at A. White 12, however, is an interesting alternative! Perhaps it was a test: would AlphaGo still tenuki as in game 2?];B[iq]C[This time, AlphaGo chose the ordinary extension. After this move, AlphaGo's win rate was 50.5%. Both players had 1 hour and 52 minutes apiece.];W[ec]C[White played the usual corner approach.];B[hd];W[cg]C[Against Black's pincer, Lee counter-pincered to emphasize the left side. However, AlphaGo preferred the press as shown in the variation.];B[ed];W[cj];B[dc];W[bp];B[nc]C[At this point, Lee had 1 hour and 40 minutes left, AlphaGo 1 hour and 47 minutes.];W[qi]C[The opening had been a balanced one, and AlphaGo's win rate stood at 53%.];B[ep]C[The better people got to know AlphaGo, the more started calling it by friendly nicknames, such as "Master A" in China and "Master Al" in Korea. Although such names mean little on their own, they represented a growing acceptance of AlphaGo as a partner and teacher, together with whom we could advance our understanding.
|
||||
|
||||
During this competition, "Master A" never ceased to surprise, playing at least one extraordinary new move in every game. This move was the first such display in game 4.
|
||||
|
||||
Theoretically, Black 23 is quite vulgar, as it induces White to strengthen the corner. However, since White's lower side is already solid, if this move helps Black on the outside, is it really as crude as it looks? See the variation.
|
||||
|
||||
One of the major ideas in Chinese philosophy, from kung fu to Taoism, is the notion that "formlessness defeats form." This is not to suggest that one should do nothing, but rather that one should be ready to make use of any resource at any moment. In other words, complete flexibility is the surest path to success. When one is not committed to any style, there are no weak points at which to aim. Naturally, reaching this state demands a strong foundation and a formidable repertoire. AlphaGo seemed to have attained this "formless" style already: simple, easy to understand, and totally unexploitable.];W[eo];B[dk]C[Lee chose the outside hane, and just as I was reading out the cut, AlphaGo changed course with the shoulder hit at 25. Lee cracked a smile, as if he were looking at a naughty child. AlphaGo was being mischievous indeed!];W[fp]C[Coolly, Lee protected the corner with 26. Most players found this move too slow, and even AlphaGo thought White should respond with the crawl as shown in the variation. Nonetheless, I saw this move as a sign that Lee had finally found the confidence to play his own game, regardless of anyone's approval. This was the Lee Sedol I knew: the wolf that, starving in the winter winds, still waited for his prey to come closer, biding his time for the moment his intuition knew would come.];B[ck]C[When Black blocked here, AlphaGo’s win rate was 55%.];W[dj];B[ej];W[ei];B[fi]C[Perhaps because it was playing Black, AlphaGo played very aggressively during this game. With the double hane at 29 and 31, it seemed as if Black was attempting to completely overwhelm White.];W[eh];B[fh];W[bj]C[Even more shocking is that Lee submitted to it! Meek as a lamb, he let AlphaGo blockade the center and seal in White's group on the left side.
|
||||
|
||||
Sometimes to endure is difficult, sometimes it is irrational, and sometimes it is futile. Whatever it meant, Lee endured, and perhaps this was way of showing us his unwavering faith and unwillingness to surrender.
|
||||
|
||||
However, White did not need to suffer quite so badly. See the variation.];B[fk];W[fg];B[gg];W[ff];B[gf]C[At this point, AlphaGo's win rate reached 60%.];W[mc]C[After Black had constructed such a thick wall, it became urgent for White to invade the top. The attachment at 40 was a classic technique to do just that.];B[md]C[At this point, Lee Sedol had 1 hour and 15 minutes left, AlphaGo 1 hour and 35 minutes.];W[lc];B[nb];W[id]C[AlphaGo thought this move was problematic. In view of Black's thickness, White's most attractive option would have been to live on the spot, as shown in the variation.];B[hc];W[jg];B[pj]C[Compared to the previous games, Lee appeared much more relaxed and focused. Gone were the sighs, and the shaking of the head. Instead, he wore a look of intense concentration, as if waiting for something to arrive.
|
||||
|
||||
White gracefully leapt out into the center, but Black initiated a magnificent leaning attack!];W[pi];B[oj];W[oi];B[ni]C[The greater the pressure became on White's top group, the more time Lee took to ponder each move. When Black played 51, Lee hesitated even longer. If Black dared to play so boldly, choosing the hane with only two stones to White's three, I could not imagine any move for White except the cut! Even AlphaGo thought cutting was the only move.];W[nh]C[But Lee continued to endure! Against Black's hane, he haned in return.];B[mh];W[ng]C[Against the double hane, he extended!
|
||||
|
||||
"I can feel Lee's conviction," I wrote in my notebook, "waiting for the critical moment. But will that moment ever come?" As the game progressed, it seemed to everyone that Lee was once again on the verge of defeat.
|
||||
|
||||
At this point, Lee's clock had 51 minutes, AlphaGo's 1 hour and 28 minutes.];B[mg];W[mi];B[nj];W[mf];B[li]C[When Black ataried at 59, White had the option of linking up the group on the top. See the variation.];W[ne];B[nd];W[mj]C[Instead, however, Lee chose to pull out his center stone. With White's group now isolated and in grave peril, Lee's heart must have been overwhelmed with emotions. But perhaps he also sensed the long-awaited moment at hand.];B[lf]C[Lee now had 42 minutes, AlphaGo 1 hour and 22 minutes.];W[mk];B[me];W[nf];B[lh];W[qj]C[At this point, people suddenly began to congregate near the playing room. The rumor was spreading that the game was about to end, with AlphaGo victorious as expected. But Lee looked cool-headed as ever as he played the turn at 68. Was he really not afraid of dying?];B[kk]C[AlphaGo continued striding forward with the knight's move, aiming to swallow the white group whole. Lee pondered deeply over his next move. If White was to have any chance, he would have to seize it now.
|
||||
|
||||
At this point, Lee had 34 minutes, AlphaGo 1 hour and 19 minutes.];W[ik];B[ji]C[When Black closed off the center with 71, Lee had 27 minutes, AlphaGo 1 hour and 17 minutes.];W[gh];B[hj]C[After ten minutes of thought, Lee played the cut. Right on schedule, one minute later, AlphaGo enclosed White's cutting stone. Lee sighed, and kept thinking.];W[ge];B[he];W[fd];B[fc]C[At move 77, Lee had only 11 minutes left. Moreover, AlphaGo’s win rate had climbed over 70%. It seemed the game was over.
|
||||
|
||||
In fact, Lee had just completed the last of the preparations for his final charge!];W[ki]C[At last, Lee Sedol launched his attack. Like an earthquake, the wedge at 78 tore apart the cracks in Black's fortress! None of us had anticipated this. When Gu Li saw White 78 from his broadcasting studio in China, he shouted: "The divine move!" All of Lee's painstaking preparations were finally about to bear fruit.
|
||||
|
||||
Actually, Lee spent very little time on this move itself. Later, during the press conference, he told the assembled reporters that he had not spent much time calculating. He had simply played what felt right.
|
||||
|
||||
Without a doubt, this move is a spectacular flash of insight - but does it really work? See the variation for an explanation of White's plan and Black's best response.
|
||||
|
||||
Regardless, this move cast AlphaGo into complete confusion.];B[jj];W[lj]C[Move 78 might not really work, but AlphaGo was at a complete loss to deal with it. When Black pulled back, White blocked at 80, and Black could no longer kill the white stones unconditionally.];B[kh];W[jh]C[Everybody still thought AlphaGo was aiming for the ko shown in the variation, but this was not the case.];B[ml];W[nk];B[ol]C[Inexplicably, AlphaGo began trying to extract the dead stones on the right side!];W[ok];B[pk];W[pl];B[qk];W[nl];B[kj];W[ii]C[When White haned here, it was already difficult for Black to contain the white stones.
|
||||
|
||||
At this point, AlphaGo’s win rate was in free fall. When White played 92, it dropped all the way to 55%, a full 15 points lower than before! What was going on?!];B[rk];W[om]LB[rk:A][om:B]C[AlphaGo seemed to have gone crazy, and began thrashing around wildly. The exchange of A for B reinforced White's center with no compensation.];B[pg];W[ql];B[cp]LB[ki:A]C[This wedge was completely beyond understanding! Aja Huang, who had been the picture of calmness since the beginning of the match, now looked at me as if to ask, "What's happening!?" I answered with a look that said, "I don't know."
|
||||
|
||||
Even now, we still do not know why AlphaGo lost its mind, playing senseless blunders one after another. Only one thing is certain: the original cause was the wedge at A, Lee Sedol's mystical "divine move." White 78 was incontrovertible proof of his determination to fight on, and his perseverance was rewarded with victory.];W[co];B[oe];W[rl];B[sk];W[rj];B[hg]C[Little by little, AlphaGo recovered its sanity, but too late to save the game. When Black played 103, AlphaGo’s win rate had fallen to 30%, the first time such low numbers had appeared since the match began.
|
||||
|
||||
At this point, Lee entered byo-yomi, but for the first time, he was seeing the light of victory.
|
||||
|
||||
The press room had been half-empty for some time, but now people began to pour in. Many reporters had already left, but when they heard that Lee Sedol might win, they turned around and came rushing back!];W[ij];B[km];W[gi];B[fj];W[jl];B[kl];W[gl];B[fl];W[gm];B[ch];W[ee];B[eb];W[bg];B[dg];W[eg];B[en];W[fo];B[df];W[dh];B[im];W[hk];B[bn];W[if];B[gd];W[fe];B[hf];W[ih];B[bh];W[ci];B[ho];W[go];B[or];W[rg]C[Everyone was anxiously awaiting the news of Lee's victory. As the professional commentators grew more and more certain of the conclusion, the game continued to progress onscreen. Playing in byo-yomi, Lee continued to answer AlphaGo's every move with utmost caution.];B[dn];W[cq];B[pr];W[qr];B[rf];W[qg];B[qf];W[jc];B[gr];W[sf];B[se];W[sg];B[rd];W[bl];B[bk];W[ak];B[cl];W[hn];B[in];W[hp];B[fr];W[er];B[es];W[ds];B[ah];W[ai];B[kd];W[ie];B[kc];W[kb];B[gk];W[ib];B[qh];W[rh];B[qs];W[rs];B[oh];W[sl];B[of];W[sj];B[ni];W[nj];B[oo];W[jp]C[At move 180, AlphaGo finally resigned.
|
||||
|
||||
Lee Sedol entered the press room to thunderous applause. At long last, he could smile. It was a simple, modest smile, nothing world-changing - but then, aren't little moments like this the real source of joy in life?])
|
||||
@@ -0,0 +1,131 @@
|
||||
(;FF[4]
|
||||
CA[UTF-8]
|
||||
GM[1]
|
||||
DT[2020-03-31]
|
||||
PC[OGS: https://online-go.com/game/1234]
|
||||
GN[blabla]
|
||||
PB[bbb]
|
||||
PW[aaa]
|
||||
BR[4k]
|
||||
WR[5k]
|
||||
TM[259200]OT[86400 fischer]
|
||||
RE[?]
|
||||
SZ[19]
|
||||
KM[6.5]
|
||||
RU[Japanese]
|
||||
C[Game chats will be available in SGF downloads once the game has finished.
|
||||
]
|
||||
;B[qd]
|
||||
(;W[dp]
|
||||
(;B[pp]
|
||||
(;W[dc]
|
||||
(;B[oc]
|
||||
(;W[ce]
|
||||
(;B[qj]
|
||||
(;W[nq]
|
||||
(;B[lp]
|
||||
(;W[qq]
|
||||
(;B[qp]
|
||||
(;W[pq]
|
||||
(;B[op]
|
||||
(;W[rq]
|
||||
(;B[np]
|
||||
(;W[mq]
|
||||
(;B[rp]
|
||||
(;W[lq]
|
||||
(;B[nj]
|
||||
(;W[nd]
|
||||
(;B[od]
|
||||
(;W[jd]
|
||||
(;B[ne]
|
||||
(;W[kp]
|
||||
(;B[ck]
|
||||
(;W[cm]
|
||||
(;B[ch]
|
||||
(;W[em]
|
||||
(;B[fp]
|
||||
(;W[hp]
|
||||
(;B[go]
|
||||
(;W[ho]
|
||||
(;B[gn]
|
||||
(;W[ek]
|
||||
(;B[hm]
|
||||
(;W[hn]
|
||||
(;B[gm]
|
||||
(;W[im]
|
||||
(;B[cq]
|
||||
(;W[dq]
|
||||
(;B[cp]
|
||||
(;W[co]
|
||||
(;B[bo]
|
||||
(;W[bn]
|
||||
(;B[do]
|
||||
(;W[cn]
|
||||
(;B[dr]
|
||||
(;W[eo]
|
||||
(;B[eq]
|
||||
(;W[ep]
|
||||
(;B[fr]
|
||||
(;W[eh]
|
||||
(;B[dg]
|
||||
(;W[ef]
|
||||
(;B[df]
|
||||
(;W[de]
|
||||
(;B[eg]
|
||||
(;W[fg]
|
||||
(;B[ff]
|
||||
(;W[ee]
|
||||
(;B[gg]
|
||||
(;W[fh]
|
||||
(;B[ge]
|
||||
(;W[dh]
|
||||
(;B[bg]
|
||||
(;W[bf]
|
||||
(;B[cg]
|
||||
(;W[ci]
|
||||
(;B[bi]
|
||||
(;W[cj]
|
||||
(;B[bj]
|
||||
(;W[ag]
|
||||
(;B[ah]
|
||||
(;W[af]
|
||||
(;B[dk]
|
||||
(;W[ej]
|
||||
(;B[el]
|
||||
(;W[fl]
|
||||
(;B[dl]
|
||||
(;W[fm]
|
||||
(;B[fk]
|
||||
(;W[fj]
|
||||
(;B[gk]
|
||||
(;W[gj]
|
||||
(;B[dm]
|
||||
(;W[dn]
|
||||
(;B[fn]
|
||||
(;W[en]
|
||||
(;B[hk]
|
||||
(;W[ii]
|
||||
(;B[bl]
|
||||
(;W[bp]
|
||||
(;B[bq]
|
||||
(;W[ao]
|
||||
(;B[am]
|
||||
(;W[bm]
|
||||
(;B[al]
|
||||
(;W[cl]
|
||||
(;B[bk]
|
||||
(;W[aj]
|
||||
(;B[an]
|
||||
(;W[aq]
|
||||
(;B[ar]
|
||||
(;W[br]
|
||||
(;B[ap]
|
||||
(;W[bo]
|
||||
(;B[cr]
|
||||
(;W[aq]
|
||||
(;B[bs]
|
||||
(;W[cf]
|
||||
(;B[fo]
|
||||
(;W[bh]
|
||||
(;B[gl]
|
||||
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
|
||||
@@ -0,0 +1,243 @@
|
||||
(;
|
||||
GM[1]EV[Internet Go Server game: player1 vs player2]
|
||||
US[Brought to you by IGS PANDANET]
|
||||
CoPyright[
|
||||
Copyright (c) PANDANET Inc. 2020
|
||||
Permission to reproduce this game is given, provided proper credit is given.
|
||||
No warrantee, implied or explicit, is understood.
|
||||
Use of this game is an understanding and agreement of this notice.
|
||||
]
|
||||
GN[player1-player2(B) IGS]RE[W+34.5]
|
||||
PW[player1]WR[11k+]NW[18]
|
||||
PB[player2]BR[12k ]NB[17]
|
||||
PC[IGS: igs.joyjoy.net 6969]DT[2020-04-10]
|
||||
SZ[19]TM[60]KM[0.500000]LT[]
|
||||
RR[Normal]
|
||||
HA[2]AB[pd][dp]C[
|
||||
player2 12k : gg
|
||||
player1 11k+: Hi!
|
||||
player2 12k : Hi!
|
||||
]
|
||||
;W[pp]WL[45]
|
||||
;B[dc]BL[56]
|
||||
;W[ce]WL[40]
|
||||
;B[cd]BL[52]
|
||||
;W[de]WL[38]
|
||||
;B[bd]BL[49]
|
||||
;W[ec]WL[35]
|
||||
;B[dd]BL[44]
|
||||
;W[ed]WL[31]
|
||||
;B[be]BL[40]
|
||||
;W[cg]WL[28]
|
||||
;B[ci]BL[30]
|
||||
;W[eg]WL[13]
|
||||
;B[ej]BL[18]
|
||||
;W[fq]WL[9]
|
||||
;B[eq]BL[15]
|
||||
;W[fp]WL[7]
|
||||
;B[er]BL[12]
|
||||
;W[cn]WL[2]
|
||||
;B[bo]BL[8]
|
||||
;W[ck]WL[596]
|
||||
;B[cj]BL[5]
|
||||
;W[en]WL[593]
|
||||
;B[dk]BL[1]
|
||||
;W[cl]WL[589]
|
||||
;B[dl]BL[590]
|
||||
;W[dm]WL[586]
|
||||
;B[fl]BL[588]
|
||||
;W[gn]WL[577]
|
||||
;B[np]BL[583]
|
||||
;W[kq]WL[569]
|
||||
;B[or]BL[578]
|
||||
;W[pr]WL[562]
|
||||
;B[oq]BL[575]
|
||||
;W[pq]WL[559]
|
||||
;B[lp]BL[554]
|
||||
;W[kp]WL[556]
|
||||
;B[lq]BL[552]
|
||||
;W[kr]WL[551]
|
||||
;B[lr]BL[550]
|
||||
;W[qm]WL[544]
|
||||
;B[qk]BL[540]
|
||||
;W[qf]WL[539]
|
||||
;B[qh]BL[537]
|
||||
;W[qc]WL[535]
|
||||
;B[pc]BL[534]
|
||||
;W[qd]WL[531]
|
||||
;B[pb]BL[526]
|
||||
;W[qb]WL[528]
|
||||
;B[lc]BL[524]
|
||||
;W[jc]WL[524]
|
||||
;B[kc]BL[514]
|
||||
;W[jd]WL[521]
|
||||
;B[kd]BL[512]
|
||||
;W[je]WL[519]
|
||||
;B[ke]BL[509]
|
||||
;W[jg]WL[517]
|
||||
;B[qg]BL[496]
|
||||
;W[of]WL[511]
|
||||
;B[oe]BL[493]
|
||||
;W[pf]WL[507]
|
||||
;B[oh]BL[488]
|
||||
;W[ol]WL[501]
|
||||
;B[oj]BL[483]
|
||||
;W[mf]WL[491]
|
||||
;B[me]BL[480]
|
||||
;W[ne]WL[489]
|
||||
;B[nd]BL[478]
|
||||
;W[nf]WL[484]
|
||||
;B[le]BL[467]
|
||||
;W[fr]WL[592]
|
||||
;B[es]BL[464]
|
||||
;W[lo]WL[588]
|
||||
;B[mo]BL[461]
|
||||
;W[ln]WL[578]
|
||||
;B[mn]BL[598]
|
||||
;W[lm]WL[572]
|
||||
;B[hl]BL[592]
|
||||
;W[im]WL[566]
|
||||
;B[fh]BL[587]
|
||||
;W[fg]WL[562]
|
||||
;B[gh]BL[584]
|
||||
;W[gg]WL[560]
|
||||
;B[li]BL[581]
|
||||
;W[ii]WL[556]
|
||||
;B[hi]BL[578]
|
||||
;W[hh]WL[554]
|
||||
;B[hg]BL[575]
|
||||
;W[ih]WL[551]
|
||||
;B[hj]BL[573]
|
||||
;W[hf]WL[548]
|
||||
;B[eh]BL[555]
|
||||
;W[dh]WL[545]
|
||||
;B[di]BL[552]
|
||||
;W[bn]WL[537]
|
||||
;B[co]BL[549]
|
||||
;W[rl]WL[495]
|
||||
;B[rk]BL[545]
|
||||
;W[sk]WL[493]
|
||||
;B[sj]BL[542]
|
||||
;W[sl]WL[491]
|
||||
;B[rj]BL[540]
|
||||
;W[eb]WL[449]
|
||||
;B[db]BL[537]
|
||||
;W[da]WL[447]
|
||||
;B[ca]BL[534]
|
||||
;W[ea]WL[446]
|
||||
;B[bb]BL[532]
|
||||
;W[ao]WL[442]
|
||||
;B[ap]BL[529]
|
||||
;W[an]WL[440]
|
||||
;B[bp]BL[527]
|
||||
;W[pe]WL[405]
|
||||
;B[od]BL[524]
|
||||
;W[fs]WL[386]
|
||||
;B[ks]BL[520]
|
||||
;W[js]WL[383]
|
||||
;B[ls]BL[517]
|
||||
;W[jr]WL[380]
|
||||
;B[ps]BL[515]
|
||||
;W[qs]WL[590]
|
||||
;B[os]BL[512]
|
||||
;W[qr]WL[588]
|
||||
;B[ql]BL[509]
|
||||
;W[rm]WL[581]
|
||||
;B[pl]BL[598]
|
||||
;W[pm]WL[579]
|
||||
;B[on]BL[596]
|
||||
;W[pn]WL[576]
|
||||
;B[ok]BL[582]
|
||||
;W[ml]WL[573]
|
||||
;B[mk]BL[571]
|
||||
;W[lk]WL[568]
|
||||
;B[mj]BL[569]
|
||||
;W[rf]WL[560]
|
||||
;B[rg]BL[566]
|
||||
;W[pa]WL[557]
|
||||
;B[oa]BL[563]
|
||||
;W[qa]WL[555]
|
||||
;B[nb]BL[561]
|
||||
;W[jb]WL[550]
|
||||
;B[kb]BL[558]
|
||||
;W[ep]WL[532]
|
||||
;B[eo]BL[552]
|
||||
;W[fo]WL[524]
|
||||
;B[do]BL[550]
|
||||
;W[em]WL[518]
|
||||
;B[el]BL[544]
|
||||
;W[bj]WL[488]
|
||||
;B[bi]BL[541]
|
||||
;W[aj]WL[463]
|
||||
;B[ai]BL[536]
|
||||
;W[bk]WL[460]
|
||||
;B[bg]BL[534]
|
||||
;W[ch]WL[447]
|
||||
;B[bh]BL[529]
|
||||
;W[cf]WL[440]
|
||||
;B[bf]BL[521]
|
||||
;W[ij]WL[419]
|
||||
;B[hk]BL[515]
|
||||
;W[gm]WL[414]
|
||||
;B[gl]BL[512]
|
||||
;W[mh]WL[411]
|
||||
;B[mi]BL[505]
|
||||
;W[nh]WL[408]
|
||||
;B[ni]BL[502]
|
||||
;W[sg]WL[396]
|
||||
;B[sh]BL[498]
|
||||
;W[sf]WL[394]
|
||||
;B[nl]BL[488]
|
||||
;W[om]WL[597]
|
||||
;B[mm]BL[481]
|
||||
;W[ll]WL[592]
|
||||
;B[nm]BL[477]
|
||||
;W[oo]WL[590]
|
||||
;B[nn]BL[595]
|
||||
;W[op]WL[581]
|
||||
;B[cq]BL[588]
|
||||
;W[no]WL[578]
|
||||
;B[nq]BL[580]
|
||||
;W[ja]WL[569]
|
||||
;B[ka]BL[576]
|
||||
;W[il]WL[566]
|
||||
;B[lh]BL[566]
|
||||
;W[og]WL[549]
|
||||
;B[ph]BL[560]
|
||||
;W[ik]WL[544]
|
||||
;B[mg]BL[548]
|
||||
;W[ng]WL[541]
|
||||
;B[lg]BL[545]
|
||||
;W[kj]WL[535]
|
||||
;B[ki]BL[536]
|
||||
;W[ji]WL[532]
|
||||
;B[kf]BL[531]
|
||||
;W[jf]WL[530]
|
||||
;B[kh]BL[526]
|
||||
;W[hm]WL[513]
|
||||
;B[fm]BL[512]
|
||||
;W[fn]WL[510]
|
||||
;B[tt]BL[492]
|
||||
;W[lj]WL[489]
|
||||
;B[tt]BL[482]
|
||||
;W[tt]WL[484]
|
||||
;B[tt]BL[482]C[
|
||||
player1 11k+: player1 dead @ H13
|
||||
player2 done
|
||||
];
|
||||
|
||||
TW[am][al][ak][bm][bl][cm][dg][df][ef][ee][ff][fe][fd][fc][fb][fa][gs][gr]
|
||||
[gq][gp][go][gf][ge][gd][gc][gb][ga][hs][hr][hq][hp][ho][hn][hg][he][hd]
|
||||
[hc][hb][ha][is][ir][iq][ip][io][in][ig][if][ie][id][ic][ib][ia][jq][jp]
|
||||
[jo][jn][jm][jl][jk][jj][ko][kn][km][kl][kk][po][qq][qp][qo][qn][qe][rs]
|
||||
[rr][rq][rp][ro][rn][re][rd][rc][rb][ra][ss][sr][sq][sp][so][sn][sm][se]
|
||||
[sd][sc][sb][sa]
|
||||
|
||||
TB[as][ar][aq][ah][ag][af][ae][ad][ac][ab][aa][bs][br][bq][bc][ba][cs][cr]
|
||||
[cp][cc][cb][ds][dr][dq][dj][ek][ei][fk][fj][fi][gk][gj][gi][ld][lb][la]
|
||||
[ms][mr][mq][mp][md][mc][mb][ma][ns][nr][nk][nj][nc][na][oi][oc][ob][pk]
|
||||
[pj][pi][qj][qi][ri][rh][si]
|
||||
|
||||
OS[Trilan]
|
||||
|
||||
)
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from core.game import Game, IllegalMoveException, Move
|
||||
from katrain.core.game import Game, IllegalMoveException, Move
|
||||
|
||||
|
||||
class MockKaTrain:
|
||||
@@ -18,7 +18,7 @@ class TestBoard:
|
||||
return [c for c in b.chains if c]
|
||||
|
||||
def test_merge(self):
|
||||
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
|
||||
b = Game(MockKaTrain(), MockEngine(), config={"init_size": 9})
|
||||
b.play(Move.from_gtp("B9", player="B"))
|
||||
b.play(Move.from_gtp("A3", player="B"))
|
||||
b.play(Move.from_gtp("A9", player="B"))
|
||||
@@ -27,7 +27,7 @@ class TestBoard:
|
||||
assert 0 == len(b.prisoners)
|
||||
|
||||
def test_collide(self):
|
||||
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
|
||||
b = Game(MockKaTrain(), MockEngine(), config={"init_size": 9})
|
||||
b.play(Move.from_gtp("B9", player="B"))
|
||||
with pytest.raises(IllegalMoveException):
|
||||
b.play(Move.from_gtp("B9", player="W"))
|
||||
@@ -36,7 +36,7 @@ class TestBoard:
|
||||
assert 0 == len(b.prisoners)
|
||||
|
||||
def test_capture(self):
|
||||
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
|
||||
b = Game(MockKaTrain(), MockEngine(), config={"init_size": 9})
|
||||
b.play(Move.from_gtp("A2", player="B"))
|
||||
b.play(Move.from_gtp("B1", player="W"))
|
||||
b.play(Move.from_gtp("A1", player="W"))
|
||||
@@ -57,7 +57,7 @@ class TestBoard:
|
||||
assert 2 == len(b.prisoners)
|
||||
|
||||
def test_snapback(self):
|
||||
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
|
||||
b = Game(MockKaTrain(), MockEngine(), config={"init_size": 9})
|
||||
for move in ["C1", "D1", "E1", "C2", "D3", "E4", "F2", "F3", "F4"]:
|
||||
b.play(Move.from_gtp(move, player="B"))
|
||||
for move in ["D2", "E2", "C3", "D4", "C4"]:
|
||||
@@ -75,7 +75,7 @@ class TestBoard:
|
||||
assert 4 == len(b.prisoners)
|
||||
|
||||
def test_ko(self):
|
||||
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
|
||||
b = Game(MockKaTrain(), MockEngine(), config={"init_size": 9})
|
||||
for move in ["A2", "B1"]:
|
||||
b.play(Move.from_gtp(move, player="B"))
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
|
||||
from katrain.core.sgf_parser import SGF
|
||||
|
||||
|
||||
def test_simple():
|
||||
input_sgf = "(;GM[1]FF[4]SZ[19]DT[2020-04-12]AB[dd][dj];B[dp];W[pp];B[pj])"
|
||||
root = SGF.parse(input_sgf)
|
||||
assert "4" == root.get_property("FF")
|
||||
assert root.get_property("XYZ") is None
|
||||
assert "dp" == root.children[0].get_property("B")
|
||||
assert input_sgf == root.sgf()
|
||||
|
||||
|
||||
def test_branch():
|
||||
input_sgf = "(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]AB[dd][dj](;B[dp];W[pp](;B[pj])(;PL[B]AW[jp]C[sdfdsfdsf]))(;B[pd]))"
|
||||
root = SGF.parse(input_sgf)
|
||||
assert input_sgf == root.sgf()
|
||||
|
||||
|
||||
def test_weird_escape():
|
||||
input_sgf = """(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]C[how does it escape
|
||||
[
|
||||
or \\]
|
||||
])"""
|
||||
root = SGF.parse(input_sgf)
|
||||
assert input_sgf == root.sgf()
|
||||
|
||||
|
||||
def test_alphago():
|
||||
file = os.path.join(os.path.dirname(__file__), "data/LS vs AG - G4 - English.sgf")
|
||||
SGF.parse_file(file)
|
||||
|
||||
|
||||
def test_pandanet():
|
||||
file = os.path.join(os.path.dirname(__file__), "data/panda1.sgf")
|
||||
root = SGF.parse_file(file)
|
||||
root_props = {"GM", "EV", "US", "CoPyright", "GN", "RE", "PW", "WR", "NW", "PB", "BR", "NB", "PC", "DT", "SZ", "TM", "KM", "LT", "RR", "HA", "AB", "C"}
|
||||
assert root_props == root.properties.keys()
|
||||
|
||||
move = root
|
||||
while move.children:
|
||||
move = move.children[0]
|
||||
assert 94 == len(move.get_list_property("TW"))
|
||||
assert "Trilan" == move.get_property("OS")
|
||||
while move.parent:
|
||||
move = move.parent
|
||||
assert move is root
|
||||
|
||||
|
||||
def test_ogs():
|
||||
file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf")
|
||||
tree = SGF.parse_file(file)
|
||||
Reference in new issue
Block a user