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
	   [:enum :spades :hearts :clubs :diamonds]
	   [: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
	   [:enum :spades :hearts :clubs :diamonds]
	   [: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
	   [:enum :spades :hearts :clubs :diamonds]
	   [: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

duckie strikes again ๐Ÿ™‚

๐Ÿ’ฏ 1
๐Ÿ˜„ 1
valtteri17:07:22

Works for me :thinking_face: