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 
3HOPR nodes with their respectiveHTTP_ENDPOINT,WS_ENDPOINTandAPI_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.
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.
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.
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.
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:
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.
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 formataddress-move. We need both values to know who’s playing and reply to them.In lines
9-10we make sure to ignore messages that do not follow the expected format.Line
12makes sure the game can only be played by different users.Lines
14-16and21-23are the ones actually comparing the moves, and send their respective lines after sending the appropriate response to the players, concluding the game.Last but not least, line
33ensures this logic is only called byRefereenodes, so even ifPlayernodes get messages, they will not act asRefereenodes 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 
/signAPI 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 
Refereesending 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
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>
  )
}