2.7 平局则继续比赛直至判断胜负

609 阅读2分钟

原文链接


在本节中,我们将继续编写我们的应用程序,让 Alice 和 Bob 可以在平手的时后继续对抗,直到有一个赢家出现;也就是说,如果结果出现平手,他们将继续进行。
我们只需要修改 Reach 程序,而不用修改 JavaScript 前端,但是为了接下来处理平局时再战的情况,我们将修改前端,因为现在两个人出拳时都需要加上“超时”机制了。 (记得原先我们只在 Bob 出拳时加了“超时”机制)
我们首先调整一下交互对象 Player 其中的 getHand 方法 :
tut-7/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 函数中去掉了超时代码,因为我们现在测试的方式不同了。所以把这部份恢复到之前的简单版本。
tut-7/index.mjs

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


现在,让我们看看 Reach 应用程序。游戏的所有细节和玩家接口将保持不变。唯一不同的是游戏行为发生的顺序。
以前的步骤是:

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

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

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

现在,我们就开始做这些改变 :
tut-7/index.rsh

..    // ...
42    A.only(() => {
43      const wager = declassify(interact.wager); });
44    A.publish(wager)
45      .pay(wager);
46    commit();
..    // ...
  • 第 44 行 Alice 公开并支付赌注。

tut-7/index.rsh

..    // ...
48    B.only(() => {
49      interact.acceptWager(wager); });
50    B.pay(wager)
51      .timeout(DEADLINE, () => closeTo(A, informTimeout));
52    
..    // ...
  • 第 50 行 Bob 支付赌注。
  • 注意! 第 52 行没有提交共识步骤。


现在可以开始实现反复比赛了,此时,双方会反复出拳直到结果不是平手为止。在正常的编程语言中,这样的情况会通过 while 循环实现,Reach 也是如此。然而 Reach 中的 while 循环需要特别小心,正如在 Reach循环指南中所讨论的,所以我们慢慢来。
在 Reach 程序的其余部分中,所有标识符绑定值都是静态、不可更改的,但是如果在整个 Reach 都如此,那么 while循环将永远不会开始或永远不会终止,因为循环条件永远不会改变。所以,Reach 中的 while 循环允许引入变量绑定值。
接下来,为了让 Reach 的自动验证引擎运作,我们必须在 while 循环体执行之前和之后,声明程序的哪些属性是不变的,这就是所谓的“循环不变量
最后,这样的循环可能只发生在共识步骤。这就是为什么刚刚 Bob 的交易没有提交,因为我们需要保持在共识内部来运行 while 循环 。这是为了让所有参与者都同意应用程序中控制流的方向。
结构是这样的:
tut-7/index.rsh

..    // ...
53    var outcome = DRAW;
54    invariant(balance() == 2 * wager && isOutcome(outcome) );
55    while ( outcome == DRAW ) {
..    // ...
  • 第 53 行定义了循环变量 outcome 。
  • 第 54 行声明了循环不变量,即合约账户的余额,以及有效的结果 outcome 。
  • 第 55 行以这样的条件开始循环:只要结果是平局,循环就会继续。

现在,让我们看看循环体中的其他步骤,从 Alice 对手牌的承诺开始。
tut-7/index.rsh

..    // ...
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();
..    // ...
  • 第 56 行提交了最后一个事务,在循环里一开始是 Bob 接受该赌注,随后 Alice 发布她的手势。
  • 第 58 行到第 64 行里,除了赌注是已知且已支付的以外,与之前的版本都相同。

tut-7/index.rsh

..    // ...
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();
..    // ...

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

..    // ...
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 的下一步实际上是相同的,因为她仍然以完全相同的方式发布她的手势。
接下来是循环的最后一部份。
tut-7/index.rsh

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

程序的其余部分发生在循环之外,但是内容可以与以前完全相同,不过我们将简化它,因为我们知道结果永远不会是平手。
tut-7/index.rsh

..    // ...
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(); });
  • 第 82 行断言结果不是平手 ,这是显然是正确的,否则我们就不会退出 while 循环。
  • 第 83 行将资金转给胜者。


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

$ ./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 10 to 14.9999.
Bob went from 10 to 4.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 10 to 4.9999.
Bob went from 10 to 14.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 10 to 4.9999.
Bob went from 10 to 14.9999.


跟之前一样,您运行的结果可能会有所不同,但您应该也会看到有时单轮就有胜者,有时出现多轮和来自双方的延时出现的情况。

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

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

您知道了吗?

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

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

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


您知道了吗?

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

  1. while 循环前的程序部份声明不变量。
  2. 条件和循环体必须声明不变量。
  3. 条件的否定和不变量必须声明代码其余部分的任何属性。

答案是: 以上皆是

上一篇 : 2.6 超时问题的处理

下一篇 : 2.8 交互及自主运行