Skip to main content
Version: Next

Demo - Rock, Paper & Scissors Game

The Rock, Paper, Scissors (RPS) Game is a HOPR app which can use the HOPR network as a p2p gaming platform. Players can send off-chain moves to a HOPR node which acts as a "referee" using fixed logic (i.e., rock beats scissors, scissors beat paper, and paper beats rock) to produce a winner. After each node has sent its move, the referee displays the winner of the game and the moves made by each player.

In this demo app, we will also show the pros and cons of the HOPR protocol when sending private messages.

Requirements

  • a HOPR cluster, and at least a 3 HOPR nodes with their respective HTTP_ENDPOINT, WS_ENDPOINT and API_TOKEN
Tip

If you need instructions on how to run a HOPR cluster, see our "HOPR Cluster Development Setup" section for guidance.

How it works

The game requires at least 3 HOPR nodes within a HOPR cluster. Each node has two possible roles: Player and Referee. A Player plays a game by sending its move (i.e., "rock", "paper", or "scissors") to a Referee. When 2 Players have sent a move to a single Referee, then the Referee will send a message back to each Player telling them whether they have won, lost, or tied.


Move Phase
move: rock
┌───────────────────┐
│ ▼
┌──────┴─────┐ ┌───────────┐ ┌────────────┐
│ │ │ │ │ │
│ Player 1 │ │ Referee │ │ Player 2 │
│ │ │ │ │ │
└────────────┘ └───────────┘ └─────┬──────┘
▲ │
└───────────────────┘
move: paper



result: lost Decision Phase
┌──────────────────┐
▼ │
┌────────────┐ ┌────┴──────┐ ┌────────────┐
│ │ │ │ │ │
│ Player 1 │ │ Referee │ │ Player 2 │
│ │ │ │ │ │
└────────────┘ └─────┬─────┘ └────────────┘
│ ▲
└───────────────────┘
result: won

Source code

Unlike in our "Demo - Boomerang Chat", we'll only go over the most important parts particular to this demo. We recommend reviewing that example first to get a better overview of the relevant components.

Connector

As required by most HOPR Applications, we use a <Connector> component that handles the input of the HTTP_ENDPOINT, WS_ENDPOINT, and SECURITY_TOKEN values, required to connect to our HOPR nodes.

Connector.jsx
import React from 'react'

export default function Connector({
httpEndpoint,
setHTTPEndpoint,
securityToken,
setSecurityToken,
wsEndpoint,
setWsEndpoint
}) {
return (
<>
<div>
<label>WS Endpoint</label>{' '}
<input
name="wsEndpoint"
placeholder={wsEndpoint}
value={wsEndpoint}
onChange={(e) => setWsEndpoint(e.target.value)}
/>
</div>
<div>
<label>HTTP Endpoint</label>{' '}
<input
name="httpEndpoint"
placeholder={httpEndpoint}
value={httpEndpoint}
onChange={(e) => setHTTPEndpoint(e.target.value)}
/>
</div>
<div>
<label>Security Token</label>{' '}
<input
name="securityToken"
placeholder={securityToken}
value={securityToken}
onChange={(e) => setSecurityToken(e.target.value)}
/>
</div>
</>
)
}

WebSocketHandler

Similarly, a HOPR app needs to initialize a WebSocket client and reload upon changes to the WS_ENDPOINT value. Underneath we also use a useWebSocket hook which does the actual binding against the socket via addEventListener.

WebSocketHandler.jsx
import React, { useEffect, useState } from 'react'
import useWebsocket from '../hooks/useWebSockets'

export const WebSocketHandler = ({
wsEndpoint,
securityToken,
multipleMessages = false,
messages = [],
setMessages = () => {}
}) => {
const [message, setMessage] = useState('')
const websocket = useWebsocket({ wsEndpoint, securityToken })
const { socketRef } = websocket
const handleReceivedMessage = async (ev) => {
try {
// we are only interested in messages, not all the other events coming in on the socket
const data = JSON.parse(ev.data)
console.log('WebSocket Data', data)
if (data.type === 'message') {
setMessage(data.msg)
setMessages((prevArray) => [...prevArray, data.msg])
}
} catch (err) {
console.error(err)
}
}
useEffect(() => {
if (!socketRef.current) return
socketRef.current.addEventListener('message', handleReceivedMessage)

return () => {
if (!socketRef.current) return
socketRef.current.removeEventListener('message', handleReceivedMessage)
}
}, [socketRef.current])

return (
<>
{multipleMessages ? (
<div>
{messages.map((message) => (
<p>{message}</p>
))}
</div>
) : (
<span>{message ? message : 'You have no messages.'}</span>
)}
</>
)
}

export default WebSocketHandler

ClusterHelper

When developing locally, it quickly gets annoying to pre-load the HOPR node settings for multiple nodes. <ClusterHelper> is a development-only component that quickly loads information about individual nodes in a HOPR cluster to the application by clicking a button.

ClusterHelper
import React from 'react'

export default function ClusterHelper({
setHTTPEndpoint,
setWsEndpoint,
setSecurityToken,
selectedNode,
setSelectedNode
}) {
const CLUSTER_NODES = 5
const setEndpointsValueUsingIndex = (index) => {
const BASE_HTTP = 'http://localhost:1330'
const BASE_WS = 'ws://localhost:1950'
const DEFAULT_SECURITY_TOKEN = '^^LOCAL-testing-123^^'
setHTTPEndpoint(BASE_HTTP + index)
setWsEndpoint(BASE_WS + index)
setSecurityToken(DEFAULT_SECURITY_TOKEN)
}

return (
<div style={{ display: 'inline-block ' }}>
Preload Cluster Node -
{[...Array(CLUSTER_NODES)].map((_, index) => (
<button
style={{
background: selectedNode == index + 1 && 'blue',
color: selectedNode == index + 1 && 'white'
}}
onClick={() => {
setSelectedNode(index + 1)
setEndpointsValueUsingIndex(index + 1)
}}
>
{index + 1}
</button>
))}
</div>
)
}

RockPaperScissorsGame

The main component wrapping everything so far, including the game logic. In addition to loading everything, it includes all the state managers passed down to the previous components, alongside the event handlers and API calls against the actual HOPR nodes.

Tip

You can see the entire code under the Annex section.

RockPaperScissorsGame.jsx
import React, { useEffect, useState } from 'react'
import WebSocketHandler from './WebSocketHandler'
import Connector from './atoms/Connector'
import ClusterHelper from './atoms/ClusterHelper'
import { getHeaders } from './utils'

export default function RPSGame() {
...

useEffect(() => {
const loadAddress = async () => {
...
}
loadAddress()
}, [securityToken, httpEndpoint])

const sendMessage = async (recipient, body) => {
...
}

const sendMove = async (move) => {
...
}

useEffect(() => {
// Game logic goes here, when messages are received.
const gameLogic = async () => {
...
}
isReferee && gameLogic()
}, [messages])

return (
<div>
<ClusterHelper
...
/>
<br />
<span>PeerId: {address}</span>
<Connector
...
/>
<div>
<div style={{ display: 'inline-block', marginRight: '10px' }}>
<label htmlFor="isReferee">Is Referee</label>
<input
onChange={(e) => setIsReferee(e.target.checked)}
id="isReferee"
type="checkbox"
/>
</div>{''}
<label>Referee</label>{' '}
<input
name="referee"
disabled={isReferee}
placeholder={referee}
value={referee}
onChange={(e) => setReferee(e.target.value)}
/>
</div>
{address && !isReferee &&
<>
<button disabled={!referee} onClick={() => sendMove(PAPER_MOVE)}>Send "paper" move</button>
<button disabled={!referee} onClick={() => sendMove(SCISSORS_MOVE)}>Send "scissors" move</button>
<button disabled={!referee} onClick={() => sendMove(ROCK_MOVE)}>Send "rock" move</button>
</>
}
{notification && <><br />{notification}</>}
<>
<br />
<WebSocketHandler
...
/>
</>
</div >
)
}

Game Dynamic

Upon load, the RPS game prompts the user with the connecting information and a decision on whether the particular node should be a Player or a Referee. Player nodes can send moves, and Referee nodes can listen to multiple messages.

Move Phase

To play, Player nodes need to paste a Referee address before being able to send a move. Once they have done that, they can pick one of three moves: "rock", "paper", and "scissors". Picking a move will send an API request for a message to the Referee.

Once at least 2 Player nodes have sent a move to the same Referee, the game moves to the Decision Phase.

Note

A Referee node will echo whatever move is sent. In an ideal setup, this information will only be known to the Referee and not displayed. For the purposes of this demo, everything is shown.

Decision Phase

A Referee node that has received at least two messages can execute the following game logic within the application:

RockPaperScissorsGame.jsx
useEffect(() => {
// Game logic goes here, when messages are received.
const gameLogic = async () => {
const [player1, player2] = messages
.slice(messages.length - 2)
.map((move) => ({ address: move.split('-')[0], move: move.split('-')[1] }))

// We ignore all other messages.
if (!player1 || !player2) return
if (!player1.move || !player2.move) return

if (player1.address != player2.address) {
if (
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE) ||
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE) ||
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE)
) {
await sendMessage(player1.address, `You tied with ${player2.address}: [1] ${player1.move}, [2] ${player2.move}`)
await sendMessage(player2.address, `You tied with ${player1.address}: [1] ${player1.move}, [2] ${player2.move}`)
} else if (
(player1.move == ROCK_MOVE && player2.move == SCISSORS_MOVE) ||
(player1.move == SCISSORS_MOVE && player2.move == PAPER_MOVE) ||
(player1.move == PAPER_MOVE && player2.move == ROCK_MOVE)
) {
await sendMessage(player1.address, `You won! ${player2.address} lost: [1] ${player1.move}, [2] ${player2.move}`)
await sendMessage(
player2.address,
`You lost... ${player1.address} won: [1] ${player1.move}, [2] ${player2.move}`
)
} else {
await sendMessage(player2.address, `You won! ${player1.address} lost: [1] ${player1.move}, [2] ${player2.move}`)
await sendMessage(
player1.address,
`You lost... ${player2.address} won: [1] ${player1.move}, [2] ${player2.move}`
)
}
}
}
isReferee && gameLogic()
}, [messages])

The logic can be described as follows.

  1. First, we make sure we parse only moves and not any message sent to our nodes. In lines 4-6, we parse the messages given the expected format address-move. We need both values to know who’s playing and reply to them.

  2. In lines 9-10 we make sure to ignore messages that do not follow the expected format.

  3. Line 12 makes sure the game can only be played by different users.

  4. Lines 14-16 and 21-23 are the ones actually comparing the moves, and send their respective lines after sending the appropriate response to the players, concluding the game.

  5. Last but not least, line 33 ensures this logic is only called by Referee nodes, so even if Player nodes get messages, they will not act as Referee nodes and send messages back.

Demo - Rock, Paper, Scissors Game

Player 1

Loading Player 1...

Referee

Loading Referee...

Player 2

Loading Player 2...

Important notes

This RPS game already highlights some of the benefits of the HOPR network as a p2p platform:

  • Instead of relying on a central authority, anyone can execute a game logic and evaluate player actions as needed.
  • Because of how the technology works, these applications can be coded as simple web pages that connect to a more complex backend.
  • Since all information is private, you could send sensitive information and even create games around secrets or private keys.

That being said, it should also be obvious there are also wrinkles and shortcomings:

  • Since there is no sender-receiver linkage, anyone can pretend to send a message (this can be solved using the /sign API endpoint, which ensures a message was sent by your node and not any node).
  • At the time of writing, the HOPR protocol does not have an acknowledge-like feature, so message senders have no way to know whether their messages arrive (this is currently handled on an application level, like the Referee sending a response back).
  • Messages can take any shape or form, so the encoding and decoding of the messages to information that’s relevant is currently handled on an application basis. In the future, there will be a HOPR SDK which will help with some basic message parsing.

Annex: RockPaperScissorsGame.jsx

RockPaperScissorsGame.jsx
import React, { useEffect, useState } from 'react'
import WebSocketHandler from './WebSocketHandler'
import Connector from './atoms/Connector'
import ClusterHelper from './atoms/ClusterHelper'
import { getHeaders } from './utils'

export default function RPSGame() {
const [securityToken, setSecurityToken] = useState('')
const [selectedNode, setSelectedNode] = useState()
const [wsEndpoint, setWsEndpoint] = useState('ws://localhost:3000')
const [httpEndpoint, setHTTPEndpoint] = useState('http://localhost:3001')
const [messages, setMessages] = useState([])
const [address, setAddress] = useState('')
const [isReferee, setIsReferee] = useState()
const [referee, setReferee] = useState('')
const [notification, setNotification] = useState('')

const SCISSORS_MOVE = 'scissors'
const ROCK_MOVE = 'rock'
const PAPER_MOVE = 'paper'

useEffect(() => {
const loadAddress = async () => {
const headers = getHeaders(securityToken)
const account = await fetch(`${httpEndpoint}/api/v2/account/addresses`, {
headers
})
.then((res) => res.json())
.catch((err) => console.error(err))
setAddress(account?.hopr)
}
loadAddress()
}, [securityToken, httpEndpoint])

const sendMessage = async (recipient, body) => {
if (!address) return
await fetch(`${httpEndpoint}/api/v2/messages`, {
method: 'POST',
headers: getHeaders(securityToken, true),
body: JSON.stringify({
recipient,
body
})
}).catch((err) => console.error(err))
}

const sendMove = async (move) => {
await sendMessage(referee, `${address}-${move}`)
setNotification(`You have sent the move ${move} to referee ${referee}`)
}

useEffect(() => {
// Game logic goes here, when messages are received.
const gameLogic = async () => {
const [player1, player2] = messages
.slice(messages.length - 2)
.map((move) => ({ address: move.split('-')[0], move: move.split('-')[1] }))

// We ignore all other messages.
if (!player1 || !player2) return
if (!player1.move || !player2.move) return

if (player1.address != player2.address) {
if (
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE) ||
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE) ||
(player1.move == ROCK_MOVE && player2.move == ROCK_MOVE)
) {
await sendMessage(
player1.address,
`You tied with ${player2.address}: [1] ${player1.move}, [2] ${player2.move}`
)
await sendMessage(
player2.address,
`You tied with ${player1.address}: [1] ${player1.move}, [2] ${player2.move}`
)
} else if (
(player1.move == ROCK_MOVE && player2.move == SCISSORS_MOVE) ||
(player1.move == SCISSORS_MOVE && player2.move == PAPER_MOVE) ||
(player1.move == PAPER_MOVE && player2.move == ROCK_MOVE)
) {
await sendMessage(
player1.address,
`You won! ${player2.address} lost: [1] ${player1.move}, [2] ${player2.move}`
)
await sendMessage(
player2.address,
`You lost... ${player1.address} won: [1] ${player1.move}, [2] ${player2.move}`
)
} else {
await sendMessage(
player2.address,
`You won! ${player1.address} lost: [1] ${player1.move}, [2] ${player2.move}`
)
await sendMessage(
player1.address,
`You lost... ${player2.address} won: [1] ${player1.move}, [2] ${player2.move}`
)
}
}
}
isReferee && gameLogic()
}, [messages])

return (
<div>
<ClusterHelper
selectedNode={selectedNode}
setSelectedNode={setSelectedNode}
setHTTPEndpoint={setHTTPEndpoint}
setWsEndpoint={setWsEndpoint}
setSecurityToken={setSecurityToken}
/>
<br />
<span>PeerId: {address}</span>
<Connector
httpEndpoint={httpEndpoint}
setHTTPEndpoint={setHTTPEndpoint}
wsEndpoint={wsEndpoint}
setWsEndpoint={setWsEndpoint}
securityToken={securityToken}
setSecurityToken={setSecurityToken}
/>
<div>
<div style={{ display: 'inline-block', marginRight: '10px' }}>
<label htmlFor="isReferee">Is Referee</label>
<input onChange={(e) => setIsReferee(e.target.checked)} id="isReferee" type="checkbox" />
</div>
{''}
<label>Referee</label>{' '}
<input
name="referee"
disabled={isReferee}
placeholder={referee}
value={referee}
onChange={(e) => setReferee(e.target.value)}
/>
</div>
{address && !isReferee && (
<>
<button disabled={!referee} onClick={() => sendMove(PAPER_MOVE)}>
Send "paper" move
</button>
<button disabled={!referee} onClick={() => sendMove(SCISSORS_MOVE)}>
Send "scissors" move
</button>
<button disabled={!referee} onClick={() => sendMove(ROCK_MOVE)}>
Send "rock" move
</button>
</>
)}
{notification && (
<>
<br />
{notification}
</>
)}
<>
<br />
<WebSocketHandler
wsEndpoint={wsEndpoint}
securityToken={securityToken}
multipleMessages={isReferee}
messages={messages}
setMessages={setMessages}
/>
</>
</div>
)
}