beginners 2025-09-19

I could use a mini code review. In the following code, I don't like that have game, game2 , and game3 ... but it seems like I need all these intermediate variables. Thoughts on how I could do away with those, or does it seem fine as-is?

(deftest test
  (testing "replenish-deck replenishes deck when deck is empty, 
shuffling the whole discard pile EXCEPT for the top card"
    (let [game (make-init-game)
          game2 (-> game
                    (assoc :deck [])
                    (update :discard-pile
                            #(vec (concat %1 %2))
                            (:deck game)))
          game3 (replenish-deck game2)]
      (is (= (dec (count (:discard-pile game2)))
             (count (:deck game3))))
      (is (= 1
             (count (:discard-pile game3)))))))

There's a lot of abstraction breaking here. The test code seems to need to know much about the internal structure of a game. I don't really understand what the operations are meant to accomplish, but if you extracted them to functions with names (and assuming making a game is pure), you could have tests that read somewhat like:

(is (= (cards-in-deck (-> (init-game)))
       (cards-in-discard (-> (init-game)
                             (flush-to-discard)))))
(is (= 1
       (cards-in-discard (-> (init-game)
                             (flush-to-discard)
                             (replenish-deck)))))
and you don't need variables at all. There is a similarity with the previous discussion about representing moves as data, too, which could be one step further than the explicit function calls here.

To me it actually looks a bit harder to understand. Extracting functions into names is good, but repeating function usage just to avoid named bindings is not something I'd do. I would definitely have something like base-game, flushed-game, at least.

That seems fine to me. The only thing I'd change would be to use (update :discard-pile into (:deck game)), assuming :discard-pile is a vector. Could use (fnil into []) if it can be nil.

👍 1

one option would be to give them names that have more significance like 'empty-deck' and 'replenished' or something - might also make the is checks sound clearer/more pertinent

👍 2
👍🏻 1

i've used game, game', game'' before

👍 1
👍🏼 1

Also it seems like you don't test that the top card is still the same as before?

you could avoid the first variable by either externalizing the game2 logic or by swapping the update/assoc using as->, but I don't think it will be more readable than you actual code.

Things you could consider: * Using associative destructuring: (let [{deck :deck discard-pile :discard-pile :as game} (make-init-game)...). * Changeing the name game3 to game-under-test. I would try to distinguish what is the setup for your test (game and game2) and the test itself (game3). * Using assoc with multiple key/value pairs, so that you don't need update. * Eliminating the -> macro. I agree, it is not nice, that you still need game. But I also see no alternative. You want to operate on two things in game, which means you need the original. Maybe something like this looks also good:

(defn prepare-test [{deck :deck discard-pile :discard-pile :as game}]
  (assoc :deck []
         :discard-pile (into deck discard-pile)
         game))

(let [{deck-pre :deck discard-pile-pre :discard-pile :as game-pre} (prepare-test (make-init-game))
      {deck-ut :deck discard-pile-ut :discard-pile} (replenish-deck game-pre)]...)

👍 1
👎 1

(deftest test
  (testing "replenish-deck replenishes deck when deck is empty,
            shuffling the whole discard pile EXCEPT for the top card"
    (let [game (make-init-game)
          game2 (assoc game
                       :deck []
                       :discard-pile (into (game :discard-pile) (game :deck)))
          game-ut (replenish-deck game2)]
      (is (= (count (game-ut :deck))
             (dec (count (game2 :discard-pile)))))
      (is (= (count (game-ut :discard-pile))
             1)))))

👍 1