diff --git a/game.py b/game.py index 35f4a1a..bd2f221 100644 --- a/game.py +++ b/game.py @@ -9,34 +9,121 @@ Programmer Beast Mode Spotify playlist: https://open.spotify.com/playlist/4Akns5EUb3gzmlXIdsJkPs?si=qGc4ubKRRYmPHAJAIrCxVQ """ +# Developed by phoenix marie. import math import time -from player import HumanPlayer, RandomComputerPlayer, SmartComputerPlayer +from random import randint +import json # For saving and loading game history + + +class Player(): + def __init__(self, letter): + # letter is x or o + self.letter = letter + + def get_move(self, game): + pass + + +class HumanPlayer(Player): + def __init__(self, letter): + super().__init__(letter) + + def get_move(self, game): + valid_square = False + val = None + while not valid_square: + square = input(self.letter + '\'s turn. Input move (0-8): ') + try: + val = int(square) + if val not in game.available_moves(): + raise ValueError + valid_square = True + except ValueError: + print('Invalid square. Please try again.') + return val + + +class RandomComputerPlayer(Player): + def __init__(self, letter): + super().__init__(letter) + + def get_move(self, game): + square = randint(0, 8) + while square not in game.available_moves(): + square = randint(0, 8) + return square + + +class SmartComputerPlayer(Player): + def __init__(self, letter): + super().__init__(letter) + + def get_move(self, game): + if len(game.available_moves()) == 9: + square = randint(0, 8) # choose one at random + else: + square = self.minimax(game, self.letter)['position'] + return square + + def minimax(self, state, player): + max_player = self.letter # yourself + other_player = 'O' if player == 'X' else 'X' + + # base case: if the previous move made someone win + if state.current_winner == other_player: + return {'position': None, 'score': 1 * (state.num_empty_squares() + 1) if other_player == max_player else -1 * ( + state.num_empty_squares() + 1)} + elif not state.empty_squares(): + return {'position': None, 'score': 0} + + if player == max_player: + best = {'position': None, 'score': -math.inf} # each score should maximize + else: + best = {'position': None, 'score': math.inf} # each score should minimize + for possible_move in state.available_moves(): + state.make_move(possible_move, player) + sim_score = self.minimax(state, other_player) # simulate a game after making that move + + # undo move + state.board[possible_move] = ' ' + state.current_winner = None + sim_score['position'] = possible_move # this represents the move consistent with the score + + if player == max_player: # maximize over the max player + if sim_score['score'] > best['score']: + best = sim_score + else: # minimize over the other player + if sim_score['score'] < best['score']: + best = sim_score + return best class TicTacToe(): def __init__(self): self.board = self.make_board() self.current_winner = None + self.move_history = [] # To store the moves made in the current game @staticmethod def make_board(): return [' ' for _ in range(9)] def print_board(self): - for row in [self.board[i*3:(i+1) * 3] for i in range(3)]: + for row in [self.board[i * 3:(i + 1) * 3] for i in range(3)]: print('| ' + ' | '.join(row) + ' |') @staticmethod def print_board_nums(): # 0 | 1 | 2 - number_board = [[str(i) for i in range(j*3, (j+1)*3)] for j in range(3)] + number_board = [[str(i) for i in range(j * 3, (j + 1) * 3)] for j in range(3)] for row in number_board: print('| ' + ' | '.join(row) + ' |') def make_move(self, square, letter): if self.board[square] == ' ': self.board[square] = letter + self.move_history.append((square, letter)) # Record the move if self.winner(square, letter): self.current_winner = letter return True @@ -45,12 +132,12 @@ def make_move(self, square, letter): def winner(self, square, letter): # check the row row_ind = math.floor(square / 3) - row = self.board[row_ind*3:(row_ind+1)*3] + row = self.board[row_ind * 3:(row_ind + 1) * 3] # print('row', row) if all([s == letter for s in row]): return True col_ind = square % 3 - column = [self.board[col_ind+i*3] for i in range(3)] + column = [self.board[col_ind + i * 3] for i in range(3)] # print('col', column) if all([s == letter for s in column]): return True @@ -74,9 +161,42 @@ def num_empty_squares(self): def available_moves(self): return [i for i, x in enumerate(self.board) if x == " "] + def is_tie(self): + return not self.empty_squares() and self.current_winner is None -def play(game, x_player, o_player, print_game=True): + def reset_board(self): + self.board = self.make_board() + self.current_winner = None + self.move_history = [] + + def get_game_state(self): + """Returns the current state of the game as a dictionary.""" + return { + 'board': list(self.board), + 'current_winner': self.current_winner, + 'move_history': list(self.move_history) + } + def load_game_state(self, game_state): + """Loads a game state from a dictionary.""" + if 'board' in game_state and len(game_state['board']) == 9: + self.board = list(game_state['board']) + if 'current_winner' in game_state: + self.current_winner = game_state['current_winner'] + if 'move_history' in game_state: + self.move_history = list(game_state['move_history']) + + def display_move_history(self): + """Prints the history of moves made in the current game.""" + if self.move_history: + print("\n--- Move History ---") + for move, player in self.move_history: + print(f"Player {player} moved to square {move}") + else: + print("No moves have been made yet.") + + +def play(game, x_player, o_player, print_game=True): if print_game: game.print_board_nums() @@ -95,19 +215,141 @@ def play(game, x_player, o_player, print_game=True): if game.current_winner: if print_game: - print(letter + ' wins!') + print(letter + ' wins!") return letter # ends the loop and exits the game letter = 'O' if letter == 'X' else 'X' # switches player time.sleep(.8) if print_game: - print('It\'s a tie!') + if game.is_tie(): + print('It\'s a tie!') + elif game.current_winner: + print(game.current_winner + ' wins!') + return game.current_winner # Return the winner or None for a tie + + +def play_again(): + while True: + response = input("Do you want to play again? (yes/no): ").lower() + if response in ['yes', 'y']: + return True + elif response in ['no', 'n']: + return False + else: + print("Invalid input. Please enter 'yes' or 'no'.") + + +def get_player_type(player_char): + while True: + player_choice = input(f"Choose the type for Player {player_char} (human/random/smart): ").lower() + if player_choice == 'human': + return HumanPlayer(player_char) + elif player_choice == 'random': + return RandomComputerPlayer(player_char) + elif player_choice == 'smart': + return SmartComputerPlayer(player_char) + else: + print("Invalid choice. Please enter 'human', 'random', or 'smart'.") + + +def save_game(game, filename="tictactoe_save.json"): + """Saves the current game state to a JSON file.""" + game_data = game.get_game_state() + try: + with open(filename, 'w') as f: + json.dump(game_data, f) + print(f"Game saved to {filename}") + return True + except IOError: + print(f"Error saving game to {filename}") + return False + + +def load_game(filename="tictactoe_save.json"): + """Loads a game state from a JSON file.""" + try: + with open(filename, 'r') as f: + game_data = json.load(f) + game = TicTacToe() + game.load_game_state(game_data) + print(f"Game loaded from {filename}") + return game + except FileNotFoundError: + print(f"Save file {filename} not found. Starting a new game.") + return TicTacToe() + except json.JSONDecodeError: + print(f"Error decoding JSON from {filename}. Starting a new game.") + return TicTacToe() + except IOError: + print(f"Error reading file {filename}. Starting a new game.") + return TicTacToe() + + +def display_game_history(history): + """Displays a list of game outcomes.""" + if not history: + print("No game history available.") + return + + print("\n--- Game History ---") + for i, result in enumerate(history): + if result == 'X': + print(f"Game {i+1}: X wins") + elif result == 'O': + print(f"Game {i+1}: O wins") + else: + print(f"Game {i+1}: Tie") + + +def play_multiple_games(): + x_wins = 0 + o_wins = 0 + ties = 0 + game_history = [] + + while True: + print("\n--- New Game ---") + x_player = get_player_type('X') + o_player = get_player_type('O') + game = TicTacToe() + + # Option to load a saved game at the start of multiple games + load_option = input("Load saved game? (yes/no): ").lower() + if load_option in ['yes', 'y']: + loaded_game = load_game() + if loaded_game: + game = loaded_game + game.print_board() + game.display_move_history() + + winner = play(game, x_player, o_player) + game_history.append(winner) + + if winner == 'X': + x_wins += 1 + elif winner == 'O': + o_wins += 1 + else: + ties += 1 + + print(f"\n--- Game Results ---") + print(f"X Wins: {x_wins}") + print(f"O Wins: {o_wins}") + print(f"Ties: {ties}") + + game.display_move_history() + + save_option = input("Save this game? (yes/no): ").lower() + if save_option in ['yes', 'y']: + save_game(game) + + if not play_again(): + break + display_game_history(game_history) if __name__ == '__main__': - x_player = SmartComputerPlayer('X') - o_player = HumanPlayer('O') - t = TicTacToe() - play(t, x_player, o_player, print_game=True) + play_multiple_games() + diff --git a/player.py b/player.py index 252cdbe..fb17583 100644 --- a/player.py +++ b/player.py @@ -9,19 +9,42 @@ Programmer Beast Mode Spotify playlist: https://open.spotify.com/playlist/4Akns5EUb3gzmlXIdsJkPs?si=qGc4ubKRRYmPHAJAIrCxVQ """ +# Developed by phoenix marie. import math import random class Player(): - def __init__(self, letter): + """Base class for a player.""" + def __init__(self, letter: str): + """ + Initializes a Player instance. + + Args: + letter (str): The identifier for the player. + """ + if not isinstance(letter, str) or len(letter) != 1: + raise ValueError("Player letter must be a single character string.") self.letter = letter def get_move(self, game): - pass + """ + Determines and returns the player's move in the given game. + + Args: + game: The current game state or object. + + Returns: + The player's chosen move. The specific type depends on the game. + + Raises: + NotImplementedError: This method should be implemented by subclasses. + """ + raise NotImplementedError("Subclasses must implement the get_move method.") class HumanPlayer(Player): + """Represents a human player.""" def __init__(self, letter): super().__init__(letter) @@ -41,6 +64,7 @@ def get_move(self, game): class RandomComputerPlayer(Player): + """Represents a computer player making random moves.""" def __init__(self, letter): super().__init__(letter) @@ -50,6 +74,7 @@ def get_move(self, game): class SmartComputerPlayer(Player): + """Represents a computer player using the minimax algorithm.""" def __init__(self, letter): super().__init__(letter) @@ -64,7 +89,7 @@ def minimax(self, state, player): max_player = self.letter # yourself other_player = 'O' if player == 'X' else 'X' - # first we want to check if the previous move is a winner + # Base cases: check for winner or tie if state.current_winner == other_player: return {'position': None, 'score': 1 * (state.num_empty_squares() + 1) if other_player == max_player else -1 * ( state.num_empty_squares() + 1)} @@ -72,22 +97,250 @@ def minimax(self, state, player): return {'position': None, 'score': 0} if player == max_player: - best = {'position': None, 'score': -math.inf} # each score should maximize + best = {'position': None, 'score': -math.inf} # Maximize score else: - best = {'position': None, 'score': math.inf} # each score should minimize + best = {'position': None, 'score': math.inf} # Minimize score + for possible_move in state.available_moves(): - state.make_move(possible_move, player) - sim_score = self.minimax(state, other_player) # simulate a game after making that move + state.make_move(possible_move, player, record=False) # Simulate move + sim_score = self.minimax(state, other_player) # Recursive call - # undo move + # Undo move state.board[possible_move] = ' ' state.current_winner = None - sim_score['position'] = possible_move # this represents the move optimal next move + sim_score['position'] = possible_move - if player == max_player: # X is max player + if player == max_player: if sim_score['score'] > best['score']: best = sim_score else: if sim_score['score'] < best['score']: best = sim_score return best + + +class TicTacToe(): + """Represents the Tic Tac Toe game.""" + def __init__(self): + self.board = self.make_board() + self.current_winner = None + self.move_history = [] # Keep track of moves made + + def make_board(self): + return [' '] * 9 + + def print_board(self): + for row in [self.board[i*3:(i+1)*3] for i in range(3)]: + print('| ' + ' | '.join(row) + ' |') + + @staticmethod + def print_board_nums(): + number_board = [[str(i) for i in range(j*3, (j+1)*3)] for j in range(3)] + for row in number_board: + print('| ' + ' | '.join(row) + ' |') + + def available_moves(self): + return [i for i, spot in enumerate(self.board) if spot == ' '] + + def empty_squares(self): + return ' ' in self.board + + def num_empty_squares(self): + return self.board.count(' ') + + def make_move(self, square, letter, record=True): + if self.board[square] == ' ': + self.board[square] = letter + if record: + self.move_history.append((square, letter)) + if self.winner(square, letter): + self.current_winner = letter + return True + return False + + def undo_move(self): + if self.move_history: + last_move, last_letter = self.move_history.pop() + self.board[last_move] = ' ' + self.current_winner = None + + def winner(self, square, letter): + row_ind = square // 3 + row = self.board[row_ind*3 : (row_ind + 1) * 3] + if all([spot == letter for spot in row]): + return True + col_ind = square % 3 + column = [self.board[col_ind+i*3] for i in range(3)] + if all([spot == letter for spot in column]): + return True + if square % 2 == 0: + diagonal1 = [self.board[i] for i in [0, 4, 8]] + if all([spot == letter for spot in diagonal1]): + return True + diagonal2 = [self.board[i] for i in [2, 4, 6]] + if all([spot == letter for spot in diagonal2]): + return True + return False + + def get_board_copy(self): + return self.board[:] + + def is_full(self): + return ' ' not in self.board + + def get_winning_combinations(self): + return [ + self.board[0:3], self.board[3:6], self.board[6:9], # Rows + [self.board[i] for i in [0, 3, 6]], [self.board[i] for i in [1, 4, 7]], [self.board[i] for i in [2, 5, 8]], # Columns + [self.board[i] for i in [0, 4, 8]], [self.board[i] for i in [2, 4, 6]] # Diagonals + ] + + def check_win(self, letter): + for combo in self.get_winning_combinations(): + if all(spot == letter for spot in combo): + return True + return False + + def get_potential_winning_moves(self, letter): + winning_moves = [] + for move in self.available_moves(): + temp_board = self.get_board_copy() + temp_board[move] = letter + temp_game = TicTacToe() + temp_game.board = temp_board + if temp_game.check_win(letter): + winning_moves.append(move) + return winning_moves + + def get_potential_blocking_moves(self, letter): + opponent_letter = 'O' if letter == 'X' else 'X' + return self.get_potential_winning_moves(opponent_letter) + + def evaluate_board(self, maximizing_player): + if self.check_win(maximizing_player.letter): + return 1 + elif self.check_win('O' if maximizing_player.letter == 'X' else 'X'): + return -1 + elif self.is_full(): + return 0 + else: + return 0 + + def get_empty_board_indices(self): + return [i for i, spot in enumerate(self.board) if spot == ' '] + + def get_occupied_board_indices(self): + occupied = {'X': [], 'O': []} + for i, spot in enumerate(self.board): + if spot == 'X': + occupied['X'].append(i) + elif spot == 'O': + occupied['O'].append(i) + return occupied + + def print_move_history(self): + if not self.move_history: + print("No moves have been made yet.") + else: + print("Move History:") + for move, letter in self.move_history: + print(f"Player {letter} moved to square {move}") + + def check_future_win(self, letter, depth=2): + if depth == 0: + return False + + for move in self.available_moves(): + temp_board = self.get_board_copy() + temp_board[move] = letter + temp_game = TicTacToe() + temp_game.board = temp_board + if temp_game.check_win(letter): + return True + opponent_letter = 'O' if letter == 'X' else 'X' + can_opponent_block_future_win = False + for opponent_move in temp_game.available_moves(): + temp_board_opponent = temp_game.get_board_copy() + temp_board_opponent[opponent_move] = opponent_letter + temp_game_opponent = TicTacToe() + temp_game_opponent.board = temp_board_opponent + if temp_game_opponent.get_potential_winning_moves(letter): + can_opponent_block_future_win = True + break + if not can_opponent_block_future_win and temp_game.empty_squares(): + return temp_game.check_future_win(letter, depth - 1) + return False + + +def play(game, x_player, o_player, print_game=True): + if print_game: + game.print_board_nums() + + letter = 'X' + while True: + if letter == 'O': + square = o_player.get_move(game) + else: + square = x_player.get_move(game) + + if game.make_move(square, letter): + if print_game: + print(f"{letter} makes a move to square {square}") + game.print_board() + print('') + + if game.current_winner: + if print_game: + print(f"{letter} wins!") + return letter + + if not game.empty_squares(): + if print_game: + print("It's a tie!") + return None + + letter = 'O' if letter == 'X' else 'X' + else: + if isinstance(x_player, HumanPlayer) and letter == 'X' or isinstance(o_player, HumanPlayer) and letter == 'O': + print('That square is already taken. Try again.') + + +if __name__ == '__main__': + x_wins = 0 + o_wins = 0 + ties = 0 + num_games = 1 + for _ in range(num_games): + x_player = SmartComputerPlayer('X') + o_player = HumanPlayer('O') + t = TicTacToe() + result = play(t, x_player, o_player, print_game=True) + if result == 'X': + x_wins += 1 + elif result == 'O': + o_wins += 1 + else: + ties += 1 + + print(f'After {num_games} games, X won {x_wins} times, O won {o_wins} times, and there were {ties} ties') + + # Example of using the new functions: + game = TicTacToe() + game.make_move(0, 'X') + game.make_move(4, 'O') + print("\nExample using new functions:") + print("Current board:") + game.print_board() + board_copy = game.get_board_copy() + print("Copy of the board:", board_copy) + print("Is the board full?", game.is_full()) + print("Winning combinations:", game.get_winning_combinations()) + print("Does X win?", game.check_win('X')) + print("Does O win?", game.check_win('O')) + print("Potential winning moves for X:", game.get_potential_winning_moves('X')) + print("Potential blocking moves for X:", game.get_potential_blocking_moves('X')) + print("Empty board indices:", game.get_empty_board_indices()) + print("Occupied board indices:", game.get_occupied_board_indices()) + game.print_move_history() + print("Can X win in the next 2 moves?", game.check_future_win('X', depth=2)) +