OOD - Othello (Reversi) Game
1. Problem Statement
Othello (also known as Reversi) is a classic two-player strategy board game played on an 8×8 grid where each game piece has two sides: one black and one white. The game begins with four pieces placed in the center of the board forming a square pattern—two white pieces diagonally opposite each other and two black pieces occupying the remaining center squares. Players alternate turns placing pieces of their color on empty squares, and critically, each move must result in capturing at least one opponent's piece by bracketing a continuous line of opponent pieces between the newly placed piece and another piece of the player's color already on the board. When opponent pieces are captured, they are flipped to reveal the capturing player's color, fundamentally changing the board dynamics.
The bracketing (or flanking) mechanism operates across eight possible directions from any placed piece: horizontally left and right, vertically up and down, and diagonally in all four diagonal directions. A single move can simultaneously capture opponent pieces in multiple directions if valid bracketing exists in more than one line from the newly placed piece. This multi-directional capture rule creates complex tactical situations where a single well-placed piece can flip numerous opponent pieces across several lines simultaneously. The game's strategic depth emerges from players needing to balance immediate piece count gains against long-term positional advantages, as control of key squares (particularly corners and edges) provides stable pieces that cannot be flipped by opponents.
The game concludes when neither player has any legal moves remaining, which typically occurs when the board is completely filled but can also happen earlier if one player has no valid moves and passing their turn results in the opponent also having no valid moves. Victory is determined by simple piece majority: the player with more pieces of their color visible on the board wins, with ties occurring when both players have exactly 32 pieces. The implementation challenge involves accurately detecting all eight directions for potential captures, validating move legality by checking if at least one opponent piece would be flipped, efficiently flipping all captured pieces in multiple directions simultaneously, maintaining correct piece counts for both players throughout the game, detecting the end-game condition when no valid moves exist for either player, and providing clear visual feedback of the current board state and valid move options to guide player decisions.

2. System Requirements
Functional Requirements:
- Initialize an 8×8 game board with the standard starting position (4 pieces in center)
- Support two players (Black and White) alternating turns
- Allow players to place pieces on valid empty squares
- Validate each move to ensure it captures at least one opponent piece
- Detect captures in all 8 directions (horizontal, vertical, diagonal)
- Flip all captured opponent pieces when a valid move is played
- Update piece counts for both players after each move
- Display the current board state with clear piece visualization
- Detect when a player has no valid moves and automatically pass their turn
- Detect end-of-game when neither player has valid moves
- Determine the winner by counting pieces of each color
- Handle edge cases: invalid coordinates, occupied squares, no-capture moves
Non-Functional Requirements:
- Correctness: Move validation and piece flipping must strictly follow official Othello rules
- Performance: Move validation should complete within 100ms even for complex board states
- Usability: Clear board display with row/column labels for easy coordinate input
- Testability: Core game logic separated from I/O for unit testing
- Extensibility: Support adding AI player, undo/redo, move hints in future versions
Assumptions:
- Standard 8×8 board (not configurable board size)
- Two human players (AI opponent is future enhancement)
- No time limits per move
- No move history or game replay functionality
- Simple console-based I/O (no GUI)
3. Use Case Diagram
Actors:
- Player (Black): Makes moves with black pieces
- Player (White): Makes moves with white pieces
- Game System: Manages game state, validates moves, detects end conditions
Core Use Cases:
- Start New Game
- Place Piece
- Validate Move
- Flip Captured Pieces
- Pass Turn (when no valid moves)
- End Game
- Declare Winner
graph TB subgraph OthelloGame["Othello Game System"] UC1["Start New Game"] UC2["Place Piece"] UC3["Validate Move"] UC4["Flip Pieces"] UC5["Pass Turn"] UC6["View Board"] UC7["End Game"] UC8["Declare Winner"] end BlackPlayer([Black Player]) WhitePlayer([White Player]) System([Game System]) BlackPlayer --> UC2 WhitePlayer --> UC2 BlackPlayer --> UC6 WhitePlayer --> UC6 System --> UC1 System --> UC3 System --> UC4 System --> UC5 System --> UC7 System --> UC8 UC2 -.->|triggers| UC3 UC3 -.->|if valid| UC4 UC4 -.->|updates| UC6 UC5 -.->|checks| UC7 UC7 -.->|triggers| UC8 style BlackPlayer fill:#333,color:#fff style WhitePlayer fill:#fff,color:#000,stroke:#000 style System fill:#e8f5e9
4. Class Diagram
Core Classes:
- Position: Value object representing (row, col) coordinates on the board
- Direction: Enum for 8 movement directions (N, NE, E, SE, S, SW, W, NW)
- Piece: Enum for piece colors (BLACK, WHITE, EMPTY)
- Player: Represents a player with color, piece count, and move validation
- Cell: Represents a single board cell with its piece state
- Board: Manages the 8×8 grid, validates moves, flips pieces
- Game: Orchestrates game flow, turn management, win detection
- MoveValidator: Encapsulates move validation logic for all directions
- GameState: Enum for game status (IN_PROGRESS, BLACK_WINS, WHITE_WINS, TIE)
classDiagram class Position { -int row -int col +Position(int, int) +isValid() boolean +move(Direction) Position +equals(Object) boolean } class Direction { <<enumeration>> NORTH(-1, 0) NORTHEAST(-1, 1) EAST(0, 1) SOUTHEAST(1, 1) SOUTH(1, 0) SOUTHWEST(1, -1) WEST(0, -1) NORTHWEST(-1, -1) -int rowDelta -int colDelta } class Piece { <<enumeration>> BLACK WHITE EMPTY +opposite() Piece } class Cell { -Position position -Piece piece +Cell(Position) +setPiece(Piece) void +getPiece() Piece +isEmpty() boolean +flip() void } class Player { -Piece color -String name -int pieceCount +Player(Piece, String) +incrementPieces() void +decrementPieces() void +getPieceCount() int } class Board { -Cell[][] grid -int size +Board(int) +getCell(Position) Cell +placePiece(Position, Piece) void +flipPiece(Position) void +isValidPosition(Position) boolean +countPieces(Piece) int +display() void } class MoveValidator { -Board board +MoveValidator(Board) +isValidMove(Position, Piece) boolean +getFlippablePositions(Position, Piece) List~Position~ +checkDirection(Position, Piece, Direction) List~Position~ } class Game { -Board board -Player blackPlayer -Player whitePlayer -Player currentPlayer -MoveValidator validator -GameState state +Game() +start() void +makeMove(Position) boolean +switchTurn() void +hasValidMoves(Player) boolean +isGameOver() boolean +getWinner() Player } class GameState { <<enumeration>> IN_PROGRESS BLACK_WINS WHITE_WINS TIE } Board "1" *-- "64" Cell Cell "1" --> "1" Position Cell "1" --> "1" Piece Game "1" --> "1" Board Game "1" --> "2" Player Game "1" --> "1" MoveValidator Game "1" --> "1" GameState Player "1" --> "1" Piece MoveValidator "1" --> "1" Board Position ..> Direction : uses
5. Activity Diagrams
Making a Move
graph TD A([Player inputs coordinates]) --> B{Position valid?} B -->|No| C[Display error: Invalid coordinates] C --> A B -->|Yes| D{Cell empty?} D -->|No| E[Display error: Cell occupied] E --> A D -->|Yes| F[Check all 8 directions for captures] F --> G{At least one capture?} G -->|No| H[Display error: Must capture pieces] H --> A G -->|Yes| I[Place piece at position] I --> J[Flip all captured pieces in valid directions] J --> K[Update piece counts] K --> L[Display updated board] L --> M([Switch to next player])
Game Flow
graph TD Start([Start Game]) --> Init[Initialize board with 4 center pieces] Init --> SetBlack[Set current player to Black] SetBlack --> Turn{Current player has valid moves?} Turn -->|Yes| Input[Get player move input] Input --> Validate[Validate and execute move] Validate --> Switch[Switch current player] Switch --> BothCheck{Both players have no moves?} Turn -->|No| Pass[Pass turn to opponent] Pass --> BothCheck BothCheck -->|No| Turn BothCheck -->|Yes| Count[Count pieces for both players] Count --> Winner{Compare counts} Winner -->|Black > White| WinB[Black Wins] Winner -->|White > Black| WinW[White Wins] Winner -->|Equal| Tie[Tie Game] WinB --> End([End Game]) WinW --> End Tie --> End
6. Java Implementation
Enums and Value Objects
/**
* Represents the 8 possible directions on an Othello board
*/
public enum Direction {
NORTH(-1, 0),
NORTHEAST(-1, 1),
EAST(0, 1),
SOUTHEAST(1, 1),
SOUTH(1, 0),
SOUTHWEST(1, -1),
WEST(0, -1),
NORTHWEST(-1, -1);
private final int rowDelta;
private final int colDelta;
Direction(int rowDelta, int colDelta) {
this.rowDelta = rowDelta;
this.colDelta = colDelta;
}
public int getRowDelta() {
return rowDelta;
}
public int getColDelta() {
return colDelta;
}
}
/**
* Represents piece colors and empty cells
*/
public enum Piece {
BLACK('●'),
WHITE('○'),
EMPTY('·');
private final char symbol;
Piece(char symbol) {
this.symbol = symbol;
}
public char getSymbol() {
return symbol;
}
public Piece opposite() {
if (this == BLACK) return WHITE;
if (this == WHITE) return BLACK;
return EMPTY;
}
@Override
public String toString() {
return String.valueOf(symbol);
}
}
/**
* Represents game end states
*/
public enum GameState {
IN_PROGRESS,
BLACK_WINS,
WHITE_WINS,
TIE
}
Position Class
import java.util.Objects;
/**
* Immutable value object representing board coordinates
*/
public class Position {
private final int row;
private final int col;
public Position(int row, int col) {
this.row = row;
this.col = col;
}
public int getRow() {
return row;
}
public int getCol() {
return col;
}
/**
* Check if position is within 8x8 board bounds
*/
public boolean isValid(int boardSize) {
return row >= 0 && row < boardSize && col >= 0 && col < boardSize;
}
/**
* Create new position by moving in a direction
*/
public Position move(Direction direction) {
return new Position(
row + direction.getRowDelta(),
col + direction.getColDelta()
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Position)) return false;
Position position = (Position) o;
return row == position.row && col == position.col;
}
@Override
public int hashCode() {
return Objects.hash(row, col);
}
@Override
public String toString() {
return String.format("(%d,%d)", row, col);
}
}
Cell and Board Classes
/**
* Represents a single cell on the board
*/
public class Cell {
private final Position position;
private Piece piece;
public Cell(Position position) {
this.position = position;
this.piece = Piece.EMPTY;
}
public void setPiece(Piece piece) {
this.piece = piece;
}
public Piece getPiece() {
return piece;
}
public boolean isEmpty() {
return piece == Piece.EMPTY;
}
/**
* Flip piece to opposite color
*/
public void flip() {
if (piece != Piece.EMPTY) {
piece = piece.opposite();
}
}
public Position getPosition() {
return position;
}
}
/**
* Represents the game board
*/
public class Board {
private static final int BOARD_SIZE = 8;
private final Cell[][] grid;
public Board() {
grid = new Cell[BOARD_SIZE][BOARD_SIZE];
initializeBoard();
}
private void initializeBoard() {
// Create all cells
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
grid[row][col] = new Cell(new Position(row, col));
}
}
// Set up initial 4 pieces in center
int mid = BOARD_SIZE / 2;
grid[mid - 1][mid - 1].setPiece(Piece.WHITE);
grid[mid - 1][mid].setPiece(Piece.BLACK);
grid[mid][mid - 1].setPiece(Piece.BLACK);
grid[mid][mid].setPiece(Piece.WHITE);
}
public Cell getCell(Position position) {
if (!isValidPosition(position)) {
throw new IllegalArgumentException("Invalid position: " + position);
}
return grid[position.getRow()][position.getCol()];
}
public void placePiece(Position position, Piece piece) {
getCell(position).setPiece(piece);
}
public void flipPiece(Position position) {
getCell(position).flip();
}
public boolean isValidPosition(Position position) {
return position.isValid(BOARD_SIZE);
}
public int getBoardSize() {
return BOARD_SIZE;
}
/**
* Count total pieces of a specific color on the board
*/
public int countPieces(Piece piece) {
int count = 0;
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (grid[row][col].getPiece() == piece) {
count++;
}
}
}
return count;
}
/**
* Display board with row/column labels
*/
public void display() {
System.out.println("\n 0 1 2 3 4 5 6 7");
System.out.println(" ----------------");
for (int row = 0; row < BOARD_SIZE; row++) {
System.out.print(row + "|");
for (int col = 0; col < BOARD_SIZE; col++) {
System.out.print(grid[row][col].getPiece() + " ");
}
System.out.println("|" + row);
}
System.out.println(" ----------------");
System.out.println(" 0 1 2 3 4 5 6 7\n");
}
}
Move Validator
import java.util.ArrayList;
import java.util.List;
/**
* Validates moves and identifies flippable pieces
*/
public class MoveValidator {
private final Board board;
public MoveValidator(Board board) {
this.board = board;
}
/**
* Check if placing a piece at position is a valid move
*/
public boolean isValidMove(Position position, Piece playerColor) {
// Cell must be empty
if (!board.isValidPosition(position) || !board.getCell(position).isEmpty()) {
return false;
}
// Must capture at least one opponent piece
return !getFlippablePositions(position, playerColor).isEmpty();
}
/**
* Get all positions that would be flipped by this move
*/
public List<Position> getFlippablePositions(Position position, Piece playerColor) {
List<Position> flippable = new ArrayList<>();
// Check all 8 directions
for (Direction direction : Direction.values()) {
List<Position> captured = checkDirection(position, playerColor, direction);
flippable.addAll(captured);
}
return flippable;
}
/**
* Check one direction for capturable opponent pieces
* Returns list of positions to flip, or empty list if no capture
*/
private List<Position> checkDirection(Position start, Piece playerColor, Direction direction) {
List<Position> toFlip = new ArrayList<>();
Position current = start.move(direction);
Piece opponentColor = playerColor.opposite();
// First, we must encounter at least one opponent piece
while (board.isValidPosition(current) &&
board.getCell(current).getPiece() == opponentColor) {
toFlip.add(current);
current = current.move(direction);
}
// Then we must encounter our own piece (not empty, not out of bounds)
if (toFlip.isEmpty() ||
!board.isValidPosition(current) ||
board.getCell(current).getPiece() != playerColor) {
return new ArrayList<>(); // No valid capture in this direction
}
return toFlip;
}
/**
* Get all valid moves for a player
*/
public List<Position> getValidMoves(Piece playerColor) {
List<Position> validMoves = new ArrayList<>();
int size = board.getBoardSize();
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
Position pos = new Position(row, col);
if (isValidMove(pos, playerColor)) {
validMoves.add(pos);
}
}
}
return validMoves;
}
}
Player Class
/**
* Represents a player in the game
*/
public class Player {
private final Piece color;
private final String name;
private int pieceCount;
public Player(Piece color, String name) {
if (color == Piece.EMPTY) {
throw new IllegalArgumentException("Player color cannot be EMPTY");
}
this.color = color;
this.name = name;
this.pieceCount = 2; // Each player starts with 2 pieces
}
public Piece getColor() {
return color;
}
public String getName() {
return name;
}
public int getPieceCount() {
return pieceCount;
}
public void setPieceCount(int count) {
this.pieceCount = count;
}
@Override
public String toString() {
return name + " (" + color + ")";
}
}
Game Class
import java.util.List;
import java.util.Scanner;
/**
* Main game controller orchestrating the Othello game flow
*/
public class Game {
private final Board board;
private final Player blackPlayer;
private final Player whitePlayer;
private Player currentPlayer;
private final MoveValidator validator;
private GameState state;
public Game() {
this.board = new Board();
this.blackPlayer = new Player(Piece.BLACK, "Black");
this.whitePlayer = new Player(Piece.WHITE, "White");
this.currentPlayer = blackPlayer; // Black moves first
this.validator = new MoveValidator(board);
this.state = GameState.IN_PROGRESS;
}
/**
* Main game loop
*/
public void start() {
Scanner scanner = new Scanner(System.in);
System.out.println("=== OTHELLO GAME ===");
System.out.println("Black (●) moves first");
System.out.println("Enter moves as: row col (e.g., '3 4')");
System.out.println("Enter 'quit' to exit\n");
while (state == GameState.IN_PROGRESS) {
board.display();
displayScore();
// Check if current player has valid moves
if (!hasValidMoves(currentPlayer)) {
System.out.println(currentPlayer.getName() + " has no valid moves. Passing turn...");
switchTurn();
// If next player also has no moves, game is over
if (!hasValidMoves(currentPlayer)) {
endGame();
break;
}
continue;
}
System.out.print(currentPlayer.getName() + "'s turn. Enter move: ");
String input = scanner.nextLine().trim();
if (input.equalsIgnoreCase("quit")) {
System.out.println("Game ended by player.");
break;
}
try {
String[] parts = input.split("\\s+");
if (parts.length != 2) {
System.out.println("Invalid input. Use format: row col");
continue;
}
int row = Integer.parseInt(parts[0]);
int col = Integer.parseInt(parts[1]);
Position position = new Position(row, col);
if (makeMove(position)) {
switchTurn();
}
} catch (NumberFormatException e) {
System.out.println("Invalid coordinates. Please enter numbers.");
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
scanner.close();
}
/**
* Attempt to make a move at the specified position
* @return true if move was valid and executed
*/
public boolean makeMove(Position position) {
Piece playerColor = currentPlayer.getColor();
if (!validator.isValidMove(position, playerColor)) {
System.out.println("Invalid move! Must be empty cell that captures opponent pieces.");
return false;
}
// Place the piece
board.placePiece(position, playerColor);
// Flip all captured pieces
List<Position> toFlip = validator.getFlippablePositions(position, playerColor);
for (Position pos : toFlip) {
board.flipPiece(pos);
}
// Update piece counts
updatePieceCounts();
System.out.println("Move executed! Flipped " + toFlip.size() + " piece(s).\n");
return true;
}
/**
* Switch to the other player
*/
private void switchTurn() {
currentPlayer = (currentPlayer == blackPlayer) ? whitePlayer : blackPlayer;
}
/**
* Check if player has any valid moves
*/
public boolean hasValidMoves(Player player) {
return !validator.getValidMoves(player.getColor()).isEmpty();
}
/**
* Update piece counts by counting board pieces
*/
private void updatePieceCounts() {
blackPlayer.setPieceCount(board.countPieces(Piece.BLACK));
whitePlayer.setPieceCount(board.countPieces(Piece.WHITE));
}
/**
* Display current score
*/
private void displayScore() {
System.out.println("Score - Black: " + blackPlayer.getPieceCount() +
" | White: " + whitePlayer.getPieceCount());
}
/**
* End game and determine winner
*/
private void endGame() {
updatePieceCounts();
System.out.println("\n=== GAME OVER ===");
board.display();
displayScore();
int blackCount = blackPlayer.getPieceCount();
int whiteCount = whitePlayer.getPieceCount();
if (blackCount > whiteCount) {
state = GameState.BLACK_WINS;
System.out.println("🏆 Black wins!");
} else if (whiteCount > blackCount) {
state = GameState.WHITE_WINS;
System.out.println("🏆 White wins!");
} else {
state = GameState.TIE;
System.out.println("🤝 Tie game!");
}
}
public GameState getState() {
return state;
}
public Player getWinner() {
if (state == GameState.BLACK_WINS) return blackPlayer;
if (state == GameState.WHITE_WINS) return whitePlayer;
return null;
}
}
Main Application
/**
* Entry point for Othello game
*/
public class OthelloApp {
public static void main(String[] args) {
Game game = new Game();
game.start();
}
}
7. Design Patterns Applied
1. Immutable Value Object (Position)
Intent: Represent board coordinates as immutable objects that can be safely shared.
Implementation:
Positionclass hasfinalfields- No setters, only getters
- Factory method
move()returns new Position instead of mutating
Benefits:
- Thread-safe by default
- Can be used as HashMap keys
- Prevents accidental coordinate changes
- Simplifies reasoning about state
Position start = new Position(3, 4);
Position next = start.move(Direction.NORTH); // Returns new Position
// start remains unchanged
2. Strategy Pattern (Move Validation)
Intent: Encapsulate move validation logic separately from game flow.
Implementation:
MoveValidatorclass handles all validation rulesGameclass delegates validation without knowing implementation details- Easy to modify validation rules without changing Game
Benefits:
- Single Responsibility: validation logic isolated
- Easy to add new validation rules (e.g., tournament time rules)
- Testable independently from Game class
MoveValidator validator = new MoveValidator(board);
if (validator.isValidMove(position, playerColor)) {
// Execute move
}
3. Enum Pattern (Direction, Piece, GameState)
Intent: Type-safe constants with associated behavior.
Implementation:
Directionenum encapsulates row/col deltasPieceenum has display symbols andopposite()methodGameStateenum represents finite game states
Benefits:
- Compile-time safety (no invalid directions/pieces)
- Self-documenting code
- Can add methods to enums (e.g.,
Piece.opposite())
for (Direction dir : Direction.values()) {
Position next = current.move(dir);
}
4. Model-View-Controller (MVC) Architecture
Intent: Separate game logic, display, and user interaction.
Implementation:
- Model:
Board,Cell,Player,Position(game state) - View:
Board.display(), console output (presentation) - Controller:
Gameclass (orchestrates flow, handles input)
Benefits:
- Easy to swap console UI for GUI
- Game logic testable without UI
- Clear separation of concerns
5. Encapsulation (Cell State Management)
Intent: Hide piece state behind cell abstraction.
Implementation:
Cellclass owns piece state- Provides
flip()method that handles logic internally - Client code doesn't manipulate piece state directly
Benefits:
- Board grid operations are atomic
- Flipping logic centralized in Cell
- Easy to add cell-level features (e.g., highlighting)
cell.flip(); // Internally handles piece.opposite()
6. Factory Method Pattern (Board Initialization)
Intent: Encapsulate object creation logic.
Current Implementation:
private void initializeBoard() {
// Creates 64 Cell objects with proper positions
// Sets up initial 4-piece configuration
}
Benefits:
- Board construction logic centralized
- Easy to modify starting configuration
- Could support alternate game modes (different initial positions)
8. Key Design Decisions
Decision 1: Immutable Position Class
Rationale: Coordinates shouldn't change after creation. The move() method returns a new Position rather than mutating the original.
Benefits:
- No defensive copying needed
- Safe to use as HashMap keys
- Thread-safe
- Clearer semantics (positions don't "move", you get new positions)
Trade-off: Slightly more memory allocation, but modern JVMs handle this efficiently.
Decision 2: Separate MoveValidator Class
Rationale: Move validation is complex (8 directions, multiple flips) and deserves its own class.
Benefits:
- Single Responsibility Principle
- Easy to unit test validation logic
- Game class stays focused on orchestration
- Could swap validators for different rule variants
Alternative Considered: Putting validation in Board class would bloat it with complex logic.
Decision 3: Direction Enum with Deltas
Rationale: Each direction has associated row/column deltas. Storing them in the enum eliminates repetitive switch statements.
Benefits:
- DRY: deltas defined once
- Easy to iterate all directions:
for (Direction dir : Direction.values()) - Type-safe direction references
Code Elimination:
// Without enum approach (repetitive):
if (direction == "NORTH") { nextRow = row - 1; nextCol = col; }
else if (direction == "SOUTH") { nextRow = row + 1; nextCol = col; }
// ... 6 more cases
// With enum:
Position next = current.move(direction); // Clean!
Decision 4: Cell-Based Board Representation
Rationale: Each square is a Cell object containing position and piece state, rather than just a 2D array of enums.
Benefits:
- Cells can have additional properties (e.g., highlighted, last move)
- Encapsulates piece state and flip logic
- Easier to add cell-specific behavior later
Trade-off: Slightly more memory than raw Piece[][] array, but worth it for extensibility.
Decision 5: Piece Count Recalculation
Rationale: Instead of tracking piece counts incrementally, we recalculate by scanning the board after each move.
Benefits:
- Source of truth is always the board state
- No risk of count desynchronization bugs
- Simpler state management
Trade-off: O(64) scan per move, but 64 is small enough to be negligible.
Alternative Considered: Incremental tracking would be O(1) but more error-prone with flips affecting both players.
Decision 6: Validator Returns Flippable List
Rationale: getFlippablePositions() returns all positions that would be flipped, allowing Game to execute flips.
Benefits:
- Separation: validator identifies targets, Game executes
- Validator is pure function (no side effects)
- Easy to preview moves (show what would flip)
- Testable without affecting board state
List<Position> toFlip = validator.getFlippablePositions(position, color);
// Preview: "This move will flip 5 pieces"
for (Position pos : toFlip) {
board.flipPiece(pos);
}
Decision 7: No Undo/Redo Initially
Rationale: Simplified scope to focus on core game mechanics.
Future Enhancement: Could add Command pattern for move history:
interface Command {
void execute();
void undo();
}
class PlacePieceCommand implements Command {
private Position position;
private List<Position> flipped;
// ... execute/undo implementation
}
Decision 8: Console-Based UI
Rationale: Simple text interface allows focusing on OOD without GUI complexity.
Benefits:
- Easy to test via automated input
- Cross-platform (no GUI dependencies)
- Clear demonstration of MVC separation
Future GUI Enhancement:
interface BoardDisplay {
void display(Board board);
}
class ConsoleDisplay implements BoardDisplay { ... }
class SwingGUIDisplay implements BoardDisplay { ... }
Decision 9: Pass Turn When No Moves
Rationale: Othello rules require passing if you have no valid moves. Implementation checks both players before ending game.
Logic:
if (!hasValidMoves(currentPlayer)) {
switchTurn();
if (!hasValidMoves(currentPlayer)) {
endGame(); // Both players stuck
}
}
Edge Case Handled: One player has no moves but opponent does—game continues.
Decision 10: Piece.EMPTY vs null
Rationale: Using Piece.EMPTY enum value instead of null for empty cells.
Benefits:
- No null pointer exceptions
- Can iterate all cells without null checks
Pieceenum is complete domain model
Example Safety:
// Safe without null check
char symbol = cell.getPiece().getSymbol();
// vs. with null:
if (cell.getPiece() != null) {
char symbol = cell.getPiece().getSymbol();
}
This design creates a clean, maintainable Othello implementation that strictly follows game rules while demonstrating solid object-oriented principles through clear class responsibilities, immutable value objects, and proper encapsulation.