Tuesday, November 25, 2025

Tahsin's Python Chess Software with Tkinter and 4-ply Minimax (Alpha-beta)

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

Mini RDBMS (with persistent storage) using only Python Standard Library

Mini RDBMS (with persistent storage) using only the Python Standard Library import re import json import os from typing import Any, Dict, Li...