Skip to main content
Version: v1.87

Demo - Rock, Paper & Scissors Game

The Rock, Paper & Scissors (RPS) Game is a HOPR app which is able to use the HOPR network as a p2p gaming platform. Using a HOPR node as a "referee", players can send off-chain moves to it, which given a known logic (i.e. rock beats scissors, scissors beat paper, and paper beats rock), can produce a winner. After each node has send 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 "Running a local cluster" section for guidance.

How it works

The game relies on 3 HOPR nodes within a HOPR Cluster. Each of these nodes have 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 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 most HOPR applications require, 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

In a similar fashion, a HOPR app needs to initialize a WebSocket client and reload upon change of 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

During local development, it quickly gets annoying to pre-load the HOPR node settings for multiple nodes. <ClusterHelper> is a development-only component that quickly loads HOPR cluster single node information to the application by clicking a single 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 are able to 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 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 them back.

  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 send the appropiate 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 do not act as a Referee and send messages back.

Demo - Rock, Paper, Scissors Game

Player 1

Loading Player 1...

Referee

Loading Referee...

Player 2

Loading Player 2...

Important notes

With the RPS game, you can see already some of the benefits of the HOPR network as a p2p platform:

  • Instead of having to rely 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 simply web pages that connect to a more complex backend.
  • Since all information is private, you could send sensitive information, and create games around secrets or private keys.

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

  • Since there are 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).
  • As of time of writing, the HOPR protocol does not have an acknowledge-like feature, so message senders will not have a way to know whether the messages arrive or not (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>
)
}