popups galore
This commit is contained in:
1 parent
2232f4cf0c
commit
8428cf15aa
11 files changed
+352
-122
No files matched your search
+9
-6
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Whitespace-only changes.
+25
-25
@@ -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)
|
||||||
Reference in new issue
Block a user