# Auto-Expiring Games
Make sure you have everything you need before proceeding:
- You understand the concepts of ABCI.
- Go is installed.
- You have the checkers blockchain codebase with the elements necessary for forfeit. If not, follow the previous steps or check out the relevant version (opens new window).
In this section, you will:
- Do begin block and end block operations.
- Forfeit games automatically.
- Do garbage collection.
In the previous section you prepared the expiration of games:
- A First-In-First-Out (FIFO) that always has old games at its head and freshly updated games at its tail.
- A deadline field to guide the expiration.
- A winner field to further assist with forfeiting.
- A move count field to inform the action to take when forfeiting.
# New information
A game expires in two different situations:
- It was never really played, so it is removed quietly. That includes a single move by a single player.
- Moves were played by both players, making it a proper game, and forfeit is the outcome because a player then failed to play a move in time.
In the latter case, you want to emit a new event which differentiates forfeiting a game from a win involving a move. Therefore you define new error constants:
# Putting callbacks in place
When you use Ignite CLI to scaffold your module, it creates the x/checkers/module.go
(opens new window) file with a lot of functions to accommodate your application. In particular, the function that may be called on your module on EndBlock
is named EndBlock
:
Ignite CLI left this empty. It is here that you add what you need done right before the block gets sealed. Create a new file named x/checkers/keeper/end_block_server_game.go
to encapsulate the knowledge about game expiry. Leave your function empty for now:
In x/checkers/module.go
update EndBlock
with:
This ensures that if your module's EndBlock
function is called the expired games will be handled. For the whole application to call your module you have to instruct it to do so. This takes place in app/app.go
, where the application is initialized with the proper order to call the EndBlock
functions in different modules. In fact, yours has already been placed at the end by Ignite:
Your ForfeitExpiredGames
function will now be called at the end of each block.
Also prepare a new error:
# Expire games handler
With the callbacks in place, it is time to code the expiration properly.
# Prepare the main loop
In ForfeitExpiredGames
, it is a matter of looping through the FIFO, starting from the head, and handling games that are expired. You can stop at the first active game, as all those that come after are also active thanks to the careful updating of the FIFO.
Prepare useful information:
Initialize the parameters before entering the loop:
Enter the loop:
See below for what replaces this
TODO
.After the loop has ended do not forget to save the latest FIFO state:
So what goes in the for { TODO }
?
# Identify an expired game
Start with a loop breaking condition, if your cursor has reached the end of the FIFO:
Fetch the expired game candidate and its deadline:
Test for expiration:
Now, what goes into this if "expired" { TODO }
?
# Handle an expired game
If the game has expired, remove it from the FIFO:
Check whether the game is worth keeping. If it is, set the winner as the opponent of the player whose turn it is, remove the board, and save:
Emit the relevant event:
Move along the FIFO for the next run of the loop:
For an explanation as to why this setup is resistant to an attack from an unbounded number of expired games, see the section on the game's FIFO.
# Unit tests
How do you test something that is supposed to happen during the EndBlock
event? You call the function that will be called within EndBlock
(i.e. Keeper.ForfeitExpiredGames
). Create a new test file end_block_server_game_test.go
for your tests. The situations that you can test are:
A game was never played, while alone in the state or not (opens new window). Or two games (opens new window) were never played. In this case, you need to confirm that the game was fully deleted, and that an event was emitted with no winners:
A game was played with only one move, while alone in the state or not (opens new window). Or two games (opens new window) were played in this way. In this case, you need to confirm that the game was fully deleted, and that an event was emitted with no winners:
A game was played with at least two moves, while alone in the state or not (opens new window). Or two games (opens new window) were played in this way. In this case, you need to confirm the game was not deleted, and instead that a winner was announced, including in events:
Note how all the attributes of an event of a given type (such as "game-forfeited"
) aggregate in a single array. The context is not reset on a new transaction, so when testing attributes you either have to compare the full array or take slices to compare what matters.
# Interact via the CLI
Currently, the game expiry is one day in the future. This is too long to test with the CLI. Temporarily set it to 5 minutes:
Avoid having games in the FIFO that expire in a day because of your earlier tests:
Export your aliases again:
Create three games one minute apart. Have Alice play the middle one, and both Alice and Bob play the last one:
First game:
Wait a minute, then create your second game and play it:
Wait another minute, then create your third game and play on it:
Space each tx
command from a given account by a couple of seconds so that they each go into a different block - by default checkersd
is limited because it uses the account's transaction sequence number by fetching it from the current state.
If you want to overcome this limitation, look at checkersd
's --sequence
flag:
And at your account's current sequence. For instance:
Which returns something like:
With three games in, confirm that you see them all:
List them again after two, three, four, and five minutes. You should see games 1
and 2
disappear, and game 3
being forfeited by Alice, i.e. red
Bob wins:
This prints:
Confirm that the FIFO no longer references the removed games nor the forfeited game:
This should show:
To summarize, this section has explored:
- How games can expire under two conditions: when the game never really begins or only one player makes an opening move, in which case it is removed; or when both players have participated but one has since failed to play a move in time, in which case the game is forfeited.
- What new information and functions need to be created, and to update
EndBlock
to call theForfeitExpiredGames
function at the end of each block. - The correct coding for how to prepare the main loop through the FIFO, identify an expired game, and handle an expired game.
- How to test your code to ensure that it functions as desired.
- How to interact with the CLI to check the effectiveness of your code for handling expired games.