~subsetpark/whist

de17310dc61b9802a23b04fd36332a34641b1c76 — Zach Smith 6 months ago 4593072
Add lit source files
2 files changed, 262 insertions(+), 0 deletions(-)

A lit/events.lit
A whist.lit
A lit/events.lit => lit/events.lit +24 -0
@@ 0,0 1,24 @@
@s Appendix: All Events

--- events.janet
(import players)

@{Events: Pick 1}
@{Events: Draw}
@{Events: Add Decoration}

(defn clear-decoration [player name] {:event "clear_decoration" :name name :player player})

(defn end-game
  [players team1 score1 team2 score2]
  (let [players1 (players/of-team players (string team1))
	players2 (players/of-team players (string team2))]
    {:event "end_game" :scores (zipcoll (array/concat players1 players2)
					[score1 score1 score2 score2])}))

(defn prompt-play [player &opt from] {:event "prompt_play" :player player :to "trick" :count 1 :from from})

(defn prompt-discard [player count] {:event "prompt_discard" :player player :count count})

(defn add-info [id label] {:event "add_info" :id id :label label})
---
\ No newline at end of file

A whist.lit => whist.lit +238 -0
@@ 0,0 1,238 @@
@title Bid Whist: An Implementation for Tamerlane
@code_type janet .janet
@comment_type # %s
@compiler JANET_PATH=janet_modules jpm build

@s Introduction

This program implements a **rules engine** for Tamerlane, the Card
Game Server. It provides a working version of the game Bid Whist, as
well as an example implementation of a realistically complex rules
engine.

--- whist.janet
(defn next
  ```
  Rules engine for Bid Whist.
  ```
  [{:state state
    :players players
    :action action}]
  @{Return Next State})
---

The most important function that we need to implement when building a
rules engine is `next`. Ultimately, we'll expose an API endpoint that
accepts a POST request and passes the body to `next`. The input to our
function describes the state of the entire session, and the return
value of this function will be to describe *what happens next.*

The three components of the input are the `state`, the `players`, and
an `action`.

Every *next state* describes a change after some single action. 

@s State

The `state` consists of the state of the game. This includes things
like the cards on the table and the scores for the players, but it
also will include arbitrary metadata that you read and write.

The `players` are a list of the players, their metadata, and their
current hands.

@s

The most basic and important attribute of the game state is the
`phase`. You can populate this however you like, though it must be
present.

Here we'll match on the game phase to call one of the five main game
modules. Each one has the same return signature and implements the
`next` function.

--- whist.janet :=
(import game/deal)
(import game/bid)
(import game/discard)
(import game/beginplay)
(import game/play)

(defn next
  ```
  Rules engine for Bid Whist.
  ```
  [{:state state
    :players players
    :action action}]
  (match (state :phase)
    "deal" (deal/deal-phase state players)
    "bid" (bid/bid-phase state players action)
    "discard" (discard/discard-phase state players action)
    "begin_play" (beginplay/begin-play-phase state players action)
    "play" (play/play-phase state players action)))
---

We'll start with the deal. We see that we only need the game state and
the players, as it's the first thing that happens in the game.

@s The Deal

There's very little logic in the deal, so it's a good place to
start. We simply pull out the first player and then return.

--- Main Deal Function
(defn deal-phase
  ```
  Each player starts with 12 cards. 

  The first player is prompted to begin bidding.
  ```
  [state players]
  @{Get The First Player}
  @{Return Value}
---

@s

`players` is a list of objects representing each player. `(player :id)`
is a string that's guaranteed to be unique among the players. Here
we'll simply grab the first player's id.

--- Get The First Player
(let [bidder ((in players 0) :id)]
---

@s Return: the New State and a List of Events

Finally we see the return value of our functions.

We simply return a tuple of two elements. The first is the new game
state; the second is an array of **events** to be emitted.

--- Return Value
[
 @{New State}
 @{Deal Events}
]))
---

@s

First the state.

We begin by merging some values into the state we were passed in. In
general this is good practice, because whatever we return will become
the new state. So if we clobber something useful, it be lost.

The new state we're merging in has two attributes: `phase`, which
we've already covered, and `meta`. In fact, `meta` has no semantics to
the Tamerlane system. We'll be using it for our own purposes. The game
won't display it; it will simply include it in the next game state it
sends over. Thus, it's an easy way for us to keep track of any game
variables we like.

In this case, we'll need to keep track of two variables: `high_bid`,
which is empty for now; and `not_passed`, which is a set containing
all the players. We'll use them both in the bidding phase.

--- New State
# State: Deal -> Bid 
(merge state {:phase "bid"
              :meta (new-meta players)})
---

--- Initialize Metadata
(defn- new-meta [players] {:high_bid {}
			   :not_passed (zipcoll (map |($0 :id) players) [true true true true])})
---

@s

Next are the events.

--- Deal Events
(array/concat
 (all-draw players)
 (events/add-decoration bidder "bid_action" "bidding")
  (events/pick1 "bid" bidder (bids/available-bids)))
---

@s Draw

Each element in the events array is, fittingly, a call to the `events`
module. There are three types of event. First, the `draw` event:

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

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

`all-draw` emits one event for each player. We end up with four
structs corresponding to the four players.

As we will see, every event has an `event` attribute, which identifies
a specific side effect understood by the Tamerlane server. Every type
of event has its own set of attributes. Together they constitute the Tamerlane API.

The `draw` event tells the server to draw a certain number of cards
into the specified player's hand.

Notice that the hands are not part of the state, and we haven't seen
the deck anywhere either; these are controlled by the server. When the
server processes one of these events, it will remove the first 12
cards from the deck and append them to the hand of the specified
player.

@s Add Decoration

The next type of event is `add-decoration`.

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

**decoration** is a generic way of displaying information associated
with a player. In this case we set the `bid_action` decoration for
the first player to `bidding`. The name, `bid_action`, is for our use
only; the server will display `bidding` next to the icon for that
player.

@s Pick 1

Finally, we include a **prompt**.

--- Events: Pick 1
(defn pick1 [name player choices] {:event "prompt_select" :name name :player player :count 1 :from choices})
---

Most state changes (except for the first, as we've seen!) are in
response to **actions**. And all actions are in response to
prompts. This is the first kind of prompt we've seen: a **select
prompt**, where the player is presented with a simple list of choices
and they have to choose one.

When they do that, the game server will make a `next` call to our
rules server, and the value of their selection will be the `action`.

In this case, the choices are a list of possible bids. Thus the bid phase begins.

@s

We've now performed everything we need to complete the deal: we've set
up our state, populated the players' hands, set a player decoration,
and prompted for the first player action.

--- game/deal.janet
(import events)
(import bids)

@{Each Player Draws}
@{Initialize Metadata}
@{Main Deal Function}
---

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