From 8428cf15aa5408d53c1d6c431fb23b7176976183 Mon Sep 17 00:00:00 2001 From: Sander Land Date: Sat, 18 Apr 2020 00:49:34 +0200 Subject: [PATCH] popups galore --- config.json | 15 ++++--- engine.py | 7 +++- game.py | 60 ++++++++++++++++----------- game_node.py | 8 ++-- gui/__init__.py | 2 +- gui/kivyutils.py | 64 +++++++++++++++++++++++++++++ gui/popups.py | 99 ++++++++++++++++++++++++++++++++++++++++++++- katrain.kv | 96 +++++++++++++++++++++++++++++-------------- katrain.py | 73 ++++++++++++++++++++------------- tests/__init__.py | 0 tests/test_board.py | 50 +++++++++++------------ 11 files changed, 352 insertions(+), 122 deletions(-) create mode 100644 tests/__init__.py diff --git a/config.json b/config.json index 795934b..56a6ecb 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "engine": { - "command": "KataGo/katago", + "katago": "KataGo/katago", "model": " models/b15-1.3.2.txt.gz", "config": "KataGo/analysis_config.cfg", "threads": 8, @@ -16,13 +16,16 @@ "size": 19, "komi_19": 6.5, "komi_13": 6.5, - "komi_9": 6.5, + "komi_9": 6.5 + }, + "trainer": { + "lock_undos": true, + "num_undo_prompts": 1, + "undo_point_threshold": 1.5, "balance_play_target_score": 2, "balance_play_randomize_eval": 1, "balance_play_min_eval": 2, - "balance_play_min_visits": 20, - "undo_point_threshold": 1.5, - "num_undo_prompts": 1 + "balance_play_min_visits": 20 }, "board_ui": { "size_min": 1, @@ -43,6 +46,6 @@ "eval_off_show_last": 3 }, "debug": { - "level": 1 + "level": 2 } } diff --git a/engine.py b/engine.py index d68a6bb..a6a49a3 100644 --- a/engine.py +++ b/engine.py @@ -43,6 +43,11 @@ class KataGoEngine: self.base_priority += 1 self.queries = {} + def shutdown(self): + process = getattr(self, "katago_process") + if process: + process.terminate() + def is_idle(self): return not self.queries @@ -55,7 +60,7 @@ class KataGoEngine: continue analysis = json.loads(line) if "error" in analysis: - self.katrain.log(f"ERROR IN KATA ANALYSIS: {analysis['error']}", OUTPUT_ERROR) + self.katrain.log(f"{analysis} received from KataGo", OUTPUT_ERROR) elif analysis["id"] in self.queries: callback, start_time = self.queries[analysis["id"]] time_taken = time.time() - start_time diff --git a/game.py b/game.py index ed6cd87..0124f75 100644 --- a/game.py +++ b/game.py @@ -1,3 +1,4 @@ +import math import os import random from datetime import datetime @@ -20,10 +21,9 @@ class KaTrainSGF(SGF): class Game: """Represents a game of go, including an implementation of capture rules.""" - GAME_COUNTER = 0 + DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "RU": "JP", "AP": "KaTrain:https://github.com/sanderland/katrain"} def __init__(self, katrain, engine, config, board_size=None, move_tree=None): - Game.GAME_COUNTER += 1 self.katrain = katrain self.engine = engine self.config = config @@ -39,9 +39,7 @@ class Game: else: self.board_size = board_size or config["size"] self.komi = self.config.get(f"komi_{self.board_size}", 6.5) - self.root = GameNode( - properties={"GM": 1, "FF": 4, "RU": "JP", "SZ": self.board_size, "KM": self.komi, "AP": "KaTrain:https://github.com/sanderland/katrain", "DT": self.game_id} - ) + self.root = GameNode(properties={"SZ": self.board_size, "KM": self.komi, "DT": self.game_id, **Game.DEFAULT_PROPERTIES}) self.current_node = self.root self._init_chains() @@ -62,7 +60,7 @@ class Game: try: # for m in self.moves: for node in self.current_node.nodes_from_root: - for m in node.move_with_placements: + for m in node.move_with_placements: # TODO: placements are never illegal self._validate_move_and_update_chains(m, True) # ignore ko since we didn't know if it was forced except IllegalMoveException as e: raise Exception(f"Unexpected illegal move ({str(e)})") @@ -119,38 +117,54 @@ class Game: try: self._validate_move_and_update_chains(move, ignore_ko) except IllegalMoveException: + self._init_chains() raise played_node = self.current_node.play(move) self.current_node = played_node played_node.analyze(self.engine) return played_node - def undo(self): - if self.current_node is not self.root: - self.current_node = self.current_node.parent - self._init_chains() - - def redo(self): + def undo(self, n_times=1): cn = self.current_node # avoid race conditions - if cn.children: - self.current_node = cn.children[-1] - self._init_chains() + for _ in range(n_times): + if not cn.is_root: + cn = cn.parent + self.current_node = cn + self._init_chains() + + def redo(self, n_times=1): + cn = self.current_node # avoid race conditions + for _ in range(n_times): + if cn.children: + cn = cn.children[-1] + self.current_node = cn + self._init_chains() def switch_branch(self, direction): - cm = self.current_node # avoid race conditions - if cm.parent and len(cm.parent.children) > 1: - ix = cm.parent.children.index(cm) - self.current_node = cm.parent.children[(ix + direction) % len(cm.parent.children)] + cn = self.current_node # avoid race conditions + if cn.parent and len(cn.parent.children) > 1: + ix = cn.parent.children.index(cn) + self.current_node = cn.parent.children[(ix + direction) % len(cn.parent.children)] self._init_chains() def place_handicap_stones(self, n_handicaps): near = 3 if self.board_size >= 13 else 2 far = self.board_size - 1 - near middle = self.board_size // 2 - stones = [(far, far), (near, near), (far, near), (near, far)] - if n_handicaps % 2 == 1: - stones.append((middle, middle)) - stones += [(near, middle), (far, middle), (middle, near), (middle, far)] + if n_handicaps > 9: + stones_per_row = math.ceil(math.sqrt(n_handicaps)) + spacing = (far - near) / (stones_per_row - 1) + if spacing < near: + far += 1 + near -= 1 + spacing = (far - near) / (stones_per_row - 1) + coords = [math.floor(0.5 + near + i * spacing) for i in range(stones_per_row)] + stones = sorted([(x, y) for x in coords for y in coords], key=lambda xy: -((xy[0] - self.board_size / 2) ** 2 + (xy[1] - self.board_size / 2) ** 2)) + else: + stones = [(far, far), (near, near), (far, near), (near, far)] + if n_handicaps % 2 == 1: + stones.append((middle, middle)) + stones += [(near, middle), (far, middle), (middle, near), (middle, far)] self.root.add_property("AB", [Move(stone).sgf(board_size=self.board_size) for stone in stones[:n_handicaps]]) @property diff --git a/game_node.py b/game_node.py index 4f6b74e..cfd4171 100644 --- a/game_node.py +++ b/game_node.py @@ -45,8 +45,7 @@ class GameNode(SGFNode): def format_win_rate(self, win_rate=None): win_rate = win_rate or self.analysis[0]["winrate"] - b_adv = win_rate - 0.5 - return f"{'B' if b_adv > 0 else 'W'}+{abs(b_adv):.1%}" + return f"{'B' if win_rate > 0.5 else 'W'} {max(win_rate,1-win_rate):.1%}" def comment(self, sgf=False, eval=False, hints=False): single_move = self.single_move @@ -60,11 +59,10 @@ class GameNode(SGFNode): if sgf: text += f"Score: {self.format_score(score)}\n" if self.parent and self.parent.analysis_ready: - if sgf or hints: + if sgf or hints and self.parent.analysis[0]["move"] != single_move.gtp(): text += f"Top move was {self.parent.analysis[0]['move']} ({self.format_score(self.parent.analysis[0]['scoreLead'])})\n" - elif self.parent.analysis[0]["move"] != single_move.gtp(): points_lost = self.points_lost - if points_lost > 0.5: + if sgf and points_lost > 0.5: text += f"Estimated point loss: {points_lost:.1f}\n" else: text = "No analysis available" if sgf else "Analyzing move..." diff --git a/gui/__init__.py b/gui/__init__.py index 1162215..a601ede 100644 --- a/gui/__init__.py +++ b/gui/__init__.py @@ -1,4 +1,4 @@ from gui.badukpan import BadukPanWidget from gui.controls import Controls -from gui.kivyutils import BWCheckBoxHint, CensorableLabel, CensorableScoreLabel, CheckBoxHint +from gui.kivyutils import BWCheckBoxHint, CensorableLabel, CensorableScoreLabel, CheckBoxHint, LabelledFloatInput, LabelledIntInput, StyledButton from gui.popups import LoadSGFPopup diff --git a/gui/kivyutils.py b/gui/kivyutils.py index 1d6936b..fdd6b69 100644 --- a/gui/kivyutils.py +++ b/gui/kivyutils.py @@ -1,6 +1,16 @@ from kivy.core.text import Label as CoreLabel from kivy.graphics import * +from kivy.properties import BooleanProperty, StringProperty from kivy.uix.boxlayout import BoxLayout +import re + +from kivy.uix.button import Button +from kivy.uix.checkbox import CheckBox +from kivy.uix.textinput import TextInput + + +class StyledButton(Button): + pass class CheckBoxHint(BoxLayout): @@ -14,6 +24,60 @@ class CheckBoxHint(BoxLayout): pass +class LabelledTextInput(TextInput): + input_property = StringProperty("") + + @property + def input_value(self): + return self.text + + +class LabelledCheckBox(CheckBox): + input_property = StringProperty("") + + def __init__(self, text=None, **kwargs): + if text is not None: + kwargs["active"] = bool(text) + super().__init__(**kwargs) + + @property + def input_value(self): + return bool(self.active) + + +class LabelledFloatInput(LabelledTextInput): + signed = BooleanProperty(True) + pat = re.compile("[^0-9-]") + + def insert_text(self, substring, from_undo=False): + pat = self.pat + if "." in self.text: + s = re.sub(pat, "", substring) + else: + s = ".".join([re.sub(pat, "", s) for s in substring.split(".", 1)]) + r = super().insert_text(s, from_undo=from_undo) + if not self.signed and "-" in self.text: + self.text = self.text.replace("-", "") + elif self.text and "-" in self.text[1:]: + self.text = self.text[0] + self.text[1:].replace("-", "") + return r + + @property + def input_value(self): + return float(self.text) + + +class LabelledIntInput(LabelledTextInput): + pat = re.compile("[^0-9]") + + def insert_text(self, substring, from_undo=False): + return super().insert_text(re.sub(self.pat, "", substring), from_undo=from_undo) + + @property + def input_value(self): + return int(self.text) + + class BWCheckBoxHint(BoxLayout): __events__ = ("on_active",) diff --git a/gui/popups.py b/gui/popups.py index efc7907..927a11c 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -1,10 +1,105 @@ from kivy.uix.boxlayout import BoxLayout import os +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.label import Label +from kivy.uix.textinput import TextInput + +from game import Game, GameNode +from gui.kivyutils import LabelledFloatInput, LabelledIntInput, LabelledTextInput, StyledButton, LabelledCheckBox + + +class InputParseError(Exception): + pass + + +class QuickConfigGui(BoxLayout): + def __init__(self, katrain, popup, initial_values=None): + super().__init__() + self.katrain = katrain + self.popup = popup + if initial_values: + self.set_properties(self, initial_values) + + def collect_properties(self, widget): + if isinstance(widget, LabelledTextInput): + try: + ret = {widget.input_property: widget.input_value} + except Exception as e: + raise InputParseError(f"Could not parse value for {widget.input_property} ({widget.__class__}): {e}") + else: + ret = {} + for c in widget.children: + for k, v in self.collect_properties(c).items(): + ret[k] = v + return ret + + def set_properties(self, widget, properties): + if isinstance(widget, LabelledTextInput): + key = widget.input_property + if key in properties: + widget.text = str(properties[key]) + for c in widget.children: + self.set_properties(c, properties) + class LoadSGFPopup(BoxLayout): pass -class NewGamePopup(BoxLayout): - pass +class NewGamePopup(QuickConfigGui): + def new_game(self): + new_root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **self.collect_properties(self)}) + self.katrain("new-game", None, new_root) + self.popup.dismiss() + + +class ConfigPopup(QuickConfigGui): + @staticmethod + def type_to_widget_class(value): + if isinstance(value, float): + return LabelledFloatInput + elif isinstance(value, bool): + return LabelledCheckBox + elif isinstance(value, int): + return LabelledIntInput + else: + return LabelledTextInput + + def __init__(self, katrain, popup, config): + self.config = config + self.orientation = "vertical" + super().__init__(katrain, popup) + cols = [BoxLayout(orientation="vertical"), BoxLayout(orientation="vertical")] + props_in_col = [0, 0] + for k1, all_d in config.items(): + d = {k: v for k, v in all_d.items() if isinstance(v, (int, float, str, bool))} # no complex objects + cat = GridLayout(cols=2, rows=len(d) + 1, size_hint=(1, len(d) + 1)) + cat.add_widget(Label(text="Settings for", bold=True)) + cat.add_widget(Label(text=k1, bold=True)) + for k2, v in d.items(): + cat.add_widget(Label(text=f"{k2}:")) + print(v, v.__class__, self.type_to_widget_class(v)) + cat.add_widget(self.type_to_widget_class(v)(text=str(v), input_property=f"{k1}/{k2}")) + if props_in_col[0] <= props_in_col[1]: + cols[0].add_widget(cat) + props_in_col[0] += len(d) + else: + cols[1].add_widget(cat) + props_in_col[1] += len(d) + + col_container = BoxLayout(size_hint=(1, 0.95)) + col_container.add_widget(cols[0]) + col_container.add_widget(cols[1]) + self.add_widget(col_container) + self.save_button = StyledButton(text="Update Settings", on_press=lambda _: self.update_config(), size_hint=(1, 0.05)) + self.add_widget(self.save_button) + + def update_config(self): + try: + print(self.collect_properties(self)) + self.popup.dismiss() + except InputParseError as e: + self.save_button.text = str(e) + print(e) diff --git a/katrain.kv b/katrain.kv index f14654d..b2aa081 100644 --- a/katrain.kv +++ b/katrain.kv @@ -1,6 +1,6 @@ #:kivy 1.11.0 -: +: text_color: 0.95,0.95,0.95,1 button_color: 0.157,0.455,0.753,1 button_color_down: (*[c/2 for c in self.button_color[:3]],1) @@ -31,6 +31,9 @@ color: (0.05,0.05,0.05,1) + + color: (0.95,0.95,0.95,1) + : halign: 'center' valign: 'center' @@ -191,8 +194,47 @@ size_hint: 0.05,1 - Label: - text: "New Game" + orientation: 'vertical' + BoxLayout: + size_hint: 1, 0.9 + orientation: 'horizontal' + GridLayout: + size_hint: 0.5, 1 + rows: 10 + cols: 2 + Label: + text: "Size" + size_hint: 0.5, 0.1 + IntInput: + text: '19' + size_hint: 0.5, 0.1 + input_property: 'SZ' + Label: + text: "Handicap" + size_hint: 0.5, 0.1 + IntInput: + text: '0' + size_hint: 0.5, 0.1 + input_property: 'HA' + Label: + text: "Komi" + size_hint: 0.5, 0.1 + FloatInput: + text: '6.5' + size_hint: 0.5, 0.1 + input_property: 'KM' + Label: + text: " " + size_hint: 0.5, 0.6 + GridLayout: + size_hint: 0.5, 1 + rows: 10 + cols: 2 + StyledButton: + size_hint: 0.5, 0.1 + text: 'Start Game' + on_press: root.new_game() + : size: self.parent.height, self.parent.height @@ -267,7 +309,7 @@ id: ai_move size_hint: 0.5, 0.5 text: 'AI Move' - on_press: root.katrain("aimove") + on_press: root.katrain("ai_move") CheckBoxHint: size_hint: 0.166, 0.5 text: 'fast' @@ -318,38 +360,30 @@ orientation: 'horizontal' size_hint: 1, 0.05 StyledButton: - text: 'Save' - id: save - size_hint: 0.5, 1 - on_press: info.text = root.parent.output_sgf() + text: ' New\nGame' + id: new_game + size_hint: 0.33, 1 + font_size: 0.25*self.size[1] + on_press: root.katrain("new-game-popup") StyledButton: text: 'Load' id: load - size_hint: 0.5, 1 - on_press: root.katrain("analyze-sgf",info.text) - GridLayout: - size_hint: 1, 0.05 - cols: 4 - rows: 1 - LargeLabel: - size_hint: 0.3, 0.25 - text: ' New\nGame' - font_size: 0.3*self.size[1] + size_hint: 0.33, 1 + font_size: 0.25*self.size[1] + on_press: root.katrain("analyze-sgf-popup",info.text) + margin: 0,1 StyledButton: - size_hint: 0.233, 1 - text: '9' - margin: (1,1) - on_press: root.katrain('new-game',9) + text: 'Save' + id: save + size_hint: 0.33, 1 + font_size: 0.25*self.size[1] + on_press: info.text = root.parent.output_sgf() StyledButton: - size_hint: 0.233, 1 - text: '13' - margin: (0,1) - on_press: root.katrain('new-game',13) - StyledButton: - size_hint: 0.233, 1 - text: '19' - margin: (1,1) - on_press: root.katrain('new-game',19) + text: 'C' + size_hint: 0.1, 1 + font_size: 0.25*self.size[1] + on_press: root.katrain('config-popup') + : board_gui: board_gui diff --git a/katrain.py b/katrain.py index 31a2f62..f35c76a 100644 --- a/katrain.py +++ b/katrain.py @@ -13,8 +13,9 @@ from kivy.uix.popup import Popup from constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_INFO from engine import KataGoEngine -from game import Game, GameNode, IllegalMoveException, KaTrainSGF, Move -from gui import BadukPanWidget, BWCheckBoxHint, CensorableLabel, CensorableScoreLabel, CheckBoxHint, Controls, LoadSGFPopup +from game import Game, IllegalMoveException, KaTrainSGF, Move +from gui import * +from gui.popups import NewGamePopup, ConfigPopup class KaTrainGui(BoxLayout): @@ -69,6 +70,16 @@ class KaTrainGui(BoxLayout): threading.Thread(target=self._message_loop_thread, daemon=True).start() self._do_new_game() + def update_state(self, include_board=False): # TODO: rename? does more now + cn = self.game.current_node + if cn.analysis_ready and self.controls.ai_auto.active(cn.next_player) and not cn.children and not self.game.game_ended: + self("ai-move", cn) + + if include_board: + Clock.schedule_once(self.board_gui.draw_board, -1) # main thread needs to do this + Clock.schedule_once(self.board_gui.draw_board_contents, -1) + self.controls.update_evaluation() + def _message_loop_thread(self): while True: game, msg, *args = self.message_queue.get() @@ -96,23 +107,19 @@ class KaTrainGui(BoxLayout): self.update_state(include_board=True) - def _do_aimove(self): - self.game.ai_move() + def _do_ai_move(self, node=None): + if node is None or self.game.current_node == node: + self.game.ai_move() - def _do_undo(self): - if ( - self.controls.ai_lock.active - and self.contols.auto_undo.active(self.game.current_node.player) - and len(self.game.current_node.parent.children) > self.num_undos(self.game.current_node) - and not self.train_settings.get("dont_lock_undos") - ): - self.info.text = f"Can't undo this move more than {self.num_undos(self.game.current_node)} time(s) when locked" + def _do_undo(self, n_times=1): + if self.controls.ai_lock.active and self.contols.auto_undo.active(self.game.current_node.player) and self.config("trainer/lock_undos"): + self.controls.set_status(f"Can't undo manually when Automatic Undo and Lock AI are both set. (Change the `lock_undos` setting to false to allow this regardless)") return - self.game.undo() + self.game.undo(n_times) self.update_state() - def _do_redo(self): - self.game.redo() + def _do_redo(self, n_times=1): + self.game.redo(n_times) self.update_state() def _do_switch_branch(self, direction): @@ -137,7 +144,7 @@ class KaTrainGui(BoxLayout): def _do_analyze_extra(self, mode): self.game.analyze_extra(mode) - def _do_analyze_sgf(self, sgf): + def _do_analyze_sgf_popup(self, sgf): fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)) popup_contents = LoadSGFPopup() fileselect_popup.add_widget(popup_contents) @@ -150,6 +157,18 @@ class KaTrainGui(BoxLayout): popup_contents.filesel.on_submit = readfile fileselect_popup.open() + def _do_new_game_popup(self): + new_game_popup = Popup(title="New Game", size_hint=(0.9, 0.9)) + popup_contents = NewGamePopup(self, new_game_popup, {k: v[0] for k, v in self.game.root.properties.items()}) + new_game_popup.add_widget(popup_contents) + new_game_popup.open() + + def _do_config_popup(self): + config_popup = Popup(title="Edit Settings", size_hint=(0.9, 0.9)) + popup_contents = ConfigPopup(self, config_popup, dict(self._config_store)) + config_popup.add_widget(popup_contents) + config_popup.open() + def output_sgf(self): for pl in Move.PLAYERS: if not self.game.root.get_first(f"P{pl}"): @@ -157,21 +176,14 @@ class KaTrainGui(BoxLayout): self.game.root.properties[f"P{pl}"] = [f"KaTrain (KataGo {model_file})" if self.controls.ai_auto.active(pl) else "Player"] return self.game.write_sgf() - def update_state(self, include_board=False): # TODO: rename? does more now - cn = self.game.current_node - if cn.analysis_ready and self.controls.ai_auto.active(cn.next_player) and not cn.children and not self.game.game_ended: - self._do_aimove() - - if include_board: - Clock.schedule_once(self.board_gui.draw_board, -1) # main thread needs to do this - Clock.schedule_once(self.board_gui.draw_board_contents, -1) - self.controls.update_evaluation() - 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 + if keycode[1] == "up": - self("undo") + self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999) elif keycode[1] == "down": - self("redo") + self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999) elif keycode[1] == "right": self("switch-branch", 1) elif keycode[1] == "left": @@ -210,12 +222,16 @@ class KaTrainApp(App): def build(self): self.icon = "./icon.png" self.gui = KaTrainGui() + Window.bind(on_request_close=self.on_request_close) return self.gui def on_start(self): self.gui.start() signal.signal(signal.SIGINT, self.signal_handler) + def on_request_close(self, *args): + self.gui.engine.shutdown() + def signal_handler(self, signal, frame): import sys import traceback @@ -228,6 +244,7 @@ class KaTrainApp(App): print(f"\tFile: {filename}, line {lineno}, in {name}") if line: print(f"\t\t{line.strip()}") + self.on_request_close() sys.exit(0) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_board.py b/tests/test_board.py index abc9617..954087f 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -20,38 +20,38 @@ class TestBoard: def test_merge(self): b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) - b.play(Move.from_gtp("B9", player=0)) - b.play(Move.from_gtp("A3", player=0)) - b.play(Move.from_gtp("A9", player=0)) + b.play(Move.from_gtp("B9", player="B")) + b.play(Move.from_gtp("A3", player="B")) + b.play(Move.from_gtp("A9", player="B")) assert 2 == len(self.nonempty_chains(b)) assert 3 == len(b.stones) assert 0 == len(b.prisoners) def test_collide(self): b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) - b.play(Move.from_gtp("B9", player=0)) + b.play(Move.from_gtp("B9", player="B")) with pytest.raises(IllegalMoveException): - b.play(Move.from_gtp("B9", player=1)) + b.play(Move.from_gtp("B9", player="W")) assert 1 == len(self.nonempty_chains(b)) assert 1 == len(b.stones) assert 0 == len(b.prisoners) def test_capture(self): b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) - b.play(Move.from_gtp("A2", player=0)) - b.play(Move.from_gtp("B1", player=1)) - b.play(Move.from_gtp("A1", player=1)) - b.play(Move.from_gtp("C1", player=0)) + b.play(Move.from_gtp("A2", player="B")) + b.play(Move.from_gtp("B1", player="W")) + b.play(Move.from_gtp("A1", player="W")) + b.play(Move.from_gtp("C1", player="B")) assert 3 == len(self.nonempty_chains(b)) assert 4 == len(b.stones) assert 0 == len(b.prisoners) - b.play(Move.from_gtp("B2", player=0)) + b.play(Move.from_gtp("B2", player="B")) assert 2 == len(self.nonempty_chains(b)) assert 3 == len(b.stones) assert 2 == len(b.prisoners) - b.play(Move.from_gtp("B1", player=0)) + b.play(Move.from_gtp("B1", player="B")) with pytest.raises(IllegalMoveException) as exc: - b.play(Move.from_gtp("A1", player=1)) + b.play(Move.from_gtp("A1", player="W")) assert "Suicide" in str(exc.value) assert 1 == len(self.nonempty_chains(b)) assert 4 == len(b.stones) @@ -60,17 +60,17 @@ class TestBoard: def test_snapback(self): b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) for move in ["C1", "D1", "E1", "C2", "D3", "E4", "F2", "F3", "F4"]: - b.play(Move.from_gtp(move, player=0)) + b.play(Move.from_gtp(move, player="B")) for move in ["D2", "E2", "C3", "D4", "C4"]: - b.play(Move.from_gtp(move, player=1)) + b.play(Move.from_gtp(move, player="W")) assert 5 == len(self.nonempty_chains(b)) assert 14 == len(b.stones) assert 0 == len(b.prisoners) - b.play(Move.from_gtp("E3", player=1)) + b.play(Move.from_gtp("E3", player="W")) assert 4 == len(self.nonempty_chains(b)) assert 14 == len(b.stones) assert 1 == len(b.prisoners) - b.play(Move.from_gtp("D3", player=0)) + b.play(Move.from_gtp("D3", player="B")) assert 4 == len(self.nonempty_chains(b)) assert 12 == len(b.stones) assert 4 == len(b.prisoners) @@ -78,25 +78,25 @@ class TestBoard: def test_ko(self): b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) for move in ["A2", "B1"]: - b.play(Move.from_gtp(move, player=0)) + b.play(Move.from_gtp(move, player="B")) for move in ["B2", "C1"]: - b.play(Move.from_gtp(move, player=1)) - b.play(Move.from_gtp("A1", player=1)) + b.play(Move.from_gtp(move, player="W")) + b.play(Move.from_gtp("A1", player="W")) assert 4 == len(self.nonempty_chains(b)) assert 4 == len(b.stones) assert 1 == len(b.prisoners) with pytest.raises(IllegalMoveException) as exc: - b.play(Move.from_gtp("B1", player=0)) + b.play(Move.from_gtp("B1", player="B")) assert "Ko" in str(exc.value) - b.play(Move.from_gtp("B1", player=0), ignore_ko=True) + b.play(Move.from_gtp("B1", player="B"), ignore_ko=True) assert 2 == len(b.prisoners) with pytest.raises(IllegalMoveException) as exc: - b.play(Move.from_gtp("A1", player=1)) + b.play(Move.from_gtp("A1", player="W")) - b.play(Move.from_gtp("F1", player=1)) - b.play(Move(coords=(None, None), player=0)) - b.play(Move.from_gtp("A1", player=1)) + b.play(Move.from_gtp("F1", player="W")) + b.play(Move(coords=None, player="B")) + b.play(Move.from_gtp("A1", player="W")) assert 3 == len(b.prisoners)