0% found this document useful (0 votes)
7 views22 pages

Go Game AI: Deterministic Move Strategy

The document describes a Python script for a Go player that implements a deterministic fallback mechanism when Q-values are absent or Monte Carlo evaluations are inconclusive. It includes functions for board management, move selection, and Q-learning, allowing the player to choose moves based on heuristic evaluations or learned Q-values. The script also handles input/output operations and maintains game rules such as KO and no-suicide rules.

Uploaded by

peter
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views22 pages

Go Game AI: Deterministic Move Strategy

The document describes a Python script for a Go player that implements a deterministic fallback mechanism when Q-values are absent or Monte Carlo evaluations are inconclusive. It includes functions for board management, move selection, and Q-learning, allowing the player to choose moves based on heuristic evaluations or learned Q-values. The script also handles input/output operations and maintains game rules such as KO and no-suicide rules.

Uploaded by

peter
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

#!

/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

my_player3.py (patched - deterministic fallback when Q absent / MC inconclusive)

This patch addresses the "it runs random only" symptom by:

- When no Q-values are loaded, preferring a deterministic greedy fallback:

* pick capturing moves first (largest capture)

* otherwise pick the top heuristic move (eval delta)

- When Monte-Carlo evaluations are run but return very few sims or zero win-rates

(i.e., inconclusive), fall back to the deterministic greedy/heuristic choice

instead of letting the small noisy MC results determine the move.

- Avoids selecting PASS unless it's the only legal move.

- Keeps MC and alpha-beta fallback available for tactical improvements when MC is

informative.

Keep other behavior intact (Q use when loaded, KO/no-suicide rules, territory eval).

"""

import sys, time, copy, json, os, math, random

BOARD_SIZE = 5

TIME_LIMIT = 9.5

Q_FILE = "q_values.json"

KOMI = 2.5
# ------------------------- I/O Utilities -------------------------

def read_input(filename="[Link]"):

empty_board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]

if not [Link](filename):

return 1, copy_board(empty_board), copy_board(empty_board)

with open(filename, "r") as f:

lines = [[Link]() for ln in [Link]().splitlines()]

while len(lines) < 11:

[Link]("0" * BOARD_SIZE)

try:

color = int(lines[0])

if color not in (1, 2):

color = 1

except Exception:

color = 1

prev, curr = [], []

for i in range(1, 6):

row = lines[i][:BOARD_SIZE].ljust(BOARD_SIZE, "0")

[Link]([int(c) if c in "012" else 0 for c in row])

for i in range(6, 11):

row = lines[i][:BOARD_SIZE].ljust(BOARD_SIZE, "0")

[Link]([int(c) if c in "012" else 0 for c in row])

return color, prev, curr

def write_output(move_string, filename="[Link]"):

with open(filename, "w") as f:

[Link](move_string + "\n")
# ------------------------- Board Utilities -------------------------

def in_bounds(r, c):

return 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE

def neighbors(r, c):

for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):

nr, nc = r + dr, c + dc

if in_bounds(nr, nc):

yield nr, nc

def copy_board(b):

return [row[:] for row in b]

def boards_equal(a, b):

for i in range(BOARD_SIZE):

for j in range(BOARD_SIZE):

if a[i][j] != b[i][j]:

return False

return True

def find_group_and_liberties(board, r, c):

color = board[r][c]
if color == 0:

return set(), set()

stack = [(r, c)]

seen, libs = set(), set()

while stack:

x, y = [Link]()

if (x, y) in seen:

continue

[Link]((x, y))

for nx, ny in neighbors(x, y):

if board[nx][ny] == 0:

[Link]((nx, ny))

elif board[nx][ny] == color and (nx, ny) not in seen:

[Link]((nx, ny))

return seen, libs

def remove_group(board, group):

for (r, c) in group:

board[r][c] = 0

def apply_move(board, r, c, color):

b = copy_board(board)

if r is None:

return b, 0, True

if b[r][c] != 0:

return b, 0, False
opp = 1 if color == 2 else 2

b[r][c] = color

total_cap, captured = 0, False

for nx, ny in neighbors(r, c):

if b[nx][ny] == opp:

grp, libs = find_group_and_liberties(b, nx, ny)

if len(libs) == 0:

remove_group(b, grp)

total_cap += len(grp)

captured = True

grp_self, libs_self = find_group_and_liberties(b, r, c)

if len(libs_self) == 0 and not captured:

return board, 0, False

return b, total_cap, True

def legal_moves(prev_board, curr_board, color):

moves = []

for r in range(BOARD_SIZE):

for c in range(BOARD_SIZE):

if curr_board[r][c] != 0:

continue

nb, cap, legal = apply_move(curr_board, r, c, color)

if not legal:

continue

if prev_board is not None and boards_equal(nb, prev_board):

continue

[Link](((r, c), cap))


[Link]((("PASS", "PASS"), 0))

return moves

# ------------------------- Territory Estimation -------------------------

def estimate_territory(board):

visited = [[False] * BOARD_SIZE for _ in range(BOARD_SIZE)]

terr = {1: 0, 2: 0}

for r in range(BOARD_SIZE):

for c in range(BOARD_SIZE):

if board[r][c] != 0 or visited[r][c]:

continue

stack = [(r, c)]

visited[r][c] = True

region = [(r, c)]

border_colors = set()

while stack:

x, y = [Link]()

for nx, ny in neighbors(x, y):

if board[nx][ny] == 0 and not visited[nx][ny]:

visited[nx][ny] = True

[Link]((nx, ny))

[Link]((nx, ny))

elif board[nx][ny] in (1, 2):

border_colors.add(board[nx][ny])

if len(border_colors) == 1:

owner = list(border_colors)[0]

terr[owner] += len(region)
return terr

def evaluate_board(board, color):

my, opp = color, 1 if color == 2 else 2

my_stones = opp_stones = my_libs = opp_libs = 0

for r in range(BOARD_SIZE):

for c in range(BOARD_SIZE):

if board[r][c] == my:

my_stones += 1

_, libs = find_group_and_liberties(board, r, c)

my_libs += len(libs)

elif board[r][c] == opp:

opp_stones += 1

_, libs = find_group_and_liberties(board, r, c)

opp_libs += len(libs)

terr = estimate_territory(board)

terr_score = terr[my] - terr[opp]

score = (my_stones - opp_stones) + 0.7 * (my_libs - opp_libs) + 0.9 * terr_score

if color == 2:

score += KOMI

return score

# ------------------------- Alpha–Beta Search -------------------------

class TimeOutException(Exception):

pass
def minimax_ab(prev, board, color, maxp, depth, alpha, beta, start, limit):

if [Link]() - start > limit:

raise TimeOutException()

if depth == 0:

return evaluate_board(board, color), None

to_move = color if maxp else (1 if color == 2 else 2)

moves = legal_moves(prev, board, to_move)

moves = sorted(moves, key=lambda m: m[1], reverse=True)[:10]

best = None

if maxp:

val = -1e9

for m, _ in moves:

nb = copy_board(board) if m == ("PASS", "PASS") else apply_move(board, m[0], m[1],


to_move)[0]

v, _ = minimax_ab(board, nb, color, False, depth - 1, alpha, beta, start, limit)

if v > val:

val, best = v, m

alpha = max(alpha, val)

if beta <= alpha:

break

return val, best

else:

val = 1e9

for m, _ in moves:

nb = copy_board(board) if m == ("PASS", "PASS") else apply_move(board, m[0], m[1],


to_move)[0]

v, _ = minimax_ab(board, nb, color, True, depth - 1, alpha, beta, start, limit)

if v < val:
val, best = v, m

beta = min(beta, val)

if beta <= alpha:

break

return val, best

# ------------------------- Q-Learning -------------------------

class QLearner:

def __init__(self, alpha=0.3, gamma=0.9, epsilon=0.2):

[Link] = alpha

[Link] = gamma

[Link] = epsilon

self.q = {}

def get_Q(self, s, a):

return float([Link](s, {}).get(a, 0.0))

def best_action(self, s, acts):

if not acts:

return "PASS"

[Link](acts)

return max(acts, key=lambda a: self.get_Q(s, a))

def choose_action(self, s, acts, greedy=False):

if not acts:

return "PASS"

if greedy or [Link]() > [Link]:


return self.best_action(s, acts)

return [Link](acts)

def update(self, s, a, r, s2, acts2):

old = self.get_Q(s, a)

fb = max([self.get_Q(s2, a2) for a2 in acts2], default=0.0)

new = (1 - [Link]) * old + [Link] * (r + [Link] * fb)

[Link](s, {})[a] = new

def save(self, fn=Q_FILE):

try:

with open(fn, "w") as f:

[Link](self.q, f)

except Exception:

pass

def load(self, fn=Q_FILE):

if not [Link](fn):

return False

try:

with open(fn, "r") as f:

data = [Link](f)

fixed = {}

for s, amap in [Link]():

fixed[s] = {}

for a, v in [Link]():

try:

fixed[s][a] = float(v)
except Exception:

fixed[s][a] = 0.0

self.q = fixed

return True

except Exception:

self.q = {}

return False

# ------------------------- Helpers -------------------------

def normalize_board_for_q(board, my_color):

if my_color == 1:

return copy_board(board)

flip_map = {1: 2, 2: 1, 0: 0}

return [[flip_map[cell] for cell in row] for row in board]

def board_to_runtime_key(curr_board, player_color):

def to_str(b):

return "".join(str(c) for row in b for c in row)

def rotate90(b):

return [[b[BOARD_SIZE - 1 - c][r] for c in range(BOARD_SIZE)] for r in


range(BOARD_SIZE)]

def flip_h(b):

return [list(reversed(row)) for row in b]

b = copy_board(curr_board)

variants = []

for rot in range(4):


if rot > 0:

b = rotate90(b)

[Link](copy_board(b))

[Link](flip_h(b))

best = min(to_str(v) for v in variants)

return best + "|" + str(player_color)

def available_action_keys(prev, curr, color):

return ["PASS" if p == ("PASS", "PASS") else f"{p[0]},{p[1]}" for p, _ in legal_moves(prev,


curr, color)]

# ------------------------- Monte Carlo Simulation -------------------------

def simulate_random_game(prev, curr, color, max_moves=BOARD_SIZE * BOARD_SIZE + 6):

prev_b, curr_b = copy_board(prev), copy_board(curr)

to_move, passes = color, 0

moves_left = max_moves

while passes < 2 and moves_left > 0:

moves_left -= 1

acts = legal_moves(prev_b, curr_b, to_move)

if not acts:

passes += 1

new_b = copy_board(curr_b)

else:

(move, _) = [Link](acts)

if move == ("PASS", "PASS"):

passes += 1
new_b = copy_board(curr_b)

else:

r, c = move

new_b, _, legal = apply_move(curr_b, r, c, to_move)

passes = passes + 1 if not legal else 0

prev_b, curr_b = curr_b, new_b

to_move = 1 if to_move == 2 else 2

black = sum([Link](1) for row in curr_b)

white = sum([Link](2) for row in curr_b) + KOMI

return 1 if black > white else 2

def monte_carlo_evaluate_move(prev, curr, my_color, move, time_budget=0.6,


max_sims=150):

start = [Link]()

wins = 0

sims = 0

if move == ("PASS", "PASS"):

new_board = copy_board(curr)

else:

new_board, _, legal = apply_move(curr, move[0], move[1], my_color)

if not legal:

return 0.0, 0

prev_after = copy_board(curr)

next_to_move = 1 if my_color == 2 else 2

while sims < max_sims and ([Link]() - start) < time_budget:

winner = simulate_random_game(prev_after, new_board, next_to_move)

sims += 1
if winner == my_color:

wins += 1

return (wins / sims) if sims > 0 else 0.0, sims

# ------------------------- Move Selection -------------------------

def choose_move_with_q(prev_board, curr_board, my_color, qlearner):

norm_board = normalize_board_for_q(curr_board, my_color)

state = board_to_runtime_key(norm_board, 1)

acts = available_action_keys(prev_board, curr_board, my_color)

if not acts:

return "PASS"

[Link] = 0.0

# sort actions by Q descending and pick first legal

acts_sorted = sorted(acts, key=lambda a: qlearner.get_Q(state, a), reverse=True)

for a in acts_sorted:

if a == "PASS":

return "PASS"

try:

r, c = map(int, [Link](","))

_, _, legal = apply_move(curr_board, r, c, my_color)

if legal:

return a

except Exception:

continue

return "PASS"
def choose_greedy_from_scored(scored):

# scored: list of (pos, score) sorted desc

if not scored:

return ("PASS", "PASS")

# prefer captures (we might have computed cap into score separately earlier),

# but here we assume top scored already favors captures. Return top.

return scored[0][0]

def choose_move(prev, curr, color, qlearner=None):

# Q-learner prefered when loaded

if qlearner and qlearner.q:

try:

mv = choose_move_with_q(prev, curr, color, qlearner)

if mv and mv != "PASS":

return mv

if mv == "PASS":

return "PASS"

except Exception:

pass

start = [Link]()

moves = legal_moves(prev, curr, color)

# If only pass available, accept

if len(moves) == 1 and moves[0][0] == ("PASS", "PASS"):

return "PASS"

# Build heuristic scores


scored = []

for pos, cap in moves:

if pos == ("PASS", "PASS"):

score = -1.0

else:

r, c = pos

nb, _, _ = apply_move(curr, r, c, color)

val_after = evaluate_board(nb, color)

val_before = evaluate_board(curr, color)

score = cap * 6 + (val_after - val_before)

[Link]((pos, score))

# sort descending

[Link](key=lambda x: x[1], reverse=True)

# deterministic greedy fallback (used if MC inconclusive)

greedy_choice = choose_greedy_from_scored(scored)

# Prepare candidates for MC but avoid PASS unless it's the only candidate

candidates = [p for p, _ in scored if p != ("PASS", "PASS")]

if not candidates:

candidates = [("PASS", "PASS")]

# limit candidate count

K = min(5, len(candidates))

candidates = candidates[:K]

# time budget split

remaining = max(0.25, TIME_LIMIT - ([Link]() - start) - 0.25)


per = max(0.04, remaining / max(1, len(candidates)))

mc_results = {}

total_sims = 0

for cand in candidates:

wr, sims = monte_carlo_evaluate_move(prev, curr, color, cand, time_budget=min(per,


0.9), max_sims=120)

mc_results[cand] = (wr, sims)

total_sims += sims

if [Link]() - start > TIME_LIMIT - 0.15:

break

# If MC produced useful data, pick best. Otherwise fall back to greedy deterministic
choice.

if mc_results:

# Determine if MC is informative: require at least some sims and at least one positive
win-rate

best_cand, (best_wr, best_sims) = max(mc_results.items(), key=lambda x: (x[1][0], x[1]


[1]))

# If all win rates are zero or too few sims overall, use greedy instead

max_wr = max([v[0] for v in mc_results.values()]) if mc_results else 0.0

if total_sims < max(4, len(candidates) * 2) or max_wr == 0.0:

# MC inconclusive -> use greedy deterministic candidate

pick = greedy_choice

else:

pick = best_cand

else:

pick = greedy_choice

if pick == ("PASS", "PASS"):


return "PASS"

return f"{pick[0]},{pick[1]}"

# ------------------------- Entrypoint / Training harness -------------------------

def main():

args = [Link][1:]

if "train" in args or [Link]("PYTHONTRAIN") == "1":

q = QLearner()

EPISODES = int([Link]("EPISODES", "80000"))

OPPONENT_MIX = ["self", "random", "greedy", "aggressive", "aggressive"]

from math import exp

def heuristic_random(prev, curr, color):

m, _ = [Link](legal_moves(prev, curr, color))

return "PASS" if m == ("PASS", "PASS") else f"{m[0]},{m[1]}"

def heuristic_greedy(prev, curr, color):

moves = legal_moves(prev, curr, color)

m, _ = max(moves, key=lambda x: x[1])

return "PASS" if m == ("PASS", "PASS") else f"{m[0]},{m[1]}"

def heuristic_aggressive(prev, curr, color):

moves = legal_moves(prev, curr, color)

opp = 1 if color == 2 else 2

scored = []

for (pos, cap) in moves:


if pos == ("PASS", "PASS"):

[Link]((pos, -1000))

continue

r, c = pos

nb, _, _ = apply_move(curr, r, c, color)

opp_libs = sum(len(find_group_and_liberties(nb, rr, cc)[1])

for rr in range(BOARD_SIZE) for cc in range(BOARD_SIZE) if nb[rr][cc] ==


opp)

[Link]((pos, cap * 6 - opp_libs))

best = max(scored, key=lambda x: x[1])[0]

return "PASS" if best == ("PASS", "PASS") else f"{best[0]},{best[1]}"

def pick_heuristic(prev, curr, color, kind):

if kind == "random":

return heuristic_random(prev, curr, color)

if kind == "greedy":

return heuristic_greedy(prev, curr, color)

if kind == "aggressive":

return heuristic_aggressive(prev, curr, color)

return heuristic_random(prev, curr, color)

for ep in range(1, EPISODES + 1):

if ep % 2000 == 0:

print(f"[Training] Episode {ep}/{EPISODES}")

[Link] = 0.02 + 0.38 * [Link](-ep / 2500.0)

prev = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]

curr = copy_board(prev)
player = [Link]([1, 2])

passes = 0

opp_kind = [Link](OPPONENT_MIX)

for _ in range(BOARD_SIZE * BOARD_SIZE - 1):

s = board_to_runtime_key(curr, 1)

acts = available_action_keys(prev, curr, player)

use_heuristic = (opp_kind != "self" and [Link]() < 0.25)

if use_heuristic:

a = pick_heuristic(prev, curr, player, opp_kind)

else:

a = q.choose_action(s, acts)

if a == "PASS":

new = copy_board(curr)

cap = 0

else:

try:

r, c = map(int, [Link](","))

new, cap, legal = apply_move(curr, r, c, player)

if not legal:

new = copy_board(curr)

cap = 0

except Exception:

new = copy_board(curr)

cap = 0

my_stones = sum([Link](player) for row in new)


opp_stones = sum([Link](3 - player) for row in new)

my_libs = sum(len(find_group_and_liberties(new, r, c)[1])

for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if new[r][c] ==


player)

opp_libs = sum(len(find_group_and_liberties(new, r, c)[1])

for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if new[r][c] == 3 -


player)

prev_my_libs = sum(len(find_group_and_liberties(curr, r, c)[1])

for r in range(BOARD_SIZE) for c in range(BOARD_SIZE) if curr[r][c] ==


player)

delta_libs = my_libs - prev_my_libs

suicidal_penalty = -3.0 if delta_libs < 0 and cap == 0 else 0.0

defense_bonus = 0.4 if my_libs > opp_libs else 0.0

pass_bonus = 0.9 if a == "PASS" and my_stones >= opp_stones else 0.0

reward = 3.0 * cap + 0.35 * (my_stones - opp_stones) + 0.12 * (my_libs - opp_libs) +


suicidal_penalty + defense_bonus + pass_bonus

nextp = 1 if player == 2 else 2

s2 = board_to_runtime_key(new, 1)

[Link](s, a, reward, s2, available_action_keys(curr, new, nextp))

prev, curr = curr, new

player = nextp

if a == "PASS":

passes += 1

if passes >= 2:

break
[Link]()

print("[Training complete] Saved q_values.json")

return

# Play mode

my_color, prev, curr = read_input("[Link]")

qlearner = QLearner()

loaded = [Link]()

if loaded:

[Link] = 0.0

move = choose_move(prev, curr, my_color, qlearner if loaded else None)

move = [Link]()

if move != "PASS":

try:

r, c = map(int, [Link](","))

r = max(0, min(BOARD_SIZE - 1, r))

c = max(0, min(BOARD_SIZE - 1, c))

move = f"{r},{c}"

except Exception:

move = "PASS"

write_output(move, "[Link]")

if __name__ == "__main__":

main()

You might also like