Algorand Blockchain Development using Reach Part 2 RPS
Software Development is hard. Blockchain development is even harder. With Reach the last statement is false. In this tutorial, I’m going to show Algorand developers how they can implement a Blockchain game by implemtenting Rock, Paper, Scissors game mechanics.
Requirements
[Reach SDK] Algorand - Ethereum SDK
Background
In creating a game of Rock, Paper, Scissors. We need to represent the hands in the game.
A simple approach would be to represent these as numbers. 0 == Rock
1 == Paper
and 2 == Scissors
. Reach does not support unsigned integers of exactly two bits, so it is better to represent them as the equivalence class of integers modulo three, so we won’t distinguish between 0 and 3 as Rock
We can use an approach for representing the three outcomes of the game: B wins, Draw, and A wins
.
Steps
1. Rock, Paper, Scissors - Creating our Reach dApp
We can use an approach for representing the three outcomes of the game: B wins, Draw, and A wins
.
Let’s add to the example from the last video. If you are starting the from scratch. In working working folder use the following command.
./reach init
this will give you a starter project.
'reach 0.1';
The above line imports Reach
const Player = {
getHand:Fun([],UInt),
seeOutcome:Fun([UInt],Null)
};
The above code defines a participant interact interface that will be shared between the two players. In this case, it provides two methods: getHand, which returns a number; and seeOutcome, which receives a number.
A.only(() => {
The above states that this block of code is something that only A (i.e., Alice) performs. That means that the variable, handA, bound on line 13 is known only to Alice
const handA = declassify(interact.getHand());});
The above code binds that value to the result of interacting with Alice through the getHand method, which we wrote in JavaScript. It also declassifies the value, because in Reach, all information from frontends is secret until it is explicitly made public.
A.publish(handA);
The above code has Alice join the application by publishing the value to the consensus network, so it can be used to evaluate the outcome of the game. Once this happens, the code is in a “consensus step” where all participants act together.
commit();
The above code commits the state of the consensus network and returns to “local step” where individual participants can act alone. Now, that we have implmented this. Let us do the same for Bob.
B.only(() => {
const handB = declassify(interact.getHand()); });
B.publish(handB);
The above code is the same as Alice’s code. We match Alice’s similar local step and joining of the application through a consensus transfer publication.
const outcome = (handA + (4 - handB)) % 3;
The above code computes the outcome of the game before committing. ((handA + (4 - handB)) % 3 is a clever equation to compute the winner of a game of Rock, Paper, Scissors! using modular arithmetic. Consider when handA is 0 (i.e., Rock) and handB is 2 (i.e., Scissors), then this equation is ((handA + (4 - handB)) % 3) = ((0 + (4 - 2)) % 3) = ((0 + 2) % 3) = (2 % 3) = 2, which is the last outcome, that is A wins, as we expect it to be.)
each([A, B], () => {
interact.seeOutcome(outcome); });
The above code states that this is a local step that each of the participants performs.
2. Rock, Paper, Scissors - Creating our Reach Test Runner
const HAND = ['Rock', 'Paper', 'Scissors'];
const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
The above define arrays to hold the meaning of the hands and outcomes
const Player = (Who) => ({
getHand: () => {
const hand = Math.floor(Math.random() * 3);
console.log(`${Who} played ${HAND[hand]}`);
return hand;
},
seeOutcome: (outcome) => {
console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
},
});
The above code defines a constructor for the Player implementation and defines the getHand
and seeOutcome
methods.
await Promise.all([
backend.Alice(
stdlib, ctcAlice,
Player('Alice'),
),
backend.Bob(
stdlib, ctcBob,
Player('Bob'),
),
]);
The above code instantiates the implementation once for Alice and once for Bob. These are the actual objects that will be bound to interact in the Reach program.
There is nothing mind-blowing here and that is the beauty of Reach. We don’t think about the chain. We really think about the business logic and what we should not do.
At this point, we can run the program and see its output by running
./reach run
If everything is successful you should see the following in your console output.
./reach run
Alice played Scissors
Bob played Paper
Alice saw outcome Alice wins
Bob saw outcome Alice wins
Completed Source File
index.rsh
'reach 0.1';
const Player = {
getHand:Fun([],UInt),
seeOutcome:Fun([UInt],Null)
};
export const main = Reach.App(
{}, [['Alice', Player], ['Bob', Player]], (A, B) => {
A.only(() => {
const handA = declassify(interact.getHand());
});
A.publish(handA);
commit();
B.only(() => {
const handB = declassify(interact.getHand());
});
B.publish(handB);
const outcome = (handA + (4 - handB)) % 3;
commit();
each([A, B], () => {
interact.seeOutcome(outcome);
});
});
index.mjs
import {loadStdlib} from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
(async () => {
const stdlib = await loadStdlib();
const startingBalance = stdlib.parseCurrency(100);
const alice = await stdlib.newTestAccount(startingBalance);
const bob = await stdlib.newTestAccount(startingBalance);
const ctcAlice = alice.deploy(backend);
const ctcBob = bob.attach(backend, ctcAlice.getInfo());
const HAND = ['Rock', 'Paper', 'Scissors'];
const OUTCOME = ['Bob wins', 'Draw', 'Alice wins'];
const Player = (Who) => ({
getHand: () => {
const hand = Math.floor(Math.random() * 3);
console.log(`${Who} played ${HAND[hand]}`);
return hand;
},
seeOutcome: (outcome) => {
console.log(`${Who} saw outcome ${OUTCOME[outcome]}`);
},
});
await Promise.all([
backend.Alice(
stdlib, ctcAlice,
Player('Alice'),
),
backend.Bob(
stdlib, ctcBob,
Player('Bob'),
),
]);
})();
Here is a video showing the above.