This tutorial guides you through building a Rock Paper Scissors smart contract and dApp frontend with Scaffold-Eth. If you’re new to Scaffold-Eth, you should definitively check out the great walkthroughs on youtube and attempt the challenges at SpeedRunEthereum.
Setup
Clone or fork the repo, checkout the rock-paper-scissors branch
git clone https://github.com/danielkhoo/scaffold-eth.git
git checkout rock-paper-scissors
NOTE: This branch comes with the completed contract code. If you would like to implement it from scratch, replace RockPaperScissors.sol
with an empty contract:
pragma solidity >=0.8.0 <0.9.0;
//SPDX-License-Identifier: MITcontract RockPaperScissors {}
Navigate to your new directory and install dependencies with
yarn install
In your project directory, we’re going to open three terminal windows as follows:
Start the local development Hardhat chain at http://localhost:8545
yarn chain
Start the React frontend, available at http://localhost:3000
yarn start
Deploy your contract
yarn deploy
Video Walkthrough
Commit Reveal Pattern
We want to adapt the classic children’s game of Rock, Paper, Scissors with a smart contract and a dApp frontend.
In real life, the game relies on both players showing their choices simultaneously. For our smart contract, we’ll be using the commit/reveal pattern to enable asynchronous game play.
You can read more about the commit/reveal pattern. But in short, the player’s choice will be hashed with a password/salt. After both players have committed their choices, they can then “reveal” their choice by providing the password that generates a matching hash. This mechanic exploits the one-way nature of hashing, allowing publishing of information beforehand while keeping the content a secret until it needs to be revealed.
Gameplay
Before we look at the contract, it’s useful to run through the overall structure and gameplay phases:
1. Join Phase
A player can host a new game by providing the address of the other player. This will generate a unique game address which can be shared for player 2 to join.
2. Commit Phase
Once both players are in the game, they can “commit” their choices along with a password to be hashed.
3. Reveal Phase
Once both players have committed, the game moves to the reveal phase.
4. Result Phase
When both players have revealed, the result is shown
Contract Part 1: Data Structures
Now that we have the outline, we can look at the contract. We’ll need a few data structures. We want to store the state of a game in single struct with some useful enums.
// 4 Game Phases: Join, Commit, Reveal, Result
enum GameState {
JoinPhase,
CommitPhase,
RevealPhase,
ResultPhase
}
// 3 Game Results: P1 win, P2 win, draw
enum GameResult {
P1Win,
P2Win,
Draw
}
// Holds the game data for a single match
struct GameStruct {
bool initialized;
address player1;
address player2;
GameState gameState;
bytes32 commit1;
bytes32 commit2;
bytes32 reveal1;
bytes32 reveal2;
uint256 revealDeadline;
GameResult gameResult;
}
We also need a mapping to lookup individual games and a mapping of players to their current game.
// Maps Game address => Game data
mapping(address => GameStruct) public games;// Maps Player address to their current 'active' game
mapping(address => address) public activeGame;
Contract Part 2: Hosting or Joining a game
Next we want to implement the functions to let players host or join a game. We’ll need function createGame(address otherPlayer)
that creates a new game with a unique game address, setting player1 as msg.sender
and player2 as otherPlayer
. Note that our game address is a pseudo-random value generated from a hash of player1’s address and the previous block hash.
To join an existing game, we also want a joinGame(address gameHash)
function that advances the game to the commit phase and updates player2's active game. The contract should look something like this:
Note that I’ve extracted some validation logic into a modifier validGameState
as we will reuse it for other functions. Also included is a helper function getActiveGameData(address player)
for our RockPaperScissors frontend.
Try it out! Deploy your contract and open `http://localhost:3000/` with 2 different browsers/wallets. Try creating a game and joining with the game address.
Contract Part 3: Commit
Next we implement the commit(string memory choice, string memory salt)
function. The function should check that the player is in a valid game in the correct game phase. It should then generate a hash with the player choice + password and save it to the game struct. If player is the second to commit, then advance the game state to the reveal phase.
Contract Part 4: Reveal & Results
This is the most critical function for the game. We want to implement a reveal(string memory salt)
function that takes the players password/salt, verifies that it matches the commit, before revealing the unsalted hash. For better user experience, we don’t ask for the users choice of rock/paper/scissors again, but instead compare the hash against all three possibilities for a match. If player is the second to reveal, then the result is calculated based on the hashes.
Anti-Griefing
Note that after a player has revealed, their choice will be discoverable as the unsalted hash will be one of the three possibilities. At this point an unsporting player could view the hash on chain and decide not to reveal at all to deny their opponent the win. For this reason we include a revealDeadline
that starts when the first player reveals. Such that if the other player doesn’t reveal, there is a “win-by-default” condition, this is done by calling the determindDefaultWinner
function.
The completed contract should look like this:
Try it out again! Deploy your contract and open `http://localhost:3000/` with 2 different browsers/wallets. Try playing multiple games with yourself, win/lose/draw. Try defaulting and not revealing in time.
Frontend
Check out the deployed frontend on Rinkeby Testnet: https://rpsgame.surge.sh/
Most of the frontend code is in GameUI.jsx
, it’s minimal UI with Scaffold-Eth components, Ant Design and Eth-Hooks to interface with our deployed contract.
Summary
And we’re done! We’ve learnt about the commit/reveal pattern, how to translate a synchronous game into asynchronous flow in smart contracts and building a simple UI. All with 🏗 Scaffold-Eth.