package up
This commit is contained in:
1 parent
3e04ac2942
commit
f8341801ee
61 files changed
+693
-435
No files matched your search
+3
-1
@@ -1,8 +1,10 @@
|
||||
# additions
|
||||
KataGoData
|
||||
KataGo/KataGoData
|
||||
katrain/KataGo/KataGoData
|
||||
experiments
|
||||
.idea
|
||||
.buildozer
|
||||
bin
|
||||
gtp.log
|
||||
*.zip
|
||||
log.txt
|
||||
|
||||
+31
-41
@@ -1,55 +1,45 @@
|
||||
# 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`
|
||||
## Installation and running KaTrain from PyPi
|
||||
* Run `pip3 install katrain`
|
||||
* The first time you run KaTrain you will 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 `katago` and then click 'Apply and Save'.
|
||||
* If katago is not in your path, try `/usr/local/bin/katago`, or the path where you compiled KataGo.
|
||||
* Run the program by typing `katrain` in the terminal.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ but has since grown to include a wide range of features, including:
|
||||
|
||||
| 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
|
||||
|
||||
+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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+4
-4
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
-347
@@ -1,347 +1,3 @@
|
||||
from kivy.config import Config # isort:skip
|
||||
|
||||
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.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="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
|
||||
# for backward compatibility
|
||||
from katrain.__main__ import run_app
|
||||
run_app()
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Whitespace-only changes.
@@ -0,0 +1,360 @@
|
||||
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
|
||||
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:
|
||||
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):
|
||||
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__}"
|
||||
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)
|
||||
|
||||
|
||||
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": "katrain/KataGo/katago",
|
||||
"model": "katrain/KataGo/models/b15-1.3.2.txt.gz",
|
||||
"config": "katrain/KataGo/analysis_config.cfg",
|
||||
"threads": 16,
|
||||
"max_visits": 500,
|
||||
"fast_visits": 50,
|
||||
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]:
|
||||
@@ -1,4 +1,10 @@
|
||||
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
|
||||
@@ -22,3 +28,13 @@ def evaluation_class(points_lost: float, eval_thresholds: List[float]):
|
||||
while i < len(eval_thresholds) - 1 and points_lost < eval_thresholds[i]:
|
||||
i += 1
|
||||
return i
|
||||
|
||||
|
||||
def find_package_resource(path):
|
||||
if path.startswith("katrain"):
|
||||
parts = path.replace("\\", "/").split("/")
|
||||
with pkg_resources.path(".".join(parts[:-1]), parts[-1]) as path_obj:
|
||||
found_path = str(path_obj)
|
||||
return found_path
|
||||
else:
|
||||
return path # absolute path
|
||||
@@ -5,9 +5,10 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Optional
|
||||
import importlib.resources as pkg_resources
|
||||
|
||||
from core.common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, OUTPUT_INFO
|
||||
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 +28,8 @@ 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']}"
|
||||
|
||||
self.command = f"{find_package_resource(config['katago'])} analysis -model {config['model']} -config {config['config']} -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
|
||||
@@ -45,7 +47,7 @@ class KataGoEngine:
|
||||
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:
|
||||
except (FileNotFoundError, PermissionError) 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,
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
File renamed without changes.
File renamed without changes.
Whitespace-only changes.
@@ -8,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
|
||||
|
||||
|
||||
@@ -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):
|
||||
File renamed without changes.
@@ -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,
|
||||
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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,43 @@
|
||||
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, 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/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"],
|
||||
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,
|
||||
)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from core.game import Game, IllegalMoveException, Move
|
||||
from katrain.core.game import Game, IllegalMoveException, Move
|
||||
|
||||
|
||||
class MockKaTrain:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from core.sgf_parser import SGF
|
||||
from katrain.core.sgf_parser import SGF
|
||||
|
||||
|
||||
def test_simple():
|
||||
|
||||
Reference in new issue
Block a user