play mode

This commit is contained in:
Sander Land committed 2020-04-20 23:09:47 +02:00
1 parent 12b1b1a212
commit d6141d91d6
8 files changed
+227 -193

No files matched your search

+30 -21
View File
@@ -7,6 +7,7 @@ from typing import List
from kivy.clock import Clock from kivy.clock import Clock
from constants import OUTPUT_DEBUG
from game_node import GameNode from game_node import GameNode
from sgf_parser import SGF, Move from sgf_parser import SGF, Move
@@ -195,26 +196,34 @@ class Game:
return f"SGF with analysis written to {file_name}" return f"SGF with analysis written to {file_name}"
def ai_move(self, train_settings): def ai_move(self, train_settings):
while not self.current_node.analysis_ready: cn = self.current_node
self.katrain.controls.set_status("Thinking...") while not cn.analysis_ready:
self.katrain.controls.set_status("Thinking...") # TODO: non blocking somehow?
time.sleep(0.05) time.sleep(0.05)
# select move # select move
ai_moves = self.current_node.candidate_moves ai_moves = cn.candidate_moves
pos_moves = [ mode = self.katrain.controls.player_mode(cn.next_player)
[d["move"], d["scoreLead"], d["pointsLost"]] for i, d in enumerate(ai_moves) if i == 0 or int(d["visits"]) >= train_settings["balance_play_min_visits"]
] # TODO: lcb based ? if "policy" in mode and cn.policy:
sel_moves = pos_moves[:1] policy_moves = cn.policy_ranking
# don't play suicidal to balance score - pass when it's best self.katrain.log(f"Top 5 policy moves are: {policy_moves[:5]}", OUTPUT_DEBUG)
if self.katrain.controls.ai_balance.active and pos_moves[0][0] != "pass": # TODO: settings where they belong? aimove = policy_moves[0][0]
sel_moves = [ elif "balance" in mode and ai_moves[0]['move'] != "pass": # don't play suicidal to balance score - pass when it's best
(move, score, points_lost) sign = cn.player_sign(cn.next_player) # TODO check
for move, score, points_lost in pos_moves sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead
if points_lost < train_settings["balance_play_randomize_score"] move
or points_lost < train_settings["balance_play_min_eval"] for i, move in enumerate(ai_moves)
and -self.current_node.move.player_sign * score > self.config["balance_play_target_score"] if i == 0
] or sel_moves or move["visits"] >= train_settings["balance_play_min_visits"]
aimove = Move.from_gtp(random.choice(sel_moves)[0], player=self.next_player) and (
move["pointsLost"] < train_settings["balance_play_randomize_score"]
or move["pointsLost"] < train_settings["balance_play_min_eval"]
and sign * move["scoreLead"] > train_settings["balance_play_target_score"]
)
]
aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player) # TODO: could be weighted towards worse
else:
aimove = Move.from_gtp(ai_moves[0]["move"], player=cn.next_player)
self.play(aimove) self.play(aimove)
def analyze_undo(self, node, train_config): def analyze_undo(self, node, train_config):
@@ -252,7 +261,7 @@ class Game:
return return
if mode == "extra": if mode == "extra":
visits = cn.analysis['root']['visits'] + self.engine.config["visits"] visits = cn.analysis["root"]["visits"] + self.engine.config["visits"]
self.katrain.controls.set_status(f"Performing additional analysis to {visits} visits") self.katrain.controls.set_status(f"Performing additional analysis to {visits} visits")
cn.analyze(self.engine, visits=visits, priority=-1_000) cn.analyze(self.engine, visits=visits, priority=-1_000)
return return
@@ -262,8 +271,8 @@ class Game:
self.katrain.controls.set_status(f"Refining analysis of entire board to {visits} visits") self.katrain.controls.set_status(f"Refining analysis of entire board to {visits} visits")
priority = -1_000_000_000 priority = -1_000_000_000
else: # mode=='refine': else: # mode=='refine':
analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp,_ in cn.analysis['moves'].items()] analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
visits = max(d['visits'] for d in cn.analysis['moves'].values()) + self.engine.config["visits_fast"] visits = max(d["visits"] for d in cn.analysis["moves"].values()) + self.engine.config["visits_fast"]
self.katrain.controls.set_status(f"Refining analysis of candidate moves to {visits} visits") self.katrain.controls.set_status(f"Refining analysis of candidate moves to {visits} visits")
priority = -1_000 priority = -1_000
for move in analyze_moves: for move in analyze_moves:
+13 -1
View File
@@ -2,7 +2,7 @@ import copy
import random import random
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sgf_parser import SGFNode from sgf_parser import SGFNode, Move
class GameNode(SGFNode): class GameNode(SGFNode):
@@ -110,3 +110,15 @@ class GameNode(SGFNode):
[{"pointsLost": self.player_sign(self.next_player) * (self.analysis["root"]["scoreLead"] - d["scoreLead"]), **d} for d in self.analysis["moves"].values()], [{"pointsLost": self.player_sign(self.next_player) * (self.analysis["root"]["scoreLead"] - d["scoreLead"]), **d} for d in self.analysis["moves"].values()],
key=lambda d: (d["order"], d["pointsLost"]), key=lambda d: (d["order"], d["pointsLost"]),
) )
@property
def policy_ranking(self) -> Optional[List]: # return moves from highest policy value to lowest
if self.policy:
ix = 0
moves = []
for y in range(self.board_size - 1, -1, -1):
for x in range(self.board_size):
moves.append((Move((x, y)), self.policy[ix]))
ix += 1
moves.append((Move(None), self.policy[ix]))
return sorted(moves, key=lambda mp: -mp[1])
+3 -4
View File
@@ -12,7 +12,7 @@ from game import Move
class BadukPanWidget(Widget): class BadukPanWidget(Widget):
board_color = ListProperty( [0.85, 0.68, 0.40] ) board_color = ListProperty([0.85, 0.68, 0.40])
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(BadukPanWidget, self).__init__(**kwargs) super(BadukPanWidget, self).__init__(**kwargs)
@@ -180,10 +180,9 @@ class BadukPanWidget(Widget):
Rectangle(pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz)) Rectangle(pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz))
ix = ix + 1 ix = ix + 1
policy = current_node.policy policy = current_node.policy
if not policy and current_node.parent and current_node.parent.policy and set(katrain.controls.ai_auto.active_map.values())=={True}: if not policy and current_node.parent and current_node.parent.policy and set(katrain.controls.ai_auto.active_map.values()) == {True}:
policy = current_node.parent.policy # in the case of AI self-play we allow the policy to be one step out of date policy = current_node.parent.policy # in the case of AI self-play we allow the policy to be one step out of date
pass_btn = katrain.board_controls.pass_btn pass_btn = katrain.board_controls.pass_btn
pass_btn.canvas.after.clear() pass_btn.canvas.after.clear()
if katrain.controls.policy.active and policy and not katrain.controls.ownership.active: if katrain.controls.policy.active and policy and not katrain.controls.ownership.active:
+29 -55
View File
@@ -11,7 +11,7 @@ class Controls(BoxLayout):
def set_status(self, msg, at_node=None): def set_status(self, msg, at_node=None):
self.status = msg self.status = msg
self.status_node = at_node or self.parent.game.current_node self.status_node = at_node or self.parent.game and self.parent.game.current_node
self.info.text = msg self.info.text = msg
self.update_evaluation() self.update_evaluation()
@@ -38,6 +38,9 @@ class Controls(BoxLayout):
else: else:
self.points_lost.text = f"..." self.points_lost.text = f"..."
def player_mode(self, player):
return self.player_mode_groups[player].value
def unlock(self): def unlock(self):
if self.ai_lock.active: if self.ai_lock.active:
self.ai_lock.checkbox.trigger_action(duration=0) self.ai_lock.checkbox.trigger_action(duration=0)
@@ -50,65 +53,36 @@ class Controls(BoxLayout):
# handles showing completed analysis and score graph # handles showing completed analysis and score graph
def update_evaluation(self): def update_evaluation(self):
katrain = self.parent katrain = self.parent
current_node = katrain.game.current_node current_node = katrain.game and katrain.game.current_node
move = current_node.single_move
current_player_is_human_or_both_robots = not current_node.player or not self.ai_auto.active(current_node.player) or self.ai_auto.active(current_node.next_player)
info = "" info = ""
if current_node is self.status_node: if current_node is self.status_node or (self.status is not None and self.status_node is None and current_node.is_root): # startup errors on root
info += self.status + "\n" info += self.status + "\n"
else: else:
self.status_node = None self.status_node = None
if current_player_is_human_or_both_robots and not current_node.is_root and move: if current_node:
info += current_node.comment(eval=True, hints=self.hints.active(move.player)) move = current_node.single_move
if current_player_is_human_or_both_robots: current_player_is_human_or_both_robots = not current_node.player or not self.ai_auto.active(current_node.player) or self.ai_auto.active(current_node.next_player)
self.show_evaluation_stats(current_node) if current_player_is_human_or_both_robots and not current_node.is_root and move:
info += current_node.comment(eval=True, hints=self.hints.active(move.player))
if current_player_is_human_or_both_robots:
self.show_evaluation_stats(current_node)
game_node = katrain.game.current_node
scores = [n.score for n in game_node.nodes_from_root]
# TODO: like redo, what is the node to redo / should we append? cache?
self.graph.canvas.clear()
with self.graph.canvas:
pt = []
nnscores = [s for s in scores if s is not None] + [-5, 5]
scale = max(max(*nnscores), -min(*nnscores)) * 1.05
xscale = self.graph.width * 0.9 / max(len(scores), 20)
ls = 0
for i, s in enumerate(scores):
ls = s or ls
pt.extend([self.graph.pos[0] + 0.05 * self.graph.width + i * xscale, self.graph.pos[1] + self.graph.height / 2 * (1 + ls / scale)])
Color(0, 0, 0)
Line(points=pt, width=1.0) # just set points?
self.info.text = info self.info.text = info
game_node = katrain.game.current_node
scores = [n.score for n in game_node.nodes_from_root]
# TODO: like redo, what is the node to redo / should we append? cache?
self.graph.canvas.clear()
with self.graph.canvas:
pt = []
nnscores = [s for s in scores if s is not None] + [-5, 5]
scale = max(max(*nnscores), -min(*nnscores)) * 1.05
xscale = self.graph.width * 0.9 / max(len(scores), 20)
ls = 0
for i, s in enumerate(scores):
ls = s or ls
pt.extend([self.graph.pos[0] + 0.05 * self.graph.width + i * xscale, self.graph.pos[1] + self.graph.height / 2 * (1 + ls / scale)])
Color(0, 0, 0)
Line(points=pt, width=1.0) # just set points?
if False: # TODO: UNDO AND AI MOVE
if current_node.analysis_ready and current_node.parent and current_node.parent.analysis_ready and not current_node.children and not current_node.x_comment.get("undo"):
# handle automatic undo
if self.auto_undo.active(move.player) and not self.ai_auto.active(move.player) and not current_node.auto_undid:
ts = self.train_settings
# TODO: is this overly generous wrt low visit outdated evaluations?
evaluation = current_node.evaluation if current_node.evaluation is not None else 1 # assume move is fine if temperature is negative
move_eval = max(evaluation, current_node.outdated_evaluation or 0)
points_lost = (current_node.parent or current_node).temperature_stats[2] * (1 - move_eval)
if move_eval < ts["undo_eval_threshold"] and points_lost >= ts["undo_point_threshold"]:
if self.num_undos(current_node) == 0:
current_node.x_comment["undid"] = f"Move was below threshold, but no undo granted (probability is {ts['num_undo_prompts']:.0%}).\n"
self.update_evaluation()
else:
current_node.auto_undid = True
self.parent.game.undo()
if len(current_node.parent.children) >= ts["num_undo_prompts"] + 1:
best_move = sorted([m for m in current_node.parent.children], key=lambda m: -(m.evaluation_info[0] or 0))[0]
best_move.x_comment["undo_autoplay"] = f"Automatically played as best option after max. {ts['num_undo_prompts']} undo(s).\n"
self.parent.game.play(best_move)
self.update_evaluation()
return
# ai player doesn't technically need parent ready, but don't want to override waiting for undo
current_node = self.parent.game.current_node # this effectively checks undo didn't just happen
if self.ai_auto.active(move.opponent) and not self.parent.game.game_ended:
if current_node.children:
self.info.text = "AI paused since moves were undone. Press 'AI Move' or choose a move for the AI to continue playing."
else:
self._do_aimove()
+65 -5
View File
@@ -1,20 +1,21 @@
import random
from kivy.clock import Clock
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, NumericProperty from kivy.properties import BooleanProperty, StringProperty, NumericProperty, ListProperty, ObjectProperty
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
import re import re
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.uix.checkbox import CheckBox from kivy.uix.checkbox import CheckBox
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label from kivy.uix.label import Label
from kivy.uix.spinner import Spinner from kivy.uix.spinner import Spinner
from kivy.uix.textinput import TextInput from kivy.uix.textinput import TextInput
class StyledButton(Button):
pass
class CheckBoxHint(BoxLayout): class CheckBoxHint(BoxLayout):
__events__ = ("on_active",) __events__ = ("on_active",)
@@ -30,6 +31,65 @@ class DarkLabel(Label):
pass pass
class StyledButton(Button):
pass
class StyledToggleButton(StyledButton, ToggleButtonBehavior):
value = StringProperty("")
class ToggleButtonContainer(GridLayout):
__events__ = ("on_selection",)
options = ListProperty([])
labels = ListProperty([])
selected = StringProperty("")
group = StringProperty(None)
autosize = BooleanProperty(True)
button_class = ObjectProperty(StyledToggleButton)
margin = ListProperty([1, 1, 0, 0])
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self._build, 0)
def on_selection(self, *args):
pass
def _build(self, _dt):
self.rows = 1
self.cols = len(self.options)
self.group = self.group or str(random.random())
if not self.selected and self.options:
self.selected = self.options[0]
if len(self.labels) < len(self.options):
self.labels += self.options[len(self.labels) + 1 :]
def state_handler(btn,*args):
self.dispatch("on_selection")
btn.state = 'down' # no toggle
for i, opt in enumerate(self.options):
state = "down" if opt == self.selected else "normal"
self.add_widget(self.button_class(group=self.group, text=self.labels[i], value=opt, state=state,
margin=self.margin,on_press=state_handler))
Clock.schedule_once(self._size, 0)
def _size(self, _dt):
if self.autosize:
for tb in self.children:
tb.size_hint = (tb.texture_size[0] + 3, 1)
@property
def value(self):
for tb in self.children:
if tb.state == "down":
return tb.value
if self.options:
return self.options[0]
class BaseCircleWithText(DarkLabel): class BaseCircleWithText(DarkLabel):
radius = NumericProperty(0.48) radius = NumericProperty(0.48)
+72 -93
View File
@@ -1,6 +1,8 @@
#:kivy 1.11.0 #:kivy 1.11.0
#:import ew kivy.uix.effectwidget #:import ew kivy.uix.effectwidget
# import utils?
# margin left bottom right top # margin left bottom right top
<StyledButton>: <StyledButton>:
text_color: 0.95,0.95,0.95,1 text_color: 0.95,0.95,0.95,1
@@ -24,7 +26,10 @@
pos: (self.pos[0]+self.margin[0],self.pos[1]+self.margin[1]) pos: (self.pos[0]+self.margin[0],self.pos[1]+self.margin[1])
radius: root.radius or (0.0,) radius: root.radius or (0.0,)
<StyledToggleButton@StyledButton+ToggleButtonBehavior>:
<StyledToggleButton>:
<ToggleButtonContainer>:
<StyledTabButton@StyledToggleButton>: <StyledTabButton@StyledToggleButton>:
bold: True bold: True
@@ -32,7 +37,10 @@
radius: (self.size[1]/3,self.size[1]/3,0,0) radius: (self.size[1]/3,self.size[1]/3,0,0)
<StyledIconButton@StyledButton>
<StyledIconButton@StyledButton>:
button_color: 0.71, 0.78, 0.81, 1 button_color: 0.71, 0.78, 0.81, 1
icon_margin: 0.15 * self.size[0] icon_margin: 0.15 * self.size[0]
icon: 'missing.png' icon: 'missing.png'
@@ -43,7 +51,7 @@
pos: [root.pos[i] + (root.size[i] - root.icon_size)/2 for i in [0,1]] if root.icon_size else [0,0] pos: [root.pos[i] + (root.size[i] - root.icon_size)/2 for i in [0,1]] if root.icon_size else [0,0]
source: root.icon source: root.icon
<TransparentIconButton@Button> <TransparentIconButton@Button>:
background_normal: '' background_normal: ''
background_color: (0,0,0,0) background_color: (0,0,0,0)
margin: (1,1,1,1) margin: (1,1,1,1)
@@ -61,17 +69,17 @@
<LargeLabel@DarkLabel>: <LargeLabel@DarkLabel>:
bold: True bold: True
<CheckBox> <CheckBox>:
color: (0.05,0.05,0.05,1) color: (0.05,0.05,0.05,1)
<LabelledCheckBox> <LabelledCheckBox>:
color: (0.95,0.95,0.95,1) color: (0.95,0.95,0.95,1)
<CheckBoxHintLabel@ButtonBehavior+DarkLabel>: <CheckBoxHintLabel@ButtonBehavior+DarkLabel>:
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
<CheckBoxHint> <CheckBoxHint>:
orientation: 'vertical' orientation: 'vertical'
checkbox: checkbox checkbox: checkbox
label: label label: label
@@ -90,7 +98,7 @@
on_active: root.dispatch('on_active') on_active: root.dispatch('on_active')
active: root.default_active active: root.default_active
<BWCheckBoxHint> <BWCheckBoxHint>:
black: black black: black
white: white white: white
label: label label: label
@@ -114,7 +122,7 @@
active: root.default_active active: root.default_active
on_active: root.dispatch('on_active') on_active: root.dispatch('on_active')
<CensorableLabel> <CensorableLabel>:
orientation: 'horizontal' orientation: 'horizontal'
text: '' text: ''
label: '' label: ''
@@ -130,7 +138,7 @@
id: value id: value
bold: True bold: True
<BaseCircleWithText> <BaseCircleWithText>:
text: '' text: ''
color: 0.95,0.95,0.95,1 color: 0.95,0.95,0.95,1
font_size: min(self.height,self.width) * 0.6 / (0.5 + 0.5 * len(self.text)) font_size: min(self.height,self.width) * 0.6 / (0.5 + 0.5 * len(self.text))
@@ -138,7 +146,7 @@
valign: 'center' valign: 'center'
bold: True bold: True
<BlackCircleWithText@BaseCircleWithText> <BlackCircleWithText@BaseCircleWithText>:
canvas.before: canvas.before:
Color: Color:
rgba: 0.05,0.05,0.05,1 rgba: 0.05,0.05,0.05,1
@@ -146,7 +154,7 @@
pos: self.pos[0] + self.width/2 - min(self.height,self.width) * root.radius, self.pos[1] + self.height/2 - min(self.height,self.width) * root.radius pos: self.pos[0] + self.width/2 - min(self.height,self.width) * root.radius, self.pos[1] + self.height/2 - min(self.height,self.width) * root.radius
size: min(self.height,self.width) * 2 * root.radius, min(self.height,self.width) * 2* root.radius size: min(self.height,self.width) * 2 * root.radius, min(self.height,self.width) * 2* root.radius
<WhiteCircleWithText@BaseCircleWithText> <WhiteCircleWithText@BaseCircleWithText>:
color: 0.05,0.05,0.05,1 color: 0.05,0.05,0.05,1
outline: True outline: True
canvas.before: canvas.before:
@@ -228,7 +236,7 @@
size_hint: 0.25, 1 size_hint: 0.25, 1
<Controls> <Controls>:
orientation: 'vertical' orientation: 'vertical'
play_tab_button: play_tab_button play_tab_button: play_tab_button
analyze_tab_button: analyze_tab_button analyze_tab_button: analyze_tab_button
@@ -246,6 +254,7 @@
ai_lock: ai_lock ai_lock: ai_lock
ai_move: ai_move ai_move: ai_move
auto_undo: auto_undo auto_undo: auto_undo
player_mode_groups: {'B':B_player_mode,'W':W_player_mode}
graph: graph graph: graph
katrain: self.parent katrain: self.parent
canvas.before: canvas.before:
@@ -345,39 +354,6 @@
size_hint: 1,1 size_hint: 1,1
opacity: 1 opacity: 1
id: play_tab id: play_tab
GridLayout:
cols: 5
rows: 1
size_hint: 1, 0.1
BoxLayout:
orientation: 'vertical'
size_hint: 0.1, 0.5
Label:
size_hint: 1, 0.2
BlackCircleWithText:
text: 'B'
WhiteCircleWithText:
text: 'W'
BWCheckBoxHint:
text: 'ai'
id: ai_auto
default_active: False
size_hint: 0.25, 1
BWCheckBoxHint:
size_hint: 0.2, 0.5
id: auto_undo
text: 'undo'
on_active: root.katrain.update_state()
CheckBoxHint:
size_hint: 0.25, 1
text: 'fast'
id: ai_fast
default_active: True
CheckBoxHint:
size_hint: 0.25, 1
text: 'balance\nscore'
id: ai_balance
default_active: False
GridLayout: GridLayout:
cols: 4 cols: 4
rows: 1 rows: 1
@@ -393,60 +369,30 @@
id: ai_lock id: ai_lock
on_active: self.checkbox.disabled = analyze_tab_button.disabled = ai_auto.white = ai_auto.black = ai_move.disabled = True on_active: self.checkbox.disabled = analyze_tab_button.disabled = ai_auto.white = ai_auto.black = ai_move.disabled = True
GridLayout: GridLayout:
cols: 6 cols: 2
rows: 2 rows: 2
size_hint: 1,0.1 size_hint: 1,0.125
BlackCircleWithText: BlackCircleWithText:
text: 'B' text: 'B'
size_hint: 0.4, 1 size_hint: 0.1, 1
StyledToggleButton: ToggleButtonContainer:
group: 'black' size_hint: 0.9, 1
text: 'Human' id: B_player_mode
state: 'down' options: ['human','human+undo','ai','ai+balance','ai+policy']
size_hint: 1, 1 labels: ['Human', ' AI\nAssist','AI',' AI\nBalance',' AI\nPolicy']
StyledToggleButton: on_selection: root.katrain.update_state()
group: 'black'
text: ' AI\nAssist'
size_hint: 1, 1
StyledToggleButton:
group: 'black'
text: 'AI'
size_hint: 0.5, 1
StyledToggleButton:
group: 'black'
text: ' AI\nBalance'
size_hint: 1, 1
StyledToggleButton:
group: 'black'
text: ' AI\nPolicy'
size_hint: 1, 1
WhiteCircleWithText: WhiteCircleWithText:
text: 'W' text: 'W'
size_hint: 0.4, 1 size_hint: 0.1, 1
StyledToggleButton: ToggleButtonContainer:
group: 'white' size_hint: 0.9, 1
text: 'Human' id: W_player_mode
state: 'down' options: ['human','human+undo','ai','ai+balance','ai+policy']
size_hint: 1, 1 labels: ['Human', ' AI\nAssist','AI',' AI\nBalance',' AI\nPolicy']
StyledToggleButton: on_selection: root.katrain.update_state()
group: 'white'
text: ' AI\nAssist'
size_hint: 1, 1
StyledToggleButton:
group: 'white'
text: 'AI'
size_hint: 0.5, 1
StyledToggleButton:
group: 'white'
text: ' AI\nBalance'
size_hint: 1, 1
StyledToggleButton:
group: 'white'
text: ' AI\nPolicy'
size_hint: 1, 1
LargeLabel: LargeLabel:
text: 'free real estate' text: 'free real estate'
size_hint: 1,0.1 size_hint: 1,0.2
CensorableLabel: CensorableLabel:
id: points_lost id: points_lost
size_hint: 1, 0.03 size_hint: 1, 0.03
@@ -456,6 +402,39 @@
id: info id: info
size_hint: 1, 0.25 size_hint: 1, 0.25
valign: 'middle' valign: 'middle'
GridLayout:
cols: 5
rows: 1
size_hint: 1, 0.1
BoxLayout:
orientation: 'vertical'
size_hint: 0.1, 0.5
Label:
size_hint: 1, 0.2
BlackCircleWithText:
text: 'B'
WhiteCircleWithText:
text: 'W'
BWCheckBoxHint:
text: 'ai'
id: ai_auto
default_active: False
size_hint: 0.25, 1
BWCheckBoxHint:
size_hint: 0.2, 0.5
id: auto_undo
text: 'undo'
on_active: root.katrain.update_state()
CheckBoxHint:
size_hint: 0.25, 1
text: 'balance\nscore'
id: ai_balance
default_active: False
CheckBoxHint:
size_hint: 0.25, 1
text: 'fast'
id: ai_fast
default_active: True
BoxLayout: BoxLayout:
orientation: 'horizontal' orientation: 'horizontal'
size_hint: 1, None size_hint: 1, None
@@ -545,7 +524,7 @@
color: (0.95, 0.95, 0.95) color: (0.95, 0.95, 0.95)
size_hint: 0.05,1 size_hint: 0.05,1
<NewGamePopup> <NewGamePopup>:
orientation: 'vertical' orientation: 'vertical'
rules_spinner: rules_spinner rules_spinner: rules_spinner
BoxLayout: BoxLayout:
+8 -7
View File
@@ -40,7 +40,7 @@ class KaTrainGui(BoxLayout):
def log(self, message, level=OUTPUT_INFO): def log(self, message, level=OUTPUT_INFO):
if level == OUTPUT_ERROR: if level == OUTPUT_ERROR:
self.controls.set_status(f"ERROR: {message}", self.game.current_node) self.controls.set_status(f"ERROR: {message}")
print(f"ERROR: {message}") print(f"ERROR: {message}")
elif self.debug_level >= level: elif self.debug_level >= level:
print(message) print(message)
@@ -81,10 +81,11 @@ class KaTrainGui(BoxLayout):
def update_state(self, redraw_board=False): def update_state(self, redraw_board=False):
# AI and Trainer/auto-undo handlers # AI and Trainer/auto-undo handlers
cn = self.game.current_node cn = self.game.current_node
auto_undo = cn.player and self.controls.auto_undo.active(cn.player) auto_undo = cn.player and "undo" in self.controls.player_mode(cn.player)
if auto_undo and cn.analysis_ready: if auto_undo and cn.analysis_ready:
self.game.analyze_undo(cn, self.config("trainer")) # not via message loop self.game.analyze_undo(cn, self.config("trainer")) # not via message loop
if cn.analysis_ready and self.controls.ai_auto.active(cn.next_player) and not cn.children and not self.game.game_ended and not (auto_undo and cn.auto_undo is None):
if cn.analysis_ready and "ai" in self.controls.player_mode(cn.next_player) and not cn.children and not self.game.game_ended and not (auto_undo and cn.auto_undo is None):
self("ai-move", cn) # cn mismatch stops this if undo fired self("ai-move", cn) # cn mismatch stops this if undo fired
# Handle prisoners and next player display # Handle prisoners and next player display
@@ -264,7 +265,7 @@ class KaTrainApp(App):
self.gui.start() self.gui.start()
def on_request_close(self, *args): def on_request_close(self, *args):
if getattr(self,'gui',None) and self.gui.engine: if getattr(self, "gui", None) and self.gui.engine:
self.gui.engine.shutdown() self.gui.engine.shutdown()
def signal_handler(self, signal, frame): def signal_handler(self, signal, frame):
@@ -284,8 +285,8 @@ class KaTrainApp(App):
if __name__ == "__main__": if __name__ == "__main__":
with open("gui.kv", encoding="utf-8") as f: # avoid windows using another encoding # with open("katrain.kv", encoding="utf-8") as f: # avoid windows using another encoding
Builder.load_string(f.read()) # Builder.load_string(f.read())
app = KaTrainApp() app = KaTrainApp()
signal.signal(signal.SIGINT, app.signal_handler) signal.signal(signal.SIGINT, app.signal_handler)
try: try:
+7 -7
View File
@@ -131,18 +131,18 @@ class SGFNode:
# root properties available on any node # root properties available on any node
@property @property
def board_size(self) -> Union[int,Tuple]: def board_size(self) -> Union[int, Tuple]:
size = str(self.root.get_first("SZ", '19')) size = str(self.root.get_first("SZ", "19"))
if ':' in size: if ":" in size:
return tuple(map(size.split(':'),int)) return tuple(map(size.split(":"), int))
return int(size) return int(size)
@property @property
def board_size_xy(self) -> Tuple[int,int]: def board_size_xy(self) -> Tuple[int, int]:
x, y = self.board_size x, y = self.board_size
if not y: if not y:
y=x y = x
return x,y return x, y
@property @property
def komi(self) -> float: def komi(self) -> float: