diff --git a/.gitignore b/.gitignore index ef0f3a0..85908ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # additions KataGoData -KataGo/KataGoData +katrain/KataGo/KataGoData experiments .idea +.buildozer +bin gtp.log *.zip log.txt diff --git a/INSTALL.md b/INSTALL.md index 0ba8454..0f1c187 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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 ` 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 - +## 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 ` command. +* Execute the command `python setup.py install` +* Start the app by running `katrain` in the directory where you downloaded the scripts. + +# 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 diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..31d17ef --- /dev/null +++ b/Pipfile @@ -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" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..ba377c5 --- /dev/null +++ b/Pipfile.lock @@ -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" + } + } +} diff --git a/README.md b/README.md index 92e2903..470af51 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ but has since grown to include a wide range of features, including: | Analyze games | Play against an AI Teacher | | ------------- | ------------- | -| ![screenshot](img/anim_analyze.gif) | ![screenshot](img/anim_teach.gif) | +| ![screenshot](katrain/img/anim_analyze.gif) | ![screenshot](katrain/img/anim_teach.gif) | ## 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 diff --git a/bots/ai2gtp.py b/bots/ai2gtp.py index 0417ac6..c91d3be 100644 --- a/bots/ai2gtp.py +++ b/bots/ai2gtp.py @@ -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"] diff --git a/bots/engine_server.py b/bots/engine_server.py index cdf0156..761bd56 100644 --- a/bots/engine_server.py +++ b/bots/engine_server.py @@ -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 diff --git a/bots/selfplay.py b/bots/selfplay.py index 6a51ab4..0585402 100644 --- a/bots/selfplay.py +++ b/bots/selfplay.py @@ -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" diff --git a/bots/start_bot.py b/bots/start_bot.py index 72b07d0..010680a 100755 --- a/bots/start_bot.py +++ b/bots/start_bot.py @@ -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"] diff --git a/gui/__init__.py b/gui/__init__.py deleted file mode 100644 index f1c94bf..0000000 --- a/gui/__init__.py +++ /dev/null @@ -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 diff --git a/katrain.py b/katrain.py index 1853892..f4bb42b 100644 --- a/katrain.py +++ b/katrain.py @@ -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() diff --git a/KataGo/OpenCL.dll b/katrain/KataGo/OpenCL.dll similarity index 100% rename from KataGo/OpenCL.dll rename to katrain/KataGo/OpenCL.dll diff --git a/core/__init__.py b/katrain/KataGo/__init__.py similarity index 100% rename from core/__init__.py rename to katrain/KataGo/__init__.py diff --git a/KataGo/analysis_config.cfg b/katrain/KataGo/analysis_config.cfg similarity index 100% rename from KataGo/analysis_config.cfg rename to katrain/KataGo/analysis_config.cfg diff --git a/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll b/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll similarity index 100% rename from KataGo/boost_filesystem-vc142-mt-x64-1_72.dll rename to katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll diff --git a/KataGo/katago b/katrain/KataGo/katago similarity index 81% rename from KataGo/katago rename to katrain/KataGo/katago index 611b9cc..bd32582 100755 Binary files a/KataGo/katago and b/katrain/KataGo/katago differ diff --git a/KataGo/katago-bs52.exe b/katrain/KataGo/katago-bs52.exe similarity index 100% rename from KataGo/katago-bs52.exe rename to katrain/KataGo/katago-bs52.exe diff --git a/KataGo/katago.exe b/katrain/KataGo/katago.exe similarity index 100% rename from KataGo/katago.exe rename to katrain/KataGo/katago.exe diff --git a/KataGo/models/b10-1.3.txt.gz b/katrain/KataGo/models/b10-1.3.txt.gz similarity index 100% rename from KataGo/models/b10-1.3.txt.gz rename to katrain/KataGo/models/b10-1.3.txt.gz diff --git a/KataGo/models/b15-1.3.2.txt.gz b/katrain/KataGo/models/b15-1.3.2.txt.gz similarity index 100% rename from KataGo/models/b15-1.3.2.txt.gz rename to katrain/KataGo/models/b15-1.3.2.txt.gz diff --git a/KataGo/msvcp140.dll b/katrain/KataGo/msvcp140.dll similarity index 100% rename from KataGo/msvcp140.dll rename to katrain/KataGo/msvcp140.dll diff --git a/KataGo/msvcp140_1.dll b/katrain/KataGo/msvcp140_1.dll similarity index 100% rename from KataGo/msvcp140_1.dll rename to katrain/KataGo/msvcp140_1.dll diff --git a/KataGo/msvcp140_2.dll b/katrain/KataGo/msvcp140_2.dll similarity index 100% rename from KataGo/msvcp140_2.dll rename to katrain/KataGo/msvcp140_2.dll diff --git a/KataGo/vcruntime140.dll b/katrain/KataGo/vcruntime140.dll similarity index 100% rename from KataGo/vcruntime140.dll rename to katrain/KataGo/vcruntime140.dll diff --git a/KataGo/vcruntime140_1.dll b/katrain/KataGo/vcruntime140_1.dll similarity index 100% rename from KataGo/vcruntime140_1.dll rename to katrain/KataGo/vcruntime140_1.dll diff --git a/KataGo/zlib1.dll b/katrain/KataGo/zlib1.dll similarity index 100% rename from KataGo/zlib1.dll rename to katrain/KataGo/zlib1.dll diff --git a/katrain/__init__.py b/katrain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/katrain/__main__.py b/katrain/__main__.py new file mode 100644 index 0000000..3030b21 --- /dev/null +++ b/katrain/__main__.py @@ -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() diff --git a/config.json b/katrain/config.json similarity index 97% rename from config.json rename to katrain/config.json index c38fbaa..6f77f26 100644 --- a/config.json +++ b/katrain/config.json @@ -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, diff --git a/katrain/core/__init__.py b/katrain/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/ai.py b/katrain/core/ai.py similarity index 98% rename from core/ai.py rename to katrain/core/ai.py index 0753663..e9951b3 100644 --- a/core/ai.py +++ b/katrain/core/ai.py @@ -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]: diff --git a/core/common.py b/katrain/core/common.py similarity index 60% rename from core/common.py rename to katrain/core/common.py index b667dde..cdf841b 100644 --- a/core/common.py +++ b/katrain/core/common.py @@ -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 diff --git a/core/engine.py b/katrain/core/engine.py similarity index 94% rename from core/engine.py rename to katrain/core/engine.py index bda9d6d..bfcf27f 100644 --- a/core/engine.py +++ b/katrain/core/engine.py @@ -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, diff --git a/core/game.py b/katrain/core/game.py similarity index 98% rename from core/game.py rename to katrain/core/game.py index 08f0aa2..133722f 100644 --- a/core/game.py +++ b/katrain/core/game.py @@ -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): diff --git a/core/game_node.py b/katrain/core/game_node.py similarity index 98% rename from core/game_node.py rename to katrain/core/game_node.py index cb84456..a949b6a 100644 --- a/core/game_node.py +++ b/katrain/core/game_node.py @@ -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): diff --git a/core/sgf_parser.py b/katrain/core/sgf_parser.py similarity index 100% rename from core/sgf_parser.py rename to katrain/core/sgf_parser.py diff --git a/katrain.kv b/katrain/gui.kv similarity index 100% rename from katrain.kv rename to katrain/gui.kv diff --git a/katrain/gui/__init__.py b/katrain/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/badukpan.py b/katrain/gui/badukpan.py similarity index 99% rename from gui/badukpan.py rename to katrain/gui/badukpan.py index 003439f..bc05e70 100644 --- a/gui/badukpan.py +++ b/katrain/gui/badukpan.py @@ -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 diff --git a/gui/controls.py b/katrain/gui/controls.py similarity index 98% rename from gui/controls.py rename to katrain/gui/controls.py index 690f4f4..25e2711 100644 --- a/gui/controls.py +++ b/katrain/gui/controls.py @@ -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): diff --git a/gui/kivyutils.py b/katrain/gui/kivyutils.py similarity index 100% rename from gui/kivyutils.py rename to katrain/gui/kivyutils.py diff --git a/gui/popups.py b/katrain/gui/popups.py similarity index 98% rename from gui/popups.py rename to katrain/gui/popups.py index bb21833..29f5748 100644 --- a/gui/popups.py +++ b/katrain/gui/popups.py @@ -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, diff --git a/img/anim_analyze.gif b/katrain/img/anim_analyze.gif similarity index 100% rename from img/anim_analyze.gif rename to katrain/img/anim_analyze.gif diff --git a/img/anim_pvtop.gif b/katrain/img/anim_pvtop.gif similarity index 100% rename from img/anim_pvtop.gif rename to katrain/img/anim_pvtop.gif diff --git a/img/anim_teach.gif b/katrain/img/anim_teach.gif similarity index 100% rename from img/anim_teach.gif rename to katrain/img/anim_teach.gif diff --git a/img/flaticon/bar.png b/katrain/img/flaticon/bar.png similarity index 100% rename from img/flaticon/bar.png rename to katrain/img/flaticon/bar.png diff --git a/img/flaticon/gears.png b/katrain/img/flaticon/gears.png similarity index 100% rename from img/flaticon/gears.png rename to katrain/img/flaticon/gears.png diff --git a/img/flaticon/go-game.png b/katrain/img/flaticon/go-game.png similarity index 100% rename from img/flaticon/go-game.png rename to katrain/img/flaticon/go-game.png diff --git a/img/flaticon/next.png b/katrain/img/flaticon/next.png similarity index 100% rename from img/flaticon/next.png rename to katrain/img/flaticon/next.png diff --git a/img/flaticon/next5.png b/katrain/img/flaticon/next5.png similarity index 100% rename from img/flaticon/next5.png rename to katrain/img/flaticon/next5.png diff --git a/img/flaticon/next999.png b/katrain/img/flaticon/next999.png similarity index 100% rename from img/flaticon/next999.png rename to katrain/img/flaticon/next999.png diff --git a/img/flaticon/open-folder.png b/katrain/img/flaticon/open-folder.png similarity index 100% rename from img/flaticon/open-folder.png rename to katrain/img/flaticon/open-folder.png diff --git a/img/flaticon/previous.png b/katrain/img/flaticon/previous.png similarity index 100% rename from img/flaticon/previous.png rename to katrain/img/flaticon/previous.png diff --git a/img/flaticon/previous5.png b/katrain/img/flaticon/previous5.png similarity index 100% rename from img/flaticon/previous5.png rename to katrain/img/flaticon/previous5.png diff --git a/img/flaticon/previous999.png b/katrain/img/flaticon/previous999.png similarity index 100% rename from img/flaticon/previous999.png rename to katrain/img/flaticon/previous999.png diff --git a/img/icon.png b/katrain/img/icon.png similarity index 100% rename from img/icon.png rename to katrain/img/icon.png diff --git a/img/screenshot_analyze.png b/katrain/img/screenshot_analyze.png similarity index 100% rename from img/screenshot_analyze.png rename to katrain/img/screenshot_analyze.png diff --git a/img/screenshot_play.png b/katrain/img/screenshot_play.png similarity index 100% rename from img/screenshot_play.png rename to katrain/img/screenshot_play.png diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f990cf3 --- /dev/null +++ b/setup.py @@ -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, +) diff --git a/tests/test_board.py b/tests/test_board.py index 5dff3ee..b389e48 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -1,6 +1,6 @@ import pytest -from core.game import Game, IllegalMoveException, Move +from katrain.core.game import Game, IllegalMoveException, Move class MockKaTrain: diff --git a/tests/test_parser.py b/tests/test_parser.py index 2a424f5..8872617 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,6 +1,6 @@ import os -from core.sgf_parser import SGF +from katrain.core.sgf_parser import SGF def test_simple():