Fork me on GitHub
#clojure-spec
<
2019-05-10
>
eskemojoe00717:05:34

I'm trying to make a custom generator for the deck of cards example in the spec guide. I have a spec that looks like (s/def ::player (s/keys :req [::name ::score ::hand ::scored])). Each key already has the proper tested generator, but for ::player It shouldn't allow duplicate cards in the hand and scored category for example. I need to make a custom generator for ::player that checks that forces them to be unique.

eskemojoe00717:05:16

I guess I didn't ask a question. Question1: Is it possible to make a custom generator for just part of the ::player?

seancorfield17:05:09

I'm not sure I'm reading your problem statement correctly: are you just trying to ensure ::hand generates unique cards, and ::scored generates unique cards, or are you also trying to ensure there's no duplication between ::hand and ::scored?

seancorfield17:05:40

If it's the former, you can just write custom generators for those specs. If it's the latter, you pretty much have to write a generator for the whole ::player spec.

eskemojoe00717:05:52

Its the later. Given a typical deck of 52 cards. Scored is cards that have scored, then are out of play (Making go-fish).

seancorfield17:05:35

Then, yeah, you have no choice really -- you need a generator for ::player

eskemojoe00717:05:53

So I have to make a custom generator for the whole ::player and I can't rely on the generator even for the other keys such as ::score?

seancorfield17:05:29

Your ::player generator will run the generators for the individual elements if it wants default behavior.

eskemojoe00717:05:16

What does that syntax look like?

seancorfield17:05:53

(s/gen ::my-spec) gets you a generator for a spec

eskemojoe00717:05:09

Ahh...perfect!

eskemojoe00717:05:25

That makes sense, just didn't realize thats what it was doing.

seancorfield17:05:08

And (gen/hash-map ...) to generate the ::player hash map from those generators.

seancorfield17:05:23

(assuming gen is clojure.spec.gen.alpha)

eskemojoe00717:05:54

So a very redundant generator would look something like this:

(s/def ::player (s/with-gen
                  (s/keys :req [::name ::score ::hand ::scored])
                  #(gen/hash-map ::name (s/gen ::name)
                                 ::score (s/gen ::score)
                                 ::hand (s/gen ::hand)
                                 ::scored (s/gen ::scored))))

seancorfield17:05:18

Yup, that would get you started.

eskemojoe00717:05:42

Perfect. I'll post my solution just for reference, but I appreciate the help.

seancorfield18:05:37

I'd probably have a spec for a deck of cards with a custom generator to ensure they're all distinct (it could just shuffle an ordered generation of all the cards), then ::scored would be the first random N of those and ::hand would be the next random M of them.

eskemojoe00718:05:15

The ::scored spec generator is weird, it takes exactly 4 of the same rank card to score. So I was going to run that first, then ommit any results from the ::hand based on that.

eskemojoe00718:05:12

But taht seems like a terrible idea.

eskemojoe00718:05:19

I'm gonna make a full deck spec.

eskemojoe00717:05:44

Finally got back to this. I came up with something that looks like

(gen/sample (gen/bind (s/gen ::scored)
                      #(gen/hash-map ::name (s/gen ::name)
                                     ::score (gen/return (/ (count %) 4))
                                     ::hand (gen-hand %)
                                     ::scored (gen/return %))))

(defn remove-by-rank
  [cards ranks]
  (vec (reduce (fn [new-cards rank]
                (remove #(rank-match? % rank) new-cards))
           cards
           ranks)))

(defn gen-hand
  [scored-cards]
  (gen/bind (s/gen ::hand)
            #(gen/return (remove-by-rank % (should-score? scored-cards)))))

eskemojoe00717:05:29

So it generates the scored cards first, uses gen/bind to get those, and bases the the other generation based on that (I didn't include all the functions).

1