LLD of the Snake and Ladder game

Abhijeet Gulve
4 min readJul 26, 2020

Problem Statement :

Basic Board: On a board (Of size 100), for a dice throw a player should
move from the initial position by the number on dice throw.

Add a snake on the board: A snake moves a player from its start position to end position. where start position > end position
Test data: Add a snake at position 14 moving the player
to position 7.

Make A Crooked Dice: A dice that only throws Even numbers.
The can game can be started with normal dice or crooked dice.

Implementation:

Before jump into the design, we will figure out entities. The first entity that came into my mind is Game. The game will consist of Players, Board, and Dice. Let's break each entity to find new entities. If we take the player we didn't find any special entity. Then take the board, we see one entity a list of cells. In dice, we see just one method that will return 1–6 random numbers for now. One more class is for moving the object from higher cell to lower (call snake) or vice versa (ladder). You can see these all entities in the diagram below.

class diagram snake and ladder game

Now let's start writing some code for each class.

1.Game

import exception.GameOverException;
import exception.InvalidPlayerException;
import model.Board;
import model.Player;

import strategy.Dice;

import java.util.*;

public class Game {
private final Board board;
private Dice dice;
private final Map<Integer, Player> res;
private int winnerCount;
private final Queue<Player> playerQueue;

public Queue<Player> getPlayerQueue() {
return playerQueue;
}

public Map<Integer, Player> getRes() {
return res;
}


public void setDice(Dice dice) {
this.dice = dice;
}

public Game(List<Player> playerList, Board board, Dice dice) {
this.board = board;
this.dice = dice;
this.res = new HashMap<>();
this.playerQueue = new ArrayDeque<>(playerList);
}

public Player getNextPlayerToPlay() throws GameOverException {
if (playerQueue.size() <= 1)
throw new GameOverException("Game is already over");
return playerQueue.poll();
}

public void play(Player player) throws InvalidPlayerException {
if (!checkReachToEnd(player)) {
int nextPosition = board.moveToNextPosition(player.getPosition(), dice.rollDice());
player.setPosition(nextPosition);
updateGameStatus(player);
} else throw new InvalidPlayerException("player already reach end of game");
}

private void updateGameStatus(Player player) {
if (checkReachToEnd(player)) {
res.put(++winnerCount, player);
if (playerQueue.size() == 1) {
res.put(++winnerCount, playerQueue.poll());
}
} else playerQueue.add(player);
}

private void declareResult(Map<Integer, Player> res) {
res.forEach((k, v) -> System.out.println(v + " ranks " + k));
}

private boolean checkReachToEnd(Player player) {
return player.getPosition() == 100;
}
}

2.Board

import java.util.List;
import java.util.Optional;

public class Board {
List<Cell> cells;

public List<Cell> getCells() {
return cells;
}

public Board(List<Cell> cells) {
this.cells = cells;
}

public int moveToNextPosition(int currentPosition, int score) {
if (currentPosition < 0)
throw new IllegalArgumentException("Position should be greater than zero");
Optional<Cell> nextCellByPosition = getNextCellByPosition(currentPosition + score);
return nextCellByPosition.map(Cell::nextPosition).orElse(currentPosition);
}

private Optional<Cell> getNextCellByPosition(int position) {
return cells.stream().filter(cell -> cell.getPosition() == position)
.findFirst();
}
}

3.Player

import java.util.Objects;

public class Player {
private int position;
private final String playerName;

public String getPlayerName() {
return playerName;
}

public int getPosition() {
return position;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Player)) return false;
Player player = (Player) o;
return position == player.position &&
Objects.equals(playerName, player.playerName);
}

@Override
public int hashCode() {
return Objects.hash(position, playerName);
}

public Player(int position, String playerName) {
this.position = position;
this.playerName = playerName;
}

@Override
public String toString() {
return "Player :" + playerName;
}

public void setPosition(int position) {
this.position = position;
}
}

4. Dice

Dice will be an interface with different implementations. Here is normal dice implementation.

import java.util.Random;

public class NormalDice implements Dice {
@Override
public int rollDice() {
Random random = new Random();
return 1+random.nextInt(6);
}
}

5. Move

The move will abstract class and their implementations will be different types of snake and ladders.

import exception.InvalidPositionException;

public class DefaultMove extends Move {

public DefaultMove() {
super(0);
}
@Override
public boolean isValidPosition(int cellPosition) throws InvalidPositionException {
return false;
}
}

6. GreenSnake

As already mention Move can extended to different types of snakes, we have one implementation for a green snake, a snake that bites only once, if once it bites once, it will be ineffective in the rest of the game.

import exception.InvalidPositionException;

public class GreenSnake extends Move {
private boolean canMove = true;

public GreenSnake(int nextPosition) {
super(nextPosition);
}

@Override
public boolean isValidPosition(int cellPosition) throws InvalidPositionException {
return false;
}

@Override
public int getNextPosition() {
int next = nextPosition;
if (canMove) {
nextPosition = 0;
canMove = false;
}
return next;
}
}

Test: We can test this game using test cases.

import exception.GameOverException;
import exception.InvalidPlayerException;
import model.Board;
import model.Cell;
import model.DefaultMove;
import model.Player;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import strategy.Dice;
import strategy.MockDice;

import java.util.ArrayList;
import java.util.List;

public class GameTest {
private Game game;
List<Player> playerList;
Board board;
Dice dice;

@Before
public void before() {
playerList = new ArrayList<>();
playerList.add(new Player(0, "A"));
playerList.add(new Player(0, "B"));
List<Cell> cells = getCells(100);
board = new Board(cells);
dice = new MockDice();
game = new Game(playerList, board, dice);
}

@Test
public void playTest() throws InvalidPlayerException {
Player p = new Player(1, "Test");
game.setDice(new MockDice());
game.play(p);
Assert.assertEquals(p.getPosition(), 26);
}

@Test
public void endToEndTest() throws InvalidPlayerException, GameOverException {
Player p1 = playerList.get(0);
Player p2 = playerList.get(1);
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(25, p1.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(25, p2.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(50, p1.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(50, p2.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(75, p1.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(75, p2.getPosition());
game.play(game.getNextPlayerToPlay());
Assert.assertEquals(100, p1.getPosition());
Assert.assertEquals(p1, game.getRes().get(1));
p1.setPosition(0);
p2.setPosition(0);
game.getPlayerQueue().add(p1);
}

@Test(expected = GameOverException.class)
public void testGameOver() throws InvalidPlayerException, GameOverException {
Player p1 = game.getNextPlayerToPlay();
p1.setPosition(75);
game.play(p1);
game.getNextPlayerToPlay();
}

@Test(expected = InvalidPlayerException.class)
public void testInvalidPlayerPlay() throws InvalidPlayerException {
Player player = new Player(1, "Test");
player.setPosition(100);
game.play(player);
}

public static List<Cell> getCells(int numberOfCells) {
List<Cell> cells = new ArrayList<>();
for (int i = 1; i <= numberOfCells; i++) {
cells.add(new Cell(i, new DefaultMove()));
}
return cells;
}
}

I hope you liked this tutorial, for complete code you can refer here.

--

--