Back to LLD explorer
OOD / Game• Patterns: Singleton, Strategy, Observer

Snake & Ladder

Medium

Problem Summary

Design a multiplayer Snake & Ladder board game with custom dimensions, randomized or configured snakes and ladders, flexible dice roll strategies, and cycle-free board validation.

Functional Scope

  • Customizable board size N x N (default 1 to 100 cells).
  • Dynamic placement of Snakes (head > tail) and Ladders (bottom < top).
  • Cycle detection during board initialization to prevent infinite looping.
  • Pluggable dice rolling strategy: Standard 1-6 dice, dual dice, or crooked dice.
  • Win condition: player must reach exact target cell (100).

Entity-Relationship (ER) Schema

Game [1] --> [1] Board
Game [1] --> [2..*] Player
Game [1] --> [1] DiceStrategy
Board [1] --> [*] BoardEntity
BoardEntity <|-- Snake
BoardEntity <|-- Ladder

Design Approach

Store snakes and ladders in a lookup map for instant O(1) jump resolution. Ensure fairness and customization through a Strategy-based dice roller, using a circular queue to manage turn rotations.

Core Classes & Models

SnakeAndLadderGame (Turn orchestrator and referee)Board (Maintains positions and snakes/ladders lookup map)Player (Id, name, and current square position)DiceStrategy (StandardDice, CrookedDice)GameObserver (Logs movements and special events)
Code Blueprint
118 lines
/**
 * Low-Level Design: Snake and Ladder Game
 * Design Patterns Applied:
 *  - Strategy Pattern: Pluggable dice rolling algorithms (Standard, Rigged, Multi-Dice)
 *  - Singleton Pattern: Central GameManager coordinating players, board jumps, and turns
 *  - Observer Pattern: Real-time player move and ladder/snake jump notifications
 */

import java.util.*;

// --- [DESIGN PATTERN: Observer] Observer for player movements ---
interface GameObserver {
    void onPlayerMoved(String playerName, int from, int to, String note);
    void onPlayerWon(String playerName);
}

class ConsoleGameNotifier implements GameObserver {
    @Override
    public void onPlayerMoved(String playerName, int from, int to, String note) {
        System.out.println("🎲 [" + playerName + "] moved " + from + " -> " + to + " " + note);
    }
    @Override
    public void onPlayerWon(String playerName) {
        System.out.println("🏆 CONGRATULATIONS! " + playerName + " has WON the match!");
    }
}

// --- [DESIGN PATTERN: Strategy] Pluggable dice strategy ---
interface DiceStrategy {
    int roll();
}

class StandardDiceStrategy implements DiceStrategy {
    private final Random rand = new Random();
    @Override public int roll() { return rand.nextInt(6) + 1; }
}

class Player {
    private final String name;
    private int position = 0;
    public Player(String name) { this.name = name; }
    public String getName() { return name; }
    public int getPosition() { return position; }
    public void setPosition(int position) { this.position = position; }
}

// --- [DESIGN PATTERN: Singleton / Context] Game manager orchestrator ---
class SnakeAndLadderGame {
    private final int boardSize;
    private final Map<Integer, Integer> jumps = new HashMap<>();
    private final Queue<Player> players = new LinkedList<>();
    private final DiceStrategy diceStrategy;
    private final List<GameObserver> observers = new ArrayList<>();
    private boolean isFinished = false;

    public SnakeAndLadderGame(int boardSize, DiceStrategy diceStrategy) {
        this.boardSize = boardSize;
        this.diceStrategy = diceStrategy;
    }

    public void addObserver(GameObserver obs) { observers.add(obs); }
    public void addJump(int from, int to) { jumps.put(from, to); }
    public void addPlayer(Player player) { players.add(player); }

    public boolean playTurn() {
        if (isFinished || players.isEmpty()) return false;
        Player current = players.poll();
        int roll = diceStrategy.roll();
        int startPos = current.getPosition();
        int nextPos = startPos + roll;

        if (nextPos > boardSize) {
            System.out.println(">> [" + current.getName() + "] rolled " + roll + " but needs exact score to finish.");
            players.add(current);
            return true;
        }

        String note = "";
        if (jumps.containsKey(nextPos)) {
            int finalDest = jumps.get(nextPos);
            note = (finalDest > nextPos) ? "🪜 Climbed Ladder!" : "🐍 Bitten by Snake!";
            nextPos = finalDest;
        }

        current.setPosition(nextPos);
        for (GameObserver obs : observers) obs.onPlayerMoved(current.getName(), startPos, nextPos, note);

        if (nextPos == boardSize) {
            for (GameObserver obs : observers) obs.onPlayerWon(current.getName());
            isFinished = true;
            return false;
        }

        players.add(current);
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        SnakeAndLadderGame game = new SnakeAndLadderGame(100, new StandardDiceStrategy());
        game.addObserver(new ConsoleGameNotifier());

        // Snakes and Ladders
        game.addJump(4, 14);
        game.addJump(9, 31);
        game.addJump(17, 7);
        game.addJump(62, 19);

        game.addPlayer(new Player("Alice"));
        game.addPlayer(new Player("Bob"));

        System.out.println("--- Starting Snake & Ladder Match ---");
        for (int i = 0; i < 6; i++) {
            game.playTurn();
        }
    }
}