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..4b47051 100644
--- a/data/classes/Board.py
+++ b/data/classes/Board.py
@@ -9,13 +9,14 @@
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.config = [
['bR', 'bN', 'bB', 'bQ', 'bK', 'bB', 'bN', 'bR'],
@@ -31,6 +32,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 +103,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,6 +119,30 @@ 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)]
output = False
@@ -186,6 +215,48 @@ def get_piece_from_pos(self, pos):
return self.get_square_from_pos(pos).occupying_piece
+ 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 draw(self, display):
if self.selected_piece is not None:
self.get_square_from_pos(self.selected_piece.pos).highlight = True
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/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..550d4a2
--- /dev/null
+++ b/data/classes/chess_bot/evaluate.py
@@ -0,0 +1,2 @@
+def evaluate():
+ pass
\ 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..feac52a
--- /dev/null
+++ b/data/classes/chess_bot/precompute.py
@@ -0,0 +1,27 @@
+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()
\ No newline at end of file
diff --git a/data/classes/chess_bot/search.py b/data/classes/chess_bot/search.py
new file mode 100644
index 0000000..34b5f72
--- /dev/null
+++ b/data/classes/chess_bot/search.py
@@ -0,0 +1,2 @@
+def negamax():
+ pass
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/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..80dcfea
--- /dev/null
+++ b/data/states/PvEState.py
@@ -0,0 +1,81 @@
+import pygame
+import random
+from data.states.State import State
+from data.classes.Board import Board
+
+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):
+ # TODO: Add Model prediction here.
+ # Integrate the AI model to calculate and return the best move on self.board.
+
+ bot_color = 'white' if self.player_color == 'black' else 'black'
+
+ # --- Temporarily use a random move to prevent soft locks during testing ---
+ # NOTE: Remove this stubbed logic once the model provides a move!
+ valid_moves = []
+ for square in self.board.squares:
+ piece = square.occupying_piece
+ if piece and piece.color == bot_color:
+ moves = piece.get_valid_moves(self.board)
+ for move_square in moves:
+ valid_moves.append((piece, move_square))
+
+ if valid_moves:
+ piece, move_square = random.choice(valid_moves)
+ # The .move() function returns True if valid execution
+ if piece.move(self.board, move_square):
+ # Switch turn back to the player manually after bot hits move
+ self.board.turn = self.player_color
+ else:
+ # No valid moves edge case (Should be covered by checkmate checks normally)
+ pass
+
+ 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