在本节中,我们将继续编写我们的应用程序,让 Alice 和 Bob 可以在平手的时后继续对抗,直到有一个赢家出现;也就是说,如果结果出现平手,他们将继续进行。
    我们只需要修改 Reach 程序,而不用修改 JavaScript 前端,但是我们现在将一起修改前端,以便在轮到双方提交手势时可以延迟。我们这样做是为不让他影响我们去除平手情况的工作。。
    我们要把Player改为交互对象,使其具有不同的getHand方法。
    tut-6/index.mjs

    1. .. // ...
    2. 20 const Player = (Who) => ({
    3. 21 ...stdlib.hasRandom,
    4. 22 getHand: async () => { // <-- async now
    5. 23 const hand = Math.floor(Math.random() * 3);
    6. 24 console.log(`${Who} played ${HAND[hand]}`);
    7. 25 if ( Math.random() <= 0.01 ) {
    8. 26 for ( let i = 0; i < 10; i++ ) {
    9. 27 console.log(` ${Who} takes their sweet time sending it back...`);
    10. 28 await stdlib.wait(1);
    11. 29 }
    12. 30 }
    13. 31 return hand;
    14. 32 },
    15. 33 seeOutcome: (outcome) => {
    16. 34 console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
    17. 35 },
    18. 36 informTimeout: () => {
    19. 37 console.log(`${Who} observed a timeout`);
    20. 38 },
    21. 39 });
    22. .. // ...
    • 第25到30行 将Bob的acceptWager函数中的强制超时代码移到这个方法中。我们还调整了让超时只有1%的概率发生。因为这个行为并不有趣,所以将它发生的频率大大降低。

    因为我们现在测试的方式不同,所以我们在Bob的acceptWager函数中去掉了超时代码,其实就是恢复到了之前的简单版本。我们也在Bob的acceptWager函数中去掉了超时代码,因为我们现在测试的方式不同了。其实就是恢复到了之前的简单版本。
    tut-6/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. 如果是平局,返回第 3 步;否则,比赛结束。

    现在,我们就开始做这些调整。
    tut-6/index.rsh

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

    tut-6/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-6/index.rsh

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

    现在,让我们看看循环体中的剩余步骤,从 Alice 对手牌的承诺开始。
    tut-6/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-6/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-6/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-6/index.rsh

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

    程序的其余部分发生在循环之外,但是内容可以与以前完全相同,不过我们将简化它,因为我们知道结果永远不会是平手。
    tut-6/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运行
    Bob下了 5 注。
    Alice出布
    Bob出石头
    Bob看到了Alice获胜的结果
    Alice看到的结果是Alice赢了
    Alice从10变成14.9999
    Bob从10变成了4.9999。
    reach运行
    Bob下5 注。
    Alice出石头
    Bob出石头
    Alice出布
    Bob出剪刀
    Bob看到了结果Bob赢了
    Alice 看到了 Bob 获胜的结果
    Alice从10升到4.9999
    Bob从10升到了14.9999。
    reach运行
    Bob下 5注。
    Alice出剪刀
    Bob出石头
    Bob看到了结果Bob赢了
    Alice 看到了 Bob 获胜的结果
    Alice从10变成了4.9999
    Bob从10升到了14.9999。

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

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

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

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

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

    答案是: 2 ; Reach 支持 while 循环。 您知道了吗?当你检查一个带有 while 循环的程序是否正确时,你需要有一个叫做循环不变量的属性。关于循环不变量,下列哪项陈述时正确的?

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

    答案是: 以上皆是