many bugfixes

This commit is contained in:
Sander Land committed 2020-01-25 19:30:51 +01:00
1 parent da5d99194b
commit 99930f4a7c
5 files changed
+52 -50

No files matched your search

+19 -20
View File
@@ -6,9 +6,11 @@ class Move:
GTP_COORD = "ABCDEFGHJKLMNOPQRSTUVWYXYZ" GTP_COORD = "ABCDEFGHJKLMNOPQRSTUVWYXYZ"
PLAYERS = "BW" PLAYERS = "BW"
SGF_COORD = [chr(i) for i in range(97, 123)] SGF_COORD = [chr(i) for i in range(97, 123)]
_move_id_counter = -1
def __init__(self, player, coords=None, gtpcoords=None, sgfcoords=None, robot=False): def __init__(self, player, coords=None, gtpcoords=None, sgfcoords=None, robot=False):
self.id = None Move._move_id_counter += 1
self.id = Move._move_id_counter
self.player = player self.player = player
self.coords = coords or (gtpcoords and self.gtp2ix(gtpcoords)) or self.sgf2ix(sgfcoords) self.coords = coords or (gtpcoords and self.gtp2ix(gtpcoords)) or self.sgf2ix(sgfcoords)
self.children = [] self.children = []
@@ -30,6 +32,7 @@ class Move:
def __hash__(self): def __hash__(self):
return self.gtp().__hash__() return self.gtp().__hash__()
# move tree building
def play(self, move): def play(self, move):
try: try:
return self.children[self.children.index(move)] return self.children[self.children.index(move)]
@@ -39,14 +42,6 @@ class Move:
self.children.append(move) self.children.append(move)
return move return move
def temperature(self):
if self.analysis:
best_score = float(self.analysis[0]["scoreLead"])
worst_score = -float(self.pass_analysis[0]["scoreLead"])
return best_score - worst_score
else:
return 0
### various analysis functions ### various analysis functions
def set_analysis(self, analysis_blob, is_pass): def set_analysis(self, analysis_blob, is_pass):
if is_pass: if is_pass:
@@ -68,7 +63,9 @@ class Move:
return "" return ""
text = f"Move {self.move_number}: {self.bw_player()} {self.gtp()} {'(AI Move)' if self.robot else ''}\n" text = f"Move {self.move_number}: {self.bw_player()} {self.gtp()} {'(AI Move)' if self.robot else ''}\n"
text += self.x_comment text += self.x_comment
text += "".join(f"Auto undid move {m.gtp()} ({m.evaluation*100:.1f}% efficient)\n" for m in self.children if m.auto_undid)
if eval and not sgf: # show undos and on previous move as well while playing
text += "".join(f"Auto undid move {m.gtp()} ({m.evaluation*100:.1f}% efficient)\n" for m in self.children if m.auto_undid)
if self.analysis_ready: if self.analysis_ready:
score, _, temperature = self.temperature_stats score, _, temperature = self.temperature_stats
@@ -81,7 +78,7 @@ class Move:
text += f"Top move was {self.parent.analysis[0]['move']} ({self.format_score(prev_best_score)})\n" text += f"Top move was {self.parent.analysis[0]['move']} ({self.format_score(prev_best_score)})\n"
text += f"Pass score was {self.format_score(prev_worst_score)}\n" text += f"Pass score was {self.format_score(prev_worst_score)}\n"
if prev_temperature < 0.5: if prev_temperature < 0.5:
text += f"Previous temperature ({prev_temperature}) too low for evaluation\n" text += f"Previous temperature ({prev_temperature:.1f}) too low for evaluation\n"
else: else:
if sgf or eval: if sgf or eval:
text += f"Evaluation: {100*self.evaluation:.1f}% efficient\n" text += f"Evaluation: {100*self.evaluation:.1f}% efficient\n"
@@ -91,6 +88,12 @@ class Move:
points_lost = self.player_sign * (prev_best_score - score) points_lost = self.player_sign * (prev_best_score - score)
if points_lost > 0.5: if points_lost > 0.5:
text += f"Estimate point loss: {points_lost:.1f}\n" text += f"Estimate point loss: {points_lost:.1f}\n"
if eval or sgf: # show undos on move itself in both sgf and while playing
undids = [m.gtp() + (f"({m.evaluation_info[0]*100:.1f}% efficient)" if m.evaluation_info[0] else "") for m in self.parent.children if m!=self]
if undids:
text += "Other attempted move(s): " + ", ".join(undids) + "\n"
else: else:
text = "No analysis available" if sgf else "Analyzing move..." text = "No analysis available" if sgf else "Analyzing move..."
return text return text
@@ -175,14 +178,12 @@ class Move:
class Board: class Board:
_move_id_counter = 0 # used to make a map to all moves across all games
def __init__(self, board_size=19): def __init__(self, board_size=19):
self.board_size = board_size self.board_size = board_size
self.root = Move(1, (None, None)) # root is 1=white so black is first self.root = Move(1, (None, None)) # root is 1=white so black is first
self.root.id = -1
self.current_move = self.root self.current_move = self.root
self.all_moves = {-1: self.root} self.all_moves = {self.root.id: self.root}
self._init_chains() self._init_chains()
# -- move tree functions -- # -- move tree functions --
@@ -253,9 +254,6 @@ class Board:
raise raise
move = self.current_move.play(move) # traverse or append move = self.current_move.play(move) # traverse or append
if not move.id:
move.id = Board._move_id_counter
Board._move_id_counter += 1
self.all_moves[move.id] = move self.all_moves[move.id] = move
self.current_move = move self.current_move = move
return move return move
@@ -295,13 +293,14 @@ class Board:
def stones(self): def stones(self):
return sum(self.chains, []) return sum(self.chains, [])
@property
def game_ended(self):
return self.current_move.parent and self.current_move.is_pass and self.current_move.parent.is_pass
@property @property
def prisoner_count(self): def prisoner_count(self):
return [sum([m.player==player for m in self.prisoners]) for player in [0,1]] return [sum([m.player==player for m in self.prisoners]) for player in [0,1]]
def sgf(self):
return "SGF[]"
def __str__(self): def __str__(self):
return ( return (
"\n".join("".join(Move.PLAYERS[self.chains[c][0].player] if c >= 0 else "-" for c in l) for l in self.board) "\n".join("".join(Move.PLAYERS[self.chains[c][0].player] if c >= 0 else "-" for c in l) for l in self.board)
+2 -1
View File
@@ -35,7 +35,8 @@
"balance_play_min_visits": 20, "balance_play_min_visits": 20,
"undo_eval_threshold": 0.875, "undo_eval_threshold": 0.875,
"undo_point_threshold": 1, "undo_point_threshold": 1,
"num_undo_prompts": 1 "num_undo_prompts": 1,
"sgf_show_best_move_threshold": 0.95
}, },
"debug": { "debug": {
"level": 1 "level": 1
+22 -21
View File
@@ -87,7 +87,7 @@ class EngineControls(GridLayout):
# mr.waiting_for_analysis # mr.waiting_for_analysis
self.redraw() self.redraw()
def update_evaluation(self): def update_evaluation(self,undo_triggered = False):
current_move = self.board.current_move current_move = self.board.current_move
if self.eval.active(current_move.player): if self.eval.active(current_move.player):
self.info.text = current_move.comment(eval=self.eval.active(current_move.player), hints=self.hints.active(current_move.player)) self.info.text = current_move.comment(eval=self.eval.active(current_move.player), hints=self.hints.active(current_move.player))
@@ -100,6 +100,7 @@ class EngineControls(GridLayout):
if current_move.analysis_ready and current_move.parent and current_move.parent.analysis_ready and not current_move.children: if current_move.analysis_ready and current_move.parent and current_move.parent.analysis_ready and not current_move.children:
# handle automatic undo # handle automatic undo
if self.auto_undo.active(current_move.player) and not self.ai_auto.active(current_move.player) and not current_move.auto_undid: if self.auto_undo.active(current_move.player) and not self.ai_auto.active(current_move.player) and not current_move.auto_undid:
ts = self.train_settings ts = self.train_settings
# TODO: is this overly generous wrt low visit outdated evaluations? # TODO: is this overly generous wrt low visit outdated evaluations?
@@ -108,13 +109,14 @@ class EngineControls(GridLayout):
if eval < ts["undo_eval_threshold"] and points_lost >= ts["undo_point_threshold"]: if eval < ts["undo_eval_threshold"] and points_lost >= ts["undo_point_threshold"]:
current_move.auto_undid = True current_move.auto_undid = True
self.board.undo() self.board.undo()
undo_triggered = True
if len(current_move.parent.children) >= ts["num_undo_prompts"] + 1: if len(current_move.parent.children) >= ts["num_undo_prompts"] + 1:
best_move = sorted([m for m in current_move.parent.children], key=lambda m: -(m.evaluation_info[0] or 0) )[0] best_move = sorted([m for m in current_move.parent.children], key=lambda m: -(m.evaluation_info[0] or 0) )[0]
best_move.x_comment = f"Automatically played as best option after max. {ts['num_undo_prompts']} undo(s).\n" best_move.x_comment = f"Automatically played as best option after max. {ts['num_undo_prompts']} undo(s).\n"
self.board.play(best_move) self.board.play(best_move)
self.update_evaluation() self.update_evaluation(undo_triggered=True)
# ai player doesn't technically need parent ready, but don't want to override waiting for undo # ai player doesn't technically need parent ready, but don't want to override waiting for undo
elif self.ai_auto.active(1 - current_move.player) and not current_move.children: elif self.ai_auto.active(1 - current_move.player) and not current_move.children and not undo_triggered and not self.board.game_ended:
self._do_aimove() self._do_aimove()
def _do_aimove(self): def _do_aimove(self):
@@ -130,20 +132,20 @@ class EngineControls(GridLayout):
for d in current_move.ai_moves for d in current_move.ai_moves
if int(d["visits"]) >= ts["balance_play_min_visits"] if int(d["visits"]) >= ts["balance_play_min_visits"]
] ]
selmove = pos_moves[0][0] sel_moves = [pos_moves[0][0]]
# don't play suicidal to balance score - pass when it's best # don't play suicidal to balance score - pass when it's best
if self.ai_balance.active and pos_moves[0][0] != "pass": if self.ai_balance.active and pos_moves[0][0] != "pass":
selmoves = [ sel_moves = [
move move
for move, score, eval in pos_moves for move, score, eval in pos_moves
if eval > ts["balance_play_randomize_eval"] if eval > ts["balance_play_randomize_eval"]
or eval > ts["balance_play_min_eval"] or eval > ts["balance_play_min_eval"]
and current_move.player_sign * score > ts["balance_play_target_score"] and -current_move.player_sign * score > ts["balance_play_target_score"]
] ] or sel_moves
selmove = random.choice(selmoves) # TODO: some kind of when further ahead play worse? aimove = Move(player=self.board.current_player, gtpcoords=random.choice(sel_moves), robot=True)
print('SEL',selmoves) if len(sel_moves) > 1:
print('POS',pos_moves, 'MOVE', selmove) aimove.x_comment = "{'AI Balance on, moves considered: " + ", ".join(f"{move} ({aimove.format_score(score)})" for move, score, eval in sel_moves) + "\n"
self.play(Move(player=self.board.current_player, gtpcoords=selmove, robot=True)) self.play(aimove)
def _do_undo(self): def _do_undo(self):
if self.ai_auto.active and self.board.current_move.robot: if self.ai_auto.active and self.board.current_move.robot:
@@ -218,22 +220,21 @@ class EngineControls(GridLayout):
query["analyzeTurns"][0] += 1 query["analyzeTurns"][0] += 1
self._send_analysis_query(query) self._send_analysis_query(query)
def sgf(self): def sgf(self):
def sgfify(mvs): def sgfify(mvs):
return f"(;GM[1]FF[4]SZ[{self.board_size}]KM[{self.komi}]RU[JP];" + ";".join(mvs) + ")" return f"(;GM[1]FF[4]SZ[{self.board_size}]KM[{self.komi}]RU[JP];" + ";".join(mvs) + ")"
def format_move(m, pm): def format_move(move, prev_move):
undo_comment = "".join(f"\nUndo: {u.gtp()} was {100*u.evaluation:.1f}%" for u in pm.undos if u.evaluation) undos = [m for m in prev_move.children if m!=move]
undo_cr = "".join(f"MA[{u.sgfcoords(self.board_size)}]" for u in pm.undos if u.coords[0]) undo_cr = "".join(f"MA[{u.sgfcoords(self.board_size)}]" for u in undos if u.coords[0])
if pm.analysis and pm.analysis[0]["move"] != "pass": if prev_move.analysis and prev_move.analysis[0]["move"] != "pass" and (move.evaluation_info[0] or 0.0) < self.train_settings['sgf_show_best_move_threshold']:
best_sq = f"SQ[{Move(gtpcoords=pm.analysis[0]['move'], player=0).sgfcoords(self.board_size)}]" best_sq = f"SQ[{Move(gtpcoords=prev_move.analysis[0]['move'], player=0).sgfcoords(self.board_size)}]"
else: else:
best_sq = "" best_sq = ""
return m.sgf(self.board_size) + f"C[{m.comment}{undo_comment}]{undo_cr}{best_sq}" return move.sgf(self.board_size) + f"C[{move.comment(sgf=True)}]{undo_cr}{best_sq}"
moves = self.board.moves
sgfmoves_small = [mv.sgf(self.board_size) for mv in self.moves[1:]] sgfmoves_small = [mv.sgf(self.board_size) for mv in moves]
sgfmoves = [format_move(mv, pmv) for mv, pmv in zip(self.moves[1:], self.moves[:-1])] sgfmoves = [format_move(mv, pmv) for mv, pmv in zip(moves, [self.board.root] + moves[:-1])]
with open("out.sgf", "w") as f: with open("out.sgf", "w") as f:
f.write(sgfify(sgfmoves)) f.write(sgfify(sgfmoves))
+1 -1
View File
@@ -201,7 +201,7 @@
size_hint: 0.2, 0.5 size_hint: 0.2, 0.5
text: 'lock\nai' text: 'lock\nai'
id: ai_lock id: ai_lock
on_active: self.checkbox.disabled = hints.black.disabled = hints.white.disabled = ai_move.disabled = auto_undo.black.disabled = auto_undo.white.disabled = ai_auto.checkbox.disabled = True on_active: self.checkbox.disabled = hints.black.disabled = hints.white.disabled = ai_auto.black.disabled = ai_auto.white.disabled = auto_undo.black.disabled = auto_undo.white.disabled = ai_move.disabled = True
GridLayout: GridLayout:
cols: 2 cols: 2
rows: 1 rows: 1
+8 -7
View File
@@ -128,14 +128,14 @@ class BadukPanWidget(Widget):
inner = COLORS[1 - m.player] if (m == last_move) else None inner = COLORS[1 - m.player] if (m == last_move) else None
self.draw_stone(m.coords[0], m.coords[1], COLORS[m.player], inner, evalcol, evalsize) self.draw_stone(m.coords[0], m.coords[1], COLORS[m.player], inner, evalcol, evalsize)
# ownership # ownership - allow one move out of date for smooth animation
if self.engine.ownership.active and last_move.ownership: ownership = last_move.ownership or (last_move.parent and last_move.parent.ownership)
ownership = last_move.ownership if self.engine.ownership.active and ownership:
rsz = self.grid_size * 0.2 rsz = self.grid_size * 0.2
ix = 0 ix = 0
for y in range(self.engine.board_size - 1, -1, -1): for y in range(self.engine.board_size - 1, -1, -1):
for x in range(self.engine.board_size): for x in range(self.engine.board_size):
ix_owner = current_player if ownership[ix] > 0 else 1 - current_player ix_owner = 0 if ownership[ix] > 0 else 1
if ix_owner != (has_stone.get((x, y), -1)): if ix_owner != (has_stone.get((x, y), -1)):
Color(*COLORS[ix_owner], abs(ownership[ix])) Color(*COLORS[ix_owner], abs(ownership[ix]))
Rectangle(pos=(self.gridpos[x] - rsz / 2, self.gridpos[y] - rsz / 2), size=(rsz, rsz)) Rectangle(pos=(self.gridpos[x] - rsz / 2, self.gridpos[y] - rsz / 2), size=(rsz, rsz))
@@ -145,9 +145,10 @@ class BadukPanWidget(Widget):
undo_coords = set() undo_coords = set()
alpha = Config.get("ui")["undo_alpha"] alpha = Config.get("ui")["undo_alpha"]
for m in last_move.children: for m in last_move.children:
if m.evaluation and m.coords[0] is not None: eval_info = m.evaluation_info
if eval_info[0] and m.coords[0] is not None:
undo_coords.add(m.coords) undo_coords.add(m.coords)
evalcol = (*self._eval_spectrum(m.evaluation), alpha) evalcol = (*self._eval_spectrum(eval_info[0]), alpha)
self.draw_stone(m.coords[0], m.coords[1], (*COLORS[m.player][:3], alpha), Config.get("ui")["undo_circle_col"], evalcol, self.EVAL_BOUNDS[1]) self.draw_stone(m.coords[0], m.coords[1], (*COLORS[m.player][:3], alpha), Config.get("ui")["undo_circle_col"], evalcol, self.EVAL_BOUNDS[1])
# hints # hints
@@ -165,7 +166,7 @@ class BadukPanWidget(Widget):
# pass circle # pass circle
passed = len(moves) > 1 and last_move.is_pass passed = len(moves) > 1 and last_move.is_pass
if passed: if passed:
if len(moves) > 2 and moves[-2].is_pass: if self.engine.board.game_ended:
text = "game\nend" text = "game\nend"
else: else:
text = "pass" text = "pass"