Source code for othello

import numpy as np
import os
import copy

# A game of Othello (Reversi)
# Players take turns placing tokens on an 8x8 board.
# When you place a token, all opponent tokens "sandwiched"
# between your new token and an existing token of yours
# (in any direction) are flipped to your color.
# The player with the most tokens when neither player
# can move wins.

# Game parameters
n = 8       # board size (n x n), standard Othello uses 8

n_players = 2

# Symbols for players
P1 = "X"
P2 = "O"
PSYM = [" ", P1, P2]

# Special move representing a forced pass (no valid moves available)
PASS_MOVE = None

# All 8 directions to check for flips
DIRECTIONS = [(-1,-1), (-1, 0), (-1, 1),
              ( 0,-1),          ( 0, 1),
              ( 1,-1), ( 1, 0), ( 1, 1)]


[docs]def new_game(): """ Creates the standard Othello starting position. The four central squares are pre-filled. The state is a list [grid, player] where player is the current player. This allows state-only functions to always know whose turn it is. Returns: list: [grid, player] as the initial game state """ grid = np.zeros([n, n], dtype=int) mid = n // 2 grid[mid-1, mid-1] = 1 grid[mid-1, mid ] = 2 grid[mid, mid-1] = 2 grid[mid, mid ] = 1 return [grid, 1] # player 1 starts
[docs]def next_player(player): """ Number of the player whose turn comes after the given player. Note: Player numbering starts from 1 and ends at n_players. Args: player (int): the player to check Returns: int: the number of the next player """ return player % n_players + 1
[docs]def previous_player(player): """ Number of the player whose turn comes before the given player. Note: Player numbering starts from 1 and ends at n_players. Args: player (int): the player to check Returns: int: the number of the previous player """ return player % n_players + 1
def _get_flips(row, col, player, grid): """ For a proposed move at (row, col) by player, find all opponent tokens that would be flipped. Args: row (int): row index of the proposed move col (int): column index of the proposed move player (int): the player making the move grid (array): the play area Returns: list: (row, col) positions that would be flipped """ opponent = next_player(player) flips = [] for dr, dc in DIRECTIONS: r, c = row + dr, col + dc candidates = [] # Walk in this direction while we see opponent tokens while 0 <= r < n and 0 <= c < n and grid[r, c] == opponent: candidates.append((r, c)) r += dr c += dc # If we stopped on our own token, the sandwiched pieces flip if candidates and 0 <= r < n and 0 <= c < n and grid[r, c] == player: flips.extend(candidates) return flips def _is_valid_move(row, col, player, grid): """ Checks if placing a token at (row, col) is a valid move for player. A move is valid if the square is empty and flips at least one opponent. Args: row (int): row index of the proposed move col (int): column index of the proposed move player (int): the player making the move grid (array): the play area Returns: boolean: True if the move is valid, False otherwise """ if grid[row, col] != 0: return False return len(_get_flips(row, col, player, grid)) > 0 def _all_plays(player, grid): """ Lists all valid moves for player as (row, col) tuples. Args: player (int): the player making the move grid (array): the play area Returns: list: valid (row, col) moves """ moves = [] for i in range(n): for j in range(n): if _is_valid_move(i, j, player, grid): moves.append((i, j)) return moves
[docs]def all_reasonable_plays(state, player): """ Lists all valid moves for the current player. In Othello all valid moves are reasonable. If the current player has no valid moves, returns [PASS_MOVE] so the game loop can still make a move (which does nothing). Args: state (list): [grid, player] Returns: list: (row, col) moves, or [PASS_MOVE] if none exist """ grid, pl = state moves = _all_plays(player, grid) if not moves: return [PASS_MOVE] return moves
[docs]def can_continue(state): """ Checks if the game can continue. The game ends when BOTH players have no valid moves. A single player having no moves does not end the game since they can pass their turn. Args: state (list): [grid, player] Returns: boolean: True if at least one player can still move, False otherwise """ grid, player = state if _all_plays(player, grid): return True if _all_plays(next_player(player), grid): return True return False
[docs]def check_winner(state): """ Determines the winner when the game is over. Returns 0 if the game is still in progress. The game is over when neither player has valid moves. The player with the most tokens then wins. Args: state (list): [grid, player] Returns: int: the winning player number, or 0 for a draw or unfinished game """ grid, player = state # If the game can still continue, there is no winner yet if can_continue(state): return 0 count1 = np.sum(grid == 1) count2 = np.sum(grid == 2) if count1 > count2: return 1 elif count2 > count1: return 2 else: return 0
[docs]def make_move(play, player, state): """ Let a player place a token and flip the appropriate opponent tokens. Modifies state in place. Also updates state[1] to the next player so that state-only functions always know whose turn it is. If play is PASS_MOVE, the board is unchanged and the turn passes. Args: play (array): (row, col), or PASS_MOVE player (int): number of the player who makes the move state (list): [grid, player] """ grid = state[0] if play is not PASS_MOVE: row, col = play flips = _get_flips(row, col, player, grid) grid[row, col] = player for r, c in flips: grid[r, c] = player # Update the stored current player to the next one state[1] = next_player(player)
[docs]def copy_game(state): """ Returns a deep copy of the game state. Args: state (list): [grid, player] Returns: list: a copy of state """ grid, player = state return [copy.deepcopy(grid), player]
[docs]def draw(state): """ Draw the board with text graphics, showing token counts and valid moves for the current player. Args: state (list): [grid, player] """ grid, player = state os.system('cls' if os.name == 'nt' else 'clear') count1 = np.sum(grid == 1) count2 = np.sum(grid == 2) # Header with column indices topheader = " " vline = " " for j in range(n): topheader += " " + str(j) + " " vline += "---+" print(topheader) print() for i in range(n): line = " " + str(i) + " " for j in range(n): if grid[i, j] == 0: line += " |" else: line += " " + PSYM[grid[i, j]] + " |" print(line[:-1]) if i < n - 1: print(vline[:-1]) print() print(f" {P1}: {count1} {P2}: {count2}") print() valid = _all_plays(player, grid) if valid: print(" Valid moves for " + PSYM[player] + ": " + ", ".join(str(m) for m in valid)) else: print(" No valid moves for " + PSYM[player] + " - turn will be skipped!") print()
[docs]def ask_for_move(player, state): """ Asks a human player for a move. If the player has no valid moves, automatically returns PASS_MOVE. Args: player (int): the player whose turn it is state (list): [grid, player] Returns: int, int: (row, col), or PASS_MOVE """ grid = state[0] draw(state) valid = _all_plays(player, grid) if not valid: input(f" {PSYM[player]} has no valid moves. Press Enter to pass...") return PASS_MOVE print("You are " + PSYM[player]) while True: try: row = int(input(" Enter row: ")) col = int(input(" Enter col: ")) except: print(" Please enter integers (or -9 to quit)") continue if row == -9 or col == -9: quit() if (row, col) in valid: return (row, col) else: print(f" ({row}, {col}) is not a valid move. Try again.")
[docs]def declare_winner(winner): """ Announces the winner or a draw. Args: winner (int): winning player number, or 0 for a draw """ if winner == 0: print("The game ended in a draw!") else: print(PSYM[winner] + " won!")