diff --git a/Halma/halma.py b/Halma/halma.py index 839330e..c2f7cf2 100644 --- a/Halma/halma.py +++ b/Halma/halma.py @@ -93,6 +93,8 @@ class Halma: self.setup_board() # Show the UI self.create_ui() + # Start the game loop + self.handle_turn() def setup_board(self): # Determine the number of rows/columns for the goal area based on the board size @@ -176,45 +178,41 @@ class Halma: self.canvas.create_oval(x1 + 5, y1 + 5, x2 - 5, y2 - 5, fill=PLAYER_2_COLOR) def on_click(self, event): - # Bound function to the left mouse button, controls the game - # Calculate where the click happened + # calculates the clicked position on the board clicked_column = (event.x - 50) // self.cell_size clicked_row = (event.y - 50) // self.cell_size - # Ensure the click is within the board boundaries + # ensures the click is within the board boundaries if not (0 <= clicked_column < self.board_size and 0 <= clicked_row < self.board_size): return - # Check if the clicked piece belongs to the current player - if not hasattr(self, "selected_piece") or self.selected_piece is None: - if self.board[clicked_row][clicked_column] == self.current_player: - self.selected_piece = (clicked_column, clicked_row) - self.valid_moves = self.move_handler.generate_moves(self.board, (clicked_column, clicked_row)) - self.highlight_piece(clicked_column, clicked_row) - # Redraw the board to show valid moves - self.draw_board() - else: - # Attempt to move the selected piece - start_position = self.selected_piece - end_position = (clicked_column, clicked_row) - if end_position in self.valid_moves and self.move_handler.is_valid_move(self.board, start_position, end_position): - # Handle the move - result = self.make_move(start_position, end_position) - if result == "continue": - # Allow the player to continue jumping - self.status_label.config(text="Jump Possible.") - return - elif result: - # Move completed, check the winner - self.check_winner() - # Switch to the other player - self.current_player = 3 - self.current_player - current_color = PLAYER_1_COLOR.capitalize() if self.current_player == 1 else PLAYER_2_COLOR.capitalize() - self.status_label.config(text=f"Player {self.current_player}'s turn ({current_color})") + # checks if it's the human player's turn + if self.current_player == self.human_player_color: + # handles human player's logic + if not hasattr(self, "selected_piece") or self.selected_piece is None: + if self.board[clicked_row][clicked_column] == self.current_player: + self.selected_piece = (clicked_column, clicked_row) + self.valid_moves = self.move_handler.generate_moves(self.board, (clicked_column, clicked_row)) + self.highlight_piece(clicked_column, clicked_row) + self.draw_board() else: - print(f"Invalid Move. The move you attempted is invalid. Try again.") - # Reset the selected piece and valid moves after attempting a move - self.selected_piece = None - self.valid_moves = [] - self.draw_board() + start_position = self.selected_piece + end_position = (clicked_column, clicked_row) + if end_position in self.valid_moves and self.move_handler.is_valid_move(self.board, start_position, end_position): + result = self.make_move(start_position, end_position) + if result == "continue": + self.status_label.config(text="Jump Possible.") + return + elif result: + self.check_winner() + self.current_player = 3 - self.current_player + current_color = PLAYER_1_COLOR.capitalize() if self.current_player == 1 else PLAYER_2_COLOR.capitalize() + self.status_label.config(text=f"Player {self.current_player}'s turn ({current_color})") + else: + print(f"invalid move. the move you attempted is invalid. try again.") + self.selected_piece = None + self.valid_moves = [] + self.draw_board() + # triggers the next turn + self.handle_turn() def highlight_piece(self, column, row): x1, y1 = column * self.cell_size + 50, row * self.cell_size + 50 @@ -303,6 +301,148 @@ class Halma: print(f"\tPlayer 1 Jumps: {self.player_1_jumps}\n\tPlayer 2 Jumps: {self.player_2_jumps}") self.window.quit() + def ai_move(self): + best_score = float('-inf') if self.current_player == 1 else float('inf') + best_move = None + for y in range(self.board_size): + for x in range(self.board_size): + if self.board[y][x] == self.current_player: + moves = self.move_handler.generate_moves(self.board, (x, y)) + for move in moves: + new_board = [row[:] for row in self.board] + new_board[y][x] = 0 + new_board[move[1]][move[0]] = self.current_player + score = alpha_beta_pruning( + new_board, depth=3, alpha=float('-inf'), beta=float('inf'), + is_maximizing=(self.current_player == 1), + move_handler=self.move_handler, board_size=self.board_size + ) + if (self.current_player == 1 and score > best_score) or (self.current_player == 2 and score < best_score): + best_score = score + best_move = ((x, y), move) + if best_move: + self.make_move(best_move[0], best_move[1]) + + def handle_turn(self): + # checks whose turn it is + if self.current_player == self.human_player_color: + # waits for the human player to make a move + return + else: + # triggers the ai's move + self.ai_move() + self.check_winner() + # switches to the next player + self.current_player = 3 - self.current_player + # updates the status label with the current player's turn + current_color = PLAYER_1_COLOR.capitalize() if self.current_player == 1 else PLAYER_2_COLOR.capitalize() + self.status_label.config(text=f"Player {self.current_player}'s turn ({current_color})") + self.draw_board() + # schedules the next turn to check if it's the ai's turn again + self.window.after(100, self.handle_turn) + +def util(board, board_size): + # defines the goal areas for both players + player_1_goal = [(board_size - 1 - y, board_size - 1 - x) + for y in range(3) for x in range(3) if x + y < 3] + player_2_goal = [(y, x) for y in range(3) for x in range(3) if x + y < 3] + + # calculates the score for player 1 based on distance to their goal + player_1_score = sum(min(abs(y - gy) + abs(x - gx) for gy, gx in player_1_goal) + for y in range(board_size) for x in range(board_size) if board[y][x] == 1) + # calculates the score for player 2 based on distance to their goal + player_2_score = sum(min(abs(y - gy) + abs(x - gx) for gy, gx in player_2_goal) + for y in range(board_size) for x in range(board_size) if board[y][x] == 2) + + # returns the difference in scores (lower is better for player 2, higher for player 1) + return player_2_score - player_1_score + + +def minimax(board, depth, is_maximizing, move_handler, board_size): + # returns the utility score if the maximum depth is reached + if depth == 0: + return util(board, board_size) + + if is_maximizing: + # maximizes the score for player 1 + max_eval = float('-inf') + for y in range(board_size): + for x in range(board_size): + if board[y][x] == 1: + moves = move_handler.generate_moves(board, (x, y)) + for move in moves: + # creates a copy of the board and applies the move + new_board = [row[:] for row in board] + new_board[y][x] = 0 + new_board[move[1]][move[0]] = 1 + # recursively evaluates the move + eval = minimax(new_board, depth - 1, False, move_handler, board_size) + max_eval = max(max_eval, eval) + return max_eval + else: + # minimizes the score for player 2 + min_eval = float('inf') + for y in range(board_size): + for x in range(board_size): + if board[y][x] == 2: + moves = move_handler.generate_moves(board, (x, y)) + for move in moves: + # creates a copy of the board and applies the move + new_board = [row[:] for row in board] + new_board[y][x] = 0 + new_board[move[1]][move[0]] = 2 + # recursively evaluates the move + eval = minimax(new_board, depth - 1, True, move_handler, board_size) + min_eval = min(min_eval, eval) + return min_eval + + +def alpha_beta_pruning(board, depth, alpha, beta, is_maximizing, move_handler, board_size): + # returns the utility score if the maximum depth is reached + if depth == 0: + return util(board, board_size) + + if is_maximizing: + # maximizes the score for player 1 + max_eval = float('-inf') + for y in range(board_size): + for x in range(board_size): + if board[y][x] == 1: + moves = move_handler.generate_moves(board, (x, y)) + for move in moves: + # creates a copy of the board and applies the move + new_board = [row[:] for row in board] + new_board[y][x] = 0 + new_board[move[1]][move[0]] = 1 + # recursively evaluates the move with alpha-beta pruning + eval = alpha_beta_pruning(new_board, depth - 1, alpha, beta, False, move_handler, board_size) + max_eval = max(max_eval, eval) + alpha = max(alpha, eval) + # prunes the branch if beta is less than or equal to alpha + if beta <= alpha: + break + return max_eval + else: + # minimizes the score for player 2 + min_eval = float('inf') + for y in range(board_size): + for x in range(board_size): + if board[y][x] == 2: + moves = move_handler.generate_moves(board, (x, y)) + for move in moves: + # creates a copy of the board and applies the move + new_board = [row[:] for row in board] + new_board[y][x] = 0 + new_board[move[1]][move[0]] = 2 + # recursively evaluates the move with alpha-beta pruning + eval = alpha_beta_pruning(new_board, depth - 1, alpha, beta, True, move_handler, board_size) + min_eval = min(min_eval, eval) + beta = min(beta, eval) + # prunes the branch if beta is less than or equal to alpha + if beta <= alpha: + break + return min_eval + if __name__ == "__main__": # Default values for the game board_size = 8