在上一部分,我们的剪刀石头布会一直运行直到确定了最终的赢家为止。在本节中,我们不会改变Reach程序本身。相反地,我们将在 Reach run的情况下建立一个我们的游戏版本,该版本是交互式的,可以在私人开发者测试网络之外运行。


    在过去,当我们运行 ./reach run时,它会为我们的reach程序创建一个Docker映像,其中包含一个临时的Node.js包,将我们的JavaScript前端连接到reach标准库和一个私有开发者测试网络的新实例。因为在本节中,我们将对其进行定制,并构建一个非自动化版本的石头、布、剪刀!,并提供连接到真正以太坊网络的选项。
    开始运行
    $ ./reach scaffold
    它会自动为我们生成以下文件:

    • package.json 一个 Node.js 的包文件,链接 index.mjs 到 Reach 标准库
    • Dockerfile 一个 Docker 镜像脚本,高效地建立并运行package包程序
    • docker-compose.yml 一个 Docker Compose 脚本,链接 Docker 镜像到Reach私有开发人员测试网络的一个新实例。
    • Makefile 一个 Makefile 文件,快速重建并运行 Docker 镜像。

    我们将保留前两个文件不变。你可以在tut-7/packagetut-7/Dockerfile中看到它们。但是细节并不是特别重要。但是,我们将自定义其他两个文件。
    首先,让我们看看tut-7/docker-compose.yml文件:
    tut-7/docker-compose.yml

    1. 1 version: '3.4'
    2. 2 x-app-base: &app-base
    3. 3 image: reachsh/reach-app-tut-7:latest
    4. 4 services:
    5. 5 ethereum-devnet:
    6. 6 image: reachsh/ethereum-devnet:0.1
    7. 7 algorand-devnet:
    8. 8 image: reachsh/algorand-devnet:0.1
    9. 9 depends_on:
    10. 10 - algorand-postgres-db
    11. 11 environment:
    12. 12 - REACH_DEBUG
    13. 13 - POSTGRES_HOST=algorand-postgres-db
    14. 14 - POSTGRES_USER=algogrand
    15. 15 - POSTGRES_PASSWORD=indexer
    16. 16 - POSTGRES_DB=pgdb
    17. 17 ports:
    18. 18 - 9392
    19. 19 algorand-postgres-db:
    20. 20 image: postgres:11
    21. 21 environment:
    22. 22 - POSTGRES_USER=algogrand
    23. 23 - POSTGRES_PASSWORD=indexer
    24. 24 - POSTGRES_DB=pgdb
    25. 25 reach-app-tut-7-ETH-live:
    26. 26 <<: *app-base
    27. 27 environment:
    28. 28 - REACH_DEBUG
    29. 29 - REACH_CONNECTOR_MODE=ETH-live
    30. 30 - ETH_NODE_URI
    31. 31 - ETH_NODE_NETWORK
    32. 32 reach-app-tut-7-ETH-test-dockerized-geth: &default-app
    33. 33 <<: *app-base
    34. 34 depends_on:
    35. 35 - ethereum-devnet
    36. 36 environment:
    37. 37 - REACH_DEBUG
    38. 38 - REACH_CONNECTOR_MODE=ETH-test-dockerized-geth
    39. 39 - ETH_NODE_URI=http://ethereum-devnet:8545
    40. 40 reach-app-tut-7-ETH-test-embedded-ganache:
    41. 41 <<: *app-base
    42. 42 environment:
    43. 43 - REACH_DEBUG
    44. 44 - REACH_CONNECTOR_MODE=ETH-test-embedded-ganache
    45. 45 reach-app-tut-7-FAKE-test-embedded-mock:
    46. 46 <<: *app-base
    47. 47 environment:
    48. 48 - REACH_DEBUG
    49. 49 - REACH_CONNECTOR_MODE=FAKE-test-embedded-mock
    50. 50 reach-app-tut-7-ALGO-test-dockerized-algod-local:
    51. 51 <<: *app-base
    52. 52 environment:
    53. 53 - REACH_DEBUG
    54. 54 - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
    55. 55 - ALGO_SERVER=http://host.docker.internal
    56. 56 - ALGO_PORT=4180
    57. 57 - ALGO_INDEXER_SERVER=http://host.docker.internal
    58. 58 - ALGO_INDEXER_PORT=8980
    59. 59 extra_hosts:
    60. 60 - 'host.docker.internal:172.17.0.1'
    61. 61 reach-app-tut-7-ALGO-test-dockerized-algod:
    62. 62 <<: *app-base
    63. 63 depends_on:
    64. 64 - algorand-devnet
    65. 65 environment:
    66. 66 - REACH_DEBUG
    67. 67 - REACH_CONNECTOR_MODE=ALGO-test-dockerized-algod
    68. 68 - ALGO_SERVER=http://algorand-devnet
    69. 69 - ALGO_PORT=4180
    70. 70 - ALGO_INDEXER_SERVER=http://algorand-devnet
    71. 71 - ALGO_INDEXER_PORT=8980
    72. 72 reach-app-tut-7-: *default-app
    73. 73 reach-app-tut-7: *default-app
    74. 74 # After this is new!
    75. 75 player: &player
    76. 76 <<: *default-app
    77. 77 stdin_open: true
    78. 78 alice: *player
    79. 79 bob: *player
    • 第2行和第3行定义了启动应用程序的服务。如果您在教程中一直停留在同一个目录中,那么第3行将显示tut,而不是tut-7。
    • 第5行和第6行定义了以太坊的Reach私有开发者测试网络服务。
    • 第7至24行定义了为Algorand提供的Reach private developer测试网络服务。
    • 第25到73行定义了允许应用程序在不同网络上运行的服务;包括第24行,它定义了reach-app-tut-7-ETH-live连接到一个现有网络。
    • 我们还将添加第73行到第77行来定义一个玩家,它是我们的应用程序,具有开放的标准输入,以及两个名为alice和bob的实例。

    有了这些,我们就可以运行
    $ docker-compose run WHICH
    其中 WHICH 是 reach-app-tut-7-ETH-live 作为一个 live 实例,又或者是一个alice或者一个bob作为一个测试实例。如果我们使用live版本,那么我们必须定义环境变量ETH_NODE_URI作为我们以太坊节点的URI。
    我们将修改tut-7/Makefile,以拥有运行这些变量的命令:
    tut-7/Makefile

    ..    
    29    .PHONY: run-live
    30    run-live:
    31        docker-compose run --rm reach-app-tut-7-ETH-live
    32    
    33    .PHONY: run-alice
    34    run-alice:
    35        docker-compose run --rm alice
    36    
    37    .PHONY: run-bob
    38    run-bob:
    39        docker-compose run --rm bob
    

    然而,如果我们尝试运行其中任何一个,它会做同样的事情:为每个用户创建测试帐户,并模拟随机游戏。让我们修改JavaScript前端,使它们具有交互性。


    我们将从头开始,再次显示程序的每一行。您将看到这个版本与上一个版本有很多相似之处,但是为了完整起见,我们将显示每一行。
    tut-7/index.mjs

    1    import { loadStdlib } from '@reach-sh/stdlib';
     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 stdlib = await loadStdlib();
    ..    // ...
    
    • 第1行和第2行和之前一样:导入标准库和后端。
    • 第3行是新的,为简单的控制台应用程序导入了一个来自Reach标准库名为ask.mjs的有用库。下面我们将看到如何使用这三个函数。

    tut-7/index.mjs

    ..    // ...
     7    
     8    const isAlice = await ask(
     9      `Are you Alice?`,
    10      yesno
    11    );
    12    const who = isAlice ? 'Alice' : 'Bob';
    ..    // ...
    
    • 第7行到第10行,问他们是否在扮演爱丽丝,并期望得到“是”或“不是”的回答。ask会显示一个提示并收集一行输入,直到它的参数没有出错。如果没有给出“y”或“n”,yesno出错。

    tut-7/index.mjs

    ..    // ...
    13    
    14    console.log(`Starting Rock, Paper, Scissors! as ${who}`);
    15    
    16    let acc = null;
    17    const createAcc = await ask(
    18      `Would you like to create an account? (only possible on devnet)`,
    19      yesno
    20    );
    21    if (createAcc) {
    22      acc = await stdlib.newTestAccount(stdlib.parseCurrency(1000));
    23    } else {
    24      const secret = await ask(
    25        `What is your account secret?`,
    26        (x => x)
    27      );
    28      acc = await stdlib.newAccountFromSecret(secret);
    29    }
    ..    // ...
    
    • 第16行到第19行,用户可以选择创建一个测试帐户,或者输入一个秘密来加载一个现有的帐户。
    • 第21行像前面一样创建测试帐户。
    • 第27行加载现有帐户。

    tut-7/index.mjs

    ..    // ...
    30    
    31    let ctc = null;
    32    const deployCtc = await ask(
    33      `Do you want to deploy the contract? (y/n)`,
    34      yesno
    35    );
    36    if (deployCtc) {
    37      ctc = acc.deploy(backend);
    38      const info = await ctc.getInfo();
    39      console.log(`The contract is deployed as = ${JSON.stringify(info)}`);
    40    } else {
    41      const info = await ask(
    42        `Please paste the contract information:`,
    43        JSON.parse
    44      );
    45      ctc = acc.attach(backend, info);
    46    }
    ..    // ...
    
    • 第31至34行询问参与者是否将部署合约。
    • 第36到38行部署它并打印出可以给其他玩家的公共信息(ctc.getInfo)。
    • 第40行到第44行请求、解析和处理这个信息。

    tut-7/index.mjs

    ..    // ...
    47    
    48    const fmt = (x) => stdlib.formatCurrency(x, 4);
    49    const getBalance = async () => fmt(await stdlib.balanceOf(acc));
    50    
    51    const before = await getBalance();
    52    console.log(`Your balance is ${before}`);
    53    
    54    const interact = { ...stdlib.hasRandom };
    ..    // ...
    

    接下来,我们定义几个helper函数并启动participant交互接口。
    tut-7/index.mjs

    ..    // ...
    55    
    56    interact.informTimeout = () => {
    57      console.log(`There was a timeout.`);
    58      process.exit(1);
    59    };
    ..    // ...
    

    首先我们定义一个超时处理程序。
    tut-7/index.mjs

    ..    // ...
    60    
    61    if (isAlice) {
    62      const amt = await ask(
    63        `How much do you want to wager?`,
    64        stdlib.parseCurrency
    65      );
    66      interact.wager = amt;
    67    } else {
    68      interact.acceptWager = async (amt) => {
    69        const accepted = await ask(
    70          `Do you accept the wager of ${fmt(amt)}?`,
    71          yesno
    72        );
    73        if (accepted) {
    74          return;
    75        } else {
    76          process.exit(0);
    77        }
    78      };
    79    }
    ..    // ...
    

    接下来,我们请求赌注金额或定义acceptWager方法,这取决于我们是否是Alice。
    tut-7/index.mjs

    ..    // ...
    80    
    81    const HAND = ['Rock', 'Paper', 'Scissors'];
    82    const HANDS = {
    83      'Rock': 0, 'R': 0, 'r': 0,
    84      'Paper': 1, 'P': 1, 'p': 1,
    85      'Scissors': 2, 'S': 2, 's': 2,
    86    };
    87    interact.getHand = async () => {
    88      const hand = await ask(`What hand will you play?`, (x) => {
    89        const hand = HANDS[x];
    90        if ( hand == null ) {
    91          throw Error(`Not a valid hand ${hand}`);
    92        }
    93        return hand;
    94      });
    95      console.log(`You played ${HAND[hand]}`);
    96      return hand;
    97    };
    ..    // ...
    

    接下来,我们定义共享gehand方法。
    tut-7/index.mjs

    ..    // ...
    98    
    99    const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
    100    interact.seeOutcome = async (outcome) => {
    101      console.log(`The outcome is: ${OUTCOME[outcome]}`);
    102    };
    ..    // ...
    

    最后是seeOutcome方法。
    tut-7/index.mjs

    ..    // ...
    103    
    104    const part = isAlice ? backend.Alice : backend.Bob;
    105    await part(ctc, interact);
    106    
    107    const after = await getBalance();
    108    console.log(`Your balance is now ${after}`);
    109    
    110    done();
    111    })();
    

    最后,我们选择合适的后端功能并等待其完成。


    现在我们可以运行
    $ make build
    然后重建镜像,在该目录的一个终端中
    $ make run-alice
    在该目录下的另一个终端中:
    $ make run-bob
    下面是一个运行的例子:

    $ 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":"0xc2a875afbdFb39b1341029A7deceC03750519Db6","creation_block":18,"args":[],"value":{"type":"BigNumber","hex":"0x00"},"creator":"0x2486Cf6C788890885D71667BBCCD1A783131547D"}
    Your balance is 999.9999
    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.9999
    

    另一个实例

    $ 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":"0xc2a875afbdFb39b1341029A7deceC03750519Db6","creation_block":18,"args":[],"value":{"type":"BigNumber","hex":"0x00"},"creator":"0x2486Cf6C788890885D71667BBCCD1A783131547D"}
    Your balance is 1000
    Do you accept the wager of 10?
    y
    What hand will you play?
    p
    You played Paper
    The outcome is: Bob wins
    Your balance is now 1009.9999
    

    当然,运行时准确的数量和地址可能不同。


    如果我们要编辑tut-7/docker-compose。然后将第24行中的&default-app移动到第51行,这样我们就可以在Algorand上测试和运行我们的应用,而不是在以太坊上运行。


    现在,我们的剪刀石头布应用完成了,我们将免于遭受攻击,超时,平局的影响,同时我们可以在非测试网络上交互运行。
    在这一步中,我们为Reach程序创建了一个命令行界面。在下一步中,我们将用同一个Reach程序的Web界面替换它。

    如果你的版本不工作,看看tut-7/index.rsh, tut-7/index.mjs, tut-7/package.json, tut-7/Dockerfile, tut-7/docker-compose.yml, tut-7/Makefile 是否是完整版本,以确保您正确地复制了所有内容! 我们可能还需要更改tut-7/index.rsh的第32行,将deadline定义到更大的数字,比如30。这是因为Algorand不支持 input-enabled 开发网络,在交易出现时只运行轮次,所以超时可能会意外发生。我们经常在CPU负载过高的机器上观察到这种情况。 检查你的理解是否到位:Reach帮助你为去中心化的应用程序构建自动化测试,但它不支持构建交互式用户界面? 答案: Reach不会对添加到Reach应用程序的前端类型施加任何限制。ㄑ