Skip to content

Commit

Permalink
Merge branch 'master' of www.github.com:sanderland/katrain
Browse files Browse the repository at this point in the history
  • Loading branch information
Sander Land committed May 3, 2020
2 parents 3759ead + de16348 commit e0d8008
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 45 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ sgf_ogs
log*
tmp.pickle
my
logs
callgrind.*

# debug
outdated_log.txt
Expand Down
25 changes: 21 additions & 4 deletions bots/ai2gtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import sys
import time
import random

from core.ai import ai_move
from core.common import OUTPUT_ERROR, OUTPUT_INFO
Expand Down Expand Up @@ -85,7 +86,6 @@ def malkovich_analysis(cn):


while True:
p = game.current_node.next_player
line = input()
logger.log(f"GOT INPUT {line}", OUTPUT_ERROR)
if "boardsize" in line:
Expand All @@ -103,8 +103,14 @@ def malkovich_analysis(cn):
logger.log(f"Setting komi {game.root.properties}", OUTPUT_ERROR)
elif "place_free_handicap" in line:
_, n = line.split(" ")
game.place_handicap_stones(int(n))
gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in game.root.get_list_property("AB")]
n = int(n)
game.place_handicap_stones(n)
handicaps = set(game.root.get_list_property("AB"))
bx, by = game.board_size
while len(handicaps) < min(n, bx * by): # really obscure cases
handicaps.add(Move((random.randint(0, bx - 1), random.randint(0, by - 1)), player="B").sgf(board_size=game.board_size))
game.root.set_property("AB", list(handicaps))
gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in handicaps]
logger.log(f"Chose handicap placements as {gtp}", OUTPUT_ERROR)
print(f"= {' '.join(gtp)}\n")
sys.stdout.flush()
Expand All @@ -115,6 +121,12 @@ def malkovich_analysis(cn):
game.root.set_property("AB", [Move.from_gtp(move.upper()).sgf(game.board_size) for move in stones])
logger.log(f"Set handicap placements to {game.root.get_list_property('AB')}", OUTPUT_ERROR)
elif "genmove" in line:
_, player = line.strip().split(" ")
if player[0].upper() != game.next_player:
logger.log(f"ERROR generating move: UNEXPECTED PLAYER {player} != {game.next_player}.", OUTPUT_ERROR)
print(f"= ??\n")
sys.stdout.flush()
continue
logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR)
game.current_node.analyze(engine)
malkovich_analysis(game.current_node)
Expand All @@ -130,7 +142,12 @@ def malkovich_analysis(cn):
move = game.play(Move(None, player=game.next_player)).single_move
else:
move, node = ai_move(game, ai_strategy, ai_settings)
logger.log(f"Generated move {move}", OUTPUT_ERROR)
if node is None:
while node is None:
logger.log(f"ERROR generating move, backing up with weighted.", OUTPUT_ERROR)
move, node = ai_move(game, "p:weighted", {"pick_override": 1.0, "lower_bound": 0.001, "weaken_fac": 1})
else:
logger.log(f"Generated move {move}", OUTPUT_ERROR)
print(f"= {move.gtp()}\n")
sys.stdout.flush()
malkovich_analysis(game.current_node)
Expand Down
50 changes: 26 additions & 24 deletions core/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died")
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: # pure policy based move
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
Expand Down Expand Up @@ -131,32 +130,35 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}."
else:
raise ValueError(f"Unknown AI mode {ai_mode}")
elif "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best
sign = cn.player_sign(cn.next_player)
sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead
move
for i, move in enumerate(candidate_ai_moves)
if i == 0
or move["visits"] >= ai_settings["min_visits"]
and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"])
]
aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player)
ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}."
elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass":
sign = cn.player_sign(cn.next_player)
jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"]))
aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win"
else:
if "default" not in ai_mode and "katago" not in ai_mode:
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
else: # Engine based move
candidate_ai_moves = cn.candidate_moves
if "balance" in ai_mode and candidate_ai_moves[0]["move"] != "pass": # don't play suicidal to balance score - pass when it's best
sign = cn.player_sign(cn.next_player)
sel_moves = [ # top move, or anything not too bad, or anything that makes you still ahead
move
for i, move in enumerate(candidate_ai_moves)
if i == 0
or move["visits"] >= ai_settings["min_visits"]
and (move["pointsLost"] < ai_settings["random_loss"] or move["pointsLost"] < ai_settings["max_loss"] and sign * move["scoreLead"] > ai_settings["target_score"])
]
aimove = Move.from_gtp(random.choice(sel_moves)["move"], player=cn.next_player)
ai_thoughts += f"Balance strategy selected moves {sel_moves} based on target score and max points lost, and randomly chose {aimove.gtp()}."
elif "jigo" in ai_mode and candidate_ai_moves[0]["move"] != "pass":
sign = cn.player_sign(cn.next_player)
jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"]))
aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
ai_thoughts += f"Jigo strategy found candidate moves {candidate_ai_moves} moves and chose {aimove.gtp()} as closest to 0.5 point win"
else:
if "default" not in ai_mode and "katago" not in ai_mode:
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
aimove = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG)
try:
played_node = game.play(aimove)
played_node.ai_thoughts = ai_thoughts
return aimove, played_node
except IllegalMoveException as e:
game.katrain.log(f"AI Strategy {ai_mode} generated illegal move {aimove.gtp()}: {e}", OUTPUT_ERROR)
return None, None
2 changes: 1 addition & 1 deletion core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def send_query(self, query, callback, error_callback, next_move=None):
query["id"] = f"QUERY:{str(self.query_counter)}"
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move)
if self.katago_process:
self.katrain.log(f"Sending query {query['id']}: {str(query)}", OUTPUT_DEBUG)
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
try:
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
self.katago_process.stdin.flush()
Expand Down
8 changes: 3 additions & 5 deletions core/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,8 @@ def switch_branch(self, direction):

def place_handicap_stones(self, n_handicaps):
board_size_x, board_size_y = self.board_size
near_x = 3 if board_size_x >= 13 else 2
near_y = 3 if board_size_y >= 13 else 2
if board_size_x < 3 or board_size_y < 3:
return
near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1)
near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1)
far_x = board_size_x - 1 - near_x
far_y = board_size_y - 1 - near_y
middle_x = board_size_x // 2 # what for even sizes?
Expand All @@ -176,7 +174,7 @@ def place_handicap_stones(self, n_handicaps):
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.set_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]])
self.root.set_property("AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]}))

@property
def board_size(self):
Expand Down
21 changes: 12 additions & 9 deletions core/sgf_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ParseError(Exception):


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

Expand Down Expand Up @@ -79,10 +79,11 @@ def order_children(children):

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

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

@property
def next_player(self):
if "B" in self.properties or "AB" in self.properties:
if "B" in self.properties or "AB" in self.properties: # root or black moved
return "W"
return "B"
else:
return "B"

@property
def player(self):
if "W" in self.properties:
return "W"
return "B"
if "B" in self.properties or "AB" in self.properties:
return "B"
else:
return "W" # nb root is considered white played if no handicap stones are placed


class SGF:
Expand Down
3 changes: 1 addition & 2 deletions gui/popups.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,12 @@ def restart_engine(_dt):
old_proc = old_engine.katago_process
if old_proc:
old_engine.shutdown(finish=True)

new_engine = KataGoEngine(self.katrain, self.config["engine"])
self.katrain.engine = new_engine
self.katrain.game.engines = {"B": new_engine, "W": new_engine}
if not old_proc:
self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures

self.katrain.update_state()
Clock.schedule_once(restart_engine, 0)

self.katrain.debug_level = self.config["debug"]["level"]
Expand Down

0 comments on commit e0d8008

Please sign in to comment.