Building a Rock Paper Scissors Smart Contract with Scaffold-Eth

Daniel
5 min readFeb 7, 2022

--

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: MIT
contract 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

On-chain Rock Paper Scissors with Commit/Reveal

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.

Joining or Hosting a game

2. Commit Phase

Once both players are in the game, they can “commit” their choices along with a password to be hashed.

Commit Phase

3. Reveal Phase

Once both players have committed, the game moves to the reveal phase.

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.

--

--

Daniel
Daniel

Written by Daniel

full stack software engineer | web3 builder

No responses yet