diff --git a/src/board.py b/src/board.py new file mode 100644 index 0000000..28f43aa --- /dev/null +++ b/src/board.py @@ -0,0 +1,332 @@ +import pygame + +from src import settings + + +def init_board(): + board = [['_' for _ in range(settings.BOARD_SIZE)] for _ in range(settings.BOARD_SIZE)] + board[3][3], board[4][4] = 'W', 'W' + board[3][4], board[4][3] = 'B', 'B' + return board + + +def display_background_and_title(title=settings.CAPTION): + window = pygame.display.set_mode((settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT)) + window.fill(settings.GREEN) + + title = settings.TITLE_FONT.render(title, True, settings.BLACK) + + window.blit( + title, + title.get_rect(center=(settings.WINDOW_WIDTH // 2, 50)) + ) + + pygame.display.flip() + + return window + + +class Board: + def __init__(self, to_display=True): + self.board = init_board() + if to_display: + self.window = display_background_and_title() + + def display_board(self): + self.window = display_background_and_title() + for row in range(settings.BOARD_SIZE): + for column in range(settings.BOARD_SIZE): + pygame.draw.rect( + self.window, + settings.LIGHT_BLUE, + pygame.Rect( + settings.LEFT_GRID_PADDING + column * settings.BOARD_CELL_SIZE, + settings.TOP_GRID_PADDING + row * settings.BOARD_CELL_SIZE, + settings.BOARD_CELL_SIZE, + settings.BOARD_CELL_SIZE + ), + 1 + ) + pygame.draw.circle( + self.window, + settings.BLACK if self.board[row][column] == 'B' else settings.WHITE if self.board[row][ + column] == 'W' else settings.GREEN, + ( + settings.LEFT_GRID_PADDING + column * settings.BOARD_CELL_SIZE + settings.BOARD_CELL_SIZE // 2, + settings.TOP_GRID_PADDING + row * settings.BOARD_CELL_SIZE + settings.BOARD_CELL_SIZE // 2 + ), + settings.BOARD_CELL_SIZE // 2 - 5 + ) + + pygame.display.flip() + + def display_ia_thinking(self, show_score_during_thinking=False): + self.display_board() + if show_score_during_thinking: + self.display_score() + thinking_text = settings.FONT.render( + "L'IA réfléchit...", + True, + settings.BLACK + ) + thinking_rect = pygame.Rect( + 0, + settings.WINDOW_HEIGHT - 100, + settings.WINDOW_WIDTH, + 50 + ) + + self.window.blit( + thinking_text, + thinking_text.get_rect(center=thinking_rect.center) + ) + + pygame.display.flip() + + def display_score(self): + black_score = sum(row.count('B') for row in self.board) + white_score = sum(row.count('W') for row in self.board) + total_score = black_score + white_score + player_turn = 'noir' if total_score % 2 == 0 else 'blanc' + + texts = [ + (f"Noir: {black_score}", 20), + (f"Blanc: {white_score}", 60), + (f"Tour: {total_score}", 100), + (f"Joueur: {player_turn}", 140) + ] + + for text, y_position in texts: + rendered_text = settings.FONT.render(text, True, settings.BLACK) + self.window.blit(rendered_text, rendered_text.get_rect(topright=(settings.WINDOW_WIDTH - 20, y_position))) + + button_texts = ["Quitter", "Rejouer"] + for i, button_text in enumerate(button_texts): + pygame.draw.rect(self.window, settings.LIGHT_BLUE, + (settings.WINDOW_WIDTH - 100, settings.WINDOW_HEIGHT - 60 - i * 60, 80, 40)) + rendered_button_text = settings.FONT.render(button_text, True, settings.BLACK) + self.window.blit(rendered_button_text, rendered_button_text.get_rect( + center=(settings.WINDOW_WIDTH - 60, settings.WINDOW_HEIGHT - 40 - i * 60))) + + pygame.display.flip() + + def display_winner(self): + self.display_board() + + black_score = sum(row.count('B') for row in self.board) + white_score = sum(row.count('W') for row in self.board) + + winner = 'noir' if black_score > white_score else 'blanc' if white_score > black_score else 'aucun' + winner_text = settings.FONT.render( + "Match nul !" if winner == 'aucun' else f"Le gagnant est le joueur {winner} !", + True, + settings.BLACK + ) + winner_rect = pygame.Rect( + 0, + settings.WINDOW_HEIGHT - 100, + settings.WINDOW_WIDTH, + 50 + ) + + pygame.draw.rect( + self.window, + settings.GREEN, + winner_rect + ) + + self.window.blit( + winner_text, + winner_text.get_rect(center=winner_rect.center) + ) + + pygame.display.flip() + + def display_invalid_move(self): + invalid_move_text = settings.FONT.render( + "Coup invalide !", + True, + settings.BLACK + ) + invalid_move_rect = pygame.Rect( + 0, + settings.WINDOW_HEIGHT - 100, + settings.WINDOW_WIDTH, + 50 + ) + + self.window.blit( + invalid_move_text, + invalid_move_text.get_rect(center=invalid_move_rect.center) + ) + + pygame.display.flip() + + def display_turn_skipped(self): + turn_skipped_text = settings.FONT.render( + "Aucun coup n'est disponible ! Vous passez votre tour.", + True, + settings.BLACK + ) + turn_skipped_rect = pygame.Rect( + 0, + settings.WINDOW_HEIGHT - 100, + settings.WINDOW_WIDTH, + 50 + ) + + self.window.blit( + turn_skipped_text, + turn_skipped_text.get_rect(center=turn_skipped_rect.center) + ) + + pygame.display.flip() + + def available_cells(self, player): + positions = [] + for x in range(8): + for y in range(8): + if self.is_valid_move(x, y, player)[0]: + positions.append((x, y)) + return positions + + def is_valid_move(self, x, y, player): + opponent = 'W' if player == 'B' else 'B' + flipped_cells = [] + + # List of possible directions + directions = [(dx, dy) for dx in [-1, 0, 1] for dy in [-1, 0, 1] if dx != 0 or dy != 0] + + # Verify that the cell is empty + if self.board[x][y] != '_': + return False, [] + + # For each direction + for dx, dy in directions: + # List of opponent cells to flip + temp_flips = [] + + # Coordinates of the next cell in the direction + nx, ny = x + dx, y + dy + + while 0 <= nx < 8 and 0 <= ny < 8: + if self.board[nx][ny] == opponent: + # If the next cell is an opponent cell, add it to the list of opponent cells to flip + temp_flips.append((nx, ny)) + + elif self.board[nx][ny] == player: + if temp_flips: + # If the next cell is a player cell and there are opponent cells to flip, the move is valid + flipped_cells.extend(temp_flips) + break + else: + break # If the next cell is empty, the move is invalid + # Move to the next cell in the direction + nx += dx + ny += dy + + # If there are opponent cells to flip, the move is valid + return len(flipped_cells) > 0, flipped_cells + + def find_player(self): + return 'B' if sum(row.count('B') for row in self.board) == sum(row.count('W') for row in self.board) else 'W' + + def evaluate_board(self, ai_player): + if ai_player.evaluating_method == settings.AVAILABLE_AIS[ai_player.ai_type][0]: + return self.evaluate_board_by_position(ai_player, 1) + + elif ai_player.evaluating_method == settings.AVAILABLE_AIS[ai_player.ai_type][1]: + return self.evaluate_board_by_position(ai_player, 2) + + elif ai_player.evaluating_method == settings.AVAILABLE_AIS[ai_player.ai_type][2]: + return self.evaluate_board_by_score(ai_player) + + elif ai_player.evaluating_method == settings.AVAILABLE_AIS[ai_player.ai_type][3]: + return self.evaluate_board_by_mobility(ai_player) + + def evaluate_board_by_score(self, ai_player): + + player_score = sum(cell == ai_player.color for row in self.board for cell in row) + opponent_score = sum(cell != ' ' and cell != ai_player.color for row in self.board for cell in row) + + return player_score - opponent_score + + def evaluate_board_by_mobility(self, ai_player): + opponent = 'W' if ai_player.color == 'B' else 'B' + + # Weights + mobility_weight = 1.0 # Maximizes the player's mobility + opponent_mobility_weight = -1.0 # Minimizes the opponent's mobility + corner_weight = 10.0 # Maximizes the number of corners taken by the player + + # Count the number of moves available for the player and the opponent + player_mobility = len(self.available_cells(ai_player.color)) + opponent_mobility = len(self.available_cells(opponent)) + + # Count the number of corners taken by the player and the opponent + + player_corners = [self.board[0][0], self.board[0][7], self.board[7][0], self.board[7][7]].count(ai_player.color) + opponent_corners = [self.board[0][0], self.board[0][7], self.board[7][0], self.board[7][7]].count(opponent) + + # Compute the score + return ( + mobility_weight * player_mobility + + opponent_mobility_weight * opponent_mobility + + corner_weight * (player_corners - opponent_corners) + ) + + def evaluate_board_by_position(self, ai_player, version): + player_score = 0 + opponent_score = 0 + position_values = [] + opponent = 'W' if ai_player.color == 'B' else 'B' + + # Board position values to prioritize corners and edges + if version == 1: + position_values = [ + [500, -150, 30, 10, 10, 30, -150, 500], + [-150, -250, 0, 0, 0, 0, -250, -150], + [30, 0, 1, 2, 2, 1, 0, 30], + [10, 0, 2, 16, 16, 2, 0, 10], + [10, 0, 2, 16, 16, 2, 0, 10], + [30, 0, 1, 2, 2, 1, 0, 30], + [-150, -250, 0, 0, 0, 0, -250, -150], + [500, -150, 30, 10, 10, 30, -150, 500], + ] + elif version == 2: + position_values = [ + [100, -20, 10, 5, 5, 10, -20, 100], + [-20, -50, -2, -2, -2, -2, -50, -20], + [10, -2, -1, -1, -1, -1, -2, 10], + [5, -2, -1, -1, -1, -1, -2, 5], + [5, -2, -1, -1, -1, -1, -2, 5], + [10, -2, -1, -1, -1, -1, -2, 10], + [-20, -50, -2, -2, -2, -2, -50, -20], + [100, -20, 10, 5, 5, 10, -20, 100], + ] + + for i in range(8): + for j in range(8): + if self.board[i][j] == ai_player.color: + player_score += position_values[i][j] + elif self.board[i][j] == opponent: + opponent_score += position_values[i][j] + + return player_score - opponent_score + + def add_move_to_board(self, x, y, player): + if player is None: + player = self.find_player() + + is_valid, flipped_cells = self.is_valid_move(x, y, player) + + if is_valid: + self.board[x][y] = player + + for fx, fy in flipped_cells: + self.board[fx][fy] = player + + return is_valid + + def board_is_full(self): + return all(cell != '_' for row in self.board for cell in row) diff --git a/src/main.py b/src/main.py index 102800a..0cb542e 100644 --- a/src/main.py +++ b/src/main.py @@ -1,171 +1,90 @@ -import copy -import time - - -# Initialisation du plateau de jeu -def init_board(): - board = [[' ' for _ in range(8)] for _ in range(8)] - board[3][3], board[4][4] = 'W', 'W' - board[3][4], board[4][3] = 'B', 'B' - return board - - -# Affichage du plateau de jeu -def display_board(board): - print(" 0 1 2 3 4 5 6 7") - for i, row in enumerate(board): - print(i, end=' ') - for cell in row: - print(cell, end=' ') - print() - - -# Vérification de la validité d'un mouvement -def is_valid_move(board, x, y, player): - opponent = 'W' if player == 'B' else 'B' - if board[x][y] != ' ': - return False, [] - - directions = [(dx, dy) for dx in [-1, 0, 1] for dy in [-1, 0, 1] if dx != 0 or dy != 0] - flipped_cells = [] - - for dx, dy in directions: - temp_flips = [] - nx, ny = x + dx, y + dy - while 0 <= nx < 8 and 0 <= ny < 8: - if board[nx][ny] == opponent: - temp_flips.append((nx, ny)) - elif board[nx][ny] == player: - if temp_flips: - flipped_cells.extend(temp_flips) - break - else: - break - nx += dx - ny += dy - - return len(flipped_cells) > 0, flipped_cells - - -# Appliquer un mouvement sur le plateau -def make_move(board, x, y, player): - is_valid, flipped_cells = is_valid_move(board, x, y, player) - if is_valid: - board[x][y] = player - for fx, fy in flipped_cells: - board[fx][fy] = player - return is_valid - - -# Fonction d'évaluation simple -def evaluate_board(board, player): - player_score = sum(cell == player for row in board for cell in row) - opponent_score = sum(cell != ' ' and cell != player for row in board for cell in row) - return player_score - opponent_score - - -# Mémoire des états déjà traités pour éviter les calculs redondants -memo = {} - - -# Algorithme Min-Max avec time-out et mémoire -def minmax_with_memory(board, depth, maximizing, player, timeout): - if time.time() > timeout: - return None, None - - board_str = ''.join(''.join(row) for row in board) + player - if board_str in memo: - return memo[board_str] - - if depth == 0: - score = evaluate_board(board, player) - memo[board_str] = score, None - return score, None - - opponent = 'W' if player == 'B' else 'B' - best_move = None - - if maximizing: - max_eval = float('-inf') - for x in range(8): - for y in range(8): - new_board = copy.deepcopy(board) - if make_move(new_board, x, y, player): - eval_value, _ = minmax_with_memory(new_board, depth - 1, False, player, timeout) - if eval_value is None: # Timeout occurred - return None, None - if eval_value > max_eval: - max_eval = eval_value - best_move = (x, y) - memo[board_str] = max_eval, best_move - return max_eval, best_move - else: - min_eval = float('inf') - for x in range(8): - for y in range(8): - new_board = copy.deepcopy(board) - if make_move(new_board, x, y, opponent): - eval_value, _ = minmax_with_memory(new_board, depth - 1, True, player, timeout) - if eval_value is None: # Timeout occurred - return None, None - if eval_value < min_eval: - min_eval = eval_value - best_move = (x, y) - memo[board_str] = min_eval, best_move - return min_eval, best_move - - -# Fonction pour récupérer le mouvement du joueur humain -def get_human_move(board, player): - while True: - try: - x, y = map(int, input(f"Enter the coordinates where you want to place your '{player}' (row col): ").split()) - is_valid, _ = is_valid_move(board, x, y, player) - if is_valid: - return x, y - else: - print("Invalid move. Try again.") - except ValueError: - print("Invalid input. Please enter two integers separated by a space.") - - -# Fonction principale pour jouer au jeu -def play_game(): - board = init_board() - player_turn = 'W' +from time import sleep - while True: - display_board(board) +import pygame + +from src.board import Board +from src.menu import AIConfig, Menu, AIVisibility +from src.players.ai_player import AIPlayer +from src.players.human_player import HumanPlayer + + +def main(): + clock = pygame.time.Clock() + pygame.init() + + # Menu + menu = Menu() + chosen_game_mode = menu.run() + show_ai_moves = False + standby_duration = 0.2 + + # AI Config + if chosen_game_mode == "Joueur vs. IA" or chosen_game_mode == "IA vs. IA": + ai_config = AIConfig() + ai_type, evaluating_method, depth, max_timeout = ai_config.run("First AI param") + player2 = AIPlayer(color='W', ai_type=ai_type, evaluating_method=evaluating_method, depth=depth, + max_timeout=max_timeout) + + if chosen_game_mode == "IA vs. IA": + ai_type, evaluating_method, depth, max_timeout = ai_config.run("Second AI param") + player1 = AIPlayer(color='B', ai_type=ai_type, evaluating_method=evaluating_method, depth=depth, + max_timeout=max_timeout) - if player_turn == 'W': - print("Human's turn:") - x, y = get_human_move(board, 'W') else: - print("AI's turn:") - timeout = time.time() + 2 # 2 secondes de time-out pour l'IA - _, (x, y) = minmax_with_memory(board, 3, True, 'B', timeout) - if x is None and y is None: - print("AI timeout. Human wins!") - break - - make_move(board, x, y, player_turn) - - # Vérification de la fin du jeu - if all(cell != ' ' for row in board for cell in row): - w_score = sum(cell == 'W' for row in board for cell in row) - b_score = sum(cell == 'B' for row in board for cell in row) - print(f"Final scores - W: {w_score}, B: {b_score}") - if w_score > b_score: - print("Human wins!") - elif b_score > w_score: - print("AI wins!") - else: - print("It's a tie!") - break - - player_turn = 'B' if player_turn == 'W' else 'W' - - -# Jouer au jeu -if __name__ == '__main__': - play_game() + player1 = HumanPlayer('B') + + ai_visibility = AIVisibility() + show_ai_moves, standby_duration = ai_visibility.run() + + else: + player1 = HumanPlayer('B') + player2 = HumanPlayer('W') + + board = Board() + turn_skipped = False + players = [player1, player2] + + while True: + for player in players: + move_made = False + + available_cells = board.available_cells(player.color) + if len(available_cells) == 0: + if turn_skipped: + board.display_winner() + sleep(5) + return + else: + turn_skipped = True + board.display_turn_skipped() + sleep(0.3) + break + + while not move_made: # Boucle pour gérer les coups invalides + board.display_board() + board.display_score() + + if isinstance(player, AIPlayer): + if chosen_game_mode == "Joueur vs. IA": + move_made = player.make_move(board, standby_duration=standby_duration, + show_ai_moves=show_ai_moves, show_score_during_thinking=False) + else: + move_made = player.make_move(board, standby_duration=standby_duration, + show_ai_moves=show_ai_moves, show_score_during_thinking=True) + else: + move_made = player.make_move(board) + + if not move_made: + board.display_invalid_move() + sleep(0.3) + + turn_skipped = False + + if isinstance(player, AIPlayer): + sleep(1) + + clock.tick(10) + + +if __name__ == "__main__": + main() diff --git a/src/menu.py b/src/menu.py new file mode 100644 index 0000000..7d9cc99 --- /dev/null +++ b/src/menu.py @@ -0,0 +1,390 @@ +import sys + +import pygame + +from src import settings, board + + +class Menu: + def __init__(self): + self.selected_option = None + self.buttons = [] + + def update_buttons(self): + self.buttons = [] + for i, _ in enumerate(settings.OPTIONS): + button = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + i * 80, + settings.WINDOW_WIDTH // 2, + 60 + ) + self.buttons.append(button) + + def display_menu(self, title=settings.CAPTION, option_list=settings.OPTIONS): + window = board.display_background_and_title(title) + for i, option in enumerate(option_list): + button = self.buttons[i] + if self.selected_option == i: + pygame.draw.rect(window, settings.LIGHT_BLUE, button) + else: + pygame.draw.rect(window, settings.WHITE, button) + + text = settings.FONT.render( + option, + True, + settings.BLACK) + window.blit(text, text.get_rect(center=button.center)) + return window + + def run(self): + pygame.display.set_caption(settings.CAPTION) + clock = pygame.time.Clock() + + while True: + self.update_buttons() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + x, y = event.pos + for i, button in enumerate(self.buttons): + if button.collidepoint(x, y): + self.selected_option = i + if self.selected_option == 3: + pygame.quit() + sys.exit() + else: + print(f"Lancement du jeu {settings.OPTIONS[self.selected_option]}") + return settings.OPTIONS[self.selected_option] + + self.display_menu() + + pygame.display.flip() + clock.tick(10) + + +class AIConfig: + def __init__(self): + self.selected_option = None + self.selected_ai = None + self.cursor_dragging = None + self.cursors = [ + {'name': 'Profondeur', 'value': settings.MINIMAX_DEPTH, 'min_value': settings.MINIMAX_DEPTH, + 'max_value': settings.MAXIMAL_DEPTH}, + {'name': 'Timeout', 'value': settings.MAX_TIMEOUT, 'min_value': settings.MIN_TIMEOUT, + 'max_value': settings.MAX_TIMEOUT}, + ] + self.final_selection = None + + def display_ai_parameters(self, title): + window = board.display_background_and_title(title) + self.update_buttons(window) + return window + + def update_buttons(self, window): + offset = 0 + for i, option in enumerate(settings.AVAILABLE_AIS.keys()): + if self.selected_ai is not None and i > self.selected_ai: + offset = 40 * len(settings.AVAILABLE_AIS[list(settings.AVAILABLE_AIS.keys())[self.selected_ai]]) + + button = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + i * 80 + offset, + settings.WINDOW_WIDTH // 2, + 60 + ) + + if self.selected_option == i: + pygame.draw.rect(window, settings.LIGHT_BLUE, button) + else: + pygame.draw.rect(window, settings.WHITE, button) + + text = settings.FONT.render( + option, + True, + settings.BLACK + ) + window.blit(text, text.get_rect(center=button.center)) + + if self.selected_ai is not None: + ai_name = list(settings.AVAILABLE_AIS.keys())[self.selected_ai] + eval_methods = settings.AVAILABLE_AIS[ai_name] + + for i, eval_method in enumerate(eval_methods): + sub_button = pygame.Rect( + settings.WINDOW_WIDTH // 3, + 120 + self.selected_ai * 80 + (i + 1) * 40, + settings.WINDOW_WIDTH // 3, + 30 + ) + if sub_button.collidepoint(pygame.mouse.get_pos()): + pygame.draw.rect(window, settings.LIGHT_BLUE, sub_button) + else: + pygame.draw.rect(window, settings.WHITE, sub_button) + + sub_text = settings.FONT.render( + eval_method, + True, + settings.BLACK + ) + window.blit(sub_text, sub_text.get_rect(center=sub_button.center)) + + if self.selected_ai is not None: + offset = 40 * len(settings.AVAILABLE_AIS[list(settings.AVAILABLE_AIS.keys())[self.selected_ai]]) + + for idx, cursor in enumerate(self.cursors): + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + len(settings.AVAILABLE_AIS.keys()) * 80 + offset + 80 * idx, # Change 40 * idx to 80 * idx + settings.WINDOW_WIDTH // 2, + 60 + ) + pygame.draw.rect(window, settings.WHITE, cursor_rect) + + pygame.draw.rect( + window, + settings.BLACK, + ( + (cursor['value'] - cursor['min_value']) / (cursor['max_value'] - cursor['min_value']) * ( + cursor_rect.width - 30) + cursor_rect.x + 10, + cursor_rect.y + 10, + 10, + 40 + ) + ) + + sub_text = settings.FONT.render( + f"{cursor['name']}: {cursor['value']}", + True, + settings.BLACK + ) + window.blit(sub_text, sub_text.get_rect(center=cursor_rect.center)) + + def run(self, title): + self.__init__() + pygame.display.set_caption(settings.CAPTION) + clock = pygame.time.Clock() + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + x, y = event.pos + self.handle_click(x, y) + + elif event.type == pygame.MOUSEMOTION and self.cursor_dragging is not None: + x, _ = event.pos + self.handle_cursor_drag(x) + + elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: + self.cursor_dragging = None + + if self.final_selection is not None: + return self.final_selection + + self.display_ai_parameters(title) + + pygame.display.flip() + clock.tick(10) + + def handle_click(self, x, y): + offset = 0 + for i, option in enumerate(settings.AVAILABLE_AIS.keys()): + if self.selected_ai is not None and i > self.selected_ai: + offset = 40 * len(settings.AVAILABLE_AIS[list(settings.AVAILABLE_AIS.keys())[self.selected_ai]]) + + button = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + i * 80 + offset, + settings.WINDOW_WIDTH // 2, + 60 + ) + + if button.collidepoint(x, y): + self.selected_option = i + if self.selected_ai == self.selected_option: + self.selected_ai = None + else: + self.selected_ai = self.selected_option + + if self.selected_ai is not None: + ai_name = list(settings.AVAILABLE_AIS.keys())[self.selected_ai] + eval_methods = settings.AVAILABLE_AIS[ai_name] + + for i, eval_method in enumerate(eval_methods): + sub_button = pygame.Rect( + settings.WINDOW_WIDTH // 3, + 120 + self.selected_ai * 80 + (i + 1) * 40, + settings.WINDOW_WIDTH // 3, + 30 + ) + if sub_button.collidepoint(x, y): + cursor_values = ', '.join([f"{cursor['name']}: {cursor['value']}" for cursor in self.cursors]) + print( + f"Lancement de l'IA {ai_name} avec la méthode d'évaluation {eval_method} et les paramètres {cursor_values}") + self.final_selection = (ai_name, eval_method, self.cursors[0]['value'], self.cursors[1]['value']) + + if self.selected_ai is not None: + offset = 40 * len(settings.AVAILABLE_AIS[list(settings.AVAILABLE_AIS.keys())[self.selected_ai]]) + + for idx, cursor in enumerate(self.cursors): + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + len(settings.AVAILABLE_AIS.keys()) * 80 + offset + 80 * idx, # Change 40 * idx to 80 * idx + settings.WINDOW_WIDTH // 2, + 60 + ) + if cursor_rect.collidepoint(x, y): + print(f"Cursor {cursor['name']} clicked") + self.cursor_dragging = idx # Save the index of the cursor being dragged + + def handle_cursor_drag(self, x): + if self.cursor_dragging is not None: + cursor = self.cursors[self.cursor_dragging] + + # Calcul de l'offset avant de définir cursor_rect + offset = 0 + if self.selected_ai is not None: + offset = 40 * len(settings.AVAILABLE_AIS[list(settings.AVAILABLE_AIS.keys())[self.selected_ai]]) + + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + len(settings.AVAILABLE_AIS.keys()) * 80 + offset + 80 * self.cursor_dragging, + settings.WINDOW_WIDTH // 2, + 60 + ) + cursor['value'] = round(int(((max(cursor_rect.left + 10, + min(cursor_rect.right - 10, x)) - cursor_rect.left - 10) / ( + cursor_rect.width - 20)) * ( + cursor['max_value'] - cursor['min_value']) + cursor['min_value'])) + + +class AIVisibility: + def __init__(self): + self.final_selection = None + self.selected_option = None + self.buttons = [] + self.options = ["Voir les coups de l'IA", "Ne pas voir les coups de l'IA"] + self.cursor_dragging = None + self.cursor = {'name': 'Durée (ms)', 'value': 200, 'min_value': 100, 'max_value': 3000} + + def update_buttons(self): + self.buttons = [] + for i, _ in enumerate(self.options): + button = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 90 + i * 80, + settings.WINDOW_WIDTH // 2, + 60 + ) + self.buttons.append(button) + + def display_menu(self): + window = board.display_background_and_title("Visibilité de l'IA") + for i, option in enumerate(self.options): + button = self.buttons[i] + if self.selected_option == i: + pygame.draw.rect(window, settings.LIGHT_BLUE, button) + else: + pygame.draw.rect(window, settings.WHITE, button) + + text = settings.FONT.render( + option, + True, + settings.BLACK) + window.blit(text, text.get_rect(center=button.center)) + + # Add cursor for duration + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 250, + settings.WINDOW_WIDTH // 2, + 60 + ) + pygame.draw.rect(window, settings.WHITE, cursor_rect) + + pygame.draw.rect( + window, + settings.BLACK, + ( + (self.cursor['value'] - self.cursor['min_value']) / ( + self.cursor['max_value'] - self.cursor['min_value']) * ( + cursor_rect.width - 30) + cursor_rect.x + 10, + cursor_rect.y + 10, + 10, + 40 + ) + ) + + sub_text = settings.FONT.render( + f"{self.cursor['name']}: {self.cursor['value']}", + True, + settings.BLACK + ) + window.blit(sub_text, sub_text.get_rect(center=cursor_rect.center)) + + return window + + def run(self): + pygame.display.set_caption("Visibilité de l'IA") + clock = pygame.time.Clock() + self.final_selection = None + + while True: + self.update_buttons() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + x, y = event.pos + for i, button in enumerate(self.buttons): + if button.collidepoint(x, y): + self.selected_option = i + self.final_selection = (self.selected_option == 0, self.cursor['value'] / 1000) + + # Check if the cursor rectangle is clicked + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 250, + settings.WINDOW_WIDTH // 2, + 60 + ) + if cursor_rect.collidepoint(x, y): + self.cursor_dragging = True # Initialize cursor_dragging + + elif event.type == pygame.MOUSEMOTION and self.cursor_dragging: + x, _ = event.pos + self.handle_cursor_drag(x) + + elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: + self.cursor_dragging = None + + self.display_menu() + + pygame.display.flip() + clock.tick(10) + + if self.final_selection is not None: + return self.final_selection + + def handle_cursor_drag(self, x): + if self.cursor_dragging is not None: + cursor_rect = pygame.Rect( + settings.WINDOW_WIDTH // 4, + 250, + settings.WINDOW_WIDTH // 2, + 60 + ) + self.cursor['value'] = round(int(((max(cursor_rect.left + 10, + min(cursor_rect.right - 10, x)) - cursor_rect.left - 10) / ( + cursor_rect.width - 20)) * ( + self.cursor['max_value'] - self.cursor['min_value']) + self.cursor[ + 'min_value'])) diff --git a/src/players/__init__.py b/src/players/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/players/ai_player.py b/src/players/ai_player.py new file mode 100644 index 0000000..a5f952e --- /dev/null +++ b/src/players/ai_player.py @@ -0,0 +1,261 @@ +import copy +import time + +from src import settings +from src.board import Board +from src.players.player import Player + +MAX_SCORE = 1000000 + + +class AIPlayer(Player): + + def __init__(self, color, ai_type, evaluating_method, max_timeout=settings.MAX_TIMEOUT, depth=6): + super().__init__(color) # AI always plays as 'B' + self.depth = depth + self.max_timeout = max_timeout + self.ai_type = ai_type + self.evaluating_method = evaluating_method + self.transposition_table = {} + + def make_move(self, board, show_ai_moves=True, show_score_during_thinking=False, standby_duration=0.2): + print("AI is thinking...") + move_to_make = [] + + if self.ai_type == list(settings.AVAILABLE_AIS.keys())[0]: + available_cells = board.available_cells(self.color) + + move_to_make = self.min_max( + board=board, + maximizing=True, + timeout=time.time() + self.max_timeout, + available_moves=available_cells, + depth=self.depth, + standby_duration=standby_duration, + show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking + )[1] + + elif self.ai_type == list(settings.AVAILABLE_AIS.keys())[1]: + + move_to_make = self.alpha_beta( + board=board, + alpha=float('-inf'), + beta=float('inf'), + maximizing=True, + timeout=time.time() + self.max_timeout, + depth=self.depth, + standby_duration=standby_duration, + show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking + )[1] + + print("AI has found a move") + + if move_to_make is not None: + if board.add_move_to_board(move_to_make[0], move_to_make[1], self.color): + print("AI made a move: ", move_to_make) + return True + else: + return False + + def min_max(self, board, maximizing, timeout, available_moves, depth, standby_duration=0.2, + show_ai_moves=True, show_score_during_thinking=False): + print("Entering minmax with memory") + if show_ai_moves: + board.display_ia_thinking(show_score_during_thinking) + time.sleep(standby_duration) + + opponent = 'W' if self.color == 'B' else 'B' + best_move = None + board_str = ''.join(''.join(row) for row in board.board) + self.color + + if self.depth == 0: + print("depth = 0") + return board.evaluate_board(self), None + + if time.time() >= timeout: + print("timeout") + + if not available_moves: + print("no available moves") + return board.evaluate_board(self), (-1, -1) + + best_move = available_moves[0] + print("best move: ", best_move) + return board.evaluate_board(self), best_move + + if board_str in self.transposition_table: + print("board in memo") + return self.transposition_table[board_str] + + if maximizing: + max_eval = -MAX_SCORE + + for move in available_moves: + x, y = move + new_board = Board(to_display=False) + new_board.board = copy.deepcopy(board.board) + + if new_board.add_move_to_board(x, y, self.color): + eval_value, _ = self.min_max(new_board, False, timeout, available_moves, depth=depth - 1, + standby_duration=standby_duration, show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking) + + if eval_value is None: + available_moves.append((x, y)) + + elif eval_value > max_eval: + max_eval = eval_value + best_move = (x, y) + + if best_move is None: + best_move = available_moves[0] + max_eval = board.evaluate_board(self) + + self.transposition_table[board_str] = max_eval, best_move + + print("Maximizing. Maximal evaluation: ", max_eval) + print("best move: ", best_move) + + return max_eval, best_move + + else: + min_eval = MAX_SCORE + + for move in available_moves: + x, y = move + new_board = Board(to_display=False) + new_board.board = copy.deepcopy(board.board) + + if new_board.add_move_to_board(x, y, opponent): + eval_value, _ = self.min_max(new_board, True, timeout, available_moves, depth=depth - 1, + standby_duration=standby_duration, show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking) + + if eval_value is None: + available_moves.append((x, y)) + + elif eval_value < min_eval: + min_eval = eval_value + best_move = (x, y) + + if best_move is None: + best_move = available_moves[0] + min_eval = board.evaluate_board(self) + + self.transposition_table[board_str] = min_eval, best_move + + print("Minimizing. Minimal evaluation: ", min_eval) + print("best move: ", best_move) + + return min_eval, best_move + + def alpha_beta(self, board, alpha, beta, maximizing, timeout, depth, standby_duration=0.2, + show_ai_moves=True, show_score_during_thinking=False): + print("Entering alpha beta minmax") + if show_ai_moves: + board.display_ia_thinking(show_score_during_thinking) + time.sleep(standby_duration) + + board_str = ''.join(''.join(row) for row in board.board) + self.color + best_move = None + current_player = self.color if maximizing else ('W' if self.color == 'B' else 'B') + best_move_so_far = None + + """if time.time() > timeout: + print("timeout") + return None, None""" + + if board_str in self.transposition_table: + print("board in transposition table : ", self.transposition_table[board_str]) + return self.transposition_table[board_str] + + if depth == 0: + print("depth = 0") + score = board.evaluate_board(self) + # self.transposition_table[board_str] = score, None + return score, None + + if maximizing: + max_eval = float('-inf') + move_found = False + + for x in range(8): + + for y in range(8): + new_board = Board(to_display=False) + new_board.board = copy.deepcopy(board.board) + move_made = new_board.add_move_to_board(x, y, current_player) + + if move_made: + move_found = True # Update this flag + eval_value, _ = self.alpha_beta(board=new_board, alpha=alpha, beta=beta, maximizing=False, + timeout=timeout, depth=depth - 1, + standby_duration=standby_duration, show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking) + + if eval_value is None: # Timeout occurred + print("timeout") + return max_eval, best_move_so_far + + if eval_value > max_eval: + max_eval = eval_value + best_move = (x, y) + best_move_so_far = best_move + + alpha = max(alpha, eval_value) + + if beta <= alpha: + break + + if not move_found: # Check the flag here + print("no available moves") + return board.evaluate_board(self), None + + self.transposition_table[board_str] = max_eval, best_move + + print("Maximizing. Maximal evaluation: ", max_eval) + return max_eval, best_move + + else: + min_eval = float('inf') + move_found = False + + for x in range(8): + + for y in range(8): + + new_board = Board(to_display=False) + new_board.board = copy.deepcopy(board.board) + move_made = new_board.add_move_to_board(x, y, current_player) + + if move_made: + move_found = True + + eval_value, _ = self.alpha_beta(board=new_board, alpha=alpha, beta=beta, maximizing=True, + timeout=timeout, depth=depth - 1, + standby_duration=standby_duration, show_ai_moves=show_ai_moves, + show_score_during_thinking=show_score_during_thinking) + + if eval_value is None: # Timeout occurred + print("timeout") + return min_eval, best_move_so_far + + if eval_value < min_eval: + min_eval = eval_value + best_move = (x, y) + best_move_so_far = best_move + + beta = min(beta, eval_value) + + if beta <= alpha: + break + + if not move_found: # Check the flag here + print("no available moves") + return board.evaluate_board(self), None + + self.transposition_table[board_str] = min_eval, best_move + print("Minimizing. Minimal evaluation: ", min_eval) + return min_eval, best_move diff --git a/src/players/human_player.py b/src/players/human_player.py new file mode 100644 index 0000000..8a1100b --- /dev/null +++ b/src/players/human_player.py @@ -0,0 +1,36 @@ +import sys + +import pygame + +from src import settings +from src.players.player import Player + + +class HumanPlayer(Player): + + def __init__(self, color): + super().__init__(color) + self.move_made = False + + def make_move(self, board): + self.move_made = False + while not self.move_made: + + for event in pygame.event.get(): + + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + if event.type == pygame.MOUSEBUTTONDOWN: + x, y = event.pos + row = int((y - settings.TOP_GRID_PADDING) // settings.BOARD_CELL_SIZE) + column = int((x - settings.LEFT_GRID_PADDING) // settings.BOARD_CELL_SIZE) + + if row < 0 or row >= settings.BOARD_SIZE or column < 0 or column >= settings.BOARD_SIZE: + return False + if board.add_move_to_board(row, column, self.color): + self.move_made = True + return True + else: + return False diff --git a/src/players/player.py b/src/players/player.py new file mode 100644 index 0000000..6bc1c2d --- /dev/null +++ b/src/players/player.py @@ -0,0 +1,3 @@ +class Player: + def __init__(self, color): + self.color = color diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..5f41b24 --- /dev/null +++ b/src/settings.py @@ -0,0 +1,35 @@ +import pygame + +# Initialize Pygame +pygame.init() + +# Window settings +WINDOW_WIDTH, WINDOW_HEIGHT = 800, 600 +CAPTION = "Othello" + +# Colors +GREEN = (42, 81, 45) +WHITE = (255, 255, 255) +LIGHT_BLUE = (161, 212, 164) +BLACK = (0, 0, 0) + +# Text settings +FONT = pygame.font.Font(None, 32) +TITLE_FONT = pygame.font.Font(None, 72) + +# Menu settings +OPTIONS = ["Joueur vs. Joueur", "Joueur vs. IA", "IA vs. IA", "Quitter"] + +# Game settings +BOARD_SIZE = 8 +BOARD_CELL_SIZE = 50 +LEFT_GRID_PADDING = (WINDOW_WIDTH - (BOARD_SIZE * BOARD_CELL_SIZE)) // 2 +TOP_GRID_PADDING = (WINDOW_HEIGHT - (BOARD_SIZE * BOARD_CELL_SIZE)) // 2 +MINIMAX_DEPTH = 2 +MAXIMAL_DEPTH = 10 +AVAILABLE_AIS = { + "Minimax": ["Positionnel1", "Positionnel2", "Score", "Mobilité"], + "Alphabeta": ["Positionnel1", "Positionnel2", "Score", "Mobilité"] +} +MAX_TIMEOUT = 60 +MIN_TIMEOUT = 1