2.8 Interaction and Independence

In the last section, we made our Rock, Paper, Scissors! run until there was a definitive winner. In this section, we won’t be making any changes to the Reach program itself. Instead, we’ll go under the covers of reach run, as well as build a version of our game that is interactive and can be played away from a private developer test network.

In the past, when we’ve run ./reach run, it would create a Docker image just for our Reach program that contained a temporary Node.js package connecting our JavaScript frontend to the Reach standard library and a fresh instance of a private developer test network. Since in this section, we will customize this and build a non-automated version of Rock, Paper, Scissors!, as well as give the option to connect to a real Ethereum network.

We’ll start by running

  $ ./reach scaffold

which will automatically generate the following files for us:

We’re going to leave the first two files unchanged. You can look at them at tut-7/package.json and tut-7/Dockerfile, but the details aren’t especially important. However, we’ll customize the other two files.

First, let’s look at the tut-7/docker-compose.yml file:

 1    version: '3.4'
 2    x-app-base: &app-base
 3      image: reachsh/reach-app-tut-7:latest
 4    services:
 5      devnet:
 6        image: reachsh/ethereum-devnet:latest
 7      reach-app-tut-7-ETH-test-dockerized-geth: &default-app
 8        <<: *app-base
 9        depends_on:
10          - devnet
11        environment:
12          - REACH_CONNECTOR_MODE=ETH-test-dockerized-geth
13          - ETH_NODE_URI=http://devnet:8545
14      reach-app-tut-7-ETH-test-embedded-ganache:
15        <<: *app-base
16        environment:
17          - REACH_CONNECTOR_MODE=ETH-test-embedded-ganache
18      reach-app-tut-7-FAKE-test-embedded-mock:
19        <<: *app-base
20        environment:
21          - REACH_CONNECTOR_MODE=FAKE-test-embedded-mock
22      reach-app-tut-7-: *default-app
23      reach-app-tut-7: *default-app
24      # After this is new!
25      live:
26        <<: *app-base
27        environment:
28          - ETH_NODE_URI
29      player: &player
30        <<: *default-app
31        stdin_open: true
32      alice: *player 
33      bob: *player 

With these in place, we can run

  $ docker-compose run WHICH

where WHICH is live for a live instance, or alice or bob for a test instance. If we use the live version, then we have to define the environment variable ETH_NODE_URI as the URI of our Ethereum node.

We’ll modify the tut-7/Makefile to have commands to run each of these variants:

..    
25    .PHONY: run-live
26    run-live:
27    	docker-compose run --rm live
28    
29    .PHONY: run-alice
30    run-alice:
31    	docker-compose run --rm alice
32    
33    .PHONY: run-bob
34    run-bob:
35    	docker-compose run --rm bob

However, if we try to run either of these, it will do the same thing it always has: create test accounts for each user and simulate a random game. Let’s modify the JavaScript frontend and make them interactive.

We’ll start from scratch and show every line of the program again. You’ll see a lot of similarity between this and the last version, but for completeness, we’ll show every line.

 1    import * as stdlib from '@reach-sh/stdlib/ETH.mjs';
 2    import * as backend from './build/index.main.mjs';
 3    import { ask, yesno, done } from '@reach-sh/stdlib/ask.mjs';
 4    
 5    ( async () => {
 6    const toNetworkFormat = (n) => stdlib.toWeiBigNumber(n, 'ether');
..    // ...

..    // ...
 8    const isAlice = await ask(
 9      `Are you Alice?`, yesno);
10    const who = isAlice ? 'Alice' : 'Bob';
11    
12    console.log(`Starting Rock, Paper, Scissors! as ${who}`);
..    // ...

..    // ...
14    let acc = null;
15    if ( await ask(
16      `Would you like to create an account? (only possible on devnet)`, yesno) ) {
17      acc = await stdlib.newTestAccount( toNetworkFormat('1000') ); }
18    else {
19      const phrase = await ask(
20        `What is your account mnemonic?`, (x => x));
21      acc = await stdlib.newAccountFromMnemonic(phrase); }
..    // ...

..    // ...
23    let ctc = null;
24    if ( await ask(
25      `Do you want to deploy the contract? (y/n)`, yesno) ) {
26      ctc = await acc.deploy(backend);
27      const info = await ctc.getInfo();
28      console.log(`The contract is deployed as = ${JSON.stringify(info)}`); }
29    else {
30      const info = await ask(
31        `Please paste the contract information:`,
32        JSON.parse );
33      ctc = await acc.attach(backend, info); }
..    // ...

..    // ...
35    const getBalance = async () =>
36          stdlib.fromWei ( await stdlib.balanceOf(acc) );
37    
38    const before = await getBalance();
39    console.log(`Your balance is ${before}`);
40    
41    const interact = { ...stdlib.hasRandom };
..    // ...

Next we define a few helper functions and start the participant interaction interface.

..    // ...
43    interact.informTimeout = () => {
44      console.log(`There was a timeout.`);
45      process.exit(1); };
..    // ...

First we define a timeout handler.

..    // ...
47    if ( isAlice ) {
48      const amt = await ask(
49        `How much do you want to wager?`,
50        toNetworkFormat );
51      interact.wager = amt; }
52    else {
53      interact.acceptWager =
54        async (amt) => {
55          if ( await ask(
56            `Do you accept the wager of ${stdlib.fromWei(amt)}?`,
57            yesno) ) {
58            return; }
59          else {
60            process.exit(0); } } }
..    // ...

Next, we request the wager amount or define the acceptWager method, depending on if we are Alice or not.

..    // ...
62    const HAND = ['Rock', 'Paper', 'Scissors'];
63    const HANDS = {'Rock': 0, 'R': 0, 'r': 0,
64                   'Paper': 1, 'P': 1, 'p': 1,
65                   'Scissors': 2, 'S': 2, 's': 2 };
66    interact.getHand =
67      async () => {
68        const hand = await ask(
69          `What hand will you play?`,
70          (x) => {
71            const hand = HANDS[x];
72            if ( hand == null ) {
73              throw Error(`Not a valid hand ${hand}`); }
74            return hand; } );
75        console.log(`You played ${HAND[hand]}`);
76        return hand; };
..    // ...

Next, we define the shared getHand method.

..    // ...
78    const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
79    interact.seeOutcome =
80      async (outcome) => {
81        console.log(`The outcome is: ${OUTCOME[outcome]}`); };
..    // ...

Finally, the seeOutcome method.

..    // ...
83    const part = isAlice ? backend.Alice : backend.Bob;
84    await part(stdlib, ctc, interact);
85    
86    const after = await getBalance();
87    console.log(`Your balance is now ${after}`);
88    
89    done();
90    })();

Lastly, we choose the appropriate backend function and await its completion.

We can now run

  $ make build

to rebuilt the images, then

  $ make run-alice

in one terminal in this directory and

  $ make run-bob

in another terminal in this directory.

Here’s an example run:

$ make run-alice

Are you Alice?

y

Starting Rock, Paper, Scissors as Alice

Would you like to create an account? (only possible on devnet)

y

Do you want to deploy the contract? (y/n)

y

The contract is deployed as = {"address": "0xdd1a445d4a85C4676094f84fFe19Fb3d76E502E0", "creation_block": 73}

Your balance is 999.999999999999123878

How much do you want to wager?

10

What hand will you play?

r

You played Rock

The outcome is: Bob wins

Your balance is now 989.999999999999040247

and

$ make run-bob

Are you Alice?

n

Starting Rock, Paper, Scissors as Bob

Would you like to create an account? (only possible on devnet)

y

Do you want to deploy the contract? (y/n)

n

Please paste the contract information:

{"address": "0xdd1a445d4a85C4676094f84fFe19Fb3d76E502E0", "creation_block": 73}

Your balance is 1000.0

Do you accept the wager of 10.0?

y

What hand will you play?

p

You played Paper

The outcome is: Bob wins

Your balance is now 1009.999999999999938073

Of course, when you run the exact amounts and addresses may be different.

If your version isn’t working, look at the complete versions of tut-7/index.rsh, tut-7/index.mjs, tut-7/package.json, tut-7/Dockerfile, tut-7/docker-compose.yml, and tut-7/Makefile to make sure you copied everything down correctly!

Now our implementation of Rock, Paper, Scissors! is finished! We are protected against attacks, timeouts, and draws, and we can run interactively on non-test networks.

In the next section, we’ll summarize where we’ve gone and direct you to the next step of your journey to decentralized application mastery.