98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
import copy
|
|
import random
|
|
from typing import Dict, List, Optional
|
|
|
|
from sgf_parser import SGFNode
|
|
|
|
|
|
class GameNode(SGFNode):
|
|
"""Represents a single game node, with one or more moves and placements."""
|
|
|
|
def __init__(self, parent=None, properties=None, move=None):
|
|
super().__init__(parent=parent, properties=properties, move=move)
|
|
self.analysis = None
|
|
self.ownership = None
|
|
self.auto_undid = False
|
|
self.move_number = 0
|
|
self.undo_threshold = random.random() # for fractional undos, store the random threshold in the move itself for consistency
|
|
|
|
@property
|
|
def sgf_properties(self):
|
|
best_sq = []
|
|
properties = copy.copy(super().sgf_properties)
|
|
if best_sq and "SQ" not in properties:
|
|
properties["SQ"] = best_sq
|
|
comment = self.comment(sgf=True)
|
|
if comment:
|
|
properties["C"] = [properties.get("C", "") + comment]
|
|
return properties
|
|
|
|
# various analysis functions
|
|
def analyze(self, engine, priority=0):
|
|
engine.request_analysis(self, lambda result: self.set_analysis(result), priority=priority)
|
|
|
|
def set_analysis(self, analysis_blob):
|
|
self.analysis = analysis_blob["moveInfos"] # TODO: fix when rootInfos comes in
|
|
self.ownership = analysis_blob["ownership"]
|
|
|
|
@property
|
|
def analysis_ready(self):
|
|
return self.analysis is not None
|
|
|
|
def format_score(self, score=None):
|
|
score = score or self.score
|
|
return f"{'B' if score >= 0 else 'W'}+{abs(score):.1f}"
|
|
|
|
def format_win_rate(self, win_rate=None):
|
|
win_rate = win_rate or self.analysis[0]['winrate']
|
|
b_adv = win_rate-0.5
|
|
return f"{'B' if b_adv > 0 else 'W'}+{abs(b_adv):.1%}"
|
|
|
|
|
|
def comment(self, sgf=False, eval=False, hints=False):
|
|
single_move = self.single_move
|
|
if not self.parent or not single_move: # root
|
|
return ""
|
|
|
|
text = f"Move {self.depth}: {single_move.player} {single_move.gtp()}\n"
|
|
|
|
if self.analysis_ready:
|
|
score = self.score
|
|
if sgf:
|
|
text += f"Score: {self.format_score(score)}\n"
|
|
if self.parent and self.parent.analysis_ready:
|
|
if sgf or hints:
|
|
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
|
|
if points_lost > 0.5:
|
|
text += f"Estimated point loss: {points_lost:.1f}\n"
|
|
else:
|
|
text = "No analysis available" if sgf else "Analyzing move..."
|
|
return text
|
|
|
|
@property
|
|
def points_lost(self) -> Optional[float]:
|
|
single_move = self.single_move
|
|
if single_move and self.parent and self.analysis_ready and self.parent.analysis_ready:
|
|
parent_score = self.parent.score
|
|
score = self.score
|
|
return self.player_sign(single_move.player) * (parent_score - score)
|
|
|
|
@property
|
|
def score(self):
|
|
return self.analysis[0]["scoreLead"] # TODO: update for rootInfo
|
|
|
|
@staticmethod
|
|
def player_sign(player):
|
|
return {"B": 1, "W": -1, None: 0}[player]
|
|
|
|
@property
|
|
def ai_moves(self) -> List[Dict]:
|
|
if not self.analysis_ready:
|
|
return []
|
|
analysis = copy.copy(self.analysis) # not deep, so eval is saved, but avoids race conditions
|
|
for d in analysis:
|
|
d["pointsLost"] = self.player_sign(self.next_player) * (analysis[0]["scoreLead"] - d["scoreLead"]) # TODO: update for rootInfo
|
|
return analysis
|