This commit is contained in:
Sander Land committed 2020-07-01 21:45:44 +02:00
1 parent 24bfdf3e4a
commit 7530eff4d3
8 files changed
+85 -50

No files matched your search

+1
View File
@@ -256,6 +256,7 @@ class KaTrainGui(Screen, KaTrainBase):
player_info.name = move_tree.root.get_property("P" + bw) player_info.name = move_tree.root.get_property("P" + bw)
self.update_player(bw) self.update_player(bw)
self.controls.graph.initialize_from_game(self.game.root) self.controls.graph.initialize_from_game(self.game.root)
self.controls.rank_graph.initialize_from_game(self.game.root)
self.update_state(redraw_board=True) self.update_state(redraw_board=True)
def _do_ai_move(self, node=None): def _do_ai_move(self, node=None):
+17 -7
View File
@@ -101,12 +101,15 @@ class KataGoEngine:
self.shutdown(finish=False) self.shutdown(finish=False)
self.start() self.start()
def check_alive(self, exception_if_dead=False): def check_alive(self, os_error='', exception_if_dead=False):
ok = self.katago_process and self.katago_process.poll() is None ok = self.katago_process and self.katago_process.poll() is None
if not ok and exception_if_dead: if not ok and exception_if_dead:
raise EngineDiedException( if self.katago_process:
f"Engine died (process {self.katago_process}, poll {self.katago_process and self.katago_process.poll()}) config {self.config}" os_error += f"status {self.katago_process and self.katago_process.poll()}"
) died_msg = i18n._("Engine died unexpectedly").format(error=os_error)
self.katrain.log(died_msg, OUTPUT_ERROR)
self.katago_process = None
raise EngineDiedException(died_msg)
return ok return ok
def shutdown(self, finish=False): def shutdown(self, finish=False):
@@ -134,15 +137,22 @@ class KataGoEngine:
self.katrain.log(line.decode(errors="ignore").strip(), OUTPUT_KATAGO_STDERR) self.katrain.log(line.decode(errors="ignore").strip(), OUTPUT_KATAGO_STDERR)
except Exception as e: except Exception as e:
print("ERROR in processing KataGo stderr:", line, "Exception", e) print("ERROR in processing KataGo stderr:", line, "Exception", e)
except: else:
self.check_alive(exception_if_dead=True)
except Exception as e:
self.katrain.log(f"Exception in reading stdout {e}" , OUTPUT_DEBUG)
return return
def _analysis_read_thread(self): def _analysis_read_thread(self):
while self.katago_process is not None: while self.katago_process is not None:
try: try:
line = self.katago_process.stdout.readline() line = self.katago_process.stdout.readline()
if not line:
self.check_alive(exception_if_dead=True)
except OSError as e: except OSError as e:
raise EngineDiedException(i18n("Engine died unexpectedly").format(error=e)) self.check_alive(os_error=str(e),exception_if_dead=True)
return
if b"Uncaught exception" in line: if b"Uncaught exception" in line:
self.katrain.log(f"KataGo Engine Failed: {line.decode(errors='ignore')}", OUTPUT_ERROR) self.katrain.log(f"KataGo Engine Failed: {line.decode(errors='ignore')}", OUTPUT_ERROR)
return return
@@ -193,7 +203,7 @@ class KataGoEngine:
self.katago_process.stdin.write((json.dumps(query) + "\n").encode()) self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
self.katago_process.stdin.flush() self.katago_process.stdin.flush()
except OSError as e: except OSError as e:
self.katrain.log(i18n._("Engine died unexpectedly").format(error=e), OUTPUT_ERROR) self.check_alive(os_error=str(e),exception_if_dead=True)
return # do not raise, since there's nothing to catch it return # do not raise, since there's nothing to catch it
def request_analysis( def request_analysis(
+18 -8
View File
@@ -7,10 +7,18 @@ from typing import Dict, List, Optional, Union
from kivy.clock import Clock from kivy.clock import Clock
from katrain.core.constants import HOMEPAGE, OUTPUT_DEBUG, OUTPUT_INFO, STATUS_ANALYSIS, STATUS_INFO, STATUS_TEACHING from katrain.core.constants import (
HOMEPAGE,
OUTPUT_DEBUG,
OUTPUT_INFO,
STATUS_ANALYSIS,
STATUS_INFO,
STATUS_TEACHING,
PLAYER_AI,
)
from katrain.core.engine import KataGoEngine from katrain.core.engine import KataGoEngine
from katrain.core.game_node import GameNode from katrain.core.game_node import GameNode
from katrain.core.lang import i18n from katrain.core.lang import i18n, rank_label
from katrain.core.sgf_parser import SGF, Move from katrain.core.sgf_parser import SGF, Move
from katrain.core.utils import var_to_grid from katrain.core.utils import var_to_grid
@@ -270,12 +278,14 @@ class Game:
def player_name(player_info): def player_name(player_info):
return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})" return f"{i18n._(player_info.player_type)} ({i18n._(player_info.player_subtype)})"
player_names = { if "KaTrain" in self.root.get_property("AP", ""):
bw: re.sub( for bw in "BW":
r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw) or player_name(self.katrain.players_info[bw]) self.root.set_property("P" + bw, player_name(self.katrain.players_info[bw]))
) player_info = self.katrain.players_info[bw]
for bw in "BW" if player_info.player_type == PLAYER_AI:
} self.root.set_property(bw + "R", rank_label(player_info.calculated_rank))
player_names = {bw: re.sub(r"['<>:\"/\\|?*]", "", self.root.get_property("P" + bw, bw)) for bw in "BW"}
game_name = f"katrain_{player_names['B']} vs {player_names['W']} {self.game_id}" game_name = f"katrain_{player_names['B']} vs {player_names['W']} {self.game_id}"
file_name = os.path.abspath(os.path.join(path, f"{game_name}.sgf")) file_name = os.path.abspath(os.path.join(path, f"{game_name}.sgf"))
os.makedirs(os.path.dirname(file_name), exist_ok=True) os.makedirs(os.path.dirname(file_name), exist_ok=True)
+1 -1
View File
@@ -85,4 +85,4 @@ def rank_label(rank):
if rank > 0: if rank > 0:
return f"{rank:.0f}{i18n._('strength:dan')}" return f"{rank:.0f}{i18n._('strength:dan')}"
else: else:
return f"{1-rank:.0f}{i18n._('strength:kyu')}" return f"{1-rank:.0f}{i18n._('strength:kyu')}"
+2 -2
View File
@@ -31,8 +31,8 @@ EVAL_COLORS = [
[0.8, 0, 0, 1], [0.8, 0, 0, 1],
[0.9, 0.4, 0.1, 1], [0.9, 0.4, 0.1, 1],
[0.85, 0.89, 0.3, 1], [0.85, 0.89, 0.3, 1],
[0.67, 0.9, 0.18, 1.0], [0.67, 0.9, 0.18, 1],
[0.117, 0.588, 0, 1.0], [0.117, 0.588, 0, 1],
] ]
EVAL_DOT_MAX_SIZE = 0.5 EVAL_DOT_MAX_SIZE = 0.5
EVAL_DOT_MIN_SIZE = 0.25 EVAL_DOT_MIN_SIZE = 0.25
+46 -31
View File
@@ -4,23 +4,25 @@ import threading
from kivy.lang import Builder from kivy.lang import Builder
from kivy.metrics import dp from kivy.metrics import dp
from kivy.properties import BooleanProperty, Clock, ListProperty, NumericProperty, StringProperty from kivy.properties import BooleanProperty, Clock, ListProperty, NumericProperty, StringProperty
from kivy.uix.widget import Widget
from kivymd.app import MDApp from kivymd.app import MDApp
from katrain.core.constants import OUTPUT_ERROR from katrain.core.constants import OUTPUT_ERROR
from katrain.core.lang import rank_label from katrain.core.lang import rank_label
from katrain.gui.kivyutils import BackgroundMixin
class Graph(BackgroundMixin): class Graph(Widget):
marker_font_size = NumericProperty(0) marker_font_size = NumericProperty(0)
background_image = StringProperty("img/graph_bg.png") background_image = StringProperty("img/graph_bg.png")
background_color = ListProperty([1, 1, 1, 1])
highlighted_index = NumericProperty(0)
nodes = ListProperty([]) nodes = ListProperty([])
hidden = BooleanProperty(False) hidden = BooleanProperty(False)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._lock = threading.Lock() self._lock = threading.Lock()
self.redraw_on_highlight_change = True
self.bind(pos=self.update_graph, size=self.update_graph) self.bind(pos=self.update_graph, size=self.update_graph)
self.redraw_trigger = Clock.create_trigger(self.update_graph, 0.1) self.redraw_trigger = Clock.create_trigger(self.update_graph, 0.1)
@@ -31,19 +33,24 @@ class Graph(BackgroundMixin):
node = node.ordered_children[0] node = node.ordered_children[0]
self.nodes.append(node) self.nodes.append(node)
self.highlighted_index = 0 self.highlighted_index = 0
self.redraw_trigger()
def update_graph(self, *args): def update_graph(self, *args):
pass pass
def update_value(self, node): def update_value(self, node):
with self._lock: with self._lock:
changed = (self.redraw_on_highlight_change and self.highlighted_index != node.depth)
self.highlighted_index = index = node.depth self.highlighted_index = index = node.depth
self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1))) self.nodes.extend([None] * max(0, index - (len(self.nodes) - 1)))
if self.nodes[index]!=node:
changed=True
self.nodes[index] = node self.nodes[index] = node
if index > 1 and node.parent: # sometimes things go so fast if index > 1 and node.parent: # sometimes there are gaps
backfill, bfnode = index - 1, node.parent backfill, bfnode = index - 1, node.parent
while bfnode is not None and self.nodes[backfill] != bfnode: while bfnode is not None and self.nodes[backfill] != bfnode:
self.nodes[backfill] = bfnode self.nodes[backfill] = bfnode
changed = True
backfill -= 1 backfill -= 1
bfnode = bfnode.parent bfnode = bfnode.parent
@@ -51,11 +58,13 @@ class Graph(BackgroundMixin):
node is None or not node.children or self.nodes[index + 1] != node.ordered_children[0] node is None or not node.children or self.nodes[index + 1] != node.ordered_children[0]
): ):
self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch self.nodes = self.nodes[: index + 1] # on branch switching, don't show history from other branch
changed = True
if index == len(self.nodes) - 1: # possibly just switched branch or the line above triggered if index == len(self.nodes) - 1: # possibly just switched branch or the line above triggered
while node.children: # add children back while node.children: # add children back
node = node.ordered_children[0] node = node.ordered_children[0]
self.nodes.append(node) self.nodes.append(node)
self.redraw_trigger() if changed:
self.redraw_trigger()
class ScoreGraph(Graph): class ScoreGraph(Graph):
@@ -67,7 +76,6 @@ class ScoreGraph(Graph):
score_dot_pos = ListProperty([0, 0]) score_dot_pos = ListProperty([0, 0])
winrate_dot_pos = ListProperty([0, 0]) winrate_dot_pos = ListProperty([0, 0])
highlighted_index = NumericProperty(None)
highlight_size = NumericProperty(dp(6)) highlight_size = NumericProperty(dp(6))
score_scale = NumericProperty(5) score_scale = NumericProperty(5)
@@ -173,9 +181,11 @@ class RankGraph(Graph):
black_rank_points = ListProperty([]) black_rank_points = ListProperty([])
white_rank_points = ListProperty([]) white_rank_points = ListProperty([])
segment_length = NumericProperty(80) segment_length = NumericProperty(80)
RANK_CAP = 5
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.redraw_on_highlight_change = False
self.calculate_trigger = Clock.create_trigger(lambda *args: self.rank_game(), 0.25) self.calculate_trigger = Clock.create_trigger(lambda *args: self.rank_game(), 0.25)
self.rank_by_player = {} self.rank_by_player = {}
@@ -189,11 +199,7 @@ class RankGraph(Graph):
if not non_obvious_moves: if not non_obvious_moves:
return None return None
num_legal, rank, value = zip(*non_obvious_moves) num_legal, rank, value = zip(*non_obvious_moves)
rank = list(rank) rank = [min(r, nl * 0.09) for r, nl in zip(rank, num_legal)]
for (i, item) in enumerate(rank):
if item > num_legal[i] * 0.09:
rank[i] = num_legal[i] * 0.09
rank = tuple(rank)
averagemod_rank = averagemod(rank) averagemod_rank = averagemod(rank)
averagemod_len_legal = averagemod(num_legal) averagemod_len_legal = averagemod(num_legal)
norm_avemod_len_legal = averagemod_len_legal / num_intersec norm_avemod_len_legal = averagemod_len_legal / num_intersec
@@ -209,9 +215,7 @@ class RankGraph(Graph):
) )
else: else:
rank_kyu = -4 rank_kyu = -4
if rank_kyu < -4: return min(RankGraph.RANK_CAP,1 - rank_kyu) # dan rank
rank_kyu = -4
return 1 - rank_kyu # dan rank
@staticmethod @staticmethod
def calculate_ranks(segment_stats, num_intersec): def calculate_ranks(segment_stats, num_intersec):
@@ -241,10 +245,11 @@ class RankGraph(Graph):
ranks = {"B": [], "W": []} ranks = {"B": [], "W": []}
dx = self.segment_length // 4 dx = self.segment_length // 4
for segment_mid in range(0, len(nodes), dx): for segment_mid in range(dx, len(nodes), dx):
bounds = (max(0, segment_mid - half_seg), min(segment_mid + half_seg, len(nodes))) bounds = (max(0, segment_mid - half_seg), min(segment_mid + half_seg, len(nodes)))
for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items(): num_analyzed = sum(num_mv is not None for num_mv, _, _, _ in policy_stats[bounds[0] : bounds[1] + 1])
if bounds[1] - bounds[0] > self.segment_length * 0.75: if num_analyzed >= self.segment_length * 0.75:
for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items():
ranks[pl].append((segment_mid, rank)) ranks[pl].append((segment_mid, rank))
self.rank_by_player = ranks self.rank_by_player = ranks
except Exception as e: except Exception as e:
@@ -270,15 +275,17 @@ class RankGraph(Graph):
min_rank = math.floor(min(all_ranks)) min_rank = math.floor(min(all_ranks))
max_rank = math.ceil(max(all_ranks)) max_rank = math.ceil(max(all_ranks))
if max_rank==min_rank:
min_rank -= 1
if (max_rank - min_rank) % 2 != 0: # make midpoint whole integer if (max_rank - min_rank) % 2 != 0: # make midpoint whole integer
if abs(max_rank - max(all_ranks)) < abs(min(all_ranks) - min_rank): if abs(max_rank - max(all_ranks)) < abs(min(all_ranks) - min_rank) and max_rank < self.RANK_CAP:
max_rank += 1 max_rank += 1
else: else:
min_rank -= 1 min_rank -= 1
rank_range = max_rank - min_rank rank_range = max_rank - min_rank
self.ids.mid_marker.text = rank_label((max_rank + min_rank) / 2) self.ids.mid_marker.text = rank_label((max_rank + min_rank) / 2)
self.ids.top_marker.text = rank_label(max_rank) self.ids.top_marker.text = rank_label(max_rank) + ("+" if max_rank==self.RANK_CAP else "")
self.ids.bottom_marker.text = rank_label(min_rank) self.ids.bottom_marker.text = rank_label(min_rank)
graph_points = {} graph_points = {}
@@ -292,12 +299,16 @@ class RankGraph(Graph):
] ]
self.black_rank_points = sum(graph_points["B"], []) self.black_rank_points = sum(graph_points["B"], [])
self.white_rank_points = sum(graph_points["W"], []) self.white_rank_points = sum(graph_points["W"], [])
else:
self.black_rank_points = []
self.white_rank_points = []
Builder.load_string( Builder.load_string(
""" """
#:set GRAPH_CENTER_COLOR [0.5,0.5,0.5] #:set GRAPH_CENTER_COLOR [0.5,0.5,0.5]
#:set GRAPH_DOT_COLOR [0.85, 0.3, 0.3,1] #:set GRAPH_DOT_COLOR [0.85,0.3,0.3,1]
#:set WINRATE_MARKER_COLOR [0.05, 0.7, 0.05, 1]
#:set SCORE_MARKER_COLOR [0.2, 0.6, 0.8, 1]
#:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR #:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR
#:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR #:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR
@@ -307,11 +318,15 @@ Builder.load_string(
#:import WHITE katrain.gui.style.WHITE #:import WHITE katrain.gui.style.WHITE
#:import YELLOW katrain.gui.style.YELLOW #:import YELLOW katrain.gui.style.YELLOW
<Graph>: <Graph>:
background_color: BOX_BACKGROUND_COLOR background_color: BOX_BACKGROUND_COLOR
marker_font_size: 0.1 * self.height marker_font_size: 0.1 * self.height
canvas: canvas.before:
Color:
rgba: root.background_color
Rectangle:
size: self.size
pos: self.pos
Color: Color:
rgba: [1,1,1,1] rgba: [1,1,1,1]
Rectangle: Rectangle:
@@ -320,7 +335,7 @@ Builder.load_string(
source: root.background_image source: root.background_image
<ScoreGraph>: <ScoreGraph>:
canvas.after: canvas:
Color: Color:
rgba: SCORE_COLOR rgba: SCORE_COLOR
Line: Line:
@@ -351,39 +366,39 @@ Builder.load_string(
# score ticks # score ticks
GraphMarkerLabel: GraphMarkerLabel:
font_size: root.marker_font_size font_size: root.marker_font_size
color: SCORE_COLOR color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.pos[1]+root.height - self.font_size - 1 pos: root.x + root.width - self.width-1, root.pos[1]+root.height - self.font_size - 1
text: 'B+{}'.format(root.score_scale) text: 'B+{}'.format(root.score_scale)
opacity: int(root.show_score) opacity: int(root.show_score)
GraphMarkerLabel: GraphMarkerLabel:
font_size: root.marker_font_size font_size: root.marker_font_size
color: SCORE_COLOR color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.y + root.height*0.5 - self.height/2 + 2 pos: root.x + root.width - self.width-1, root.y + root.height*0.5 - self.height/2 + 2
text: i18n._('Jigo') text: i18n._('Jigo')
opacity: int(root.show_score) opacity: int(root.show_score)
GraphMarkerLabel: GraphMarkerLabel:
font_size: root.marker_font_size font_size: root.marker_font_size
color: SCORE_COLOR color: SCORE_MARKER_COLOR
pos: root.x + root.width - self.width-1, root.pos[1] pos: root.x + root.width - self.width-1, root.pos[1]
text: 'W+' + str(int(root.score_scale)) text: 'W+' + str(int(root.score_scale))
opacity: int(root.show_score) opacity: int(root.show_score)
# wr ticks # wr ticks
GraphMarkerLabel: GraphMarkerLabel:
font_size: root.marker_font_size font_size: root.marker_font_size
color: WINRATE_COLOR color: WINRATE_MARKER_COLOR
pos: root.pos[0]+1, root.pos[1] + root.height - self.font_size - 1 pos: root.pos[0]+1, root.pos[1] + root.height - self.font_size - 1
text: "{}%".format(50 + root.winrate_scale) text: "{}%".format(50 + root.winrate_scale)
opacity: int(root.show_winrate) opacity: int(root.show_winrate)
GraphMarkerLabel: GraphMarkerLabel:
font_size: root.marker_font_size font_size: root.marker_font_size
color: WINRATE_COLOR color: WINRATE_MARKER_COLOR
pos:root.pos[0]+1, root.pos[1] pos:root.pos[0]+1, root.pos[1]
text: "{}%".format(50 - root.winrate_scale) text: "{}%".format(50 - root.winrate_scale)
opacity: int(root.show_winrate) opacity: int(root.show_winrate)
<RankGraph>: <RankGraph>:
background_color: LIGHTER_BACKGROUND_COLOR background_color: LIGHTER_BACKGROUND_COLOR
canvas.after: canvas:
Color: Color:
rgba: WHITE rgba: WHITE
Line: Line:
@@ -635,4 +635,3 @@ msgstr "Estimated Strength"
#. TODO - label in little popup for analyzing entire game #. TODO - label in little popup for analyzing entire game
msgid "reanalyze max visits" msgid "reanalyze max visits"
msgstr "Number of visits per move" msgstr "Number of visits per move"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB