Kata: C** de chouette - multi-players

In the previous log, we have started a coding kata based on a dices game called Cul de chouette.

I have mentioned that I have skipped the multi-players to stay simple.

Let's pick some rules as the following ones:

  • La Chouette Velute: owls are equal and their sum is equal to the bottom, the double of the square of the bottom, points are given to the first player clapping their hands and yelling "Pas mou le caillou !"
  • La Suite: three following dices (e.g. 3-4-5), except 1-2-3 which is a Velute (18 points), the last player banging his/her fist on the table yelling "Grelotte ça picote !" loose 10 points

We can start by defining Players and adding it to score, as follows:

newtype Player
  = Player {unPlayer :: Text}
  deriving newtype (Eq, Ord, Show, IsString)

score :: Player -> Dice -> Dice -> Dice -> (Player, Score)
score p o0 o1 b
  | diceInt o0 + diceInt o1 == diceInt b = (p, ScorePoints $ Points $ 2 * diceInt b * diceInt b)
  | o0 == o1 && o0 == b = (p, ScorePoints $ Points $ diceInt o0 * 10 + 40)
  | o0 == o1 || o0 == b = (p, ScorePoints $ Points $ diceInt o0 * diceInt o0)
  | o1 == b = (p, ScorePoints $ Points $ diceInt o1 * diceInt o1)
  | sort [o0, o1, b] == take 3 [(minimum [o0, o1, b]) ..] = (p, ScorePoints $ Points (-10))
  | otherwise = (p, ScoreGrelotting)

The next step is to define Actions as follows:

data Actions = PasMouLeCailloux | GrelotteCaPicotte
  deriving stock (Eq, Show)

And finally to integrate both:

score :: Player -> Dice -> Dice -> Dice -> [(Player, Actions)] -> (Player, Score)
score p o0 o1 b actions
  | diceInt o0 + diceInt o1 == diceInt b =
      case lookup PasMouLeCailloux $ map swap actions of
        Just p' -> (p', ScorePoints $ Points $ 2 * diceInt b * diceInt b)
  | o0 == o1 && o0 == b = (p, ScorePoints $ Points $ diceInt o0 * 10 + 40)
  | o0 == o1 || o0 == b = (p, ScorePoints $ Points $ diceInt o0 * diceInt o0)
  | o1 == b = (p, ScorePoints $ Points $ diceInt o1 * diceInt o1)
  | sort [o0, o1, b] == take 3 [(minimum [o0, o1, b]) ..] =
      case lookup GrelotteCaPicotte $ reverse $ map swap actions of
        Just p' -> (p', ScorePoints $ Points (-10))
  | otherwise = (p, ScoreGrelotting)

You can notice that the two new case are non-exhaustive, there is a case called "Bévue" (- 10 points) when a player does not respect the rules (e.g. use GrelotteCaPicotte instead PasMouLeCailloux, or when not needed).

But nothing when no action is given, let's produce multiple scores:

score :: Player -> Dice -> Dice -> Dice -> [(Player, Actions)] -> [(Player, Score)]
score p o0 o1 b actions
  | diceInt o0 + diceInt o1 == diceInt b =
      let errors = actionError <$> filter ((/= PasMouLeCailloux) . snd) actions
       in case lookup PasMouLeCailloux $ map swap actions of
            Just p' -> (p', ScorePoints $ Points $ 2 * diceInt b * diceInt b) : errors
            Nothing -> errors
  | o0 == o1 && o0 == b = (p, ScorePoints $ Points $ diceInt o0 * 10 + 40) : allActionsErrors
  | o0 == o1 || o0 == b = (p, ScorePoints $ Points $ diceInt o0 * diceInt o0) : allActionsErrors
  | o1 == b = (p, ScorePoints $ Points $ diceInt o1 * diceInt o1) : allActionsErrors
  | sort [o0, o1, b] == take 3 [(minimum [o0, o1, b]) ..] =
      let errors = actionError <$> filter ((/= GrelotteCaPicotte) . snd) actions
       in case lookup GrelotteCaPicotte $ reverse $ map swap actions of
            Just p' -> (p', ScorePoints $ Points (-10)) : errors
            Nothing -> errors
  | otherwise = (p, ScoreGrelotting) : allActionsErrors
  where
    allActionsErrors = actionError <$> actions
    actionError = fmap (const $ ScorePoints $ Points (-10))

It's a bit hairy, but that's it for the basis, next time we will see additional rules which involve dealing with time.