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 and fromY and make them uint.
The final position of the pawn after a player's move. Call the fields toX and toY to be uint 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 and capturedY. Make then int so that you can pass -1 when no pieces have been captured.
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 and to are identical.
Declare your new errors:
Copy
var(...+ ErrInvalidGameIndex = sdkerrors.Register(ModuleName,1103,"game index is invalid")+ ErrInvalidPositionIndex = sdkerrors.Register(ModuleName,1104,"position index is invalid")+ ErrMoveAbsent = sdkerrors.Register(ModuleName,1105,"there is no move")) x checkers types errors.go View source
The positions are within bounds, checking an array of situations:
Copy
boardChecks :=[]struct{
value uint64
err string}{{
value: msg.FromX,
err:"fromX out of range (%d)",},{
value: msg.ToX,
err:"toX out of range (%d)",},{
value: msg.FromY,
err:"fromY out of range (%d)",},{
value: msg.ToY,
err:"toY out of range (%d)",},}for_, situation :=range boardChecks {if situation.value <0|| rules.BOARD_DIM <= situation.value {return sdkerrors.Wrapf(ErrInvalidPositionIndex, situation.err, situation.value)}} x checkers types message_play_move.go View source
Yes, a uint64 like msg.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:
Copy
if msg.FromX == msg.ToX && msg.FromY == msg.ToY {return sdkerrors.Wrapf(ErrMoveAbsent,"x (%d) and y (%d)", msg.FromX, msg.FromY)} x checkers types message_play_move.go View source
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.
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:
Copy
var(...+ ErrGameNotFound = sdkerrors.Register(ModuleName,1106,"game by id not found")+ ErrCreatorNotPlayer = sdkerrors.Register(ModuleName,1107,"message creator is not a player")+ ErrNotPlayerTurn = sdkerrors.Register(ModuleName,1108,"player tried to play out of turn")+ ErrWrongMove = sdkerrors.Register(ModuleName,1109,"wrong move")) x checkers types errors.go View source
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.
The Captured and Winner 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.
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:
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:
Copy
funcTestPlayMoveCannotParseGame(t *testing.T){
msgServer, k, context :=setupMsgServerWithOneGameForPlayMove(t)
ctx := sdk.UnwrapSDKContext(context)
storedGame,_:= k.GetStoredGame(ctx,"1")
storedGame.Board ="not a board"
k.SetStoredGame(ctx, storedGame)deferfunc(){
r :=recover()
require.NotNil(t, r,"The code did not panic")
require.Equal(t, r,"game cannot be parsed: invalid board string: not a board")}()
msgServer.PlayMove(context,&types.MsgPlayMove{
Creator: bob,
GameIndex:"1",
FromX:1,
FromY:2,
ToX:2,
ToY:3,})} x checkers keeper msg_server_play_move_test.go View source
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:
Copy
$ go test github.com/alice/checkers/x/checkers/keeper
Copy
$ docker run --rm -it \
-v $(pwd):/checkers \
-w /checkers \
checkers_i \
go test github.com/alice/checkers/x/checkers/keeper
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.
Copy
$ checkersd tx checkers play-move 1 0 5 1 4 --from $bob
^ ^ ^ ^ ^
| | | | To Y
| | | To X
| | From Y
| From X
Game id
Copy
$ docker exec -it checkers \
checkersd tx checkers play-move 1 0 5 1 4 --from $bob
^ ^ ^ ^ ^
| | | | To Y
| | | To X
| | From Y
| From X
Game id
After you accept sending the transaction, it should complain with the result including:
Copy
...
raw_log: 'failed to execute message; message index: 0: {red}: player tried to play
out of turn'
...
txhash: D10BB8A706870F65F19E4DF48FB870E4B7D55AF4232AE0F6897C23466FF7871B
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:
Copy
...
raw_log: 'failed to execute message; message index: 0: {red}: player tried to play
out of turn'
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.
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:
When you are done with this exercise you can stop Ignite's chain serve.
synopsis
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.