Kata: C** de chouette

It's been now few years that I have not been on a greenfield projects.

These projects require a huge creativity and agility to make sense of what is in product owner's mind.

So I wonder, how could I "create" a code kata from something which dos not make sense.

In the early 2000s, a medieval fantasy comedy named Kaamelott was broadcast in France.

In one of the episodes, Perceval introduced a dices game called Cul de chouette (a preview), it could be translated in "Owl bottom".

It works with 3 dices (2 owls, 1 bottom), there are rules to make and loose points, but the game is won by the first reaching 343 (from the first letters position CDC).

To score you have to make one of the following combination:

  • La Chouette: two identical dices we get the square of it (e.g. dices of 1 = 1 points, dices 2 = 4 points, etc.)
  • La Velute: the sum of the owls is equal to the bottom, we get the double of the square of the bottom (e.g. dice of 1 & 2 and a bottom of 3, we get 18 points)
  • La Chouette Velute: owls are equal and their sum is equal to the bottom, same as above, the double of the square of the bottom
  • Le Cul de Chouette: three identical dices, we get 40 + 10 * the dice value (e.g. dice of 1 = 50 points, dice of 2 = 60 points, etc.)
  • La Suite: three following dices (e.g. 3-4-5), we loose 10 points, except 1-2-3 which is a Velute (18 points)
  • Le Néant (the void): no combination, we get a Grelottine if we did not already have one, used for challenges

Note: There are also some things I did not mentioned here which involves a multi-players awareness and clapping their hand and yelling somethings

Note: I purposefully did not translate names as some of them are not even existing words, and does not even make sense in the context (which not so far from coming in some new business domains)

Let's focus on one throw score computation.

We can start with the following boilerplate:

spec :: Spec
spec =
  describe "Cul de chouette" $ do
    describe "Chouette" $ do
      forM_
        [ ((D1, D1, D5), 1)
        ]
        $ \((o0, o1, b), s) ->
          it (show (o0, o1, b)) $
            score o0 o1 b `shouldBe` s

data Dice = D1 | D2 | D3 | D4 | D5 | D6
  deriving stock (Eq, Ord, Show, Enum, Bounded)

newtype Points
  = Points {unPoints :: Int}
  deriving newtype (Eq, Ord, Show, Num)

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | otherwise = 0

Let's fix that as follows:

-- describe "Chouette" $ do
--   forM_
--     [ ((D1, D1, D5), 1),
--       ((D2, D2, D5), 4),
--       ((D3, D3, D5), 9),
--       ((D4, D4, D5), 16),
--       ((D5, D5, D6), 25),
--       ((D6, D6, D5), 36)
--     ]
--     $ \((o0, o1, b), s) ->
--       it (show (o0, o1, b)) $
--         score o0 o1 b `shouldBe` s

data Dice = D1 | D2 | D3 | D4 | D5 | D6
  deriving stock (Eq, Ord, Show, Enum, Bounded)

diceInt :: Dice -> Int
diceInt =
  \case
    D1 -> 1
    D2 -> 2
    D3 -> 3
    D4 -> 4
    D5 -> 5
    D6 -> 6

intDice :: Int -> Maybe Dice
intDice =
  \case
    1 -> Just D1
    2 -> Just D2
    3 -> Just D3
    4 -> Just D4
    5 -> Just D5
    6 -> Just D6
    _ -> Nothing

newtype Points
  = Points {unPoints :: Int}
  deriving newtype (Eq, Ord, Show, Num)

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | o0 == o1 || o0 == b = Points $ diceInt o0 * diceInt o0
  | otherwise = 0

This works until you swap dices, let's fix this too:

-- forM_ (permutations [o0, o1, b]) $ \[x, y, z] ->
--   it (show [x, y, z]) $
--     score x y z `shouldBe` s

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | o0 == o1 || o0 == b = Points $ diceInt o0 * diceInt o0
  | o1 == b = Points $ diceInt o1 * diceInt o1
  | otherwise = 0

Next one, we have have Velute, we have to sum two owls and compare it to the bottom, as follows:

-- describe "Velute" $ do
--   forM_
--     [ ((D1, D2, D3), 18),
--       ((D2, D3, D5), 50)
--     ]
--     $ \((o0, o1, b), s) -> do
--       it (show [o0, o1, b]) $
--         score o0 o1 b `shouldBe` s
--       it (show [o1, o0, b]) $
--         score o1 o0 b `shouldBe` s

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | o0 == o1 || o0 == b = Points $ diceInt o0 * diceInt o0
  | o1 == b = Points $ diceInt o1 * diceInt o1
  | diceInt o0 + diceInt o1 == diceInt b = Points $ 2 * diceInt b * diceInt b
  | otherwise = 0

Then, it's a bit more tricky, Chouette Velute is suppose to be already handled, but, if we try, it fails as it first get Chouette scores, we have to put the Velute score first as follows:

-- describe "Chouette velute" $ do
--   forM_
--     [ ((D1, D2), 8),
--       ((D2, D4), 32),
--       ((D3, D6), 72)
--     ]
--     $ \((o, b), s) ->
--       it (show [o, o, b]) $
--         score o o b `shouldBe` s

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | diceInt o0 + diceInt o1 == diceInt b = Points $ 2 * diceInt b * diceInt b
  | o0 == o1 || o0 == b = Points $ diceInt o0 * diceInt o0
  | o1 == b = Points $ diceInt o1 * diceInt o1
  | otherwise = 0

The next one is easier, as Cul de Chouette is only triggered when the three dices are equals, as follows:

-- describe "Cul de Chouette" $ do
--   forM_
--     [ (D1, 50),
--       (D2, 60),
--       (D3, 70),
--       (D4, 80),
--       (D5, 90),
--       (D6, 100)
--     ]
--     $ \(d, s) ->
--       it (show [d, d, d]) $
--         score d d d `shouldBe` s

score :: Dice -> Dice -> Dice -> Points
score o0 o1 b
  | diceInt o0 + diceInt o1 == diceInt b = Points $ 2 * diceInt b * diceInt b
  | o0 == o1 && o0 == b = Points $ diceInt o0 * 10 + 40
  | o0 == o1 || o0 == b = Points $ diceInt o0 * diceInt o0
  | o1 == b = Points $ diceInt o1 * diceInt o1
  | otherwise = 0

The next one is a bit trickier as we need to compare the three dices.

To simplify, we can try to compare the sorted dices and compare it to and idea incremental list, starting with the small element:

-- describe "Suite" $ do
--   forM_ [D2, D3, D4] $ \d ->
--     forM_ (permutations $ take 3 [d ..]) $ \[x, y, z] ->
--       it (show [x, y, z]) $
--         score x y z `shouldBe` (-10)

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

The next one is interesting, it consists in all other cases, we face an interesting challenge, score can give either Points of a Grelottine.

We have to create a proper Score data-type and adapt our current code as follows:

data Score = ScorePoints Points | ScoreGrelotting
  deriving stock (Eq, Show)

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

Finally, we can add the last following tests:

describe "Néant" $ do
  forM_
    [ (D1, D2, D4),
      (D1, D2, D5),
      (D1, D2, D6),
      (D1, D3, D5),
      (D1, D3, D6),
      (D2, D3, D6),
      (D2, D4, D5),
      (D2, D5, D6),
      (D3, D5, D6)
    ]
    $ \(o0, o1, b) ->
      forM_ (permutations [o0, o1, b]) $ \[x, y, z] ->
        it (show [x, y, z]) $
          score x y z `shouldBe` ScoreGrelotting