Below is a single-file Python program using only the standard library and Tkinter. It supports legal move generation (including castling, en passant, and promotion), check detection, and an AI that searches 4 plies using minimax with alpha-beta pruning. White is human by default; black is AI.
Source Code:
import tkinter as tk
from tkinter import messagebox
import math
import random
import time
# Chess program with Tkinter GUI and 4-ply minimax (alpha-beta)
# Standard library only. Single file. Basic evaluation and full legal move generation.
# Board representation:
# 8x8 list of lists, each cell is a piece char or '.' for empty.
# White: 'P','N','B','R','Q','K'
# Black: 'p','n','b','r','q','k'
START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
WHITE = 'w'
BLACK = 'b'
PIECE_VALUES = {
'P': 100, 'N': 320, 'B': 330, 'R': 500, 'Q': 900, 'K': 20000,
'p': -100, 'n': -320, 'b': -330, 'r': -500, 'q': -900, 'k': -20000
}
# Simple piece-square tables (midgame, from white's perspective)
# Values in centipawns; black uses mirrored indices
PST_PAWN = [
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5,-5,-10, 0, 0,-10,-5, 5,
5,10,10,-20,-20,10,10, 5,
0, 0, 0, 0, 0, 0, 0, 0
]
PST_KNIGHT = [
-50,-40,-30,-30,-30,-30,-40,-50,
-40,-20, 0, 0, 0, 0,-20,-40,
-30, 0,10,15,15,10, 0,-30,
-30, 5,15,20,20,15, 5,-30,
-30, 0,15,20,20,15, 0,-30,
-30, 5,10,15,15,10, 5,-30,
-40,-20, 0, 5, 5, 0,-20,-40,
-50,-40,-30,-30,-30,-30,-40,-50
]
PST_BISHOP = [
-20,-10,-10,-10,-10,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5,10,10, 5, 0,-10,
-10, 5, 5,10,10, 5, 5,-10,
-10, 0,10,10,10,10, 0,-10,
-10,10,10,10,10,10,10,-10,
-10, 5, 0, 0, 0, 0, 5,-10,
-20,-10,-10,-10,-10,-10,-10,-20
]
PST_ROOK = [
0, 0, 0, 0, 0, 0, 0, 0,
5,10,10,10,10,10,10, 5,
-5, 0, 0, 0, 0, 0, 0,-5,
-5, 0, 0, 0, 0, 0, 0,-5,
-5, 0, 0, 0, 0, 0, 0,-5,
-5, 0, 0, 0, 0, 0, 0,-5,
-5, 0, 0, 0, 0, 0, 0,-5,
0, 0, 0, 5, 5, 0, 0, 0
]
PST_QUEEN = [
-20,-10,-10,-5,-5,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5, 5, 5, 5, 0,-10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0,-10,
-10, 0, 5, 0, 0, 0, 0,-10,
-20,-10,-10,-5,-5,-10,-10,-20
]
PST_KING_MID = [
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-20,-30,-30,-40,-40,-30,-30,-20,
-10,-20,-20,-20,-20,-20,-20,-10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20
]
UNICODE_MAP = {
'P': '♙', 'N': '♘', 'B': '♗', 'R': '♖', 'Q': '♕', 'K': '♔',
'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚'
}
SQUARE_SIZE = 64
BOARD_COLOR_LIGHT = "#F0D9B5"
BOARD_COLOR_DARK = "#B58863"
HIGHLIGHT_COLOR = "#FFD966"
class Position:
def __init__(self, board, turn, castling, ep_target, halfmove, fullmove):
self.board = board # 8x8 list
self.turn = turn # 'w' or 'b'
self.castling = castling # string like "KQkq"
self.ep_target = ep_target # (r,c) or None
self.halfmove = halfmove
self.fullmove = fullmove
def copy(self):
return Position([row[:] for row in self.board], self.turn, self.castling, self.ep_target, self.halfmove, self.fullmove)
def parse_fen(fen):
parts = fen.split()
rows = parts[0].split('/')
board = []
for r in rows:
row = []
for ch in r:
if ch.isdigit():
for _ in range(int(ch)):
row.append('.')
else:
row.append(ch)
board.append(row)
turn = parts[1]
castling = parts[2]
ep = parts[3]
ep_target = None
if ep != '-':
file = ord(ep[0]) - ord('a')
rank = 8 - int(ep[1])
ep_target = (rank, file)
halfmove = int(parts[4]) if len(parts) > 4 else 0
fullmove = int(parts[5]) if len(parts) > 5 else 1
return Position(board, turn, castling, ep_target, halfmove, fullmove)
def to_fen(pos):
rows = []
for r in range(8):
empty = 0
row_str = ""
for c in range(8):
piece = pos.board[r][c]
if piece == '.':
empty += 1
else:
if empty:
row_str += str(empty)
empty = 0
row_str += piece
if empty:
row_str += str(empty)
rows.append(row_str)
board_part = "/".join(rows)
ep = '-'
if pos.ep_target:
ep = chr(pos.ep_target[1] + ord('a')) + str(8 - pos.ep_target[0])
return f"{board_part} {pos.turn} {pos.castling if pos.castling else '-'} {ep} {pos.halfmove} {pos.fullmove}"
def in_bounds(r, c):
return 0 <= r < 8 and 0 <= c < 8
def is_white(piece):
return piece.isupper()
def is_black(piece):
return piece.islower()
def side_of(piece):
if piece == '.':
return None
return WHITE if is_white(piece) else BLACK
def opposite(side):
return WHITE if side == BLACK else BLACK
def king_position(pos, side):
target = 'K' if side == WHITE else 'k'
for r in range(8):
for c in range(8):
if pos.board[r][c] == target:
return (r, c)
return None
def attacks_square(pos, r, c, side):
# Check if side attacks (r,c). Used for check determination and castling.
# Generate pseudo-legal attacks quickly.
directions_bishop = [(-1,-1), (-1,1), (1,-1), (1,1)]
directions_rook = [(-1,0), (1,0), (0,-1), (0,1)]
directions_knight = [(-2,-1), (-2,1), (-1,-2), (-1,2), (1,-2), (1,2), (2,-1), (2,1)]
directions_king = directions_bishop + directions_rook
# Pawns
if side == WHITE:
for dc in (-1, 1):
rr, cc = r+1, c+dc # white pawns attack down from black perspective; but our board top is row 0 (rank 8). White pawns move towards decreasing row? Let's define:
# Clarify: row 0 is top (rank 8), row 7 is bottom (rank 1). White pawns move from row 6->5->... upward (towards row 0).
# So white pawn attacks (r-1, c±1); black pawn attacks (r+1, c±1).
# Fix above:
# White pawn attacks
for dc in (-1, 1):
rr, cc = r-1, c+dc
if in_bounds(rr, cc) and pos.board[rr][cc] == 'P' and side_of('P') == side:
return True
# Black pawn attacks
for dc in (-1, 1):
rr, cc = r+1, c+dc
if in_bounds(rr, cc) and pos.board[rr][cc] == 'p' and side_of('p') == side:
return True
# Knights
for dr, dc in directions_knight:
rr, cc = r+dr, c+dc
if in_bounds(rr, cc):
p = pos.board[rr][cc]
if (p == 'N' and side == WHITE) or (p == 'n' and side == BLACK):
return True
# Bishops / Queens (diagonals)
for dr, dc in directions_bishop:
rr, cc = r+dr, c+dc
while in_bounds(rr, cc):
p = pos.board[rr][cc]
if p != '.':
if (side == WHITE and (p == 'B' or p == 'Q')) or (side == BLACK and (p == 'b' or p == 'q')):
return True
break
rr += dr
cc += dc
# Rooks / Queens (straight)
for dr, dc in directions_rook:
rr, cc = r+dr, c+dc
while in_bounds(rr, cc):
p = pos.board[rr][cc]
if p != '.':
if (side == WHITE and (p == 'R' or p == 'Q')) or (side == BLACK and (p == 'r' or p == 'q')):
return True
break
rr += dr
cc += dc
# Kings
for dr, dc in directions_king:
rr, cc = r+dr, c+dc
if in_bounds(rr, cc):
p = pos.board[rr][cc]
if (p == 'K' and side == WHITE) or (p == 'k' and side == BLACK):
return True
return False
def is_in_check(pos, side):
kpos = king_position(pos, side)
if not kpos:
return False
return attacks_square(pos, kpos[0], kpos[1], opposite(side))
def generate_moves(pos):
# Returns list of moves as tuples: ((r1,c1),(r2,c2),promotion_char_or_None,special)
# special can be 'castle','enpassant', or None
moves = []
side = pos.turn
forward = -1 if side == WHITE else 1
start_rank = 6 if side == WHITE else 1
promotion_rank = 0 if side == WHITE else 7
for r in range(8):
for c in range(8):
p = pos.board[r][c]
if p == '.':
continue
if side == WHITE and not is_white(p):
continue
if side == BLACK and not is_black(p):
continue
if p.upper() == 'P':
# Forward moves
rr = r + forward
if in_bounds(rr, c) and pos.board[rr][c] == '.':
if rr == promotion_rank:
for promo in ('Q','R','B','N'):
moves.append(((r,c),(rr,c), promo if side==WHITE else promo.lower(), None))
else:
moves.append(((r,c),(rr,c), None, None))
# Double move
if r == start_rank:
rr2 = r + 2*forward
if in_bounds(rr2,c) and pos.board[rr2][c] == '.' and pos.board[rr][c] == '.':
moves.append(((r,c),(rr2,c), None, None))
# Captures
for dc in (-1,1):
rr = r + forward
cc = c + dc
if in_bounds(rr,cc):
target = pos.board[rr][cc]
if target != '.' and side_of(target) == opposite(side):
if rr == promotion_rank:
for promo in ('Q','R','B','N'):
moves.append(((r,c),(rr,cc), promo if side==WHITE else promo.lower(), None))
else:
moves.append(((r,c),(rr,cc), None, None))
# En passant
if pos.ep_target:
er, ec = pos.ep_target
if er == r + forward and abs(ec - c) == 1:
if r == (3 if side == WHITE else 4): # ep capture rank
moves.append(((r,c),(er,ec), None, 'enpassant'))
elif p.upper() == 'N':
for dr, dc in [(-2,-1),(-2,1),(-1,-2),(-1,2),(1,-2),(1,2),(2,-1),(2,1)]:
rr, cc = r+dr, c+dc
if in_bounds(rr,cc):
target = pos.board[rr][cc]
if target == '.' or side_of(target) == opposite(side):
moves.append(((r,c),(rr,cc), None, None))
elif p.upper() == 'B':
for dr, dc in [(-1,-1),(-1,1),(1,-1),(1,1)]:
rr, cc = r+dr, c+dc
while in_bounds(rr,cc):
target = pos.board[rr][cc]
if target == '.':
moves.append(((r,c),(rr,cc), None, None))
else:
if side_of(target) == opposite(side):
moves.append(((r,c),(rr,cc), None, None))
break
rr += dr
cc += dc
elif p.upper() == 'R':
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
rr, cc = r+dr, c+dc
while in_bounds(rr,cc):
target = pos.board[rr][cc]
if target == '.':
moves.append(((r,c),(rr,cc), None, None))
else:
if side_of(target) == opposite(side):
moves.append(((r,c),(rr,cc), None, None))
break
rr += dr
cc += dc
elif p.upper() == 'Q':
for dr, dc in [(-1,-1),(-1,1),(1,-1),(1,1),(-1,0),(1,0),(0,-1),(0,1)]:
rr, cc = r+dr, c+dc
while in_bounds(rr,cc):
target = pos.board[rr][cc]
if target == '.':
moves.append(((r,c),(rr,cc), None, None))
else:
if side_of(target) == opposite(side):
moves.append(((r,c),(rr,cc), None, None))
break
rr += dr
cc += dc
elif p.upper() == 'K':
for dr in (-1,0,1):
for dc in (-1,0,1):
if dr == 0 and dc == 0: continue
rr, cc = r+dr, c+dc
if in_bounds(rr,cc):
target = pos.board[rr][cc]
if target == '.' or side_of(target) == opposite(side):
moves.append(((r,c),(rr,cc), None, None))
# Castling
if side == WHITE and p == 'K' and 'K' in pos.castling:
# King side: e1->g1; squares f1,g1 empty; not in check on e1,f1,g1
if pos.board[7][5] == '.' and pos.board[7][6] == '.':
if not attacks_square(pos,7,4,BLACK) and not attacks_square(pos,7,5,BLACK) and not attacks_square(pos,7,6,BLACK):
moves.append(((7,4),(7,6), None, 'castle'))
if side == WHITE and p == 'K' and 'Q' in pos.castling:
if pos.board[7][3] == '.' and pos.board[7][2] == '.' and pos.board[7][1] == '.':
if not attacks_square(pos,7,4,BLACK) and not attacks_square(pos,7,3,BLACK) and not attacks_square(pos,7,2,BLACK):
moves.append(((7,4),(7,2), None, 'castle'))
if side == BLACK and p == 'k' and 'k' in pos.castling:
if pos.board[0][5] == '.' and pos.board[0][6] == '.':
if not attacks_square(pos,0,4,WHITE) and not attacks_square(pos,0,5,WHITE) and not attacks_square(pos,0,6,WHITE):
moves.append(((0,4),(0,6), None, 'castle'))
if side == BLACK and p == 'k' and 'q' in pos.castling:
if pos.board[0][3] == '.' and pos.board[0][2] == '.' and pos.board[0][1] == '.':
if not attacks_square(pos,0,4,WHITE) and not attacks_square(pos,0,3,WHITE) and not attacks_square(pos,0,2,WHITE):
moves.append(((0,4),(0,2), None, 'castle'))
# Filter for legal (king not in check after move)
legal = []
for mv in moves:
npos = make_move(pos, mv)
if not is_in_check(npos, side):
legal.append(mv)
return legal
def make_move(pos, move):
# Move application returns a new Position
(r1,c1), (r2,c2), promo, special = move
side = pos.turn
npos = pos.copy()
piece = npos.board[r1][c1]
target = npos.board[r2][c2]
# Update halfmove clock
if piece.upper() == 'P' or target != '.':
npos.halfmove = 0
else:
npos.halfmove += 1
# Clear en passant by default
npos.ep_target = None
# Move piece
npos.board[r2][c2] = piece
npos.board[r1][c1] = '.'
# Special: en passant capture
if special == 'enpassant':
if side == WHITE:
npos.board[r2+1][c2] = '.'
else:
npos.board[r2-1][c2] = '.'
# Special: promotion
if promo:
npos.board[r2][c2] = promo
# Special: castling move rook
if special == 'castle':
if side == WHITE:
if c2 == 6: # king side
npos.board[7][5] = 'R'
npos.board[7][7] = '.'
else: # queen side
npos.board[7][3] = 'R'
npos.board[7][0] = '.'
else:
if c2 == 6:
npos.board[0][5] = 'r'
npos.board[0][7] = '.'
else:
npos.board[0][3] = 'r'
npos.board[0][0] = '.'
# Set en passant target if double pawn push
if piece.upper() == 'P' and abs(r2 - r1) == 2:
ep_row = (r1 + r2) // 2
npos.ep_target = (ep_row, c1)
# Update castling rights
def remove_castling(side_castles):
npos.castling = ''.join(ch for ch in npos.castling if ch not in side_castles)
# If king moves, remove that side's castling
if piece == 'K':
remove_castling('KQ')
if piece == 'k':
remove_castling('kq')
# If rook moves or is captured, update
if r1 == 7 and c1 == 7 and npos.board[7][7] != 'R': # white h1 rook moved
remove_castling('K')
if r1 == 7 and c1 == 0 and npos.board[7][0] != 'R': # white a1 rook moved
remove_castling('Q')
if r1 == 0 and c1 == 7 and npos.board[0][7] != 'r': # black h8 rook moved
remove_castling('k')
if r1 == 0 and c1 == 0 and npos.board[0][0] != 'r': # black a8 rook moved
remove_castling('q')
# If rook captured
if r2 == 7 and c2 == 7 and target == 'R':
remove_castling('K')
if r2 == 7 and c2 == 0 and target == 'R':
remove_castling('Q')
if r2 == 0 and c2 == 7 and target == 'r':
remove_castling('k')
if r2 == 0 and c2 == 0 and target == 'r':
remove_castling('q')
# Switch turn
npos.turn = opposite(pos.turn)
if npos.turn == WHITE:
npos.fullmove += 1
return npos
def evaluate(pos):
# Material + piece-square tables; perspective: White positive
score = 0
for r in range(8):
for c in range(8):
p = pos.board[r][c]
if p == '.': continue
score += PIECE_VALUES[p]
idx_white = r*8 + c
idx_black = (7-r)*8 + c # mirror for black
if p == 'P':
score += PST_PAWN[idx_white]
elif p == 'p':
score -= PST_PAWN[idx_black]
elif p == 'N':
score += PST_KNIGHT[idx_white]
elif p == 'n':
score -= PST_KNIGHT[idx_black]
elif p == 'B':
score += PST_BISHOP[idx_white]
elif p == 'b':
score -= PST_BISHOP[idx_black]
elif p == 'R':
score += PST_ROOK[idx_white]
elif p == 'r':
score -= PST_ROOK[idx_black]
elif p == 'Q':
score += PST_QUEEN[idx_white]
elif p == 'q':
score -= PST_QUEEN[idx_black]
elif p == 'K':
score += PST_KING_MID[idx_white]
elif p == 'k':
score -= PST_KING_MID[idx_black]
# Mobility
legal = generate_moves(pos)
mob = len(legal) if pos.turn == WHITE else -len(legal)
score += 2 * mob
return score
TT = {} # Transposition table: key -> (depth, score, flag, best_move)
Z_KEYS = [[random.getrandbits(64) for _ in range(12)] for _ in range(64)]
Z_SIDE = random.getrandbits(64)
Z_CASTLE_KEYS = {ch: random.getrandbits(64) for ch in "KQkq"}
Z_EP_KEYS = [[random.getrandbits(64) for _ in range(8)] for _ in range(8)]
PIECE_TO_INDEX = {'P':0,'N':1,'B':2,'R':3,'Q':4,'K':5,'p':6,'n':7,'b':8,'r':9,'q':10,'k':11}
def zobrist_hash(pos):
h = 0
for r in range(8):
for c in range(8):
p = pos.board[r][c]
if p != '.':
h ^= Z_KEYS[r*8+c][PIECE_TO_INDEX[p]]
if pos.turn == BLACK:
h ^= Z_SIDE
for ch in pos.castling:
if ch in Z_CASTLE_KEYS:
h ^= Z_CASTLE_KEYS[ch]
if pos.ep_target:
er, ec = pos.ep_target
h ^= Z_EP_KEYS[er][ec]
return h
def order_moves(pos, moves):
# Simple move ordering: captures first, promotions next, then others; MVV-LVA
def score_mv(mv):
(r1,c1),(r2,c2),promo,special = mv
target = pos.board[r2][c2]
sc = 0
if target != '.':
sc += abs(PIECE_VALUES[target]) - abs(PIECE_VALUES[pos.board[r1][c1]])//10
if promo:
sc += 500
if special == 'castle':
sc += 50
return -sc # sort ascending then
return sorted(moves, key=score_mv)
def minimax(pos, depth, alpha, beta):
# Alpha-beta with transposition table
key = zobrist_hash(pos)
best_move = None
if depth == 0:
return evaluate(pos), None
legal = generate_moves(pos)
if not legal:
# Checkmate or stalemate
if is_in_check(pos, pos.turn):
return (-999999 + (8-depth)) if pos.turn == WHITE else (999999 - (8-depth)), None
else:
return 0, None
# TT lookup
if key in TT:
tt_depth, tt_score, tt_flag, tt_move = TT[key]
if tt_depth >= depth:
if tt_flag == 'EXACT':
return tt_score, tt_move
elif tt_flag == 'ALPHA' and tt_score <= alpha:
return tt_score, tt_move
elif tt_flag == 'BETA' and tt_score >= beta:
return tt_score, tt_move
if tt_move:
# Move ordering: try stored best move first
legal = [tt_move] + [m for m in legal if m != tt_move]
legal = order_moves(pos, legal)
if pos.turn == WHITE:
value = -math.inf
for mv in legal:
child = make_move(pos, mv)
sc, _ = minimax(child, depth-1, alpha, beta)
if sc > value:
value = sc
best_move = mv
alpha = max(alpha, value)
if alpha >= beta:
break
flag = 'EXACT'
if value <= alpha:
flag = 'ALPHA'
elif value >= beta:
flag = 'BETA'
TT[key] = (depth, value, flag, best_move)
return value, best_move
else:
value = math.inf
for mv in legal:
child = make_move(pos, mv)
sc, _ = minimax(child, depth-1, alpha, beta)
if sc < value:
value = sc
best_move = mv
beta = min(beta, value)
if alpha >= beta:
break
flag = 'EXACT'
if value <= alpha:
flag = 'ALPHA'
elif value >= beta:
flag = 'BETA'
TT[key] = (depth, value, flag, best_move)
return value, best_move
class ChessGUI:
def __init__(self, master):
self.master = master
master.title("Python Chess (8-ply AI)")
self.canvas = tk.Canvas(master, width=8*SQUARE_SIZE, height=8*SQUARE_SIZE)
self.canvas.pack()
self.status = tk.StringVar()
self.status_label = tk.Label(master, textvariable=self.status, font=("Arial", 12))
self.status_label.pack(pady=4)
self.canvas.bind("<Button-1>", self.on_click)
self.pos = parse_fen(START_FEN)
self.selected = None
self.legal_moves_for_selected = []
self.game_over = False
self.ai_depth = 8 # fixed per request
self.human_side = WHITE # human plays white by default
self.draw_board()
self.update_status()
def draw_board(self):
self.canvas.delete("all")
for r in range(8):
for c in range(8):
x1 = c*SQUARE_SIZE
y1 = r*SQUARE_SIZE
x2 = x1 + SQUARE_SIZE
y2 = y1 + SQUARE_SIZE
color = BOARD_COLOR_LIGHT if (r+c) % 2 == 0 else BOARD_COLOR_DARK
self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="")
if self.selected == (r, c):
self.canvas.create_rectangle(x1, y1, x2, y2, fill=HIGHLIGHT_COLOR, outline="")
piece = self.pos.board[r][c]
if piece != '.':
# Choose color for piece
fill = "#333" if is_black(piece) else "#EEE"
# Draw Unicode piece
self.canvas.create_text(x1+SQUARE_SIZE/2, y1+SQUARE_SIZE/2,
text=UNICODE_MAP[piece], font=("Arial", 32), fill=fill)
# Highlight legal targets for selected
for mv in self.legal_moves_for_selected:
(_, _), (r2,c2), _, _ = mv
x1 = c2*SQUARE_SIZE
y1 = r2*SQUARE_SIZE
x2 = x1 + SQUARE_SIZE
y2 = y1 + SQUARE_SIZE
self.canvas.create_rectangle(x1, y1, x2, y2, outline="#FFD700", width=3)
def on_click(self, event):
if self.game_over:
return
c = event.x // SQUARE_SIZE
r = event.y // SQUARE_SIZE
if not in_bounds(r,c): return
if self.pos.turn != self.human_side:
return
piece = self.pos.board[r][c]
# If selecting a piece to move
if self.selected is None:
if piece != '.' and ((self.human_side == WHITE and is_white(piece)) or (self.human_side == BLACK and is_black(piece))):
self.selected = (r,c)
self.legal_moves_for_selected = [mv for mv in generate_moves(self.pos) if mv[0] == (r,c)]
else:
self.selected = None
self.legal_moves_for_selected = []
else:
# Try to make a move to clicked square
target_moves = [mv for mv in self.legal_moves_for_selected if mv[1] == (r,c)]
if target_moves:
# If multiple (promotion), pick queen by default; allow hold Shift for knight? Keep simple: pick first
mv = target_moves[0]
self.pos = make_move(self.pos, mv)
self.selected = None
self.legal_moves_for_selected = []
self.draw_board()
self.update_status()
self.check_end()
if not self.game_over:
self.master.after(50, self.ai_move) # AI responds
else:
# Reselect if clicked own piece; otherwise clear
if piece != '.' and ((self.human_side == WHITE and is_white(piece)) or (self.human_side == BLACK and is_black(piece))):
self.selected = (r,c)
self.legal_moves_for_selected = [mv for mv in generate_moves(self.pos) if mv[0] == (r,c)]
else:
self.selected = None
self.legal_moves_for_selected = []
self.draw_board()
def ai_move(self):
if self.game_over:
return
start = time.time()
score, best = minimax(self.pos, self.ai_depth, -math.inf, math.inf)
elapsed = time.time() - start
if best is None:
# No legal move
self.check_end()
return
self.pos = make_move(self.pos, best)
self.draw_board()
self.status.set(f"AI moved. Eval: {score/100:.2f}. Time: {elapsed:.2f}s")
self.check_end()
def update_status(self):
side = "White" if self.pos.turn == WHITE else "Black"
self.status.set(f"Turn: {side} — FEN: {to_fen(self.pos)}")
def check_end(self):
legal = generate_moves(self.pos)
if legal:
return
side = self.pos.turn
if is_in_check(self.pos, side):
self.game_over = True
winner = "Black" if side == WHITE else "White"
messagebox.showinfo("Game Over", f"Checkmate. {winner} wins.")
else:
self.game_over = True
messagebox.showinfo("Game Over", "Stalemate.")
def main():
root = tk.Tk()
app = ChessGUI(root)
root.mainloop()
if __name__ == "__main__":
main()
No comments:
Post a Comment