# The Expired Game Elements

synopsis

Make sure you have all you need before proceeding:

In the previous section you prepared 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.

# New information

An expired game will expire in two different cases:

  1. It was never really played on so it is removed quietly.
  2. It was played on, making it a proper game, and forfeit is the outcome because a player failed to play 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:

Copy const ( ForfeitGameEventKey = "GameForfeited" ForfeitGameEventIdValue = "IdValue" ForfeitGameEventWinner = "Winner" ) x checkers types keys.go View source

# Putting callbacks in place

When you use Starport 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:

Copy func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } x checkers module.go View source

Starport left it empty. It is here that you add what you need to see done, right before the block gets sealed. Create a brand new file named x/checkers/keeper/end_block_server_game.go to encapsulate the knowledge about game expiry. Leave your function empty for now:

Copy func (k Keeper) ForfeitExpiredGames(goCtx context.Context) { // TODO } x checkers keeper end_block_server_game.go View source

In x/checkers/module.go you can update EndBlock with:

Copy func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { am.keeper.ForfeitExpiredGames(sdk.WrapSDKContext(ctx)) return []abci.ValidatorUpdate{} } x checkers module.go View source

With this you ensure that if your module's EndBlock function is called, the expired games will be handled. For the whole application 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:

Copy app.mm.SetOrderEndBlockers(crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, checkersmoduletypes.ModuleName) app app.go View source

Your ForfeitExpiredGames function will now be called at the end of each block.

# Expire games handler

With the callbacks in place it is time to code the expiration properly. In ForfeitExpiredGames, it is simply 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.

  1. Prepare useful information:

    Copy ctx := sdk.UnwrapSDKContext(goCtx) opponents := map[string]string{ rules.BLACK_PLAYER.Color: rules.RED_PLAYER.Color, rules.RED_PLAYER.Color: rules.BLACK_PLAYER.Color, } x checkers keeper end_block_server_game.go View source
  2. 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
  3. Enter the loop:

    Copy for { // TODO } x checkers keeper end_block_server_game.go View source
    1. Start with the loop breaking condition:

      Copy if strings.Compare(storedGameId, types.NoFifoIdKey) == 0 { break } x checkers keeper end_block_server_game.go View source
    2. 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
    3. Test for expiration:

      Copy if deadline.Before(ctx.BlockTime()) { // TODO } else { // All other games come after anyway break } 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
      • Then 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 == 0 { storedGame.Winner = rules.NO_PLAYER.Color // No point in keeping a game that was never 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
      • Emit the relevant event:

        Copy ctx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(sdk.AttributeKeyAction, types.ForfeitGameEventKey), sdk.NewAttribute(types.ForfeitGameEventIdValue, storedGameId), sdk.NewAttribute(types.ForfeitGameEventWinner, storedGame.Winner), ), ) x checkers keeper end_block_server_game.go View source
      • Move along the FIFO for the next run of the loop:

        Copy storedGameId = nextGame.FifoHead x checkers keeper end_block_server_game.go View source
  4. After the loop has ended, do not forget to save the latest FIFO state:

    Copy k.SetNextGame(ctx, nextGame) x checkers keeper end_block_server_game.go View source

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.

# Next up

The next section introduces token wagers.