~subsetpark/whist

8a11661a0450a6b4f2c7189184d57067d81f3e52 — Zach Smith 6 months ago c761d03
Add bids.lit
6 files changed, 136 insertions(+), 10 deletions(-)

M bids.janet
A lit/bids.lit
M test/bid.janet
A tests.do
A whist.do
M whist.lit
M bids.janet => bids.janet +12 -6
@@ 1,3 1,5 @@
## bids.janet
## All Bids
(def-
  bids
  [[{:count 3 :direction "up"} "3 Uptown"]


@@ 16,17 18,21 @@
   [{:count 7 :direction "down"} "7 Downtown"]
   [{:count 7 :suit "no_trumps"} "7 No-Trumps"]])

(def direction [[{:direction "up"} "Uptown"]
## Second Bids
(def- direction [[{:direction "up"} "Uptown"]
		[{:direction "down"} "Downtown"]])

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

(def second-bids (array/concat @[] direction suit))

(defn- no-trumps? [bid] (= (bid :suit) "no_trumps"))
(defn second-bid
  [bid]
  (if (no-trumps? bid) direction suit))

(defn- find-row-index [bid source] (find-index |(= ($0 0) bid) source))

(defn to-text


@@ 36,6 42,8 @@
	   row (source ind)]
    (row 1)
    (error (string "Not found: " (string/format "%q" bid) " in " (string/format "%q" source)))))
## Available Bids
(defn- no-trumps? [bid] (= (bid :suit) "no_trumps"))

(defn available-bids
  [&opt high-bid]


@@ 53,6 61,4 @@
  []
  (array ;bids))

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


A lit/bids.lit => lit/bids.lit +111 -0
@@ 0,0 1,111 @@
@s Appendix: Bids

We can hardcode the list of possible bids and define a few functions
to expose them. They are written in the `select` format (ie, in the
format used to define the choices for a `select` prompt). It's a
simple format, a list of pairs, where the first element is the value
that will be sent to the rules engine if that choice is selected, and
the second element is the string to display when exposing the prompt
to the player.

This kind of general format gives us a maximum of flexibility as
game-developers; in this case, the value of a selection is an object
containing `count` and `direction` attributes, but this value will be
passed transparently back to us from the server, so we can make it
whatever we like (as long as it can be serialized and deserialized
from JSON!).

--- All Bids
(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"]])
---

@s Determining the Second Bid

For each of the two types of bids in the auction--suited bids and
no-trumps bids---there's a second call that the declarer makes when
they have won the auction. For suited bids, the declarer calls the
suit. For no-trumps bids, the declarer calls the direction.

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

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

(def second-bids (array/concat @[] direction suit))

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

@s Determining Available Bids

The bulk of the game logic that we need to implement in the bids
module is determining what bids are available for the next bidder
given what the current high bid in the auction is.

The logic is slightly complex, because *Uptown and Downtown bids are
equivalent* in terms of the auction. That means that if the current
high bid is a suited bid, the next available bid is the no-trumps bid
of the same number. If the high bid is a no-trumps bid, the next
available bid is the Uptown bid of the next number.

Finally, if three out of four players pass, then the fourth player
*has* to bid; so in that case, the `Pass` option isn't available to them.

---Available Bids
(defn- no-trumps? [bid] (= (bid :suit) "no_trumps"))

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

(defn force-bid
  "The bidder can't pass; they can only select an actual bid."
  []
  (array ;bids))
---

@s 

--- bids.janet
@{All Bids}
@{Second Bids}
(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 (freeze bid) source)
	   row (source ind)]
    (row 1)
    (error (string "Not found: " (string/format "%q" bid) " in " (string/format "%q" source)))))
@{Available Bids}
---
\ No newline at end of file

M test/bid.janet => test/bid.janet +5 -1
@@ 59,6 59,10 @@
  (is (deep= @{} (get-in state [:meta :high_bid])))
  (is (deep= @{:East true :South true :North true} (get-in state [:meta :not_passed]))))

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

(deftest last-pass
  (def last-player-passed {:players player-state


@@ 70,7 74,7 @@
  (def [north-decoration east-prompt] events)
  (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 (= {:name "bid" :count 1 :event "prompt_select" :player "East" :from 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])))

A tests.do => tests.do +3 -0
@@ 0,0 1,3 @@
redo-ifchange whist

JANET_PATH=./janet_modules jpm test
\ No newline at end of file

A whist.do => whist.do +2 -0
@@ 0,0 1,2 @@
redo-ifchange whist.lit
lit --md-compiler pandoc whist.lit
\ No newline at end of file

M whist.lit => whist.lit +3 -3
@@ 981,8 981,6 @@ If the bidders have won or lost, the game is over. In the Tamerlane
system, we simply end a game by emitting an event; it's not part of
the game state.

@s End Game

Different card games have different concepts of ending a game; in some
games (such as this one), teams win and lose a game together; in some,
each player plays for themselves; in yet others, players might be on


@@ 1007,7 1005,7 @@ player ID to their final score.
					[score1 score1 score2 score2])}))
---

@s play.janet
@s 

Having covered all the possible outcomes of a play state transition,
we can wrap up all the components into a single module.


@@ 1029,4 1027,6 @@ we can wrap up all the components into a single module.
@{Main Play Function}
---

@include lit/bids.lit

@include lit/events.lit
\ No newline at end of file