~subsetpark/whist

7274d34a65d63035b91493662a69120ffc58b0b0 — Zach Smith 11 months ago b087404
Respect json module: use keywords for keys
M bids.janet => bids.janet +26 -26
@@ 1,35 1,35 @@
(def-
  bids
  [[{"count" 3 "direction" "up"} "3 Uptown"]
   [{"count" 3 "direction" "down"} "3 Downtown"]
   [{"count" 3 "suit" "no_trumps"} "3 No-Trumps"]
   [{"count" 4 "direction" "up"} "4 Uptown"]
   [{"count" 4 "direction" "down"} "4 Downtown"]
   [{"count" 4 "suit" "no_trumps"} "4 No-Trumps"]
   [{"count" 5 "direction" "up"} "5 Uptown"]
   [{"count" 5 "direction" "down"} "5 Downtown"]
   [{"count" 5 "suit" "no_trumps"} "5 No-Trumps"]
   [{"count" 6 "direction" "up"} "6 Uptown"]
   [{"count" 6 "direction" "down"} "6 Downtown"]
   [{"count" 6 "suit" "no_trumps"} "6 No-Trumps"]
   [{"count" 7 "direction" "up"} "7 Uptown"]
   [{"count" 7 "direction" "down"} "7 Downtown"]
   [{"count" 7 "suit" "no_trumps"} "7 No-Trumps"]])
  [[{:count 3 :direction "up"} "3 Uptown"]
   [{:count 3 :direction "down"} "3 Downtown"]
   [{:count 3 :suit "no_trumps"} "3 No-Trumps"]
   [{:count 4 :direction "up"} "4 Uptown"]
   [{:count 4 :direction "down"} "4 Downtown"]
   [{:count 4 :suit "no_trumps"} "4 No-Trumps"]
   [{:count 5 :direction "up"} "5 Uptown"]
   [{:count 5 :direction "down"} "5 Downtown"]
   [{:count 5 :suit "no_trumps"} "5 No-Trumps"]
   [{:count 6 :direction "up"} "6 Uptown"]
   [{:count 6 :direction "down"} "6 Downtown"]
   [{:count 6 :suit "no_trumps"} "6 No-Trumps"]
   [{:count 7 :direction "up"} "7 Uptown"]
   [{:count 7 :direction "down"} "7 Downtown"]
   [{:count 7 :suit "no_trumps"} "7 No-Trumps"]])

(def direction [[{"direction" "up"} "Uptown"]
		[{"direction" "down"} "Downtown"]])
(def direction [[{:direction "up"} "Uptown"]
		[{:direction "down"} "Downtown"]])

(def suit [[{"suit" "hearts"} "Hearts"]
	   [{"suit" "spades"} "Spades"]
	   [{"suit" "diamonds"} "Diamonds"]
	   [{"suit" "clubs"} "Clubs"]])
(def suit [[{:suit "hearts"} "Hearts"]
	   [{:suit "spades"} "Spades"]
	   [{:suit "diamonds"} "Diamonds"]
	   [{:suit "clubs"} "Clubs"]])

(defn- no-trumps? [bid] (= (bid "suit") "no_trumps"))
(defn- no-trumps? [bid] (= (bid :suit) "no_trumps"))
(defn- find-row-index [bid source] (find-index |(= ($0 0) bid) source))

(defn to-text [bid &opt source]
  (default source bids)
  (if-let [ind (find-row-index bid source)
  (if-let [ind (find-row-index (freeze bid) source)
	   row (source ind)]
    (row 1)
    (error (string "Not found: " (string/format "%q" bid) " in " (string/format "%q" source)))))


@@ 40,9 40,9 @@
  (case high-bid
    nil bids
    (let [minimum-bid (if (no-trumps? high-bid)
		 	{"count" (inc (high-bid "count")) "direction" "up"} 
			{"count" (high-bid "count") "suit" "no_trumps"})
		 	{:count (inc (high-bid :count)) :direction "up"} 
			{:count (high-bid :count) :suit "no_trumps"})
	  minimum-bid-ind (find-row-index minimum-bid bids)]
      (slice bids minimum-bid-ind))))
      (array/push (array/slice bids minimum-bid-ind) ["pass" "Pass"]))))

(defn second-bid [bid] (if (no-trumps? bid) direction suit))

M cards.janet => cards.janet +23 -23
@@ 7,13 7,13 @@
  (loop [suit :in ["D" "S" "C" "H"]
	 rank :in ["A" "2" "3" "4" "5" "6" "7" "8" "9" "T" "J" "Q" "K"]]
    (let [sym (symbol suit rank)]
      (array/push defs ~(def ,sym @{"suit" ,(in suits suit) "rank" ,(in ranks rank)}))))
      (array/push defs ~(def ,sym @{:suit ,(in suits suit) :rank ,(in ranks rank)}))))
  (loop [rank :in ["1" "2"]]
    (let [sym (symbol "J" rank)]
      (array/push defs ~(def ,sym @{"suit" "joker" "rank" ,(in ranks rank)}))))
      (array/push defs ~(def ,sym @{:suit "joker" :rank ,(in ranks rank)}))))
  (do ;defs))

(defn to-text [{"suit" suit "rank" rank}]
(defn to-text [{:suit suit :rank rank}]
  (let [suits {"diamonds" "♦" "spades" "♠" "clubs" "♣" "hearts" "♥"}
	ranks {1 "Ace" 2 "2" 3 "3" 4 "4" 5 "5" 6 "6" 7 "7" 8 "8" 9 "9" 10 "10" 11 "J" 12 "Q" 13 "K"}]
    (string (suits suit) (ranks rank))))


@@ 21,17 21,17 @@
(defn of-suit
  "Return any cards in stack that match the led suit."
  [led-suit stack]
  (let [with-jokers |(or (= led-suit ($0 "suit"))
			 (= "joker" ($0 "suit")))]
  (let [with-jokers |(or (= led-suit ($0 :suit))
			 (= "joker" ($0 :suit)))]
    (filter with-jokers stack)))

(defn of-suit-or-trumps
  "Return any cards in stack that match the led suit."
  [led-suit trumps stack]
  (let [with-jokers-and-trumps
	|(or (= led-suit ($0 "suit"))
	     (= trumps ($0 "suit"))
	     (= "joker" ($0 "suit")))]
	|(or (= led-suit ($0 :suit))
	     (= trumps ($0 :suit))
	     (= "joker" ($0 :suit)))]
    (filter with-jokers-and-trumps stack)))

(defn of-suit-or-off


@@ 56,45 56,45 @@

(defn- uptown-card [trumps]
  @{:compare (fn [self other]
	       (match [(self "suit") (other "suit")]
		 @["joker" "joker"] (compare (self "rank") (other "rank"))
	       (match [(self :suit) (other :suit)]
		 @["joker" "joker"] (compare (self :rank) (other :rank))
		 @["joker" _] 1
		 @[_ "joker"] -1
		 @[trumps trumps] (uptown (self "rank") (other "rank"))
		 @[trumps trumps] (uptown (self :rank) (other :rank))
		 @[trumps _] 1
		 @[_ trumps] -1 
		 @[_ _] (uptown (self "rank") (other "rank"))))})
		 @[_ _] (uptown (self :rank) (other :rank))))})

(defn- downtown-card [trumps]
  @{:compare (fn [self other]
	       (match [(self "suit") (other "suit")]
		 @["joker" "joker"] (compare (self "rank") (other "rank"))
	       (match [(self :suit) (other :suit)]
		 @["joker" "joker"] (compare (self :rank) (other :rank))
		 @["joker" _] 1
		 @[_ "joker"] -1
		 @[trumps trumps] (downtown (self "rank") (other "rank"))
		 @[trumps trumps] (downtown (self :rank) (other :rank))
		 @[trumps _] 1
		 @[_ trumps] -1 
		 @[_ _] (downtown (self "rank") (other "rank"))) )})
		 @[_ _] (downtown (self :rank) (other :rank))) )})

(def- notrumps-card
  @{:compare (fn [self other]
	       (match [(self "suit") (other "suit")]
		 @["joker" "joker"] (compare (self "rank") (other "rank"))
	       (match [(self :suit) (other :suit)]
		 @["joker" "joker"] (compare (self :rank) (other :rank))
		 @["joker" _] -1
		 @[_ "joker"] 1
		 @[_ _] (uptown (self "rank") (other "rank"))))})
		 @[_ _] (uptown (self :rank) (other :rank))))})

(def- notrumps-downtown-card
  @{:compare (fn [self other]
	       (match [(self "suit") (other "suit")]
		 @["joker" "joker"] (compare (self "rank") (other "rank"))
	       (match [(self :suit) (other :suit)]
		 @["joker" "joker"] (compare (self :rank) (other :rank))
		 @["joker" _] -1
		 @[_ "joker"] 1
		 @[_ _] (downtown (self "rank") (other "rank"))))})
		 @[_ _] (downtown (self :rank) (other :rank))))})

(defn apply-ordering [current-bid]
  (fn [card]
    (let [proto (match [(current-bid "suit") (current-bid "direction")]
    (let [proto (match [(current-bid :suit) (current-bid :direction)]
		  @["notrumps" "up"] notrumps-card
		  @["notrumps" "down"] notrumps-downtown-card
		  @[trumps "up"] (uptown-card trumps)

M events.janet => events.janet +3 -3
@@ 1,5 1,5 @@
(defn pick1 [name player choices] {:event "prompt_select" "name" name "player" player "count" 1 "from" choices})
(defn pick1 [name player choices] {:event "prompt_select" :name name :player player :count 1 :from choices})

(defn draw [player count] {:event "draw" "player" player "count" count})
(defn draw [player count] {:event "draw" :player player :count count})

(defn add-decoration [player name value] {:event "add_decoration" "name" name "player" player "value" value})
(defn add-decoration [player name value] {:event "add_decoration" :name name :player player :value value})

M game/beginplay.janet => game/beginplay.janet +5 -5
@@ 14,11 14,11 @@
  Provides:
  - `suit`: The led suit of the current trick.
  ```
  [{:meta meta} players {"player" bidder "value" to-discard}]
  [{:meta meta} players {:player bidder :value to-discard}]
  # It's the first trick; bidder gets one trick for the discard.
    (let [bidder-record (find |(= ($0 :id) bidder) players)
	    current-hand (in bidder-record :hand)]
      [{:phase "play" :meta (merge meta {"suit" :null})}
       [{:event "add_score" "player" bidder "value" 1}
	{:event "discard" "player" bidder "value" to-discard}
	{:event "prompt_play" "player" bidder "to" "trick" "count" 1}]]))
      [{:phase "play" :meta (merge meta {:suit :null})}
       [{:event "add_score" :player bidder :value 1}
	{:event "discard" :player bidder :value to-discard}
	{:event "prompt_play" :player bidder :to "trick" :count 1}]]))

M game/bid.janet => game/bid.janet +10 -9
@@ 2,7 2,7 @@
(import bids)
(import events)

(defn- track-pass [not-passed player] (put not-passed player nil))
(defn- track-pass [not-passed player] (put not-passed (keyword player) nil))

(defn bid-phase
  ```


@@ 16,27 16,28 @@
  - `not_passed`: The players that haven't yet passed.
  ```
  [{:meta meta} players action]
  (let [{"player" last-bidder "value" last-bid} action
	{"player" previous-high-bidder "bid" high-bid} (meta "high_bid")
  (let [{:player last-bidder :value last-bid} action
	{:player previous-high-bidder :bid high-bid} (meta :high_bid)
	# Record the high bid (whether it's a new bid or the existing high bid).
	[high-bid high-bidder] (case last-bid
				 "pass" [high-bid previous-high-bidder]
				 [last-bid last-bidder])
	remaining-players (filter |(in (meta "not_passed") ($0 :id)) players)
	not-passed (meta :not_passed)
	remaining-players (filter |(in not-passed (keyword ($0 :id))) players)
	next-bidder (players/next-player last-bidder remaining-players)
	[events not-passed] (case last-bid
			      "pass"
			      # Handle passes. Mark the player as passed and remove them from the set.
			      [@[(events/add-decoration last-bidder "bid_action" "passed")]
			       (track-pass (meta "not_passed") last-bidder)]
			       (track-pass not-passed last-bidder)]
			      # Handle a bid. Mark the most recent bidder.
			      [@[(events/add-decoration last-bidder "bid_action" "declarer")
				 (events/add-decoration last-bidder "bid" (bids/to-text high-bid))]
			       (meta "not_passed")])
	meta @{"high_bid" {"player" high-bidder "bid" high-bid}}]
			       (meta :not_passed)])
	meta @{:high_bid @{:player high-bidder :bid high-bid}}]
    # Remove "declarer" annotation from previous bidder.
    (if (and (not= last-bid "pass") (not (nil? previous-high-bidder)))
      (array/push events {:event "remove_decoration" "name" "bid_action" "player" previous-high-bidder}))
      (array/push events {:event "remove_decoration" :name "bid_action" :player previous-high-bidder}))
    (if (= 1 (length not-passed))
      # If all but one have passed, bidding is over.
      [{:meta meta


@@ 46,7 47,7 @@
       (array/push events (events/pick1 "bid" high-bidder (bids/second-bid high-bid)))]

      # Otherwise, move to the next bidder.
      [{:meta (put meta "not_passed" not-passed)
      [{:meta (put meta :not_passed not-passed)
	#State: Bid -> Bid
	:phase "bid"}
       (array/concat

M game/deal.janet => game/deal.janet +1 -1
@@ 3,7 3,7 @@

(defn- all-draw [players] (map |(events/draw ($0 :id) 12) players))

(defn- new-meta [[p1 p2 p3 p4]] {"high_bid" {} "not_passed" {p1 true p2 true p3 true p4 true}})
(defn- new-meta [players] {"high_bid" {} "not_passed" (zipcoll (map |($0 :id) players) [true true true true])})

(defn deal-phase
  ```

M game/discard.janet => game/discard.janet +5 -7
@@ 15,12 15,10 @@
  Provides:
  - `bid`: The full bid for the hand. 
  ```
  [{:meta {"high_bid" {"bid" high-bid}}} {"player" bidder "value" second-bid}]
  [{:meta {:high_bid {:bid high-bid}}} {:player bidder :value second-bid}]
  (def high-bid-text (string (bids/to-text high-bid) ": " (bids/to-text second-bid bids/suit)))
  # State: Discard -> Begin Play
  [{:phase "begin_play" :meta {"bid" (make-full-bid high-bid second-bid)}}
   [(events/add-decoration
     "high_bid"
     bidder
     (string (bids/to-text high-bid) ": " (bids/to-text second-bid bids/suit)))
  [{:phase "begin_play" :meta {:bid (make-full-bid high-bid second-bid)}}
   [(events/add-decoration "high_bid" bidder high-bid-text)
    (events/draw bidder 6)
    {:event "prompt_discard" "player" bidder "count" 6}]])
    {:event "prompt_discard" :player bidder :count 6}]])

M game/play.janet => game/play.janet +16 -16
@@ 3,7 3,7 @@
(import events)
 
(defn- add-to-stack [stack card] (tuple ;stack card))
(defn- with-player [card player] (merge-into @{"player" player} card))
(defn- with-player [card player] (merge-into @{:player player} card))

(defn play-phase
  ```


@@ 21,36 21,36 @@
  - `bid`: The current contract.
  - `suit`: The led suit of the current trick.
  ```
  [old-state players {"player" player "value" card}]
  [old-state players {:player player :value card}]
  # We need to explicitly set the current suit to null. This
  # allows us to check for the edge case where the suit has
  # not been set, even after a card (or two) has been played
  # - if the first card is a Joker.
  (var new-state @{:phase "play" :meta @{"suit" :null} :stacks @{"trick" []}})
  (var new-state @{:phase "play" :meta @{:suit :null} :stacks @{:trick []}})
  (var events @[(events/add-decoration player "play_action" (string "played " (cards/to-text card)))])
  (let [current-trick (get-in old-state [:stacks "trick"])]
  (let [current-trick (get-in old-state [:stacks :trick])]
    (case (length current-trick)
      3 (let [current-bid ("bid" (old-state :meta))
      3 (let [current-bid (get-in old-state [:meta :bid])
	      ordering-f (cards/apply-ordering current-bid)
	      with-compare (map ordering-f current-trick) 
	      current-suit ("suit" (old-state :meta))
	      current-suit (get-in old-state [:meta :suit])
	      highest (cards/high-card current-suit ;with-compare)
	      highest-player (highest :player)]
	  (array/push events {:event "add_score" "player" highest-player "value" 1}))
	  (array/push events {:event "add_score" :player highest-player :value 1}))
      (let [old-meta (old-state :meta)
	    current-suit (old-meta "suit")
	    new-suit (if (and (= :null current-suit) (not= (card "suit") "joker"))
		       (card "suit")
	    current-suit (old-meta :suit)
	    new-suit (if (and (= :null current-suit) (not= (card :suit) "joker"))
		       (card :suit)
		       current-suit)
	    id-to-prompt (players/next-player player players)
	    player-to-prompt (find |(= ($0 :id) id-to-prompt) players)]
	(put-in new-state [:meta "suit"] new-suit)
	(put-in new-state [:stacks "trick"] (add-to-stack current-trick (with-player card player)))
	(put-in new-state [:meta :suit] new-suit)
	(put-in new-state [:stacks :trick] (add-to-stack current-trick (with-player card player)))
	(array/push
	 events
	 {:event "prompt_play"
	  "player" id-to-prompt
	  "to" "trick"
	  "count" 1
	  "from" (cards/of-suit-or-off new-suit (player-to-prompt :hand))})))
	  :player id-to-prompt
	  :to "trick"
	  :count 1
	  :from (cards/of-suit-or-off new-suit (player-to-prompt :hand))})))
    [new-state events]))

M init.janet => init.janet +7 -10
@@ 3,8 3,8 @@
  []
  {:deck "52JJ"
   :stacks {"trick" {:orientation :up
		    :max-size 4
		    :alignment :stagger}}})
		     :max-size 4
		     :alignment :stagger}}})

(defn-
  make-player


@@ 14,11 14,8 @@
(defn init
  "Create an initial game state."
  [fst snd thd fth]
  (let [rng (math/rng (os/time))
	players [fst snd thd fth]
	forehand (players (math/rng-int rng 4))]
      {:players [(make-player fst 1)
		 (make-player snd 2)
		 (make-player thd 1)
		 (make-player fth 2)]
       :state {:phase "deal"}}))
  {:players [(make-player fst 1)
	     (make-player snd 2)
	     (make-player thd 1)
	     (make-player fth 2)]
   :state {:phase "deal"}})

M main.janet => main.janet +6 -3
@@ 9,13 9,16 @@
(route :post "/api/v1/whist/next" :next)

(defn next [request]
  (print "body:")
  (pp (request :body))
  (let [state (get-in request [:body :state])
	players (get-in request [:body :players])
	[new-state events] (whist/next {:state state :players players})]
    (pp state)
	action (get-in request [:body :action])
	[new-state events] (whist/next {:state state :players players :action action})]
    (print "response:")
    (pp new-state)
    (pp events)
    (application/json {:state new-state :events []})))
    (application/json {:state new-state :events events})))

(defn init [request]
  (pp request)

M players.janet => players.janet +0 -2
@@ 5,5 5,3 @@
	     players)
	new-ind (mod (inc ind) (length players))]
    ((players new-ind) :id)))



M test/cards.janet => test/cards.janet +7 -7
@@ 9,7 9,7 @@
       (in {(chr "D") true (chr "S") true (chr "C") true (chr "H") true (chr "J") true} (0 sym))))

(defmacro test-with-bid [bid & tests]
  (let [trumps (bid "suit")
  (let [trumps (bid :suit)
	tests
	(postwalk
	 (fn [form]


@@ 25,24 25,24 @@
(defmacro card= [c1 c2 note]
  (with-syms [$c1 $c2]
    ~(let [,$c1 ,c1 ,$c2 ,c2]
       [(is (= (,$c1 "suit") (,$c2 "suit")) (string ,note " (by suit)"))
	(is (= (,$c1 "rank") (,$c2 "rank")) (string ,note " (by rank)"))])))
       [(is (= (,$c1 :suit) (,$c2 :suit)) (string ,note " (by suit)"))
	(is (= (,$c1 :rank) (,$c2 :rank)) (string ,note " (by rank)"))])))

(test-with-bid {"suit" "clubs" "direction" "up"}
(test-with-bid {:suit "clubs" :direction "up"}
	       (card= J1 (cards/high-card "diamonds" J1 C2) "jokers beat anything")
	       (card= J2 (cards/high-card "diamonds" J1 J2) "big joker beats little joker")
	       (card= D3 (cards/high-card "diamonds" D2 D3) "led suits rank normally")
	       (card= D2 (cards/high-card "diamonds" D2 S3) "led suits beat higher-off-suit")
	       (card= C2 (cards/high-card "diamonds" D3 C2) "trump beats higher off-suit"))

(test-with-bid {"suit" "clubs" "direction" "down"}
(test-with-bid {:suit "clubs" :direction "down"}
	       (card= J1 (cards/high-card "diamonds" J1 C2) "jokers beat anything")
	       (card= J2 (cards/high-card "diamonds" J2 J1) "big joker beats little joker")
	       (card= D2 (cards/high-card "diamonds" D2 D3) "led suits rank reversed")
	       (card= D3 (cards/high-card "diamonds" D3 S2) "led suits beat higher-off-suit")
	       (card= C3 (cards/high-card "diamonds" D2 C3) "trump beats higher off-suit"))

(test-with-bid {"suit" "notrumps" "direction" "up"}
(test-with-bid {:suit "notrumps" :direction "up"}
	       (card= D2 (cards/high-card "diamonds" J2 D2) "jokers never win (1)")
	       (card= D2 (cards/high-card "diamonds" J1 D2) "jokers never win (2)")
	       (card= J2 (cards/high-card "diamonds" J1 J2) "big joker beats little joker")


@@ 50,7 50,7 @@
	       (card= D3 (cards/high-card "diamonds" D3 S2) "led suits beat higher-off-suit")
	       (card= D2 (cards/high-card "diamonds" D2 C3) "trump doesn't beat higher off-suit"))

(test-with-bid {"suit" "notrumps" "direction" "down"}
(test-with-bid {:suit "notrumps" :direction "down"}
	       (card= D2 (cards/high-card "diamonds" J2 D2) "jokers never win (1)")
	       (card= D2 (cards/high-card "diamonds" J1 D2) "jokers never win (2)")
	       (card= J2 (cards/high-card "diamonds" J1 J2) "big joker beats little joker")

M test/whist.janet => test/whist.janet +58 -58
@@ 14,7 14,7 @@
		    {:score 0 :team 1 :id "South"}
		    {:score 0 :team 2 :id "West"}])

(defn all-not-passed [] @{"North" true "East" true "South" true "West" true}) 
(defn all-not-passed [] @{:North true :East true :South true :West true}) 

(deftest init
  (is (= {:players player-state


@@ 24,84 24,84 @@
(deftest deal
  (def [_state events] (whist/next {:players player-state :state {:phase "deal"}}))
  (def [north-draw _east-draw _south-draw _west_draw north-decoration north-prompt] events)
  (is (= {:event "draw" "count" 12 "player" "North"} north-draw))
  (is (= {"value" "bidding" :event "add_decoration" "player" "North" "name" "bid_action"} north-decoration))
  (is (= {"name" "bid" "count" 1 :event "prompt_select" "player" "North" "from" (bids/available-bids)} north-prompt)))
  (is (= {:event "draw" :count 12 :player "North"} north-draw))
  (is (= {:value "bidding" :event "add_decoration" :player "North" :name "bid_action"} north-decoration))
  (is (= {:name "bid" :count 1 :event "prompt_select" :player "North" :from (bids/available-bids)} north-prompt)))

(deftest bidding
  (def first-bid {:players player-state
		  :state {:phase "bid"
			  :meta {"high_bid" {}
				 "not_passed" (all-not-passed)}}
		  :action {"player" "North" "name" "bid" "value" {"count" 3 "direction" "up"}}})
			  :meta {:high_bid {}
				 :not_passed (all-not-passed)}}
		  :action {:player "North" :name "bid" :value {:count 3 :direction "up"}}})
  (def [state events] (whist/next first-bid))
  (def [north-action_decoration north-bid_decoration east-action_decoration east-prompt] events)
  (is (= {"value" "declarer" :event "add_decoration" "player" "North" "name" "bid_action"} north-action_decoration))
  (is (= {"value" "3 Uptown" :event "add_decoration" "player" "North" "name" "bid"} north-bid_decoration))
  (is (= {"value" "bidding" :event "add_decoration" "player" "East" "name" "bid_action"} east-action_decoration))
  (is (= {"name" "bid" "count" 1 :event "prompt_select" "player" "East" "from" (bids/available-bids {"count" 3 "direction" "up"})} east-prompt))
  (is (= {"bid" {"count" 3 "direction" "up"} "player" "North"} (get-in state [:meta "high_bid"])))
  (is (deep= @{"North" true "East" true "South" true "West" true} (get-in state [:meta "not_passed"]))))
  (is (= {:value "declarer" :event "add_decoration" :player "North" :name "bid_action"} north-action_decoration))
  (is (= {:value "3 Uptown" :event "add_decoration" :player "North" :name "bid"} north-bid_decoration))
  (is (= {:value "bidding" :event "add_decoration" :player "East" :name "bid_action"} east-action_decoration))
  (is (deep= {:name "bid" :count 1 :event "prompt_select" :player "East" :from (bids/available-bids {:count 3 :direction "up"})} east-prompt))
  (is (deep= @{:bid {:count 3 :direction "up"} :player "North"} (get-in state [:meta :high_bid])))
  (is (deep= @{:North true :East true :South true :West true} (get-in state [:meta :not_passed]))))

(deftest overbid
  (def player-overbid {:players player-state
		       :state {:phase "bid"
			       :meta {"high_bid" {"bid" {"count" 3 "direction" "up"} "player" "North"}
				      "not_passed" (all-not-passed)}}
		       :action {"player" "East" "value" {"count" 3 "suit" "no_trumps"}}})
			       :meta {:high_bid {:bid {:count 3 :direction "up"} :player "North"}
				      :not_passed (all-not-passed)}}
		       :action {:player "East" :value {:count 3 :suit "no_trumps"}}})
  (def [state events] (whist/next player-overbid))
  (def [east-action_decoration east-bid_decoration north-remove_decoration south-decoration south-prompt] events)
  (is (= {"value" "declarer" :event "add_decoration" "player" "East" "name" "bid_action"} east-action_decoration))
  (is (= {"value" "3 No-Trumps" :event "add_decoration" "player" "East" "name" "bid"} east-bid_decoration))
  (is (= {:event "remove_decoration" "player" "North" "name" "bid_action"}  north-remove_decoration))
  (is (= {"value" "bidding" :event "add_decoration" "player" "South" "name" "bid_action"} south-decoration))
  (is (= {"name" "bid" "count" 1 :event "prompt_select" "player" "South" "from" (bids/available-bids {"count" 3 "suit" "no_trumps"})} south-prompt))
  (is (= {"bid" {"count" 3 "suit" "no_trumps"} "player" "East"} (get-in state [:meta "high_bid"])))
  (is (deep= @{"North" true "East" true "South" true "West" true} (get-in state [:meta "not_passed"]))))
  (is (= {:value "declarer" :event "add_decoration" :player "East" :name "bid_action"} east-action_decoration))
  (is (= {:value "3 No-Trumps" :event "add_decoration" :player "East" :name "bid"} east-bid_decoration))
  (is (= {:event "remove_decoration" :player "North" :name "bid_action"}  north-remove_decoration))
  (is (= {:value "bidding" :event "add_decoration" :player "South" :name "bid_action"} south-decoration))
  (is (deep= {:name "bid" :count 1 :event "prompt_select" :player "South" :from (bids/available-bids {:count 3 :suit "no_trumps"})} south-prompt))
  (is (deep= @{:bid {:count 3 :suit "no_trumps"} :player "East"} (get-in state [:meta :high_bid])))
  (is (deep= @{:North true :East true :South true :West true} (get-in state [:meta :not_passed]))))

(deftest pass
  (def player-passed {:players player-state
		      :state {:phase "bid"
			      :meta {"high_bid" {}
				     "not_passed" (all-not-passed)}}
		      :action {"name" "bid" "player" "North" "value" "pass"}})
			      :meta {:high_bid {}
				     :not_passed (all-not-passed)}}
		      :action {:name "bid" :player "North" :value "pass"}})
  (def [state events] (whist/next player-passed))
  (def [north-decoration east-decoration east-prompt] events)
  (is (= {"value" "passed" :event "add_decoration" "player" "North" "name" "bid_action"} north-decoration))
  (is (= {"value" "bidding" :event "add_decoration" "player" "East" "name" "bid_action"} east-decoration))
  (is (= {"name" "bid" "count" 1 :event "prompt_select" "player" "East" "from" (bids/available-bids)} east-prompt))
  (is (= {} (get-in state [:meta "high_bid"])))
  (is (deep= @{"East" true "South" true "West" true} (get-in state [:meta "not_passed"]))))
  (is (= {:value "passed" :event "add_decoration" :player "North" :name "bid_action"} north-decoration))
  (is (= {:value "bidding" :event "add_decoration" :player "East" :name "bid_action"} east-decoration))
  (is (= {:name "bid" :count 1 :event "prompt_select" :player "East" :from (bids/available-bids)} east-prompt))
  (is (deep= @{} (get-in state [:meta :high_bid])))
  (is (deep= @{:East true :South true :West true} (get-in state [:meta :not_passed]))))


(deftest last-pass
  (def last-player-passed {:players player-state
			   :state {:phase "bid"
				   :meta {"high_bid" {"bid" {"count" 3 "direction" "up"} "player" "East"}
					  "not_passed" @{"East" true "North" true}}}
			   :action {"name" "bid" "player" "North" "value" "pass"}})
				   :meta {:high_bid {:bid {:count 3 :direction "up"} :player "East"}
					  :not_passed @{:East true :North true}}}
			   :action {:name "bid" :player "North" :value "pass"}})
  (def [state events] (whist/next last-player-passed))
  (def [north-decoration east-prompt] events)
  # events for East is nil, because there are no side effects for that player this round. 
  (is (= {"value" "passed" :event "add_decoration" "player" "North" "name" "bid_action"} north-decoration))
  (is (= {:value "passed" :event "add_decoration" :player "North" :name "bid_action"} north-decoration))

  (is (= {"name" "bid" "count" 1 :event "prompt_select" "player" "East" "from" bids/suit} east-prompt))
  (is (= {"bid" {"count" 3 "direction" "up"} "player" "East"} (get-in state [:meta "high_bid"])))
  (is (= {:name "bid" :count 1 :event "prompt_select" :player "East" :from bids/suit} east-prompt))
  (is (deep= @{:bid {:count 3 :direction "up"} :player "East"} (get-in state [:meta :high_bid])))
  # not_passed is gone because we are moved to the next phase.
  (is (= nil (get-in state [:meta "not_passed"])))
  (is (= nil (get-in state [:meta :not_passed])))
  (is (= "discard" (in state :phase))))

(deftest discard-prompt
  (def player-completed-bid {:players player-state
			     :state {:phase "discard"
				     :meta {"high_bid" {"bid" {"count" 3 "direction" "up"} "player" "East"}}}
			     :action {"player" "North" "value" {"suit" "clubs"}}})
				     :meta {:high_bid {:bid {:count 3 :direction "up"} :player "East"}}}
			     :action {:player "North" :value {:suit "clubs"}}})
  (def [state events] (whist/next player-completed-bid))
  (def [north-decoration north-draw north-prompt] events)
  (is (= {"value" "3 Uptown: Clubs" :event "add_decoration" "player" "high_bid" "name" "North"} north-decoration))
  (is (= {:event "draw" "count" 6 "player" "North"} north-draw))
  (is (= {:event "prompt_discard" "count" 6 "player" "North"} north-prompt))
  (is (deep= {:phase "begin_play" :meta {"bid" @{"count" 3 "direction" "up" "suit" "clubs"}}} state)))
  (is (= {:value "3 Uptown: Clubs" :event "add_decoration" :player "high_bid" :name "North"} north-decoration))
  (is (= {:event "draw" :count 6 :player "North"} north-draw))
  (is (= {:event "prompt_discard" :count 6 :player "North"} north-prompt))
  (is (deep= {:phase "begin_play" :meta {:bid @{:count 3 :direction "up" :suit "clubs"}}} state)))

(deftest begin-play
  (let [two-cards [CA C2]


@@ 111,14 111,14 @@
		  {:hand [] :score 0 :team 2 :meta {} :id "West"}]
	player-discarded {:players with-two
			  :state {:phase "begin_play"
				  :meta {"suit" :null}}
			  :action {"player" "North" "name" "discard" "value" [C2]}}]
				  :meta {:suit :null}}
			  :action {:player "North" :name "discard" :value [C2]}}]
    (def [state events] (whist/next player-discarded))
    (def [north-score north-discard north-prompt] events)
    (is (= {:event "add_score" "value" 1 "player" "North"} north-score))
    (is (deep= {:event "discard" "value" [@{"rank" 2 "suit" "clubs"}] "player" "North"} north-discard))
    (is (= {"count" 1 "to" "trick" :event "prompt_play" "player" "North"} north-prompt))
    (is (deep= {:phase "play" :meta @{"suit" :null}} state))))
    (is (= {:event "add_score" :value 1 :player "North"} north-score))
    (is (deep= {:event "discard" :value [@{:rank 2 :suit "clubs"}] :player "North"} north-discard))
    (is (= {:count 1 :to "trick" :event "prompt_play" :player "North"} north-prompt))
    (is (deep= {:phase "play" :meta @{:suit :null}} state))))

(def- players-with-full-hands
  # Discarded: Deuces + Jokers


@@ 129,16 129,16 @@

(deftest first-lead
  (def player-lead {:players players-with-full-hands
		    :state {:phase "play" :meta {"suit" :null} :stacks {"trick" []}}
		    :action {"player" "North" "name" "play" "value" CJ}})
		    :state {:phase "play" :meta {:suit :null} :stacks {:trick []}}
		    :action {:player "North" :name "play" :value CJ}})
  (def [state events] (whist/next player-lead))
  (def [north-decoration east-prompt] events)
  (let [played-card (merge-into @{"player" "North"} CJ)]
    (is (deep= @{"suit" "clubs"} (state :meta)))
    (is (deep= @{"trick" [played-card]}  (state :stacks))))
  (is (= {"value" "played \xE2\x99\xA3J" :event "add_decoration" "player" "North" "name" "play_action"} north-decoration))
  (is (deep= {"player" "East" :event "prompt_play" "count" 1 "to" "trick"
		       "from" [DA D3 D4 D5 D6 D7 D8 D9 DT DJ DQ DK]}
  (let [played-card (merge-into @{:player "North"} CJ)]
    (is (deep= @{:suit "clubs"} (state :meta)))
    (is (deep= @{:trick [played-card]} (state :stacks))))
  (is (= {:value "played \xE2\x99\xA3J" :event "add_decoration" :player "North" :name "play_action"} north-decoration))
  (is (deep= {:player "East" :event "prompt_play" :count 1 :to "trick"
		       :from [DA D3 D4 D5 D6 D7 D8 D9 DT DJ DQ DK]}
	     east-prompt)))

(run-tests!)