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. Add yours at the end:
With the callbacks in place, it is time to code the expiration properly. 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
nextGame, found := k.GetNextGame(ctx)if!found {panic("NextGame not found")}
storedGameId := nextGame.FifoHead
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
Start with a loop breaking condition, if your cursor has reached the end of the FIFO:
Copy
if strings.Compare(storedGameId, types.NoFifoIdKey)==0{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, storedGameId)if!found {panic("Fifo head game not found "+ nextGame.FifoHead)}
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 come after anywaybreak} x checkers keeper end_block_server_game.go View source
If the game has expired, remove it from the FIFO:
Copy
k.RemoveFromFifo(ctx,&storedGame,&nextGame) 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 and save:
Copy
if storedGame.MoveCount <=1{// No point in keeping a game that was never really played
k.RemoveStoredGame(ctx, storedGameId)}else{
storedGame.Winner, found = opponents[storedGame.Turn]if!found {panic(fmt.Sprintf(types.ErrCannotFindWinnerByColor.Error(), storedGame.Turn))}
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 events aggregate in a single context. The context is not reset on a new transaction, so you have to take slices to compare what matters. One create adds 6 attributes, one play adds 7.
Avoid having games in the FIFO that expire in a day because of your earlier tests:
Copy
$ ignite chain serve --reset-once
Export your aliases again:
Copy
$ export alice=$(checkersd keys show alice -a)
$ export bob=$(checkersd keys show bob -a)
Create three games 1 minute apart. Have Bob play the middle one, and both Alice and Bob play the last one:
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:
Copy
$ checkersd tx checkers create-game --help
And at your account's current sequence. For instance: