# Emit 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 ( GameCreatedEventType = "new-game-created" // Indicates what event type to listen to GameCreatedEventCreator = "creator" // Subsidiary information GameCreatedEventGameIndex = "game-index" // What game is relevant GameCreatedEventBlack = "black" // Is it relevant to me? GameCreatedEventRed = "red" // 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(types.GameCreatedEventType, sdk.NewAttribute(types.GameCreatedEventCreator, msg.Creator), sdk.NewAttribute(types.GameCreatedEventGameIndex, newIndex), sdk.NewAttribute(types.GameCreatedEventBlack, msg.Black), sdk.NewAttribute(types.GameCreatedEventRed, msg.Red), ), ) 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 ( MovePlayedEventType = "move-played" MovePlayedEventCreator = "creator" MovePlayedEventGameIndex = "game-index" MovePlayedEventCapturedX = "captured-x" MovePlayedEventCapturedY = "captured-y" MovePlayedEventWinner = "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(types.MovePlayedEventType, sdk.NewAttribute(types.MovePlayedEventCreator, msg.Creator), sdk.NewAttribute(types.MovePlayedEventGameIndex, msg.GameIndex), sdk.NewAttribute(types.MovePlayedEventCapturedX, strconv.FormatInt(int64(captured.X), 10)), sdk.NewAttribute(types.MovePlayedEventCapturedY, strconv.FormatInt(int64(captured.Y), 10)), sdk.NewAttribute(types.MovePlayedEventWinner, 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, Black: bob, Red: 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: "new-game-created", Attributes: []sdk.Attribute{ {Key: "creator", Value: alice}, {Key: "game-index", Value: "1"}, {Key: "black", Value: bob}, {Key: "red", 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 on the line 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.

As for the events emitted during the play move test, there are two of them: one for the creation and the other for the play. Because this is a unit test and each action is not isolated into individual transactions, the context collects all events emitted during the test. It just so happens that the context prepends them - the newest one is at index 0. Which is why, when you fetch them, the play event is at events[0].

Copy func TestPlayMoveEmitted(t *testing.T) { msgServer, _, context := setupMsgServerWithOneGameForPlayMove(t) msgServer.PlayMove(context, &types.MsgPlayMove{ Creator: bob, GameIndex: "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, 2) event := events[0] require.EqualValues(t, sdk.StringEvent{ Type: "move-played", Attributes: []sdk.Attribute{ {Key: "creator", Value: bob}, {Key: "game-index", Value: "1"}, {Key: "captured-x", Value: "-1"}, {Key: "captured-y", Value: "-1"}, {Key: "winner", Value: "*"}, }, }, event) } x checkers keeper msg_server_play_move_test.go View source

When two players play one after the other, the context collates the attributes of move-played all together in a single array in an appending fashion, with the older attributes at the lower indices, starting at 0. For instance, you have to rely on array slices like event.Attributes[5:] to test the attributes of the second move-played event:

Copy func TestPlayMove2Emitted(t *testing.T) { msgServer, _, context := setupMsgServerWithOneGameForPlayMove(t) msgServer.PlayMove(context, &types.MsgPlayMove{ Creator: bob, GameIndex: "1", FromX: 1, FromY: 2, ToX: 2, ToY: 3, }) msgServer.PlayMove(context, &types.MsgPlayMove{ Creator: carol, GameIndex: "1", FromX: 0, FromY: 5, ToX: 1, ToY: 4, }) ctx := sdk.UnwrapSDKContext(context) require.NotNil(t, ctx) events := sdk.StringifyEvents(ctx.EventManager().ABCIEvents()) require.Len(t, events, 2) event := events[0] require.Equal(t, "move-played", event.Type) require.EqualValues(t, []sdk.Attribute{ {Key: "creator", Value: carol}, {Key: "game-index", Value: "1"}, {Key: "captured-x", Value: "-1"}, {Key: "captured-y", Value: "-1"}, {Key: "winner", Value: "*"}, }, event.Attributes[5:]) } x checkers keeper msg_server_play_move_test.go View source

Try these tests:

# Interact with the CLI

If you did not do it already, start your chain with Ignite.

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

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

Copy ... raw_log: '[{"events":[{"type":"message","attributes":[{"key":"action","value":"play_move"}]},{"type":"move-played","attributes":[{"key":"creator","value":"cosmos1xf6s64kaw7at7um8lnwj65vadxqr6hnyhr9v83"},{"key":"game-index","value":"1"},{"key":"captured-x","value":"-1"},{"key":"captured-y","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": "play_move" } ] }, { "type": "move-played", "attributes": [ { "key": "creator", "value": "cosmos1xf6s64kaw7at7um8lnwj65vadxqr6hnyhr9v83" }, { "key": "game-index", "value": "1" }, { "key": "captured-x", "value": "-1" }, { "key": "captured-y", "value": "-1" }, { "key": "winner", "value": "*" } ] } ] } ]

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

Which prints:

Copy *b*b*b*b b*b*b*b* ***b*b*b **b***** *r****** <-- Ready to be captured **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 Alice captures the piece:

This returns:

Copy ... raw_log: '[{"events":[{"type":"message","attributes":[{"key":"action","value":"play_move"}]},{"type":"move-played","attributes":[{"key":"creator","value":"cosmos1qxeu0aclpl45429aeveh3t4e7y9ghr22r5d9r2"},{"key":"game-index","value":"1"},{"key":"captured-x","value":"1"},{"key":"captured-y","value":"4"},{"key":"winner","value":"*"}]}]}]'

When formatted for clarity, you see the following::

Copy [ { "events": [ { "type": "message", "attributes": [ { "key": "action", "value": "play_move" } ] }, { "type": "move-played", "attributes": [ { "key": "creator", "value": "cosmos1qxeu0aclpl45429aeveh3t4e7y9ghr22r5d9r2" }, { "key": "game-index", "value": "1" }, { "key": "captured-x", "value": "1" }, { "key": "captured-y", "value": "4" }, { "key": "winner", "value": "*" } ] } ] } ]

Correct: Alice 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 confirm the same for the game created event.

When you are done with this exercise you can stop Ignite's chain serve.


To summarize, this section has explored:

  • How to define event types and then emit events to cause the UI to notify players of game actions as they occur, such as creating games and playing moves.
  • How listening to known events which let clients determine which player must move next is better than the impractical alternative of examining all transactions to search for the ones which signify a player's turn.
  • How to define a Game-created event that will notify the participating players and make it easy for them to find the game.
  • How to define a Player-moved event that will indicate which player and game is involved, when the move occurred, the move's outcome, and whether the game was won as a result.
  • How to test your code to ensure that it functions as desired.
  • How to interact with the CLI to check the effectiveness of an emitted event.