(x,y)
, on the opponent’s grid, in an attempt to find the location of the opponent’s ships. If a player guesses a coordinate where a ship is located, the opponent must respond with “hit”. If a player guesses a coordinate where there is no ship, the opponent must respond with “miss”. The player who sinks all of their opponent’s ships first wins the game.
r
, s
, and v
. We can derive these parameters from a signature by splitting it into the requisite number of bytes. Retrieving the public address from these parameters is as simple as invoking Solidity’s ecrecover
function. This is a relatively costly process and consumes quite a bit of gas compared to normal transactions. To optimize the gas cost, we made use of assembly in our smart contract to split the signature.
We could also have split the signature offchain with JavaScript or another programming language.
SigVerifier
contract from the Verify.sol
file, which provides functions for verifying digital signatures.players
— a mapping of addresses to booleans indicating whether each address is a player in the game.ships
— a mapping of each player’s address to a mapping of ship hashes to booleans. The ships
mapping keeps track of which ship pieces each player has placed on the grid.destroyedShips
— a mapping of each player’s address to an array of Coordinate
structs. The destroyedShips
mapping keeps track of which of a player’s ships have been destroyed.playerShots
— a mapping of each player’s address to a Coordinate
struct. The playerShots
mapping keeps track of the coordinates of the square each player has shot at during their turn.playerHasPlayed
— a mapping of each player’s address to a boolean indicating whether that player has taken a shot during the current turn.playerHasPlacedShips
— a mapping of each player’s address to a boolean indicating whether that player has placed all their ships on the grid.playerHasReportedHits
— a mapping of each player’s address to a boolean indicating whether that player has reported all the hits from their opponent’s shots during the current turn.joinGame
— allows a player to join the game and place their ships on the grid.takeAShot
— allows a player to take a shot at their opponent’s grid.reportHits
— allows a player to report the hits from their opponent’s shots.isHit
— whether a shot hits a ship and returns a boolean and the coordinate of the shot.destroyPlayerShip
: an internal function that adds a destroyed ship coordinate to the destroyedShips
mapping and checks whether the player has lost all their ships. If so, the game is over.owner
— the address of the contract owner.NO_PLAYERS
— a constant that specifies the number of players in the game.NO_SHIP_PIECES
— a constant that specifies the number of ship pieces each player can place on the grid.playersAddress
— an array of addresses representing the players in the game.numberOfDestroyedPlayers
— a counter of the number of players who have lost all their ships.isGameOver
— a boolean indicating whether the game is over.ecrecover
method. It can be a little tricky to get signatures right with ethers.js, but here’s one way:
signShipCoordinates(ships: Array<Ship>, signer)
takes an array of ships and a Signer
object (wallet) as arguments, and returns an array of signed ships.
generateShipShotProof(player: Signer, allPlayers: Array<string>, battleshipGame: any)
quickly generates a list of proofs for each reported shot coordinate.
"\x19Ethereum Signed Message:\n"
+ length of the message
. Taking a look at our SigVerifier contract, we hardcoded the length of the message to be 32. We do that because we always hash our messages and data, and the length of hashes is always 32 bytes.
Hardhat test run 1
Hardhat test run 2
Hardhat test run 3
Hardhat test run 4
joinGame
is our most expensive function.
isTurnOver
, isGameOver
etc.). I chose plain reverts for simplicity.