2.7 Play and Play Again

In this section, we extend our application so that Alice and Bob will continue to play against each other until there is a clear winner, so if it is a draw they will continue playing.

This will only require a change to the Reach program, not the JavaScript frontend, but we will take the opportunity to modify the frontend so that timeouts can happen to both parties when they are asked to submit their hands. Let’s do that to get it out of the way and not distract from the main task of removing draws.

We’ll modify the Player interact object so that it will have a different getHand method.

..    // ...
18    const Player = (Who) => ({
19      ...stdlib.hasRandom,
20      getHand: async () => { // <-- async now
21        const hand = Math.floor(Math.random()*3);
22        console.log(`${Who} played ${HAND[hand]}`);
23        if ( Math.random() <= 0.01 ) {
24          for ( let i = 0; i < 10; i++ ) {
25            console.log(`  ${Who} takes their sweet time sending it back...`);
26            await stdlib.wait(1); } }
27        return hand; },
28      seeOutcome: (outcome) =>
29      console.log(`${Who} saw outcome ${OUTCOME[outcome]}`),
30      informTimeout: () =>
31      console.log(`${Who} observed a timeout`) });
..    // ...

We also adjust Bob’s acceptWager function to remove the timeout code, since we’re testing that differently now. It’s just a matter of reverting to the simpler version from before.

..    // ...
33    await Promise.all([
34      backend.Alice(
35        stdlib, ctcAlice,
36        { ...Player('Alice'),
37          wager: toNetworkFormat('5')
38        }),
39      backend.Bob(
40        stdlib, ctcBob,
41        { ...Player('Bob'),
42          acceptWager: (amt) =>
43          console.log(`Bob accepts the wager of ${stdlib.fromWei(amt)}.`)  } )
44    ]);
..    // ...

Now, let’s look at the Reach application. All of the details about the playing of the game and the interface to the players will remain the same. The only thing that’s going to be different is the order the actions take place.

It used to be that the steps were:

  1. Alice sends her wager and commitment.

  2. Bob accepts the wager and sends his hand.

  3. Alice reveals her hand.

  4. The game ends.

But, now because the players may submit many hands, but should only have a single wager, we’ll break these steps up differently, as follows:

  1. Alice sends her wager.

  2. Bob accepts the wager.

  3. Alice sends her commitment.

  4. Bob sends his hand.

  5. Alice reveals her hand.

  6. If it’s draw, return to step 3; otherwise, the game ends.

Let’s make these changes now.

..    // ...
42    A.only(() => {
43      const wager = declassify(interact.wager); });
44    A.publish(wager)
45      .pay(wager);
46    commit();
..    // ...

..    // ...
48    B.only(() => {
49      interact.acceptWager(wager); });
50    B.pay(wager)
51      .timeout(DEADLINE, () => closeTo(A, informTimeout));
52    
..    // ...

It’s now time to begin the repeatable section of the application, where each party will repeatedly submit hands until the the outcome is not a draw. In normal programming languages, such a circumstance would be implemented with a while loop, which is exactly what we’ll do in Reach. However, while loops in Reach require extra care, as discussed in the guide on loops in Reach, so we’ll take it slow.

In the rest of a Reach program, all identifier bindings are static and unchangable, but if this were the case throughout all of Reach, then while loops would either never start or never terminate, because the loop condition would never change. So, a while loop in Reach can introduce a variable binding.

Next, because of Reach’s automatic verification engine, we must be able to make a statement about what properties of the program are invariant before and after a while loop body’s execution, a so-called "loop invariant".

Finally, such loops may only occur inside of consensus steps. That’s why Bob’s transaction was not committed, because we need to remain inside of the consensus to start the while loop. This is because all of the participants must agree on the direction of control flow in the application.

Here’s what the structure looks like:

..    // ...
53    var outcome = DRAW;
54    invariant(balance() == 2 * wager && isOutcome(outcome) );
55    while ( outcome == DRAW ) {
..    // ...

Now, let’s look at the body of the loop for the remaining steps, starting with Alice’s commitment to her hand.

..    // ...
56    commit();
57    
58    A.only(() => {
59      const _handA = interact.getHand();
60      const [_commitA, _saltA] = makeCommitment(interact, _handA);
61      const commitA = declassify(_commitA); });
62    A.publish(commitA)
63      .timeout(DEADLINE, () => closeTo(B, informTimeout));
64    commit();
..    // ...

..    // ...
66    unknowable(B, A(_handA, _saltA));
67    B.only(() => {
68      const handB = declassify(interact.getHand()); });
69    B.publish(handB)
70      .timeout(DEADLINE, () => closeTo(A, informTimeout));
71    commit();
..    // ...

Similarly, Bob’s code is almost identical to the prior version, except that he’s already accepted and paid the wager.

..    // ...
73    A.only(() => {
74      const [saltA, handA] = declassify([_saltA, _handA]); });
75    A.publish(saltA, handA)
76      .timeout(DEADLINE, () => closeTo(B, informTimeout));
77    checkCommitment(commitA, saltA, handA);
..    // ...

Alice’s next step is actually identical, because she is still revealing her hand in exactly the same way.

Next is the end of the loop.

..    // ...
79    outcome = winner(handA, handB);
80    continue; }
..    // ...

The rest of the program could be exactly the same as it was before, except now it occurs outside of the loop, but we will simplify it, because we know that the outcome can never be a draw.

..    // ...
82    assert(outcome == A_WINS || outcome == B_WINS);
83    transfer(2 * wager).to(outcome == A_WINS ? A : B);
84    commit();
85    
86    each([A, B], () => {
87      interact.seeOutcome(outcome); });
88    exit(); });

Let’s run the program and see what happens:

$ ./reach run

Bob accepts the wager of 5.0.

Alice played Paper

Bob played Rock

Bob saw outcome Alice wins

Alice saw outcome Alice wins

Alice went from 10.0 to 14.999999999999040261.

Bob went from 10.0 to 4.999999999999938085.

 

$ ./reach run

Bob accepts the wager of 5.0.

Alice played Rock

Bob played Rock

Alice played Paper

Bob played Scissors

Bob saw outcome Bob wins

Alice saw outcome Bob wins

Alice went from 10.0 to 4.999999999998975474.

Bob went from 10.0 to 14.999999999999906275.

 

$ ./reach run

Bob accepts the wager of 5.0.

Alice played Scissors

Bob played Rock

Bob saw outcome Bob wins

Alice saw outcome Bob wins

Alice went from 10.0 to 4.999999999999040265.

Bob went from 10.0 to 14.999999999999938097.

As usual, your results may differ, but you should be able to see single round victories like this, as well as multi-round fights and timeouts from either party.

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

Now our implementation of Rock, Paper, Scissors! will always result in a pay-out, which is much more fun for everyone. In the final step, we’ll show how to exit "testing" mode with Reach and turn our JavaScript into an interactive Rock, Paper, Scissors! game with real users.