diff --git a/123.py b/123.py index fbb17bc..d0cbd39 100644 --- a/123.py +++ b/123.py @@ -1,740 +1,479 @@ +"""Zero Point One bot for CodeCup 26. + +This implementation keeps the single-file requirement while providing a +self-contained engine with: + +* compact leaper move generation that understands both regular moves and + redeployments ("drops") of captured pieces; +* reversible move application so the search can explore game trees safely; +* iterative-deepening negamax with alpha-beta pruning, capture-only + quiescence, move ordering (MVV/LVA + history heuristic) and a light-weight + time manager; and +* a basic positional evaluator that rewards central control, mobility and the + material value of captured pieces still in hand. + +The file is intentionally free of external dependencies – it only relies on the +standard library – so that it can be compiled and executed directly by the +CodeCup judges. +""" + +from __future__ import annotations + import sys import time -import random -import math -from sys import stdin, stdout - -drW = [1, -1, 0, 0] -dcW = [0, 0, 1, -1] -drF = [1, 1, -1, -1] -dcF = [1, -1, 1, -1] -drN = [1, 1, -1, -1, 2, 2, -2, -2] -dcN = [2, -2, 2, -2, 1, -1, 1, -1] -drD = [0, 0, 2, -2] -dcD = [2, -2, 0, 0] -drA = [2, 2, -2, -2] -dcA = [2, -2, 2, -2] - -PIECE_VALUES = {'W': 10000, 'N': 500, 'F': 400, 'D': 300, 'A': 200} - -CENTER_BONUS = 30 -DEVELOPMENT_BONUS = 15 -KING_SAFETY_BONUS = 100 -MOBILITY_BONUS = 5 -THREAT_BONUS = 20 -DEFENSE_BONUS = 10 - -OPENING_BOOK = { - "red": [ - "WNFFDDDDAAAAAAAA", - "WNFDADAAADDFDAAAA", - "WNFDADAAADDFDAAAA" - ], - "blue": [ - "wnffddddaaaaaaaa", - "wnfddddaaaaaaaa", - "wnfddddaaaaaaaa" - ] +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional, Tuple + +# Board helpers ----------------------------------------------------------------- + +FILES = "abcdefgh" +RANKS = "12345678" + +MOVE_BOARD = 0 +MOVE_DROP = 1 +PIECE_ORDER = ["W", "N", "F", "D", "A"] +PIECE_INDEX = {p: i for i, p in enumerate(PIECE_ORDER)} + +PIECE_VALUES = { + "W": 20000, + "N": 700, + "F": 550, + "D": 450, + "A": 320, } -ENDGAME_PATTERNS = { - 'king_attack': 500, - 'king_defense': 300, - 'piece_activity': 50, - 'pawn_promotion': 200 +# Raw leaper offsets expressed as (d_row, d_col) on the 8x8 board where rows are +# labelled a-h (0-7) and columns 1-8 (0-7). +LEAPER_OFFSETS = { + "W": ((1, 0), (-1, 0), (0, 1), (0, -1)), + "N": ( + (1, 2), + (1, -2), + (-1, 2), + (-1, -2), + (2, 1), + (2, -1), + (-2, 1), + (-2, -1), + ), + "F": ((1, 1), (1, -1), (-1, 1), (-1, -1)), + "D": ((0, 2), (0, -2), (2, 0), (-2, 0)), + "A": ((2, 2), (2, -2), (-2, 2), (-2, -2)), } -class ZeroPointOneAI: - def __init__(self): - self.board = [['.' for _ in range(8)] for __ in range(8)] - self.capRed = [0] * 5 - self.capBlue = [0] * 5 - self.isRed = False - self.ourTurn = False - self.start_time = time.time() - self.time_limit = 25.0 - - def is_upper(self, c): - return 'A' <= c <= 'Z' - - def is_lower(self, c): - return 'a' <= c <= 'z' - - def index_of(self, c): - u = c.upper() - if u == 'W': return 0 - if u == 'N': return 1 - if u == 'F': return 2 - if u == 'D': return 3 - if u == 'A': return 4 - return -1 - - def get_piece_value(self, piece): - if piece == '.': - return 0 - return PIECE_VALUES.get(piece.upper(), 0) - - def get_positional_bonus(self, r, c, piece): - bonus = 0 - - center_distance = abs(r - 3.5) + abs(c - 3.5) - bonus += max(0, CENTER_BONUS - center_distance * 3) - - if self.isRed: - if r <= 1: - bonus += DEVELOPMENT_BONUS - else: - if r >= 6: - bonus += DEVELOPMENT_BONUS - - if piece.upper() == 'W': - friendly_count = 0 - for dr in [-1, 0, 1]: - for dc in [-1, 0, 1]: - nr, nc = r + dr, c + dc - if 0 <= nr < 8 and 0 <= nc < 8: - if self.board[nr][nc] != '.': - if (self.isRed and self.is_upper(self.board[nr][nc])) or \ - (not self.isRed and self.is_lower(self.board[nr][nc])): - friendly_count += 1 - bonus += friendly_count * KING_SAFETY_BONUS - - if self.isRed and r > 2: - bonus += 50 - elif not self.isRed and r < 5: - bonus += 50 - - mobility = self.count_mobility(r, c, piece) - bonus += mobility * MOBILITY_BONUS - - threat_bonus = self.calculate_threats(r, c, piece) - bonus += threat_bonus * THREAT_BONUS - - defense_bonus = self.calculate_defense(r, c, piece) - bonus += defense_bonus * DEFENSE_BONUS - - return bonus - - def count_mobility(self, r, c, piece): - moves = self.get_piece_moves(r, c, piece) - return len(moves) - - def calculate_threats(self, r, c, piece): - threats = 0 - moves = self.get_piece_moves(r, c, piece) - for move in moves: - if len(move) == 5 and move[4] is not None: - target = move[4] - if target != '.': - if (self.isRed and self.is_lower(target)) or (not self.isRed and self.is_upper(target)): - threats += 1 - return threats - - def calculate_defense(self, r, c, piece): - defense = 0 - moves = self.get_piece_moves(r, c, piece) - for move in moves: - if len(move) == 5 and move[4] is not None: - target = move[4] - if target != '.': - if (self.isRed and self.is_upper(target)) or (not self.isRed and self.is_lower(target)): - defense += 1 - return defense - - def evaluate_board(self): - score = 0 - - for r in range(8): - for c in range(8): - piece = self.board[r][c] - if piece == '.': - continue - - piece_value = self.get_piece_value(piece) - positional_bonus = self.get_positional_bonus(r, c, piece) - - if (self.isRed and self.is_upper(piece)) or (not self.isRed and self.is_lower(piece)): - score += piece_value + positional_bonus +# Pre-compute a simple centrality/initiative table used by the evaluation and +# move ordering. Squares closer to the centre and further from the owner’s home +# rows are preferred. +SQUARE_ACTIVITY: List[int] = [] +for row in range(8): + for col in range(8): + centre = 14 - int(abs(row - 3.5) * 3 + abs(col - 3.5) * 3) + advance_red = row * 2 # reward reaching deeper ranks for red + retreat_blue = (7 - row) * 2 # analogous for blue pieces (mirrored) + SQUARE_ACTIVITY.append(centre + advance_red + retreat_blue) + + +# Utility functions -------------------------------------------------------------- + +def square_to_index(coord: str) -> int: + """Convert a board coordinate like "b5" into a 0..63 index.""" + if len(coord) != 2: + raise ValueError(f"Invalid square '{coord}'") + row = FILES.index(coord[0]) + col = RANKS.index(coord[1]) + return row * 8 + col + + +def index_to_square(index: int) -> str: + row, col = divmod(index, 8) + return f"{FILES[row]}{RANKS[col]}" + + +def mirror_index(index: int) -> int: + row, col = divmod(index, 8) + return (7 - row) * 8 + col + + +@dataclass +class Move: + kind: int + src: Optional[int] + dst: int + piece: str + captured: str + + @staticmethod + def board(src: int, dst: int, piece: str, target: str) -> "Move": + return Move(MOVE_BOARD, src, dst, piece, target) + + @staticmethod + def drop(dst: int, piece: str) -> "Move": + return Move(MOVE_DROP, None, dst, piece, ".") + + +class ZeroPointOneBot: + """Single-file Zero Point One engine with search and evaluation.""" + + def __init__(self) -> None: + self.board: List[str] = ["."] * 64 + self.captured_red: List[int] = [0] * 5 + self.captured_blue: List[int] = [0] * 5 + self.turn_red: bool = True + self.our_color_red: bool = True + self.awaiting_opponent_start: bool = False + + self.history: Dict[Tuple[str, int], int] = {} + + self.time_limit: float = 2.8 # seconds per search burst + self.search_start: float = 0.0 + self.time_exceeded: bool = False + + self.max_depth: int = 6 + self.quiescence_depth: int = 6 + + # ------------------------------------------------------------------ Game IO + def reset_game(self, we_are_red: bool) -> None: + self.board = ["."] * 64 + self.captured_red = [0] * 5 + self.captured_blue = [0] * 5 + self.turn_red = True + self.our_color_red = we_are_red + self.awaiting_opponent_start = we_are_red + self.history.clear() + + def choose_start_sequence(self, for_red: bool) -> str: + if for_red: + # Compact central structure guarding the wazir while letting knights + # and ferzes develop quickly. + return "WNFDDFAAADAFNAAA" + return "wnfddfa aad afnaaa".replace(" ", "") + + def apply_start_sequence(self, sequence: str, for_red: bool) -> None: + rows = (0, 1) if for_red else (6, 7) + idx = 0 + for row in rows: + for col in range(8): + piece = sequence[idx] + idx += 1 + if piece == ".": + self.board[row * 8 + col] = "." else: - score -= piece_value + positional_bonus - - for i in range(5): - piece_value = PIECE_VALUES[['W', 'N', 'F', 'D', 'A'][i]] - if self.isRed: - score += self.capRed[i] * piece_value - score -= self.capBlue[i] * piece_value - else: - score += self.capBlue[i] * piece_value - score -= self.capRed[i] * piece_value - - score += self.evaluate_endgame() - - score += self.evaluate_tactical_patterns() - - return score - - def evaluate_endgame(self): - score = 0 - - red_pieces = sum(1 for r in range(8) for c in range(8) - if self.board[r][c] != '.' and self.is_upper(self.board[r][c])) - blue_pieces = sum(1 for r in range(8) for c in range(8) - if self.board[r][c] != '.' and self.is_lower(self.board[r][c])) - - total_pieces = red_pieces + blue_pieces - - if total_pieces <= 8: - for r in range(8): - for c in range(8): - piece = self.board[r][c] - if piece.upper() == 'W': - if (self.isRed and self.is_upper(piece)) or (not self.isRed and self.is_lower(piece)): - center_distance = abs(r - 3.5) + abs(c - 3.5) - score += max(0, 100 - center_distance * 20) - - return score - - def evaluate_tactical_patterns(self): - score = 0 - - for r in range(8): - for c in range(8): - piece = self.board[r][c] - if piece == '.': - continue - - if self.is_double_attack(r, c, piece): - piece_value = self.get_piece_value(piece) - if (self.isRed and self.is_upper(piece)) or (not self.isRed and self.is_lower(piece)): - score += piece_value * 0.5 - else: - score -= piece_value * 0.5 - - return score - - def is_double_attack(self, r, c, piece): - moves = self.get_piece_moves(r, c, piece) - enemy_targets = 0 - - for move in moves: - if len(move) == 5 and move[4] is not None: - target = move[4] - if target != '.': - if (self.isRed and self.is_lower(target)) or (not self.isRed and self.is_upper(target)): - enemy_targets += 1 - - return enemy_targets >= 2 - - def get_all_moves(self, is_red_turn): - moves = [] - - for r in range(8): - for c in range(8): - piece = self.board[r][c] - if piece == '.': - continue - if is_red_turn and not self.is_upper(piece): + self.board[row * 8 + col] = piece.upper() if for_red else piece.lower() + + def parse_move(self, token: str, is_red_move: bool) -> Move: + if len(token) == 4: + src = square_to_index(token[:2]) + dst = square_to_index(token[2:]) + piece = self.board[src] + target = self.board[dst] + return Move.board(src, dst, piece, target) + if len(token) == 3: + piece = token[0] + dst = square_to_index(token[1:]) + return Move.drop(dst, piece) + raise ValueError(f"Invalid move token '{token}'") + + def format_move(self, move: Move) -> str: + if move.kind == MOVE_DROP: + return f"{move.piece}{index_to_square(move.dst)}" + assert move.src is not None + return f"{index_to_square(move.src)}{index_to_square(move.dst)}" + + def play_move_from_string(self, token: str, is_red_move: bool) -> None: + move = self.parse_move(token, is_red_move) + self.play_move(move, is_red_move) + + def play_move(self, move: Move, is_red_move: bool) -> None: + captured = self.make_move(move, is_red_move) + # No undo – this updates the permanent game state. + if captured is not None and captured.upper() == "W": + # Game would end; nothing special to do for bookkeeping because the + # arbiter will send Quit immediately afterwards. + pass + + # --------------------------------------------------------------- Move utils + def make_move(self, move: Move, is_red_move: bool) -> Optional[str]: + if move.kind == MOVE_BOARD: + assert move.src is not None + piece = move.piece + target = move.captured + assert self.board[move.src] == piece + assert self.board[move.dst] == target + self.board[move.src] = "." + self.board[move.dst] = piece + if target != ".": + pool = self.captured_red if is_red_move else self.captured_blue + pool[PIECE_INDEX[target.upper()]] += 1 + return target + # Drop + pool = self.captured_red if is_red_move else self.captured_blue + pool[PIECE_INDEX[move.piece.upper()]] -= 1 + self.board[move.dst] = move.piece + return None + + def undo_move(self, move: Move, is_red_move: bool, captured: Optional[str]) -> None: + if move.kind == MOVE_BOARD: + assert move.src is not None + self.board[move.src] = move.piece + self.board[move.dst] = captured if captured is not None else "." + if captured and captured != ".": + pool = self.captured_red if is_red_move else self.captured_blue + pool[PIECE_INDEX[captured.upper()]] -= 1 + else: + self.board[move.dst] = "." + pool = self.captured_red if is_red_move else self.captured_blue + pool[PIECE_INDEX[move.piece.upper()]] += 1 + + # --------------------------------------------------------------- Move gen + def generate_moves(self, is_red_turn: bool) -> List[Move]: + moves: List[Move] = [] + board = self.board + for idx, piece in enumerate(board): + if piece == ".": + continue + if piece.isupper() != is_red_turn: + continue + p_type = piece.upper() + for dr, dc in LEAPER_OFFSETS[p_type]: + row, col = divmod(idx, 8) + nr, nc = row + dr, col + dc + if not (0 <= nr < 8 and 0 <= nc < 8): continue - if not is_red_turn and not self.is_lower(piece): + dst = nr * 8 + nc + target = board[dst] + if target == "." or target.isupper() != is_red_turn: + moves.append(Move.board(idx, dst, piece, target)) + pool = self.captured_red if is_red_turn else self.captured_blue + if any(pool): + for p_idx, count in enumerate(pool): + if count <= 0: continue - - piece_moves = self.get_piece_moves(r, c, piece) - for move in piece_moves: - if self.is_valid_move(move, is_red_turn): - moves.append(move) - - captured = self.capRed if is_red_turn else self.capBlue - for i in range(5): - if captured[i] > 0: - piece = (['W', 'N', 'F', 'D', 'A'][i] if is_red_turn else - ['w', 'n', 'f', 'd', 'a'][i]) - for rr in range(8): - for cc in range(8): - if self.board[rr][cc] == '.': - move = (piece, rr, cc, None, None) - if self.is_valid_move(move, is_red_turn): - moves.append(move) - - return moves - - def get_piece_moves(self, r, c, piece): - moves = [] - t = piece.upper() - - if t == 'W': - for i in range(4): - nr, nc = r + drW[i], c + dcW[i] - if 0 <= nr < 8 and 0 <= nc < 8: - moves.append((r, c, nr, nc, self.board[nr][nc])) - elif t == 'N': - for i in range(8): - nr, nc = r + drN[i], c + dcN[i] - if 0 <= nr < 8 and 0 <= nc < 8: - moves.append((r, c, nr, nc, self.board[nr][nc])) - elif t == 'F': - for i in range(4): - nr, nc = r + drF[i], c + dcF[i] - if 0 <= nr < 8 and 0 <= nc < 8: - moves.append((r, c, nr, nc, self.board[nr][nc])) - elif t == 'D': - for i in range(4): - nr, nc = r + drD[i], c + dcD[i] - if 0 <= nr < 8 and 0 <= nc < 8: - moves.append((r, c, nr, nc, self.board[nr][nc])) - elif t == 'A': - for i in range(4): - nr, nc = r + drA[i], c + dcA[i] - if 0 <= nr < 8 and 0 <= nc < 8: - moves.append((r, c, nr, nc, self.board[nr][nc])) - + piece = PIECE_ORDER[p_idx] if is_red_turn else PIECE_ORDER[p_idx].lower() + for sq, occupant in enumerate(board): + if occupant == ".": + moves.append(Move.drop(sq, piece)) return moves - - def is_valid_move(self, move, is_red_turn): - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - piece = self.board[r1][c1] - if piece == '.': - return False - if is_red_turn and not self.is_upper(piece): - return False - if not is_red_turn and not self.is_lower(piece): - return False - - if target != '.': - if (is_red_turn and self.is_upper(target)) or (not is_red_turn and self.is_lower(target)): - return False - - return True - elif len(move) == 5 and move[4] is None: - piece, r, c, _, _ = move - if self.board[r][c] != '.': - return False - - idx = self.index_of(piece) - if idx == -1: - return False - - captured = self.capRed if is_red_turn else self.capBlue - if captured[idx] <= 0: - return False - - return True - - return False - - def make_move(self, move): - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - piece = self.board[r1][c1] - - self.board[r2][c2] = piece - self.board[r1][c1] = '.' - - if target != '.': - idx = self.index_of(target) - if self.isRed: - self.capRed[idx] += 1 - else: - self.capBlue[idx] += 1 - - return target.upper() == 'W' - elif len(move) == 5 and move[4] is None: - piece, r, c, _, _ = move - self.board[r][c] = piece - idx = self.index_of(piece) - if (self.isRed and self.is_upper(piece)) or (not self.isRed and self.is_lower(piece)): - if self.isRed: - self.capRed[idx] -= 1 - else: - self.capBlue[idx] -= 1 - - return False - - def unmake_move(self, move, captured_piece=None): - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - piece = self.board[r2][c2] - - self.board[r1][c1] = piece - self.board[r2][c2] = target - - if target and target != '.': - idx = self.index_of(target) - if self.isRed: - self.capRed[idx] -= 1 - else: - self.capBlue[idx] -= 1 - elif len(move) == 5 and move[4] is None: - piece, r, c, _, _ = move - self.board[r][c] = '.' - idx = self.index_of(piece) - if (self.isRed and self.is_upper(piece)) or (not self.isRed and self.is_lower(piece)): - if self.isRed: - self.capRed[idx] += 1 - else: - self.capBlue[idx] += 1 - - def order_moves(self, moves, is_red_turn): - def move_priority(move): - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - piece = self.board[r1][c1] - - # Base: captures prioritized by target value (and king highest) - capture_score = 0 - if target != '.': - target_value = self.get_piece_value(target) - piece_value = self.get_piece_value(piece) - if target.upper() == 'W': - capture_score = 10000 + + def generate_captures(self, is_red_turn: bool) -> List[Move]: + return [ + move + for move in self.generate_moves(is_red_turn) + if move.kind == MOVE_BOARD and move.captured != "." + ] + + def order_moves(self, moves: Iterable[Move]) -> List[Move]: + ordered: List[Tuple[int, Move]] = [] + for move in moves: + if move.kind == MOVE_BOARD: + target_value = PIECE_VALUES.get(move.captured.upper(), 0) if move.captured != "." else 0 + mover_value = PIECE_VALUES[move.piece.upper()] + capture_bonus = 0 + if move.captured != ".": + if move.captured.upper() == "W": + capture_bonus = 100_000 else: - # MVV-LVA style - capture_score = 1000 + target_value - piece_value + capture_bonus = 5_000 + target_value - mover_value // 2 + history_bonus = self.history.get((move.piece, move.dst), 0) + activity_bonus = SQUARE_ACTIVITY[move.dst] + score = capture_bonus + history_bonus + activity_bonus + else: + drop_value = PIECE_VALUES[move.piece.upper()] + history_bonus = self.history.get((move.piece, move.dst), 0) // 4 + score = 1_000 + drop_value + SQUARE_ACTIVITY[move.dst] + history_bonus + ordered.append((score, move)) + ordered.sort(key=lambda item: item[0], reverse=True) + return [move for _, move in ordered] - # Advancement and centralization - if is_red_turn: - advance_bonus = r2 * 10 - else: - advance_bonus = (7 - r2) * 10 - center_distance = abs(r2 - 3.5) + abs(c2 - 3.5) - center_bonus = 50 - center_distance * 10 - - # Defense improvement: simulate move and score added defense for the moved piece - defense_improvement = 0 - moved_piece_defense_after = 0 - # Simulate quickly - saved_target = self.board[r2][c2] - self.board[r2][c2] = piece - self.board[r1][c1] = '.' - try: - moved_piece_defense_after = self.calculate_defense(r2, c2, piece) - finally: - # revert - self.board[r1][c1] = piece - self.board[r2][c2] = saved_target - defense_improvement = moved_piece_defense_after * 20 - - return capture_score + advance_bonus + center_bonus + defense_improvement + # --------------------------------------------------------------- Evaluation + def evaluate(self) -> int: + score = 0 + for idx, piece in enumerate(self.board): + if piece == ".": + continue + value = PIECE_VALUES[piece.upper()] + activity = SQUARE_ACTIVITY[idx] if piece.isupper() else SQUARE_ACTIVITY[mirror_index(idx)] + mobility = len(self._mobility_cache(idx, piece)) + total = value + activity + mobility * 5 + if piece.isupper(): + score += total else: - return 0 + score -= total + for i, count in enumerate(self.captured_red): + score += count * PIECE_VALUES[PIECE_ORDER[i]] + for i, count in enumerate(self.captured_blue): + score -= count * PIECE_VALUES[PIECE_ORDER[i]] + return score - return sorted(moves, key=move_priority, reverse=True) + def _mobility_cache(self, idx: int, piece: str) -> List[int]: + row, col = divmod(idx, 8) + offsets = LEAPER_OFFSETS[piece.upper()] + moves: List[int] = [] + for dr, dc in offsets: + nr, nc = row + dr, col + dc + if 0 <= nr < 8 and 0 <= nc < 8: + moves.append(nr * 8 + nc) + return moves - def quiescence(self, alpha, beta, is_red_turn): - # Stand-pat evaluation - stand_pat = self.evaluate_board() - if stand_pat >= beta: - return stand_pat - if stand_pat > alpha: - alpha = stand_pat + def evaluate_for(self, is_red_turn: bool) -> int: + base = self.evaluate() + return base if is_red_turn else -base - # Generate capture moves only for side to move - capture_moves = [] - all_moves = self.get_all_moves(is_red_turn) - for mv in all_moves: - if len(mv) == 5 and mv[4] is not None and mv[4] != '.': - capture_moves.append(mv) - - # Simple ordering: most valuable victim - least valuable attacker - def cap_key(mv): - r1, c1, r2, c2, target = mv - return self.get_piece_value(target) - self.get_piece_value(self.board[r1][c1]) - capture_moves.sort(key=cap_key, reverse=True) - - start_check = time.time() - for mv in capture_moves: - # Time guard inside quiescence as well - if time.time() - self.start_time > self.time_limit * 0.98: - break - won = self.make_move(mv) - score = self.quiescence(-beta, -alpha, not is_red_turn) - self.unmake_move(mv) - score = -score - if score >= beta: - return score - if score > alpha: - alpha = score - return alpha - - def minimax(self, depth, alpha, beta, maximizing_player, is_red_turn): - if depth <= 0: - return self.quiescence(alpha, beta, is_red_turn) - - if time.time() - self.start_time > self.time_limit * 0.98: - return self.evaluate_board() - - moves = self.get_all_moves(is_red_turn) + # --------------------------------------------------------------- Search + def search(self, is_red_turn: bool) -> Move: + moves = self.generate_moves(is_red_turn) if not moves: - return self.evaluate_board() - - moves = self.order_moves(moves, is_red_turn) - # Consider more moves now; ordering will prune via alpha-beta - moves = moves[:20] - - if maximizing_player: - max_eval = float('-inf') - for i, move in enumerate(moves): - if not self.is_valid_move(move, is_red_turn): - continue - - if i > 5 and time.time() - self.start_time > self.time_limit * 0.8: - break - - won = self.make_move(move) - if won: - self.unmake_move(move) - return 10000 if is_red_turn == self.isRed else -10000 - - eval_score = self.minimax(depth - 1, alpha, beta, False, not is_red_turn) - self.unmake_move(move) - - max_eval = max(max_eval, eval_score) - alpha = max(alpha, eval_score) - if beta <= alpha: - break - - return max_eval - else: - min_eval = float('inf') - for i, move in enumerate(moves): - if not self.is_valid_move(move, is_red_turn): - continue - - if i > 5 and time.time() - self.start_time > self.time_limit * 0.8: - break - - won = self.make_move(move) - if won: - self.unmake_move(move) - return 10000 if is_red_turn == self.isRed else -10000 - - eval_score = self.minimax(depth - 1, alpha, beta, True, not is_red_turn) - self.unmake_move(move) - - min_eval = min(min_eval, eval_score) - beta = min(beta, eval_score) - if beta <= alpha: - break - - return min_eval - - def has_tactical_moves(self, moves): - for move in moves: - if len(move) == 5 and move[4] is not None and move[4] != '.': - return True - return False - - def get_best_move(self): - moves = self.get_all_moves(self.isRed) - if not moves: - return None - - for move in moves: - won = self.make_move(move) - if won: - self.unmake_move(move) - return move - self.unmake_move(move) - - best_move = None - best_score = float('-inf') if self.isRed else float('inf') - - moves = self.order_moves(moves, self.isRed) - - # Iterative deepening until time budget nearly exhausted + return Move.drop(0, PIECE_ORDER[0]) # fallback, should never happen + + ordered_moves = self.order_moves(moves) + best_move = ordered_moves[0] + best_score = -float("inf") + + self.search_start = time.time() + self.time_exceeded = False + depth = 1 - while True: - if time.time() - self.start_time > self.time_limit * 0.95: + while depth <= self.max_depth: + alpha = -100_000_000 + beta = 100_000_000 + score, move = self.negamax(depth, alpha, beta, is_red_turn) + if self.time_exceeded: break - - depth_best_move = None - depth_best_score = float('-inf') if self.isRed else float('inf') - - # Expand moves researched with depth - moves_to_search = moves[:min(12, len(moves))] - for move in moves_to_search: - - if time.time() - self.start_time > self.time_limit * 0.98: - break - - won = self.make_move(move) - if won: - self.unmake_move(move) - return move - - score = self.minimax(depth - 1, float('-inf'), float('inf'), False, not self.isRed) - self.unmake_move(move) - - if self.isRed: - if score > depth_best_score: - depth_best_score = score - depth_best_move = move - else: - if score < depth_best_score: - depth_best_score = score - depth_best_move = move - - if depth_best_move is not None: - best_move = depth_best_move - best_score = depth_best_score + if move is not None: + best_move = move + best_score = score depth += 1 - - if best_move is None: - for move in moves: - if len(move) == 5 and move[4] is not None and move[4] != '.': - return move - for move in moves: - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - if self.isRed and r2 > r1: - return move - elif not self.isRed and r2 < r1: - return move - return moves[0] if moves else None - + if best_score == -float("inf"): + best_move = ordered_moves[0] return best_move - - def get_opening_sequence(self, color): - if color == "red": - return OPENING_BOOK["red"][0] - else: - return OPENING_BOOK["blue"][0] - - def format_move(self, move): - if len(move) == 5 and move[4] is not None: - r1, c1, r2, c2, target = move - return chr(ord('a') + r1) + str(c1 + 1) + chr(ord('a') + r2) + str(c2 + 1) - elif len(move) == 5 and move[4] is None: - piece, r, c, _, _ = move - if 0 <= r < 8 and 0 <= c < 8: - return piece + chr(ord('a') + r) + str(c + 1) - return "" - - def play_game(self): - line = stdin.readline().strip() - if not line: - sys.exit(0) - - if line == "Start": - self.isRed = True - seq = self.get_opening_sequence("red") - stdout.write(seq + "\n") - stdout.flush() - oppseq = stdin.readline().strip() - - for i in range(8): - self.board[0][i] = seq[i] if i < len(seq) else '.' - for i in range(8): - self.board[1][i] = seq[8 + i] if 8 + i < len(seq) else '.' - for i in range(8): - self.board[6][i] = oppseq[i] if i < len(oppseq) else '.' - for i in range(8): - self.board[7][i] = oppseq[8 + i] if 8 + i < len(oppseq) else '.' - self.ourTurn = True - else: - self.isRed = False - redseq = line - seq = self.get_opening_sequence("blue") - stdout.write(seq + "\n") - stdout.flush() - - for i in range(8): - self.board[0][i] = redseq[i] if i < len(redseq) else '.' - for i in range(8): - self.board[1][i] = redseq[8 + i] if 8 + i < len(redseq) else '.' - for i in range(8): - self.board[6][i] = seq[i] if i < len(seq) else '.' - for i in range(8): - self.board[7][i] = seq[8 + i] if 8 + i < len(seq) else '.' - self.ourTurn = False - - while True: - if self.ourTurn: - self.start_time = time.time() - - try: - best_move = self.get_best_move() - except Exception as e: - moves = self.get_all_moves(self.isRed) - if moves: - best_move = moves[0] - else: - best_move = None - - if best_move is None: - stdout.write("Quit\n") - stdout.flush() - break - - move_str = self.format_move(best_move) - - if len(move_str) != 4: - moves = self.get_all_moves(self.isRed) - if moves: - for move in moves: - if len(move) == 5 and move[4] is not None: - move_str = self.format_move(move) - if move_str and len(move_str) == 4: - break - - # Allow 4-char regular moves and 3-char revival moves - if move_str and (len(move_str) == 4 or len(move_str) == 3): - stdout.write(move_str + "\n") - stdout.flush() - else: - stdout.write("Quit\n") - stdout.flush() - break - - won = self.make_move(best_move) - if won: - break - - self.ourTurn = False + + def negamax(self, depth: int, alpha: int, beta: int, is_red_turn: bool) -> Tuple[int, Optional[Move]]: + if self.check_time(): + return 0, None + if depth == 0: + return self.quiescence(alpha, beta, is_red_turn), None + + best_move: Optional[Move] = None + local_alpha = alpha + moves = self.order_moves(self.generate_moves(is_red_turn)) + if not moves: + return self.evaluate_for(is_red_turn), None + + for move in moves: + captured = self.make_move(move, is_red_turn) + if captured is not None and captured.upper() == "W": + score = 100_000_000 - (self.max_depth - depth) * 100 else: - line = stdin.readline() - if not line: - break - line = line.strip() - if line == "Quit": + score, _ = self.negamax(depth - 1, -beta, -local_alpha, not is_red_turn) + score = -score + self.undo_move(move, is_red_turn, captured) + if self.time_exceeded: + return 0, None + if score > local_alpha: + local_alpha = score + best_move = move + if local_alpha >= beta: + self.record_history(move, depth) break - - if len(line) == 4: - r1 = ord(line[0]) - ord('a') - c1 = int(line[1]) - 1 - r2 = ord(line[2]) - ord('a') - c2 = int(line[3]) - 1 - moving = self.board[r1][c1] - dest = self.board[r2][c2] - self.board[r2][c2] = moving - self.board[r1][c1] = '.' - if dest != '.': - idx = self.index_of(dest) - if self.isRed: - self.capRed[idx] += 1 - else: - self.capBlue[idx] += 1 - if (self.isRed and dest == 'W') or (not self.isRed and dest == 'w'): - break - elif len(line) == 3: - piece = line[0] - r = ord(line[1]) - ord('a') - c = int(line[2]) - 1 - self.board[r][c] = piece - idx = self.index_of(piece) - if (self.isRed and self.is_lower(piece)) or (not self.isRed and self.is_upper(piece)): - if self.isRed: - self.capRed[idx] -= 1 - else: - self.capBlue[idx] -= 1 - else: - if self.isRed: - self.capBlue[idx] -= 1 - else: - self.capRed[idx] -= 1 - - self.ourTurn = True + if best_move is None: + best_move = moves[0] + return local_alpha, best_move + + def quiescence(self, alpha: int, beta: int, is_red_turn: bool, depth: int = 0) -> int: + if self.check_time(): + return 0 + stand_pat = self.evaluate_for(is_red_turn) + if stand_pat >= beta: + return beta + if stand_pat > alpha: + alpha = stand_pat + if depth >= self.quiescence_depth: + return alpha + captures = self.order_moves(self.generate_captures(is_red_turn)) + for move in captures: + captured = self.make_move(move, is_red_turn) + if captured is not None and captured.upper() == "W": + score = 100_000_000 - depth * 10 + else: + score = -self.quiescence(-beta, -alpha, not is_red_turn, depth + 1) + self.undo_move(move, is_red_turn, captured) + if self.time_exceeded: + return alpha + if score >= beta: + self.record_history(move, depth + 1) + return beta + if score > alpha: + alpha = score + return alpha + + def record_history(self, move: Move, depth: int) -> None: + key = (move.piece, move.dst) + self.history[key] = self.history.get(key, 0) + (1 << depth) + + def check_time(self) -> bool: + if self.time_exceeded: + return True + if time.time() - self.search_start >= self.time_limit: + self.time_exceeded = True + return self.time_exceeded + + +# --------------------------------------------------------------------------- CLI + +def main() -> None: + bot = ZeroPointOneBot() + color_known = False + + for raw in sys.stdin: + token = raw.strip() + if not token: + continue + + if token == "Quit": + break + + if token == "Start": + bot.reset_game(we_are_red=True) + start = bot.choose_start_sequence(for_red=True) + bot.apply_start_sequence(start, for_red=True) + print(start) + sys.stdout.flush() + color_known = True + continue + + if not color_known: + bot.reset_game(we_are_red=False) + bot.apply_start_sequence(token, for_red=True) + reply = bot.choose_start_sequence(for_red=False) + bot.apply_start_sequence(reply, for_red=False) + print(reply) + sys.stdout.flush() + color_known = True + bot.turn_red = True + continue + + if bot.awaiting_opponent_start: + bot.apply_start_sequence(token, for_red=False) + bot.awaiting_opponent_start = False + bot.turn_red = True + move = bot.search(is_red_turn=True) + bot.play_move(move, True) + bot.turn_red = False + print(bot.format_move(move)) + sys.stdout.flush() + continue + + is_red_move = bot.turn_red + bot.play_move_from_string(token, is_red_move) + bot.turn_red = not bot.turn_red + + our_turn = (bot.turn_red and bot.our_color_red) or (not bot.turn_red and not bot.our_color_red) + if our_turn: + move = bot.search(bot.turn_red) + bot.play_move(move, bot.turn_red) + bot.turn_red = not bot.turn_red + print(bot.format_move(move)) + sys.stdout.flush() + if __name__ == "__main__": - ai = ZeroPointOneAI() - ai.play_game() + main()