What is Snake and Ladder Game?

Snake and Ladder is a classic turn-based board game played by two or more players on a numbered grid, typically containing 100 cells. Each player starts at the beginning of the board and takes turns rolling a dice to determine how many positions they can move forward.

The board contains two special types of positions:

  • Ladders, which help players move forward by taking them from a lower cell to a higher cell.

  • Snakes, which move players backward from a higher cell to a lower cell.

For example, if a ladder connects cell 12 to cell 35, a player landing on 12 immediately moves to 35. Similarly, if a snake starts at cell 47 and ends at cell 19, a player landing on 47 slides down to 19.

The first player to land exactly on the final cell, typically cell 100, is declared the winner.

Although the rules of Snake and Ladder are simple, designing it is a useful LLD exercise because it brings together several important object-oriented design concepts such as encapsulation, inheritance, composition, validation, turn management, and design patterns.

In this chapter, we will explore the low-level design of a Snake and Ladder game in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it is important to clarify the rules and remove any ambiguity from the problem statement.

In an LLD interview, this step is important because small assumptions can significantly affect the design. Instead of immediately creating classes, we first understand exactly how the game is expected to behave and then design around those requirements.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

Discussion

Candidate: Should the game support a standard 10x10 board with 100 cells, or should the board size be configurable?Interviewer: For this version, let’s stick with the standard 10x10 board.

Candidate: Should the number and positions of snakes and ladders be fixed, or should they be configurable? Interviewer: They should be configurable. The board should allow us to define the number and positions of snakes and ladders at initialization.

Candidate: How many players should the game support? Should it be limited to two, or should we support multiple players?
Interviewer: The game should support at least two players but potentially more. Player turns should rotate in order.

Candidate: How should dice rolls be handled? Should we simulate a dice roll in the code or take it as input? Interviewer: Let’s simulate dice rolls using random number generation from 1 to 6. No need for user input for the roll itself.
Candidate: What should happen if a player rolls a 6? Should they get another turn? Interviewer: Yes, if a player rolls a 6, they get an extra turn immediately.
Candidate: And if they keep rolling 6s? Should the extra turns continue forever? Interviewer: No. Three 6s in a row forfeits the whole turn. The player returns to the position they started the turn from, and play moves to the next player.
Candidate: Should a player roll an exact number to land on cell 100, or can they overshoot and still win? Interviewer: A player must land exactly on 100 to win. If the roll takes them beyond 100, their turn is skipped.
Candidate: Can multiple players occupy the same square at the same time? Interviewer: Yes, more than one player can land on the same square. There's no interaction or conflict when this happens. Players never displace each other and there are no penalties.

Therefore, the board does not need to track player occupancy for individual cells.

After gathering the details, we can summarize the key system requirements.

Functional Requirements

  • The game is played on a standard 10x10 board with 100 numbered cells.

  • The board supports configuration of snakes and ladders with flexible start and end positions.

  • The game supports multiple players, with a minimum of two players.

  • Players take turns in round-robin order.

  • The system simulates dice rolls with random values between 1 and 6.

  • A player gets an extra turn when they roll a 6.

  • Three consecutive 6s forfeit the entire turn, returning the player to the position where that turn started.

  • A player must roll the exact number required to land on cell 100 to win.

  • A roll that would move the player beyond 100 is ignored and the player's position remains unchanged.

  • Multiple players can occupy the same cell without any interaction or penalty.

Non-Functional Requirements

  • Modularity: The system should follow object-oriented principles with clean separation between components.

  • Extensibility: The design should allow future enhancements such as custom board sizes, different types of dice, or additional game rules.

  • Maintainability: The codebase should be clean, readable, loosely coupled, and easy to extend.

  • User Feedback: The system should provide clear console output after each turn, indicating player moves, dice rolls, snake or ladder interactions, and current positions.

After gathering the requirements, we can now identify the core entities and objects that will represent the system.

2. Identifying Core Entities

How do we go from a list of requirements to actual classes?

A useful starting point is to look for nouns that represent meaningful entities, state, or responsibilities. However, not every noun automatically needs to become a class. We create a class when a concept has its own state, behavior, or responsibility that deserves to be encapsulated.

Let's walk through our requirements and identify what needs to exist in our system.

1. The game is played on a standard 10x10 board with 100 cells.

The board is the central environment in which the game takes place, so we need a Board entity.

However, we do not necessarily need to create a separate object for every cell. The cells themselves do not contain meaningful independent state. What matters is whether a particular position contains a special transition.

The Board therefore needs to know the size of the board and the special transitions associated with snakes and ladders.

2. Support configuration of snakes and ladders with flexible start and end positions

We clearly need Snake and Ladder entities. Both have the same basic structure:

  • A starting position

  • An ending position

However, their movement rules are different.

For a snake:

 start > end

For a ladder:

 start < end

This similarity suggests an abstract base class called BoardEntity.

The common start and end attributes can be defined in BoardEntity, while Snake and Ladder can enforce their respective validation rules.

Why use inheritance here?

Inheritance is appropriate because Snake and Ladder are specialized forms of the same conceptual entity: a board transition.

The base class captures their common structure, while the subclasses enforce the rules that distinguish them.

3. Allow multiple players (minimum two), with turn rotation in round-robin order

We need a Player entity to represent each participant.

A player has a name and a current position on the board.

Players start at position 0, which represents the state before entering cell 1. Their position changes throughout the game as they roll the dice and move.

4. Simulate dice rolls with random values between 1 and 6

We need a Dice entity responsible for generating random values.

We could directly perform random number generation inside the game logic, but that would tightly couple the Game class to a specific dice implementation.

Encapsulating dice behavior in its own class provides several benefits:

  • The dice logic is isolated from the game logic.

  • The dice range can be configurable.

  • Different dice implementations can be introduced later.

  • The dice can be replaced with a deterministic implementation during testing.

5. The game should manage turns, apply snakes and ladders, and determine when a player wins

Something needs to coordinate all the components and enforce the rules of the game.

That responsibility belongs to the Game entity.

The Game will:

  • Manage the turn order.

  • Ask the dice for a roll.

  • Calculate player movement.

  • Handle overshooting the final cell.

  • Resolve snakes and ladders.

  • Handle the extra-turn rule for rolling a 6.

  • Detect three consecutive 6s.

  • Determine when a player has won.

  • Maintain the current game state.

The game also needs to track its lifecycle.

An enum called GameStatus can represent the three possible states:

NOT_STARTED
RUNNING
FINISHED

Entity Overview

Here's how these entities relate to each other:

Whiteboard
Loading diagram...


We've identified three types of entities:

Enums define fixed sets of values. GameStatus tracks the lifecycle of the game.

Data Classes primarily hold state with minimal behavior. Player stores player information, while BoardEntity and its subclasses represent board transitions.

Core Classes contain the main application logic. Dice handles random rolls, Board manages position transitions, and Game orchestrates the entire gameplay.

EntityTypeResponsibility
GameStatusEnumGame lifecycle: NOT_STARTED, RUNNING, FINISHED
PlayerData ClassHolds player name and current position
BoardEntityAbstract ClassBase class for snakes and ladders with start/end positions
SnakeData ClassBoard entity where start > end (sends player backward)
LadderData ClassBoard entity where start < end (moves player forward)
DiceCore ClassSimulates random dice rolls within a range
BoardCore ClassManages the board and position transitions
GameCore ClassOrchestrates gameplay, turns, and win detection

With our entities identified, let's define their attributes, behaviors, and relationships.

3. Designing Classes and Relationships

Now that we know which entities are required, the next step is to define their responsibilities more precisely.

For each class, we will identify what state it owns, what behavior it provides, and how it interacts with the other classes.

We'll work bottom-up: simple types first, then data containers, and finally the classes containing the actual game logic. This makes the dependencies easier to understand and keeps each design decision grounded in the requirements.

Note

While listing class methods, we will skip trivial getters and setters to keep the walkthrough focused on core behaviors and design decisions.

3.1 Class Definitions

We'll work bottom-up: simple types first, then data classes, and finally the classes with real logic. This order makes sense because complex classes depend on simpler ones.

Enums

Enums define fixed sets of values that provide type safety and make the code self-documenting.

GameStatus

Tracks where we are in the game lifecycle.

Whiteboard
Loading diagram...


ValueDescriptionTerminal?
NOT_STARTEDGame is initialized but not yet startedNo
RUNNINGPlayers are actively taking turnsNo
FINISHEDA player has won the gameYes

These three states completely describe the lifecycle of the game.

Unlike Tic-Tac-Toe, there is no DRAW state because the specified rules always produce a winner once a player reaches the final cell.

Design Decision

We use a simple three-state enum rather than embedding winner information in the status itself.

The winner is tracked separately in the Game class. This keeps GameStatus focused on one responsibility: representing the lifecycle state of the game.

Data Classes

Data classes are primarily responsible for holding state with minimal behavior.

Player

Encapsulates all relevant information about a player.

Whiteboard
Loading diagram...


AttributeTypeDescriptionMutable?
nameStringPlayer identifier, such as AliceNo
positionintCurrent position on the board, from 0 to 100Yes
MethodDescription
Player(name)Constructor that initializes the player at position 0

The Player starts at position 0, which represents the state before entering cell 1.

The position is mutable because it changes after every valid move. The name remains immutable because the player's identity does not change during gameplay.

BoardEntity

Abstract base class for snakes and ladders.

Whiteboard
Loading diagram...


AttributeTypeDescription
startintPosition where the entity begins
endintPosition where the entity transports the player

The BoardEntity class stores the common state shared by Snake and Ladder.

Both fields are final because a snake or ladder is part of the board configuration and should not change during the game.

The base class does not decide whether the movement is upward or downward. That responsibility belongs to the subclasses.

Snake

Represents a snake on the board.

ValidationRule
ConstructorThrows exception if start <= end

When a player lands on the snake's head at start, they slide down to end.

The constructor enforces the rule that a snake must always move downward:

 start > end

For example, a configuration such as 47 → 19 is valid, while 19 → 47 is not.

This is an example of fail-fast validation. Instead of allowing an invalid snake to exist and discovering the problem later during gameplay, we reject the configuration immediately when the object is created.

Ladder

Represents a ladder on the board.

ValidationRule
ConstructorThrows exception if start >= end

A ladder always moves a player forward, so it must satisfy:

 start < end

For example, 12 → 35 is valid, while 35 → 12 is not.

Just like Snake, the Ladder constructor validates its invariant immediately. This keeps invalid board configurations from entering the game.

Core Classes

Core classes contain the actual game logic.

Dice

A utility class responsible for simulating a dice roll.

Whiteboard
Loading diagram...


AttributeTypeDescription
minValueintMinimum possible dice value
maxValueintMaximum possible dice value
MethodDescription
Dice(minValue, maxValue)Constructor with configurable range
roll()Returns a random value between minValue and maxValue, inclusive

For a standard dice:

minValue = 1maxValue = 6

The important design decision is that the Game does not need to know how randomness is generated.

It simply asks the Dice to roll.

This keeps the randomness isolated and makes it easier to replace the implementation later, for example with a different dice type or a deterministic dice during testing.

Board

Manages the game board and position transitions.

Whiteboard
Loading diagram...


AttributeTypeDescription
sizeintTotal number of cells on the board
snakesAndLaddersMap<Integer, Integer>Maps starting positions to their destinations
MethodDescription
Board(size, entities)Builds the board and creates the position-transition map
getFinalPosition(position)Returns the final position after applying a snake or ladder

The Board does not need to store individual cell objects because the cells themselves do not contain meaningful state.

Instead, it stores only the positions where a special transition exists.

For example:

12 → 35

47 → 19

65 → 24

Internally, these transitions are stored in a map.

When a player lands on a position, the board performs a lookup:

  • If the position exists in the map, the corresponding destination is returned.

  • If it does not exist, the original position is returned.

This allows snakes and ladders to be handled through a single mechanism.

The Board also validates its configuration while building this map.

Every entity must:

  • Start within the board.

  • End within the board.

  • Follow its own start/end rule.

  • Have a unique starting position.

For example, this configuration is invalid:

Snake: 50 → 20

Ladder: 50 → 80

A single position cannot have two different destinations. Since the map can contain only one value for key 50, the board should reject the configuration rather than silently overwriting one entity.

The lookup is also applied only once per move.

For example, suppose we have:

Ladder: 10 → 40

and:

Snake: 40 → 15

If a player lands on 10, they move to 40. The game does not immediately apply the snake at 40.

This keeps each move predictable and avoids chained snake-and-ladder transitions.

Design Decision

Using a Map provides an average O(1) lookup for snake and ladder positions.

We could maintain separate lists of snakes and ladders, but then every movement would require searching through those lists.

The map gives us a much simpler representation:

starting position → destination

It also allows both snakes and ladders to be resolved using the same method.

Game

Main orchestrator that coordinates all game elements.

Whiteboard
Loading diagram...


AttributeTypeDescription
boardBoardThe game board
playersQueuePlayers maintained in turn order
diceDiceDice used to generate rolls
statusGameStatusCurrent state of the game
winnerPlayerPlayer who wins the game; null until the game ends
MethodDescription
Game(Builder)Private constructor that receives validated configuration from Builder
play()Main game loop that runs until someone wins
takeTurn(player)Handles a single player's turn and applies all relevant game rules

Key Design Principles:

Orchestration: Game acts as the central coordinator. It brings together the Board, Dice, and players and is responsible for enforcing game-level rules.

Queue for Turn Management: Using a Queue is a natural way to model round-robin turns. We take the player at the front of the queue, execute their turn, and if the game continues, add them back to the end.

For example:

Alice → Bob → Charlie

After Alice's turn:

Bob → Charlie → Alice

After Bob's turn:

Charlie → Alice → Bob

This handles any number of players without requiring a separate turn index.

Builder Pattern: Constructing a Game requires multiple components such as the board, players, and dice. The Builder pattern keeps object creation readable and provides a single place to validate the configuration before creating the game.

3.2 Key Design Patterns

Several useful design patterns emerge naturally from this design.

The important point is that we are not introducing patterns simply for the sake of using them. Each pattern addresses a specific design problem in the system.

Builder Pattern

The Problem: Creating a Game requires multiple configuration steps: setting up the board with snakes and ladders, adding players, and configuring the dice.

If we use a constructor with many parameters, it becomes harder to read and easier to misuse.

For example, as the number of configuration options grows, a constructor can quickly become difficult to understand:

Game(board, players, dice, ...)

It may also become unclear which parameters are mandatory and which are optional.

The Solution: The Builder Pattern encapsulates the construction process in a separate Builder class.

This allows us to configure the game step by step:

Whiteboard
Loading diagram...


Each configuration method returns the builder itself, allowing method chaining.

Design Decision

The Builder is implemented as an inner static class of Game.

This keeps the construction logic close to the object being created while allowing the builder to invoke the private Game constructor.

The build() method can also validate that all required components have been provided before creating the game.

Facade Pattern (Game as Controller)

The Problem: External code should not need to understand the internal implementation of Board, Dice, and player management.

A caller should not have to manually coordinate:

  • Player selection

  • Dice rolling

  • Position calculation

  • Snake and ladder resolution

  • Extra turns

  • Three-six handling

  • Winner detection

  • Queue rotation

The Solution: The Game class acts as a facade over the underlying components.

External code can simply call:

game.play()

The Game internally coordinates the entire process.

The Game class therefore provides:

  • A single entry point for running the game.

  • Encapsulation of the game loop.

  • Turn management.

  • Movement and rule processing.

  • Winner detection.

  • Console output describing game progress.

This keeps the public interface simple while hiding the complexity of the underlying implementation.

3.3 Full Class Diagram

Whiteboard
Loading diagram...


4. Code Implementation

Now let's translate our design into working code. We'll build bottom-up: foundational types first, then data classes, and finally the classes containing the actual game logic.

This order keeps the implementation easy to follow because each class depends on concepts that have already been introduced.

4.1 Enum

We start with the enum that tracks game state.

GameStatus

The GameStatus enum contains three states:

The game starts in NOT_STARTED.

When play() begins, the state changes to RUNNING.

Once a player reaches the final cell, the state changes to FINISHED.

Keeping the lifecycle explicit makes the state of the game easy to understand and prevents other parts of the system from having to infer it indirectly.

4.2 Data Classes

These classes primarily hold data with minimal behavior.

Player

The Player class encapsulates the state associated with an individual player.


A newly created player starts at position 0, representing the position before cell 1.

The player's name remains unchanged throughout the game, while the position changes after every valid move.

Keeping this state inside the Player object means the Game does not need to maintain a separate structure for player positions.

Now let's implement the BoardEntity hierarchy.

BoardEntity

The abstract BoardEntity class stores the common attributes shared by snakes and ladders:

Both fields are final because the position of a snake or ladder should not change after the board has been initialized.

The class is abstract because we do not need to create a generic board entity directly. Its purpose is to provide a common abstraction for Snake and Ladder.

Snake

The Snake constructor enforces the rule:

This guarantees that every snake represents a downward movement.

If someone attempts to create a snake with an invalid configuration, the constructor immediately throws an exception.

This is preferable to allowing an invalid object to exist and discovering the problem later during gameplay.

Ladder

Similarly, the Ladder constructor enforces:

This guarantees that every ladder represents an upward movement.

Again, the validation occurs during construction so configuration errors are detected as early as possible.

4.3 Dice Class

The Dice encapsulates random number generation with a configurable range.

The constructor accepts the minimum and maximum values, while roll() generates a random integer within that inclusive range.

For a standard six-sided dice:

minValue = 1

maxValue = 6

The formula Math.random() * (max - min + 1) + min generates a random integer within the configured range.

For the standard configuration, the possible values are 1 through 6.

The important architectural point is that the Game does not perform random number generation itself.

It simply asks the Dice to roll.

This keeps randomness isolated from the game rules and makes the component easier to replace during testing.

4.4 Board Class

The Board manages the game surface and position transitions.

When the board is created, it receives a list of BoardEntity objects and converts them into a position-transition map.

For example, if the board contains:

Snake: 47 → 19

Ladder: 12 → 35

Ladder: 25 → 60

the internal map contains the corresponding transitions.

The getFinalPosition() method then performs a single lookup.

If the position exists in the map, the mapped destination is returned. Otherwise, the original position is returned.

This gives us one common mechanism for handling both snakes and ladders.

The board also validates its configuration while constructing this map:

  • Every start position must be within the board.

  • Every end position must be within the board.

  • Every snake and ladder must follow its respective invariant.

  • No two entities can have the same starting position.

This ensures that an invalid board configuration fails immediately instead of causing unpredictable behavior during gameplay.

4.5 Game Class

This is where everything comes together. The Game orchestrates the entire gameplay loop.

The play() method is responsible for:

  1. Validating the minimum player count.

  2. Setting the game status to RUNNING.

  3. Taking the player at the front of the queue.

  4. Executing that player's turn.

  5. Checking whether the game has finished.

  6. Returning the player to the back of the queue if the game continues.

  7. Repeating the process until a winner is found.

The takeTurn() method handles all the rules associated with a player's turn:

  1. Store the player's position at the beginning of the turn.

  2. Roll the dice.

  3. Track consecutive sixes.

  4. Calculate the tentative position.

  5. Reject moves that overshoot the final cell.

  6. Detect a winning move when the player lands exactly on the final cell.

  7. Resolve snakes and ladders.

  8. Update the player's position.

  9. Grant an additional roll when the player rolls a 6.

  10. Restore the original position if three consecutive sixes are rolled.

Let's break down the key aspects of the Game class:

The play() method:

The method first validates that the game contains at least two players.

It then changes the status from NOT_STARTED to RUNNING and begins taking players from the front of the queue.

After a player completes their turn, they are placed at the back of the queue if the game is still running.

For example:

Alice → Bob → Charlie

After Alice's turn:

Bob → Charlie → Alice

After Bob's turn:

Charlie → Alice → Bob

This provides simple round-robin turn management without requiring a separate turn counter.

The takeTurn() method handles all the game rules:

At the beginning of the turn, we store the player's starting position.

This is necessary because three consecutive 6s require the entire turn to be cancelled.

We also initialize a counter for consecutive sixes.

Each roll then follows this sequence:

  1. Generate and display the dice result.

  2. Update the consecutive-six counter.

  3. If three consecutive sixes have occurred, restore the player's original position and end the turn.

  4. Otherwise, calculate the tentative new position.

  5. If the new position exceeds the board size, leave the player's position unchanged.

  6. If the player lands exactly on the final cell, declare them the winner and finish the game.

  7. Otherwise, ask the Board to resolve any snake or ladder at the new position.

  8. Update the player's position.

  9. If the dice result is 6, allow another roll.

  10. Otherwise, end the turn.

The most important detail is that the rollback position belongs to the entire turn, not the previous dice roll.

For example, if a player starts at position 20 and rolls:

6 → 6 → 6

the player must return to position 20.

They should not return to position 26 or 32.

The Builder pattern:

The Game constructor is private, so callers must create the game through Game.Builder.

Each builder method configures one part of the game:

setBoard(...)

setPlayers(...)

setDice(...)

The final build() method validates the configuration and creates the Game.

This keeps construction readable and prevents partially initialized game objects.

Game Flow Sequence

The following diagram illustrates what happens during a player's turn: