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

Support Vector Machines in Machine Learning

Support Vector Machines in Machine Learning Introduction Support Vector Machines (SVMs) are powerful supervised learning algorithms used ...