269 lines
9.3 KiB
Python
269 lines
9.3 KiB
Python
import random
|
|
|
|
|
|
class TicTacToe:
|
|
def __init__(self, ai_player="O", ai_difficulty=None):
|
|
"""
|
|
Initialize a new Tic Tac Toe game.
|
|
|
|
Parameters:
|
|
ai_player (str): The player that the AI controls ('X' or 'O').
|
|
ai_difficulty (str): AI difficulty level. Should be one of:
|
|
'random' - chooses a random valid move.
|
|
'rule' - uses simple rules (win, block, take center/corner).
|
|
'minimax' - uses the minimax algorithm for perfect play.
|
|
None - no AI moves; both players are human.
|
|
"""
|
|
self.board = [" "] * 9 # 3x3 board represented in a list.
|
|
self.current_player = "X"
|
|
self.winner = None
|
|
self.game_over = False
|
|
# If no AI difficulty is provided, no player is controlled by the computer.
|
|
self.ai_player = ai_player if ai_difficulty is not None else None
|
|
self.ai_difficulty = ai_difficulty
|
|
|
|
def reset(self):
|
|
"""Reset the game to its initial state."""
|
|
self.board = [" "] * 9
|
|
self.current_player = "X"
|
|
self.winner = None
|
|
self.game_over = False
|
|
|
|
def get_board(self):
|
|
"""Return a copy of the current board."""
|
|
return self.board[:]
|
|
|
|
def play_turn(self, position=None):
|
|
"""
|
|
Play one turn of the game.
|
|
|
|
If it is the human's turn, you must supply the 'position' (an integer from 0 to 8).
|
|
If it is the AI's turn, you may call play_turn() without a position; the AI
|
|
will pick and execute its move automatically and return the move's index.
|
|
|
|
Returns:
|
|
int: The position where the move was made.
|
|
|
|
Raises:
|
|
ValueError: If the move is invalid or if the game is already over.
|
|
"""
|
|
if self.game_over:
|
|
raise ValueError("Game is over.")
|
|
# If the current player is controlled by AI, ignore any supplied position.
|
|
if self.current_player == self.ai_player:
|
|
move = self._select_ai_move()
|
|
self._make_move(move)
|
|
return move
|
|
else:
|
|
if position is None:
|
|
raise ValueError("Human move required. Provide a position (0-8).")
|
|
self._make_move(position)
|
|
return position
|
|
|
|
def _make_move(self, position):
|
|
"""Internal method to update the board with the current player's move."""
|
|
if self.game_over:
|
|
raise ValueError("Game is over.")
|
|
if self.board[position] != " ":
|
|
raise ValueError("Invalid move; spot already taken.")
|
|
self.board[position] = self.current_player
|
|
self._check_game_over()
|
|
if not self.game_over:
|
|
self._switch_player()
|
|
|
|
def _switch_player(self):
|
|
"""Switch the turn to the other player."""
|
|
self.current_player = "O" if self.current_player == "X" else "X"
|
|
|
|
def _check_game_over(self):
|
|
"""Check the board for a win or tie."""
|
|
win_combinations = [
|
|
[0, 1, 2],
|
|
[3, 4, 5],
|
|
[6, 7, 8], # Rows
|
|
[0, 3, 6],
|
|
[1, 4, 7],
|
|
[2, 5, 8], # Columns
|
|
[0, 4, 8],
|
|
[2, 4, 6], # Diagonals
|
|
]
|
|
for combo in win_combinations:
|
|
if (
|
|
self.board[combo[0]]
|
|
== self.board[combo[1]]
|
|
== self.board[combo[2]]
|
|
!= " "
|
|
):
|
|
self.winner = self.board[combo[0]]
|
|
self.game_over = True
|
|
return
|
|
if " " not in self.board:
|
|
self.game_over = True # It's a tie.
|
|
|
|
def is_game_over(self):
|
|
"""Return whether the game is over."""
|
|
return self.game_over
|
|
|
|
def get_winner(self):
|
|
"""Return the winner ('X' or 'O'); if it's a tie, returns None."""
|
|
return self.winner
|
|
|
|
def get_current_player(self):
|
|
"""Return the marker of the player whose turn it is."""
|
|
return self.current_player
|
|
|
|
def _get_valid_moves(self, board):
|
|
"""Return a list of valid move positions given a board state."""
|
|
return [i for i, spot in enumerate(board) if spot == " "]
|
|
|
|
def _select_ai_move(self):
|
|
"""Select an AI move based on the chosen difficulty."""
|
|
if self.ai_difficulty == "random":
|
|
return self._ai_random_move()
|
|
elif self.ai_difficulty == "rule":
|
|
return self._ai_rule_move()
|
|
elif self.ai_difficulty == "minimax":
|
|
return self._ai_minimax_move()
|
|
else:
|
|
raise ValueError("Invalid AI difficulty.")
|
|
|
|
def _ai_random_move(self):
|
|
"""AI randomly selects one of the available moves."""
|
|
valid_moves = self._get_valid_moves(self.board)
|
|
return random.choice(valid_moves)
|
|
|
|
def _ai_rule_move(self):
|
|
"""
|
|
AI selects a move based on simple rules:
|
|
1. Take a winning move if available.
|
|
2. Block the opponent's winning move.
|
|
3. Take the center if available.
|
|
4. Take one of the corners.
|
|
5. Otherwise, pick a random move.
|
|
"""
|
|
valid_moves = self._get_valid_moves(self.board)
|
|
# 1. Check for a winning move.
|
|
for move in valid_moves:
|
|
board_copy = self.board[:]
|
|
board_copy[move] = self.ai_player
|
|
if self._check_win(board_copy, self.ai_player):
|
|
return move
|
|
# 2. Check for a move to block the opponent.
|
|
opponent = "X" if self.ai_player == "O" else "O"
|
|
for move in valid_moves:
|
|
board_copy = self.board[:]
|
|
board_copy[move] = opponent
|
|
if self._check_win(board_copy, opponent):
|
|
return move
|
|
# 3. Take the center if it is free.
|
|
if 4 in valid_moves:
|
|
return 4
|
|
# 4. Choose one of the available corners.
|
|
corners = [i for i in [0, 2, 6, 8] if i in valid_moves]
|
|
if corners:
|
|
return random.choice(corners)
|
|
# 5. Fallback: random move.
|
|
return self._ai_random_move()
|
|
|
|
def _check_win(self, board, player):
|
|
"""
|
|
Helper method to check if a given board state is winning for a player.
|
|
|
|
Returns True if the player has a winning combination.
|
|
"""
|
|
win_combinations = [
|
|
[0, 1, 2],
|
|
[3, 4, 5],
|
|
[6, 7, 8],
|
|
[0, 3, 6],
|
|
[1, 4, 7],
|
|
[2, 5, 8],
|
|
[0, 4, 8],
|
|
[2, 4, 6],
|
|
]
|
|
for combo in win_combinations:
|
|
if board[combo[0]] == board[combo[1]] == board[combo[2]] == player:
|
|
return True
|
|
return False
|
|
|
|
def _ai_minimax_move(self):
|
|
"""AI selects a move using the minimax algorithm."""
|
|
score, move = self._minimax(self.board, True, 0)
|
|
return move
|
|
|
|
def _minimax(self, board, is_maximizing, depth):
|
|
"""
|
|
Minimax algorithm to evaluate board positions.
|
|
|
|
Parameters:
|
|
board (list): The current board state.
|
|
is_maximizing (bool): True if the AI should maximize the score.
|
|
depth (int): Current depth of recursion (used for score adjustment).
|
|
|
|
Returns:
|
|
tuple: (score, move) where score is the evaluation of the board,
|
|
and move is the best move to make.
|
|
"""
|
|
opponent = "X" if self.ai_player == "O" else "O"
|
|
if self._check_win(board, self.ai_player):
|
|
return 10 - depth, None
|
|
if self._check_win(board, opponent):
|
|
return depth - 10, None
|
|
if " " not in board:
|
|
return 0, None
|
|
|
|
valid_moves = self._get_valid_moves(board)
|
|
|
|
if is_maximizing:
|
|
best_score = -float("inf")
|
|
best_move = None
|
|
for move in valid_moves:
|
|
board_copy = board[:]
|
|
board_copy[move] = self.ai_player
|
|
score, _ = self._minimax(board_copy, False, depth + 1)
|
|
if score > best_score:
|
|
best_score = score
|
|
best_move = move
|
|
return best_score, best_move
|
|
else:
|
|
best_score = float("inf")
|
|
best_move = None
|
|
for move in valid_moves:
|
|
board_copy = board[:]
|
|
board_copy[move] = opponent
|
|
score, _ = self._minimax(board_copy, True, depth + 1)
|
|
if score < best_score:
|
|
best_score = score
|
|
best_move = move
|
|
return best_score, best_move
|
|
|
|
|
|
# Example usage:
|
|
# from tictactoe import TicTacToe
|
|
|
|
# # Create a game with the AI controlling 'O' using minimax (perfect play).
|
|
# game = TicTacToe(ai_player='O', ai_difficulty='minimax')
|
|
|
|
# # Game loop example:
|
|
# while not game.is_game_over():
|
|
# print("Current board:", game.get_board())
|
|
# if game.get_current_player() != game.ai_player:
|
|
# # Human move: get input (for example, via input() or from a UI)
|
|
# try:
|
|
# pos = int(input("Enter your move (0-8): "))
|
|
# game.play_turn(pos)
|
|
# except ValueError as ve:
|
|
# print("Error:", ve)
|
|
# else:
|
|
# # AI move: simply call play_turn() without a parameter.
|
|
# ai_move = game.play_turn()
|
|
# print(f"AI chose position {ai_move}")
|
|
|
|
# # After game is over, show the final result.
|
|
# print("Final board:", game.get_board())
|
|
# winner = game.get_winner()
|
|
# if winner:
|
|
# print(f"Winner: {winner}")
|
|
# else:
|
|
# print("It's a tie!")
|