[Reach教程翻译] 2.7 平手则继续猜拳直至分出胜负

  • Ivan
  • 更新于 2021-11-29 20:49
  • 阅读 3030

本篇介绍了Reach的重要语法之一: While。通过加入循环,我们让猜拳游戏得以玩到分出胜负为止~

[Reach教程翻译] Reach是安全简单的Dapp开发语言 让用户可以像开发传统App一样开发DApp 目前使用Reach开发的智能合约可以部署在以太坊、Conflux、Algorand Reach官网 Reach官方文挡

2.7 平手则继续猜拳直至分出胜负

原文链接

在本节中,我们将继续编写我们的应用程序,让 Alice 和 Bob 可以在平手的时后继续对抗,直到有一个赢家出现;也就是说,如果结果出现平手,他们将继续进行。

我们只需要修改 Reach 程序,而不用修改 JavaScript 前端,但是为了接下来处理平局时再战的情况,我们将修改前端,因为现在两个人出拳时都需要加上“超时”机制了。 (回忆一下,原先我们只在 Bob 出拳时加了“超时”机制)

我们首先调整一下交互对象 Player 其中的 getHand 方法 :

rps-7-loops/index.mjs

..    // ...
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      },
33      seeOutcome: (outcome) => {
34        console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
35      },
36      informTimeout: () => {
37        console.log(`${Who} observed a timeout`);
38      },
39    });
..    // ...
  • 第 25 到 30 行 将 Bob 的 acceptWager 函数中的强制超时代码移到这个方法中。我们还调整了让超时只有1%的概率发生。因为这个行为没什么意思,所以我们将它发生的频率大大调低。

因为我们现在测试的方式不同,所以我们在 Bob 的 acceptWager 函数中去掉了超时代码,其实就是恢复到了之前的简单版本。我们也在 Bob 的 acceptWager 函数中去掉了超时代码,因为我们现在测试的方式不同了。所以把这部份恢复到之前的简单版本。

rps-7-loops/index.mjs

..    // ...
41    await Promise.all([
42      ctcAlice.p.Alice({
43        ...Player('Alice'),
44        wager: stdlib.parseCurrency(5),
45        deadline: 10,
46      }),
47      ctcBob.p.Bob({
48        ...Player('Bob'),
49        acceptWager: (amt) => {
50          console.log(`Bob accepts the wager of ${fmt(amt)}.`);
51        },
52      }),
53    ]);
..    // ...
  • 第49-51行简化了Bob的 acceptWager 方法。

现在,回到 Reach 应用程序,游戏的所有细节和玩家接口都保持不变。唯一不同的是游戏行为发生的顺序。

以前的步骤是:

  1. Alice发出她的赌约。
  2. Bob下注,出拳。
  3. Alice 出拳。
  4. 游戏结束。

但是,现在因为玩家可以提交多次出拳手势,但只有一个赌注,所以我们将调整这些步骤如下:

  1. Alice发出赌约。
  2. Bob下注。
  3. Alice下注。
  4. Bob出拳。
  5. Alice出拳。
  6. 如果是平局,返回第 4 步;否则,比赛结束。

现在,我们就开始实现这些改变 :

rps-7-loops/index.rsh

..    // ...
45    Alice.only(() => {
46      const wager = declassify(interact.wager);
47      const deadline = declassify(interact.deadline);
48    });
49    Alice.publish(wager, deadline)
50      .pay(wager);
51    commit();
..    // ...
  • 第 49 行 Alice 发布赌注与时限。
  • 第 50 行 Alice 支付赌注

rps-7-loops/index.rsh

..    // ...
53    Bob.only(() => {
54      interact.acceptWager(wager);
55    });
56    Bob.pay(wager)
57      .timeout(relativeTime(deadline), () => closeTo(Alice, informTimeout));
58    
..    // ...
  • 第 56 行 Bob 支付赌注。
  • 注意! 第 58 行没有提交共识步骤。

现在可以开始实现反复比赛了,此时,双方会反复出拳直到结果不是平手为止。在正常的编程语言中,这样的情况会通过 while 循环实现,Reach 也是如此。然而 Reach 中的 while 循环需要特别小心,正如在 Reach循环指南中所讨论的,所以我们慢慢来。

在 Reach 程序的其余部分中,所有标识符绑定值都是静态、不可更改的,但是如果在整个 Reach 都如此,那么 while 循环将永远不会开始或永远不会终止,因为循环条件永远不会改变。所以,Reach 中的 while 循环允许引入变量绑定值。

接下来,为了让 Reach 的自动验证引擎运作,我们必须在 while 循环体执行之前和之后,声明程序的哪些属性是不变的,这就是所谓的“循环不变量

最后,这样的循环可能只发生在共识步骤。这就是为什么刚刚 Bob 的交易没有提交,因为我们需要保持在共识内部来运行 while 循环 。这是为了让所有参与者都同意应用程序中控制流的方向。

结构是这样的:

rps-7-loops/index.rsh

..    // ...
59    var outcome = DRAW;
60    invariant( balance() == 2 * wager && isOutcome(outcome) );
61    while ( outcome == DRAW ) {
..    // ...
  • 第 59 行定义了循环变量 outcome
  • 第 60 行声明了循环不变量为合约账户的余额始终是两倍赌注,以及 outcome 始终是有效的结果。
  • 第 61 行声明循环条件:只要结果是平局,循环就会继续。

现在,我们进入循环体中的其他步骤,从 Alice 对出拳的承诺开始。

rps-7-loops/index.rsh

..    // ...
62    commit();
63    
64    Alice.only(() =>; {
65      const _handAlice = interact.getHand();
66      const [_commitAlice, _saltAlice] = makeCommitment(interact, _handAlice);
67      const commitAlice = declassify(_commitAlice);
68    });
69    Alice.publish(commitAlice)
70      .timeout(relativeTime(deadline), () => closeTo(Bob, informTimeout));
71    commit();
..    // ...
  • 第 62 行提交了先前的交易
  • 第 64 - 71 行,除了赌注是已知且已支付的以外,与之前的版本都相同。

rps-7-loops/index.rsh

..    // ...
73    unknowable(Bob, Alice(_handAlice, _saltAlice));
74    Bob.only(() => {
75      const handBob = declassify(interact.getHand());
76    });
77    Bob.publish(handBob)
78      .timeout(relativeTime(deadline), () => closeTo(Alice, informTimeout));
79    commit();
..    // ...

类似地, Bob 的代码也是除了赌注是已接受并支付的以外,与之前的版本都相同。

rps-7-loops/index.rsh

..    // ...
81    Alice.only(() => {
82      const saltAlice = declassify(_saltAlice);
83      const handAlice = declassify(_handAlice);
84    });
85    Alice.publish(saltAlice, handAlice)
86      .timeout(relativeTime(deadline), () => closeTo(Bob, informTimeout));
87    checkCommitment(commitAlice, saltAlice, handAlice);
..    // ...

Alice 的下一步也是相同的,因为她仍然以完全相同的方式发布她的手势。

接下来是循环的最后一部份。

rps-7-loops/index.rsh

..    // ...
89      outcome = winner(handAlice, handBob);
90      continue;
91    }
..    // ...
  • 第 89 行更新循环变量 outcome
  • 第 90 行继续循环。与大多数编程语言不同, Reach 要求在循环体中显式地写出 continue

程序的剩余部分都在循环之外了,内容可以与以前完全相同,不过我们可以稍微简化一下,因为现在结果永远不会是平手。

rps-7-loops/index.rsh

..    // ...
93    assert(outcome == A_WINS || outcome == B_WINS);
94    transfer(2 * wager).to(outcome == A_WINS ? Alice : Bob);
95    commit();
..    // ...
  • 第 93 行断言结果不是平手 ,这是显然是正确的,否则我们就不会退出 while 循环。
  • 第 94 行将资金转给胜者。

让我们运行该程序,看看会发生什么:

$ ./reach run
Bob accepts the wager of 5.
Alice played Paper
Bob played Rock
Bob saw outcome Alice wins
Alice saw outcome Alice wins
Alice went from 100 to 104.9999.
Bob went from 100 to 94.9999.

$ ./reach run
Bob accepts the wager of 5.
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 100 to 94.9999.
Bob went from 100 to 104.9999.

$ ./reach run
Bob accepts the wager of 5.
Alice played Scissors
Bob played Rock
Bob saw outcome Bob wins
Alice saw outcome Bob wins
Alice went from 100 to 94.9999.
Bob went from 100 to 104.9999.

跟之前一样,您运行的结果可能会有所不同,但您应该会看到有时猜拳一轮就有胜者、有时出现猜拳多轮或是两人中有人超时的情况。

如果您的版本不能正常运行,请查看 rps-7-loops/index.rshrps-7-loops/index.mjs ,并确保您正确地复制了所有内容!

现在,我们的石头剪刀布游戏一定会分出胜负,这让游戏更有趣。在下一节里,我们将展示如何使用 Reach 退出“测试”模式,并将我们的 JavaScript 变为一个能够与真实用户交互的“石头剪刀布”游戏。

您知道了吗?

如何在 Reach 中编写一个运行时间任意长的应用程序,比如保证不会以平局结束的石头剪刀布游戏?

  1. 这是不可能的,因为所有 Reach 程序都是有限时长的;
  2. 你可以使用一个 while 循环,一直运行到确定比赛结果为止。

答案是: 2 ; Reach 支持 while 循环。

您知道了吗?

当你检查一个带有 while 循环的程序是否正确时,你需要有一个叫做循环不变量的属性。关于循环不变量,下列哪些叙述是正确的?

  1. while 循环前必须声明循环不变量。
  2. 循环条件和循环内必须包含循环不变量。
  3. 循环条件的否定和不变量会维持至循环之外的剩余部份。

答案是: 以上皆是

欢迎关注Reach微信公众号 并在公众号目录 -> 加入群聊 选择加入官方开发者微信群与官方Discord群 与更多Reach开发者一起学习交流!

_.png

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ivan
Ivan
江湖只有他的大名,没有他的介绍。