"""Reversi (Othello) game state management module.
This module provides comprehensive state management functionality for Reversi/Othello,
including move validation, disc flipping mechanics, and game progression tracking.
Reversi (also known as Othello) is a strategy board game played on an 8×8 board with
64 discs that are black on one side and white on the other. Players take turns placing
discs with their color facing up, attempting to trap opponent discs between their own
to flip them. The game starts with four discs in the center in a cross pattern.
Classes:
ReversiStateManager: Main state management class for Reversi/Othello operations.
Example:
Basic Reversi game setup and play:
>>> from haive.games.reversi.state_manager import ReversiStateManager
>>> from haive.games.reversi.models import ReversiMove
>>>
>>> # Initialize standard Reversi game
>>> state = ReversiStateManager.initialize()
>>> print(f"Current player: {state.turn}") # "B" (Black)
>>> print(f"Board size: 8x8")
>>> print(f"Black discs: {state.black_count}, White discs: {state.white_count}")
>>>
>>> # Get legal moves (must flip at least one opponent disc)
>>> legal_moves = ReversiStateManager.get_legal_moves(state)
>>> print(f"Legal moves for Black: {len(legal_moves)}")
>>>
>>> # Make a move
>>> if legal_moves:
... move = legal_moves[0]
... new_state = ReversiStateManager.apply_move(state, move)
... print(f"Move at ({move.row}, {move.col}) flipped {move.flipped_count} discs")
Note:
- Standard 8×8 board with initial cross pattern in center
- Players are "B" (Black) and "W" (White) with Black moving first
- Legal moves must flip at least one opponent disc
- Game ends when no legal moves exist for both players
- Winner is determined by who has more discs when game ends
- Pass moves are automatic when no legal moves exist
"""
from haive.games.framework.base.state_manager import GameStateManager
from haive.games.reversi.models import ReversiAnalysis, ReversiMove
from haive.games.reversi.state import ReversiState
[docs]
class ReversiStateManager(GameStateManager[ReversiState]):
"""Manager for Reversi/Othello game state."""
# Direction vectors for all 8 directions
DIRECTIONS = [
(-1, -1),
(-1, 0),
(-1, 1), # NW, N, NE
(0, -1),
(0, 1), # W, E
(1, -1),
(1, 0),
(1, 1), # SW, S, SE
]
[docs]
@classmethod
def initialize(cls, **kwargs) -> ReversiState:
"""Initialize a new Reversi/Othello game.
Args:
**kwargs: Keyword arguments for game initialization.
first_player: Which player goes first ('B' or 'W'). Default is 'B'.
player_B: Which player is Black ('player1' or 'player2'). Default is 'player1'.
player_W: Which player is White ('player1' or 'player2'). Default is 'player2'.
Returns:
ReversiState: A new Reversi game state.
"""
first_player = kwargs.get("first_player", "B")
player_B = kwargs.get("player_B", "player1")
player_W = kwargs.get("player_W", "player2")
# Create an empty 8x8 board
board = [[None for _ in range(8)] for _ in range(8)]
# Set up initial pieces
# Standard Reversi setup: 2x2 in the center with alternating colors
board[3][3] = "W"
board[3][4] = "B"
board[4][3] = "B"
board[4][4] = "W"
return ReversiState(
board=board,
turn=first_player,
game_status="ongoing",
move_history=[],
player_B=player_B,
player_W=player_W,
skip_count=0,
)
[docs]
@classmethod
def get_legal_moves(cls, state: ReversiState) -> list[ReversiMove]:
"""Get all legal moves for the current state.
Args:
state: The current game state.
Returns:
List[ReversiMove]: A list of all legal moves.
"""
legal_moves = []
player = state.turn
for row in range(8):
for col in range(8):
# Check if the cell is empty
if state.board[row][col] is not None:
continue
# Check if placing a disc here would flip any opponent's discs
if cls._get_flips(state.board, row, col, player):
legal_moves.append(ReversiMove(row=row, col=col, player=player))
return legal_moves
[docs]
@classmethod
def apply_move(cls, state: ReversiState, move: ReversiMove) -> ReversiState:
"""Apply a move to the current state and return the new state.
Args:
state: The current game state.
move: The move to apply.
Returns:
ReversiState: A new game state after applying the move.
Raises:
ValueError: If the move is invalid.
"""
# Validate player's turn
if move.player != state.turn:
raise ValueError(f"Not {move.player}'s turn")
# Get legal moves
legal_moves = cls.get_legal_moves(state)
# Check if the move is legal
if not any(m.row == move.row and m.col == move.col for m in legal_moves):
raise ValueError(f"Illegal move: ({move.row}, {move.col})")
# Create a deep copy of the state
new_state = state.model_copy(deep=True)
new_state.skip_count = 0 # Reset skip counter since a move is being made
# Apply the move
new_state.board[move.row][move.col] = move.player
# Flip opponent's discs
flips = cls._get_flips(state.board, move.row, move.col, move.player)
for row, col in flips:
new_state.board[row][col] = move.player
# Add to move history
new_state.move_history.append(move)
# Switch turns
new_state.turn = "W" if move.player == "B" else "B"
# Check if next player has valid moves
next_player_moves = cls.get_legal_moves(new_state)
if not next_player_moves:
# Next player has no valid moves, must skip turn
new_state.turn = move.player # Switch back to current player
new_state.skip_count += 1
# Check if current player also has no moves (game over)
current_player_moves = cls.get_legal_moves(new_state)
if not current_player_moves:
new_state.skip_count += 1 # Both players skipped
# Check game status
return cls.check_game_status(new_state)
[docs]
@classmethod
def check_game_status(cls, state: ReversiState) -> ReversiState:
"""Check and update the game status.
Args:
state: The current game state.
Returns:
ReversiState: The game state with updated status.
"""
# If both players have skipped their turns, game is over
if state.skip_count >= 2:
# Count discs to determine winner
counts = state.disc_count
black_count = counts["B"]
white_count = counts["W"]
if black_count > white_count:
state.game_status = "B_win"
state.winner = "B"
elif white_count > black_count:
state.game_status = "W_win"
state.winner = "W"
else:
state.game_status = "draw"
state.winner = None
# Check if board is full
if all(
state.board[row][col] is not None for row in range(8) for col in range(8)
):
# Count discs to determine winner
counts = state.disc_count
black_count = counts["B"]
white_count = counts["W"]
if black_count > white_count:
state.game_status = "B_win"
state.winner = "B"
elif white_count > black_count:
state.game_status = "W_win"
state.winner = "W"
else:
state.game_status = "draw"
state.winner = None
return state
[docs]
@classmethod
def get_winner(cls, state: ReversiState) -> str | None:
"""Get the winner of the game, if any.
Args:
state: The current game state.
Returns:
Optional[str]: The winner ('B' or 'W'), or None if the game is ongoing or a draw.
"""
return state.winner
[docs]
@classmethod
def add_analysis(
cls, state: ReversiState, player: str, analysis: ReversiAnalysis
) -> ReversiState:
"""Add an analysis to the state.
Args:
state: The current game state.
player: The player who performed the analysis.
analysis: The analysis to add.
Returns:
ReversiState: Updated state with the analysis added.
"""
new_state = state.model_copy()
if player == "player1":
new_state.player1_analysis.append(analysis)
else:
new_state.player2_analysis.append(analysis)
return new_state
@classmethod
def _get_flips(
cls, board: list[list[str | None]], row: int, col: int, player: str
) -> set[tuple[int, int]]:
"""Get the positions of opponent's discs that would be flipped by placing.
player's disc at (row, col).
Args:
board: The current board.
row: Row index of the move.
col: Column index of the move.
player: Player making the move ('B' or 'W').
Returns:
Set[Tuple[int, int]]: Positions of discs that would be flipped.
"""
# If the cell is not empty, no flips possible
if board[row][col] is not None:
return set()
opponent = "W" if player == "B" else "B"
flips = set()
# Check all 8 directions
for dx, dy in cls.DIRECTIONS:
temp_flips = []
x, y = row + dx, col + dy
# Continue in this direction as long as we find opponent's discs
while 0 <= x < 8 and 0 <= y < 8 and board[x][y] == opponent:
temp_flips.append((x, y))
x += dx
y += dy
# If we find one of our own discs at the end, these are valid flips
if 0 <= x < 8 and 0 <= y < 8 and board[x][y] == player and temp_flips:
flips.update(temp_flips)
return flips
[docs]
@classmethod
def is_legal_move(
cls, state: ReversiState, row: int, col: int, player: str
) -> bool:
"""Check if a move is legal.
Args:
state: The current game state.
row: Row index of the move.
col: Column index of the move.
player: Player making the move ('B' or 'W').
Returns:
bool: True if the move is legal, False otherwise.
"""
# Check if the cell is within bounds and empty
if not (0 <= row < 8 and 0 <= col < 8) or state.board[row][col] is not None:
return False
# Check if this move would flip any opponent's discs
return bool(cls._get_flips(state.board, row, col, player))
[docs]
@classmethod
def get_skip_move(cls, state: ReversiState) -> ReversiState:
"""Apply a skip move when player has no legal moves.
Args:
state: The current game state.
Returns:
ReversiState: A new game state after skipping the turn.
"""
new_state = state.model_copy()
# Increment skip counter
new_state.skip_count += 1
# Switch turns
new_state.turn = "W" if state.turn == "B" else "B"
# Check game status
return cls.check_game_status(new_state)