Reach Workshop — Budget Tic-Tac-Toe

During the course of this workshop, we will gradually develop a budget tic-tac-toe smart contract using Reach, we will also develop the apps frontend to enable user interaction with the contract.

Screenshot 2022-09-27 at 06.35.54.png

Prerequisites

  • Create a directory for your project:

    $ mkdir -p ~/reach/workshop-tic-tac-toe && cd ~/reach/workshop-tic-tac-toe

  • Install Reach into this project directory

  • To ensure the executable is working, run :

    $ ./reach version

  • To get the base files for a reach project, run:

    $ ./reach init

Project Scope

The tic-tac-toe game is naturally played on three-by-three grid by 2 players, who alternately place the marks X and O in one of the nine spaces in the grid. The aim is for a player to successfully place their own character consecutively on 3 cells either horizontally, vertically, or diagonally. We are going to go a step further and add a wager to the game to make it more interesting.

Participants in the game: Includes the player1, the player that creates or starts the game, and the player2, the player that joins an already existing game

The flow of the program

  • Player1 creates a game contract, places a wager, and sets a deadline
  • Player2 connects to the contract and accepts the wager
  • Player1 plays a hand, we check if the game has been won, the hand and the win status are published
  • Player2 plays a hand, we check if the game has been won, the hand and the win status are published
  • if a player wins the get the total amount staked for the game, if the game is a draw, the stake is splitter both ways.

Side cases

  • If player2 does not accept the wager: the game will timeout, and return the stake to player1
  • if any player does not play their hand before the deadline, the game will timeout, and the total stake goes to the other player

Let's Start Coding

We have run the init command earlier, therefore we should have a basic folder structure consisting of index.mjs and index.rsh

NB: this was written and run with reach version 0.1.12

your index.rsh file should look like this

1. 'reach 0.1';
2. 
3. export const main = Reach.App(() => {
4.   const A = Participant('Alice', {
5.     // Specify Alice's interact interface here
6.   });
7.   const B = Participant('Bob', {
8.     // Specify Bob's interact interface here
9.   });
10.   init();
11.   });
  • Line 1 indicates that this is a Reach program. You'll always have this at the top of every program
  • Line 3 defines the main export from the program. When you compile, this is what the compiler will look at
  • Lines 4 through 9 specify the two participants to this application, Alice and Bob.
  • Line 10 marks the deployment of the the Reach program, which allows the program to start doing things.

your index.mjs file should look like this

1. import {loadStdlib} from '@reach-sh/stdlib';
2. import * as backend from './build/index.main.mjs';
3. const stdlib = loadStdlib(process.env);
4. 
5. const startingBalance = stdlib.parseCurrency(100);
6. 
7. const [ accAlice, accBob ] =
8.   await stdlib.newTestAccounts(2, startingBalance);
9. console.log('Hello, Alice and Bob!');
10. 
11. console.log('Launching...');
12. const ctcAlice = accAlice.contract(backend);
13. const ctcBob = accBob.contract(backend, ctcAlice.getInfo());
14. 
15. console.log('Starting backends...');
16. await Promise.all([
17.   backend.Alice(ctcAlice, {
18.     ...stdlib.hasRandom,
19.     // implement Alice's interact object here
20.   }),
21.   backend.Bob(ctcBob, {
22.     ...stdlib.hasRandom,
23.     // implement Bob's interact object here
24.   }),
25. ]);
26. 
27. console.log('Goodbye, Alice and Bob!');

Line 1 imports the Reach standard library loader. Line 2 imports the compiled backend from index.rsh which ../reach compile generates Line 3 imports the standard library dynamically based on the REACH_CONNECTOR_MODE environment variable. Line 5 specifies the amount of the network tokens as the starting balance for each test account. parseCurrency converts the number of network tokens in the standard unit to the equivalent amount in the atomic unit Lines 7 and 8 creates a test account for Alice and Bobwith an initial amount of tokens to use to perform transactions in our Dapp. This works because Reach provides a developer testing network. Line 12 has Alice deploy the contract with the backend file generated when the Reach program compiles. the return contract is stored in the ctcAlice variable Line 13 has Bob attached to the contract. Bob will need to provide contract info which is gotten from the contract deployed by Alice. ctcAlice.getInfo() gets the contract info. Line 16 waits for the backend to complete. Line17 through to 20 initialises backend for Alice Line21 through to 24 initialises backend for Bob

This is now enough for Reach to compile and run our program. Let's try by running

$ ./reach run

or

$ REACH_CONNECTOR_MODE=ALGO-devnet ./reach run

We are going to be working with Player1 and Player2 instead of Alice and Bob, therefore we have to update our code to look like this

index.rsh

1. "reach 0.1";
2. 
3. export const main = Reach.App(() => {
4.   const Player1 = Participant("Player1", {});
5. 
6.   const Player2 = Participant("Player2", {});
7. 
8.   init();
9. });

index.mjs

1. import { loadStdlib } from "@reach-sh/stdlib";
2. import * as backend from "./build/index.main.mjs";
3. const stdlib = loadStdlib(process.env);
4. 
5. const startingBalance = stdlib.parseCurrency(100);

6. const [accPlayer1, accPlayer2] = await stdlib.newTestAccounts(
7.   2,
8.   startingBalance
9. );
10. 
11. console.log("Launching...");
12. const ctcPlayer1 = accPlayer1.contract(backend);
13. const ctcPlayer2 = accPlayer2.contract(backend, ctcPlayer1.getInfo());
14. 
15. console.log("Starting backends...");
16. await Promise.all([
17.   backend.Player1(ctcPlayer1, {
18.     ...stdlib.hasRandom,
19.     // implement Alice's interact object here
20.   }),
21.   backend.Player2(ctcPlayer2, {
22.     ...stdlib.hasRandom,
23.     // implement Bob's interact object here
24.   }),
25. ]);

We will now go ahead to specify the actions these Players can carry out. Any action specific to a player will be defined inside their object, and that which can be carried out by both players will be defined in a Player object which will be used by both participants. Applying these changes to the index.rsh file we will have something like this

index.rsh

1. "reach 0.1";
2. 
3. const Player = {
4.   ...hasRandom,
5.   getHand: Fun([], UInt),
6.   seeHand: Fun([UInt, UInt], Null),
7.   seeOutcome: Fun([UInt], Null),
8.   informTimeout: Fun([], Null),
9.    informCompletion: Fun([], Null),
10.  };
11.  
12.  export const main = Reach.App(() => {
13.    const Player1 = Participant("Player1", {
14.      ...Player,
15.      wager: UInt,
16.      deadline: UInt,
17.    });
18.  
19.    const Player2 = Participant("Player2", {
20.      ...Player,
21.      acceptWager: Fun([UInt], Null),
22.    });
23.  
24.    init();
25.  });

wager: this is the wager provided by the prayer after creating the smart contract deadline: is the duration before timeout acceptWager: this function is called by the Player2, it requests that the player2 accepts the wager made by Player1 getHand: this function gets the hand played by any Player calling the function seeOutcome: this function shows the outcome of the game to any player calling the function informTimeout: this function informs the players when a timeout occurs in the game informCompletion: this function informs the players that the game has ended

The input and output types for these functions are specified.

Now, lets update our index.mjs to define these functions, first define these variables:

index.mjs

16. let AllowedHands = [0, 1, 2, 3, 4, 5, 6, 7, 8];
17. const OUTCOME = ["Player1 wins", "Draw", "Player2 wins"];
18. const Players = ["", "Player1", "player2"];
19.  let selectedHands = [];
  • Line 16 contains an array of all the hands a player can play
  • Line 17 contains the possible outcomes of the game
  • Line 18 an array of players
  • Line 19 will contain all the hands played in the game

we will now define the Player function which will return an object of all the attributes of Player in our index.rsh file

index.mjs

21. const Player = (Who) => ({
22.   ...stdlib.hasRandom,
23.   getHand: async () => {
24.     var hand = 0;
25. 
26.     hand = AllowedHands[Math.floor(Math.random() * AllowedHands.length)];
27.     AllowedHands = AllowedHands.filter((item) => item !== hand);
28. 
29.     console.log(`${Who} played ${hand}`);
30.     selectedHands += [hand];
31.     return hand;
32.   },
33.   seeHand: (player, hand) => {
34.     console.log(`${Players[player]} played ${hand}`);
35.   },
36.   seeOutcome: (outcome) => {
37.     console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
38.   },
39.   informTimeout: () => {
40.     console.log(`${Who} observed a timeout`);
41.   },
42.   informCompletion: () => {
43.     console.log(`Game has ended`);
44.   },
45. });
  • The getHand function selects a random number from the AllowedHands array, removes it from the array and returns the number.
  • seeHand function logs the hand played by a particular player
  • seeOutcome logs the outcome of the game
  • informTimeout logs any timeout observed
  • and the informCompletion logs the game has ended

index.mjs

48. await Promise.all([
49.   ctcPlayer1.p.Player1({
50.     ...Player("Player1"),
51.     wager: stdlib.parseCurrency(5),
52.     deadline: 10,
53.   }),
54.   ctcPlayer2.p.Player2({
55.     ...Player("Player2"),
56.     acceptWager: (amt) => {
57.       console.log(`Player2 accepts the wager of ${fmt(amt)}.`);
58.     },
59.   }),
60. ]);

index.mjs

  • Lines 50 and 55 attaches Player function we defined earlier to the backend
  • Line 50 specifies the wager
  • Line 52 specifies the deadline
  • Line 56 to 58 defines the accept wager function

We will like to know the balance before and after the game, therefore we will add the code like this:

index.mjs

12. const fmt = (x) => stdlib.formatCurrency(x, 4);
13. const getBalance = async (who) => fmt(await stdlib.balanceOf(who));
14. const beforePlayer1 = await getBalance(accPlayer1);
15. const beforePlayer2 = await getBalance(accPlayer2);
  • Line 12 defines a function for formatting the balance of the accounts
  • Line 13 defines a function for getting the balance of any account
  • Line 14 and 15 are the balances before the game

index.mjs

67. const afterPlayer1 = await getBalance(accPlayer1);
68. const afterPlayer2 = await getBalance(accPlayer2);
69. console.log(`Player1 went from ${beforePlayer1} to ${afterPlayer1}.`);
70. console.log(`Player2 went from ${beforePlayer2} to ${afterPlayer2}.`);
  • Line 67 and 68 are the balances after the game

At this point you can run

REACH_CONNECTOR_MODE=ALGO-devnet ./reach run

to ensure your code runs properly

Now we will go over to the index.rsh to complete the backend. We will first define a function to check if a set of hands has won the game.

index.rsh

3. const isWinner = (uHands) => {
4.   const ex = uHands;
5.   const one = ex[0] * ex[1] * ex[2];
6.   const two = ex[0] * ex[4] * ex[8];
7.   const three = ex[0] * ex[3] * ex[6];
8.   const four = ex[1] * ex[4] * ex[7];
9.   const five = ex[2] * ex[5] * ex[8];
10.   const six = ex[2] * ex[4] * ex[6];
11.   const seven = ex[3] * ex[4] * ex[5];
12.   const eigth = ex[6] * ex[7] * ex[8];
13.   const winner =
14.     one == 1
15.       ? 0
16.       : one == 8
17.       ? 2
18.       : two == 1
19.       ? 0
20.       : two == 8
21.       ? 2
22.       : three == 1
23.       ? 0
24.       : three == 8
25.       ? 2
26.       : four == 1
27.       ? 0
28.       : four == 8
29.       ? 2
30.       : five == 1
31.       ? 0
32.       : five == 8
33.       ? 2
34.       : six == 1
35.       ? 0
36.       : six == 8
37.       ? 2
38.       : seven == 1
39.       ? 0
40.       : seven == 8
41.       ? 2
42.       : eigth == 1
43.       ? 0
44.       : eigth == 8
45.       ? 2
46.       : 1;
47.   return winner;
48. };

The is mostly a static way of determining whether a set of hands won the game. uHands is an array of number of length 9 with each of its positions indicating a part of a tic-tac-toe board, and the value indicating the user that played that position or the position has not been player(with value 0).

The positions that are required to win the game have been noted and the product of those positions are taken on Line 5 through to 12.

for Player1 to win, one of the products have to be equal to one, and for Player2 to win, one of the products have to be equal to 8. Line 13 to 46 check each products to return whether player1 or player2 won, or the game is a draw.

we will define to functions that will interact with both players at the same time

index.rsh

73. const informTimeout = () => {
74.     each([Player1, Player2], () => {
75.       interact.informTimeout();
76.     });
77.   };
78.   const informCompletion = () => {
79.     each([Player1, Player2], () => {
80.       interact.informCompletion();
81.     });
82.   };

The informTimeout calls informTimeout() for each of the player, and the informCompletion calls informCompletion() for each of the players.

Now we can work on the interaction of the players with the contract, and the order which it will happen.

First, we want Player1 to initiate the contract, provide a wager and a deadline, then Player2 joins the contract and accepts the wager.

index.rsh

84.  Player1.only(() => {
85.     const wager = declassify(interact.wager);
86.     const deadline = declassify(interact.deadline);
87.   });
88.   Player1.publish(wager, deadline).pay(wager);
89.   commit();
90. 
91.   Player2.only(() => {
92.     interact.acceptWager(wager);
93.   });
94.   Player2.pay(wager).timeout(relativeTime(deadline), () =>
95.     closeTo(Player1, informTimeout)
96.   );
97.   commit();
  • Line 84 through to 87 is a Player1 only action, where the wager and deadline are gotten from the interaction.
  • Line 88 publishes the wager and deadline, and takes the wager from the balance of Player1
  • Line 91 through to 93 is a Player2 only action, where the player accepts the wager
  • Line 94 through to 96 deducts the wager from Player2's balance if the wager is accepted, and will timeout if the wager is not accepted.

index.rsh

99. const board = array(UInt, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
  • Line 99 is an array that represent the base value of the board before the game starts, where all the positions have the value 0.

We will now start getting the hands played by each player from this point.

index.rsh

101. //------------------------------ get first hand ------------------------------------------//
102.   Player1.only(() => {
103.     const hand1Player01 = declassify(interact.getHand());
104.     const hand1Player1 = hand1Player01 < 9 ? hand1Player01 : 9;
105.     assume(hand1Player1 < 9);
106.   });
107.   Player1.publish(hand1Player1).timeout(relativeTime(deadline), () =>
108.     closeTo(Player2, informTimeout)
109.   );
110.   require(hand1Player1 < 9);
111.   commit();
112. 
113.   each([Player1, Player2], () => {
114.     interact.seeHand(1, hand1Player1);
115.   });
116. 
117.   const board1 = board.set(hand1Player1, 1);
118. 
119.   Player2.only(() => {
120.     const hand1Player02 = declassify(interact.getHand());
121.     const hand1Player2 = hand1Player02 < 9 ? hand1Player02 : 9;
122.     assume(hand1Player2 < 9);
123.   });
124.   Player2.publish(hand1Player2).timeout(relativeTime(deadline), () =>
125.     closeTo(Player1, informTimeout)
126.   );
127.   require(hand1Player2 < 9);
128.   commit();
129.   each([Player1, Player2], () => {
130.     interact.seeHand(2, hand1Player2);
131.   });
132.   const board11 = board1.set(hand1Player2, 2);
133. 
134.   //------------------------------ get first hand ------------------------------------------//

Here we are getting the first hands of Player1 and Player2.

  • Line 102 through to 106 gets the hand of Player1 and ensure it is less than 9
  • Line 107 through to 109 publishes the hand and observes a timeout if the player doesn't provide a line before the deadline
  • Line 110 checks whether the hand is less than 9
  • Line 113 through to 115 calls the seeHand function for each Player to show each player the hand played
  • Line 117 updates the board with the Hand played and the player that played the hand
  • Line 119 through to 132 does the same for Player 2

index.rsh

136. //-------------------------------- get second hand --------------------------------------//
137.   Player1.only(() => {
138.     const hand2Player01 = declassify(interact.getHand());
139.     const hand2Player1 = hand2Player01 < 9 ? hand2Player01 : 9;
140.     assume(hand2Player1 < 9);
141.   });
142.   Player1.publish(hand2Player1).timeout(relativeTime(deadline), () =>
143.     closeTo(Player2, informTimeout)
144.   );
145.   require(hand2Player1 < 9);
146.   commit();
147.   each([Player1, Player2], () => {
148.     interact.seeHand(1, hand2Player1);
149.   });
150.   const board2 = board11.set(hand2Player1, 1);
151. 
152.   Player2.only(() => {
153.     const hand2Player02 = declassify(interact.getHand());
154.     const hand2Player2 = hand2Player02 < 9 ? hand2Player02 : 9;
155.     assume(hand2Player2 < 9);
156.   });
157.   Player2.publish(hand2Player2).timeout(relativeTime(deadline), () =>
158.     closeTo(Player1, informTimeout)
159.   );
160.   require(hand2Player2 < 9);
161.   commit();
162.   each([Player1, Player2], () => {
163.     interact.seeHand(2, hand2Player2);
164.   });
165.   const board22 = board2.set(hand2Player2, 2);
166.   //-------------------------------- get second hand --------------------------------------//

The same actions on the first hand are done for the second hand, because a tic-tac-toe game can only be won from the third hand.

index.rsh

168. //--------------------------------- get third hand --------------------------------------//
169.   Player1.only(() => {
170.     const hand3Player01 = declassify(interact.getHand());
171.     const hand3Player1 = hand3Player01 < 9 ? hand3Player01 : 9;
172.     assume(hand3Player1 < 9);
173.     const board3 = board22.set(hand3Player1, 1);
174.     const p1win3 = isWinner(board3);
175.   });
176.   Player1.publish(hand3Player1, p1win3).timeout(relativeTime(deadline), () =>
177.     closeTo(Player2, informTimeout)
178.   );
179.   commit();
180. 
181.   each([Player1, Player2], () => {
182.     interact.seeHand(1, hand3Player1);
183.     interact.seeOutcome(p1win3);
184.   });
185. 
186.   const [timeRemaining31, keepGoing31] = makeDeadline(
187.     p1win3 == 0 ? 0 : deadline
188.   );
189. 
190.   Player1.publish(board3)
191.     .when(p1win3 != 0)
192.     .timeout(timeRemaining31(), () => closeTo(Player1, informCompletion));
193.   commit();
194. 
195.   Player2.only(() => {
196.     const hand3Player02 = declassify(interact.getHand());
197.     const hand3Player2 = hand3Player02 < 9 ? hand3Player02 : 9;
198.     assume(hand3Player2 < 9);
199.     const board33 = board3.set(hand3Player2, 2);
200.     const p2win3 = isWinner(board33);
201.   });
202. 
203.   Player2.publish(hand3Player2, p2win3).timeout(relativeTime(deadline), () =>
204.     closeTo(Player1, informTimeout)
205.   );
206.   commit();
207. 
208.   each([Player1, Player2], () => {
209.     interact.seeHand(2, hand3Player2);
210.     interact.seeOutcome(p2win3);
211.   });
212.   const [timeRemaining32, keepGoing32] = makeDeadline(
213.     p2win3 == 2 ? 0 : deadline
214.   );
215.   Player2.publish(board33)
216.     .when(p2win3 != 2)
217.     .timeout(timeRemaining32(), () => closeTo(Player2, informTimeout));
218.   commit();
219. 
220.   //--------------------------------- get third hand --------------------------------------//

Because a player can win from this point onwards, a few things have changed

  • Line 173 updated the board with the new hand
  • Line 174 the winner was checked
  • Line 176 through to 179, the player hand and the winner were publish, and the game will observe a timeout if Player1 doesn't play a hand before the deadline
  • Line 181 through to 184 both the seeHand and seeOutcome are called for both players, to show the players both the player's hand and the outcome of the game
  • Line 186 through to 188 the deadline is updated to 0 if Player1 has won the game otherwise remains the same
  • Line 190 through to 193, Player1 publishes the updated board if there is no winner else the game observes a timeout and the wage goes to Player1
  • Line 195 through to 218 does the same for Player2

This will be repeated for the fourth and the fifth hands

index.rsh

222. //--------------------------------- get fourth hand --------------------------------------//
223.   Player1.only(() => {
224.     const hand4Player01 = declassify(interact.getHand());
225.     const hand4Player1 = hand4Player01 < 9 ? hand4Player01 : 9;
226.     assume(hand4Player1 < 9);
227.     const board4 = board33.set(hand4Player1, 1);
228.     const p1win4 = isWinner(board4);
229.   });
230. 
231.   Player1.publish(hand4Player1, p1win4).timeout(relativeTime(deadline), () =>
232.     closeTo(Player2, informTimeout)
233.   );
234.   commit();
235. 
236.   each([Player1, Player2], () => {
237.     interact.seeHand(1, hand4Player1);
238.     interact.seeOutcome(p1win4);
239.   });
240. 
241.   const [timeRemaining41, keepGoing41] = makeDeadline(
242.     p1win4 == 0 ? 0 : deadline
243.   );
244. 
245.   Player1.publish(board4)
246.     .when(p1win4 != 0)
247.     .timeout(timeRemaining41(), () => closeTo(Player1, informCompletion));
248.   commit();
249. 
250.   Player2.only(() => {
251.     const hand4Player02 = declassify(interact.getHand());
252.     const hand4Player2 = hand4Player02 < 9 ? hand4Player02 : 9;
253.     assume(hand4Player2 < 9);
254.     const board44 = board4.set(hand4Player2, 2);
255.     const p2win4 = isWinner(board44);
256.   });
257. 
258.   Player2.publish(hand4Player2, p2win4).timeout(relativeTime(deadline), () =>
259.     closeTo(Player1, informTimeout)
260.   );
261.   commit();
262. 
263.   each([Player1, Player2], () => {
264.     interact.seeHand(2, hand4Player2);
265.     interact.seeOutcome(p2win4);
266.   });
267.   const [timeRemaining42, keepGoing42] = makeDeadline(
268.     p2win4 == 2 ? 0 : deadline
269.   );
270.   Player2.publish(board44)
271.     .when(p2win4 != 2)
272.     .timeout(timeRemaining42(), () => closeTo(Player2, informTimeout));
273.   commit();
274. 
275.   //--------------------------------- get fourth hand --------------------------------------//
276. 
277.   //--------------------------------- get fifth hand --------------------------------------//
278.   Player1.only(() => {
279.     const hand5Player01 = declassify(interact.getHand());
280.     const hand5Player1 = hand5Player01 < 9 ? hand5Player01 : 9;
281.     assume(hand5Player1 < 9);
282.     const board5 = board44.set(hand5Player1, 1);
283.     const p1win5 = isWinner(board4);
284.   });
285. 
286.   Player1.publish(hand5Player1, p1win5).timeout(relativeTime(deadline), () =>
287.     closeTo(Player2, informTimeout)
288.   );
289.   commit();
290. 
291.   each([Player1, Player2], () => {
292.     interact.seeHand(1, hand5Player1);
293.     interact.seeOutcome(p1win5);
294.   });
295. 
296.   const [timeRemaining51, keepGoing51] = makeDeadline(
297.     p1win5 == 0 ? 0 : deadline
298.   );
299. 
300.   Player1.publish(board5)
301.     .when(p1win5 != 0)
302.     .timeout(timeRemaining51(), () => closeTo(Player1, informCompletion));
303. 
304.   //--------------------------------- get fifth hand --------------------------------------//

if the code gets to this point, it means no player has won the game and the game is a draw because all positions on the board have been selected.

index.rsh

306. each([Player1, Player2], () => {
307.     interact.seeOutcome(1);
308.   });
309.   transfer(wager).to(Player1);
310.   transfer(wager).to(Player2);
311.   informCompletion();
312.   commit();

we can now go ahead and split the total wager since the game ended in a draw

  • Line 306 through to 308 is sending the outcome of draw to the players
  • Line 309 and 310 are transferring half of the total wager to each player
  • Line 311 is informing both players that the game has ended.

To check your code at this point, run:

$ REACH_CONNECTOR_MODE=ALGO-devnet ./reach run

You should have a similar output on your terminal:

Verifying knowledge assertions
Verifying for generic connector
  Verifying when ALL participants are honest
  Verifying when NO participants are honest
Checked 354 theorems; No failures!
[+] Building 1.0s (7/7) FINISHED                                                                                  
 => [internal] load build definition from Dockerfile                                                         0.0s
 => => transferring dockerfile: 310B                                                                         0.1s
 => [internal] load .dockerignore                                                                            0.0s
 => => transferring context: 75B                                                                             0.0s
 => [internal] load metadata for docker.io/reachsh/runner:0.1.12                                             0.0s
 => CACHED [1/2] FROM docker.io/reachsh/runner:0.1.12                                                        0.0s
 => [internal] load build context                                                                            0.1s
 => => transferring context: 306.55kB                                                                        0.1s
 => [2/2] COPY . /app                                                                                        0.1s
 => exporting to image                                                                                       0.1s
 => => exporting layers                                                                                      0.1s
 => => writing image sha256:cbf68277fe1e6e85950026045f38b44a4fcce15389f9f74b075efb682ca09414                 0.0s
 => => naming to docker.io/reachsh/reach-app-workshop:0.1.12                                                 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Creating 2022-09-27t15-53-47z-id80_reach-app-workshop_run ... done

> index
> node --experimental-modules --unhandled-rejections=strict index.mjs

Warning: your program uses stdlib.fundFromFaucet. That means it only works on Reach devnets!
Warning: your program uses stdlib.fundFromFaucet. That means it only works on Reach devnets!
Launching...
Starting backends...
Player2 accepts the wager of 5.
Player1 played 4
Player1 played 4
Player1 played 4
Player2 played 5
player2 played 5
player2 played 5
Player1 played 1
Player1 played 1
Player1 played 1
Player2 played 8
player2 played 8
player2 played 8
Player1 played 7
Player1 played 7
Player1 saw outcome Player1 wins
Game has ended
Player1 played 7
Player2 saw outcome Player1 wins
Game has ended
Player1 went from 100 to 104.99.
Player2 went from 100 to 94.996.

We would like to have a more interactive frontend, therefore we will be making use of the ask class in @reach-sh/stdlib

1. import { loadStdlib, ask } from "@reach-sh/stdlib";
2. import * as backend from "./build/index.main.mjs";
3. const stdlib = loadStdlib();
4. 
5. const isPlayer1 = await ask.ask(`Are you Player1?`, ask.yesno);
6. const who = isPlayer1 ? "Player1" : "Player2";
7. 
8. console.log(`Starting Tic Tac Toe! as ${who}`);
9. 
10. let acc = null;
11. const createAcc = await ask.ask(
12.   `Would you like to create an account? (only possible on devnet)`,
13.   ask.yesno
14. );
15. if (createAcc) {
16.   acc = await stdlib.newTestAccount(stdlib.parseCurrency(1000));
17. } else {
18.   const secret = await ask.ask(`What is your account secret?`, (x) => x);
19.   acc = await stdlib.newAccountFromSecret(secret);
20.   // acc = await stdlib.newAccountFromMnemonic(secret); //if you want to use Mnemonic Phrase
21. }
22. 
23. let ctc = null;
24. if (isPlayer1) {
25.   ctc = acc.contract(backend);
26.   ctc.getInfo().then((info) => {
27.     console.log(`The contract is deployed as = ${JSON.stringify(info)}`);
28.   });
29. } else {
30.   const info = await ask.ask(
31.     `Please paste the contract information:`,
32.     JSON.parse
33.   );
34.   ctc = acc.contract(backend, info);
35. }
36. 
37. const fmt = (x) => stdlib.formatCurrency(x, 4);
38. const getBalance = async () => fmt(await stdlib.balanceOf(acc));
39. 
40. const before = await getBalance();
41. console.log(`Your balance is ${before}`);
42. 
43. const interact = { ...stdlib.hasRandom };
44. 
45. interact.informCompletion = async () => {
46.   console.log(`Game Over`);
47.   const after = await getBalance();
48.   console.log(`Your balance is now ${after}`);
49.   process.exit(1);
50. };
51. 
52. interact.getBalance = async () => {
53.   const cb = await getBalance();
54.   console.log(`your current balance is ${cb}`);
55.   cb = Math.floor(cb);
56.   return cb;
57. };
58. 
59. interact.informTimeout = () => {
60.   console.log(`There was a timeout.`);
61.   process.exit(1);
62. };
63. 
64. if (isPlayer1) {
65.   const amt = await ask.ask(
66.     `How much do you want to wager?`,
67.     stdlib.parseCurrency
68.   );
69.   interact.wager = amt;
70.   interact.deadline = { ETH: 100, ALGO: 100, CFX: 1000 }[stdlib.connector];
71. } else {
72.   interact.acceptWager = async (amt) => {
73.     const accepted = await ask.ask(
74.       `Do you accept the wager of ${fmt(amt)}?`,
75.       ask.yesno
76.     );
77.     if (!accepted) {
78.       process.exit(0);
79.     }
80.   };
81. }
82. 
83. const HAND = [0, 1, 2, 3, 4, 5, 6, 7, 8];
84. const Players = ["", "Player1", "player2"];
85. let selectedHands = [];
86. 
87. interact.getHand = async () => {
88.   const hand = await ask.ask(`What hand will you play?`, (x) => {
89.     if (x > 8 || x < 0) {
90.       throw Error(`Not a valid hand ${hand}`);
91.     }
92.     const hand = HAND[x];
93.     if (hand === undefined) {
94.       throw Error(`Not a valid hand ${hand}`);
95.     }
96.     if (selectedHands.includes(hand)) {
97.       throw Error(`${hand} has already been played`);
98.     }
99.     return hand;
100.   });
101.   console.log(`You played ${hand}`);
102.   selectedHands += [hand];
103.   return hand;
104. };
105. 
106. interact.seeHand = async (player, hand) => {
107.   selectedHands += [hand];
108.   console.log(`${Players[player]} played ${hand}`);
109. };
110. 
111. const OUTCOME = ["Player1 wins", "Draw", "Player2 wins"];
112. interact.seeOutcome = async (outcome) => {
113.   console.log(`The outcome is: ${OUTCOME[outcome]}`);
114. };
115. 
116. const part = isPlayer1 ? ctc.p.Player1 : ctc.p.Player2;
117. await part(interact);
118. 
119. const after = await getBalance();
120. console.log(`Your balance is now ${after}`);
121. 
122. ask.done();

you can interact with this by running it on 2 separate terminals, just as shown below:

Terminal 1

Are you Player1?
y
Starting Tic Tac Toe! as Player1
Would you like to create an account? (only possible on devnet)
y
Warning: your program uses stdlib.fundFromFaucet. That means it only works on Reach devnets!
Your balance is 1000
How much do you want to wager?
10
The contract is deployed as = {"type":"BigNumber","hex":"0x2d"}
What hand will you play?
0
You played 0
Player1 played 0
player2 played 4
What hand will you play?
1
You played 1
Player1 played 1
player2 played 7
What hand will you play?
3
You played 3
Player1 played 3
The outcome is: Draw
player2 played 8
The outcome is: Draw
What hand will you play?
2
You played 2
Player1 played 2
The outcome is: Player1 wins
Game Over
Your balance is now 1009.989
ERROR: 1

Terminal 2

Are you Player1?
n
Starting Tic Tac Toe! as Player2
Would you like to create an account? (only possible on devnet)
y
Warning: your program uses stdlib.fundFromFaucet. That means it only works on Reach devnets!
Please paste the contract information:
{"type":"BigNumber","hex":"0x2d"}
Your balance is 1000
Do you accept the wager of 10?
y
Player1 played 0
What hand will you play?
4
You played 4
player2 played 4
Player1 played 1
What hand will you play?
7
You played 7
player2 played 7
Player1 played 3
The outcome is: Draw
What hand will you play?
8
You played 8
player2 played 8
The outcome is: Draw
Player1 played 2
The outcome is: Player1 wins
Game Over
Your balance is now 989.9949
ERROR: 1

As you can see Player1 won the game and the balance increased, whereas Player2 lost and the balance decreased by the wager amount.

Congrats on completing a functional Tic-Tac-toe contract, hope to see you build next!