在这个部分中,Alice和Bob开始玩猜拳游戏!
首先,我们考虑如何表示出拳的手势。一个简单的方法是分别用数字0、1和2,代表“石头”,“布”和“剪刀”。但是,Reach不支持两位无符号整数,因此最好将它们表示为模数为3的整数等价类,即0和3是相等的,都表示石头。
我们也用一样的方法表示猜拳的三种结果:B赢,平手和A赢。
第一步要修改Reach程序,让Alice和Bob的前端交互,获取他们要出的手势,随后通知他们游戏的结果。
tut-3/index.rsh
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中,在前端中实现这些方法。
tut-3/index.mjs
.. // ...
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-2/index.rsh和tut-2/index.mjs的完整版本,并确保您正确复制了所有内容!
在下一步中,我们将增加下注机制,让Alice可以利用自己的拳法变现!
您知道了吗?:
Reach程序允许通过以下哪种方法与用户界面进行交互
- 连接到生成的智能合约的用户界面编写自定义后端
- 允许前端直接向Reach应用程序提供值,
- 允许Reach程序使用交互对象回调到前端。
答案:2和3;Reach通过参与者交互界面实现前端与后端的双向交互。
您知道了吗?: Reach应用程序中的参与者如何共享彼此信息,知道其他人共享了什么?
- Reach会生成智能合约,但是您需要实施一个流程来扫描区块链以查找与共享相对应的事件;
- Reach的原始语句publish允许参与者与所有其他参与者共享信息,该信息自动发生,不需要任何人做任何特殊的事情;
- Reach的原始语句publish允许参与者与所有其他参与者共享信息,但是他们需要显式运行接收原始语句以接收发布的信息。
回答:2; 原始语句publish替你搞定了一切!