This commit is contained in:
Sander Land committed 2020-04-27 23:03:42 +02:00
1 parent c4009a4fc8
commit da2677983b
15 files changed
+190 -122

No files matched your search

+6 -6
View File
@@ -51,14 +51,14 @@ Available AIs are:
* Balance is KataGo occasionally making weaker moves, attempting to win by ~2 points.
* Jigo is KataGo aggressively making weaker moves, attempting to win by 0.5 points.
* Policy is the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used.
* P+Pick will pick a `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
* P:Pick will pick a `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders.
This is probably the best choice for kyu players who want a chance of winning. Variants of this strategy include:
* P+Local will pick such moves biased towards the last move with probability related to `local_stddev`.
* P+Tenuki is biased in the opposite way as P+Local, using the same setting.
* P+Influence is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight.
* P+Territory is biased in the opposite way, towards 1-3rd line moves, using the same setting.
* P+Noise mixes the policy with `noise_strength` Dirichlet noise. At `noise_strength=0.9` play is near-random, while `noise_strength=0.7` is still quite strong. Regardless, mistakes are typically strange can include senseless first-line moves.
* P:Local will pick such moves biased towards the last move with probability related to `local_stddev`.
* P:Tenuki is biased in the opposite way as P:Local, using the same setting.
* P:Influence is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight.
* P:Territory is biased in the opposite way, towards 1-3rd line moves, using the same setting.
* P:Noise mixes the policy with `noise_strength` Dirichlet noise. At `noise_strength=0.9` play is near-random, while `noise_strength=0.7` is still quite strong. Regardless, mistakes are typically strange can include senseless first-line moves.
* `<Pause>` pauses AI moves, in case you want to do analysis without triggering moves, or simply hide the evaluation dots for this player.
Selecting the AI as either white or black opens up the option to configure it under 'Configure AI'.
+2 -2
View File
@@ -3,14 +3,14 @@
[x] remove fast -> in settings?
[x] Polish graph
[x] BoxLayout padding/spacing use instead of fiddling?
[x] P+noise pass not noisy
[x] P:noise pass not noisy
[x] graph wonky on branch switch -> check in children &c.
[x] Scrolling add a move on the board instead of navigating through the game. This was already the case in the 0.9 version and it's quite annoying as scrolling seemed only natural and I kept forgetting not to do it :p
[x] show PV on hint hover? Although Katrain wasn't meant to be like Lizzie to begin with, it would be really neat if we could visualize the expected variations when hovering over the top moves.
[x] Self-play tournaments in separate script.
[x] ai thoughts in sgf
[x] more AI modes?
[x] pol value override > 0.95 ? policy threshold override p+pick?
[x] pol value override > 0.95 ? policy threshold override P:pick?
[x] config player to sep. row/popups?
[x] dots: teacher settings fixes
[x] ogs bot connector
+2 -2
View File
@@ -35,7 +35,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
ai_mode = ai_mode.lower()
ai_thoughts = ""
candidate_ai_moves = cn.candidate_moves
if ("policy" in ai_mode or "p+" in ai_mode) and cn.policy:
if ("policy" in ai_mode or "p:" in ai_mode) and cn.policy:
policy_moves = cn.policy_ranking
pass_policy = cn.policy[-1]
top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]]) # dont make it jump around for the last few sensible non pass moves
@@ -68,7 +68,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
new_top = heapq.nlargest(5, noisy_policy_moves)
aimove = new_top[0][1]
ai_thoughts += f"Noisy policy strategy (strength={noise_str:.2f}) generated 5 moves {fmt_moves(new_top)} so picked {aimove.gtp()}. "
elif "p+" in ai_mode:
elif "p:" in ai_mode:
n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])
if "influence" in ai_mode or "territory" in ai_mode:
if "influence" in ai_mode:
+2 -2
View File
@@ -42,7 +42,7 @@ with open("config.json") as f:
settings = json.load(f)
all_ai_settings = settings["ai"]
all_ai_settings["dev"] = all_ai_settings["P+Noise"]
all_ai_settings["dev"] = all_ai_settings["P:Noise"]
ai_strategy = bot_strategy_names[bot]
ai_settings = all_ai_settings[ai_strategy]
@@ -62,7 +62,7 @@ while not game.ended:
logger.log(f"Init game {game.root.properties}", OUTPUT_ERROR)
if "komi" in line:
_, komi = line.split(" ")
game.root.properties["KM"] = [komi.strip()]
game.root.set_property("KM", komi.strip())
logger.log(f"Setting komi {game.root.properties}", OUTPUT_ERROR)
elif "genmove" in line:
logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR)
+7 -7
View File
@@ -7,14 +7,14 @@ OUTPUT_EXTRA_DEBUG = 2
bot_strategy_names = {
"dev": "P+Noise",
"dev": "P:Noise",
"strong": "Policy",
"influence": "P+Influence",
"territory": "P+Territory",
"balanced": "P+Pick",
"weighted": "P+Weighted",
"local": "P+Local",
"tenuki": "P+Tenuki",
"influence": "P:Influence",
"territory": "P:Territory",
"balanced": "P:Pick",
"weighted": "P:Weighted",
"local": "P:Local",
"tenuki": "P:Tenuki",
}
+7 -7
View File
@@ -61,25 +61,25 @@
"_help_right": "No settings available for this mode, strength is mainly affected by `model` in engine settings, but should be high dan regardless.",
"_help_left": ""
},
"P+Weighted": {
"P:Weighted": {
"_help_right": "Strength is mainly affected by `model` in engine settings.",
"_help_left": "`pick_override` determines when top move is chosen without randomness, and is effectively disabled by default (1.0).",
"pick_override": 1.0
},
"P+Noise": {
"P:Noise": {
"pick_override": 0.95,
"noise_strength": 0.8,
"_help_right": "Adds `noise_strength` noise to the policy and plays the top move.",
"_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes. Noise above 0.9 is near random, below 0.7 is fairly strong."
},
"P+Pick": {
"P:Pick": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.33,
"_help_right": "Picks `pick_n + pick_frac * <number of legal moves>` at random and plays the best one. Change `pick_frac` to make it see more moves.",
"_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes."
},
"P+Local": {
"P:Local": {
"pick_override": 0.95,
"stddev": 1.5,
"pick_n": 15,
@@ -87,7 +87,7 @@
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` near the last move and plays the best one.",
"_help_left": "Lower `stddev` makes it prefer closer moves."
},
"P+Tenuki": {
"P:Tenuki": {
"pick_override": 0.9,
"stddev": 7.5,
"pick_n": 20,
@@ -95,7 +95,7 @@
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` away from the last move and plays the best one.",
"_help_left": "Increase `stddev` makes it prefer moves further away."
},
"P+Influence": {
"P:Influence": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.4,
@@ -103,7 +103,7 @@
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` and plays the best one, biased towards the center.",
"_help_left": "Increase `line_weight` to penalize moves near the edge more."
},
"P+Territory": {
"P:Territory": {
"pick_override": 0.95,
"pick_n": 5,
"pick_frac": 0.4,
+3 -3
View File
@@ -35,7 +35,7 @@ class Game:
if move_tree:
self.root = move_tree
self.komi = self.root.komi
handicap = int(self.root.get_first("HA", 0))
handicap = int(self.root.get_property("HA", 0))
if handicap and not self.root.placements:
self.place_handicap_stones(handicap)
else:
@@ -174,7 +174,7 @@ class Game:
if n_handicaps % 2 == 1:
stones.append((middle_x, middle_y))
stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)]
self.root.add_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]])
self.root.set_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]])
@property
def board_size(self):
@@ -200,7 +200,7 @@ class Game:
return "\n".join("".join(Move.PLAYERS[self.chains[c][0].player] if c >= 0 else "-" for c in line) for line in self.board) + f"\ncaptures: {self.prisoner_count}"
def write_sgf(self, path=None):
black, white = self.root.get_first("PB"), self.root.get_first("PW")
black, white = self.root.get_property("PB"), self.root.get_property("PW")
black = re.sub(r"['<>:\"/\\|?*]", "", black or "Black")
white = re.sub(r"['<>:\"/\\|?*]", "", white or "White")
game_name = f"katrain_{black} vs {white} {self.game_id}"
+1 -1
View File
@@ -239,7 +239,7 @@ class BadukPanWidget(Widget):
for y in range(board_size_y - 1, -1, -1):
for x in range(board_size_x):
if policy_grid[y][x] > 0:
polsize = math.sqrt(policy_grid[y][x])
polsize = 1.1 * math.sqrt(policy_grid[y][x])
policy_circle_color = (
*self.ui_config["policy_color"],
self.ui_config["ghost_alpha"] + self.ui_config["top_move_x_alpha"] * (policy_grid[y][x] == best_move_policy),
+2 -2
View File
@@ -64,8 +64,8 @@ class Controls(BoxLayout):
self.win_rate.text = current_node.format_win_rate()
if move and next_player_is_human_or_both_robots: # don't immediately hide this when an ai moves comes in
points_lost = current_node.points_lost
self.score_change.label = f"Points lost ({move.player})" if points_lost and points_lost > 0 else f"Points gained ({move.player})"
self.score_change.text = f"{abs(points_lost):.1f}" if points_lost else "..."
self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained"
self.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "..."
elif not current_player_is_ai_playing_human:
self.score_change.label = f"Points lost"
self.score_change.text = ""
+27 -8
View File
@@ -49,7 +49,26 @@ class StyledToggleButton(StyledButton, ToggleButtonBehavior):
class StyledSpinner(Spinner):
pass
sync_height_frac = NumericProperty(1.0)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fbind("size", self._update_dropdown_size_frac)
def _update_dropdown_size_frac(self, *largs):
if not self.sync_height_frac:
return
dp = self._dropdown
if not dp:
return
container = dp.container
if not container:
return
h = self.height
fsz = self.font_size
for item in container.children[:]:
item.height = h * self.sync_height_frac
item.font_size = fsz
class ToggleButtonContainer(GridLayout):
@@ -187,7 +206,7 @@ class ScoreGraph(Label):
line_points = ListProperty([])
dot_pos = ListProperty([0, 0])
highlighted_index = NumericProperty(None)
min_scale = NumericProperty(1)
y_scale = NumericProperty(4)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -207,22 +226,22 @@ class ScoreGraph(Label):
values = [n.score if n and n.score else math.nan for n in nodes]
nn_values = [n.score for n in nodes if n and n.score]
val_range = min(nn_values or [0]), max(nn_values or [0])
scale = math.ceil(max(self.min_scale, max(-val_range[0], val_range[1]) * 1.05))
self.y_scale = math.ceil(max(4, max(-val_range[0], val_range[1])) / 2) * 2
xscale = self.width * 0.9 / max(len(values) - 1, 15)
available_height = self.height * (1 - 2 * self.marginy)
line_points = [[self.pos[0] + self.marginx * self.width + i * xscale, self.pos[1] + available_height / 2 * (1 + val / scale)] for i, val in enumerate(values)]
line_points = [[self.pos[0] + self.marginx * self.width + i * xscale, self.pos[1] + available_height / 2 * (1 + val / self.y_scale)] for i, val in enumerate(values)]
self.line_points = sum(line_points, [])
self.range_label_top.text = f"B+{scale:.0f}"
self.range_label_bottom.text = f"W+{scale:.0f}"
if self.highlighted_index is not None:
self.highlighted_index = min(self.highlighted_index, len(values) - 1)
dot_point = line_points[self.highlighted_index]
if math.isnan(dot_point[1]):
dot_point[1] = self.pos[1] + available_height / 2 * (1 + (nn_values or [0])[-1] / scale)
dot_point[1] = self.pos[1] + available_height / 2 * (1 + (nn_values or [0])[-1] / self.y_scale)
self.dot_pos = [c - self.highlight_size / 2 for c in dot_point]
print("Graph updated to ", len(line_points), "points, hl=", self.highlighted_index, self.dot_pos)
# print("Graph updated to ", len(line_points), "points, hl=", self.highlighted_index, self.dot_pos)
def update_value(self, node):
self.highlighted_index = index = node.depth
+1 -1
View File
@@ -250,7 +250,7 @@ class ConfigTeacherPopup(QuickConfigGui):
thrbox.add_widget(LabelledFloatInput(text=str(thr), input_property=f"eval_thresholds::{i}"))
thrbox.add_widget(LabelledFloatInput(text=str(undos), input_property=f"num_undo_prompts::{i}"))
thrbox.add_widget(BackgroundLabel(background=color[:3]))
thrbox.add_widget(LabelledCheckBox(text=str(color[3]==1), input_property=f"alpha::{i}"))
thrbox.add_widget(LabelledCheckBox(text=str(color[3] == 1), input_property=f"alpha::{i}"))
self.add_widget(thrbox)
+54 -16
View File
@@ -1,7 +1,7 @@
#:kivy 1.11.0
#:import ew kivy.uix.effectwidget
#:set PLAYER_MODES ['Human', 'Teach','AI:']
#:set PLAYER_MODES ['Human', 'Teach','AI']
#:set PLAYER_MODE_VALUES ['human','human+undo','ai']
#:set BUTTON_COLOR [0.23, 0.30, 0.35, 1]
#:set ICON_BUTTON_COLOR [0.71, 0.78, 0.81, 1]
@@ -39,15 +39,20 @@
radius: root.radius
<StyledSpinnerOption@SpinnerOption>:
font_size: self.size[1] * 0.33
background_color: BUTTON_COLOR
background_normal: ''
color: WHITE
canvas.after:
Color:
rgba: WHITE
Line
points: self.pos[0],self.pos[1],self.pos[0]+self.width,self.pos[1]
width: 1
<StyledSpinner>:
text: self.values[0] if self.values else ''
font_size: self.size[1] * 0.33
sync_height: True
sync_height_frac: 1.0
background_color: [*[c*255/88 for c in BUTTON_COLOR[:3]], 1] # compensate for texture
option_cls: 'StyledSpinnerOption'
@@ -211,21 +216,31 @@
size: self.texture_size
<ScoreGraph>:
range_label_bottom: range_label_bottom
range_label_top: range_label_top
highlight_size: 5
marginx: 0.05
marginy: 0.01
whalf: root.pos[1]+self.height*0.25*(1 + 2 * root.marginy)
bhalf: root.pos[1]+self.height*0.75*(1 - 2/3 * root.marginy)
mid: root.pos[1]+self.height*0.5
right_edge: root.pos[0] + root.width * (1 - root.marginx)
canvas.before:
Color:
rgba: GREY
Line:
points: [root.pos[0]+self.width*root.marginx,root.pos[1]+self.height*0.5*(1 - 2 * root.marginy),root.pos[0]+self.width*0.95,root.pos[1]+self.height*0.5]
points: [root.pos[0]+self.width*root.marginx,root.whalf,root.pos[0]+self.width*0.95,root.whalf]
width: 1.0
dashes: [2,2]
dashes: [1,2]
Line:
points: [root.pos[0]+self.width*root.marginx,root.mid,root.pos[0]+self.width*0.95,root.mid]
width: 1.0
dashes: [4,4]
Line:
points: [root.pos[0]+self.width*root.marginx,root.bhalf,root.pos[0]+self.width*0.95,root.bhalf]
width: 1.0
dashes: [1,2]
Line:
rectangle: [root.pos[0] + root.marginx * self.width, root.pos[1] + root.marginy*self.height, root.width*0.9, root.height*(1-2*root.marginy)]
canvas:
canvas.after:
Color:
rgba: BLACK
Line:
@@ -238,14 +253,26 @@
pos: root.dot_pos
size: self.highlight_size,self.highlight_size
GraphMarkerLabel:
font_size: 0.12 * root.height
font_size: 0.1 * root.height
id: range_label_top
pos: root.pos[0] + root.marginx * root.width + 3, root.pos[1] + root.height * (1 - 2 * root.marginy) - self.font_size - 1
pos: root.right_edge - self.width-1, root.pos[1]+root.height*(1-root.marginy) - self.font_size
text: 'B+' + str(int(root.y_scale))
# GraphMarkerLabel:
# font_size: 0.1 * root.height
# pos: root.right_edge - self.width-1, root.bhalf - self.font_size + 1
# text: 'B+' + str(int(root.y_scale/2))
GraphMarkerLabel:
id: range_label_bottom
font_size: 0.12 * root.height
pos: root.pos[0] + root.marginx * root.width + 3, root.pos[1] + 1
font_size: 0.1 * root.height
pos: root.right_edge - self.width-1, root.mid - self.height/2 + 2
text: 'Jigo'
# GraphMarkerLabel:
# font_size: 0.1 * root.height
# pos: root.right_edge - self.width-1, root.whalf - 1
# text: 'W+' + str(int(root.y_scale/2))
GraphMarkerLabel:
font_size: 0.1 * root.height
pos: root.right_edge - self.width-1, root.pos[1]
text: 'W+' + str(int(root.y_scale))
<BadukPanWidget>:
size: self.parent.height, self.parent.height
@@ -258,6 +285,7 @@
white_prisoners: white_prisoners
mid_circles_container: mid_circles_container
pass_btn: pass_btn
engine_status_col: 0.8,0,0,1
Label:
size_hint: 0.05, 1
BoxLayout:
@@ -322,7 +350,15 @@
font_size: 0.6*self.size[1]
on_press: root.katrain("ai-move",None)
Label:
size_hint: 0.05, 1
size_hint: 0.025, 1
canvas:
Color:
rgba: root.engine_status_col
Ellipse:
pos: self.pos[0] + self.width/2 - self.height/5, self.pos[1] + self.height/2 - self.height/5
size: self.height/2.5,self.height/2.5
Label:
size_hint: 0.025, 1
<Controls>:
orientation: 'vertical'
@@ -388,7 +424,7 @@
CheckBoxHintLabel:
text: 'next\nmoves'
CheckBoxHintLabel:
text: 'all\ndots'
text: 'more\ndots'
CheckBoxHintLabel:
text: 'top\nmoves'
CheckBoxHintLabel:
@@ -473,6 +509,7 @@
StyledSpinner:
id: B_AI_mode
values: ['Default']
sync_height_frac: 0.66
size_hint: 0.3, 1
on_text: if B_player_mode.children: B_player_mode.children[0].trigger_action(duration=0)
Label:
@@ -490,6 +527,7 @@
StyledSpinner:
id: W_AI_mode
size_hint: 0.3, 1
sync_height_frac: 0.66
values: ['Default']
on_text: if W_player_mode.children: W_player_mode.children[0].trigger_action(duration=0)
BoxLayout:
+14 -8
View File
@@ -1,3 +1,5 @@
import inspect
from kivy.config import Config # isort:skip
Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click
@@ -38,7 +40,7 @@ class KaTrainGui(BoxLayout):
self._load_config()
self.debug_level = self.config("debug/level", OUTPUT_INFO)
self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = self.config("ai").keys()
self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = list(self.config("ai").keys()) + ["<Pause>"]
self.message_queue = Queue()
self._keyboard = Window.request_keyboard(None, self, "")
@@ -94,12 +96,12 @@ class KaTrainGui(BoxLayout):
if (
cn.analysis_ready
and "ai" in self.controls.player_mode(cn.next_player).lower()
and not "pause" in self.controls.ai_mode(cn.next_player).lower()
and "pause" not in self.controls.ai_mode(cn.next_player).lower()
and not cn.children
and not self.game.ended
and not (auto_undo and cn.auto_undo is None)
):
self("ai-move", cn) # cn mismatch stops this if undo fired
self._do_ai_move(cn) # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
# Handle prisoners and next player display
prisoners = self.game.prisoner_count
@@ -131,6 +133,9 @@ class KaTrainGui(BoxLayout):
traceback.print_exc()
def __call__(self, message, *args):
# curframe = inspect.currentframe() # TODO remove
# calframe = inspect.getouterframes(curframe, 2)
# print('caller name:', calframe[1])
if self.game:
self.message_queue.put([self.game.game_id, message, *args])
@@ -144,7 +149,8 @@ class KaTrainGui(BoxLayout):
def _do_ai_move(self, node=None):
if node is None or self.game.current_node == node:
mode = self.controls.ai_mode(self.game.current_node.next_player)
settings = self.config("ai/mode")
settings = self.config(f"ai/{mode}")
print(mode, settings)
if settings:
ai_move(self.game, mode, settings)
@@ -198,11 +204,11 @@ class KaTrainGui(BoxLayout):
def _do_output_sgf(self):
for pl in Move.PLAYERS:
if not self.game.root.get_first(f"P{pl}"):
if not self.game.root.get_property(f"P{pl}"):
_, model_file = os.path.split(self.engine.config["model"])
self.game.root.properties[f"P{pl}"] = [
f"AI {self.controls.ai_mode(pl)} (KataGo { os.path.splitext(model_file)[0]})" if "ai" in self.controls.player_mode(pl) else "Player"
] # TODO: more dynamic?
self.game.root.set_property(
f"P{pl}", f"AI {self.controls.ai_mode(pl)} (KataGo { os.path.splitext(model_file)[0]})" if "ai" in self.controls.player_mode(pl) else "Player"
)
msg = self.game.write_sgf(self.config("files/sgf_save"))
self.log(msg, OUTPUT_INFO)
self.controls.set_status(msg)
+41 -41
View File
@@ -103,20 +103,20 @@ def retrieve_ais(selected_ais):
test_ais = [
AI("P+Local", {"local_stddev": 1, "pick_frac": 0.1}),
AI("P+Local", {"local_stddev": 1, "pick_frac": 0.05}),
AI("P+Local", {"local_stddev": 5}),
AI("P+Pick", {"pick_frac": 0.4, "pick_n": 20}),
AI("P+Noise", {"noise_strength": 0.8}),
AI("P+Noise", {"noise_strength": 0.9}),
AI("P+Tenuki", {"local_stddev": 1}),
AI("P+Tenuki", {"local_stddev": 5}),
AI("P+Tenuki", {"local_stddev": 10}),
AI("P+Pick", {"pick_frac": 0.2, "pick_n": 10}),
AI("P+Pick", {"pick_frac": 0.3, "pick_n": 10}),
AI("P+Local", {"local_stddev": 10}),
AI("P+Local", {"local_stddev": 5}),
AI("P+Pick", {"pick_frac": 0.0, "pick_n": 1}),
AI("P:Local", {"local_stddev": 1, "pick_frac": 0.1}),
AI("P:Local", {"local_stddev": 1, "pick_frac": 0.05}),
AI("P:Local", {"local_stddev": 5}),
AI("P:Pick", {"pick_frac": 0.4, "pick_n": 20}),
AI("P:Noise", {"noise_strength": 0.8}),
AI("P:Noise", {"noise_strength": 0.9}),
AI("P:Tenuki", {"local_stddev": 1}),
AI("P:Tenuki", {"local_stddev": 5}),
AI("P:Tenuki", {"local_stddev": 10}),
AI("P:Pick", {"pick_frac": 0.2, "pick_n": 10}),
AI("P:Pick", {"pick_frac": 0.3, "pick_n": 10}),
AI("P:Local", {"local_stddev": 10}),
AI("P:Local", {"local_stddev": 5}),
AI("P:Pick", {"pick_frac": 0.0, "pick_n": 1}),
AI("Jigo", {}, {"max_visits": 50}),
AI("Policy", {}),
# AI("Policy", {},{'model':'models/g170-b40c256x2-s2990766336-d830712531.bin.gz'}),
@@ -124,36 +124,36 @@ test_ais = [
test_ais = [
AI("Policy", {}),
AI("P+Noise", {"noise_strength": 0.6}),
AI("P+Noise", {"noise_strength": 0.7}),
AI("P+Noise", {"noise_strength": 0.8}),
AI("P+Noise", {"noise_strength": 0.9}),
AI("P+Pick", {}),
AI("P+Pick", {"pick_frac": 0.3, "pick_n": 20}),
AI("P+Pick", {"pick_frac": 0.5, "pick_n": 0}),
AI("P+Influence", {"pick_frac": 0.2, "pick_n": 20}),
AI("P+Territory", {"pick_frac": 0.2, "pick_n": 20}),
AI("P+Influence", {"pick_frac": 0.33, "line_weight": 20}),
AI("P+Territory", {"pick_frac": 0.33, "line_weight": 20}),
AI("P+Pick", {"pick_frac": 0.0, "pick_n": 1}),
AI("P+Tenuki", {"local_stddev": 20}),
AI("P+Tenuki", {"local_stddev": 10}),
AI("P+Tenuki", {"local_stddev": 5}),
AI("P+Local", {"local_stddev": 10}),
AI("P+Local", {"local_stddev": 5}),
AI("P+Local", {"local_stddev": 1}),
AI("P+Local", {"local_stddev": 1, "pick_frac": 0.0, "pick_n": 20}),
AI("P+Weighted", {"pick_override": 1.0}),
AI("P:Noise", {"noise_strength": 0.6}),
AI("P:Noise", {"noise_strength": 0.7}),
AI("P:Noise", {"noise_strength": 0.8}),
AI("P:Noise", {"noise_strength": 0.9}),
AI("P:Pick", {}),
AI("P:Pick", {"pick_frac": 0.3, "pick_n": 20}),
AI("P:Pick", {"pick_frac": 0.5, "pick_n": 0}),
AI("P:Influence", {"pick_frac": 0.2, "pick_n": 20}),
AI("P:Territory", {"pick_frac": 0.2, "pick_n": 20}),
AI("P:Influence", {"pick_frac": 0.33, "line_weight": 20}),
AI("P:Territory", {"pick_frac": 0.33, "line_weight": 20}),
AI("P:Pick", {"pick_frac": 0.0, "pick_n": 1}),
AI("P:Tenuki", {"local_stddev": 20}),
AI("P:Tenuki", {"local_stddev": 10}),
AI("P:Tenuki", {"local_stddev": 5}),
AI("P:Local", {"local_stddev": 10}),
AI("P:Local", {"local_stddev": 5}),
AI("P:Local", {"local_stddev": 1}),
AI("P:Local", {"local_stddev": 1, "pick_frac": 0.0, "pick_n": 20}),
AI("P:Weighted", {"pick_override": 1.0}),
]
# test_ais = [
# AI("Policy", {}),
# AI("P+Noise", {"noise_strength": 0.4}),
# AI("P+Noise", {"noise_strength": 0.5}),
# AI("P+Noise", {"noise_strength": 0.6}),
# AI("P+Noise", {"noise_strength": 0.7}),
# AI("P+Noise", {"noise_strength": 0.8}),
# AI("P:Noise", {"noise_strength": 0.4}),
# AI("P:Noise", {"noise_strength": 0.5}),
# AI("P:Noise", {"noise_strength": 0.6}),
# AI("P:Noise", {"noise_strength": 0.7}),
# AI("P:Noise", {"noise_strength": 0.8}),
# ]
# ai_database = [ai for ai in ai_database if "Territory" not in ai.name and "Influence" not in ai.name]
@@ -178,8 +178,8 @@ def play_games(black: AI, white: AI, n: int = N_GAMES):
try:
for i in range(n):
game = Game(Logger(), engines, {})
game.root.add_property("PW", [white.name])
game.root.add_property("PB", [black.name])
game.root.add_list_property("PW", [white.name])
game.root.add_list_property("PB", [black.name])
start_time = time.time()
while not game.ended:
p = game.current_node.next_player
+21 -16
View File
@@ -64,12 +64,12 @@ class SGFNode:
self.properties = defaultdict(list)
if properties:
for k, v in properties.items():
self.add_property(k, v)
self.set_property(k, v)
self.parent = parent
if self.parent:
self.parent.children.append(self)
if parent and move:
self.add_property(move.player, move.sgf(self.board_size))
self.set_property(move.player, move.sgf(self.board_size))
@property
def sgf_properties(self) -> Dict:
@@ -89,17 +89,22 @@ class SGFNode:
sgf_str += "(;" + ")(;".join(children) + ")"
return f"(;{sgf_str})" if self.is_root else sgf_str
def add_property(self, property: str, values: Any):
"""Add some values to the property. If not a list, it will be made into a single-value list."""
if not isinstance(values, list):
values = [values]
def add_list_property(self, property: str, values: List):
"""Add some values to the property list."""
self.properties[property] += values
def get(self, property, default=None) -> Any:
def get_list_property(self, property, default=None) -> Any:
"""Get the list of values for a property."""
return self.properties.get(property, default)
def get_first(self, property, default=None) -> Any:
def set_property(self, property: str, value: Any):
"""Add some values to the property. If not a list, it will be made into a single-value list."""
if isinstance(value, list):
self.properties[property] = value
else:
self.properties[property] = [value]
def get_property(self, property, default=None) -> Any:
"""Get the first value of the property, typically when exactly one is expected."""
return self.properties.get(property, [default])[0]
@@ -131,7 +136,7 @@ class SGFNode:
# some root properties are available on any node
@property
def board_size(self) -> Tuple[int, int]:
size = str(self.root.get_first("SZ", "19"))
size = str(self.root.get_property("SZ", "19"))
if ":" in size:
x, y = map(int, size.split(":"))
else:
@@ -141,21 +146,21 @@ class SGFNode:
@property
def komi(self) -> float:
return float(self.root.get_first("KM", 6.5))
return float(self.root.get_property("KM", 6.5))
@property
def ruleset(self) -> str:
return self.root.get_first("RU")
return self.root.get_property("RU")
@property
def moves(self) -> List[Move]:
"""Returns all moves in the node."""
return [Move.from_sgf(move, player=pl, board_size=self.board_size) for pl in Move.PLAYERS for move in self.get(pl, [])]
return [Move.from_sgf(move, player=pl, board_size=self.board_size) for pl in Move.PLAYERS for move in self.get_list_property(pl, [])]
@property
def placements(self) -> List[Move]:
"""Returns all placements (AB/AW) in the node."""
return [Move.from_sgf(sgf_coords, player=pl, board_size=self.board_size) for pl in Move.PLAYERS for sgf_coords in self.get("A" + pl, [])]
return [Move.from_sgf(sgf_coords, player=pl, board_size=self.board_size) for pl in Move.PLAYERS for sgf_coords in self.get_list_property("A" + pl, [])]
@property
def move_with_placements(self) -> List[Move]:
@@ -198,13 +203,13 @@ class SGFNode:
@property
def next_player(self):
if self.get("B") or self.get("AB"):
if self.get_list_property("B") or self.get_list_property("AB"):
return "W"
return "B"
@property
def player(self):
if self.get("B") or self.get("AB"):
if self.get_list_property("B") or self.get_list_property("AB"):
return "B"
return "W"
@@ -254,7 +259,7 @@ class SGF:
else:
property, value = match[1], match[2].strip()[1:-1]
values = re.split(r"\]\s*\[", value)
current_move.add_property(property, values)
current_move.add_list_property(property, values)
if self.ix < len(self.contents):
raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}")
raise ParseError("Parse Error: expected ')' at end of input.")