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?
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.")
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:
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.
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.