# Add a Way to Make a Move
Make sure you have all you need before proceeding:
- You understand the concepts of transactions, messages, and Protobuf.
- Go is installed.
- You have the checkers blockchain codebase with
MsgCreateGame
and its handling. If not, follow the previous steps or check out the relevant version (opens new window).
In this section, you will:
- Extend message handling - play the game.
- Handle moves and update the game state.
- Validate input.
- Extend unit tests.
Your blockchain can now create games, but can you play them? Not yet...so what do you need to make this possible?
# Some initial thoughts
Before diving into the exercise, take some time to think about the following questions:
- What goes into the message?
- How do you sanitize the inputs?
- How do you unequivocally identify games?
- How do you report back errors?
- How do you use your files that implement the checkers rules?
- How do you make sure that nothing is lost?
# Code needs
When it comes to the code you need, ask yourself:
- What Ignite CLI commands will create your message?
- How do you adjust what Ignite CLI created for you?
- 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?
As before, do not bother yet with niceties like gas metering or event emission.
To play a game a player only needs to specify:
- The ID of the game the player wants to join. Call the field
gameIndex
. - The initial positions of the pawn. Call the fields
fromX
andfromY
and make themuint
. - The final position of the pawn after a player's move. Call the fields
toX
andtoY
to beuint
too.
The player does not need to be explicitly added as a field in the message because the player is implicitly the signer of the message. Name the object PlayMove
.
Unlike when creating the game, you want to return:
- The captured piece, if any. Call the fields
capturedX
andcapturedY
. Make thenint
so that you can pass-1
when no pieces have been captured. - The (potential) winner in the field
winner
.
# With Ignite CLI
Ignite CLI can create the message and the response objects with a single command:
Ignite CLI once more creates all the necessary Protobuf files and boilerplate for you. See tx.proto
:
All you have to do is fill in the needed parts:
In
x/checkers/types/message_play_move.go
:And in
x/checkers/keeper/msg_server_play_move.go
:Where the
TODO
is replaced as per the following.
# The message basic validation
With a game index and board positions, there are a number of stateless error situations that can be detected:
- You know there will not be a game index at the value given.
- A piece position is out of the bounds of the board.
from
andto
are identical.
Declare your new errors:
Then you can check that:
The game index is reasonable:
The positions are within bounds, checking an array of situations:
Yes, a
uint64
likemsg.FromY
can never be< 0
, but since there is no compilation warning you can keep it for future reference if the type changes.There is an actual move:
It is conceivable, perhaps even for the benefit of players, to add more stateless checks. For instance, to detect when playing out of wrong cells; after all, only half the cells are valid. Or to detect when moves are not along a diagonal.
These are all worthy checks, although they tend to distract from learning about Cosmos SDK.
If you are nonetheless interested, a good place to look at for a start is the rules file (opens new window).
# The move handling
The rules
represent the ready-made file containing the rules of the game you imported earlier. Declare your new errors in x/checkers/types/errors.go
, given your code has to handle new error situations:
Take the following steps to replace the TODO
:
Fetch the stored game information using the
Keeper.GetStoredGame
(opens new window) function created by Ignite CLI:You return an error because this is a player mistake.
Is the player legitimate? Check with:
This uses the certainty that the
MsgPlayMove.Creator
has been verified by its signature (opens new window).Instantiate the board in order to implement the rules:
Fortunately you previously created this helper (opens new window). Here you
panic
because if the game cannot be parsed the cause may be database corruption.Is it the player's turn? Check using the rules file's own
TurnIs
(opens new window) function:Properly conduct the move, using the rules'
Move
(opens new window) function:Prepare the updated board to be stored and store the information:
This updates the fields that were modified using the
Keeper.SetStoredGame
(opens new window) function, as when you created and saved the game.Return relevant information regarding the move's result:
The
Captured
andWinner
information would be lost if you did not get it out of the function one way or another. More accurately, one would have to replay the transaction to discover the values. It is best to make this information easily accessible.
This completes the move process, facilitated by good preparation and the use of Ignite CLI.
# Unit tests
Adding unit tests for this play message is very similar to what you did for the previous message.
# On the message
Adjust and add to types/message_play_move_test.go
. First, change its package for consistency:
Then adjust and add to the test cases:
You can try these tests:
# On the keeper
Create a new keeper/msg_server_play_move_test.go
file and declare it as package keeper_test
. Start with a function that conveniently sets up the keeper for the tests. In this case, already having a game saved can reduce several lines of code in each test:
Note that it reuses alice
, bob
and carol
found in the file msg_server_create_game_test.go
(opens new window) of the same package.
Now test the result of a move. Blacks play first, which according to setupMsgServerWithOneGameForPlayMove
corresponds to bob
:
Also test whether the game was saved correctly (opens new window). Check what happens when the game cannot be found (opens new window), the sender is not a player (opens new window), a player tries to play out of turn (opens new window), or makes a wrong move (opens new window). Check after two (opens new window) or three turns with a capture (opens new window).
As a special case, add a test to check what happens when a board is not parseable, which is expected to end up in a panic
, not with a returned error:
Note the use of defer
(opens new window), which can be used as a Go way of implementing try catch
of panics. The defer
statement is set up right before the msgServer.PlayMove
statement that is expected to fail, so that it does not catch panics that may happen earlier.
Try these tests:
# Interact via the CLI
Start your chain again:
If you restarted from the previous section, there is already one game in storage and it is waiting for Alice's move. If that is not the case, recreate a game via the CLI.
# Bob plays out of turn
Can Bob make a move? Look at the play-move
message and which parameters it expects:
This returns:
So Bob tries:
After you accept sending the transaction, it should complain with the result including:
If you did not get this raw_log
, your transaction may have been sent asynchronously. You can always query a transaction by using the txhash
with the following command:
And you are back on track:
This error by Bob was caught when he tried to play out of turn. The check was a stateful check as the message itself was valid. This failure cost him gas.
# Alice plays a wrong move
Can Alice, who plays black, make a move? Can she make a wrong move? There are two kinds of wrong moves that Alice can make: she can make one whose wrongness will be caught statelessly, and another that will be caught because of the current state of the board.
As an example of a statelessly wrong move, she could try to take a piece on the side and move it just outside the board:
The computer says "no" immediately:
The transaction never went into the mem pool. This mistake did not cost Alice any gas.
As an example of a statefully wrong move, Alice can try to move from
0-1
to1-0
, which is occupied by one of her pieces.The computer says "no" again, but this time after the transaction has been validated:
This mistake cost Alice some gas.
So far all seems to be working.
# Alice plays correctly
Time for Alice to make a correct move:
This returns:
Confirm the move went through with your one-line formatter from the previous section:
This shows:
Alice's piece moved down and right.
When you are done with this exercise you can stop Ignite's chain serve
.
To summarize, this section has explored:
- How to add stateless checks on your message.
- How to use messages and handlers, in this case to add the capability of actually playing moves on checkers games created in your application.
- The information that needs to be specified for a game move message to function, which are the game ID, the initial positions of the pawn to be moved, and the final positions of the pawn at the end of the move.
- The information necessary to return, which includes the game ID, the location of any captured piece, and the registration of a winner should the game be won as a result of the move.
- How to modify the response object created by Ignite CLI to add additional fields.
- How to implement and check the steps required by move handling, including the declaration of the ready-made rules in the errors.go file so your code can handle new error situations.
- How to add unit tests to check the functionality of your code.
- How to interact via the CLI to confirm that correct player turn order is enforced by the application.