mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 16:43:05 +02:00
202 lines
6.8 KiB
Python
202 lines
6.8 KiB
Python
import time
|
||
import random
|
||
from typing import Optional, Tuple
|
||
|
||
import chess
|
||
|
||
|
||
class RandomEngine:
|
||
"""A simple engine with a tiny alpha-beta search and material+mobility eval.
|
||
|
||
Keeps the same name for compatibility, but no longer picks purely random moves.
|
||
"""
|
||
|
||
def __init__(self, depth: int = 100, max_time_sec: float = 5):
|
||
self.depth = depth
|
||
self.max_time_sec = max_time_sec
|
||
|
||
# Centipawn values
|
||
self.piece_values = {
|
||
chess.PAWN: 100,
|
||
chess.KNIGHT: 320,
|
||
chess.BISHOP: 330,
|
||
chess.ROOK: 500,
|
||
chess.QUEEN: 900,
|
||
chess.KING: 0,
|
||
}
|
||
|
||
def choose_move(self, board: chess.Board) -> Optional[chess.Move]:
|
||
start = time.time()
|
||
best_move: Optional[chess.Move] = None
|
||
best_score = -float("inf") if board.turn else float("inf")
|
||
|
||
# Iterative deepening up to depth or time limit
|
||
for d in range(1, self.depth + 1):
|
||
elapsed = time.time() - start
|
||
if elapsed >= self.max_time_sec:
|
||
break
|
||
score, move = self._search_root(board, d, start)
|
||
if move is not None:
|
||
best_move, best_score = move, score
|
||
|
||
# Fallback to random if search didn’t find anything
|
||
if best_move is None:
|
||
moves = list(board.legal_moves)
|
||
return random.choice(moves) if moves else None
|
||
return best_move
|
||
|
||
def choose_move_with_explanation(self, board: chess.Board) -> Tuple[Optional[chess.Move], str]:
|
||
"""Return the chosen move and a human-readable explanation of top candidates.
|
||
|
||
The explanation lists top candidates with scores and quick annotations.
|
||
"""
|
||
start = time.time()
|
||
depth_used = 0
|
||
best_move: Optional[chess.Move] = None
|
||
scores: list[Tuple[chess.Move, float]] = []
|
||
|
||
# Analyze all legal moves at the root with alpha-beta to given depth/time
|
||
for d in range(1, self.depth + 1):
|
||
if time.time() - start >= self.max_time_sec:
|
||
break
|
||
depth_used = d
|
||
scores = self._analyze_root(board, d, start)
|
||
if scores:
|
||
best_move = scores[0][0]
|
||
|
||
if not scores:
|
||
# Fallback
|
||
mv = self.choose_move(board)
|
||
return mv, "fallback: random/legal-only (no analysis)"
|
||
|
||
# Build explanation
|
||
def annotate(m: chess.Move) -> str:
|
||
tags = []
|
||
if board.is_capture(m):
|
||
tags.append("x")
|
||
if m.promotion:
|
||
tags.append(f"={chess.piece_symbol(m.promotion).upper()}")
|
||
try:
|
||
if board.gives_check(m):
|
||
tags.append("+")
|
||
except Exception:
|
||
pass
|
||
return "".join(tags)
|
||
|
||
top = scores[:5]
|
||
best_cp = top[0][1]
|
||
lines = [
|
||
f"depth={depth_used} time={time.time()-start:.2f}s candidates={len(scores)}",
|
||
f"best {board.san(top[0][0])} ({top[0][0].uci()}) score={best_cp:.1f}",
|
||
]
|
||
if len(top) > 1:
|
||
lines.append("alternatives:")
|
||
for mv, sc in top[1:]:
|
||
delta = sc - best_cp
|
||
lines.append(f" {board.san(mv)} ({mv.uci()}) score={sc:.1f} delta={delta:+.1f} {annotate(mv)}")
|
||
|
||
return best_move, "\n".join(lines)
|
||
|
||
def _analyze_root(self, board: chess.Board, depth: int, start: float) -> list[Tuple[chess.Move, float]]:
|
||
alpha = -float("inf")
|
||
beta = float("inf")
|
||
scored: list[Tuple[chess.Move, float]] = []
|
||
for move in self._ordered_moves(board):
|
||
if time.time() - start >= self.max_time_sec:
|
||
break
|
||
board.push(move)
|
||
score = -self._alphabeta(board, depth - 1, -beta, -alpha, start)
|
||
board.pop()
|
||
scored.append((move, score))
|
||
if score > alpha:
|
||
alpha = score
|
||
if alpha >= beta:
|
||
break
|
||
scored.sort(key=lambda t: t[1], reverse=True)
|
||
return scored
|
||
|
||
def _search_root(self, board: chess.Board, depth: int, start: float) -> Tuple[float, Optional[chess.Move]]:
|
||
alpha = -float("inf")
|
||
beta = float("inf")
|
||
best_move: Optional[chess.Move] = None
|
||
best_score = -float("inf")
|
||
|
||
moves = self._ordered_moves(board)
|
||
for move in moves:
|
||
if time.time() - start >= self.max_time_sec:
|
||
break
|
||
board.push(move)
|
||
score = -self._alphabeta(board, depth - 1, -beta, -alpha, start)
|
||
board.pop()
|
||
if score > best_score:
|
||
best_score = score
|
||
best_move = move
|
||
if score > alpha:
|
||
alpha = score
|
||
if alpha >= beta:
|
||
break
|
||
return best_score, best_move
|
||
|
||
def _alphabeta(self, board: chess.Board, depth: int, alpha: float, beta: float, start: float) -> float:
|
||
# Time cutoff
|
||
if time.time() - start >= self.max_time_sec:
|
||
return self._evaluate(board)
|
||
|
||
# Terminal nodes
|
||
if depth == 0 or board.is_game_over():
|
||
return self._evaluate(board)
|
||
|
||
best = -float("inf")
|
||
for move in self._ordered_moves(board):
|
||
board.push(move)
|
||
score = -self._alphabeta(board, depth - 1, -beta, -alpha, start)
|
||
board.pop()
|
||
if score > best:
|
||
best = score
|
||
if best > alpha:
|
||
alpha = best
|
||
if alpha >= beta:
|
||
break
|
||
return best
|
||
|
||
def _ordered_moves(self, board: chess.Board):
|
||
# Simple move ordering: captures/promotions first, then checks
|
||
def score_move(m: chess.Move) -> int:
|
||
s = 0
|
||
if board.is_capture(m):
|
||
s += 1000
|
||
if m.promotion:
|
||
s += 800
|
||
try:
|
||
if board.gives_check(m):
|
||
s += 100
|
||
except Exception:
|
||
pass
|
||
return s
|
||
|
||
moves = list(board.legal_moves)
|
||
moves.sort(key=score_move, reverse=True)
|
||
return moves
|
||
|
||
def _evaluate(self, board: chess.Board) -> float:
|
||
# Game end conditions
|
||
if board.is_checkmate():
|
||
return -100000 if board.turn else 100000 # side-to-move is mated
|
||
if board.is_stalemate() or board.is_insufficient_material() or board.can_claim_draw():
|
||
return 0
|
||
|
||
# Material
|
||
material = 0
|
||
for square, piece in board.piece_map().items():
|
||
val = self.piece_values[piece.piece_type]
|
||
material += val if piece.color == chess.WHITE else -val
|
||
|
||
# Mobility (only side to move for speed; acts as tempo bonus)
|
||
mobility = 5 * sum(1 for _ in board.legal_moves)
|
||
if board.turn:
|
||
mobility_term = mobility
|
||
else:
|
||
mobility_term = -mobility
|
||
|
||
return material + mobility_term
|