Snake and Ladder is a classic turn-based board game played by two or more players on a grid, typically numbered from 1 to 100. Each player starts at cell 1 and takes turns rolling a dice to determine how many steps to move forward.
The game includes:
The first player to land exactly on the final cell (e.g., cell 100) is declared the winner.
Loading simulation...
In this chapter, we will explore the low-level design of a snake and ladder game in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a conversation between the candidate and the interviewer might unfold:
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 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."
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, lets identify the core entities/objects we will have in our system.
How do you go from a list of requirements to actual classes?
The key is to look for nouns in the requirements that have distinct attributes or behaviors. Not every noun becomes a class, but this approach gives you a starting point.
Let's walk through our requirements and identify what needs to exist in our system.
The board is central to everything. We need something to represent it. This gives us our first entity: Board.
Unlike Tic-Tac-Toe where each cell holds a symbol, Snake and Ladder cells are just positions. The interesting part is what happens at certain positions. The Board needs to know which positions have snakes or ladders and where they lead.
We clearly need Snake and Ladder entities. Both have a start position and an end position. A snake's start (head) must be higher than its end (tail). A ladder's start (bottom) must be lower than its end (top).
Notice the similarity?
Both are "board entities" that transport a player from one position to another. This suggests an abstract base class BoardEntity that Snake and Ladder can inherit from. The base class holds the common logic (start and end positions), while subclasses enforce their specific validation rules.
Because snakes and ladders share structure but differ in behavior. A snake must have start > end. A ladder must have start < end. Inheritance lets us enforce these constraints in the subclass constructors while sharing the common code.
We need to represent players. Each player has a name and a current position on the board. This gives us the Player entity.
Players start at position 0 (off the board) and move toward 100. Their position changes after each dice roll, potentially modified by snakes or ladders.
We need a Dice entity to simulate random rolls. While we could just call Math.random() directly in the game logic, encapsulating dice behavior in its own class provides several benefits:
Something needs to coordinate the gameplay: roll dice, move players, check for snakes/ladders, handle the special "roll 6 for extra turn" rule, and detect when someone wins. This orchestrator is our Game entity.
The game also needs to track its current state. Is it still running? Has someone won? An enum GameStatus with values NOT_STARTED, RUNNING, and FINISHED captures all possibilities cleanly.
Here's how these entities relate to each other:
We've identified three types of entities:
Enums define fixed sets of values. GameStatus tracks the game lifecycle.
Data Classes primarily hold data with minimal behavior. Player tracks name and position. BoardEntity (and its subclasses) holds start and end positions.
Core Classes contain the main logic. Dice generates random rolls, Board manages position transitions, and Game orchestrates the entire gameplay loop.
With our entities identified, let's define their attributes, behaviors, and relationships.
Now that we know what entities we need, let's flesh out their details. For each class, we'll define what data it holds (attributes) and what it can do (methods). Then we'll look at how these classes connect to each other.
While listing class methods, we will skip trivial getters and setters to keep the walkthrough focused on core behaviors
We'll work bottom-up: simple types first, then data containers, then the classes with real logic. This order makes sense because complex classes depend on simpler ones.
Enums define fixed sets of values that provide type safety and make code self-documenting.
GameStatusTracks where we are in the game lifecycle.
Three distinct states cover the game lifecycle. Unlike Tic-Tac-Toe, there's no DRAW state because Snake and Ladder always eventually produces a winner.
We use a simple three-state enum rather than embedding winner information in the status. The winner is tracked separately in the Game class. This keeps the enum simple and focused on lifecycle state.
Data classes are simple containers that hold data with minimal behavior.
PlayerEncapsulates all relevant information about a player
The Player starts at position 0, which represents "off the board" (before cell 1). The position is mutable because it changes after every move. The name is immutable since it never changes during gameplay.
BoardEntityAbstract base class for snakes and ladders.
SnakeRepresents a snake on the board.
When a player lands on the snake's head (start position), they slide down to the tail (end position). The constructor enforces that the head is always higher than the tail.
LadderRepresents a ladder on the board.
When a player lands on the ladder's bottom (start position), they climb up to the top (end position). The constructor enforces that the bottom is always lower than the top.
Core classes contain the actual game logic.
DiceA utility class responsible for simulating a dice roll.
BoardManages the game board and position transitions.
The Board doesn't store individual cells because we don't need to track cell contents. Instead, it maintains a map from snake/ladder start positions to their end positions. When a player lands on a position, we check if there's an entry in this map. If so, we return the mapped position; otherwise, we return the original position.
The Board also validates its configuration while building this map. Every entity must start and end within the board, and no two entities can share a start cell, since the map can only hold one destination per cell. An invalid layout fails fast in the constructor instead of producing silent, hard-to-debug behavior mid-game.
Using a Map for O(1) lookup is efficient. We could store separate lists of snakes and ladders, but the map simplifies the getFinalPosition() logic to a single lookup. The lookup is applied once per move, so a ladder whose top lands on a snake’s head does not chain into a second jump. This matches how physical boards are designed and keeps a single move predictable.
GameMain orchestrator that coordinates all game elements.
Key Design Principles:
You might notice some structural patterns emerging in our design. Let's make them explicit and justify why each pattern is appropriate here.
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 hard to read and error-prone. What order do the parameters go in? Which ones are optional?
The Solution: The Builder pattern encapsulates the construction logic in a separate Builder class. Each configuration step returns the builder, allowing method chaining. The final build() call validates everything and creates the Game.
The Builder is an inner static class of Game. This keeps related code together and allows the builder to access Game's private constructor. The build() method validates that all required components are set before creating the Game.
The Problem: External code shouldn't need to understand the internals of Board, Dice, and Player management. They just want to start and play a game.
The Solution: The Game class acts as a facade, providing simple play() method that hides all the complexity of turn management, dice rolling, position updates, and win detection.
The Game class provides:
Before reading the full implementation, try building this yourself. Below are template stubs for the classes from the design. The enums, data classes, and the demo are already written for you. The job is to fill in the core logic where the TODO comments appear.
Your task: Implement every TODO using the design from the previous sections. When you are done, run the demo. A correct implementation produces the expected output.
Now let's translate our design into working code. We'll build bottom-up: foundational types first, then data classes, then the classes with real logic. This order matters because each layer depends on the ones below it.
We start with the enum that tracks game state.
GameStatusThree states cover the game lifecycle. The game starts NOT_STARTED, transitions to RUNNING when play begins, and ends as FINISHED when someone wins.
These classes primarily hold data with minimal logic.
PlayerPlayers start at position 0, which represents "off the board" (before cell 1). The name is immutable, but position changes throughout the game.
Now let's implement the BoardEntity hierarchy:
BoardEntityThe abstract base class stores the common attributes. Both fields are final because a snake or ladder's position never changes during the game.
SnakeThe Snake constructor enforces the rule that snakes go downward. If someone tries to create a snake where the head is below the tail, we fail immediately with a clear error message.
LadderSimilarly, the Ladder constructor enforces that ladders go upward. This "fail fast" validation catches configuration errors immediately.
The Dice encapsulates random number generation with a configurable range.
The formula Math.random() * (max - min + 1) + min generates a random integer in the inclusive range [min, max]. For a standard die, this returns values 1 through 6 with equal probability.
The Board manages the game surface and position transitions.
The Board converts the list of BoardEntity objects into a Map for O(1) position lookups. The getFinalPosition() method is elegant: if the position is in the map (snake head or ladder bottom), return the mapped value. Otherwise, return the original position. This single method handles both snakes and ladders uniformly.
This is where everything comes together. The Game orchestrates the entire gameplay loop.
Let's break down the key aspects of the Game class:
The play() method:
The takeTurn() method handles all the game rules:
takeTurn() for the extra turn. Three 6s in a row forfeits the whole turn and sends the player back to where they started itThe Builder pattern:
this for method chainingbuild() validates all components are setThe following diagram illustrates what happens during a player's turn:
The base design keeps each concern in its own class, so most new requirements land as new classes or new fields rather than edits to the game loop. The Game orchestrates turns, the Board owns position lookups, the Dice produces rolls, and BoardEntity covers snakes and ladders. The four extensions below build on those same classes.
Scenario: "Let players roll two dice per turn, and grant an extra turn when both dice show the same value."
The current Dice rolls a single value through roll(). To support multiple dice, we wrap a list of Dice in a DiceRoller that returns the total and reports whether the roll was a double. The takeTurn logic uses the total exactly as it used a single roll today, and an extra-turn rule reads the double flag. Because Dice.roll() stays unchanged, the existing single-die game keeps working by passing a roller with one die.
Game replaces its direct dice.roll() call with diceRoller.rollAll(), adds the returned total to the player's position, and re-adds the current player to the front of the turn queue when isDouble is true. The win and overshoot checks in takeTurn stay the same because they only depend on the total.
Scenario: "Generate boards of different sizes with snakes and ladders placed by different rules, including a randomized layout."
The Board constructor already takes a size and a list of BoardEntity objects, so a 10x10 board is just one configuration. To vary how snakes and ladders are placed, we introduce a BoardSetupStrategy that produces the entity list for a given size. A fixed strategy returns a hand-authored layout, while a random strategy generates valid snakes and ladders. The Game.Builder.setBoard step calls the strategy and passes the result to the existing Board constructor.
The strategy respects the same constraints the Snake and Ladder constructors enforce, so each generated entity passes its own validation. A production version would also reject overlapping starts and entities that chain into each other. The Board itself does not change, since it still receives a size and a list of BoardEntity objects.
20 quizzes