在本节中,我们将继续编写我们的应用程序,让 Alice 和 Bob 可以在平手的时后继续对抗,直到有一个赢家出现;也就是说,如果结果出现平手,他们将继续进行。
我们只需要修改 Reach 程序,而不用修改 JavaScript 前端,但是我们现在将一起修改前端,以便在轮到双方提交手势时可以延迟。我们这样做是为不让他影响我们去除平手情况的工作。。
我们要把Player改为交互对象,使其具有不同的getHand方法。
tut-6/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-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 应用程序。游戏的所有细节和玩家接口将保持不变。唯一不同的是游戏行为发生的顺序
以前的步骤是:
- Alice发出她的赌约。
- Bob下注,出牌。
- Alice也出牌。
- 游戏结束。
但是,现在因为玩家可以提交多次出拳手势,但只有一个赌注,所以我们将以调整这些步骤,如下所示:
- Alice发出了她的赌约。
- Bob下注。
- Alice下注。
- Bob出牌。
- Alice出牌。
- 如果是平局,返回第 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公开并支付赌注。
.. // ...
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 行里,除了赌注是已知且已支付的以外,与之前的版本都相同。
.. // ...
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 中编写一个运行时间任意长的应用程序,比如保证不会以平局结束的石头剪刀布游戏?
- 这是不可能的,因为所有 Reach 程序都是有限时长的;
- 你可以使用一个 while 循环,一直运行到确定比赛结果为止。
答案是: 2 ; Reach 支持 while 循环。 您知道了吗?当你检查一个带有 while 循环的程序是否正确时,你需要有一个叫做循环不变量的属性。关于循环不变量,下列哪项陈述时正确的?
- while 循环前的程序部份必须建立不变量。
- 条件和循环体必须建立不变量。
- 条件的否定和不变量必须建立程序其余部分的任何属性。
答案是: 以上皆是