Logic Behind The Code

Snake and ladder LLD

A visual low-level design walkthrough of your Snake and Ladder system, covering the board model, turn execution, player movement, and the design patterns used to organize the game cleanly.

100 Board positions
4 Supported players
1-6 Dice outcomes

High-Level Logic

What the code is trying to achieve

Functional Requirements

The game allows up to four players to roll a dice, move on a 100-cell board, climb ladders, slide down snakes, and win only by reaching cell 100 exactly.

Core Idea

The game loop repeatedly picks the next player, rolls the dice, calculates the next position, applies board rules, broadcasts the update, and checks for a winner.

Why this structure works

Each class owns one clear responsibility, so the design stays readable: board data stays in Board, player state stays in User, and orchestration stays in gameManager.

Turn Flow

How one player move travels through the system

01

Pick the current player

gameManager removes the next player from the queue, so turns rotate in order.

02

Roll the dice

Dice.getInstance().roll() returns a random number from 1 to 6 using ThreadLocalRandom.

03

Compute the tentative move

The next position becomes currentPosition + diceValue. If it is greater than 100, the player does not move.

04

Apply snake or ladder rules

Board.applyRules() checks whether the landing cell contains a positive ladder jump or a negative snake drop.

05

Notify all observers

playerState.updatePosition() stores the latest move and informs other players, so the game reacts immediately instead of polling continuously.

06

Check for a winner

If the adjusted position is 100, the game ends and that player is declared the winner.

Approach

How to explain the architecture in an interview or demo

Class Responsibilities

  • snake_and_ladder: client entry point that starts the game
  • gameManager: facade and game coordinator
  • Board: owns snakes, ladders, and cell transformations
  • User: stores player identity and position
  • Rule: validates win and retry conditions
  • Dice: single shared dice generator
  • playerState: observer subject for live updates
  • Play: small unit that evaluates one move

Implementation Story

Start with the simplest playable version: one board, a queue of players, a dice service, and a loop. Then improve the design by separating rules, making notifications event-driven, and keeping the facade thin so the system can grow without becoming a god object.

Build domain classes Connect queue + rules Add observer updates Make it testable

Design Patterns

Why these patterns match this game

Facade

gameManager gives one clean entry point. The caller does not need to know how board setup, dice rolling, and player rotation are internally connected.

Singleton

Dice is shared across the whole game. That makes sense for a small demo, though dependency injection would be better for testing in production code.

Observer

Players react when someone moves. This is cleaner than polling because the system sends updates only when something changes.

Builder

You mention builders as a future extension. That is a smart next step if board definitions or player setup become more configurable.

Code Walkthrough

Explain each important snippet like an interview answer

Board Model

Board owns snakes, ladders, and position transformation

The board is the best place to store jump rules because snakes and ladders are board data, not player data.

class Board {
    private final Map<Integer, Integer> jumps = new HashMap<>();

    public int applyRules(int position) {
        return jumps.getOrDefault(position, position);
    }
}
Interview answer
This follows SRP: the board knows how a cell transforms, while the game manager only asks for the final position.

Dice Singleton

One shared dice service

The dice exposes one behavior: roll a number from 1 to 6. In this demo it is a singleton so every part of the game uses the same dice object.

class Dice {
    private static final Dice INSTANCE = new Dice();

    private Dice() {}

    public static Dice getInstance() {
        return INSTANCE;
    }

    public int roll() {
        return ThreadLocalRandom.current().nextInt(1, 7);
    }
}
Design pattern
This is Singleton Pattern. In production, dependency injection would make dice behavior easier to test.

Facade

Game manager coordinates the use case

The manager hides the internal flow: player queue, dice roll, board rule, notification, and win check.

while (!winnerFound) {
    User current = players.poll();
    int roll = Dice.getInstance().roll();
    int next = current.getPosition() + roll;
    int finalPosition = board.applyRules(next);
    current.setPosition(finalPosition);
    players.offer(current);
}
Interview answer
The client starts the game through one simple entry point instead of managing board, dice, queue, and rules itself.

Observer

Notify players when state changes

The observer idea helps broadcast a move when a player position changes. This avoids polling and keeps updates event-driven.

interface playerStatusObserver {
    void update(String message);
}

class playerState {
    private final List<playerStatusObserver> observers = new ArrayList<>();

    public void notifyObservers(String message) {
        observers.forEach(observer -> observer.update(message));
    }
}
Design pattern
This is Observer Pattern. It is useful when multiple objects should react to one state change.

Run The Code

Read it here, run it in the embedded Java compiler

Java Source

This page is static, so it stays GitHub Pages friendly. For the embedded compiler version, the package line is removed so the class can run directly. If you run it locally in an IDE, you can keep the original package structure.

Embedded Compiler

Observer vs Polling

Why observer is better here

Polling

Every player or UI keeps checking for updates again and again, even when nothing happened. That wastes CPU cycles, creates noisy logic, and introduces delay between the move and the reaction.

Observer

The game pushes updates only when a move actually happens. That is more efficient, easier to test, and much closer to how real-time multiplayer events should work.

Interview Ready

Notes For Interview

How to explain the design

This Snake and Ladder LLD separates board data, player state, dice generation, game rules, and game orchestration. `Board` stores jumps from one cell to another, `User` stores each player's current position, `Dice` generates moves, and `gameManager` controls turn order through a queue.

Flow Diagram

Current player turn starts
Roll Dice singleton
Calculate tentative position
Board resolves jump if present
Rule checks winning position
Update player state and rotate queue

UML Diagram: IS-A and HAS-A

playerStatusObserver
User
playerStatusSubject
playerState
List<playerStatusObserver>
gameManager
Queue<User>
Board
Dice
Play
Rule

Core classes

  • User: represents a player and tracks current position.
  • Dice: Singleton-style dice generator for moves.
  • Board: stores all jumps and resolves final position after a move.
  • Rule: checks whether a move wins or is valid.
  • gameManager: owns player queue and turn orchestration.
  • playerState: subject that notifies observers about player state changes.

Why these patterns are used

Singleton is used for `Dice` in the demo because the game needs one shared dice generator. This avoids creating dice objects everywhere and gives a single place to change dice behavior. In production or unit testing, dependency injection is usually better because it allows deterministic dice rolls.

Observer Pattern is used for player state updates. The game manager should not directly know every UI, logger, or scoreboard that needs updates. Instead, player state acts as the subject and observers react when state changes. This keeps the game engine decoupled from display or notification behavior.

Queue is used for turn management because Snake and Ladder is naturally round-robin. The current player is removed from the front, takes a turn, and if the game is not over, the player is added back to the end.

Algorithm explanation

The move algorithm starts by polling the next player from the queue. The dice generates a number from 1 to 6. The game calculates `currentPosition + diceValue`. If the move exceeds the board size, the player usually stays in place depending on the rule. Otherwise, the board checks whether the destination has a snake or ladder and returns the final resolved position.

Board jump resolution is a map lookup algorithm. A map stores `startCell -> endCell`. If the landed cell exists in the map, the player moves to the mapped value; otherwise, the position remains unchanged. This is efficient because lookup is expected `O(1)`.

The winning check is handled by `Rule`. After final position is resolved, the rule checks whether the player reached the target cell. If yes, the game ends; otherwise, the player is pushed back into the turn queue.

Strong interview answer

"I would keep board logic separate from game orchestration. The manager controls turn order, dice gives the move, board resolves snakes or ladders, and rule validates the outcome. This keeps the design easy to extend for custom board sizes, extra dice, or different winning rules."

Production improvements

  • Rename classes to Java conventions: `GameManager`, `PlayerState`, and `PlayerStatusObserver`.
  • Add support for configurable board size, dice count, and exact-win rule.
  • Validate that jumps do not create impossible or circular board states.
  • Separate random dice from deterministic dice for unit testing.
  • Add a game status model: waiting, running, completed.

Weakness to mention

The demo explains the object model, but production-quality code should improve naming conventions, encapsulation, validation of board configuration, and deterministic testing of dice behavior.