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": { "engine": {
"command": "KataGo/katago", "katago": "KataGo/katago",
"model": " models/b15-1.3.2.txt.gz", "model": " models/b15-1.3.2.txt.gz",
"config": "KataGo/analysis_config.cfg", "config": "KataGo/analysis_config.cfg",
"threads": 8, "threads": 8,
@@ -16,13 +16,16 @@
"size": 19, "size": 19,
"komi_19": 6.5, "komi_19": 6.5,
"komi_13": 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_target_score": 2,
"balance_play_randomize_eval": 1, "balance_play_randomize_eval": 1,
"balance_play_min_eval": 2, "balance_play_min_eval": 2,
"balance_play_min_visits": 20, "balance_play_min_visits": 20
"undo_point_threshold": 1.5,
"num_undo_prompts": 1
}, },
"board_ui": { "board_ui": {
"size_min": 1, "size_min": 1,
@@ -43,6 +46,6 @@
"eval_off_show_last": 3 "eval_off_show_last": 3
}, },
"debug": { "debug": {
"level": 1 "level": 2
} }
} }
+6 -1
View File
@@ -43,6 +43,11 @@ class KataGoEngine:
self.base_priority += 1 self.base_priority += 1
self.queries = {} self.queries = {}
def shutdown(self):
process = getattr(self, "katago_process")
if process:
process.terminate()
def is_idle(self): def is_idle(self):
return not self.queries return not self.queries
@@ -55,7 +60,7 @@ class KataGoEngine:
continue continue
analysis = json.loads(line) analysis = json.loads(line)
if "error" in analysis: 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: elif analysis["id"] in self.queries:
callback, start_time = self.queries[analysis["id"]] callback, start_time = self.queries[analysis["id"]]
time_taken = time.time() - start_time time_taken = time.time() - start_time
+37 -23
View File
@@ -1,3 +1,4 @@
import math
import os import os
import random import random
from datetime import datetime from datetime import datetime
@@ -20,10 +21,9 @@ class KaTrainSGF(SGF):
class Game: class Game:
"""Represents a game of go, including an implementation of capture rules.""" """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): def __init__(self, katrain, engine, config, board_size=None, move_tree=None):
Game.GAME_COUNTER += 1
self.katrain = katrain self.katrain = katrain
self.engine = engine self.engine = engine
self.config = config self.config = config
@@ -39,9 +39,7 @@ class Game:
else: else:
self.board_size = board_size or config["size"] self.board_size = board_size or config["size"]
self.komi = self.config.get(f"komi_{self.board_size}", 6.5) self.komi = self.config.get(f"komi_{self.board_size}", 6.5)
self.root = GameNode( self.root = GameNode(properties={"SZ": self.board_size, "KM": self.komi, "DT": self.game_id, **Game.DEFAULT_PROPERTIES})
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.current_node = self.root self.current_node = self.root
self._init_chains() self._init_chains()
@@ -62,7 +60,7 @@ class Game:
try: try:
# for m in self.moves: # for m in self.moves:
for node in self.current_node.nodes_from_root: 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 self._validate_move_and_update_chains(m, True) # ignore ko since we didn't know if it was forced
except IllegalMoveException as e: except IllegalMoveException as e:
raise Exception(f"Unexpected illegal move ({str(e)})") raise Exception(f"Unexpected illegal move ({str(e)})")
@@ -119,38 +117,54 @@ class Game:
try: try:
self._validate_move_and_update_chains(move, ignore_ko) self._validate_move_and_update_chains(move, ignore_ko)
except IllegalMoveException: except IllegalMoveException:
self._init_chains()
raise raise
played_node = self.current_node.play(move) played_node = self.current_node.play(move)
self.current_node = played_node self.current_node = played_node
played_node.analyze(self.engine) played_node.analyze(self.engine)
return played_node return played_node
def undo(self): def undo(self, n_times=1):
if self.current_node is not self.root:
self.current_node = self.current_node.parent
self._init_chains()
def redo(self):
cn = self.current_node # avoid race conditions cn = self.current_node # avoid race conditions
if cn.children: for _ in range(n_times):
self.current_node = cn.children[-1] if not cn.is_root:
self._init_chains() 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): def switch_branch(self, direction):
cm = self.current_node # avoid race conditions cn = self.current_node # avoid race conditions
if cm.parent and len(cm.parent.children) > 1: if cn.parent and len(cn.parent.children) > 1:
ix = cm.parent.children.index(cm) ix = cn.parent.children.index(cn)
self.current_node = cm.parent.children[(ix + direction) % len(cm.parent.children)] self.current_node = cn.parent.children[(ix + direction) % len(cn.parent.children)]
self._init_chains() self._init_chains()
def place_handicap_stones(self, n_handicaps): def place_handicap_stones(self, n_handicaps):
near = 3 if self.board_size >= 13 else 2 near = 3 if self.board_size >= 13 else 2
far = self.board_size - 1 - near far = self.board_size - 1 - near
middle = self.board_size // 2 middle = self.board_size // 2
stones = [(far, far), (near, near), (far, near), (near, far)] if n_handicaps > 9:
if n_handicaps % 2 == 1: stones_per_row = math.ceil(math.sqrt(n_handicaps))
stones.append((middle, middle)) spacing = (far - near) / (stones_per_row - 1)
stones += [(near, middle), (far, middle), (middle, near), (middle, far)] 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]]) self.root.add_property("AB", [Move(stone).sgf(board_size=self.board_size) for stone in stones[:n_handicaps]])
@property @property
+3 -5
View File
@@ -45,8 +45,7 @@ class GameNode(SGFNode):
def format_win_rate(self, win_rate=None): def format_win_rate(self, win_rate=None):
win_rate = win_rate or self.analysis[0]["winrate"] win_rate = win_rate or self.analysis[0]["winrate"]
b_adv = win_rate - 0.5 return f"{'B' if win_rate > 0.5 else 'W'} {max(win_rate,1-win_rate):.1%}"
return f"{'B' if b_adv > 0 else 'W'}+{abs(b_adv):.1%}"
def comment(self, sgf=False, eval=False, hints=False): def comment(self, sgf=False, eval=False, hints=False):
single_move = self.single_move single_move = self.single_move
@@ -60,11 +59,10 @@ class GameNode(SGFNode):
if sgf: if sgf:
text += f"Score: {self.format_score(score)}\n" text += f"Score: {self.format_score(score)}\n"
if self.parent and self.parent.analysis_ready: 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" 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 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" text += f"Estimated point loss: {points_lost:.1f}\n"
else: else:
text = "No analysis available" if sgf else "Analyzing move..." text = "No analysis available" if sgf else "Analyzing move..."
+1 -1
View File
@@ -1,4 +1,4 @@
from gui.badukpan import BadukPanWidget from gui.badukpan import BadukPanWidget
from gui.controls import Controls 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 from gui.popups import LoadSGFPopup
+64
View File
@@ -1,6 +1,16 @@
from kivy.core.text import Label as CoreLabel from kivy.core.text import Label as CoreLabel
from kivy.graphics import * from kivy.graphics import *
from kivy.properties import BooleanProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout 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): class CheckBoxHint(BoxLayout):
@@ -14,6 +24,60 @@ class CheckBoxHint(BoxLayout):
pass 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): class BWCheckBoxHint(BoxLayout):
__events__ = ("on_active",) __events__ = ("on_active",)
+97 -2
View File
@@ -1,10 +1,105 @@
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
import os 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): class LoadSGFPopup(BoxLayout):
pass pass
class NewGamePopup(BoxLayout): class NewGamePopup(QuickConfigGui):
pass 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 #:kivy 1.11.0
<StyledButton@Button>: <StyledButton>:
text_color: 0.95,0.95,0.95,1 text_color: 0.95,0.95,0.95,1
button_color: 0.157,0.455,0.753,1 button_color: 0.157,0.455,0.753,1
button_color_down: (*[c/2 for c in self.button_color[:3]],1) button_color_down: (*[c/2 for c in self.button_color[:3]],1)
@@ -31,6 +31,9 @@
<CheckBox> <CheckBox>
color: (0.05,0.05,0.05,1) color: (0.05,0.05,0.05,1)
<LabelledCheckBox>
color: (0.95,0.95,0.95,1)
<CheckBoxHintLabel@ButtonBehavior+DarkLabel>: <CheckBoxHintLabel@ButtonBehavior+DarkLabel>:
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
@@ -191,8 +194,47 @@
size_hint: 0.05,1 size_hint: 0.05,1
<NewGamePopup> <NewGamePopup>
Label: orientation: 'vertical'
text: "New Game" 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>: <BadukPanWidget>:
size: self.parent.height, self.parent.height size: self.parent.height, self.parent.height
@@ -267,7 +309,7 @@
id: ai_move id: ai_move
size_hint: 0.5, 0.5 size_hint: 0.5, 0.5
text: 'AI Move' text: 'AI Move'
on_press: root.katrain("aimove") on_press: root.katrain("ai_move")
CheckBoxHint: CheckBoxHint:
size_hint: 0.166, 0.5 size_hint: 0.166, 0.5
text: 'fast' text: 'fast'
@@ -318,38 +360,30 @@
orientation: 'horizontal' orientation: 'horizontal'
size_hint: 1, 0.05 size_hint: 1, 0.05
StyledButton: StyledButton:
text: 'Save' text: ' New\nGame'
id: save id: new_game
size_hint: 0.5, 1 size_hint: 0.33, 1
on_press: info.text = root.parent.output_sgf() font_size: 0.25*self.size[1]
on_press: root.katrain("new-game-popup")
StyledButton: StyledButton:
text: 'Load' text: 'Load'
id: load id: load
size_hint: 0.5, 1 size_hint: 0.33, 1
on_press: root.katrain("analyze-sgf",info.text) font_size: 0.25*self.size[1]
GridLayout: on_press: root.katrain("analyze-sgf-popup",info.text)
size_hint: 1, 0.05 margin: 0,1
cols: 4
rows: 1
LargeLabel:
size_hint: 0.3, 0.25
text: ' New\nGame'
font_size: 0.3*self.size[1]
StyledButton: StyledButton:
size_hint: 0.233, 1 text: 'Save'
text: '9' id: save
margin: (1,1) size_hint: 0.33, 1
on_press: root.katrain('new-game',9) font_size: 0.25*self.size[1]
on_press: info.text = root.parent.output_sgf()
StyledButton: StyledButton:
size_hint: 0.233, 1 text: 'C'
text: '13' size_hint: 0.1, 1
margin: (0,1) font_size: 0.25*self.size[1]
on_press: root.katrain('new-game',13) on_press: root.katrain('config-popup')
StyledButton:
size_hint: 0.233, 1
text: '19'
margin: (1,1)
on_press: root.katrain('new-game',19)
<KaTrainGui>: <KaTrainGui>:
board_gui: board_gui 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 constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_INFO
from engine import KataGoEngine from engine import KataGoEngine
from game import Game, GameNode, IllegalMoveException, KaTrainSGF, Move from game import Game, IllegalMoveException, KaTrainSGF, Move
from gui import BadukPanWidget, BWCheckBoxHint, CensorableLabel, CensorableScoreLabel, CheckBoxHint, Controls, LoadSGFPopup from gui import *
from gui.popups import NewGamePopup, ConfigPopup
class KaTrainGui(BoxLayout): class KaTrainGui(BoxLayout):
@@ -69,6 +70,16 @@ class KaTrainGui(BoxLayout):
threading.Thread(target=self._message_loop_thread, daemon=True).start() threading.Thread(target=self._message_loop_thread, daemon=True).start()
self._do_new_game() 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): def _message_loop_thread(self):
while True: while True:
game, msg, *args = self.message_queue.get() game, msg, *args = self.message_queue.get()
@@ -96,23 +107,19 @@ class KaTrainGui(BoxLayout):
self.update_state(include_board=True) self.update_state(include_board=True)
def _do_aimove(self): def _do_ai_move(self, node=None):
self.game.ai_move() if node is None or self.game.current_node == node:
self.game.ai_move()
def _do_undo(self): def _do_undo(self, n_times=1):
if ( 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.ai_lock.active 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)")
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"
return return
self.game.undo() self.game.undo(n_times)
self.update_state() self.update_state()
def _do_redo(self): def _do_redo(self, n_times=1):
self.game.redo() self.game.redo(n_times)
self.update_state() self.update_state()
def _do_switch_branch(self, direction): def _do_switch_branch(self, direction):
@@ -137,7 +144,7 @@ class KaTrainGui(BoxLayout):
def _do_analyze_extra(self, mode): def _do_analyze_extra(self, mode):
self.game.analyze_extra(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)) fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8))
popup_contents = LoadSGFPopup() popup_contents = LoadSGFPopup()
fileselect_popup.add_widget(popup_contents) fileselect_popup.add_widget(popup_contents)
@@ -150,6 +157,18 @@ class KaTrainGui(BoxLayout):
popup_contents.filesel.on_submit = readfile popup_contents.filesel.on_submit = readfile
fileselect_popup.open() 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): def output_sgf(self):
for pl in Move.PLAYERS: for pl in Move.PLAYERS:
if not self.game.root.get_first(f"P{pl}"): 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"] 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() 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): 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": if keycode[1] == "up":
self("undo") self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
elif keycode[1] == "down": elif keycode[1] == "down":
self("redo") self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
elif keycode[1] == "right": elif keycode[1] == "right":
self("switch-branch", 1) self("switch-branch", 1)
elif keycode[1] == "left": elif keycode[1] == "left":
@@ -210,12 +222,16 @@ class KaTrainApp(App):
def build(self): def build(self):
self.icon = "./icon.png" self.icon = "./icon.png"
self.gui = KaTrainGui() self.gui = KaTrainGui()
Window.bind(on_request_close=self.on_request_close)
return self.gui return self.gui
def on_start(self): def on_start(self):
self.gui.start() self.gui.start()
signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler)
def on_request_close(self, *args):
self.gui.engine.shutdown()
def signal_handler(self, signal, frame): def signal_handler(self, signal, frame):
import sys import sys
import traceback import traceback
@@ -228,6 +244,7 @@ class KaTrainApp(App):
print(f"\tFile: {filename}, line {lineno}, in {name}") print(f"\tFile: {filename}, line {lineno}, in {name}")
if line: if line:
print(f"\t\t{line.strip()}") print(f"\t\t{line.strip()}")
self.on_request_close()
sys.exit(0) sys.exit(0)
View File
Whitespace-only changes.
+25 -25
View File
@@ -20,38 +20,38 @@ class TestBoard:
def test_merge(self): def test_merge(self):
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
b.play(Move.from_gtp("B9", player=0)) b.play(Move.from_gtp("B9", player="B"))
b.play(Move.from_gtp("A3", player=0)) b.play(Move.from_gtp("A3", player="B"))
b.play(Move.from_gtp("A9", player=0)) b.play(Move.from_gtp("A9", player="B"))
assert 2 == len(self.nonempty_chains(b)) assert 2 == len(self.nonempty_chains(b))
assert 3 == len(b.stones) assert 3 == len(b.stones)
assert 0 == len(b.prisoners) assert 0 == len(b.prisoners)
def test_collide(self): def test_collide(self):
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) 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): 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(self.nonempty_chains(b))
assert 1 == len(b.stones) assert 1 == len(b.stones)
assert 0 == len(b.prisoners) assert 0 == len(b.prisoners)
def test_capture(self): def test_capture(self):
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
b.play(Move.from_gtp("A2", player=0)) b.play(Move.from_gtp("A2", player="B"))
b.play(Move.from_gtp("B1", player=1)) b.play(Move.from_gtp("B1", player="W"))
b.play(Move.from_gtp("A1", player=1)) b.play(Move.from_gtp("A1", player="W"))
b.play(Move.from_gtp("C1", player=0)) b.play(Move.from_gtp("C1", player="B"))
assert 3 == len(self.nonempty_chains(b)) assert 3 == len(self.nonempty_chains(b))
assert 4 == len(b.stones) assert 4 == len(b.stones)
assert 0 == len(b.prisoners) 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 2 == len(self.nonempty_chains(b))
assert 3 == len(b.stones) assert 3 == len(b.stones)
assert 2 == len(b.prisoners) 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: 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 "Suicide" in str(exc.value)
assert 1 == len(self.nonempty_chains(b)) assert 1 == len(self.nonempty_chains(b))
assert 4 == len(b.stones) assert 4 == len(b.stones)
@@ -60,17 +60,17 @@ class TestBoard:
def test_snapback(self): def test_snapback(self):
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
for move in ["C1", "D1", "E1", "C2", "D3", "E4", "F2", "F3", "F4"]: 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"]: 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 5 == len(self.nonempty_chains(b))
assert 14 == len(b.stones) assert 14 == len(b.stones)
assert 0 == len(b.prisoners) 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 4 == len(self.nonempty_chains(b))
assert 14 == len(b.stones) assert 14 == len(b.stones)
assert 1 == len(b.prisoners) 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 4 == len(self.nonempty_chains(b))
assert 12 == len(b.stones) assert 12 == len(b.stones)
assert 4 == len(b.prisoners) assert 4 == len(b.prisoners)
@@ -78,25 +78,25 @@ class TestBoard:
def test_ko(self): def test_ko(self):
b = Game(MockKaTrain(), MockEngine(), {}, board_size=9) b = Game(MockKaTrain(), MockEngine(), {}, board_size=9)
for move in ["A2", "B1"]: 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"]: for move in ["B2", "C1"]:
b.play(Move.from_gtp(move, player=1)) b.play(Move.from_gtp(move, player="W"))
b.play(Move.from_gtp("A1", player=1)) b.play(Move.from_gtp("A1", player="W"))
assert 4 == len(self.nonempty_chains(b)) assert 4 == len(self.nonempty_chains(b))
assert 4 == len(b.stones) assert 4 == len(b.stones)
assert 1 == len(b.prisoners) assert 1 == len(b.prisoners)
with pytest.raises(IllegalMoveException) as exc: 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) 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) assert 2 == len(b.prisoners)
with pytest.raises(IllegalMoveException) as exc: 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.from_gtp("F1", player="W"))
b.play(Move(coords=(None, None), player=0)) b.play(Move(coords=None, player="B"))
b.play(Move.from_gtp("A1", player=1)) b.play(Move.from_gtp("A1", player="W"))
assert 3 == len(b.prisoners) assert 3 == len(b.prisoners)