在这个部分中, Alice 和 Bob 开始玩猜拳游戏! 首先,我们考虑如何表示出拳的手势。一个简单的方法是分别用数字 0 、 1 和 2 ,代表“石头”,“布”和“剪刀”。但是, Reach 不支持两位无符号整数,因此最好将它们表示为模数为 3 的整数等价类,即 0 和 3 是相等的,都表示石头。 我们也用一样的方法表示猜拳的三种结果: B 赢,平手和 A 赢。 第一步要修改 Reach 程序,让 Alice 和 Bob 的前端交互,获取他们要出的手势,随后通知他们游戏的结果。
1 'reach 0.1';
2
3 const Player =
4 { getHand: Fun([], UInt),
5 seeOutcome: Fun([UInt], Null) };
6
7 export const main =
8 Reach.App(
9 {},
10 [Participant('Alice', Player), Participant('Bob', Player)],
11 (A, B) => {
.. // ...
26 exit(); });
- 第 3 至 5 行定义了参与者的接口, 两个参与者都会调用该接口。在这个示例里有两个方法: getHand ,它会返回一个数字; seeOutcome ,它会接收一个数字。
- 第 10 行两个参与者都调用了该接口。通过这行代码,两个参与者实例将被绑定到前端代码中去。
在继续编写 Reach 程序之前,我们回到 JavaScript 中,在前端中实现这些方法。
.. // ...
14 const HAND = ['Rock', 'Paper', 'Scissors'];0
15 const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
16 const Player = (Who) => ({
17 getHand: () => {
18 const hand = Math.floor(Math.random() * 3);
19 console.log(`${Who} played ${HAND[hand]}`);
20 return hand;
21 },
22 seeOutcome: (outcome) => {
23 console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
24 },
25 });
26
27 await Promise.all([
28 backend.Alice(
29 ctcAlice,
30 Player('Alice'),
31 ),
32 backend.Bob(
33 ctcBob,
34 Player('Bob'),
35 ),
36 ]);
.. // ...
- 第 14 和 15 行定义了数组,表示出拳手势和猜拳结果的内容。
- 第 16 行定义了 Player 实现的构造函数。
- 第 17 至 21 行定义了 getHand 方法。
- 第 22 至 24 行定义了 seeOutcome 方法。
- 最后,第 30 和 34 行分别为 Alice 和 Bob 实例化对象。这些是 Reach 程序中绑定的实例化对象。
这部份代码相当简单,没有特别之处;这就是 Reach 的优点:我们只需要编写业务逻辑,而不必关心共识网络和去中心化应用程序的细节。
让我们回到 Reach 程序,研究 Alice 和 Bob 该怎么操作。 在现实生活中的“剪刀石头布”中, Alice 和 Bob 同时决定他们要出哪种手势并同时出拳。“同时性”是一个复杂的概念,很难实践。例如,跟小孩猜拳时,您可能会发现他们"慢出"---试图在看到您出的手势后才出拳,以获得胜利。但是,在去中心化应用程序中,同时是不可能实现的。反之,必须让一个参与者“先出”。在这个示例中,我们将让 Alice 先出。
是 Alice 先出,还是我们将先出的玩家称为 Alice ?似乎没必要这样区分,但是 Reach 巧妙地利用了这一点。在前端,我们明确区分了 backend.Alice 和 backend.Bob 。这样我们把特定的 JavaScript 线程提交为 Alice 或 Bob 。在我们的游戏中,先运行 Alice 后端的人就是先出拳的人。在更后面的教程里,用户可以选择要扮演的角色,届时这个概念将更加明确。
游戏分三步进行。 首先, Alice 的后端与前端进行交互,获取她的手势,然后发布它。
.. // ...
12 A.only(() => {
13 const handA = declassify(interact.getHand()); });
14 A.publish(handA);
15 commit();
.. // ...
- 第 12 行指出此代码块仅由 A (即 Alice )执行。
- 这意味着第 13 行上绑定的变量 handA 只对 Alice 可见。
- 第 13 行将 Alice 的手势赋给变量 handA 。
- 第 13 行还对值进行了解密,因为在 Reach 中,来自前端的所有信息都是被加密了。
- 第 14 行 Alice 将值发布到共识网络,从而可以判断游戏的结果。一旦到了这一步,代码将处于所有参与者共同参与的“共识步骤”。
- 第 15 行提交了共识网络的状态,并返回到“本地步骤”,此时每个参与者可以单独运行。
下一步也类似, Bob 发布了他的手势。不同的是,此时我们不立即提交状态,而是先计算猜拳的结果。
.. // ...
17 B.only(() => {
18 const handB = declassify(interact.getHand()); });
19 B.publish(handB);
20
21 const outcome = (handA + (4 - handB)) % 3;
22 commit();
.. // ...
- 第 17 至 19 行与 Alice 的步骤类似,通过共识转移发布该应用程序。
- 第 21 行在提交前先计算游戏的结果。((handA + (4-handB))%3 是一个巧妙的运算,用于使用公式化的算法计算猜拳的赢家。考虑当handA为0(即石头)和handB为2(即剪刀)时,等式为 ((handA +(4-handB))%3)=((0 +(4-2))%3)=((0 + 2)%3)=(2%3)= 2,表示A获胜,和我们预期的一样。)
最后,我们调用 each 方法将每个参与者运行的结果发送到他们的前端。
.. // ...
24 each([A, B], () => {
25 interact.seeOutcome(outcome); });
.. // ...
- 第 24 行指出遍历参与者 A 和 B 。
此时,我们可以运行程序查看输出结果 $ ./reach run
玩家的运行结果是随机的,因此每次的结果都会有所不同。例如我们运行该程序三次,得到的结果如下:
$ ./reach run
Alice played Scissors
Bob played Paper
Alice saw outcome Alice wins
Bob saw outcome Alice wins
$ ./reach run
Alice played Scissors
Bob played Paper
Alice saw outcome Alice wins
Bob saw outcome Alice wins
$ ./reach run
Alice played Paper
Bob played Rock
Alice saw outcome Alice wins
Bob saw outcome Alice wins
( Alice 很会猜拳!!)
我们可以看到,共识网络(尤其是 Reach )保证所有参与者都同意各自计算的结果。这就是共识网络这个名称的来源,因为它们使这些分散且不受信任的各方能够就计算的中间状态达成共识协议。如果他们同意中间状态,那么他们也会同意输出结果。这就是为什么每次运行./reach run
时, Alice 和 Bob 都会看到相同的结果!
如果您的代码不能正确运行,请查看 tut-3/index.rsh 和 tut-3/index.mjs 的完整版本,并确保您正确复制了所有内容!
在下一步中,我们将增加下注机制,让 Alice 可以利用自己的拳法变现!
您知道了吗?:
Reach 程序允许通过以下哪种方法与用户界面进行交互
- 连接到生成的智能合约的用户界面编写自定义后端
- 允许前端直接向 Reach 应用程序提供值,
- 允许 Reach 程序使用交互对象回调到前端。
答案: 2 和 3 ; Reach 通过参与者交互界面实现前端与后端的双向交互。
您知道了吗?:
Reach 应用程序中的参与者如何共享彼此信息,知道其他人共享了什么?
- Reach 会生成智能合约,但是您需要实施一个流程来扫描区块链以查找与共享相对应的事件;
- Reach 的原始语句 publish 允许参与者与所有其他参与者共享信息,该信息自动发生,不需要任何人做任何特殊的事情;
- Reach 的原始语句 publish 允许参与者与所有其他参与者共享信息,但是他们需要显式运行接收原始语句以接收发布的信息。
答案:2 ; 原始语句 publish 替你搞定了一切!
上一篇 : 2.2 创建脚手架并进行相关设置
下一篇 : 2.4 打赌与下注