2.11 Rock, Paper, Scissors in Python

Matt Audesse <matt@reach.sh>

The main sequence of the tutorial uses the JavaScript frontend support library to implement a frontend for our Reach program using JavaScript. But, Reach supports using any programming language through the Reach RPC Server.

This tutorial walks through using this technique to implement a Rock, Paper, Scissors! frontend in Python. It is based on the frontend from Play and Play Again, so it does not include a text-based interface, or a Web interface, but uses the final version of the Reach code.

Below we will compare the Play and Play Again JavaScript frontend with the equivalent Python code communicating via RPC, section by section. Follow along by typing the Python code into a file called index.py.

We begin by comparing the necessary imports and program body:

 1    import { loadStdlib } from '@reach-sh/stdlib';
 2    import * as backend from './build/index.main.mjs';
 3    const stdlib = loadStdlib(process.env);
 5    (async () => {
..    // ...
 1    # flake8: noqa
 3    import random
 4    from threading import Thread
 5    from reach_rpc import mk_rpc
 8    def main():
 9        rpc, rpc_callbacks = mk_rpc()
..    # ...

Rather than importing loadStdlib and backend as with the JavaScript version, the Python frontend instead plucks mk_rpc from its supporting reach_rpc library. It is unnecessary for an RPC frontend to import a backend because the RPC Server handles doing so instead.

The Python version also borrows functionality from the random and threading libraries. These will be necessary when providing callable methods in the participant interact interface it offers the RPC server.

On line 9 the Python program binds rpc and rpc_callbacks out of mk_rpc. These two functions are the only tools we will need to communicate with the RPC server. See Python (RPC) for more details on how they work.

Next, we define our Alice and Bob accounts and pre-fund them each with a starting balance of 10.

..    // ...
 6      const startingBalance = stdlib.parseCurrency(10);
 7      const accAlice = await stdlib.newTestAccount(startingBalance);
 8      const accBob = await stdlib.newTestAccount(startingBalance);
..    // ...
..    # ...
11        starting_balance = rpc('/stdlib/parseCurrency', 10)
12        acc_alice        = rpc('/stdlib/newTestAccount', starting_balance)
13        acc_bob          = rpc('/stdlib/newTestAccount', starting_balance)
..    # ...

Translating code which uses the JavaScript frontend support library to its Python RPC equivalent is a simple matter of specifying the corresponding RPC method (e.g. /stdlib/newTestAccount), and supplying the same arguments thereafter.

Now we define two helper functions and use them to query Alice and Bob’s beginning balances:

..    // ...
10      const fmt = (x) => stdlib.formatCurrency(x, 4);
11      const getBalance = async (who) => fmt(await stdlib.balanceOf(who));
12      const beforeAlice = await getBalance(accAlice);
13      const beforeBob = await getBalance(accBob);
..    // ...
..    # ...
15        def fmt(x):
16            return rpc('/stdlib/formatCurrency', x, 4)
18        def get_balance(w):
19            return fmt(rpc('/stdlib/balanceOf', w))
21        before_alice = get_balance(acc_alice)
22        before_bob   = get_balance(acc_bob)
..    # ...

Deploying and attaching to contracts works slightly differently over RPC:

..    // ...
15      const ctcAlice = accAlice.deploy(backend);
16      const ctcBob = accBob.attach(backend, ctcAlice.getInfo());
..    // ...
..    # ...
24        ctc_alice    = rpc('/acc/deploy', acc_alice)
..    # ...

As previously mentioned, it is the responsibility of the RPC Server (rather than that of the frontend communicating over RPC) to interface with the DApp’s backend, so that argument is absent in the Python version shown above. Instead, Alice’s account RPC handle alone is sufficient for her to deploy. We also need to delay Bob’s attach until later, because Python lacks Promises that work like JavaScript’s. When we do attach Bob, only Bob’s account RPC handle and Alice’s contract RPC handle are necessary for him to attach.

HAND and OUTCOME only differ syntactically from their JavaScript equivalents:

..    // ...
18      const HAND = ['Rock', 'Paper', 'Scissors'];
19      const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
..    // ...
..    # ...
26        HAND         = ['Rock', 'Paper', 'Scissors']
27        OUTCOME      = ['Bob wins', 'Draw', 'Alice wins']
..    # ...

Even participant interact interface definitions remain largely the same:

..    // ...
20      const Player = (Who) => ({
21        ...stdlib.hasRandom,
22        getHand: async () => { // <-- async now
23          const hand = Math.floor(Math.random() * 3);
24          console.log(`${Who} played ${HAND[hand]}`);
25          if ( Math.random() <= 0.01 ) {
26            for ( let i = 0; i < 10; i++ ) {
27              console.log(`  ${Who} takes their sweet time sending it back...`);
28              await stdlib.wait(1);
29            }
30          }
31          return hand;
32        },
..    // ...
..    # ...
29        def player(who):
30            def getHand():
31                hand = random.randint(0, 2)
32                print('%s played %s' % (who, HAND[hand]))
33                return hand
..    # ...

Here, both the JavaScript and Python frontends begin declaring a reusable "player constructor". This constructor represents those fields which are common to both Alice and Bob’s participant interact interfaces.

The JavaScript code explicitly includes ...stdlib.hasRandom itself, but the Python code can instead direct the RPC server to append it to the interface by including 'stdlib.hasRandom': True as a field in the constructor’s return value.

Next, they each define a getHand function which randomly selects an element from the previously defined HAND set and returns it to the backend. This function will be passed as a callable method of the interface later.

The Python version does not mimic the JavaScript’s occasional "pause behavior", although it easily could with a few extra lines of code.

informTimeout requires no subsequent backend interaction and is accordingly easily to implement in either language:

..    // ...
36        informTimeout: () => {
37          console.log(`${Who} observed a timeout`);
38        },
..    // ...
..    # ...
35            def informTimeout():
36                print('%s observed a timeout' % who)
..    # ...

The same is true of seeOutcome:

..    // ...
33        seeOutcome: (outcome) => {
34          console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
35        },
..    // ...
..    # ...
38            def seeOutcome(n):
39                print('%s saw outcome %s'
40                      % (who, OUTCOME[rpc('/stdlib/bigNumberToNumber', n)]))
42            return {'stdlib.hasRandom': True,
43                    'getHand':          getHand,
44                    'informTimeout':    informTimeout,
45                    'seeOutcome':       seeOutcome,
46                    }
..    # ...

At the end of the Python code we return a dict that represents those fields which are common to both Alice and Bob’s participant interact interfaces.

Again, 'stdlib.hasRandom': True has special significance when communicating via RPC: it instructs the server to append this signature on the receiving end.

Finally, we proceed to the most interesting part of the program and use the code we have built up thus far to actually play a game of Rock, Paper, Scissors!:

..    // ...
41      await Promise.all([
42        backend.Alice(ctcAlice, {
43          ...Player('Alice'),
44          wager: stdlib.parseCurrency(5),
45          deadline: 10,
46        }),
47        backend.Bob(ctcBob, {
48          ...Player('Bob'),
49          acceptWager: (amt) => {
50            console.log(`Bob accepts the wager of ${fmt(amt)}.`);
51          },
52        }),
53      ]);
55      const afterAlice = await getBalance(accAlice);
56      const afterBob = await getBalance(accBob);
58      console.log(`Alice went from ${beforeAlice} to ${afterAlice}.`);
59      console.log(`Bob went from ${beforeBob} to ${afterBob}.`);
..    // ...
..    # ...
48        def play_alice():
49            rpc_callbacks(
50                '/backend/Alice',
51                ctc_alice,
52                dict(wager=rpc('/stdlib/parseCurrency', 5), deadline=10, **player('Alice')))
54        alice = Thread(target=play_alice)
55        alice.start()
57        def play_bob():
58            def acceptWager(amt):
59                print('Bob accepts the wager of %s' % fmt(amt))
61            ctc_bob = rpc('/acc/attach', acc_bob, rpc('/ctc/getInfo', ctc_alice))
62            rpc_callbacks(
63                '/backend/Bob',
64                ctc_bob,
65                dict(acceptWager=acceptWager, **player('Bob')))
66            rpc('/forget/ctc', ctc_bob)
68        bob = Thread(target=play_bob)
69        bob.start()
71        alice.join()
72        bob.join()
74        after_alice = get_balance(acc_alice)
75        after_bob   = get_balance(acc_bob)
77        print('Alice went from %s to %s' % (before_alice, after_alice))
78        print('  Bob went from %s to %s' % (before_bob,   after_bob))
80        rpc('/forget/acc', acc_alice, acc_bob)
81        rpc('/forget/ctc', ctc_alice)
84    if __name__ == '__main__':
85        main()

In the Python version we create a function called play_alice and spawn it as a concurrent thread, which begins running in the background on line 56.

play_alice sends Alice’s contract RPC handle and her participant interact interface to the server with rpc_callbacks. The interface includes methods and values created by player('Alice'), and adds an additional wager value which is set to the result of rpc('/stdlib/parseCurrency', 5), as well as setting a deadline of 10.

Bob’s interface is likewise defined and spawned as another thread, which also begins running concurrently on line 69. In Bob’s case we add an acceptWager method instead of another value to his participant interact interface. Furthermore, his function is more complex, because we delay creating his contract handle until this time, so that the main thread does not block waiting for Alice’s contract information to resolve. This separation is not necessary in JavaScript, because of how JavaScript Promises work.

Calling .join() on alice and bob instructs the main thread to wait until both child threads have run to completion, signifying the end of the Rock, Paper, Scissors! game. At this point we again collect each player’s remaining balance and print them to the console. Each player’s child thread will have already printed their success/failure result to the screen prior to reaching this step, because that is how we encoded their seeOutcome methods.

All that remains is to release Alice and Bob’s RPC handles from the server’s memory on lines 80 and 81 with the /forget/acc and /forget/ctc methods, then instruct the Python process’ interpreter to invoke our main function.

Now that we have written an entire Rock, Paper, Scissors! game in Python it is time to try running it.

First you will need to copy the index.rsh file you used for the tutorial into the directory where you saved index.py.

Next, open a terminal in that directory and install the Reach Python RPC client:
  $ ([ -d ./venv ] || python3 -m venv ./venv) && source ./venv/bin/activate

What is this "venv" thing?

A Python venv is a "virtual environment" that sandboxes dependencies to avoid cluttering your system directories.

  $ pip install --upgrade reach-rpc-client

Then use ./reach rpc-run to play a game of Rock, Paper, Scissors!:
  $ ./reach rpc-run python3 -u ./index.py

Consult the command-line reference section for more details on how this sub-command works.

Its output will be the same as the final tutorial version of the frontend:

Bob accepts the wager of 5

Alice played Rock

Bob played Paper

Bob saw outcome Bob wins

Alice saw outcome Bob wins

Alice went from 10 to 4.9999

  Bob went from 10 to 14.9999

This will launch an RPC server using the development API key "opensesame" and a TLS certificate designed for testing.

Deploying your DApp into production with the RPC server requires obtaining a certificate which is specific to your DNS domain and which has been signed by a certificate authority such as Let’s Encrypt.

Users who are ready to go live should consult the RPC Server command-line reference section for configuration details.

When you are done, type deactivate to exit your venv.

Well done! You have just reimplemented the tutorial in Python.

This tutorial uses Python to demonstrate how RPC frontends are built in Reach, but it is similarly easy to write RPC frontends in other languages, such as with the JavaScript (RPC) and Go (RPC) libraries.