#!
/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()