popups galore

This commit is contained in:
Sander Land committed 2020-04-18 00:49:34 +02:00
1 parent 2232f4cf0c
commit 8428cf15aa
11 files changed
+352 -122

No files matched your search

+9 -6
View File
@@ -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
}
}
+6 -1
View File
@@ -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
+37 -23
View File
@@ -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
+3 -5
View File
@@ -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..."
+1 -1
View File
@@ -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
+64
View File
@@ -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",)
+97 -2
View File
@@ -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)
+65 -31
View File
@@ -1,6 +1,6 @@
#:kivy 1.11.0
<StyledButton@Button>:
<StyledButton>:
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 @@
<CheckBox>
color: (0.05,0.05,0.05,1)
<LabelledCheckBox>
color: (0.95,0.95,0.95,1)
<CheckBoxHintLabel@ButtonBehavior+DarkLabel>:
halign: 'center'
valign: 'center'
@@ -191,8 +194,47 @@
size_hint: 0.05,1
<NewGamePopup>
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()
<BadukPanWidget>:
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')
<KaTrainGui>:
board_gui: board_gui
+45 -28
View File
@@ -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)
View File
Whitespace-only changes.
+25 -25
View File
@@ -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)