diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ab343b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+_pycache_/
+*.py[cod]
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..8f8921a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..a86a030
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/python-chess.iml b/.idea/python-chess.iml
new file mode 100644
index 0000000..db9fab9
--- /dev/null
+++ b/.idea/python-chess.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/classes/Board.py b/data/classes/Board.py
index 5653d4e..c9c7634 100644
--- a/data/classes/Board.py
+++ b/data/classes/Board.py
@@ -9,13 +9,15 @@
from data.classes.pieces.Pawn import Pawn
class Board:
- def __init__(self, width, height):
+ def __init__(self, width, height, is_flipped=False):
self.width = width
self.height = height
self.square_width = width // 8
self.square_height = height // 8
self.selected_piece = None
self.turn = 'white'
+ self.is_flipped = is_flipped
+ self.en_passant_target = None
self.config = [
['bR', 'bN', 'bB', 'bQ', 'bK', 'bB', 'bN', 'bR'],
@@ -31,6 +33,8 @@ def __init__(self, width, height):
self.squares = self.generate_squares()
self.setup_board()
+ if self.is_flipped:
+ self.apply_view(True)
def generate_squares(self):
output = []
@@ -100,6 +104,8 @@ def setup_board(self):
def handle_click(self, mx, my):
x = mx // self.square_width
y = my // self.square_height
+ if x < 0 or x > 7 or y < 0 or y > 7:
+ return
clicked_square = self.get_square_from_pos((x, y))
if self.selected_piece is None:
@@ -114,8 +120,35 @@ def handle_click(self, mx, my):
if clicked_square.occupying_piece.color == self.turn:
self.selected_piece = clicked_square.occupying_piece
+ def handle_click_flipped(self, mx, my):
+ x = 7 - (mx // self.square_width)
+ y = 7 - (my // self.square_height)
+ if x < 0 or x > 7 or y < 0 or y > 7:
+ return
+ clicked_square = self.get_square_from_pos((x, y))
+
+ if self.selected_piece is None:
+ if clicked_square.occupying_piece is not None:
+ if clicked_square.occupying_piece.color == self.turn:
+ self.selected_piece = clicked_square.occupying_piece
+
+ elif self.selected_piece.move(self, clicked_square):
+ self.turn = 'white' if self.turn == 'black' else 'black'
+
+ elif clicked_square.occupying_piece is not None:
+ if clicked_square.occupying_piece.color == self.turn:
+ self.selected_piece = clicked_square.occupying_piece
+
+ def apply_view(self, is_flipped):
+ self.is_flipped = is_flipped
+ for square in self.squares:
+ square.set_view(self.is_flipped)
+
- def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2, y2)]
+ def is_in_check(self, color=None, board_change=None): # board_change = [(x1, y1), (x2, y2)]
+ if color == None:
+ color = self.turn
+
output = False
king_pos = None
@@ -123,6 +156,8 @@ def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2
old_square = None
new_square = None
new_square_old_piece = None
+ en_passant_captured_square = None
+ en_passant_captured_piece = None
if board_change is not None:
for square in self.squares:
@@ -136,6 +171,15 @@ def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2
new_square_old_piece = new_square.occupying_piece
new_square.occupying_piece = changing_piece
+ # Simulate en passant capture by temporarily removing the captured pawn.
+ if changing_piece is not None and changing_piece.notation == ' ' and self.en_passant_target is not None:
+ if board_change[1] == self.en_passant_target and new_square_old_piece is None and old_square.x != new_square.x:
+ capture_y = new_square.y + (1 if changing_piece.color == 'white' else -1)
+ if 0 <= capture_y < 8:
+ en_passant_captured_square = self.get_square_from_pos((new_square.x, capture_y))
+ en_passant_captured_piece = en_passant_captured_square.occupying_piece
+ en_passant_captured_square.occupying_piece = None
+
pieces = [
i.occupying_piece for i in self.squares if i.occupying_piece is not None
]
@@ -157,6 +201,8 @@ def is_in_check(self, color, board_change=None): # board_change = [(x1, y1), (x2
if board_change is not None:
old_square.occupying_piece = changing_piece
new_square.occupying_piece = new_square_old_piece
+ if en_passant_captured_square is not None:
+ en_passant_captured_square.occupying_piece = en_passant_captured_piece
return output
@@ -185,6 +231,167 @@ def get_square_from_pos(self, pos):
def get_piece_from_pos(self, pos):
return self.get_square_from_pos(pos).occupying_piece
+ def get_all_valid_moves(self, color=None):
+ if color == None:
+ color = self.turn
+
+ moves = []
+ for square in self.squares:
+ piece = square.occupying_piece
+ if piece and piece.color == color:
+ for move_square in piece.get_valid_moves(self):
+ moves.append((piece, move_square))
+ return moves
+
+ def is_recapturable(self, piece, target_square):
+ """Check if capturing piece can be immediately recaptured by lower-value opponent piece.
+
+ Used by move ordering to avoid scoring bad trades (e.g., Qxp when pawn recaptures).
+
+ Args:
+ piece: The attacking piece about to capture.
+ target_square: The destination square (where capture happens).
+
+ Returns:
+ Tuple (is_recapturable, min_recapture_value):
+ - is_recapturable: True if an opponent piece can immediately recapture
+ - min_recapture_value: Value of lowest-value piece that can recapture (0 if none)
+ """
+ if target_square.occupying_piece is None:
+ return False, 0
+
+ # Get all opponent pieces that can attack this square
+ opponent_color = 'black' if piece.color == 'white' else 'white'
+ min_recapture_value = float('inf')
+ can_recapture = False
+
+ # Check all opponent pieces
+ for square in self.squares:
+ opponent_piece = square.occupying_piece
+ if opponent_piece and opponent_piece.color == opponent_color:
+ # Check if this opponent piece can attack target_square
+ for attacked_square in opponent_piece.get_moves(self):
+ if attacked_square.pos == target_square.pos:
+ # This opponent piece can recapture
+ can_recapture = True
+ # Get piece value
+ piece_type = opponent_piece.notation
+ value_map = {' ': 1, 'N': 3, 'B': 3, 'R': 5, 'Q': 9, 'K': 100}
+ value = value_map.get(piece_type, 0)
+ min_recapture_value = min(min_recapture_value, value)
+ break
+
+ return can_recapture, min_recapture_value if can_recapture else 0
+
+ def get_bitboards(self):
+ bitboards = {
+ 'P': 0, 'N': 0, 'B': 0, 'R': 0, 'Q': 0, 'K': 0, # White pieces
+ 'p': 0, 'n': 0, 'b': 0, 'r': 0, 'q': 0, 'k': 0 # Black pieces
+ }
+
+ for square in self.squares:
+ piece = square.occupying_piece
+ if piece is not None:
+ # Pygame y=0 is Rank 8, y=7 is Rank 1.
+ # Invert y so index 0 is A1 and index 63 is H8 (Little Edian Rank File mapping).
+ lerf_index = (7 - square.y) * 8 + square.x
+
+ # Safely get the piece type by its class name
+ piece_type = piece.__class__.__name__
+
+ # Map the class name to standard FIDE notation
+ symbol_map = {
+ 'Pawn': 'P', 'Knight': 'N', 'Bishop': 'B',
+ 'Rook': 'R', 'Queen': 'Q', 'King': 'K'
+ }
+ symbol = symbol_map.get(piece_type)
+
+ # Convert to lowercase if the piece is black
+ if piece.color == 'black':
+ symbol = symbol.lower()
+
+ # Stamp this piece onto its specific bitboard using Bitwise OR
+ # Example: 0001, shift by the lerf index 3 to 1000 and OR 0100 to get 1100
+ bitboards[symbol] |= (1 << lerf_index)
+
+ # Generate the summary bitboards for quick AI lookups
+ bitboards['white_pieces'] = (bitboards['P'] | bitboards['N'] | bitboards['B'] |
+ bitboards['R'] | bitboards['Q'] | bitboards['K'])
+
+ bitboards['black_pieces'] = (bitboards['p'] | bitboards['n'] | bitboards['b'] |
+ bitboards['r'] | bitboards['q'] | bitboards['k'])
+
+ bitboards['occupied_squares'] = bitboards['white_pieces'] | bitboards['black_pieces']
+
+ return bitboards
+
+ def capture_move_state(self, piece, target_square):
+ """Capture all board state before executing a move.
+
+ Used by search algorithms (e.g., alpha-beta minimax) to enable move/unmake cycles.
+ Must be called BEFORE piece.move() executes.
+
+ Args:
+ piece: The piece about to move.
+ target_square: The destination square.
+
+ Returns:
+ dict with keys: from_pos, to_pos, piece_had_moved_before, captured_piece,
+ captured_pos, en_passant_target_before, is_promotion, is_castling,
+ rook_state (for castling only)
+ """
+ from_square = self.get_square_from_pos(piece.pos)
+ captured_piece = target_square.occupying_piece
+ captured_pos = target_square.pos
+
+ # Handle en passant: captured piece is on different square
+ if (piece.notation == ' ' and self.en_passant_target is not None and
+ target_square.pos == self.en_passant_target and
+ captured_piece is None and from_square.x != target_square.x):
+ capture_y = target_square.y + (1 if piece.color == 'white' else -1)
+ if 0 <= capture_y < 8:
+ en_passant_square = self.get_square_from_pos((target_square.x, capture_y))
+ captured_piece = en_passant_square.occupying_piece
+ captured_pos = en_passant_square.pos
+
+ # Detect if this is a promotion
+ is_promotion = (piece.notation == ' ' and (target_square.y == 0 or target_square.y == 7))
+
+ # Detect if this is castling and snapshot rook state
+ is_castling = (piece.notation == 'K' and abs(from_square.x - target_square.x) == 2)
+ rook_state = None
+ if is_castling:
+ if from_square.x - target_square.x == 2:
+ # Queenside castling
+ rook = self.get_piece_from_pos((0, piece.y))
+ rook_from = (0, piece.y)
+ rook_to = (3, piece.y)
+ elif from_square.x - target_square.x == -2:
+ # Kingside castling
+ rook = self.get_piece_from_pos((7, piece.y))
+ rook_from = (7, piece.y)
+ rook_to = (5, piece.y)
+
+ if rook is not None:
+ rook_state = {
+ 'rook': rook,
+ 'from_pos': rook_from,
+ 'to_pos': rook_to,
+ 'rook_had_moved_before': rook.has_moved
+ }
+
+ return {
+ 'piece': piece,
+ 'from_pos': piece.pos,
+ 'to_pos': target_square.pos,
+ 'piece_had_moved_before': piece.has_moved,
+ 'captured_piece': captured_piece,
+ 'captured_pos': captured_pos,
+ 'en_passant_target_before': self.en_passant_target,
+ 'is_promotion': is_promotion,
+ 'is_castling': is_castling,
+ 'rook_state': rook_state
+ }
def draw(self, display):
if self.selected_piece is not None:
diff --git a/data/classes/Button.py b/data/classes/Button.py
new file mode 100644
index 0000000..a466aaf
--- /dev/null
+++ b/data/classes/Button.py
@@ -0,0 +1,28 @@
+import pygame
+
+class Button:
+ def __init__(self, x, y, width, height, text, color=(100, 100, 100), hover_color=(150, 150, 150), text_color=(255, 255, 255)):
+ self.rect = pygame.Rect(x, y, width, height)
+ self.text = text
+ self.color = color
+ self.hover_color = hover_color
+ self.text_color = text_color
+ self.font = pygame.font.SysFont(None, 36)
+ self.is_hovered = False
+
+ def draw(self, surface):
+ mouse_pos = pygame.mouse.get_pos()
+ self.is_hovered = self.rect.collidepoint(mouse_pos)
+
+ current_color = self.hover_color if self.is_hovered else self.color
+ pygame.draw.rect(surface, current_color, self.rect)
+
+ text_surf = self.font.render(self.text, True, self.text_color)
+ text_rect = text_surf.get_rect(center=self.rect.center)
+ surface.blit(text_surf, text_rect)
+
+ def is_clicked(self, event):
+ if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
+ if self.rect.collidepoint(event.pos):
+ return True
+ return False
diff --git a/data/classes/Piece.py b/data/classes/Piece.py
index 7e0ed05..ffde67f 100644
--- a/data/classes/Piece.py
+++ b/data/classes/Piece.py
@@ -15,6 +15,16 @@ def move(self, board, square, force=False):
if square in self.get_valid_moves(board) or force:
prev_square = board.get_square_from_pos(self.pos)
+
+ if self.notation == ' ' and board.en_passant_target is not None:
+ if square.pos == board.en_passant_target and square.occupying_piece is None and prev_square.x != square.x:
+ capture_y = square.y + (1 if self.color == 'white' else -1)
+ if 0 <= capture_y < 8:
+ captured_square = board.get_square_from_pos((square.x, capture_y))
+ captured_piece = captured_square.occupying_piece
+ if captured_piece is not None and captured_piece.notation == ' ' and captured_piece.color != self.color:
+ captured_square.occupying_piece = None
+
self.pos, self.x, self.y = square.pos, square.x, square.y
prev_square.occupying_piece = None
@@ -22,6 +32,11 @@ def move(self, board, square, force=False):
board.selected_piece = None
self.has_moved = True
+ board.en_passant_target = None
+ if self.notation == ' ' and abs(prev_square.y - self.y) == 2:
+ middle_y = (prev_square.y + self.y) // 2
+ board.en_passant_target = (self.x, middle_y)
+
# Pawn promotion
if self.notation == ' ':
if self.y == 0 or self.y == 7:
@@ -47,6 +62,64 @@ def move(self, board, square, force=False):
return False
+ def unmake_move(self, board, move_state):
+ """Reverse all state changes from a move.
+
+ Used by search algorithms to explore move trees via make/unmake cycles.
+ Restores: piece position, has_moved flag, captures, en passant, promotion, castling.
+
+ Args:
+ board: The board object.
+ move_state: Dict returned by board.capture_move_state() before the move.
+ """
+ # Restore piece position
+ self.pos = move_state['from_pos']
+ self.x = move_state['from_pos'][0]
+ self.y = move_state['from_pos'][1]
+
+ # Restore has_moved flag (critical for castling/pawn legality)
+ self.has_moved = move_state['piece_had_moved_before']
+
+ # Restore source and destination squares
+ from_square = board.get_square_from_pos(move_state['from_pos'])
+ to_square = board.get_square_from_pos(move_state['to_pos'])
+
+ # Remove piece from destination
+ to_square.occupying_piece = None
+
+ # Place piece back on source square
+ from_square.occupying_piece = self
+
+ # Restore captured piece (if any)
+ if move_state['captured_piece'] is not None:
+ captured_square = board.get_square_from_pos(move_state['captured_pos'])
+ captured_square.occupying_piece = move_state['captured_piece']
+
+ # Restore en passant target state
+ board.en_passant_target = move_state['en_passant_target_before']
+
+ # Handle promotion: replace promoted piece back with original pawn
+ if move_state['is_promotion']:
+ from_square.occupying_piece = self
+
+ # Handle castling: recursively unmake rook's move
+ if move_state['is_castling'] and move_state['rook_state'] is not None:
+ rook_state = move_state['rook_state']
+ rook = rook_state['rook']
+ rook.unmake_move(board, {
+ 'piece': rook,
+ 'from_pos': rook_state['from_pos'],
+ 'to_pos': rook_state['to_pos'],
+ 'piece_had_moved_before': rook_state['rook_had_moved_before'],
+ 'captured_piece': None,
+ 'captured_pos': None,
+ 'en_passant_target_before': board.en_passant_target,
+ 'is_promotion': False,
+ 'is_castling': False,
+ 'rook_state': None
+ })
+
+
def get_moves(self, board):
output = []
for direction in self.get_possible_moves(board):
diff --git a/data/classes/Square.py b/data/classes/Square.py
index 4ccb6aa..23a67fb 100644
--- a/data/classes/Square.py
+++ b/data/classes/Square.py
@@ -25,6 +25,20 @@ def __init__(self, x, y, width, height):
self.height
)
+ def set_view(self, is_flipped):
+ draw_x = 7 - self.x if is_flipped else self.x
+ draw_y = 7 - self.y if is_flipped else self.y
+
+ self.abs_x = draw_x * self.width
+ self.abs_y = draw_y * self.height
+ self.abs_pos = (self.abs_x, self.abs_y)
+ self.rect = pygame.Rect(
+ self.abs_x,
+ self.abs_y,
+ self.width,
+ self.height
+ )
+
def get_coord(self):
columns = 'abcdefgh'
diff --git a/data/classes/StateManager.py b/data/classes/StateManager.py
new file mode 100644
index 0000000..4765b0d
--- /dev/null
+++ b/data/classes/StateManager.py
@@ -0,0 +1,25 @@
+class StateManager:
+ def __init__(self):
+ self.states = {}
+ self.current_state = None
+
+ def setup(self, states, initial_state):
+ self.states = states
+ self.change_state(initial_state)
+
+ def change_state(self, state_name):
+ if state_name in self.states:
+ self.current_state = self.states[state_name]
+ self.current_state.on_enter()
+
+ def handle_events(self, events):
+ if self.current_state:
+ self.current_state.handle_events(events)
+
+ def update(self):
+ if self.current_state:
+ self.current_state.update()
+
+ def draw(self, surface):
+ if self.current_state:
+ self.current_state.draw(surface)
diff --git a/data/classes/chess_bot/Bot.py b/data/classes/chess_bot/Bot.py
new file mode 100644
index 0000000..259dd44
--- /dev/null
+++ b/data/classes/chess_bot/Bot.py
@@ -0,0 +1,7 @@
+class Bot:
+ def __init__(self, depth=1, color='white'):
+ pass
+
+ def get_best_move():
+ pass
+
\ No newline at end of file
diff --git a/data/classes/chess_bot/constants.py b/data/classes/chess_bot/constants.py
new file mode 100644
index 0000000..33d90e9
--- /dev/null
+++ b/data/classes/chess_bot/constants.py
@@ -0,0 +1,21 @@
+# Colors
+WHITE = 'white'
+BLACK = 'black'
+
+W_PAWN = 'P'
+W_KNIGHT = 'N'
+W_BISHOP = 'B'
+W_ROOK = 'R'
+W_QUEEN = 'Q'
+W_KING = 'K'
+
+B_PAWN = 'p'
+B_KNIGHT = 'n'
+B_BISHOP = 'b'
+B_ROOK = 'r'
+B_QUEEN = 'q'
+B_KING = 'k'
+
+W_PIECES = 'white_pieces'
+B_PIECES = 'black_pieces'
+OCCUPIED = 'occupied_squares'
\ No newline at end of file
diff --git a/data/classes/chess_bot/evaluate.py b/data/classes/chess_bot/evaluate.py
new file mode 100644
index 0000000..f93fea7
--- /dev/null
+++ b/data/classes/chess_bot/evaluate.py
@@ -0,0 +1,165 @@
+from data.classes.Board import Board
+
+class Evaluate:
+ def __init__(self, board):
+ self.board = board
+ self.pawn_value = 1
+ self.knight_value = 3
+ self.bishop_value = 3
+ self.rook_value = 5
+ self.queen_value = 9
+
+ # Positional heatmaps for opening play (8x8 arrays, indexed [y][x])
+ # y=0 is rank 8 (black's back), y=7 is rank 1 (white's back)
+ # Higher value = more preferred square for piece in opening
+
+ PAWN_HEATMAP = [
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 8: No pawns here
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 7: Promotion imminent (not starting)
+ [2, 2, 3, 3, 3, 3, 2, 2], # Rank 6: Advanced pawns good
+ [2, 2, 3, 4, 4, 3, 2, 2], # Rank 5: Pushing forward
+ [2, 2, 3, 4, 4, 3, 2, 2], # Rank 4: Center good
+ [1, 1, 2, 3, 3, 2, 1, 1], # Rank 3: Starting to push
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 2: Starting position
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 1: No pawns here
+ ]
+
+ KNIGHT_HEATMAP = [
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 8: Starting position
+ [1, 2, 2, 3, 3, 2, 2, 1], # Rank 7: Beginning to centralize
+ [1, 2, 4, 5, 5, 4, 2, 1], # Rank 6: Good outposts
+ [1, 3, 5, 6, 6, 5, 3, 1], # Rank 5: Excellent central squares
+ [1, 3, 5, 6, 6, 5, 3, 1], # Rank 4: Excellent central squares
+ [1, 2, 4, 5, 5, 4, 2, 1], # Rank 3: Development squares (Nf3, Nc3)
+ [1, 2, 2, 3, 3, 2, 2, 1], # Rank 2: Development squares
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 1: Starting position
+ ]
+
+ BISHOP_HEATMAP = [
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 8: Starting position
+ [1, 2, 2, 2, 2, 2, 2, 1], # Rank 7: Developing
+ [1, 2, 3, 3, 3, 3, 2, 1], # Rank 6: Active squares
+ [1, 2, 3, 4, 4, 3, 2, 1], # Rank 5: Long diagonal activation
+ [1, 2, 3, 4, 4, 3, 2, 1], # Rank 4: Long diagonal activation
+ [1, 2, 3, 4, 4, 3, 2, 1], # Rank 3: Development (Bc4, Bf4)
+ [1, 2, 3, 3, 3, 3, 2, 1], # Rank 2: Starting squares
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 1: Starting position
+ ]
+
+ ROOK_HEATMAP = [
+ [2, 2, 2, 3, 3, 2, 2, 2], # Rank 8: Back rank, central files better
+ [2, 2, 2, 3, 3, 2, 2, 2], # Rank 7
+ [1, 2, 3, 3, 3, 3, 2, 1], # Rank 6: Semi-open files good
+ [1, 2, 3, 3, 3, 3, 2, 1], # Rank 5: Active play
+ [1, 2, 3, 3, 3, 3, 2, 1], # Rank 4: Active play
+ [1, 1, 2, 2, 2, 2, 1, 1], # Rank 3: Still developing
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 2: Back rank
+ [2, 2, 2, 3, 3, 2, 2, 2], # Rank 1: Starting position
+ ]
+
+ QUEEN_HEATMAP = [
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 8: Starting position
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 7
+ [1, 1, 2, 2, 2, 2, 1, 1], # Rank 6: Moderate activation
+ [1, 1, 2, 3, 3, 2, 1, 1], # Rank 5: Center-biased
+ [1, 1, 2, 3, 3, 2, 1, 1], # Rank 4: Center-biased
+ [1, 1, 2, 2, 2, 2, 1, 1], # Rank 3: Development (Qd3, Qe3 in some lines)
+ [1, 1, 1, 2, 2, 1, 1, 1], # Rank 2: Flexible
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 1: Starting position
+ ]
+
+ KING_HEATMAP = [
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 8: Starting position
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 7
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 6
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 5: Avoid center (exposed)
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 4: Avoid center (exposed)
+ [0, 0, 0, 0, 0, 0, 0, 0], # Rank 3: Avoid center (exposed)
+ [1, 1, 1, 1, 1, 1, 1, 1], # Rank 2: Back rank is safe
+ [1, 1, 1, 2, 2, 1, 2, 2], # Rank 1: Kingside castled (g1) better than center
+ ]
+
+ def countMaterial(self, color):
+ material = 0
+ if(color == 'w'):
+ material = material + bin(self.board.get_bitboards()['P']).count('1')
+ material = material + bin(self.board.get_bitboards()['N']).count('1')
+ material = material + bin(self.board.get_bitboards()['B']).count('1')
+ material = material + bin(self.board.get_bitboards()['R']).count('1')
+ material = material + bin(self.board.get_bitboards()['Q']).count('1')
+
+ else:
+ material = material + bin(self.board.get_bitboards()['p']).count('1')
+ material = material + bin(self.board.get_bitboards()['n']).count('1')
+ material = material + bin(self.board.get_bitboards()['b']).count('1')
+ material = material + bin(self.board.get_bitboards()['r']).count('1')
+ material = material + bin(self.board.get_bitboards()['q']).count('1')
+ return material
+
+
+ def get_positional_score(self):
+ """Calculate positional bonuses based on piece placement on preferred squares.
+
+ Returns:
+ Positional score (positive for white advantage, negative for black).
+ Magnitude: 0-500 (doesn't overwhelm material evaluation).
+ """
+ positional_score = 0
+
+ for square in self.board.squares:
+ piece = square.occupying_piece
+ if piece is None:
+ continue
+
+ # Get the heatmap for this piece type
+ piece_notation = piece.notation
+ y, x = square.y, square.x
+
+ if piece_notation == ' ': # Pawn
+ heatmap = self.PAWN_HEATMAP
+ base_value = 1
+ elif piece_notation == 'N': # Knight
+ heatmap = self.KNIGHT_HEATMAP
+ base_value = 1
+ elif piece_notation == 'B': # Bishop
+ heatmap = self.BISHOP_HEATMAP
+ base_value = 1
+ elif piece_notation == 'R': # Rook
+ heatmap = self.ROOK_HEATMAP
+ base_value = 1
+ elif piece_notation == 'Q': # Queen
+ heatmap = self.QUEEN_HEATMAP
+ base_value = 1
+ elif piece_notation == 'K': # King
+ heatmap = self.KING_HEATMAP
+ base_value = 1
+ else:
+ continue
+
+ # Get the positional value for this square
+ square_value = heatmap[y][x] * base_value
+
+ # Add to score (positive for white, negative for black)
+ if piece.color == 'white':
+ positional_score += square_value
+ else:
+ positional_score -= square_value
+
+ return positional_score
+
+ def evaluate(self):
+ black_eval = self.countMaterial('b')
+ white_eval = self.countMaterial('w')
+
+ material_eval = white_eval - black_eval
+ positional_eval = self.get_positional_score()
+
+ # Combine material (weighted heavily) and positional evaluation
+ # Material: ~30-39 points total, Positional: 0-500 but usually 20-100 in opening
+ total_eval = material_eval * 10 + positional_eval
+
+ perspective = 1 if self.board.turn == 'white' else -1
+
+ return total_eval * perspective
+
+
\ No newline at end of file
diff --git a/data/classes/chess_bot/move_gens.py b/data/classes/chess_bot/move_gens.py
new file mode 100644
index 0000000..543164f
--- /dev/null
+++ b/data/classes/chess_bot/move_gens.py
@@ -0,0 +1,27 @@
+from .constants import *
+from .precompute import KNIGHT_MOVES, KING_MOVES
+
+# Returns the indices of the set bits in a bitboard
+# Example: 0b1010 -> yields 1 and 3 (0-indexed from the right)
+def get_set_bits(bitboard):
+ while bitboard:
+ lsb = bitboard & -bitboard
+ yield lsb.bit_length() - 1
+ bitboard &= bitboard - 1
+
+def get_knights_moves(bitboards, color):
+ moves = []
+ if color == WHITE:
+ knights = bitboards[W_KNIGHT]
+ friendly_pieces = bitboards[W_PIECES]
+ else:
+ knights = bitboards[B_KNIGHT]
+ friendly_pieces = bitboards[B_PIECES]
+
+ for knight in get_set_bits(knights):
+ raw_moves = KNIGHT_MOVES[knight]
+ legal_moves = raw_moves & ~friendly_pieces
+
+ for target in get_set_bits(legal_moves):
+ moves.append((knight, target))
+ return moves
\ No newline at end of file
diff --git a/data/classes/chess_bot/precompute.py b/data/classes/chess_bot/precompute.py
new file mode 100644
index 0000000..85e6417
--- /dev/null
+++ b/data/classes/chess_bot/precompute.py
@@ -0,0 +1,28 @@
+NOT_A_FILE = 0xFEFEFEFEFEFEFEFE # A-file bits are 0, everywhere else is 1
+NOT_AB_FILE = 0xFCFCFCFCFCFCFCFC # A and B files are 0
+NOT_H_FILE = 0x7F7F7F7F7F7F7F7F # H-file bits are 0
+NOT_GH_FILE = 0x3F3F3F3F3F3F3F3F # G and H files are 0
+
+def precomputed_knight_moves():
+ moves = []
+ for square in range(64):
+ knight = 1 << square
+ board = 0
+ board |= (knight & NOT_A_FILE) << 15 # Move 2 up, 1 left
+ board |= (knight & NOT_H_FILE) << 17 # Move 2 up, 1 right
+ board |= (knight & NOT_AB_FILE) << 6 # Move 1 up, 2 left
+ board |= (knight & NOT_GH_FILE) << 10 # Move 1 up, 2 right
+ board |= (knight & NOT_A_FILE) >> 17 # Move 2 down, 1 left
+ board |= (knight & NOT_H_FILE) >> 15 # Move 2 down, 1 right
+ board |= (knight & NOT_AB_FILE) >> 10 # Move 1 down, 2 left
+ board |= (knight & NOT_GH_FILE) >> 6 # Move 1 down, 2 right
+ moves.append(board)
+ return moves
+
+def precomputed_king_moves():
+ moves = []
+ return moves
+
+KNIGHT_MOVES = precomputed_knight_moves()
+KING_MOVES = precomputed_king_moves()
+
diff --git a/data/classes/chess_bot/search.py b/data/classes/chess_bot/search.py
new file mode 100644
index 0000000..01ac90d
--- /dev/null
+++ b/data/classes/chess_bot/search.py
@@ -0,0 +1,167 @@
+from data.classes.chess_bot.evaluate import Evaluate
+
+def get_piece_value(piece):
+ """Return material value of a piece for move ordering."""
+ if piece is None:
+ return 0
+
+ piece_type = piece.notation
+ if piece_type == ' ':
+ return 1
+ elif piece_type == 'N':
+ return 3
+ elif piece_type == 'B':
+ return 3
+ elif piece_type == 'R':
+ return 5
+ elif piece_type == 'Q':
+ return 9
+ return 0
+
+
+def order_moves(board, legal_moves, evaluator=None):
+ """Score and sort moves for alpha-beta pruning efficiency.
+
+ Scoring heuristic:
+ - Captures: value_of_captured_piece * 10 - recapture_penalty
+ - Promotions: +8 (pawn promotion is excellent)
+ - Checks: +1 (forcing moves)
+ - Quiet moves: 0
+
+ Args:
+ board: Board object.
+ legal_moves: List of (piece, move_square) tuples.
+ evaluator: Evaluate object (optional).
+
+ Returns:
+ List of (piece, move_square) tuples sorted by score (descending).
+ """
+ if evaluator is None:
+ evaluator = Evaluate(board)
+
+ scored_moves = []
+
+ for piece, move_square in legal_moves:
+ score = 0
+ captured_piece = move_square.occupying_piece
+
+ # 1. Capture scoring
+ if captured_piece is not None:
+ victim_value = get_piece_value(captured_piece)
+ attacker_value = get_piece_value(piece)
+
+ score += victim_value * 10 - attacker_value
+
+ # 2. Promotion scoring
+ if piece.notation == ' ' and (move_square.y == 0 or move_square.y == 7):
+ score += 8
+
+ # 3. Avoid moving to square that opponent team control
+ is_capturing, min_recapture_value = board.is_recapturable(piece, move_square)
+ if is_capturing and attacker_value > min_recapture_value:
+ score -= get_piece_value(piece)
+
+ scored_moves.append((piece, move_square, score))
+
+
+ # Sort by score descending
+ scored_moves.sort(key=lambda x: x[2], reverse=True)
+
+ # Return without scores
+ return [(piece, move_square) for piece, move_square, score in scored_moves]
+
+
+def alpha_beta_negamax(board, alpha, beta, depth, evaluator=None):
+ """Alpha-beta minimax search with make/unmake move cycle.
+
+ Args:
+ board: Board object with get_all_valid_moves() and capture_move_state().
+ alpha: Alpha cutoff value.
+ beta: Beta cutoff value.
+ depth: Search depth (0 = leaf node, evaluate position).
+ evaluator: Evaluate object. Created if None.
+
+ Returns:
+ Evaluation score from current position.
+ """
+ if evaluator is None:
+ evaluator = Evaluate(board)
+
+ # Leaf node: evaluate position
+ if depth == 0:
+ return evaluator.evaluate()
+
+ # Get all legal moves for current side
+ legal_moves = board.get_all_valid_moves()
+
+ # Order moves for better alpha-beta pruning
+ legal_moves = order_moves(board, legal_moves, evaluator)
+
+ # Terminal node: checkmate or stalemate
+ if len(legal_moves) == 0:
+ if board.is_in_check():
+ # Checkmate: losing side to move
+ return -10000 - depth # Penalize distant mates
+ else:
+ # Stalemate: draw
+ return 0
+
+ # Alpha-beta minimax loop: make, recurse, unmake for each move
+ for piece, move_square in legal_moves:
+ # 1. Capture board state before move
+ move_state = board.capture_move_state(piece, move_square)
+
+ # 2. Execute move
+ piece.move(board, move_square, force=True)
+
+ # 3. Recurse (negate score for opposite side)
+ value = -alpha_beta_negamax(board, -beta, -alpha, depth - 1, evaluator)
+
+ # 4. Unmake move (restore board to state before move)
+ piece.unmake_move(board, move_state)
+
+ # 5. Alpha-beta pruning
+ alpha = max(alpha, value)
+ if alpha >= beta:
+ break # Beta cutoff: prune remaining moves
+
+ return alpha
+
+
+def find_best_move(board, depth, evaluator=None):
+ """Find the best move for the current position using alpha-beta negamax search.
+
+ Args:
+ board: Board object.
+ depth: Search depth.
+ evaluator: Evaluate object (optional).
+
+ Returns:
+ Tuple (piece, move_square) of the best move, or None if no legal moves.
+ """
+ if evaluator is None:
+ evaluator = Evaluate(board)
+
+ legal_moves = board.get_all_valid_moves()
+ if not legal_moves:
+ return None
+
+ legal_moves = order_moves(board, legal_moves, evaluator)
+
+ best_move = None
+ best_score = -float('inf')
+
+ for piece, move_square in legal_moves:
+ move_state = board.capture_move_state(piece, move_square)
+ piece.move(board, move_square, force=True)
+
+ score = -alpha_beta_negamax(board, -1000000, 1000000, depth - 1, evaluator)
+
+ piece.unmake_move(board, move_state)
+
+ if score > best_score:
+ best_score = score
+ best_move = (piece, move_square)
+
+ return best_move
+
\ No newline at end of file
diff --git a/data/classes/pieces/Bishop.py b/data/classes/pieces/Bishop.py
index 18ef4d2..b095229 100644
--- a/data/classes/pieces/Bishop.py
+++ b/data/classes/pieces/Bishop.py
@@ -13,6 +13,7 @@ def __init__(self, pos, color, board):
self.notation = 'B'
+
def get_possible_moves(self, board):
output = []
diff --git a/data/classes/pieces/King.py b/data/classes/pieces/King.py
index 6a73c87..0eb9a5b 100644
--- a/data/classes/pieces/King.py
+++ b/data/classes/pieces/King.py
@@ -83,14 +83,16 @@ def get_valid_moves(self, board):
for square in self.get_moves(board):
if not board.is_in_check(self.color, board_change=[self.pos, square.pos]):
output.append(square)
-
+
+ # Castling moves with safety validation
if self.can_castle(board) == 'queenside':
- output.append(
- board.get_square_from_pos((self.x - 2, self.y))
- )
+ queenside_dest = board.get_square_from_pos((self.x - 2, self.y))
+ if not board.is_in_check(self.color, board_change=[self.pos, queenside_dest.pos]):
+ output.append(queenside_dest)
+
if self.can_castle(board) == 'kingside':
- output.append(
- board.get_square_from_pos((self.x + 2, self.y))
- )
-
+ kingside_dest = board.get_square_from_pos((self.x + 2, self.y))
+ if not board.is_in_check(self.color, board_change=[self.pos, kingside_dest.pos]):
+ output.append(kingside_dest)
+
return output
diff --git a/data/classes/pieces/Pawn.py b/data/classes/pieces/Pawn.py
index d37b4f7..1716195 100644
--- a/data/classes/pieces/Pawn.py
+++ b/data/classes/pieces/Pawn.py
@@ -46,6 +46,8 @@ def get_moves(self, board):
else:
output.append(square)
+ en_passant_target = board.en_passant_target
+
if self.color == 'white':
if self.x + 1 < 8 and self.y - 1 >= 0:
square = board.get_square_from_pos(
@@ -54,6 +56,8 @@ def get_moves(self, board):
if square.occupying_piece != None:
if square.occupying_piece.color != self.color:
output.append(square)
+ elif en_passant_target is not None and square.pos == en_passant_target:
+ output.append(square)
if self.x - 1 >= 0 and self.y - 1 >= 0:
square = board.get_square_from_pos(
(self.x - 1, self.y - 1)
@@ -61,6 +65,8 @@ def get_moves(self, board):
if square.occupying_piece != None:
if square.occupying_piece.color != self.color:
output.append(square)
+ elif en_passant_target is not None and square.pos == en_passant_target:
+ output.append(square)
elif self.color == 'black':
if self.x + 1 < 8 and self.y + 1 < 8:
@@ -70,6 +76,8 @@ def get_moves(self, board):
if square.occupying_piece != None:
if square.occupying_piece.color != self.color:
output.append(square)
+ elif en_passant_target is not None and square.pos == en_passant_target:
+ output.append(square)
if self.x - 1 >= 0 and self.y + 1 < 8:
square = board.get_square_from_pos(
(self.x - 1, self.y + 1)
@@ -77,6 +85,8 @@ def get_moves(self, board):
if square.occupying_piece != None:
if square.occupying_piece.color != self.color:
output.append(square)
+ elif en_passant_target is not None and square.pos == en_passant_target:
+ output.append(square)
return output
diff --git a/data/states/MenuState.py b/data/states/MenuState.py
new file mode 100644
index 0000000..78ff9b6
--- /dev/null
+++ b/data/states/MenuState.py
@@ -0,0 +1,21 @@
+from data.states.State import State
+from data.classes.Button import Button
+
+class MenuState(State):
+ def __init__(self, manager):
+ super().__init__(manager)
+ # Centered horizontally for a 600x600 window
+ self.btn_pvp = Button(200, 200, 200, 60, "PvP")
+ self.btn_pve = Button(200, 300, 200, 60, "PvE")
+
+ def handle_events(self, events):
+ for event in events:
+ if self.btn_pvp.is_clicked(event):
+ self.manager.change_state('pvp')
+ elif self.btn_pve.is_clicked(event):
+ self.manager.change_state('pve')
+
+ def draw(self, surface):
+ surface.fill((230, 230, 230)) # Light gray background
+ self.btn_pvp.draw(surface)
+ self.btn_pve.draw(surface)
diff --git a/data/states/PvEState.py b/data/states/PvEState.py
new file mode 100644
index 0000000..9056870
--- /dev/null
+++ b/data/states/PvEState.py
@@ -0,0 +1,74 @@
+import pygame
+import random
+from data.states.State import State
+from data.classes.Board import Board
+from data.classes.chess_bot.search import find_best_move
+from data.classes.chess_bot.evaluate import Evaluate
+
+class PvEState(State):
+ def __init__(self, manager):
+ super().__init__(manager)
+ self.board = None
+ self.player_color = None
+ self.game_over = False
+
+ def on_enter(self):
+ # Fresh board and random color assignment on load
+ self.player_color = random.choice(['white', 'black'])
+ self.board = Board(600, 600, is_flipped=(self.player_color == 'black'))
+ self.game_over = False
+ print(f"PvE Started! You are playing as {self.player_color}.")
+
+ def handle_events(self, events):
+ if self.game_over:
+ return
+
+ for event in events:
+ if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
+ # Only let player click if it is currently their turn
+ if self.board.turn == self.player_color:
+ mx, my = event.pos
+ if self.board.is_flipped:
+ self.board.handle_click_flipped(mx, my)
+ else:
+ self.board.handle_click(mx, my)
+
+ def update(self):
+ if self.game_over:
+ return
+
+ if self.board.is_in_checkmate('black'):
+ print('White wins!')
+ self.game_over = True
+ self.manager.change_state('menu')
+ return
+ elif self.board.is_in_checkmate('white'):
+ print('Black wins!')
+ self.game_over = True
+ self.manager.change_state('menu')
+ return
+
+ # If it is not the player's turn, instantly trigger the bot
+ if self.board.turn != self.player_color:
+ self.run_bot_move()
+
+ def run_bot_move(self):
+ """Execute bot move using alpha-beta negamax search."""
+ bot_color = 'white' if self.player_color == 'black' else 'black'
+
+ # Temporarily set turn to bot so search works correctly
+ self.board.turn = bot_color
+
+ # Find best move using alpha-beta search with depth 2
+ evaluator = Evaluate(self.board)
+ best_move = find_best_move(self.board, depth=2, evaluator=evaluator)
+
+ if best_move:
+ piece, move_square = best_move
+ piece.move(self.board, move_square, force=True)
+ # Switch turn back to player
+ self.board.turn = self.player_color
+
+ def draw(self, surface):
+ surface.fill('white')
+ self.board.draw(surface)
diff --git a/data/states/PvPState.py b/data/states/PvPState.py
new file mode 100644
index 0000000..ca9cdab
--- /dev/null
+++ b/data/states/PvPState.py
@@ -0,0 +1,30 @@
+import pygame
+from data.states.State import State
+from data.classes.Board import Board
+
+class PvPState(State):
+ def __init__(self, manager):
+ super().__init__(manager)
+ self.board = None
+
+ def on_enter(self):
+ # We start a fresh board whenever we enter PvP mode
+ self.board = Board(600, 600)
+
+ def handle_events(self, events):
+ for event in events:
+ if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
+ mx, my = event.pos
+ self.board.handle_click(mx, my)
+
+ def update(self):
+ if self.board.is_in_checkmate('black'):
+ print('White wins!')
+ self.manager.change_state('menu')
+ elif self.board.is_in_checkmate('white'):
+ print('Black wins!')
+ self.manager.change_state('menu')
+
+ def draw(self, surface):
+ surface.fill('white')
+ self.board.draw(surface)
diff --git a/data/states/State.py b/data/states/State.py
new file mode 100644
index 0000000..3c33174
--- /dev/null
+++ b/data/states/State.py
@@ -0,0 +1,15 @@
+class State:
+ def __init__(self, manager):
+ self.manager = manager
+
+ def on_enter(self):
+ pass
+
+ def handle_events(self, events):
+ pass
+
+ def update(self):
+ pass
+
+ def draw(self, surface):
+ pass
diff --git a/main.py b/main.py
index 4e97ac4..1b739b8 100644
--- a/main.py
+++ b/main.py
@@ -1,38 +1,41 @@
import pygame
+import sys
-from data.classes.Board import Board
+from data.classes.StateManager import StateManager
+from data.states.MenuState import MenuState
+from data.states.PvPState import PvPState
+from data.states.PvEState import PvEState
pygame.init()
-WINDOW_SIZE = (1000, 1000)
+WINDOW_SIZE = (600, 600)
screen = pygame.display.set_mode(WINDOW_SIZE)
+pygame.display.set_caption("Chess AI")
-board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1])
-
-def draw(display):
- display.fill('white')
-
- board.draw(display)
-
- pygame.display.update()
-
+manager = StateManager()
+states = {
+ 'menu': MenuState(manager),
+ 'pvp': PvPState(manager),
+ 'pve': PvEState(manager)
+}
+manager.setup(states, 'menu')
+clock = pygame.time.Clock()
running = True
+
while running:
- mx, my = pygame.mouse.get_pos()
- for event in pygame.event.get():
+ events = pygame.event.get()
+ for event in events:
if event.type == pygame.QUIT:
running = False
+
+ manager.handle_events(events)
+ manager.update()
+
+ manager.draw(screen)
+ pygame.display.update()
+
+ clock.tick(60)
- elif event.type == pygame.MOUSEBUTTONDOWN:
- if event.button == 1:
- board.handle_click(mx, my)
-
- if board.is_in_checkmate('black'):
- print('White wins!')
- running = False
- elif board.is_in_checkmate('white'):
- print('Black wins!')
- running = False
-
- draw(screen)
\ No newline at end of file
+pygame.quit()
+sys.exit()
\ No newline at end of file
diff --git a/tempCodeRunnerFile.py b/tempCodeRunnerFile.py
new file mode 100644
index 0000000..8096b59
--- /dev/null
+++ b/tempCodeRunnerFile.py
@@ -0,0 +1 @@
+from data.classes.chess_bot.constants import *
\ No newline at end of file