Table of Contents
Problem Statement
How would you design a game of Snakes and Ladders
Hints
- Think in terms of the entities involved
- Think about the variations
- Try to relate it to the actual board game.
Solution
- A Game of Snakes and Ladders will consist of a board, players, and dice (1 or more)
- A board will have
- Positions numbered from 1 to 100.
- Snakes: encountering snakes will move a player from position A to position B, where the number at position A > the number at position B
- Ladders: encountering ladders will help move a player from position C to position D, where the number at position C < the number at position D
- Players
- players will take turns to roll a dice or more than one dice.
- based on the number or the sum of all the numbers on dice the player will move from one position to another position on the board.
- Dice or Dices
- A game could have only one dice, or it could have more than one.
- A dice could be a
- Regular dice where getting a number on the face of a dice is equally probable
- Weighted dice are where getting a number on the face of a dice is not equally probable.
At this point, there are a few open questions
- How many players can play a game at the same time?
- Let’s assume 4 players, but it should be designed so that more players can be added easily.
- How the winner or winners are determined?
- Let’s assume there will be only one winner.
- Anyone who reaches the position number 100 first would be the winner.
- How many Snakes would be there on the board?
- Let’s assume there would be a total of 5 snakes, where one snake should be at position 99 and should end at position 36 (Just to add some thrill to the game)
- How many Ladders would be there on the board?
- Let’s assume there would be three ladders, but the difference between the start and the end position of a ladder should not be greater than 9.
- How many dice would be there?
- For now let’s assume there would be only one dice.
- Whether the dice would be a regular or weighted?
- For now let’s assume the dice would be a regular dice.
- But you should design in such a way that it can be switchable at the start of the game.
Version 1
Game: Version 1
1 2 3 4 5 6 7 8 9 10 11 |
class Game { private Board board; //player would take turns to roll a dice //current player would be the player at the front of the queue //once the player turn is done it would be moved to the end of the queue private Queue<Player> playerQueue; private Dice dice; //constructor(s) //getter and setter //other methods } |
Color
1 2 3 4 5 6 7 |
public enum Color { RED, GREEN, BLUE, YELLOW, ; } |
Snake: Version 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Snake { private int start; private int end; private Color color; //constructor public Snake(int start, int end, Color color){ validate(start, end); this.start = start; this.end = end; this.color = color; } private void validate(int start, int end){ if(start < end){ //throw IllegalArgumentException - the start position for a Snake should not be less than the end position } } //getter and setter goes here } |
Ladder: Version 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Ladder { private int start; private int end; private Color color; //Contructor public Ladder(int start, int end, Color color){ validate(start, end); this.start = start; this.end = end; this.color = color; } private void validate(int start, int end){ if(start > end){ //throw IllegalArgumentException - start position for a ladder cannot be greater than the end position } if(end - start > 9){ //throw IllegalArgumentException - the difference between the end position and the start position should not exceed 9 } } } |
We can see that we are calling validate method in both Snake and Ladder class. It might be a good idea to define a Validator interface with a method named validate for performing validations.
Validator Interface
1 2 3 |
public interface Validator<T> { public void validate(T t); } |
SnakePositionValidator
1 2 3 4 5 6 7 |
public class SnakePositionValidator implements Validator<Snake> { public void validate(Snake snake){ if(start < end){ //throw IllegalArgumentException - the start position for a Snake should not be less than the end position } } } |
LadderPositionValidator
1 2 3 4 5 6 7 |
public cass LadderPositionValidator implements Validator<Ladder> { public void validate(Ladder ladder){ if(start > end){ //throw IllegalArgumentException - start position for a ladder cannot be greater than the end position } } } |
LadderPositionRangeValidator
1 2 3 4 5 6 7 |
public cass LadderPositionRangeValidator implements Validator<Ladder> { public void validate(Ladder ladder){ if(end - start > 9){ //throw IllegalArgumentException - the difference between the end position and the start position should not exceed 9 } } } |
Version 2
Snake: Version 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Snake { private int start; private int end; private Color color; private List<Validator> validatorList = List.of(new SnakePositionValidator()); //constructor public Snake(int start, int end, Color color){ this.start = start; this.end = end; this.color = color; for(Validator validator: validatorList){ validator.validate(this); } } //getter and setter goes here } |
Ladder: Version 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Ladder { private int start; private int end; private Color color; private List<Validator> = List.of(new LadderPositionValidator(), new LadderPositionRangeValidator()); //Contructor public Ladder(int start, int end, Color color){ this.start = start; this.end = end; this.color = color; for(Validator validator: validatorList){ validator.validate(this); } } //Getters and Setters } |
Player
1 2 3 4 5 6 |
public class Player { private String name; private Color color; //constructor //getters and setters } |
SnakeAndLadderPlayer
1 2 3 4 5 6 7 8 |
public class SnakeAndLadderPlayer extends Player { private int position; //constructor public SnakesAndLaddersPlayer(String name, Color color){ super(name, color); this.postion = -1; } } |
Dice
1 2 3 |
public interface Dice { int roll(); } |
RegularDice
1 2 3 4 5 6 7 8 9 10 11 |
public class RegularDice implements Dice { private final int faces; private final Random random; public RegularDice(int faces){ this.faces = faces; this.random = random; } public int roll(){ return (1 + this.random.nextInt(this.faces)); } } |
We can also add a RegularDiceValidator to check the number of faces on dice.
For example, the number of faces on the dice should be a non-negative and non-zero integer, whose value should be between 1 and 20 (both inclusive)
WeightedDice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class WeightedDice implements Dice { private final List<Integer> weightedDistribution; private final Random random; //weights[i] denotes the weight of i+1 face public WeightedDice (int[] weights){ this.weightedDistribution = new ArrayList<>(); for(int i=0;i<weights.length;i++){ for(int w=0;w<weights[i];w++){ this.weightedDistribution.add(i+1); } } this.random = new Random(); } public int roll(){ int idx = this.random.nextInt(weightedDistribution.size()); return this.weightedDistribution.get(idx); } } |
Board
- We don’t need to maintain a list of all the positions for the board.
- We need to know the range of positions allowed.
BoardSnakeAndLadderValidator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class BoardSnakeAndLadderValidator implements Validator<Board>{ public void validate(Board board){ if(board.getSnakeSet()== null || board.getSnakeSet().isEmpty()){ //throw IllegalArgumentException - the snake list cannot be empty or null } if(board.getSnakeSet().size()!=5){ //throw IllegalArgumentException - there should be exactly 5 snakes on the board } if(board.getLadderSet()== null || board.getLadderSet().isEmpty()){ //throw IllegalArgumentException - the ladder list cannot be empty or null } if(board.getLadderSet().size()!=3){ //throw IllegalArgumentException - there should be exactly 3 ladders on the board } } } |
BoardThrillnessValidator
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class BoardThrillnessValidator implements Validator<Board> { public void validate(Board board){ boolean doesHaveSufficientThrill = false; for(Snake snake: board.getSnakeSet()){ if(snake.getStart()==99 && snake.getEnd()==36){ doesHaveSufficientThrill = true; } } if(!doesHaveSufficientThrill){ //throw IllegalStateException - the board does not have sufficient thrillness. please set one of the snake start position to 99 and end position to 36 } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class Board { private static final int minPosition = 1; private static final int maxPosition = 100; private final TreeSet<Snake> snakeSet; private final TreeSet<Ladder> ladderSet; private List<Validator> validatorList = List.of(new BoardAndSnakeValidator(), new BoardThrillnessValidator()); //constructor public Board(List<Snake> snakeList, List<Ladder> ladderList){ this.snakeSet = new TreeSet((a, b)-> a.getStart() - b.getStart()); this.snakeSet.addAll(snakeList); this.ladderSet = new TreeSet((a, b)-> a.getStart() - b.getStart()); this.ladderSet.addAll(ladderList); for(Validator validator: this.validatorList){ validator.validate(this); } } //get next position given the currPosition and the roll on the dice public int getNextPosition(int currPosition, int roll){ if(currPosition+roll > maxPosition){ return currPosition; } int finalPosition = currPosition + roll; finalPosition = checkSnakeAndGetFinalPosition(finalPosition); finalPosition = checkLadderAndGetFinalPosition(finalPosition); return finalPosition; } //check if there is a snake at the current position //if there is a snake return the final position //else return the currPosition private int checkSnakeAndGetFinalPosition(int currPosition){ Snake snake = this.snakeSet.floor(new Snake(currPosition, 0)); int finalPosition = currPosition; if(snake!=null && snake.getStart()==currPosition){ finalPosition = snake.getEnd(); } return finalPosition; } //check if there is a ladder at the current position //if there is a ladder return the final position //else return the currPosition private int checkLadderAndGetFinalPosition(int currPosition){ Ladder ladder = this.ladderSet.floor(new Ladder(currPosition, 100)); int finalPosition = currPosition; if(ladder!=null && ladder.getStart()==currPosition){ finalPosition = ladder.getEnd(); } return finalPosition; } //check if the position is max position public boolean isMaxPosition(int currPosition){ return currPosition == maxPosistion; } } |
Game: Version 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class Game { private Player winner; private LocalDateTime startDateTime; private LocalDateTime endDateTime; private boolean isAborted; private Board board; //player would take turns to roll a dice //current player would be the player at the front of the queue //once the player turn is done it would be moved to the end of the queue private Queue<Player> playerQueue; private Dice dice; //constructor(s) public Game(Board board, Queue<Player> playerQueue, Dice dice){ this.board = board; this.playerQueue = playerQueue; this.dice = dice; this.startDateTime = LocalDateTime.now(); } //getter and setter //other methods public Player move(){ if(this.isAborted){ //throw IllegalStateExcption - this game has been aborted on endDateTime. Please start a new game. } Player currPlayer = this.playerQueue.poll(); int roll = this.dice.roll(); int nextPosition = board.getNextPosition(currPlayer.getPosition(), roll); if(this.isWinner(nextPosition)){ return currPlayer; } currPlayer.setPosition(nextPosition); this.playerQueue.add(currPlayer); return null; } private boolean isWinner(int position){ return this.board.isMaxPosition(position); } } |