Some Preliminary Thoughts About a New Card Game Website

For the past month and a half or so, I’ve been working on a new software system for playing card games online. If you’d like to follow along it’s being hosted on sourcehut.

The idea came to me when I was talking with a friend who shares two notable characteristics with me: 1) he is a computer programmer; 2) he’s an enthusiast of obscure traditional card games. He was implementing a new card game on Board Game Arena, which is more or less the best that you can currently do if you want to build a new card game that can be played online and don’t want to implement the entire site from scratch. It’s a generic board games/card games site which has a “studio” feature, where developers can upload their own logic which describes a new game. It’s also, from where I sit, a pretty poor experience: you need to write your game in PHP, and you need to upload it by FTP. It’s a little antiquated, in other words.

Hearing about the developer experience I had the sudden inspiration of a site that worked on the Slack installable apps model instead: one that exposed the generic bones of interaction with a deck of cards, but where you as the developer wrote a simple JSON API to describe the actual game, and installed the game simply by pointing the site at your callback URL. In other words, you could write the game itself in whatever language you liked, and only had to spin up a new web server rather than using anyone’s crufty old library.

I called it Tamerlane, being the first word that popped into my head. The viability and suitability of this model as a product aside[1], I thought I’d go into some detail about how I’ve been building it.

In the Tamerlane model, there’s a central game server, which all users interact with, and which maintains the state of all users and ongoing games. It in turn has entries for some number of heterogeneous independent rules servers. In particular, every ongoing game is pointed at a single rules server, which acts as the source of truth for the transformations of game state for that game.

The game, at any point, consists of a blob of state, for example:

{:meta {:high_bid {:player "North" :bid {...}}
        :not_passed @{:North true :South true :East true :West true}}
 :phase "bid"}

At any given moment—and I think we can make this assumption for any card game—the game state is static and waiting for the next player action. This could be making a bid, discarding a card, playing a card. The entire function of any rules server is to accept a POST request to a single endpoint—let’s call it /next—with a description of the game state, and the player action, and to return the updated game state. The only thing left required to drive the game engine is to also specify any side effects that need to happen as a result of game state changes. This includes things like updating player information displays, showing user prompts, and ultimately ending the game.

This leads to an interaction model like the following:

+---------+                +-------+                             +-------+
| Player  |                | Game  |                             | Rules |
+---------+                +-------+                             +-------+
     |                         |                                     |
     | Start new game          |                                     |
     |------------------------>|                                     |
     |                         |                                     |
     |                         | GET /init?players=North,South       |
     |                         |------------------------------------>|
     |                         |                                     |
     |                         |{"phase": "draw",                    |
     |                         | "effects": [{"event": "prompt_draw",|
     |                         |              "player": "North"}]}   |
     |                         |<------------------------------------|
     |                         |                                     |
     | <render draw modal>     |                                     |
     |<------------------------|                                     |
     |                         |                                     |
     | <select a card>         |                                     |
     |------------------------>|                                     |
     |                         |                                     |
     |                         | POST /next                          |
     |                         |   {"phase": "draw",                 |
     |                         |    "action": {"name": "draw",       |
     |                         |               "player": "North",    |
     |                         |               "value": {"rank": 1, "suit": "hearts"}}}
     |                         |------------------------------------>|
     |                         |                                     |

In other words, the rules server is entirely stateless. It expects everything it needs in order to calculate game state is included in the POST to /next. Secondarily, the meat of the game interaction will be in the effects system: it’s up to the game server to expose an inventory of effects that:

  1. is small enough to be obvious to a rules developer;
  2. can be mapped to rich user experience effects so that the game server can expose an enjoyable and suggestive UI;
  3. can cover all, or nearly all, of the types of actions and effects that actually happen during the play of a game of cards.

It’s fairly obvious that this system will live or die on the strength of the rules callback API it exposes. We need to design a DSL that rules developers can express both game state and game effects in that is expressive, simple, and elegant.

To that end I started by writing a rules server, and thinking about the most natural way that I, as a rules developer, would want to statelessly implement the rules of an entire card game. That server is part of the Sourcehut project and implements (at time of writing, nearly all of) the game of Bid Whist. Part of the developer proposition of this system is that you can write a rules engine in whatever language you like, so this server is written in the LISP Janet. It’s been an effective way to work out what the most expressive and simple DSL would be for communicating everything that goes on in a card game, and hopefully when it’s all finished will also be a useful reference implementation of a rules server.

I’d like, obviously, to have as few types of things as possible. Thus, here’s my current ontology; all of the entities you can describe and manipulate in order to drive any kind of card game:

  • cards: objects recognized by the system as cards, with suit and rank.
  • players: entities that correspond to users and can be referred to by ID.
  • hands: ordered lists of cards, one for each player.
  • stacks: named places on the table that can hold one or more cards. For instance, in Bid Whist there’s only a single stack, called trick, which is where players play to.
  • infos: named infoboxes which each track an individual piece of information that make up the game’s state. For instance, in Bid Whist there’s an infobox for each team’s score, as well as one for the number of tricks that each player has taken during the current hand. These would more naturally be called counters if they only held numerical information, which they might end up doing.
  • decorations: arbitrary named pieces of text which the rules server can set on each player. For instance, in Bid Whist these are used to display which player is bidding, what each player’s most recent bid is, and so on.
  • prompts: objects representing which player are allowed to move at any point and what their options are. For instance, in Bid Whist, during bidding, each player is prompted in turn to select one of the set of bids which is higher than the current bid, or to pass.
  • actions: the result of a player responding to a prompt. Every call to /next involves a single action, and thus the response always describes the results of that action.

The game state consists of the stacks and the infoboxes; it’s provided to the rules server in a call to /next and whatever the rules server includes at "state" in its response will be the new state.

It’s important to note that the total contents of the game state, however, is completely up to the rules developer. For instance, in the Bid Whist implementation, the rules server sets and maintains a meta attribute that it uses to keep track of the current highest bid, or what contract was selected.

In addition to the game state, the only other content in the rules server response is the game effects. This essentially covers the specialized areas of interaction with the game that would be onerous to maintain in a giant state blob. Therefore, effects include:

  • setting a player decoration
  • clearing a player decoration
  • setting a player prompt
  • describing player actions like drawing and discarding

In particular, treating player draws and discards as effects means that the rules server doesn’t need to keep track of the contents of the deck, make sure the cards have been shuffled, specify the contents of player hands, and so on. This is making the bet that there is a sufficiently general definition of drawing and discarding that can be relied on by the majority of games.

On the other hand, it doesn’t seem that there is any sufficiently general definition of points that can be relied on in the same way. Bid Whist, for instance, involves two fixed teams, tracking the tricks taken over the course of a single hand, the outcome of which impacts the team scores tracked over the course of multiple hands. Slovenian Tarok, on the other hand, has four individual players, who sometimes (but not always) form teams for the duration of a single hand, and track the number of points captured within that hand which then impact the individual scores tracked over the course of multiple hands. It doesn’t seem that there’s some concept of teams, tricks, card points or game points which is generalizable at all. Thus the specific arrangement of scores is a part of the state—the infoboxes—and is managed directly by the rules engine however the developer sees fit.

The other program which currently exists is the game server. That is, the program which you will ultimately interact with when you go to https://toulemonde.cards; the server which you’ll have an account on, create tables on, invite your friends to, and so on.

This so far takes the form of an Elixir application which currently does little aside from interact directly with the only rules server it knows about. It can spin up a new GenServer and maintain game state by conveying user input to the rules server and interpreting the response. The key, of course, is that it needs to be completely agnostic of what it’s passing back and forth; it needs to understand the business of card playing well enough to render a compelling user interface to a human player, but simultaneously encode no assumptions whatsoever about how any particular card game works.

To this end I’ve found it surprisingly useful to simply write a simple Mix.Task CLI that prompts for user input and passes it back to the rules server. Even just the task of rendering a compelling text-based view of the game board[2] is a fairly good test of the primitives and effects system.

Here’s a sample of the system as it stands:

code-src/tamerlane [trunk] ⊕ mix cli
Compiling 2 files (.ex)
=================
North draws 12 cards.
[North] Hand: %{
  "clubs" => [#Card<5♣>, #Card<9♣>],
  "diamonds" => [#Card<6♦>, #Card<T♦>],
  "hearts" => [#Card<3♥>, #Card<4♥>, #Card<7♥>, #Card<T♥>, #Card<K♥>],
  "spades" => [#Card<2♠>, #Card<9♠>, #Card<T♠>]
}
South draws 12 cards.
...
West draws 12 cards.
...
East draws 12 cards.
...
Setting Decoration for North: bid_action

[North] Please select 1 of ["3 Uptown", "3 Downtown", "3 No-Trumps", "4 Uptown", "4 Downtown",
 "4 No-Trumps", "5 Uptown", "5 Downtown", "5 No-Trumps", "6 Uptown",
 "6 Downtown", "6 No-Trumps", "7 Uptown", "7 Downtown", "7 No-Trumps", "Pass"] as bid.

North:
  bid_action: bidding
Phase: bid
East/West:
  0
North/South:
  0
Player [North]:  <User input: <ENTER>>
%{
  "clubs" => [#Card<5♣>, #Card<9♣>],
  "diamonds" => [#Card<6♦>, #Card<T♦>],
  "hearts" => [#Card<3♥>, #Card<4♥>, #Card<7♥>, #Card<T♥>, #Card<K♥>],
  "spades" => [#Card<2♠>, #Card<9♠>, #Card<T♠>]
}
Action: <User input: "3 Uptown">
=================
Setting Decoration for North: bid_action

Setting Decoration for North: bid

Setting Decoration for South: bid_action

[South] Please select 1 of ["3 No-Trumps", "4 Uptown", "4 Downtown", "4 No-Trumps", "5 Uptown",
 "5 Downtown", "5 No-Trumps", "6 Uptown", "6 Downtown", "6 No-Trumps",
 "7 Uptown", "7 Downtown", "7 No-Trumps", "Pass"] as bid.

North:
  bid: 3 Uptown
  bid_action: declarer
South:
  bid_action: bidding
Phase: bid
East/West:
  0
North/South:
  0
Player [South]: ▌

If you squint, you can imagine how this same information could be rendered by an interface which knows nothing about Bid Whist itself into a fairly generic but snappy user interface, with your hand at the bottom, a pile of cards in the middle, and all the rest.

I’d like to finish both the Whist implementation as well as the command line interface. At that point there are two dimensions I need to expand in: on the one hand, I need to build out the whole website, including the frontend, and there will be all of the challenges and problems of web development that will need to be solved along the way. On the other, I need to build engines for a couple other games that are sufficiently different from bid whist. I need to actually test, in other words, that the DSL I’m putting together is sufficiently generic that it can be relied on by different games and still be understood by the central server, and sufficiently expressive that those games still resemble themselves by the time they’re actually rendered on the page.


  1. And there are some significant issues, to be sure. As my partner put it: Isn’t it a problem that your site relies on a bunch of different other websites to be up and working in order for people to be able to play any games? Yes, yes it is.  ↩

  2. It will, of course, ultimately be a rich Javascript interface on a website; I’m planning on using Phoenix LiveView since I’m an awful frontend developer.  ↩