zkapp-cli
Version:
CLI to create zkApps (zero-knowledge apps) for Mina Protocol
234 lines (199 loc) • 6.68 kB
text/typescript
/**
* This file defines the `TicTacToe` smart contract and the helpers it needs.
*/
import {
Field,
State,
PublicKey,
SmartContract,
state,
method,
Bool,
Provable,
Signature,
Struct,
} from 'o1js';
export { Board, TicTacToe };
function Optional<T>(type: Provable<T>) {
return class Optional_ extends Struct({ isSome: Bool, value: type }) {
constructor(isSome: boolean | Bool, value: T) {
super({ isSome: Bool(isSome), value });
}
toFields() {
return Optional_.toFields(this);
}
};
}
class OptionalBool extends Optional(Bool) {}
class Board {
board: OptionalBool[][];
constructor(serializedBoard: Field) {
const bits = serializedBoard.toBits(18);
const board = [];
for (let i = 0; i < 3; i++) {
const row = [];
for (let j = 0; j < 3; j++) {
const isPlayed = bits[i * 3 + j];
const player = bits[i * 3 + j + 9];
row.push(new OptionalBool(isPlayed, player));
}
board.push(row);
}
this.board = board;
}
serialize(): Field {
const isPlayed = [];
const player = [];
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
isPlayed.push(this.board[i][j].isSome);
player.push(this.board[i][j].value);
}
}
return Field.fromBits(isPlayed.concat(player));
}
update(x: Field, y: Field, playerToken: Bool) {
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
// is this the cell the player wants to play?
const toUpdate = x.equals(new Field(i)).and(y.equals(new Field(j)));
// make sure we can play there
toUpdate.and(this.board[i][j].isSome).assertEquals(false);
// copy the board (or update if this is the cell the player wants to play)
this.board[i][j] = Provable.if(
toUpdate,
new OptionalBool(true, playerToken),
this.board[i][j]
);
}
}
}
printState() {
for (let i = 0; i < 3; i++) {
let row = '| ';
for (let j = 0; j < 3; j++) {
let token = '_';
if (this.board[i][j].isSome.toBoolean()) {
token = this.board[i][j].value.toBoolean() ? 'X' : 'O';
}
row += token + ' | ';
}
console.log(row);
}
console.log('---\n');
}
checkWinner(): Bool {
let won = new Bool(false);
// check rows
for (let i = 0; i < 3; i++) {
let row = this.board[i][0].isSome;
row = row.and(this.board[i][1].isSome);
row = row.and(this.board[i][2].isSome);
row = row.and(this.board[i][0].value.equals(this.board[i][1].value));
row = row.and(this.board[i][1].value.equals(this.board[i][2].value));
won = won.or(row);
}
// check cols
for (let i = 0; i < 3; i++) {
let col = this.board[0][i].isSome;
col = col.and(this.board[1][i].isSome);
col = col.and(this.board[2][i].isSome);
col = col.and(this.board[0][i].value.equals(this.board[1][i].value));
col = col.and(this.board[1][i].value.equals(this.board[2][i].value));
won = won.or(col);
}
// check diagonals
let diag1 = this.board[0][0].isSome;
diag1 = diag1.and(this.board[1][1].isSome);
diag1 = diag1.and(this.board[2][2].isSome);
diag1 = diag1.and(this.board[0][0].value.equals(this.board[1][1].value));
diag1 = diag1.and(this.board[1][1].value.equals(this.board[2][2].value));
won = won.or(diag1);
let diag2 = this.board[0][2].isSome;
diag2 = diag2.and(this.board[1][1].isSome);
diag2 = diag2.and(this.board[0][2].isSome);
diag2 = diag2.and(this.board[0][2].value.equals(this.board[1][1].value));
diag2 = diag2.and(this.board[1][1].value.equals(this.board[2][0].value));
won = won.or(diag2);
//
return won;
}
}
class TicTacToe extends SmartContract {
// The board is serialized as a single field element
board = State<Field>();
// false -> player 1 | true -> player 2
nextIsPlayer2 = State<Bool>();
// defaults to false, set to true when a player wins
gameDone = State<Bool>();
// the two players who are allowed to play
player1 = State<PublicKey>();
player2 = State<PublicKey>();
init() {
super.init();
this.gameDone.set(Bool(true));
this.player1.set(PublicKey.empty());
this.player2.set(PublicKey.empty());
}
async startGame(player1: PublicKey, player2: PublicKey) {
// you can only start a new game if the current game is done
this.gameDone.requireEquals(Bool(true));
this.gameDone.set(Bool(false));
// set players
this.player1.set(player1);
this.player2.set(player2);
// reset board
this.board.set(Field(0));
// player 1 starts
this.nextIsPlayer2.set(Bool(false));
}
// board:
// x 0 1 2
// y +----------
// 0 | x x x
// 1 | x x x
// 2 | x x x
async play(
pubkey: PublicKey,
signature: Signature,
x: Field,
y: Field
) {
// 1. if the game is already finished, abort.
this.gameDone.requireEquals(Bool(false)); // precondition on this.gameDone
// 2. ensure that we know the private key associated to the public key
// and that our public key is known to the zkApp
// ensure player owns the associated private key
signature.verify(pubkey, [x, y]).assertTrue();
// ensure player is valid
const player1 = this.player1.getAndRequireEquals();
const player2 = this.player2.getAndRequireEquals();
Bool.or(pubkey.equals(player1), pubkey.equals(player2)).assertTrue();
// 3. Make sure that its our turn,
// and set the state for the next player
// get player token
const player = pubkey.equals(player2); // player 1 is false, player 2 is true
// ensure its their turn
const nextPlayer = this.nextIsPlayer2.getAndRequireEquals();
nextPlayer.assertEquals(player);
// set the next player
this.nextIsPlayer2.set(player.not());
// 4. get and deserialize the board
this.board.requireEquals(this.board.get()); // precondition that links this.board.get() to the actual on-chain state
const board = new Board(this.board.get());
// 5. update the board (and the state) with our move
x.equals(Field(0))
.or(x.equals(Field(1)))
.or(x.equals(Field(2)))
.assertTrue();
y.equals(Field(0))
.or(y.equals(Field(1)))
.or(y.equals(Field(2)))
.assertTrue();
board.update(x, y, player);
this.board.set(board.serialize());
// 6. did I just win? If so, update the state as well
const won = board.checkWinner();
this.gameDone.set(won);
}
}