Search code examples
pythonartificial-intelligenceminimax

Minimax for hexapawn game


I'm working on a python minimax implementation for Hexapawn but hit a problem: it prints out the correct move for the ai, but it wont make that move.

This is my code:

import copy

class Hexapawn:
    def __init__(self):
        self.board = [[2,2,2],
                      [0,0,0],
                      [1,1,1]]
        self.player = 1

    def display_board(self):
        for row in self.board:
            print(row)

    def ai_turn(self):
            move = self.minimax(3, float('-inf'), float('inf'), True)[1]
            if move:
                self.make_move(move, self.board)
                print("AI moves:", move)
            else:
                print("AI has no valid moves.")
    def player_turn(self):
        player_move = self.get_player_move()
        self.make_move(player_move, self.board)
        if self.is_game_over():
            self.display_board()
            print("Player wins!")
            return
        self.display_board()

    def play_game(self):
        while True:
            self.player_turn()
            self.player = 1 if self.player == 2 else 2
            if self.is_game_over():
                break
            self.ai_turn()
            self.player = 1 if self.player == 2 else 2
            if self.is_game_over():
                self.display_board()
                print("AI wins!")
                break

    def evaluate_board(self):
        player1_pawns = sum(row.count(1) for row in self.board)
        player2_pawns = sum(row.count(2) for row in self.board)
        return player1_pawns- player2_pawns
    def get_player_move(self):
        while True:
            try:
                orow, ocol = map(int, input("Enter row and column of the pawn you want to move (e.g., 0 1): ").split())
                nrow, ncol = map(int, input("Enter row and column of the destination (e.g., 1 1): ").split())
                move = ((orow, ocol), (nrow, ncol))
                if move in self.get_possible_moves():
                    return move
                else:
                    print("Invalid move. Try again.")
            except ValueError:
                print("Invalid input. Please enter row and column numbers separated by a space.")
    def make_move(self, move, board):
        orow,ocol = move[0]
        nrow,ncol = move[1]
        board[nrow][ncol] = board[orow][ocol]
        board[orow][ocol] = 0
    def undo_move(self, move, board):
        orow, ocol = move[0]
        nrow, ncol = move[1]
        board[orow][ocol] = board[nrow][ncol]
        board[nrow][ncol] = 0   
    def is_game_over(self):
        if not self.get_possible_moves():
            return True
        for col in range(len(self.board[0])):
            if self.board[0][col] == 1 or self.board[2][col] == 2:
                return True

        return False
    
        

    def get_possible_moves(self):
        possible = []
        opponent=2 if self.player == 1 else 1

        for row in range(len(self.board)):
            for col in range(len(self.board[row])):
                if self.board[row][col] == self.player:
                    if self.player == 1:
                        #diagonal
                        #r
                        if row -1 >= 0 and col +1 <= 2 and self.board[row-1][col+1] == opponent:
                            possible.append(((row,col),(row-1,col+1)))
                        #l
                        if row-1 >= 0 and col-1 >= 0 and self.board[row-1][col-1] == opponent:
                            possible.append(((row,col),(row-1,col-1)))
                        #vert
                        if row -1 >= 0 and self.board[row-1][col] == 0:
                            possible.append(((row,col),(row-1,col)))
                    elif self.player == 2:
                        #diag
                        #r
                        if row+1 <= 2 and col +1 <= 2 and self.board[row+1][col+1] == opponent:
                            possible.append(((row,col), (row+1, col+1)))
                        #l
                        if row+1 <= 2 and col -1 >= 0 and self.board[row+1][col-1] == opponent:
                            possible.append(((row,col),(row+1, col-1)))
                        #vert
                        if row+1 <= 2 and self.board[row+1][col] == 0:
                            possible.append(((row,col), (row+1, col)))
        return possible

                    
    def minimax(self, depth, alpha, beta, maximizing_player):
        if depth == 0 or self.is_game_over():
            return self.evaluate_board(), None
        board = copy.deepcopy(self.board)

        if maximizing_player:
            max_eval = float('-inf')
            best_move = None
            for move in self.get_possible_moves():
                self.make_move(move, board)
                evaluate = self.minimax(depth-1,alpha, beta, False)[0]
                self.undo_move(move, board)
                if evaluate > max_eval:
                    max_eval = evaluate
                    best_move = move
                alpha = max(alpha, evaluate)
                if beta <= alpha:
                    break
            return max_eval, best_move

        else:
            min_eval= float('inf')
            for move in self.get_possible_moves():
                self.make_move(move, board)
                evaluate = self.minimax(depth-1, alpha, beta, True)[0]
                self.undo_move(move, board)
                if evaluate<min_eval:
                    min_eval = evaluate
                beta = min(beta, evaluate)
                if beta <= alpha:
                    break
            return min_eval, None

if __name__ == "__main__":
    game = Hexapawn()
    game.display_board()
    game.play_game()

I've tried to figure out why the AI move isn't reflected on the board, but without success. What am I missing?


Solution

  • it prints out the correct move for the ai but it wont make that move

    Actually, the AI move is made. But to actually see the result, you should print the board just after that move was made:

        def ai_turn(self):
            move = self.minimax(3, float('-inf'), float('inf'), True)[1]
            if move:
                self.make_move(move, self.board)
                print("AI moves:", move)
                self.display_board()  # <----------------- add this
            else:
                print("AI has no valid moves.")
    

    Other remarks

    The above answers your direct question, but your implementation has several issues:

    • During the minimax search, the turn of the player is not changed, i.e. self.player isn't changed, which means that get_possible_moves will return only moves by one and the same player throughout the minimax search. To fix this, I would suggest that self.player is toggled in the functions make_move and undo_move, and only there. And once you have that in place, you don't need the maximizing_player parameter, as it is implied by self.player.

    • minimax takes a copy of self.board into a local variable board, and plays the possible moves on that copy, but then the recursive call does not have any reference to that copy and will make a new copy from the initial self.board, ignoring the move that was just played. I would suggest to not take a copy of the board, and just play directly on self.board. As you always perform an undo of each move, you'll end up with the original board without any issues. This also means that the functions which take a board parameter (like make_move) can drop that parameter and work on self.board instead.

    • When the game is over, the evaluate function is used for the minimax score. But this function has two issues:

      1. The function maximizes the score for player 1, but the minimax function is called with the AI player (player 2) as maximizing player, so this will counter a good evaluation.

      2. The function does not favour a win over a non-ended game. For instance, here we have a won game at the left, and a non-won game at the right, both with the last move played by player 2:

        . . .       . 2 .
        2 1 2       1 1 .
        . . .       . . 2
        

        After correcting the first issue, the evaluation function will favor, for player 2, the board at the left side, because it has more pieces on the board than the opponent, but the right board is clearly the better one, because it is a win. Either first detect wins and give priority scores to such boards, or else give more points to pieces that are more advanced on the board.