v1-content
?
You are viewing an older version of the content, click here to switch to the current version

# Store Field - Record the Game Winner

Make sure you have everything you need before proceeding:

In this section, you will:

  • Check for a game winner.
  • Extend unit tests.

To be able to terminate games, you need to discern between games that are current and those that have reached an end - for example, when they have been won. Therefore a good field to add is for the winner. It needs to contain:

  • The winner of a game that reaches completion.
  • Or the winner by forfeit when a game is expired.
  • Or a neutral value when the game is active.

In this exercise a draw is not handled and it would require yet another value to save in winner.

# New information

In the StoredGame Protobuf definition file:

Copy message StoredGame { ... string winner = 11; } proto checkers stored_game.proto View source

Have Ignite CLI and Protobuf recompile this file:

Copy $ ignite generate proto-go

Add a helper function to get the winner's address, if it exists. A location is in full_game.go:

Copy func (storedGame *StoredGame) GetPlayerAddress(color string) (address sdk.AccAddress, found bool, err error) { red, err := storedGame.GetRedAddress() if err != nil { return nil, false, err } black, err := storedGame.GetBlackAddress() if err != nil { return nil, false, err } address, found = map[string]sdk.AccAddress{ rules.RED_PLAYER.Color: red, rules.BLACK_PLAYER.Color: black, }[color] return address, found, nil } func (storedGame *StoredGame) GetWinnerAddress() (address sdk.AccAddress, found bool, err error) { address, found, err = storedGame.GetPlayerAddress(storedGame.Winner) return address, found, err } x checkers types full_game.go View source

# Update and check for the winner

This is a two-part update. You set the winner where relevant, but you also introduce new checks so that a game with a winner cannot be acted upon.

Start with a new error that you define as a constant:

Copy ErrGameFinished = sdkerrors.Register(ModuleName, 1111, "game is already finished") x checkers types errors.go View source

At creation, in the create game message handler, start with a neutral value:

Copy ... storedGame := types.StoredGame{ ... Winner: rules.PieceStrings[rules.NO_PLAYER], } x checkers keeper msg_server_create_game.go View source

With further checks when handling a play in the handler:

  1. Check that the game has not finished yet:

    Copy if storedGame.Winner != rules.PieceStrings[rules.NO_PLAYER] { return nil, types.ErrGameFinished } x checkers keeper msg_server_play_move.go View source
  2. Update the winner field, which remains neutral (opens new window) if there is no winner yet:

    Copy storedGame.Winner = rules.PieceStrings[game.Winner()] x checkers keeper msg_server_play_move.go View source
  3. Handle the FIFO differently depending on whether the game is finished or not:

    Copy if storedGame.Winner == rules.PieceStrings[rules.NO_PLAYER] { k.Keeper.SendToFifoTail(ctx, &storedGame, &nextGame) } else { k.Keeper.RemoveFromFifo(ctx, &storedGame, &nextGame) } x checkers keeper msg_server_play_move.go View source

And when rejecting a game, in its handler:

Copy if storedGame.Winner != rules.PieceStrings[rules.NO_PLAYER] { return nil, types.ErrGameFinished } x checkers keeper msg_server_reject_game.go View source

Confirm the code compiles, add unit tests, and you are ready to handle the expiration of games.

# Unit tests

You need to update your existing tests so that they pass with a new Winner value. Most of your tests need to add this line:

Copy require.EqualValues(t, types.StoredGame{ ... Winner: "*", }, game1) x checkers keeper msg_server_play_move_fifo_test.go View source

This means that in your tests no games have reached a conclusion with a winner. Time to fix that. In a dedicated msg_server_play_move_winner_test.go file, prepare all the moves that will be played in the test. For convenience, a move will be written as:

Copy type GameMoveTest struct { player string fromX uint64 fromY uint64 toX uint64 toY uint64 } x checkers keeper msg_server_play_move_winner_test.go View source

If you do not want to create a complete game yourself, you can choose this one:

Copy var ( game1moves = []GameMoveTest{ {"b", 1, 2, 2, 3}, // "*b*b*b*b|b*b*b*b*|***b*b*b|**b*****|********|r*r*r*r*|*r*r*r*r|r*r*r*r*" {"r", 0, 5, 1, 4}, // "*b*b*b*b|b*b*b*b*|***b*b*b|**b*****|*r******|**r*r*r*|*r*r*r*r|r*r*r*r*" {"b", 2, 3, 0, 5}, // "*b*b*b*b|b*b*b*b*|***b*b*b|********|********|b*r*r*r*|*r*r*r*r|r*r*r*r*" ... {"r", 3, 6, 2, 5}, // "*b*b****|**b*b***|*****b**|********|********|**r*****|*B***b**|********" {"b", 1, 6, 3, 4}, // "*b*b****|**b*b***|*****b**|********|***B****|********|*****b**|********" } ) x checkers keeper msg_server_play_move_winner_test.go View source

You may want to add a small function that converts "b" and "r" into their respective player addresses:

Copy func getPlayer(color string) string { if color == "b" { return carol } return bob } x checkers keeper msg_server_play_move_winner_test.go View source

Now create the test that plays all the moves, and checks at the end that the game has been saved with the right winner and that the FIFO is empty again:

Copy func TestPlayMoveUpToWinner(t *testing.T) { msgServer, keeper, context := setupMsgServerWithOneGameForPlayMove(t) ctx := sdk.UnwrapSDKContext(context) for _, move := range game1moves { _, err := msgServer.PlayMove(context, &types.MsgPlayMove{ Creator: getPlayer(move.player), IdValue: "1", FromX: move.fromX, FromY: move.fromY, ToX: move.toX, ToY: move.toY, }) require.Nil(t, err) } nextGame, found := keeper.GetNextGame(ctx) require.True(t, found) require.EqualValues(t, types.NextGame{ Creator: "", IdValue: 2, FifoHead: "-1", FifoTail: "-1", }, nextGame) game1, found1 := keeper.GetStoredGame(ctx, "1") require.True(t, found1) require.EqualValues(t, types.StoredGame{ Creator: alice, Index: "1", Game: "*b*b****|**b*b***|*****b**|********|***B****|********|*****b**|********", Turn: "b", Red: bob, Black: carol, MoveCount: uint64(len(game1moves)), BeforeId: "-1", AfterId: "-1", Deadline: types.FormatDeadline(ctx.BlockTime().Add(types.MaxTurnDuration)), Winner: "b", }, game1) events := sdk.StringifyEvents(ctx.EventManager().ABCIEvents()) require.Len(t, events, 1) event := events[0] require.Equal(t, event.Type, "message") require.EqualValues(t, []sdk.Attribute{ {Key: "module", Value: "checkers"}, {Key: "action", Value: "MovePlayed"}, {Key: "Creator", Value: carol}, {Key: "IdValue", Value: "1"}, {Key: "CapturedX", Value: "2"}, {Key: "CapturedY", Value: "5"}, {Key: "Winner", Value: "b"}, }, event.Attributes[6+39*7:]) } x checkers keeper msg_server_play_move_winner_test.go View source

Feel free to create another game won by the red player.

# Interact via the CLI

If you have created games in an earlier version of the code, you are now in a broken state. You cannot even play the old games because they have .Winner == "" and this will be caught by the if storedGame.Winner != rules.PieceStrings[rules.NO_PLAYER] test. Start again:

Copy $ ignite chain serve --reset-once

Do not forget to export alice and bob again, as explained in an earlier section.

Confirm that there is no winner for a game when it is created:

Copy $ checkersd tx checkers create-game $alice $bob --from $alice $ checkersd query checkers show-stored-game 1

This should show:

Copy ... winner: "*" ...

And when a player plays:

Copy $ checkersd tx checkers play-move 1 1 2 2 3 --from $bob $ checkersd query checkers show-stored-game 1

Testing with the CLI up to the point where the game is resolved with a rightful winner is better covered by unit tests or with a nice GUI. You will be able to partially test this in the next section, via a forfeit.

# Next up

You have introduced a game FIFO, a game deadline, and a game winner. Time to turn your attention to the next section to look into game forfeits.