# Make a Checkers Blockchain

synopsis

Make sure you have all you need before proceeding with the exercise:

In the Starport introduction section you learned how to start a brand-new blockchain. Now it is time to dive deeper and explore how you can create a blockchain to play a decentralized game of checkers.

A good start to developing a checkers blockchain is to define the ruleset of the game. There are many versions of the rules. Choose a very simple set of basic rules (opens new window) to not get lost in the rules of checkers or the proper implementation of the board state.

Use a ready-made implementation (opens new window) with the additional rule that the board is 8x8 and played on black cells. This code will not need adjustments. Copy this rules file into a rules folder inside your module. Change its package from checkers to rules. You can do this by command-line:

Copy $ cd checkers $ mkdir x/checkers/rules $ curl https://raw.githubusercontent.com/batkinson/checkers-go/a09daeb1548dd4cc0145d87c8da3ed2ea33a62e3/checkers/checkers.go | sed 's/package checkers/package rules/' > x/checkers/rules/checkers.go

Do not focus on the GUI for the moment. You will only lay the foundation for an interface.

Now it is time to create the first object.

# The stored game object

Begin with the minimum game information needed to be kept in the storage:

  • Red player. A string, the serialized address.
  • Black player. A string, the serialized address.
  • Game proper. A string, the game as it is serialized by the rules file.
  • Player to play next. A string.

# How to store

Knowing what to store, you now have to decide how to store a game. This is important if you want your blockchain application to accommodate multiple simultaneous games. The game is identified by a unique ID.

How should you generate the ID? You cannot let players choose it themselves as this could lead to transactions failing because of an ID clash. It is better to have a counter incrementing on each new game. This is possible because the code execution happens in a single thread. You cannot rely on a large random number like a universally unique identifier (UUID) because transactions have to be verifiable in the future.

You need to keep such a counter in storage between transactions. You can keep a unique object at a singular location instead of a single counter. You can easily add relevant elements to the object as needed in the future. Designate idValue to the counter.

You can rely on Starport's assistance:

  • For the counter or rather the object that contains it, call NextGame and instruct Starport with scaffold single:

    Copy $ starport scaffold single nextGame idValue:uint --module checkers --no-message

    You need to add --no-message. If you omit it, Starport creates an sdk.Msg and an associated service, whose purpose is to overwrite your NextGame object. Your NextGame.IdValue has to be controlled/incremented by the application and not by a player sending a value of their own choosing. Starport still creates convenient getters.

  • You need a map because you're storing games by ID. Instruct Starport with scaffold map using the StoredGame name:

    Copy $ starport scaffold map storedGame game turn red black --module checkers --no-message

    The --no-message again? You do not want the game objects to be created or overwritten with a simple sdk.Msg. The application instead creates and updates the objects when receiving properly crafted messages like create game or play a move.

The Starport scaffold command creates several files as you can see here (opens new window) and here (opens new window).

The command added new constants:

Copy const ( NextGameKey = "NextGame-value-" ) const ( StoredGameKey = "StoredGame-value-" ) x checkers types keys.go View source

These constants will be used as prefixes for the keys that can access objects' storage.

# Protobuf objects

Starport creates the Protobuf objects in the proto directory before compiling them. The NextGame object looks like this:

Copy message NextGame { string creator = 1; uint64 idValue = 2; } proto checkers next_game.proto View source

And the StoredGame object looks like this:

Copy message StoredGame { string creator = 1; string index = 2; string game = 3; string turn = 4; string red = 5; string black = 6; } proto checkers stored_game.proto View source

Both objects compile to:

Copy type NextGame struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` IdValue uint64 `protobuf:"varint,2,opt,name=idValue,proto3" json:"idValue,omitempty"` } x checkers types next_game.pb.go View source

And:

Copy type StoredGame struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` Index string `protobuf:"bytes,2,opt,name=index,proto3" json:"index,omitempty"` Game string `protobuf:"bytes,3,opt,name=game,proto3" json:"game,omitempty"` Turn string `protobuf:"bytes,4,opt,name=turn,proto3" json:"turn,omitempty"` Red string `protobuf:"bytes,5,opt,name=red,proto3" json:"red,omitempty"` Black string `protobuf:"bytes,6,opt,name=black,proto3" json:"black,omitempty"` } x checkers types stored_game.pb.go View source

These are not the only created Protobuf objects. The genesis state is also defined in Protobuf:

Copy import "checkers/stored_game.proto"; import "checkers/next_game.proto"; message GenesisState { repeated StoredGame storedGameList = 2; NextGame nextGame = 1; } proto checkers genesis.proto View source

Which is compiled to:

Copy type GenesisState struct { StoredGameList []*StoredGame `protobuf:"bytes,2,rep,name=storedGameList,proto3" json:"storedGameList,omitempty"` NextGame *NextGame `protobuf:"bytes,1,opt,name=nextGame,proto3" json:"nextGame,omitempty"` } x checkers types genesis.pb.go View source

You can find query objects as part of the boilerplate objects created by Starport. NextGame might look out of place, but keep in mind Starport creates the objects according to a model. This does not prevent you from making changes later if you decide these queries are not needed:

Copy message QueryGetNextGameRequest {} message QueryGetNextGameResponse { NextGame NextGame = 1; } proto checkers query.proto View source

The query objects for StoredGame have more use to your checkers game and look like this:

Copy message QueryGetStoredGameRequest { string index = 1; } message QueryGetStoredGameResponse { StoredGame StoredGame = 1; } message QueryAllStoredGameRequest { cosmos.base.query.v1beta1.PageRequest pagination = 1; } message QueryAllStoredGameResponse { repeated StoredGame StoredGame = 1; cosmos.base.query.v1beta1.PageResponse pagination = 2; } proto checkers query.proto View source

# Starport's modus operandi

Starport puts the different Protobuf messages into different files depending on their use:

  • query.proto. For the objects related to reading the state. Starport modifies this file as you add queries. This includes the objects to query your stored elements (opens new window).
  • tx.proto. For the objects that relate to updating the state. As you have only defined storage elements with --no-message, it is empty for now. The file will be modified as you add transaction-related elements like the message to create a game.
  • genesis.proto. For the genesis. Starport modifies this file according to how your new storage elements evolve.
  • next_game.proto and stored_game.proto. Separate files created once that remain untouched by Starport after their creation. You are free to modify them but be careful with the numbering (opens new window).

Files updated by Starport include comments like:

Copy // this line is used by starport scaffolding # 2 proto checkers query.proto View source

Starport adds code right below the comments, which explains the odd numbering with the oldest members appearing lower than recent ones. But make sure to keep these comments where they are so that Starport knows where to inject code in the future. You could add your code above or below the comments. You will be fine if you keep these comments where they are.

Some files created by Starport can be updated, but you should not modify the Protobuf-compiled files *.pb.go (opens new window) and *.pb.gw.go (opens new window) as they are recreated on every re-run of starport generate proto-go or equivalent.

# Files to adjust

Starport creates files that you can and should update. For example, when it comes to the default genesis values:

Copy func DefaultGenesis() *GenesisState { return &GenesisState{ StoredGameList: []*StoredGame{}, NextGame: &NextGame{"", uint64(0)}, } } x checkers types genesis.go View source

You can choose to start with no games or insert a number of games to start with. You will need to choose the first ID of the first game in any case, which here is set at 0.

# Protobuf service interfaces

Beyond the created objects Starport also creates services that declare and define how to access the newly-created storage objects. Starport introduces empty service interfaces that can be filled as you add objects and messages when scaffolding a brand new module.

In your case, Starport added to service Query how to query for your objects:

Copy service Query { rpc StoredGame(QueryGetStoredGameRequest) returns (QueryGetStoredGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/storedGame/{index}"; } rpc StoredGameAll(QueryAllStoredGameRequest) returns (QueryAllStoredGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/storedGame"; } rpc NextGame(QueryGetNextGameRequest) returns (QueryGetNextGameResponse) { option (google.api.http).get = "/alice/checkers/checkers/nextGame"; } } proto checkers query.proto View source

Starport separates concerns into different files in the compilation of a service. Some of which you should edit and some should be left untouched. The following was already taken care of by Starport for your checkers game:

# Helper functions

Your stored game stores are only strings. But you know that they represent sdk.AccAddress or even a game from the rules file. You are going to do operations on them. So how about adding helper functions to StoredGame?

  1. Get the game Creator:

    Copy func (storedGame *StoredGame) GetCreatorAddress() (creator sdk.AccAddress, err error) { creator, errCreator := sdk.AccAddressFromBech32(storedGame.Creator) return creator, sdkerrors.Wrapf(errCreator, ErrInvalidCreator.Error(), storedGame.Creator) } x checkers types full_game.go View source

    Plus the same for the red (opens new window) and black (opens new window) players.

  2. Parse the game so that it can be played with. Notice how the Turn has to be set by hand:

    Copy func (storedGame *StoredGame) ParseGame() (game *rules.Game, err error) { game, errGame := rules.Parse(storedGame.Game) if err != nil { return game, sdkerrors.Wrapf(errGame, ErrGameNotParseable.Error()) } game.Turn = rules.Player{ Color: storedGame.Turn, } return game, nil } x checkers types full_game.go View source
  3. This is a good place to introduce your own errors:

    Copy var ( ErrInvalidCreator = sdkerrors.Register(ModuleName, 1100, "creator address is invalid: %s") ErrInvalidRed = sdkerrors.Register(ModuleName, 1101, "red address is invalid: %s") ErrInvalidBlack = sdkerrors.Register(ModuleName, 1102, "black address is invalid: %s") ErrGameNotParseable = sdkerrors.Register(ModuleName, 1103, "game cannot be parsed") ) x checkers types errors.go View source

# Next up

Want to continue developing your checkers blockchain? In the next section you will learn all about introducing an sdk.Msg to create a game.