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

# Events - Emitting Game Information

Make sure you have everything you need before proceeding:

In this section, you will:

  • Define event types.
  • Emit events.
  • Extend unit tests.

Now that you have added the possible actions, including their return values, use events to notify players. Your blockchain can now create and play games. However, it does not inform the outside world about this in a convenient way. That is where events come in - but what do you need to emit them?

Imagine a potential or current player waiting for their turn. It is not practical to look at all the transactions and search for the ones signifying the player's turn. It is better to listen to known events that let clients determine which player's turn it is.

Adding events to your application is as simple as:

  1. Defining the events you want to use.
  2. Emitting corresponding events as actions unfold.

# Some initial thoughts

Before you dive into the specifics of the exercise, ask yourself:

  • Why do actions warrant a detailed event?
  • What level of detail goes into each event?
  • How do you make it easy for external parties to understand your events?
  • At what stage do you emit events?

# Code needs

Now start by thinking about the following:

  • How do you adjust your code to do all this?
  • How would you unit-test these new elements?
  • How would you use Ignite CLI to locally run a one-node blockchain and interact with it via the CLI to see what you get?

Only focus on the narrow issue of event emission.

# Game-created event

Start with the event that announces the creation of a new game. The goal is to:

  • Inform the players about the game.
  • Make it easy for the players to find the relevant game.

Define new keys in x/checkers/types/keys.go:

Copy const ( StoredGameEventKey = "NewGameCreated" // Indicates what key to listen to StoredGameEventCreator = "Creator" StoredGameEventIndex = "Index" // What game is relevant StoredGameEventRed = "Red" // Is it relevant to me? StoredGameEventBlack = "Black" // Is it relevant to me? ) x checkers types keys.go View source

Emit the event in your handler file x/checkers/keeper/msg_server_create_game.go:

Copy ctx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, "checkers"), sdk.NewAttribute(sdk.AttributeKeyAction, types.StoredGameEventKey), sdk.NewAttribute(types.StoredGameEventCreator, msg.Creator), sdk.NewAttribute(types.StoredGameEventIndex, newIndex), sdk.NewAttribute(types.StoredGameEventRed, msg.Red), sdk.NewAttribute(types.StoredGameEventBlack, msg.Black), ), ) x checkers keeper msg_server_create_game.go View source

Now you must implement this correspondingly in the GUI, or include a server to listen for such events.

# Player-moved event

The created transaction to play a move informs the opponent about:

  • Which player is relevant.
  • Which game the move relates to.
  • When the move happened.
  • The move's outcome.
  • Whether the game was won.

Contrary to the create game event, which alerted the players about a new game, the players now know which game IDs to watch for. There is no need to repeat the players' addresses, the game ID is information enough.

You define new keys in x/checkers/types/keys.go similarly:

Copy const ( PlayMoveEventKey = "MovePlayed" PlayMoveEventCreator = "Creator" PlayMoveEventIdValue = "IdValue" PlayMoveEventCapturedX = "CapturedX" PlayMoveEventCapturedY = "CapturedY" PlayMoveEventWinner = "Winner" ) x checkers types keys.go View source

Emit the event in your file x/checkers/keeper/msg_server_play_move.go:

Copy ctx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, "checkers"), sdk.NewAttribute(sdk.AttributeKeyAction, types.PlayMoveEventKey), sdk.NewAttribute(types.PlayMoveEventCreator, msg.Creator), sdk.NewAttribute(types.PlayMoveEventIdValue, msg.IdValue), sdk.NewAttribute(types.PlayMoveEventCapturedX, strconv.FormatInt(int64(captured.X), 10)), sdk.NewAttribute(types.PlayMoveEventCapturedY, strconv.FormatInt(int64(captured.Y), 10)), sdk.NewAttribute(types.PlayMoveEventWinner, rules.PieceStrings[game.Winner()]), ), ) x checkers keeper msg_server_play_move.go View source

# Unit tests

The unit tests you have created so far still pass. However you also want to confirm that the events have been emitted in both situations. The events are recorded in the context, so the test is a little bit different. In msg_server_create_game_test.go, add this test:

Copy func TestCreate1GameEmitted(t *testing.T) { msgSrvr, _, context := setupMsgServerCreateGame(t) msgSrvr.CreateGame(context, &types.MsgCreateGame{ Creator: alice, Red: bob, Black: carol, }) ctx := sdk.UnwrapSDKContext(context) require.NotNil(t, ctx) events := sdk.StringifyEvents(ctx.EventManager().ABCIEvents()) require.Len(t, events, 1) event := events[0] require.EqualValues(t, sdk.StringEvent{ Type: "message", Attributes: []sdk.Attribute{ {Key: "module", Value: "checkers"}, {Key: "action", Value: "NewGameCreated"}, {Key: "Creator", Value: alice}, {Key: "Index", Value: "1"}, {Key: "Red", Value: bob}, {Key: "Black", Value: carol}, }, }, event) } x checkers keeper msg_server_create_game_test.go View source

How can you guess the order of elements? Easily, as you created them in this order. Alternatively, you can peek by using Visual Studio Code:

  1. Put a breakpoint after event := events[0].
  2. Run this test in debug mode: right-click the green arrow next to the test name.
  3. Observe the live values on the left.

The event emitted during a move may seem unexpected. In a move unit test, two actions occur: a create, and a move. However, in the setup of this test you do not create blocks but only hit your keeper. Therefore the context collects events but does not flush them. This is why you need to test only for the latter attributes, and verify an array slice that discards events that originate from the create action: event.Attributes[6:]. This gives the following test:

Copy func TestPlayMoveEmitted(t *testing.T) { msgServer, _, context := setupMsgServerWithOneGameForPlayMove(t) msgServer.PlayMove(context, &types.MsgPlayMove{ Creator: carol, IdValue: "1", FromX: 1, FromY: 2, ToX: 2, ToY: 3, }) ctx := sdk.UnwrapSDKContext(context) require.NotNil(t, ctx) 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: "-1"}, {Key: "CapturedY", Value: "-1"}, {Key: "Winner", Value: "*"}, }, event.Attributes[6:]) } x checkers keeper msg_server_play_move_test.go View source

# Interact with the CLI

Bob made a move. Will Alice's move emit an event?

Copy $ checkersd tx checkers play-move 1 0 5 1 4 --from $alice

The log is longer and not very readable, but the expected elements are present:

Copy ... raw_log: '[{"events":[{"type":"message","attributes":[{"key":"action","value":"PlayMove"},{"key":"module","value":"checkers"},{"key":"action","value":"MovePlayed"},{"key":"Creator","value":"cosmos1gml05nvlhr0k27unas8mj827z6m77lhfpzzr3l"},{"key":"IdValue","value":"0"},{"key":"CapturedX","value":"-1"},{"key":"CapturedY","value":"-1"},{"key":"Winner","value":"*"}]}]}]'

To parse the events and display them in a more user-friendly way, take the txhash again:

Copy $ checkersd query tx 531E5708A1EFBE08D14ABF947FBC888BFC69CD6F04A589D478204BF3BA891AB7 --output json | jq ".raw_log | fromjson"

This returns something like:

Copy [ { "events": [ { "type": "message", "attributes": [ { "key": "action", "value": "PlayMove" }, { "key": "module", "value": "checkers" }, { "key": "action", "value": "MovePlayed" }, { "key": "Creator", "value": "cosmos1r80ns8496ehe73dd70r3rnr07tk23mhu2wmw66" }, { "key": "IdValue", "value": "1" }, { "key": "CapturedX", "value": "-1" }, { "key": "CapturedY", "value": "-1" }, { "key": "Winner", "value": "*" } ] } ] } ]

As you can see, no pieces were captured. However, it turns out that Alice placed her piece ready to be captured by Bob:

Copy $ checkersd query checkers show-stored-game 1 --output json | jq ".StoredGame.game" | sed 's/"//g' | sed 's/|/\'$'\n/g'

Which prints:

Copy *b*b*b*b b*b*b*b* ***b*b*b **b***** *r****** **r*r*r* *r*r*r*r r*r*r*r*

The rules of the game included in this project mandate that the player captures a piece when possible. So Bob captures the piece:

Copy $ checkersd tx checkers play-move 1 2 3 0 5 --from $bob

This returns:

Copy ... raw_log: '[{"events":[{"type":"message","attributes":[{"key":"action","value":"PlayMove"},{"key":"module","value":"checkers"},{"key":"action","value":"MovePlayed"},{"key":"Creator","value":"cosmos1w0uumlj04eyvevhfawasm2dtjc24nexxygr8qx"},{"key":"IdValue","value":"0"},{"key":"CapturedX","value":"1"},{"key":"CapturedY","value":"4"},{"key":"Winner","value":"*"}]}]}]'

When formatted for clarity, you see the following::

Copy [ { "events": [ { "type": "message", "attributes": [ { "key": "action", "value": "PlayMove" }, { "key": "module", "value": "checkers" }, { "key": "action", "value": "MovePlayed" }, { "key": "Creator", "value": "cosmos1w0uumlj04eyvevhfawasm2dtjc24nexxygr8qx" }, { "key": "IdValue", "value": "1" }, { "key": "CapturedX", "value": "1" }, { "key": "CapturedY", "value": "4" }, { "key": "Winner", "value": "*" } ] } ] } ]

Correct: Bob captured a piece and the board now looks like this:

Copy *b*b*b*b b*b*b*b* ***b*b*b ******** ******** b*r*r*r* *r*r*r*r r*r*r*r*

This confirms that the play event is emitted as expected. You can do the same for the game created event.

# Next up

Time to add a third message to make it possible for a player to reject a game and to make your checkers blockchain more resistant to spam.