Skip to content

Commit e0d8008

Browse files
author
Sander Land
committed
Merge branch 'master' of www.github.com:sanderland/katrain
2 parents 3759ead + de16348 commit e0d8008

File tree

7 files changed

+66
-45
lines changed

7 files changed

+66
-45
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ sgf_ogs
1313
log*
1414
tmp.pickle
1515
my
16+
logs
17+
callgrind.*
1618

1719
# debug
1820
outdated_log.txt

bots/ai2gtp.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import sys
44
import time
5+
import random
56

67
from core.ai import ai_move
78
from core.common import OUTPUT_ERROR, OUTPUT_INFO
@@ -85,7 +86,6 @@ def malkovich_analysis(cn):
8586

8687

8788
while True:
88-
p = game.current_node.next_player
8989
line = input()
9090
logger.log(f"GOT INPUT {line}", OUTPUT_ERROR)
9191
if "boardsize" in line:
@@ -103,8 +103,14 @@ def malkovich_analysis(cn):
103103
logger.log(f"Setting komi {game.root.properties}", OUTPUT_ERROR)
104104
elif "place_free_handicap" in line:
105105
_, n = line.split(" ")
106-
game.place_handicap_stones(int(n))
107-
gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in game.root.get_list_property("AB")]
106+
n = int(n)
107+
game.place_handicap_stones(n)
108+
handicaps = set(game.root.get_list_property("AB"))
109+
bx, by = game.board_size
110+
while len(handicaps) < min(n, bx * by): # really obscure cases
111+
handicaps.add(Move((random.randint(0, bx - 1), random.randint(0, by - 1)), player="B").sgf(board_size=game.board_size))
112+
game.root.set_property("AB", list(handicaps))
113+
gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in handicaps]
108114
logger.log(f"Chose handicap placements as {gtp}", OUTPUT_ERROR)
109115
print(f"= {' '.join(gtp)}\n")
110116
sys.stdout.flush()
@@ -115,6 +121,12 @@ def malkovich_analysis(cn):
115121
game.root.set_property("AB", [Move.from_gtp(move.upper()).sgf(game.board_size) for move in stones])
116122
logger.log(f"Set handicap placements to {game.root.get_list_property('AB')}", OUTPUT_ERROR)
117123
elif "genmove" in line:
124+
_, player = line.strip().split(" ")
125+
if player[0].upper() != game.next_player:
126+
logger.log(f"ERROR generating move: UNEXPECTED PLAYER {player} != {game.next_player}.", OUTPUT_ERROR)
127+
print(f"= ??\n")
128+
sys.stdout.flush()
129+
continue
118130
logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR)
119131
game.current_node.analyze(engine)
120132
malkovich_analysis(game.current_node)
@@ -130,7 +142,12 @@ def malkovich_analysis(cn):
130142
move = game.play(Move(None, player=game.next_player)).single_move
131143
else:
132144
move, node = ai_move(game, ai_strategy, ai_settings)
133-
logger.log(f"Generated move {move}", OUTPUT_ERROR)
145+
if node is None:
146+
while node is None:
147+
logger.log(f"ERROR generating move, backing up with weighted.", OUTPUT_ERROR)
148+
move, node = ai_move(game, "p:weighted", {"pick_override": 1.0, "lower_bound": 0.001, "weaken_fac": 1})
149+
else:
150+
logger.log(f"Generated move {move}", OUTPUT_ERROR)
134151
print(f"= {move.gtp()}\n")
135152
sys.stdout.flush()
136153
malkovich_analysis(game.current_node)

core/ai.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
3434
raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died")
3535
ai_mode = ai_mode.lower()
3636
ai_thoughts = ""
37-
candidate_ai_moves = cn.candidate_moves
38-
if ("policy" in ai_mode or "p:" in ai_mode) and cn.policy:
37+
if ("policy" in ai_mode or "p:" in ai_mode) and cn.policy: # pure policy based move
3938
policy_moves = cn.policy_ranking
4039
pass_policy = cn.policy[-1]
4140
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
@@ -131,32 +130,35 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
131130
ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}."
132131
else:
133132
raise ValueError(f"Unknown AI mode {ai_mode}")
134-
elif "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best
135-
sign = cn.player_sign(cn.next_player)
136-
sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead
137-
move
138-
for i, move in enumerate(candidate_ai_moves)
139-
if i == 0
140-
or move["visits"] >= ai_settings["min_visits"]
141-
and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"])
142-
]
143-
aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player)
144-
ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}."
145-
elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass":
146-
sign = cn.player_sign(cn.next_player)
147-
jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"]))
148-
aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
149-
ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win"
150-
else:
151-
if "default" not in ai_mode and "katago" not in ai_mode:
152-
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
153-
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
154-
aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
155-
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
133+
else: # Engine based move
134+
candidate_ai_moves = cn.candidate_moves
135+
if "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best
136+
sign = cn.player_sign(cn.next_player)
137+
sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead
138+
move
139+
for i, move in enumerate(candidate_ai_moves)
140+
if i == 0
141+
or move["visits"] >= ai_settings["min_visits"]
142+
and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"])
143+
]
144+
aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player)
145+
ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}."
146+
elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass":
147+
sign = cn.player_sign(cn.next_player)
148+
jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"]))
149+
aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
150+
ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win"
151+
else:
152+
if "default" not in ai_mode and "katago" not in ai_mode:
153+
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
154+
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
155+
aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
156+
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
156157
game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG)
157158
try:
158159
played_node = game.play(aimove)
159160
played_node.ai_thoughts = ai_thoughts
160161
return aimove, played_node
161162
except IllegalMoveException as e:
162163
game.katrain.log(f"AI Strategy {ai_mode} generated illegal move {aimove.gtp()}: {e}", OUTPUT_ERROR)
164+
return None, None

core/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def send_query(self, query, callback, error_callback, next_move=None):
113113
query["id"] = f"QUERY:{str(self.query_counter)}"
114114
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
115115
if self.katago_process:
116-
self.katrain.log(f"Sending query {query['id']}: {str(query)}", OUTPUT_DEBUG)
116+
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
117117
try:
118118
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
119119
self.katago_process.stdin.flush()

core/game.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,8 @@ def switch_branch(self, direction):
154154

155155
def place_handicap_stones(self, n_handicaps):
156156
board_size_x, board_size_y = self.board_size
157-
near_x = 3 if board_size_x >= 13 else 2
158-
near_y = 3 if board_size_y >= 13 else 2
159-
if board_size_x < 3 or board_size_y < 3:
160-
return
157+
near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1)
158+
near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1)
161159
far_x = board_size_x - 1 - near_x
162160
far_y = board_size_y - 1 - near_y
163161
middle_x = board_size_x // 2 # what for even sizes?
@@ -176,7 +174,7 @@ def place_handicap_stones(self, n_handicaps):
176174
if n_handicaps % 2 == 1:
177175
stones.append((middle_x, middle_y))
178176
stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)]
179-
self.root.set_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]])
177+
self.root.set_property("AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]}))
180178

181179
@property
182180
def board_size(self):

core/sgf_parser.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class ParseError(Exception):
99

1010

1111
class Move:
12-
GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + ["A" + c for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # kata board size 29 support
12+
GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [xa + c for xa in "AB" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # kata board size 29 support
1313
PLAYERS = "BW"
1414
SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1515

@@ -79,10 +79,11 @@ def order_children(children):
7979

8080
def sgf(self, **xargs) -> str:
8181
"""Generates an SGF, calling sgf_properties on each node with the given xargs, so it can filter relevant properties if needed."""
82-
import sys
82+
if self.is_root:
83+
import sys
8384

84-
bszx, bszy = self.board_size
85-
sys.setrecursionlimit(max(sys.getrecursionlimit(), 3 * bszx * bszy)) # thanks to lightvector for causing stack overflows ;)
85+
bszx, bszy = self.board_size
86+
sys.setrecursionlimit(max(sys.getrecursionlimit(), 4 * bszx * bszy))
8687
sgf_str = "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in self.sgf_properties(**xargs).items() if values])
8788
if self.children:
8889
children = [c.sgf(**xargs) for c in self.order_children(self.children)]
@@ -211,15 +212,17 @@ def play(self, move) -> "SGFNode":
211212

212213
@property
213214
def next_player(self):
214-
if "B" in self.properties or "AB" in self.properties:
215+
if "B" in self.properties or "AB" in self.properties: # root or black moved
215216
return "W"
216-
return "B"
217+
else:
218+
return "B"
217219

218220
@property
219221
def player(self):
220-
if "W" in self.properties:
221-
return "W"
222-
return "B"
222+
if "B" in self.properties or "AB" in self.properties:
223+
return "B"
224+
else:
225+
return "W" # nb root is considered white played if no handicap stones are placed
223226

224227

225228
class SGF:

gui/popups.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,12 @@ def restart_engine(_dt):
173173
old_proc = old_engine.katago_process
174174
if old_proc:
175175
old_engine.shutdown(finish=True)
176-
177176
new_engine = KataGoEngine(self.katrain, self.config["engine"])
178177
self.katrain.engine = new_engine
179178
self.katrain.game.engines = {"B": new_engine, "W": new_engine}
180179
if not old_proc:
181180
self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures
182-
181+
self.katrain.update_state()
183182
Clock.schedule_once(restart_engine, 0)
184183

185184
self.katrain.debug_level = self.config["debug"]["level"]

0 commit comments

Comments
 (0)