diff --git a/katrain/__main__.py b/katrain/__main__.py index a1932fa..ff4a6d2 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -278,7 +278,9 @@ class KaTrainGui(Screen, KaTrainBase): "save_game", "find_mistake", ]: - self.controls.set_status(i18n._("gui-locked").format(action=msg), STATUS_INFO, check_level=False) + self.controls.set_status( + i18n._("gui-locked").format(action=msg), STATUS_INFO, check_level=False + ) continue fn = getattr(self, f"_do_{msg}") fn(*args, **kwargs) @@ -292,7 +294,9 @@ class KaTrainGui(Screen, KaTrainBase): if self.game: if message.endswith("popup"): # gui code needs to run in main kivy thread. if self.contributing and "save" not in message and message != "contribute-popup": - self.controls.set_status(i18n._("gui-locked").format(action=message), STATUS_INFO, check_level=False) + self.controls.set_status( + i18n._("gui-locked").format(action=message), STATUS_INFO, check_level=False + ) return fn = getattr(self, f"_do_{message.replace('-', '_')}") Clock.schedule_once(lambda _dt: fn(*args, **kwargs), -1) diff --git a/katrain/core/constants.py b/katrain/core/constants.py index 02539a4..f14ca02 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -1,5 +1,5 @@ PROGRAM_NAME = "KaTrain" -VERSION = "1.8.3" +VERSION = "1.8.4" HOMEPAGE = "https://github.com/sanderland/katrain" CONFIG_MIN_VERSION = "1.8.0" # keep config files from this version ANALYSIS_FORMAT_VERSION = "1.0" diff --git a/katrain/core/contribute_engine.py b/katrain/core/contribute_engine.py index 101da0c..028bce7 100644 --- a/katrain/core/contribute_engine.py +++ b/katrain/core/contribute_engine.py @@ -49,12 +49,15 @@ class KataGoContributeEngine(BaseEngine): self.move_speed = self.config.get("movespeed", 2.0) exe = self.get_engine_path(self.config.get("katago")) - cacert_path = os.path.join(os.path.split(exe)[0], 'cacert.pem') + cacert_path = os.path.join(os.path.split(exe)[0], "cacert.pem") if not os.path.isfile(cacert_path): try: - shutil.copyfile( find_package_resource('katrain/KataGo/cacert.pem'), cacert_path) + shutil.copyfile(find_package_resource("katrain/KataGo/cacert.pem"), cacert_path) except Exception as e: - self.katrain.log(f"Could not copy cacert file ({e}), please add it manually to your katago.exe directory",OUTPUT_ERROR) + self.katrain.log( + f"Could not copy cacert file ({e}), please add it manually to your katago.exe directory", + OUTPUT_ERROR, + ) cfg = find_package_resource(self.config.get("config")) settings_dict = { @@ -63,7 +66,7 @@ class KataGoContributeEngine(BaseEngine): "maxSimultaneousGames": self.config.get("maxgames") or self.DEFAULT_MAX_GAMES, "includeOwnership": self.config.get("ownership") or False, "logGamesAsJson": True, - "homeDataDir": os.path.expanduser(DATA_FOLDER) + "homeDataDir": os.path.expanduser(DATA_FOLDER), } self.max_buffer_games = 2 * settings_dict["maxSimultaneousGames"] settings = {f"{k}={v}" for k, v in settings_dict.items()} diff --git a/katrain/core/engine.py b/katrain/core/engine.py index f38c4d4..99def28 100644 --- a/katrain/core/engine.py +++ b/katrain/core/engine.py @@ -26,12 +26,13 @@ class EngineDiedException(Exception): class BaseEngine: # some common elements between analysis and contribute engine - # TODO: we don't support suicide in game.py, so no "tt": "tromp-taylor", "nz": "new-zealand" RULESETS_ABBR = [ ("jp", "japanese"), ("cn", "chinese"), ("ko", "korean"), ("aga", "aga"), + ("tt", "tromp-taylor"), + ("nz", "new zealand"), ("stone_scoring", "stone_scoring"), ] RULESETS = {fromkey: name for abbr, name in RULESETS_ABBR for fromkey in [abbr, name]} @@ -41,8 +42,7 @@ class BaseEngine: # some common elements between analysis and contribute engine self.config = config @staticmethod - def get_rules(node): - ruleset = node.ruleset + def get_rules(ruleset): if ruleset.strip().startswith("{"): try: ruleset = json.loads(ruleset) @@ -386,7 +386,7 @@ class KataGoEngine(BaseEngine): if self.config.get("wide_root_noise", 0.0) > 0.0: # don't send if 0.0, so older versions don't error settings["wideRootNoise"] = self.config["wide_root_noise"] query = { - "rules": self.get_rules(analysis_node), + "rules": self.get_rules(analysis_node.ruleset), "priority": self.base_priority + priority, "analyzeTurns": [len(moves)], "maxVisits": visits, diff --git a/katrain/core/game.py b/katrain/core/game.py index 2f1e0b7..37dd3ed 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -153,12 +153,11 @@ class BaseGame: if self.board[move.coords[1]][move.coords[0]] != -1: raise IllegalMoveException("Space occupied") + # merge chains connected by this move, or create a new one nb_chains = list({c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player == move.player}) if nb_chains: this_chain = nb_chains[0] - self.board = [ - [nb_chains[0] if sq in nb_chains else sq for sq in line] for line in self.board - ] # merge chains connected by this move + self.board = [[nb_chains[0] if sq in nb_chains else sq for sq in line] for line in self.board] for oc in nb_chains[1:]: self.chains[nb_chains[0]] += self.chains[oc] self.chains[oc] = [] @@ -168,9 +167,10 @@ class BaseGame: self.chains.append([move]) self.board[move.coords[1]][move.coords[0]] = this_chain + # check captures opp_nb_chains = {c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player != move.player} for c in opp_nb_chains: - if -1 not in neighbours(self.chains[c]): + if -1 not in neighbours(self.chains[c]): # no liberties self.last_capture += self.chains[c] for om in self.chains[c]: self.board[om.coords[1]][om.coords[0]] = -1 @@ -179,8 +179,21 @@ class BaseGame: raise IllegalMoveException("Ko") self.prisoners += self.last_capture - if -1 not in neighbours(self.chains[this_chain]): # TODO: NZ rules? - raise IllegalMoveException("Suicide") + # suicide: check rules and throw exception if needed + if -1 not in neighbours(self.chains[this_chain]): + rules = self.rules + if len(self.chains[this_chain]) == 1: # even in new zealand rules, single stone suicide is not allowed + raise IllegalMoveException("Single stone suicide") + elif (isinstance(rules, str) and rules in ["tromp-taylor", "new zealand"]) or ( + isinstance(rules, dict) and rules.get("suicide", False) + ): + self.last_capture += self.chains[this_chain] + for om in self.chains[this_chain]: + self.board[om.coords[1]][om.coords[0]] = -1 + self.chains[this_chain] = [] + self.prisoners += self.last_capture + else: # suicide not allowed by rules + raise IllegalMoveException("Suicide") # Play a Move from the current position, raise IllegalMoveException if invalid. def play(self, move: Move, ignore_ko: bool = False): @@ -290,7 +303,7 @@ class BaseGame: @property def rules(self): - return self.root.ruleset + return KataGoEngine.get_rules(self.root.ruleset) @property def manual_score(self): @@ -437,10 +450,6 @@ class Game(BaseGame): node.clear_analysis() node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast) - @property - def rules(self): # maybe not needed - return self.engines["B"].get_rules(self.root) - def set_current_node(self, node): if self.insert_mode: self.katrain.controls.set_status(i18n._("finish inserting before navigating"), STATUS_ERROR) diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py index 8353761..90e6fda 100644 --- a/katrain/core/game_node.py +++ b/katrain/core/game_node.py @@ -97,7 +97,7 @@ class GameNode(SGFNode): for c in v.split(SGF_SEPARATOR_MARKER) if c.strip() and SGF_INTERNAL_COMMENTS_MARKER not in c ] - self.note = "".join(comments) # no super call intended, just save as note to be editable + self.note = "".join(comments).strip() # no super call intended, just save as note to be editable else: return super().add_list_property(property, values) @@ -143,10 +143,13 @@ class GameNode(SGFNode): properties["SQ"] = best_sq if top_x and "MA" not in properties: properties["MA"] = [top_x] - comments.append(self.comment(sgf=True, interactive=False) + SGF_INTERNAL_COMMENTS_MARKER) + comments.append("\n" + self.comment(sgf=True, interactive=False) + SGF_INTERNAL_COMMENTS_MARKER) if self.is_root: - comments = [ - i18n._("SGF start message") + SGF_INTERNAL_COMMENTS_MARKER + "\n", + if save_marks: + comments = [i18n._("SGF start message") + SGF_INTERNAL_COMMENTS_MARKER + "\n"] + else: + comments = [] + comments += [ *comments, f"\nSGF generated by {PROGRAM_NAME} {VERSION}{SGF_INTERNAL_COMMENTS_MARKER}\n", ] @@ -162,9 +165,9 @@ class GameNode(SGFNode): elif "KTSID" in properties: del properties["KTSID"] if note: - comments.append(f"{self.note}") + comments.insert(0, f"{self.note}\n") # user notes at top! if comments: - properties["C"] = [SGF_SEPARATOR_MARKER.join(comments)] + properties["C"] = [SGF_SEPARATOR_MARKER.join(comments).strip("\n")] elif "C" in properties: del properties["C"] return properties diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 7e0da8d..0ad6cbc 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -455,20 +455,20 @@ class BaseConfigPopup(QuickConfigGui): KATAGOS = { "win": { - "OpenCL v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-opencl-windows-x64.zip", - "Eigen AVX2 (Modern CPUs) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigenavx2-windows-x64.zip", - "Eigen (CPU, Non-optimized) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigen-windows-x64.zip", + "OpenCL v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-opencl-windows-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigenavx2-windows-x64.zip", + "Eigen (CPU, Non-optimized) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigen-windows-x64.zip", "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-windows-x64.zip", }, "linux": { - "OpenCL v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-opencl-linux-x64.zip", - "Eigen AVX2 (Modern CPUs) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigenavx2-linux-x64.zip", - "Eigen (CPU, Non-optimized) v1.8.0": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-eigen-linux-x64.zip", + "OpenCL v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-opencl-linux-x64.zip", + "Eigen AVX2 (Modern CPUs) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigenavx2-linux-x64.zip", + "Eigen (CPU, Non-optimized) v1.8.1": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-eigen-linux-x64.zip", "OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-linux-x64.zip", }, "just-descriptions": { - "CUDA v1.8.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-gpu-cuda10.2-windows-x64.zip", - "CUDA v1.8.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.8.0/katago-v1.8.0-gpu-cuda10.2-linux-x64.zip", + "CUDA v1.8.1 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-gpu-cuda10.2-windows-x64.zip", + "CUDA v1.8.1 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.8.1/katago-v1.8.1-gpu-cuda10.2-linux-x64.zip", }, } diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo index fc8226e..1e5b72e 100644 Binary files a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po index 75d1f77..46835c7 100644 --- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po @@ -837,3 +837,11 @@ msgstr "这会产生部分的对局让你接著下。例如:你可以使用它 #. change komi etc in current game msgid "editgame" msgstr "编辑对局资讯" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo index 4339c71..db400b3 100644 Binary files a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po index a145fba..bcbfc7d 100644 --- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po @@ -904,3 +904,11 @@ msgstr "" #. change komi etc in current game msgid "editgame" msgstr "Spiel editieren" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo index a6c6b7b..abe3adb 100644 Binary files a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po index 5832c0a..c1436e7 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -398,6 +398,12 @@ msgstr "Korean" msgid "aga" msgstr "AGA" +msgid "tromp-taylor" +msgstr "Tromp-Taylor" + +msgid "new zealand" +msgstr "New Zealand" + msgid "stone_scoring" msgstr "Ancient Chinese" diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo index eb33bbb..eab6b41 100644 Binary files a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po index 4873233..07d1e60 100644 --- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po @@ -916,3 +916,11 @@ msgstr "" #. TODO - change komi etc in current game msgid "editgame" msgstr "Edit Game Info" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo index 4fa5926..1850cca 100644 Binary files a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po index fbc411f..987313e 100644 --- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po @@ -898,3 +898,11 @@ msgstr "" msgid "editgame" msgstr "Partie en cours" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo index e7027c3..5a04028 100644 Binary files a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po index b25bac2..254fe43 100644 --- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po @@ -881,3 +881,11 @@ msgstr "" #. change komi etc in current game msgid "editgame" msgstr "対局情報を編集" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo index e8a313f..2d78044 100644 Binary files a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po index 92cf59e..a16934b 100644 --- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po @@ -836,3 +836,11 @@ msgstr "" msgid "editgame" msgstr "대국 정보 편집" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo index e033dfb..32ff63a 100644 Binary files a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po index d26496c..f7bd3cf 100644 --- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po @@ -855,3 +855,11 @@ msgstr "" msgid "editgame" msgstr "Редактировать игры" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo index 251e535..3133aa8 100644 Binary files a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.mo differ diff --git a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po index d5bca45..9e19ac3 100644 --- a/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/tw/LC_MESSAGES/katrain.po @@ -863,3 +863,11 @@ msgstr "這會產生部分的對局讓你接著下。例如:你可以使用它 #. change komi etc in current game msgid "editgame" msgstr "編輯對局資訊" + +#. TODO +msgid "new zealand" +msgstr "New Zealand" + +#. TODO +msgid "tromp-taylor" +msgstr "Tromp-Taylor" diff --git a/tests/test_board.py b/tests/test_board.py index 496c1d1..c66ad43 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -1,6 +1,7 @@ import pytest from katrain.core.base_katrain import KaTrainBase +from katrain.core.engine import BaseEngine from katrain.core.game import Game, IllegalMoveException, Move, KaTrainSGF from katrain.core.game_node import GameNode @@ -55,9 +56,8 @@ class TestBoard: assert 3 == len(b.stones) assert 2 == len(b.prisoners) b.play(Move.from_gtp("B1", player="B")) - with pytest.raises(IllegalMoveException) as exc: + with pytest.raises(IllegalMoveException, match="Single stone suicide"): 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) assert 2 == len(b.prisoners) @@ -118,3 +118,25 @@ class TestBoard: root2 = KaTrainSGF.parse_sgf("(;GM[1]FF[4]SZ[19]HA[2];)") game2 = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=root2) assert 2 == len(game2.root.placements) + + def test_suicide(self): + rulesets_to_test = BaseEngine.RULESETS_ABBR + [('{"suicide":true}', ""), ('{"suicide":false}', "")] + for shortrule, _ in rulesets_to_test: + new_game = GameNode(properties={"SZ": 19, "RU": shortrule}) + b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game) + b.play(Move.from_gtp("A18", player="B")) + b.play(Move.from_gtp("B18", player="B")) + b.play(Move.from_gtp("C19", player="B")) + b.play(Move.from_gtp("A19", player="W")) + assert 4 == len(b.stones) + assert 0 == len(b.prisoners) + + if shortrule in ["tt", "nz", '{"suicide":true}']: + b.play(Move.from_gtp("B19", player="W")) + assert 3 == len(b.stones) + assert 2 == len(b.prisoners) + else: + with pytest.raises(IllegalMoveException, match="Suicide"): + b.play(Move.from_gtp("B19", player="W")) + assert 4 == len(b.stones) + assert 0 == len(b.prisoners)