DEV Community

Cover image for Snake and Ladder in Go
Tarang
Tarang

Posted on

Snake and Ladder in Go

Snake and Ladder in Go

This is a small console Snake and Ladder game in Go. Use it to see how a program can be split into a few types that each do one thing.

The board has 100 squares, plus snakes and ladders. Three coins take turns. A coin wins only when it lands on exactly 100.

roll dice → move → if over 100, stay
         → apply snake/ladder (can chain)
         → if 100, that coin wins
Enter fullscreen mode Exit fullscreen mode

Class diagram of Snake and Ladder in Go: Game uses Board, Coin, and Dice; Board holds Squares, Ladders, and Snakes; Ladder and Snake implement the Jumps interface

Solid arrow = has / uses. Dashed arrow = implements.

Read the diagram this way: Game is the teacher of the rules. Board is the map. main only builds the pieces, then asks Game to Play().


Design ideas to notice

Keep these in mind as you read the types below.

Idea What it means here
Facade Callers should not wire dice, coins, and jumps themselves. They talk to Game (NewGame, Play).
Polymorphism A snake and a ladder are different. After a move, the rule is the same: if you are on a start, go to the end. That is why both implement Jumps.
Constructors Prefer NewBoard, NewDice, NewCoin, NewLadder, NewSnake so creation stays in one place.
One job per type Board stores the layout. Game runs the turns. Dice rolls. Coin is a token.
Open for a new jump applyJumps should not say if snake / if ladder. A new jump type can implement the same interface.
Depend on the interface The turn loop should know start() and end(), not the names Snake and Ladder.

The types

Walk through each type in order. Notice what it owns, and what it refuses to own.

Square

A square is a number on the board.

type Square struct {
    SquareNumber int
}

func NewSquare(squareNumber int) *Square {
    return &Square{squareNumber}
}
Enter fullscreen mode Exit fullscreen mode

Dice

The die only rolls. It does not know whose turn it is.

type Dice struct {
    Sides int
}

func NewDice() *Dice {
    return &Dice{Sides: 6}
}

func (d *Dice) RollDice() int {
    return 1 + rand.IntN(6)
}
Enter fullscreen mode Exit fullscreen mode

Coin

A coin is a player token: a color and a position. It does not know the rules of the game.

type Coin struct {
    CurrentPosition *Square
    Color           string
}

func NewCoin(color string) *Coin {
    return &Coin{&Square{0}, color}
}
Enter fullscreen mode Exit fullscreen mode

Jump interface, Ladder, Snake

Keep snake and ladder as two structs. They mean different things. Give them a shared interface only so the game can move a coin without caring which one it hit.

type Jumps interface {
    start() *Square
    end() *Square
}

type Ladder struct {
    Start *Square
    End   *Square
}

func NewLadder(startSquare Square, endSquare Square) *Ladder {
    return &Ladder{&startSquare, &endSquare}
}

func (l *Ladder) start() *Square { return l.Start }
func (l *Ladder) end() *Square   { return l.End }

type Snake struct {
    Start *Square
    End   *Square
}

func NewSnake(startSquare Square, endSquare Square) *Snake {
    return &Snake{&startSquare, &endSquare}
}

func (s *Snake) start() *Square { return s.Start }
func (s *Snake) end() *Square   { return s.End }
Enter fullscreen mode Exit fullscreen mode

Board

The board knows the squares, the snakes, the ladders, and one combined list of jumps. It does not play.

type Board struct {
    Squares []*Square
    Ladders []*Ladder
    Snakes  []*Snake
    Jumps   []Jumps
}

func NewBoard(ladders []*Ladder, snakes []*Snake) *Board {
    var squares []*Square
    for i := 1; i <= 100; i++ {
        squares = append(squares, &Square{i})
    }
    return &Board{
        Squares: squares,
        Ladders: ladders,
        Snakes:  snakes,
        Jumps:   allJumps(ladders, snakes),
    }
}

func allJumps(ladders []*Ladder, snakes []*Snake) []Jumps {
    var jumps []Jumps
    for _, ladder := range ladders {
        jumps = append(jumps, ladder)
    }
    for _, snake := range snakes {
        jumps = append(jumps, snake)
    }
    return jumps
}
Enter fullscreen mode Exit fullscreen mode

Game

Game is the place for rules: whose turn, when to stop, when a coin has won.

type Game struct {
    Board *Board
    Coins []*Coin
    Dice  *Dice
}

func NewGame(board Board, coins []*Coin) *Game {
    return &Game{&board, coins, NewDice()}
}
Enter fullscreen mode Exit fullscreen mode

Play() is the lesson you should read in the full source: turns, the exact-100 rule, and chained jumps. Watch the pointers. If a coin reuses the ladder’s own Square, the next roll can change the board itself.

That code is on GitHub. Clone the repo, run go run ., and follow one coin until someone reaches 100.


Entire code on GitHub

Full source — Play(), applyJumps(), main, and a sample set of snakes and ladders:

https://github.com/tarang21/snake-and-ladder

Top comments (0)