2.3 Rock, Paper, and Scissors

In this section, we’ll have Alice and Bob actually execute the game of Rock, Paper, Scissors!.

We have to decide how to represent the hands of the game. A simple way is to represent them as the numbers 0, 1, and 2, standing for Rock, Paper, and Scissors. However, Reach only supports unsigned integers of 256 bits, so it is better to represent them as the equivalence class of integers modulo three, so we won’t distinguish between 0 and 3 as Rock.

We’ll use a similar strategy for representing the three outcomes of the game: B wins, Draw, and A wins.

The first step is to change the Reach program to specify that Alice and Bob’s frontends can be interacted with to get the move that they will play, and later informed of the outcome of the game.

 1    'reach 0.1';
 2    
 3    const Player =
 4          { getHand: Fun([], UInt256),
 5            seeOutcome: Fun([UInt256], Null) };
 6    
 7    export const main =
 8      Reach.App(
 9        {},
10        [['Alice', Player], ['Bob', Player]],
11        (A, B) => {
..          // ...
26          exit(); });

Before continuing with the Reach application, let’s move over to the JavaScript interface and implement these methods in our frontend.

..    // ...
13    const HAND = ['Rock', 'Paper', 'Scissors'];
14    const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
15    const Player = (Who) => ({
16      getHand: () => { const hand = Math.floor(Math.random()*3);
17                       console.log(`${Who} played ${HAND[hand]}`);
18                       return hand; },
19      seeOutcome: (outcome) =>
20      console.log(`${Who} saw outcome ${OUTCOME[outcome]}`) });
21    
22    await Promise.all([
23      backend.Alice(
24        stdlib, ctcAlice,
25        Player('Alice')),
26      backend.Bob(
27        stdlib, ctcBob,
28        Player('Bob'))
29    ]);
..    // ...

There should be nothing interesting or controversial about these implementations; that’s the point of Reach: we get to just write normal business logic without worrying about the details of the consensus network and decentralized application.

Let’s return to the Reach program and look inside of the body of the program for what actions Alice and Bob take.

In a real-life game of Rock, Paper, Scissors!, Alice and Bob simultaneously decide what hand they will play and reveal it at the same time. "Simultaneity" is a complex concept that is hard to realize in practice. For example, if you’ve ever player against a little kid, you may notice them trying to see what you’re going to choose and delaying until the last minute to show their hand so they will win. In a decentralized application, it is not possible to have simultaneity. Instead, we have to select a particular participant who will "go first". In this case, we’ll choose Alice.

Does Alice go first, or do we call the player that goes first "Alice"? This might seem like an unnecessary distinction to make, but it is a very subtle point about the way that Reach works. In our frontend, we explicitly ran backend.Alice and backend.Bob. When we did that, we were committing that particular JavaScript thread to be either Alice or Bob. In our game, whoever chose to run the Alice backend is the one that will go first. In other words, Alice goes first. This will be more obvious at the end of the tutorial when we’ll make the choice interactively about which role to play.

The game proceeds in three steps.

First, Alice’s backend interacts with her frontend, gets her hand, and publishes it.

..    // ...
12    A.only(() => {
13      const handA = declassify(interact.getHand()); });
14    A.publish(handA);
15    commit();
..    // ...

The next step is similar, in that Bob publishes his hand; however, we don’t immediately commit the state, instead we compute the outcome of the game.

..    // ...
17    B.only(() => {
18      const handB = declassify(interact.getHand()); });
19    B.publish(handB);
20    
21    const outcome = (handA + (4 - handB)) % 3;
22    commit();
..    // ...

Finally, we use the each form to have each of the participants send the final outcome to their frontends.

..    // ...
24    each([A, B], () => {
25      interact.seeOutcome(outcome); });
..    // ...

At this point, we can run the program and see its output by running

  $ ./reach run

Since the players act randomly, the results will be different every time. When I ran the program three times, this is the output I got:

$ ./reach run

Alice played Scissors

Bob played Paper

Alice saw outcome Alice wins

Bob saw outcome Alice wins

 

$ ./reach run

Alice played Scissors

Bob played Paper

Alice saw outcome Alice wins

Bob saw outcome Alice wins

 

$ ./reach run

Alice played Paper

Bob played Rock

Alice saw outcome Alice wins

Bob saw outcome Alice wins

Alice is pretty good at Rock, Paper, Scissors!!

Consensus networks in general, and Reach specifically, guarantee that all participants agree on the outcome of their decentralized computation. Indeed, this is where the name consensus network comes from, as they enable these distributed, and untrusted, parties to come to a consensus, or agreement, about the intermediate states of a computation; and if they agree on the intermediate states, they will also agree on the output. That’s why every time you run ./reach run, both Alice and Bob will see the same outcome!

If your version isn’t working, look at the complete versions of tut-2/index.rsh and tut-2/index.mjs to make sure you copied everything down correctly!

In the next step, we’ll add some stakes to the game, because Alice needs to take her skills to the bank!