OVERVIEW

This is the design for a two-player peer-to-peer card game application.

This application does only two things:
  1. It presents a graphical representation of a bunch of piles of cards, including a draw pile, your hand, a play area, and a discard pile. It allows you to move cards from any pile to any other pile.
  2. It keeps track of which cards your opponent has looked at, and which cards are truly secret. It will inform you if your opponent stacks his deck or peeks at your hand.
That's it. This application does not enforce the rules of any card game, and it cannot play any card game.

CARD PILES OR ZONES

A "zone" is an ordered list of cards. At all times, each card in the game is in exactly one zone.

Each player has the following zones:
  1. library / draw pile. This zone is private. Neither player can look at the contents or order of either player's library without asking the other player for permission.
  2. sideboard. This zone is semi-private. The contents are known to the zone's owner, but not to the other player.
  3. hand. This zone is semi-private. When a card is moved to a player's hand, it should be revealed to that player. Neither player can look at the contents or order of the other player's hand without asking for permission.
  4. discard pile. This zone is public. When a card is moved to a public zone, it should be revealed to both players.
  5. removed from game. Public. Just another discard pile.
  6. play area. This zone is semi-public. When a card is moved to a play area, it can be revealed to both players, which represents playing the card face-up. Or it can be unrevealed, which represents playing the card face-down. Each card in a play area has associated (x, y) coordinates. Also, any number of (key, value) pairs can be associated with the card.

RESTRICTIONS

No player is allowed to modify his opponent's library. (He cannot move cards out of or in to that library. If both players were allowed to modify the same library at the same time, then the peers could become out-of-sync in a way that would make it impossible to ever shuffle that library again.)

There are no other restrictions on card movement. All other moves are technically allowed, even moves that would break the rules of some card games. (For example, the protocol allows me to move cards from my opponent's hand to my hand. Fortunately, all card movements are announced to both players, so you can disconnect from players who make illegal moves.)

HOW IT WORKS

At the start of a game, each card in each player's deck is represented by a unique string, and that string encrypted by both players. Then, each double-encrypted string is assigned a unique card ID (cid). Then, the zones are initialized: Each player's card IDs are split up into that player's library and sideboard zones, and all other zones are emptied.

At all times, each player knows the exact ordered list of cids in each zone. However, for a player to find out what card is represented by a particular cid, the player must ask the other player to decrypt the cid's double-encrypted string.

Here's an example: Let's say that cid 123 is at the top of my library, and I want to draw a card. I'll send my opponent a message saying that I want to draw a card. He will remove 123 from the list of cids in my library, and add 123 to the list of cids in my hand. And I'll do the same. But what card did I actually draw? Neither of us knows. Fortunately, my opponent acknowledges that I now have the right to know what cid 123 represents. So he decrypts that card's double-encrypted string. The result is a single-encrypted string that has no meaning to him. He sends the single-encrypted string to me, and I decrypt it, and now only I know what's in my hand.

Now I want to play cid 123. So I decrypt the card's double-encrypted string, and get a string that is only encrypted by my opponent. I send it to him, and he decrypts it, and now I can move the card to a public zone.

This scheme is known as Mental Poker. It requires a commutative encryption algorithm. This prototcol was designed to use the RSA Modulo a Prime algorithm, but clients may be implemented using other encryption schemes.

DIRTY SHUFFLING

A library can be "clean" or "dirty". A library is clean if no card in the library has been revealed to either player. Otherwise, it's dirty. (To make a library dirty, either peek at the top card without drawing it, or move a card from some other zone to your library.)

To shuffle a clean library, just put the list of cids in some random order, and send the new list to your opponent.

To shuffle a dirty library, the cards need to be re-encrypyed by each player, like they were a the start of the game.

PROTOCOL

Two players connect over a network. They can use a direct TCP/IP connection. If both players are behind firewalls, then they can connect through an instant messaging service. (Jabber seems appropriate for this game.)

The players send messages to each other. The messages are not RPCs. They do not have to be answered or acknowledged. Each player must always be ready to receive and process a message from the other player, regardless of whose turn it is or anything else that's going on in the game.

Each message has a name and a series of arguments. An argument is a name and a value. The value can be any standard type, such as an integer, a string, or an array of strings.

CARD DESCRIPTIONS

A card description, as sent in the "set-deck" message, is a 32 byte string.

The first byte is a random byte, which distinguishes this individual card from other copies of this card in the deck. (The high bit must be off. A deck is limited to 128 copies of any single card.)

The second byte is the full length of the card name, even if the full length does not fit in this card description. (The length is the number of bytes in the name in UTF8 format.)

The next 30 bytes are the card name in UTF8 format. The card name is truncated if necessary, and padded with random bytes if necessary.

Then, the 32 bytes are encrypted.

MESSAGES

hello
The first message sent by each peer to the other. The arguments should contain the data that will be necessary for further communication, such as the version of this protocol that the peer supports, and any known extensions.

identify value:%d
Each peer sends a non-negative random integer to the other peer. The peer that sends the lower number will be identified as pid 0, and the other will be pid 1. If there is a tie, then each peer should send a new value.

set-player-name name:%s
Tells the opponent this player's name.

set-player-value key:%s value:%d
Associates a (key, value) pair with the player.

set-deck cards:{%s %s %s ...} kid:%d text:%s
Sends this player's deck as a list of single-encrypyed card descriptions. (This message can only be used during game-setup. It cannot be used while a game is in progress.) For player 0's deck: cards[0] becomes cid 1 and is put at the bottom of the library. cards[1] becomes cid 2 and is second from the bottom, and so on. The last card ends up at the top of the library and has the highest cid.
For player 1's deck, add 5000 to each cid.

Note that the library is initially dirty. So, both the sender and the receiver should treat this message like an "encrypt-request" message: The sender's GUI should "lock" the library, and the receiver should respond with an "encrypt" message.

kid identifies the sender's key used on the library. It has no meaning to the receiver during the game.

text is a hexadecimal 256 bit prime number, with the high bit on. Both clients will use this prime number when encrypting cards in this deck with the RSA Modulo a Prime algorithm.

set-sideboard cards:{%s %s %s ...} kid:%d
Sends this player's sideboard, just like the set-deck message. cards[0] becomes the next cid after the highest cid in the player's deck, and is put at the bottom of his sideboard zone. And so on.

encrypt-request zone:%d
Requests that the opponent send an "encrypt" message for the sender's library (or other zone). After sending this message, the GUI should "lock" the library. The player should not be allowed to modify his library until the "encrypt-library" message has been received.

encrypt pid:%d zone:%d kid:%d value:%d cards:{%s ...} kids:(%d ...)
Tells that the sender has re-encrypted and re-assigned the cards in the given player's library (or other zone).

value is 0 if the zone was not shuffled, and 1 if it was shuffled. The cid at the bottom of the library becomes cards[0], the cid second from the bottom becomes cards[1], and so on. The local (receiver's) key used on cards[x] is kids[x].

kid identifies the new sender's key used on the library.

shuffle zone:%d cids:(%d %d %d ...)
Tells the opponent that this is the new order for the cards in the sender's library (or other zone). If this method is used to shuffle a dirty library, then the game should inform the player that the library is not truly shuffled.

message text:%s
Sends a chat message to the opponent. The text is whatever was typed in the chat box (in UTF-8).

move-card cid:%d pid:%d zone:%d x:%d y:%d loc:%d card:%s
Tells that a card has been moved to a new zone.

cid identifies the card.
(pid, zone) identifies the card's new zone.
If the new zone is a playarea, then:
(x, y) are the card's location in the playarea. loc is 0 if the card is face-up, and 1 if face-down.
Else:
loc is 0 for the top of the new zone, and 1 for the bottom.

card, an optional parameter, is the card description, decrypted by the sender (but still encrypted by the receiver). It is important to specify this parameter when moving a card from your hand to a public zone. However, this parameter is unnecessary when moving a card between public zones.

move-card-request cid:%d pid:%d zone:%d x:%d y:%d loc:%d card:%s
Tells the opponent that this player wants to move a card, but that the card has not yet been moved. Typically, this message is used to move a card from the player's library, such as to draw a card.

The card parameter must be specified when the new zone is public, but it should not be specified when drawing a card.

To process this message, the opponent should move the card in its own game state, and then send a "move-card" message.

rearrange-zone cid:%d pid:%d zone:%d value:%d
Tells that the sender has moved a card within its zone.

(pid, zone) are technically unnecessary, since the receiver knows cid's current zone, but those arguments can be used to verify that the clients are in sync.

value is the card's new location in the zone. If the card was moved closer to the bottom, then it pushes other cards up, and if it was moved closer to the top, then it pushes other cards down.

set-card-value cid:%d key:%s value:%d
Sets a (key, value) pair associated with a card in a playarea. A card's (key, value) pairs are discarded when the card is moved out of the playarea.

send-ping
Requests that the opponent send a "return-ping" message.

return-ping
Tells that the client has received a "send-ping" message.

reveal-request pid:%d zone:%d size:%d
Tells the opponent to send a "reveal" message for the top "size" cards of the given player's given zone (usually his library).

reveal cids:{%d ...} cards:{%s ...}
This message may be sent at any time for any reason, but it will typically be used when a player needs to look at the top cards of some library, or look at the other player's hand. For each cid, this contains the card description, decrypted by the sender, but still encrypted by the receiver.

show-zone zone:%d size:%d
Tells the opponent to pop-up a dialog box displaying the top of this player's library or hand. The dialog box will be filled with the label "Unknown Card" unless the client sends the appropriate "reveal" message first.

select-request options:{%s ...} kid:%d value:%d
Requests that the opponent select one of the encrypted "option" strings. The client should inform the user that a random event had been requested, and then send a "select" message. When the client receives the "reveal-key" message, it can inform the user of the result of the random event.
kid is a cookie used to match the "select-request", "select", and "reveal-key" messages.
If value is 0, then it's a die-roll. Each option is two bytes: The first byte is a number from 1 to n, and the second byte is the same as the first byte, which acts as a checksum after the bytes are decrypted. The options are in random order. When the client receives the "reveal-key" message, it can verify that the correct options were available.
If value is 1, then it's a coin-flip. The options are "HEAD" and "TAIL", in random order.
If value is 2, then it's the purpose is to show a random card from the sender's hand. The options are the cids from the sender's hand, as 4-byte little-endian integers, encrypted and shuffled. When the sender receives the corresponding "select" message, it should decrypt the cid, and then send a "reveal" message before it sends the "reveal-key" message.

select option:%s kid:%d
Response to a "select-request" message.

reveal-key kid:%d key:%s
Reveals the encryption key that was used in the corresponding "select" message. When a client sends or receives this message, it should tell the user the result of the random event.

define-card name:%s pow:%s color:%d
Defines a card in the card database. The card name may then be used in "set-deck" messages, and, more likely, "add-card" messages.

add-card name:%s zone:%d
Assigns a new cid to a new card. The cid is the next highest number after the sender's deck, sideboard, and added cards. The card is added to the top of the specified zone. name is the unencrypted regular-length card name.

set-phase, next-turn, loaded-new-deck, inform-sideboard, etc.
Inform the other player that something interesting has happened.

request-new-game value:%d
If value is 1, then the player wants to start a new game. If it's 0, then the player has cancelled his request to reset the game. When a player receives value:1 while he wants to reset the game, or when he sends value:1 while the other player wants to reset the game, then he should reset his game state, and assume that the other player is doing the same. To reset the game, throw away all cards in all zones, all encryption keys and cid mappings, and all deck lists. Load a new deck, encrypt it with a new key, and send it to the other player. Wait for the other player to send his new deck, and then the new game can begin.