mental-poker-toolkit

A Mental Poker toolkit for building games in zero-trust environments.

View on GitHub

Modeling games

We model games as a sequence of actions.

A simple example

For example, here is one way to model a game of rock-paper-scissors (this is a demo in this repo):

So a game consists of 4 actions: PlayAction (from one player), PlayAction (from the other player), RevealAction (from one player), RevealAction (from the other player).

Note the order of plays and reveals doesn’t matter, as long as no RevealAction comes before the last PlayAction.

Actions

One of the most basic types defined in the types package is BaseAction. This represents the minimal properties we expect an action to have: a clientId and a type.

The clientId tells us which client originated the action, the type helps us disambiguate between the different actions that a game needs. A lot of the core types and interfaces are generic, with the constraint T extends BaseAction.

An interesting problem we have to solve is that each game has its own specific actions, but we want to provide some common infrastructure for any game. We do this by modeling game-specific actions as their own subtypes of BaseAction, then combining them into an Action. Going back to our rock-paper-scissors example, we have type Action = PlayAction | RevealAction. Then we can instantiate our types as, for example, ActionQueue<Action>.

State machine for rock-paper-scissors

We defined our PlayAction and RevealAction. The game of rock-paper-scissors state machine is:

[!NOTE] We are following the Fluid Framework design in all examples, assuming no message we sent was seen by other players until we receive it back ourselves over the transport.

That means for every action we take, we need corresponding code to expect it coming back over the transport (being dequeued). Since our BaseAction requires a clientId, we can easily tell our own incoming actions from actions coming from other players.

More complex examples

Shuffling cards

Shuffling cards in Mental Poker is a more complex example:

To reveal a card, Alice and Bob exchange keys.

The shuffling algorithm is implemented in the primitives package. It defines two Actions, ShuffleAction1 and ShuffleAction2 for each step, and the state machine to perform the shuffle.

Drawing and discarding cards

Considering the shuffling algorithm above, we start the game with a deck encrypted such that neither player can “see” any of the cards. They need the corresponding key from the other player in order to decrypt the card.

If Alice wants to draw a card from the top of the draw pile. This card, $K_1$, is double-encrypted with Alice’s key ($K_{A1}$) and Bob’s ($K_{B1}$).

Even drawing a card involves several steps. It means we need to ask the other player for a key, and the other player needs to send it.

This is where we would also run validation logic based on the game semantics: is the other player allowed to draw that card? In a zero-trust environment, we need to cover all our bases. See Implications of zero-trust.

Discard

The discard game demo is a good example of a more complex games with several actions - dealing, discarding a card, drawing a card etc. You can see how we model the game based on who’s turn is it: