Fork me on GitHub
#malli
<
2022-07-05
>
Stig Brautaset00:07:42

How to model a deck of cards? We use spec at work, but I wanted to try Malli for a personal toy project. I promptly ran into a problem I couldn't solve, and I've tried to make a simplified version of it here. Imagine a simplified game of poker: a hand is 5 cards, and we only care about whether it is a flush (i.e. 5 cards of the same suit) or not.

Stig Brautaset00:07:07

First attempt As we don't really care about the numbers or faces we can model a card as simple enum of its suit, and a hand as a 5-tuple of cards. The deck is trickier. It has up to 52 cards, but I can't find a way to model the additional constraint that we should only have a maximum of 13 of each suit. Thus the last test in this self-contained example (usually) fails:

``````(ns cards.simple-test
(:require
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.generator :as mg]))

(def Card [:enum :spades :hearts :clubs :diamonds])
(def Hand [:tuple Card Card Card Card Card])
(def Deck [:sequential {:max 52} Card])

(deftest deck
(testing "we can generate a Deck"
(doseq [deck (mg/sample Deck)]
(testing "that validates"
(is (true? (m/validate Deck deck))))

(testing "where every element is a card"
(is (every? #(m/validate Card %) deck)))

(testing "of maximum 52 cards"
(is (>= 52 (count deck))))

(testing "where each suite occurs at most 13 times"
;; this test (usually) fails, because we haven't restricted
;; the count of each suit to maximum 13.
(is (>= 13 (->> deck
frequencies
vals
(reduce max 0))))))))``````

Stig Brautaset00:07:10

Second attempt Let's give each card a suit and number to address the problem with our previous deck. A hand is now slighly more complicated: we have to use a set with `{:min 5 :max 5}` properties instead of a 5-tuple to ensure cards are not duplicated. (A hand with 5 ace of spades should not be valid!) The deck, on the other hand, is simpler than before. By using a set we can drop the `{:max 52}` property. It is now implicit, because there are only 52 possible combinations of 4 suits and 13 numbers. Self-contained example:

``````(ns cards.better-test
(:require
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.generator :as mg]))

(def Card [:tuple
[:int {:min 1 :max 13}]])
(def Hand [:set {:min 5 :max 5} Card])
(def Deck [:set Card])

(deftest deck
(testing "we can generate a Deck"
(doseq [deck (mg/sample Deck)]
(testing "that validates"
(is (true? (m/validate Deck deck))))

(testing "where every element is a card"
(is (every? #(m/validate Card %) deck)))

(testing "of maximum 52 cards"
(is (>= 52 (count deck))))

(testing "where each suite occurs at most 13 times"
(is (>= 13 (->> deck
(map first)
frequencies
vals
(reduce max 0))))))))``````

Stig Brautaset00:07:08

This solves the problem with my first attempt, but introduces another: we can neither sort nor properly shuffle our deck now. ๐

Stig Brautaset00:07:50

I think what I want is an ordered sequence of distinct values, but that may be because I can't see outside my Specs-shaped box. Is there an alternative way to model this with Malli?

``(def Deck [:sequential {:distinct true} Card])``

roklenarcic16:07:02

Define card as tuple of suit and number up to 13. A deck is a distinct sequence of cards. Unshuffled deck is a sorted deck

Stig Brautaset16:07:17

That sounds like my second attempt:

``````(def Card [:tuple
[:int {:min 1 :max 13}]])
(def Hand [:set {:min 5 :max 5} Card])
(def Deck [:set Card])``````

Stig Brautaset16:07:53

How can I specify a distinct sequence? It seems you have to go to a set, as thereโs no (documented) `{:distinct true}` property I can add.

Stig Brautaset18:07:27

Ah, I think I see. I just discovered `:fn` ๐

Stig Brautaset19:07:05

This appears to do what I want:

``````(def Card [:tuple
[:int {:min 1 :max 13}]])

(def Hand [:and
[:sequential {:min 5 :max 5} Card]
[:fn (fn [hand] (apply distinct? hand))]])

(def Deck [:and
[:sequential Card]
[:fn (fn [deck] (apply distinct? deck))]])``````

Stig Brautaset19:07:34

That can be made to work with my simplified version too:

``````(def Card [:enum :spades :hearts :clubs :diamonds])
(def Hand [:sequential {:min 5 :max 5} Card])
(def Deck [:and
[:sequential Card]
[:fn #(->> % frequencies vals (reduce max 0) (>= 13))]])``````
Thank you for the help ๐

Stig Brautaset19:07:00

strikes again ๐

๐ฏ 1
๐ 1
valtteri17:07:22

Works for me :thinking_face: