8.5 KiB
Game Flow
A game (identified with a game ID) is a pretty complex state machine, this document explains it, and how it can be observed contract-side and frontend-side.
Let's assume two players, A and B, where A is the game creator.
Note that the contracts allow for a third party to be the game creator, but the frontend assumes that the game creator will be one of the two players.
Table of Contents
Before the game starts
- A sends
createGame→ the game is assigned a gameID. - Both players concurrently join the game.
- A sends
joinGame- then A sends
drawInitialHand
- then A sends
- B sends
joinGame- then B sends
drawInitialHand
- then B sends
- A can send
cancelGameat any time before B sendsjoinGame. - Anyone (even 3rd parties) can send
timeoutif a player fails to senddrawInitialHandwithin 256 blocks of the last received transaction.- TODO: This is not good, should be within his own thing.
- A sends
- Once both
drawInitialHandhave been received, the game starts.
Note that it isn't possible to concede until the game starts, nor is it possible to cancel the game
after all joinGame transactions have been received. This needs to be fixed, as a non-joiner
can lock other players in place for 8 minutes right now.
Contract-side
Below we review how each successful transaction (== "received transaction") affects the game state and generates events.
-
createGamecauses theGameData(gdata) structure to be initialized.- The game creator specifies a number of players. The frontend assumes this to be 2.
- This value is assigned to
gdata.playersLeftToJoin.
- This value is assigned to
- The game creator is recorded in
gdata.gameCreator. gdata.lastBlockNumis set to the current block number.- This is used to check that
gdatais not uninitialized but matches a game that has been created viacreateGame. Note that we could usegdata.gameCreatorfor this instead. It's otherwise not necessary.
- This is used to check that
gdata.currentStepis set toGameStep.UNINITIALIZED.- All other values are left zeroed/uninitialized.
- [EVENT] The
GameCreated(gameID)event is emitted.
- The game creator specifies a number of players. The frontend assumes this to be 2.
-
Whenever
joinGameis received, the player is added to theinGamemapping.- He now can't shift cards in and out of his decks, create or join another game.
- The contracts permit a player to create as many games as he wants as long as he doesn't join any.
- Additionally, the player is added to the
playersarray of theGameData. gdata.playersLeftToJoinis reduced by 1.gdata.lastBlockNumis set to the current block number.- This is not used for randomness (
pdata.joinBlockNumis used for that), but it is useful to simplify the frontend code (TODO — actually make this unnecessary, OR explain why).
- This is not used for randomness (
- The player's deck cards are appended to
gdata.cards. - The player's
PlayerData(pdata) is created.pdata.healthis set toSTARTING_HEALTH.pdata.saltHashis set to the supplied value.pdata.joinBlockNumis set to the current block number.pdata.deckStartandpdata.deckEndare set to the start and exclusive-end indexes of the player's deck ingdata.cards.
- [EVENT] The
PlayerJoined(gameID, playerAddress)event is emitted. - [EVENT] If all players have joined, the
FullHouse(gameID)event is emitted.- Corresponding to
gdata.playersLeftToJoin == 0.
- Corresponding to
- He now can't shift cards in and out of his decks, create or join another game.
-
Whenever
drawInitialHandis receivedpdata.handRootandpdata.deckRootare set to the supplied values.pdata.handSizeis set toINITIAL_HAND_SIZE.- The player (represented by his index into
gdata.players) is addedgdata.livePlayers. - [EVENT] The
PlayerDrewHand(gameID, playerAddress)event is emitted. - [EVENT] If all players have drawn their hand, the
GameStarted(gameID)event is emitted.- Corresponding to
gdata.playersLeftToJoin == 0 && gdata.players.length == gdata.livePlayers.length. gdata.currentPlayeris set to a random index intogdata.players.- TODO: change the way this randomness is selected
gdata.currentStepis set toGameStep.PLAY.- It is now the current player's turn to play a card (the first player does not draw on his first turn).
- Corresponding to
-
Whenever
cancelGameis received- [EVENT] The
GameCancelled(gameID)event is emitted. - The game ends (see dedicated bullet).
- [EVENT] The
-
Whenever
timeoutis received- (only before the game starts:
gdata.currentStep == GameStep.UNINITIALIZED) - [EVENT] The
MissingPlayers(gameID)event is emitted. - The game ends (see dedicated bullet).
- (only before the game starts:
-
Whenever the game ends
gdata.currentStepis set toGameStep.ENDED.- Data may be cleared to reduce storage costs. What is safe to read:
gdata.currentStepgdata.lastBlockNum
- All players that are still in the game are removed from
inGame.
Let's now see how some of these values are used to perform contract-side checks
-
Checks:
inGameis used to check that a player is not already in a game.gdata.lastBlockNumis used to check that a gameID exists, i.e. the matchinggdatais not uninitialized but matches a game that has been created viacreateGame. It is also used to check for timeouts.gdata.playersis used to check whether a player already joined the game.gdata.livePlayersis used to check whether a player has already drawn his hand.gdata.playersLeftToJoinis used to check if the game has still space to join, or whether the game can still be cancelled.- In combination,
gdata.playersLeftToJoin == 0 && gdata.players.length == gdata.livePlayers.lengthis used to check whether the game can start. gdata.currentStepis used to check whether the game already started / ended.pdata.joinBlockNumbeing non-zero is used to check whether a player has already joined the game or not.pdata.handRootbeing non-zero is used to check whether a player has already drawn his hand or not.gdata.gameCreatoris used to check whether a player is the game creator.
Frontend-side
The frontend synchronizes with the chain by periodically pulling the whole GameData from the chain
(including all PlayerData).
From this, it extracts a player-specific GameStatus, which is one of:
- UNKNOWN
- Default value, for when we have no
gdatayet.
- Default value, for when we have no
- CREATED
- The game has been created, but
joinGamehasn't been received yet. gdata.currentStep == GameStep.UNINITIALIZEDandgdata.playersdoes not include the player.
- The game has been created, but
- JOINED
- The player's
joinGamehas been received, but notdrawInitialHand. gdata.currentStep == GameStep.UNINITIALIZED,gdata.playersincludes the player, butgdata.livePlayersdoes not.
- The player's
- HAND_DRAWN
- The player's
drawInitialHandhas been received, but the game hasn't started. gdata.currentStep == GameStep.UNINITIALIZEDandgdata.livePlayersincludes the player.
- The player's
- STARTED
- The game is ongoing.
GameStep.UNINITIALIZED < gdata.currentStep < GameStep.ENDED
- ENDED
- The game has ended (could be cancellation, timeout, or only one player left standing).
gdata.currentStep == GameStep.ENDED
Additionally, the frontend derives the following boolean properties:
isGameCreatorgdata.gameCreator == playerAddress
isGameJoinergdata.playersincludes the player, butgdata.gameCreator != playerAddress
allPlayersJoined- After all
joinGamehave been received, at which point the game can't be canceled by the creator anymore. gdata.playersLeftToJoin == 0
- After all
It also defines the function isGameReadyToStart(gameData, blockNumber), which is fed the currently
known game data and a block number. It returns true if the game is ready to start, i.e. if all
players have drawn their hands. This is used to control the transition to the play page.
The block number is used to modulate the check on the game data for the case here the block number
is the one at which we included our drawInitialHand proof, and the game data hasn't updated
accordingly yet.
TODO
- How are the various frontend pages driven by these values?
- Capture the state values that are not derived from the game data and the impact they have.
