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:
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:
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:
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.
Initialize the parameters before entering the loop:
Copy
systemInfo, found := k.GetSystemInfo(ctx)if!found {panic("SystemInfo not found")}
gameIndex := systemInfo.FifoHeadIndex
var storedGame types.StoredGame
x checkers keeper end_block_server_game.go View source
Enter the loop:
Copy
for{// TODO} x checkers keeper end_block_server_game.go View source
See below for what replaces this TODO.
After the loop has ended do not forget to save the latest FIFO state:
Copy
k.SetSystemInfo(ctx, systemInfo) x checkers keeper end_block_server_game.go View source
Start with a loop breaking condition, if your cursor has reached the end of the FIFO:
Copy
if gameIndex == types.NoFifoIndex {break} x checkers keeper end_block_server_game.go View source
Fetch the expired game candidate and its deadline:
Copy
storedGame, found = k.GetStoredGame(ctx, gameIndex)if!found {panic("Fifo head game not found "+ systemInfo.FifoHeadIndex)}
deadline, err := storedGame.GetDeadlineAsTime()if err !=nil{panic(err)} x checkers keeper end_block_server_game.go View source
Test for expiration:
Copy
if deadline.Before(ctx.BlockTime()){// TODO}else{// All other games after are active anywaybreak} x checkers keeper end_block_server_game.go View source
Copy
k.RemoveFromFifo(ctx,&storedGame,&systemInfo) x checkers keeper end_block_server_game.go View source
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:
Copy
lastBoard := storedGame.Board
if storedGame.MoveCount <=1{// No point in keeping a game that was never really played
k.RemoveStoredGame(ctx, gameIndex)}else{
storedGame.Winner, found = opponents[storedGame.Turn]if!found {panic(fmt.Sprintf(types.ErrCannotFindWinnerByColor.Error(), storedGame.Turn))}
storedGame.Board =""
k.SetStoredGame(ctx, storedGame)} x checkers keeper end_block_server_game.go View source
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.
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:
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:
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 the ForfeitExpiredGames 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.