Skip to content

Instantly share code, notes, and snippets.

@TonioGela
Created August 10, 2024 13:53
Show Gist options
  • Save TonioGela/3677b19a2bd2034938d5df0b0d2dd34f to your computer and use it in GitHub Desktop.
Save TonioGela/3677b19a2bd2034938d5df0b0d2dd34f to your computer and use it in GitHub Desktop.
Goose Game - I should refactor tests!

The Goose Game Kata

Goose game is a game where two or more players move pieces around a track by rolling a die. The aim of the game is to reach square number sixty-three before any of the other players and avoid obstacles. (wikipedia)

This kata has been invented by Matteo Vaccari, you can find the original slides here.

General requirements

  • You may use whatever programming language you prefer. Use something that you know well.
  • You should commit your code on GitHub or any other SCM repository you prefer (e.g. bitbucket, gitlab, etc) and send us the link.
  • You should release your work under an OSI-approved open-source license of your choice.
  • You should deliver the sources of your application, with a README that explains how to compile and run it.

IMPORTANT: Implement the requirements focusing on writing the best code you can produce.

Features

1. Add players

As a player, I want to add me to the game so that I can play.

Scenarios:

  1. Add Player

    If there is no participant
    the user writes: "add player Pippo"
    the system responds: "players: Pippo"
    the user writes: "add player Pluto"
    the system responds: "players: Pippo, Pluto"
  2. Duplicated Player

    If there is already a participant "Pippo"
    the user writes: "add player Pippo"
    the system responds: "Pippo: already existing player"

2. Move a player

As a player, I want to move the marker on the board to make the game progress

Scenarios:

  1. Start
    If there are two participants "Pippo" and "Pluto" on space "Start"
    the user writes: "move Pippo 4, 2"
    the system responds: "Pippo rolls 4, 2. Pippo moves from Start to 6"
    the user writes: "move Pluto 2, 2"
    the system responds: "Pluto rolls 2, 2. Pluto moves from Start to 4"
    the user writes: "move Pippo 2, 3"
    the system responds: "Pippo rolls 2, 3. Pippo moves from 6 to 11"

3. Win

As a player, I win the game if I land on space "63"

Scenarios:

  1. Victory

    If there is one participant "Pippo" on space "60"
    the user writes: "move Pippo 1, 2"
    the system responds: "Pippo rolls 1, 2. Pippo moves from 60 to 63. Pippo Wins!!"
  2. Winning with the exact dice shooting

    If there is one participant "Pippo" on space "60"
    the user writes: "move Pippo 3, 2"
    the system responds: "Pippo rolls 3, 2. Pippo moves from 60 to 63. Pippo bounces! Pippo returns to 61"

4. The game throws the dice

As a player, I want the game throws the dice for me to save effort

Scenarios:

  1. Dice roll
    If there is one participant "Pippo" on space "4"
    assuming that the dice get 1 and 2
    when the user writes: "move Pippo"
    the system responds: "Pippo rolls 1, 2. Pippo moves from 4 to 7"

5. Space "6" is "The Bridge"

As a player, when I get to the space "The Bridge", I jump to the space "12"

Scenarios:

  1. Get to "The Bridge"
    If there is one participant "Pippo" on space "4"
    assuming that the dice get 1 and 1
    when the user writes: "move Pippo"
    the system responds: "Pippo rolls 1, 1. Pippo moves from 4 to The Bridge. Pippo jumps to 12"

6. If you land on "The Goose", move again

As a player, when I get to a space with a picture of "The Goose", I move forward again by the sum of the two dice rolled before

The spaces 5, 9, 14, 18, 23, 27 have a picture of "The Goose"

Scenarios:

  1. Single Jump

    If there is one participant "Pippo" on space "3"
    assuming that the dice get 1 and 1
    when the user writes: "move Pippo"
    the system responds: "Pippo rolls 1, 1. Pippo moves from 3 to 5, The Goose. Pippo moves again and goes to 7"
  2. Multiple Jump

    If there is one participant "Pippo" on space "10"
    assuming that the dice get 2 and 2
    when the user writes: "move Pippo"
    the system responds: "Pippo rolls 2, 2. Pippo moves from 10 to 14, The Goose. Pippo moves again and goes to 18, The Goose. Pippo moves again and goes to 22"

7. Prank (Optional Step)

As a player, when I land on a space occupied by another player, I send him to my previous position so that the game can be more entertaining.

Scenarios:

  1. Prank
    If there are two participants "Pippo" and "Pluto" respectively on spaces "15" and "17"
    assuming that the dice get 1 and 1
    when the user writes: "move Pippo"
    the system responds: "Pippo rolls 1, 1. Pippo moves from 15 to 17. On 17 there is Pluto, who returns to 15"
//> using scala 3.4.2
import scala.util.Random
import scala.io.StdIn
import scala.annotation.tailrec
@main def main = gameLoop(State.empty)(StdIn.readLine())
@tailrec
def gameLoop(state: State)(s: String): Unit =
val (newState, output) = Command.parse(s).fold((state, _), _.run(state))
println(output)
if !newState.players.values.toList.contains(63) then gameLoop(newState)(s)
case class State(players: Map[String, Int]):
inline def apply(name: String): Int = players(name)
inline def contains: String => Boolean = players.contains
inline def add: String => State = s => State(players + (s -> 0))
inline def move: String => Int => State = s => i => State(players + (s -> i))
inline def names: String = players.keySet.mkString(", ").prependedAll("players: ")
object State:
def empty: State = State(Map.empty)
def apply(ps: (String, Int)*): State = State(Map(ps*))
enum Command:
case AddPlayer(name:String)
case MovePlayer(name:String, dices: (Int, Int))
inline def run: State => (State, String) = s => this match
case Command.AddPlayer(name) => if s.contains(name) then (s, s"$name: already existing player")
else (s.add(name), s.add(name).names)
case Command.MovePlayer(name, _) if !s.contains(name) => (s, s"$name: unexisting player")
case Command.MovePlayer(name, (d1, d2)) =>
val init = s(name)
val initS = if init == 0 then "Start" else init.toString
val actualCell = init + d1 + d2
val increment = d1 + d2
val diceRoll: String = s"$name rolls $d1, $d2."
val (log, pos) = actualCell match
case 6 =>
(s"$name moves from $initS to The Bridge. $name jumps to 12", 12)
case p if p > 63 =>
val posAfterBounce = 2 * 63 - p
(s"$name moves from $initS to 63. $name bounces! $name returns to $posAfterBounce", posAfterBounce)
case 63 =>
(s"$name moves from $initS to 63. $name Wins!!", 63)
case _ =>
val (second, rest) = calculateGooseStreak(actualCell, increment)
val gooseLog = rest.foldLeft(s"$name moves from $initS to $second")((s, n) =>
s"$s, The Goose. Pippo moves again and goes to $n"
)
val posAfterGoose = rest.lastOption.getOrElse(second)
(gooseLog, posAfterGoose)
val playerAtPosition = (s.players - name).map(_.swap).toMap.get(pos)
playerAtPosition.fold(( s.move(name)(pos), s"$diceRoll $log")){ prankedPlayer =>
val newState = s.move(prankedPlayer)(init).move(name)(pos)
(newState, s"$diceRoll $log. On $pos there is $prankedPlayer, who returns to $initS")
}
private inline def calculateGooseStreak(second: Int, inc: Int): (Int, List[Int]) =
def go(elem: Int, acc: List[Int] = Nil): List[Int] = elem match
case 5 | 9 | 14 | 18 | 23 | 27 =>
val next = elem + inc
go(next, acc :+ next)
case _ => acc
(second, go(second))
object Command:
extension (s: String)
inline def toDice: Option[Int] = s.toIntOption.filter(x => x > 0 || x < 7)
inline def parse(s: => String): Either[String, Command] = s.split(' ') match
case Array("add", "player", name) => Right(AddPlayer(name))
case Array("move", name, rest*) => rest.toArray.flatMap(_.split(',')) match
case Array() => Right(MovePlayer(name, (Random.between(1,7), Random.between(1,7))))
case Array(dice1, dice2) => dice1.toDice.zip(dice2.toDice).toRight(s"Invalid dice values").map(MovePlayer(name, _))
case _ => Left(s"Invalid command $s")
//> using test.dependency "org.scalameta::munit::1.0.0"
import munit.*
class GameTest extends FunSuite:
extension (s: String)
def parsesTo(command: Command | PartialFunction[Command, Unit])(using Location): Unit =
command match
case c: Command => assertEquals(Right(c), Command.parse(s))
case pf: PartialFunction[Command, Unit] =>
assert(Command.parse(s).toOption.flatMap(pf.lift).isDefined)
extension (s: State)
def run(cs: Command*): (State, List[String]) =
cs.foldLeft((s, List.empty[String])){ case ((state, ss), command) =>
val (newState, output) = command.run(state)
(newState, ss :+ output)
}
test("Commands parses correctly add commands") {
"add player Pippo".parsesTo(Command.AddPlayer("Pippo"))
}
test("Commands parses correctly move commands") {
"move Pippo".parsesTo { case Command.MovePlayer("Pippo", _) => }
"move Pippo 1,1".parsesTo(Command.MovePlayer("Pippo", (1,1) ))
"move Pippo 1, 1".parsesTo(Command.MovePlayer("Pippo", (1,1) ))
"move Pippo 1 1".parsesTo(Command.MovePlayer("Pippo", (1,1) ))
}
test("Adding a non existing player should make it appear in State at position 0") {
assertEquals(
(State("Pluto" -> 0), "players: Pluto"),
Command.AddPlayer("Pluto").run(State.empty)
)
assertEquals(
(State("Paperino" -> 2, "Pluto" -> 0), "players: Paperino, Pluto"),
Command.AddPlayer("Pluto").run(State("Paperino" -> 2))
)
}
test("Adding many players in a row should be possible") {
val (state, log) = State.empty.run(
Command.AddPlayer("Pluto"),
Command.AddPlayer("Paperino"),
Command.AddPlayer("Topolino")
)
assertEquals(State("Pluto" -> 0, "Paperino" -> 0, "Topolino" -> 0), state)
assertEquals(
List("players: Pluto", "players: Pluto, Paperino", "players: Pluto, Paperino, Topolino"),
log
)
}
test("Adding an existing player should not be possible") {
assertEquals(
(State("Pluto" -> 20), "Pluto: already existing player"),
Command.AddPlayer("Pluto").run(State("Pluto" -> 20))
)
}
test("Moving a non existing player shouldn't work") {
val (state, log) = Command.MovePlayer("Paperino", (5, 2)).run(State("Pluto" -> 7))
assertEquals(State("Pluto" -> 7), state)
assertEquals("Paperino: unexisting player", log)
}
test("Moving an existing player should work") {
val (state, log) = Command.MovePlayer("Pluto", (2, 2)).run(State("Pluto" -> 20))
assertEquals(State("Pluto" -> 24), state)
assertEquals("Pluto rolls 2, 2. Pluto moves from 20 to 24", log)
}
test("Moving an existing player from start should work") {
val (state, log) = Command.MovePlayer("Pluto", (5, 2)).run(State("Pluto" -> 0))
assertEquals(State("Pluto" -> 7), state)
assertEquals("Pluto rolls 5, 2. Pluto moves from Start to 7", log)
}
test("Moving an existing player to six should warp it") {
val (state1, log1) = Command.MovePlayer("Pluto", (4, 2)).run(State("Pluto" -> 0))
val (state2, log2) = Command.MovePlayer("Pluto", (3, 1)).run(State("Pluto" -> 2))
assertEquals(State("Pluto" -> 12), state1)
assertEquals("Pluto rolls 4, 2. Pluto moves from Start to The Bridge. Pluto jumps to 12", log1)
assertEquals(State("Pluto" -> 12), state2)
assertEquals("Pluto rolls 3, 1. Pluto moves from 2 to The Bridge. Pluto jumps to 12", log2)
}
test("Moving an existing player to past 63 should make it bounce back") {
val (state1, log1) = Command.MovePlayer("Pluto", (4, 2)).run(State("Pluto" -> 60))
val (state2, log2) = Command.MovePlayer("Pluto", (2, 2)).run(State("Pluto" -> 60))
assertEquals(State("Pluto" -> 60), state1)
assertEquals("Pluto rolls 4, 2. Pluto moves from 60 to 63. Pluto bounces! Pluto returns to 60", log1)
assertEquals(State("Pluto" -> 62), state2)
assertEquals("Pluto rolls 2, 2. Pluto moves from 60 to 63. Pluto bounces! Pluto returns to 62", log2)
}
test("Moving an existing player to 63 should make it win") {
val (state, log) = Command.MovePlayer("Pluto", (2, 1)).run(State("Pluto" -> 60))
assertEquals(State("Pluto" -> 63), state)
assertEquals("Pluto rolls 2, 1. Pluto moves from 60 to 63. Pluto Wins!!", log)
}
test("Moving an existing player to a Goose cell should make him jump") {
val (state, log) = Command.MovePlayer("Pippo", (1, 1)).run(State("Pippo" -> 3))
assertEquals(State("Pippo" -> 7), state)
assertEquals("Pippo rolls 1, 1. Pippo moves from 3 to 5, The Goose. Pippo moves again and goes to 7", log)
}
test("Goose bumps can be concatenated") {
val (state, log) = Command.MovePlayer("Pippo", (2, 2)).run(State("Pippo" -> 10))
assertEquals(State("Pippo" -> 22), state)
assertEquals("Pippo rolls 2, 2. Pippo moves from 10 to 14, The Goose. Pippo moves again and goes to 18, The Goose. Pippo moves again and goes to 22", log)
}
test("Players can prank each other") {
val (state, log) = Command.MovePlayer("Pippo", (1, 1)).run(State("Pippo" -> 15, "Pluto" -> 17))
assertEquals(State("Pippo" -> 17, "Pluto" -> 15), state)
assertEquals("Pippo rolls 1, 1. Pippo moves from 15 to 17. On 17 there is Pluto, who returns to 15", log)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment