Skip to content

Commit

Permalink
feat: Example card game (#2135)
Browse files Browse the repository at this point in the history
Resolves #1463
sirasistant authored Sep 8, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ce90b1f commit 9084b89
Showing 7 changed files with 988 additions and 1 deletion.
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -947,6 +947,17 @@ jobs:
command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_aztec_js_browser.test.ts ./scripts/docker-compose-e2e-sandbox.yml
working_directory: yarn-project/end-to-end

e2e-card-game:
machine:
image: ubuntu-2004:202010-01
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local e2e_card_game.test.ts
working_directory: yarn-project/end-to-end

aztec-rpc-sandbox:
machine:
image: ubuntu-2004:202010-01
@@ -1416,6 +1427,7 @@ workflows:
- e2e-p2p: *e2e_test
- e2e-canary-test: *e2e_test
- e2e-browser-sandbox: *e2e_test
- e2e-card-game: *e2e_test
- aztec-rpc-sandbox: *e2e_test
- guides-writing-an-account-contract: *e2e_test
- guides-dapp-testing: *e2e_test
@@ -1447,6 +1459,7 @@ workflows:
- e2e-p2p
- e2e-browser-sandbox
- e2e-canary-test
- e2e-card-game
- aztec-rpc-sandbox
- guides-writing-an-account-contract
- guides-dapp-testing
2 changes: 1 addition & 1 deletion yarn-project/acir-simulator/src/acvm/acvm.ts
Original file line number Diff line number Diff line change
@@ -91,7 +91,7 @@ function getSourceCodeLocationsFromOpcodeLocation(

const { path, source } = files[fileId];

const locationText = source.substring(span.start, span.end + 1);
const locationText = source.substring(span.start, span.end);
const precedingText = source.substring(0, span.start);
const previousLines = precedingText.split('\n');
// Lines and columns in stacks are one indexed.
288 changes: 288 additions & 0 deletions yarn-project/end-to-end/src/e2e_card_game.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecRPCServer } from '@aztec/aztec-rpc';
import { AztecAddress, Wallet } from '@aztec/aztec.js';
import { DebugLogger } from '@aztec/foundation/log';
import { CardGameContract } from '@aztec/noir-contracts/types';
import { AztecRPC, CompleteAddress } from '@aztec/types';

import { setup } from './fixtures/utils.js';

/* eslint-disable camelcase */

interface Card {
points: bigint;
strength: bigint;
}

const cardToField = (card: Card): bigint => {
return card.strength + card.points * 65536n;
};

interface PlayerGameEntry {
address: bigint;
deck_strength: bigint;
points: bigint;
}

interface Game {
players: PlayerGameEntry[];
rounds_cards: Card[];
started: boolean;
finished: boolean;
claimed: boolean;
current_player: bigint;
current_round: bigint;
}

interface NoirOption<T> {
_is_some: boolean;
_value: T;
}

function unwrapOptions<T>(options: NoirOption<T>[]): T[] {
return options.filter((option: any) => option._is_some).map((option: any) => option._value);
}

const GAME_ID = 42;

describe('e2e_card_game', () => {
let aztecNode: AztecNodeService | undefined;
let aztecRpcServer: AztecRPC;
let wallet: Wallet;
let logger: DebugLogger;
let firstPlayer: AztecAddress;
let secondPlayer: AztecAddress;
let thirdPlayer: AztecAddress;

let contract: CardGameContract;

beforeEach(async () => {
let accounts: CompleteAddress[];
({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup(3));
firstPlayer = accounts[0].address;
secondPlayer = accounts[1].address;
thirdPlayer = accounts[2].address;
await deployContract();
}, 100_000);

afterEach(async () => {
await aztecNode?.stop();
if (aztecRpcServer instanceof AztecRPCServer) {
await aztecRpcServer?.stop();
}
});

const deployContract = async () => {
logger(`Deploying L2 contract...`);
contract = await CardGameContract.deploy(wallet).send().deployed();
logger(`L2 contract deployed at ${contract.address}`);
};

const firstPlayerCollection: Card[] = [
{
points: 45778n,
strength: 7074n,
},
{
points: 60338n,
strength: 53787n,
},
{
points: 13035n,
strength: 45778n,
},
];

it('should be able to buy packs', async () => {
await contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait();
const collection = await contract.methods.view_collection_cards(firstPlayer, 0).view({ from: firstPlayer });
expect(unwrapOptions(collection)).toEqual(firstPlayerCollection);
}, 30_000);

describe('game join', () => {
beforeEach(async () => {
await Promise.all([
contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(),
contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(),
]);
}, 30_000);

it('should be able to join games', async () => {
await contract.methods
.join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])])
.send({ origin: firstPlayer })
.wait();

await expect(
contract.methods
.join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[1])])
.send({ origin: secondPlayer })
.wait(),
).rejects.toThrow(/Card not found/);

const collection = await contract.methods.view_collection_cards(firstPlayer, 0).view({ from: firstPlayer });
expect(unwrapOptions(collection)).toEqual([
{
points: 60338n,
strength: 53787n,
},
]);

expect((await contract.methods.view_game(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({
players: [
{
address: firstPlayer.toBigInt(),
deck_strength: 52852n,
points: 0n,
},
{
address: 0n,
deck_strength: 0n,
points: 0n,
},
],
started: false,
finished: false,
claimed: false,
current_player: 0n,
});
}, 30_000);

it('should start games', async () => {
const secondPlayerCollection = unwrapOptions(
(await contract.methods
.view_collection_cards(secondPlayer, 0)
.view({ from: secondPlayer })) as NoirOption<Card>[],
);

await Promise.all([
contract.methods
.join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])])
.send({ origin: firstPlayer })
.wait(),
contract.methods
.join_game(GAME_ID, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])])
.send({ origin: secondPlayer })
.wait(),
]);

await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait();

expect((await contract.methods.view_game(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({
players: expect.arrayContaining([
{
address: firstPlayer.toBigInt(),
deck_strength: 52852n,
points: 0n,
},
{
address: secondPlayer.toBigInt(),
deck_strength: expect.anything(),
points: 0n,
},
]),
started: true,
finished: false,
claimed: false,
current_player: 0n,
});
}, 30_000);
});

describe('game play', () => {
let secondPlayerCollection: Card[];
let thirdPlayerCOllection: Card[];

beforeEach(async () => {
await Promise.all([
contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(),
contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(),
contract.methods.buy_pack(27n).send({ origin: thirdPlayer }).wait(),
]);

secondPlayerCollection = unwrapOptions(
await contract.methods.view_collection_cards(secondPlayer, 0).view({ from: secondPlayer }),
);

thirdPlayerCOllection = unwrapOptions(
await contract.methods.view_collection_cards(thirdPlayer, 0).view({ from: thirdPlayer }),
);
}, 60_000);

async function joinGame(playerAddress: AztecAddress, cards: Card[], id = GAME_ID) {
await contract.methods.join_game(id, cards.map(cardToField)).send({ origin: playerAddress }).wait();
}

async function playGame(playerDecks: { address: AztecAddress; deck: Card[] }[], id = GAME_ID) {
const initialGameState = (await contract.methods.view_game(id).view({ from: firstPlayer })) as Game;
const players = initialGameState.players.map(player => player.address);
const cards = players.map(
player => playerDecks.find(playerDeckEntry => playerDeckEntry.address.toBigInt() === player)!.deck,
);

for (let roundIndex = 0; roundIndex < cards.length; roundIndex++) {
for (let playerIndex = 0; playerIndex < players.length; playerIndex++) {
const player = players[playerIndex];
const card = cards[playerIndex][roundIndex];
await contract.methods
.play_card(id, card)
.send({ origin: AztecAddress.fromBigInt(player) })
.wait();
}
}

const finalGameState = (await contract.methods.view_game(id).view({ from: firstPlayer })) as Game;

expect(finalGameState.finished).toBe(true);
return finalGameState;
}

it('should play a game, claim the winned cards and play another match with winned cards', async () => {
const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]];
const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]];
await Promise.all([joinGame(firstPlayer, firstPlayerGameDeck), joinGame(secondPlayer, secondPlayerGameDeck)]);
await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait();

let game = await playGame([
{ address: firstPlayer, deck: firstPlayerGameDeck },
{ address: secondPlayer, deck: secondPlayerGameDeck },
]);

const sotedByPoints = game.players.sort((a, b) => Number(b.points - a.points));
const winner = AztecAddress.fromBigInt(sotedByPoints[0].address);
const loser = AztecAddress.fromBigInt(sotedByPoints[1].address);

await expect(
contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(),
).rejects.toThrow(/Not the winner/);

await contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait();

const winnerCollection = unwrapOptions(
(await contract.methods.view_collection_cards(winner, 0).view({ from: winner })) as NoirOption<Card>[],
);

const winnerGameDeck = [winnerCollection[0], winnerCollection[3]];
const thirdPlayerGameDeck = [thirdPlayerCOllection[0], thirdPlayerCOllection[2]];

await Promise.all([
joinGame(winner, winnerGameDeck, GAME_ID + 1),
joinGame(thirdPlayer, thirdPlayerGameDeck, GAME_ID + 1),
]);

await contract.methods
.start_game(GAME_ID + 1)
.send({ origin: winner })
.wait();
game = await playGame(
[
{ address: winner, deck: winnerGameDeck },
{ address: thirdPlayer, deck: thirdPlayerGameDeck },
],
GAME_ID + 1,
);

expect(game.finished).toBe(true);
}, 180_000);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "card_game_contract"
authors = [""]
compiler_version = "0.1"
type = "contract"

[dependencies]
aztec = { path = "../../../../noir-libs/noir-aztec" }
value_note = { path = "../../../../noir-libs/value-note"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use dep::aztec::{
context::{PrivateContext, PublicContext},
constants_gen::{MAX_NOTES_PER_PAGE, MAX_READ_REQUESTS_PER_CALL},
log::emit_encrypted_log,
note::{
note_getter_options::NoteGetterOptions,
note_viewer_options::NoteViewerOptions,
note_getter::view_notes,
},
oracle::{
get_public_key::get_public_key,
get_secret_key::get_secret_key,
},
state_vars::set::Set,
types::point::Point,
};
use dep::std;
use dep::std::{
option::Option,
};
use dep::value_note::{
value_note::{ValueNote, ValueNoteMethods, VALUE_NOTE_LEN},
};

struct Card {
strength: u16,
points: u16,
}

impl Card {
fn from_field(field: Field) -> Card {
let value_bytes = field.to_le_bytes(32);
let strength = (value_bytes[0] as u16) + (value_bytes[1] as u16) * 256;
let points = (value_bytes[2] as u16) + (value_bytes[3] as u16) * 256;
Card {
strength,
points,
}
}

fn to_field(self) -> Field {
self.strength as Field + (self.points as Field)*65536
}

fn serialize(self) -> [Field; 2] {
[self.strength as Field, self.points as Field]
}
}

#[test]
fn test_to_from_field() {
let field = 1234567890;
let card = Card::from_field(field);
assert(card.to_field() == field);
}


struct CardNote {
card: Card,
note: ValueNote,
}

impl CardNote {
fn new(
strength: u16,
points: u16,
owner: Field,
) -> Self {
let card = Card {
strength,
points,
};
CardNote::from_card(card, owner)
}

fn from_card(card: Card, owner: Field) -> CardNote {
CardNote {
card,
note: ValueNote::new(card.to_field(), owner),
}
}

fn from_note(note: ValueNote) -> CardNote {
CardNote {
card: Card::from_field(note.value),
note,
}
}
}

struct Deck {
set: Set<ValueNote, VALUE_NOTE_LEN>,
}

fn filter_cards<N>(notes: [Option<ValueNote>; MAX_READ_REQUESTS_PER_CALL], desired_cards: [Card; N]) -> [Option<ValueNote>; MAX_READ_REQUESTS_PER_CALL] {
let mut selected = [Option::none(); MAX_READ_REQUESTS_PER_CALL];

let mut found = [false; N];

for i in 0..notes.len() {
let note = notes[i];
if note.is_some() {
let card_note = CardNote::from_note(
note.unwrap_unchecked()
);
for j in 0..N {
if !found[j] & (card_note.card.strength == desired_cards[j].strength) & (card_note.card.points == desired_cards[j].points) {
selected[i] = note;
found[j] = true;
}
}
}

}

selected
}


impl Deck {
fn new(
private_context: Option<&mut PrivateContext>,
public_context: Option<&mut PublicContext>,
storage_slot: Field,
) -> Self {
let set = Set {
private_context,
public_context,
storage_slot,
note_interface: ValueNoteMethods,
};
Deck {
set
}
}

fn add_cards<N>(&mut self, cards: [Card; N], owner: Field) -> [CardNote]{
let owner_key = get_public_key(owner);
let context = self.set.private_context.unwrap();

let mut inserted_cards = [];
for card in cards {
let mut card_note = CardNote::from_card(card, owner);
self.set.insert(&mut card_note.note);
emit_encrypted_log(
context,
(*context).this_address(),
self.set.storage_slot,
owner_key,
card_note.note.serialise(),
);
inserted_cards = inserted_cards.push_back(card_note);
}

inserted_cards
}

fn get_cards<N>(&mut self, cards: [Card; N], owner: Field) -> [CardNote; N] {
let options = NoteGetterOptions::with_filter(filter_cards, cards);
let maybe_notes = self.set.get_notes(options);
let mut found_cards = [Option::none(); N];
for i in 0..maybe_notes.len() {
if maybe_notes[i].is_some() {
let card_note = CardNote::from_note(
maybe_notes[i].unwrap_unchecked()
);
// Ensure the notes are actually owned by the owner (to prevent user from generating a valid proof while
// spending someone else's notes).
assert(card_note.note.owner == owner);

for j in 0..cards.len() {
if found_cards[j].is_none() & (cards[j].strength == card_note.card.strength) & (cards[j].points == card_note.card.points) {
found_cards[j] = Option::some(card_note);
}
}
}
}

found_cards.map(|card_note: Option<CardNote>| {
assert(card_note.is_some(), "Card not found");
card_note.unwrap_unchecked()
})
}

fn remove_cards<N>(&mut self, cards: [Card; N], owner: Field) {
let card_notes = self.get_cards(cards, owner);
for card_note in card_notes {
self.set.remove(card_note.note);
}
}

unconstrained fn view_cards(self, offset: u32) -> [Option<Card>; MAX_NOTES_PER_PAGE] {
let options = NoteViewerOptions::new().set_offset(offset);
let opt_notes = self.set.view_notes(options);
let mut opt_cards = [Option::none(); MAX_NOTES_PER_PAGE];

for i in 0..opt_notes.len() {
opt_cards[i] = opt_notes[i].map(|note: ValueNote| Card::from_field(note.value));
}

opt_cards
}

}

global PACK_CARDS = 3; // Limited by number of write requests (max 4)

fn get_pack_cards(
seed: Field,
owner_address: Field
) -> [Card; PACK_CARDS] {
// generate pseudo randomness deterministically from 'seed' and user secret
let secret = get_secret_key(owner_address);
let mix = secret.high + secret.low + seed;
let random_bytes = std::hash::sha256(mix.to_le_bytes(32));

let mut cards = [Card::from_field(0); PACK_CARDS];
// we generate PACK_CARDS cards
assert((PACK_CARDS as u64) < 8, "Cannot generate more than 8 cards");
for i in 0..PACK_CARDS {
let strength = (random_bytes[i] as u16) + (random_bytes[i + 1] as u16) * 256;
let points = (random_bytes[i + 2] as u16) + (random_bytes[i + 3] as u16) * 256;
cards[i] = Card {
strength, points
};
}

cards
}

fn compute_deck_strength<N>(cards: [Card; N]) -> Field {
cards.fold(0, |acc, card: Card| {
acc + card.strength as Field
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use dep::aztec::types::type_serialisation::TypeSerialisationInterface;
use crate::cards::Card;

global NUMBER_OF_PLAYERS = 2;
global NUMBER_OF_CARDS_DECK = 2;

struct PlayerEntry {
address: Field,
deck_strength: u32,
points: u120,
}

impl PlayerEntry {
fn is_initialised(self) -> bool {
self.address != 0
}
}

global PLAYABLE_CARDS = 4;

struct Game {
players: [PlayerEntry; NUMBER_OF_PLAYERS],
rounds_cards: [Card; PLAYABLE_CARDS],
started: bool,
finished: bool,
claimed: bool,
current_player: u32,
current_round: u32,
}

global GAME_SERIALISED_LEN: Field = 15;

fn deserialiseGame(fields: [Field; GAME_SERIALISED_LEN]) -> Game {
let players = [
PlayerEntry {
address: fields[0],
deck_strength: fields[1] as u32,
points: fields[2] as u120,
},
PlayerEntry {
address: fields[3],
deck_strength: fields[4] as u32,
points: fields[5] as u120,
},
];
let rounds_cards = [
Card::from_field(fields[6]), Card::from_field(fields[7]),
Card::from_field(fields[8]), Card::from_field(fields[9]),
];
Game {
players,
rounds_cards,
started: fields[10] as bool,
finished: fields[11] as bool,
claimed: fields[12] as bool,
current_player: fields[13] as u32,
current_round: fields[14] as u32,
}
}

fn serialiseGame(game: Game) -> [Field; GAME_SERIALISED_LEN] {
[
game.players[0].address,
game.players[0].deck_strength as Field,
game.players[0].points as Field,
game.players[1].address,
game.players[1].deck_strength as Field,
game.players[1].points as Field,
game.rounds_cards[0].to_field(),
game.rounds_cards[1].to_field(),
game.rounds_cards[2].to_field(),
game.rounds_cards[3].to_field(),
game.started as Field,
game.finished as Field,
game.claimed as Field,
game.current_player as Field,
game.current_round as Field,
]
}

impl Game {
fn serialize(self: Self) -> [Field; GAME_SERIALISED_LEN] {
serialiseGame(self)
}

fn add_player(&mut self, player_entry: PlayerEntry) -> bool {
let mut added = false;

for i in 0..NUMBER_OF_PLAYERS {
let entry = self.players[i];
if entry.is_initialised() {
assert(entry.address != player_entry.address, "Player already in game");
} else if !added {
self.players[i] = player_entry;
added = true;
}
}

added
}

fn start_game(&mut self) {
assert(!self.started, "Game already started");
for i in 0..NUMBER_OF_PLAYERS {
let entry = self.players[i];
assert(entry.is_initialised(), "Game not full");
}
let sorted_by_deck_strength = self.players.sort_via(|a: PlayerEntry, b: PlayerEntry| a.deck_strength < b.deck_strength);
self.players = sorted_by_deck_strength;
self.started = true;
}

fn current_player(self) -> PlayerEntry {
assert(self.started, "Game not started");
assert(!self.finished, "Game finished");
self.players[self.current_player]
}

fn winner(self) -> PlayerEntry {
assert(self.finished, "Game not finished");
let mut winner = self.players[0];
for i in 1..NUMBER_OF_PLAYERS {
let entry = self.players[i];
if entry.points > winner.points {
winner = entry;
}
}
winner
}

fn play_card(&mut self, card: Card) {
assert(self.started, "Game not started");
assert(!self.finished, "Game finished");

let round_offset = self.current_round * NUMBER_OF_PLAYERS;

self.rounds_cards[round_offset + self.current_player] = card;
self.current_player = (self.current_player + 1) % NUMBER_OF_PLAYERS;

if self.current_player == 0 {
self._finish_round();
}
}

fn _finish_round(&mut self) {
let round_offset = self.current_round * NUMBER_OF_PLAYERS;
self.current_round += 1;

let mut winner_index = 0;
let mut winner_strength = 0;
let mut round_points = 0;

for i in 0..NUMBER_OF_PLAYERS {
let card = self.rounds_cards[round_offset + i];
round_points += (card.points as u120);
if card.strength > winner_strength {
winner_strength = card.strength;
winner_index = i;
}
}

self.players[winner_index].points += round_points;
if self.current_round == NUMBER_OF_CARDS_DECK {
self.finished = true;
}
}
}

global GameSerialisationMethods = TypeSerialisationInterface {
deserialise: deserialiseGame,
serialise: serialiseGame,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
mod cards;
mod game;

use dep::aztec::{
context::{PrivateContext, PublicContext},
state_vars::{
map::Map,
public_state::PublicState,
},
};

use dep::std::option::Option;

use cards::{Deck};
use game::{Game, GameSerialisationMethods, GAME_SERIALISED_LEN};

struct Storage {
collections: Map<Deck>,
game_decks: Map<Map<Deck>>,
games: Map<PublicState<Game, GAME_SERIALISED_LEN>>,
}

impl Storage {
fn init(
private_context: Option<&mut PrivateContext>,
public_context: Option<&mut PublicContext>,
) -> Self {
Storage {
collections: Map::new(
private_context,
public_context,
1,
|private_context, public_context, slot| {
Deck::new(
private_context,
public_context,
slot,
)
},
),
game_decks: Map::new(
private_context,
public_context,
2,
|private_context, public_context, slot| {
Map::new(
private_context,
public_context,
slot,
|private_context, public_context, slot|{
Deck::new(
private_context,
public_context,
slot,
)
}
)
},
),
games: Map::new(
private_context,
public_context,
3,
|private_context, public_context, slot| {
PublicState::new(
private_context,
public_context,
slot,
GameSerialisationMethods,
)
},
)
}
}
}

contract CardGame {
use dep::std::option::Option;
use dep::value_note::{
balance_utils,
value_note::{
ValueNoteMethods,
VALUE_NOTE_LEN,
},
};

use dep::aztec::{
abi,
constants_gen::{MAX_NOTES_PER_PAGE},
abi::{
Hasher, PrivateContextInputs,
},
context::PrivateContext,
note::{
note_header::NoteHeader,
utils as note_utils,
},
oracle::compute_selector::compute_selector
};

use crate::Storage;
use crate::cards::{
PACK_CARDS,
Deck,
Card,
get_pack_cards,
compute_deck_strength,
};
use crate::game::{
NUMBER_OF_PLAYERS,
NUMBER_OF_CARDS_DECK,
PLAYABLE_CARDS,
PlayerEntry,
Game
};

#[aztec(private)]
fn constructor() {}

#[aztec(private)]
fn buy_pack(
seed: Field, // The randomness used to generate the cards. Passed in for now.
) {
let storage = Storage::init(Option::some(&mut context), Option::none());
let buyer = context.msg_sender();
let mut cards = get_pack_cards(seed, buyer);

let mut collection = storage.collections.at(buyer);
let _inserted_cards = collection.add_cards(cards, buyer);
}

#[aztec(private)]
fn join_game(
game: u32,
cards_fields: [Field; 2],
) {
let cards = cards_fields.map(|card_field| Card::from_field(card_field));
let storage = Storage::init(Option::some(&mut context), Option::none());
let player = context.msg_sender();

let mut collection = storage.collections.at(player);
collection.remove_cards(cards, player);
let mut game_deck = storage.game_decks.at(game as Field).at(player);
let _added_to_game_deck = game_deck.add_cards(cards, player);
let selector = compute_selector("on_game_joined(u32,Field,u32)");
let strength = compute_deck_strength(cards);
context.call_public_function(context.this_address(), selector, [game as Field, player, strength]);
}

#[aztec(public)]
internal fn on_game_joined(
game: u32,
player: Field,
deck_strength: u32,
) {
let storage = Storage::init(Option::none(), Option::some(&mut context));
let game_storage = storage.games.at(game as Field);

let mut game_data = game_storage.read();
assert(game_data.add_player(PlayerEntry {address: player, deck_strength, points: 0}), "Game full");

game_storage.write(game_data);
}

#[aztec(public)]
fn start_game(game: u32) {
let storage = Storage::init(Option::none(), Option::some(&mut context));
let game_storage = storage.games.at(game as Field);

let mut game_data = game_storage.read();
game_data.start_game();
game_storage.write(game_data);
}

#[aztec(private)]
fn play_card(
game: u32,
card: Card,
) {
let storage = Storage::init(Option::some(&mut context), Option::none());
let player = context.msg_sender();

let mut game_deck = storage.game_decks.at(game as Field).at(player);
game_deck.remove_cards([card], player);

let selector = compute_selector("on_card_played(u32,Field,Field)");
context.call_public_function(context.this_address(), selector, [game as Field, player, card.to_field()]);
}

#[aztec(public)]
internal fn on_card_played(game: u32, player: Field, card_as_field: Field) {
let storage = Storage::init(Option::none(), Option::some(&mut context));
let game_storage = storage.games.at(game as Field);

let mut game_data = game_storage.read();

let card = Card::from_field(card_as_field);
let current_player = game_data.current_player();
assert(current_player.address == player, "Not your turn");
game_data.play_card(card);

game_storage.write(game_data);
}

#[aztec(private)]
fn claim_cards(
game: u32,
cards_fields: [Field; PLAYABLE_CARDS],
) {
let storage = Storage::init(Option::some(&mut context), Option::none());
let player = context.msg_sender();
let cards = cards_fields.map(|card_field| Card::from_field(card_field));

let mut collection = storage.collections.at(player);
let _inserted_cards = collection.add_cards(cards, player);

let selector = compute_selector("on_cards_claimed(u32,Field,Field)");
context.call_public_function(
context.this_address(),
selector,
[game as Field, player, dep::std::hash::pedersen(cards_fields)[0]]
);
}

#[aztec(public)]
internal fn on_cards_claimed(game: u32, player: Field, cards_hash: Field) {
let storage = Storage::init(Option::none(), Option::some(&mut context));
let game_storage = storage.games.at(game as Field);
let mut game_data = game_storage.read();

assert(!game_data.claimed, "Already claimed");
game_data.claimed = true;

assert_eq(
cards_hash,
dep::std::hash::pedersen(game_data.rounds_cards.map(|card: Card| card.to_field()))[0]
);

let winner = game_data.winner();
assert_eq(player, winner.address, "Not the winner");

game_storage.write(game_data);
}

unconstrained fn view_collection_cards(owner: Field, offset: u32) -> [Option<Card>; MAX_NOTES_PER_PAGE] {
let storage = Storage::init(Option::none(), Option::none());
let collection = storage.collections.at(owner);

collection.view_cards(offset)
}

unconstrained fn view_game_cards(game: u32, player: Field, offset: u32) -> [Option<Card>; MAX_NOTES_PER_PAGE] {
let storage = Storage::init(Option::none(), Option::none());
let game_deck = storage.game_decks.at(game as Field).at(player);

game_deck.view_cards(offset)
}

unconstrained fn view_game(game: u32) -> Game {
Storage::init(Option::none(), Option::none()).games.at(game as Field).read()
}

// Computes note hash and nullifier.
// Note 1: Needs to be defined by every contract producing logs.
// Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes.
unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {
let note_header = NoteHeader { contract_address, nonce, storage_slot };
note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, preimage)
}
}

0 comments on commit 9084b89

Please sign in to comment.