From 29b70f2d49df7142bfcb06eb1e9227f777bae328 Mon Sep 17 00:00:00 2001 From: HoangMinhDuc Date: Fri, 17 Apr 2026 14:35:11 +0700 Subject: [PATCH 1/6] add .gitignore to exclude Python cache files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore 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 From 9ddd288fd60b9fa7a01753b693b5f90b3b3a8ce5 Mon Sep 17 00:00:00 2001 From: phamnam-data-ai <24021579@vnu.edu.vn> Date: Fri, 17 Apr 2026 15:47:00 +0700 Subject: [PATCH 2/6] =?UTF-8?q?=C4=91=E1=BB=95i=20k=C3=ADch=20c=E1=BB=A1?= =?UTF-8?q?=20m=C3=A0n=20h=C3=ACnh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 5 +++++ .idea/inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 7 +++++++ .idea/modules.xml | 8 ++++++++ .idea/python-chess.iml | 10 ++++++++++ .idea/vcs.xml | 6 ++++++ main.py | 2 +- 7 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/python-chess.iml create mode 100644 .idea/vcs.xml 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/main.py b/main.py index 4e97ac4..bce38df 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ pygame.init() -WINDOW_SIZE = (1000, 1000) +WINDOW_SIZE = (600, 600) screen = pygame.display.set_mode(WINDOW_SIZE) board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1]) From b6da5eb6fbfd4c9504ab278960d055ac3b612fb5 Mon Sep 17 00:00:00 2001 From: HoangMinhDuc Date: Wed, 22 Apr 2026 14:33:03 +0700 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20th=C3=AAm=20bitboard,=20l=E1=BB=9Bp?= =?UTF-8?q?=20Bot=20v=C3=A0=20m=E1=BB=99t=20s=E1=BB=91=20h=C3=A0m=20h?= =?UTF-8?q?=E1=BB=97=20tr=E1=BB=A3=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/classes/Board.py | 42 ++++++++++++++++++++++++++++ data/classes/chess_bot/Bot.py | 7 +++++ data/classes/chess_bot/constants.py | 21 ++++++++++++++ data/classes/chess_bot/evaluate.py | 2 ++ data/classes/chess_bot/move_gens.py | 27 ++++++++++++++++++ data/classes/chess_bot/precompute.py | 27 ++++++++++++++++++ data/classes/chess_bot/search.py | 2 ++ data/classes/pieces/Bishop.py | 1 + main.py | 37 ++++++++++++++++++++++-- tempCodeRunnerFile.py | 1 + 10 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 data/classes/chess_bot/Bot.py create mode 100644 data/classes/chess_bot/constants.py create mode 100644 data/classes/chess_bot/evaluate.py create mode 100644 data/classes/chess_bot/move_gens.py create mode 100644 data/classes/chess_bot/precompute.py create mode 100644 data/classes/chess_bot/search.py create mode 100644 tempCodeRunnerFile.py diff --git a/data/classes/Board.py b/data/classes/Board.py index 5653d4e..1e8c37d 100644 --- a/data/classes/Board.py +++ b/data/classes/Board.py @@ -186,6 +186,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/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/main.py b/main.py index bce38df..8362427 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ import pygame +from data.classes.chess_bot.constants import * +from data.classes.chess_bot.move_gens import get_knights_moves from data.classes.Board import Board @@ -9,6 +11,29 @@ board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1]) +# debug function to visualize bitboards in the console +def print_bitboards(bitboard): + print("\n A B C D E F G H") + print(" ---------------") + + for rank in range(7, -1, -1): + row_string = f"{rank + 1}|" + + for file in range(8): + square_index = (rank * 8) + file + + # check if the bit at this index is 1 + # do this by creating a temporary board with a 1 at the target square, + # using Bitwise AND to see if they overlap. + if bitboard & (1 << square_index): + row_string += "1 " # Piece exists + else: + row_string += ". " # Empty square + + print(row_string) + + print(" ---------------\n") + def draw(display): display.fill('white') @@ -16,7 +41,6 @@ def draw(display): pygame.display.update() - running = True while running: mx, my = pygame.mouse.get_pos() @@ -27,7 +51,16 @@ def draw(display): elif event.type == pygame.MOUSEBUTTONDOWN: if event.button == 1: board.handle_click(mx, my) - + + # debug + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_d: + bitboards = board.get_bitboards() + knight_test_moves = bitboards[W_KNIGHT] + for move in get_knights_moves(bitboards, WHITE): + knight_test_moves |= (1 << move[1]) + print_bitboards(knight_test_moves) + if board.is_in_checkmate('black'): print('White wins!') running = False 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 From 51d1fbfa563f29b16e07cd46712b5e14b8b072cc Mon Sep 17 00:00:00 2001 From: minhcaoj <87977362+minhcaoj@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:16:08 +0700 Subject: [PATCH 4/6] fix: fix window size --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index bce38df..4e97ac4 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ pygame.init() -WINDOW_SIZE = (600, 600) +WINDOW_SIZE = (1000, 1000) screen = pygame.display.set_mode(WINDOW_SIZE) board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1]) From 0e1123ea8594590f632ef5034dd11f0808206e1c Mon Sep 17 00:00:00 2001 From: minhcaoj <87977362+minhcaoj@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:16:54 +0700 Subject: [PATCH 5/6] fix: window size --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 4e97ac4..bce38df 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ pygame.init() -WINDOW_SIZE = (1000, 1000) +WINDOW_SIZE = (600, 600) screen = pygame.display.set_mode(WINDOW_SIZE) board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1]) From 61aa33fb84a3834f578d09a2cb15f6d69a353f4d Mon Sep 17 00:00:00 2001 From: minhcaoj <87977362+minhcaoj@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:48:49 +0700 Subject: [PATCH 6/6] feat: add UI --- data/classes/Board.py | 31 +++++++++++++- data/classes/Button.py | 28 ++++++++++++ data/classes/Square.py | 14 ++++++ data/classes/StateManager.py | 25 +++++++++++ data/states/MenuState.py | 21 +++++++++ data/states/PvEState.py | 81 +++++++++++++++++++++++++++++++++++ data/states/PvPState.py | 30 +++++++++++++ data/states/State.py | 15 +++++++ main.py | 82 ++++++++++++------------------------ 9 files changed, 270 insertions(+), 57 deletions(-) create mode 100644 data/classes/Button.py create mode 100644 data/classes/StateManager.py create mode 100644 data/states/MenuState.py create mode 100644 data/states/PvEState.py create mode 100644 data/states/PvPState.py create mode 100644 data/states/State.py diff --git a/data/classes/Board.py b/data/classes/Board.py index 1e8c37d..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 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/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 8362427..1b739b8 100644 --- a/main.py +++ b/main.py @@ -1,71 +1,41 @@ import pygame -from data.classes.chess_bot.constants import * -from data.classes.chess_bot.move_gens import get_knights_moves +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 = (600, 600) screen = pygame.display.set_mode(WINDOW_SIZE) +pygame.display.set_caption("Chess AI") -board = Board(WINDOW_SIZE[0], WINDOW_SIZE[1]) - -# debug function to visualize bitboards in the console -def print_bitboards(bitboard): - print("\n A B C D E F G H") - print(" ---------------") - - for rank in range(7, -1, -1): - row_string = f"{rank + 1}|" - - for file in range(8): - square_index = (rank * 8) + file - - # check if the bit at this index is 1 - # do this by creating a temporary board with a 1 at the target square, - # using Bitwise AND to see if they overlap. - if bitboard & (1 << square_index): - row_string += "1 " # Piece exists - else: - row_string += ". " # Empty square - - print(row_string) - - print(" ---------------\n") - -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 - - elif event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 1: - board.handle_click(mx, my) - - # debug - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_d: - bitboards = board.get_bitboards() - knight_test_moves = bitboards[W_KNIGHT] - for move in get_knights_moves(bitboards, WHITE): - knight_test_moves |= (1 << move[1]) - print_bitboards(knight_test_moves) + + manager.handle_events(events) + manager.update() + + manager.draw(screen) + pygame.display.update() - if board.is_in_checkmate('black'): - print('White wins!') - running = False - elif board.is_in_checkmate('white'): - print('Black wins!') - running = False + clock.tick(60) - draw(screen) \ No newline at end of file +pygame.quit() +sys.exit() \ No newline at end of file