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!")