From 2336fb1a6b5c699bdcefaad55dca8c5a14adf72c Mon Sep 17 00:00:00 2001 From: sirasistant Date: Thu, 7 Sep 2023 15:28:19 +0000 Subject: [PATCH 01/14] feat: buy packs --- .../end-to-end/src/e2e_card_game.test.ts | 68 +++++++++ .../contracts/card_game_contract/Nargo.toml | 10 ++ .../contracts/card_game_contract/src/cards.nr | 143 ++++++++++++++++++ .../contracts/card_game_contract/src/main.nr | 106 +++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 yarn-project/end-to-end/src/e2e_card_game.test.ts create mode 100644 yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml create mode 100644 yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr create mode 100644 yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts new file mode 100644 index 000000000000..9aae82eb0987 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -0,0 +1,68 @@ +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'; + +interface Card { + points: bigint; + strength: bigint; +} + +describe('e2e_card_game', () => { + let aztecNode: AztecNodeService | undefined; + let aztecRpcServer: AztecRPC; + let wallet: Wallet; + let logger: DebugLogger; + let firstPlayer: AztecAddress; + // let secondPlayer: AztecAddress; + + let contract: CardGameContract; + + beforeEach(async () => { + let accounts: CompleteAddress[]; + ({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup(2)); + firstPlayer = accounts[0].address; + // secondPlayer = accounts[1].address; + }, 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: 7074n, + strength: 45778n, + }, + { + points: 53787n, + strength: 60338n, + }, + { + points: 45778n, + strength: 13035n, + }, + ]; + + it('should be able to buy packs', async () => { + await deployContract(); + await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); + const collection = await contract.methods.getCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); + expect(collection.filter((option: any) => option._is_some).map((option: any) => option._value)).toEqual( + firstPlayerCollection, + ); + }, 30_000); +}); diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml b/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml new file mode 100644 index 000000000000..e2491ce5d1fa --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml @@ -0,0 +1,10 @@ +[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"} +easy_private_state = { path = "../../../../noir-libs/easy-private-state"} \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr new file mode 100644 index 000000000000..b53cbe6f6833 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -0,0 +1,143 @@ +use dep::aztec::{ + context::{PrivateContext, PublicContext}, + constants_gen::{MAX_NOTES_PER_PAGE}, + 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::{ + unsafe::zeroed, + option::Option, +}; +use dep::value_note::{ + value_note::{ValueNote, ValueNoteMethods, VALUE_NOTE_LEN}, +}; + +struct Card { + strength: u16, + points: u16, +} + +struct CardNote { + card: Card, + note: ValueNote, +} + +impl CardNote { + fn new( + strength: u16, + points: u16, + owner: Field, + ) -> Self { + CardNote { + card: Card { + strength, + points, + }, + note: ValueNote::new(strength as Field + (points as Field)*65536, owner), + } + } + + fn from_note(note: ValueNote) -> CardNote { + let value_bytes = note.value.to_le_bytes(32); + let points = (value_bytes[0] as u16) + (value_bytes[1] as u16) * 256; + let strength = (value_bytes[2] as u16) + (value_bytes[3] as u16) * 256; + + CardNote { + card: Card { + strength, + points, + }, + note, + } + } +} + +struct Deck { + set: Set, +} + +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(&mut self, cards: [CardNote; 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 mutable_card = card; + self.set.insert(&mut mutable_card.note); + emit_encrypted_log( + context, + (*context).this_address(), + self.set.storage_slot, + owner_key, + mutable_card.note.serialise(), + ); + inserted_cards = inserted_cards.push_back(mutable_card); + } + + inserted_cards + } + + unconstrained fn view_cards(self, offset: u32) -> [Option; 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(CardNote::from_note); + } + + opt_cards + } + +} + +global PACK_CARDS = 3; // Limited by number of write requests (max 4) + +fn get_pack_cards( + seed: Field, + owner_address: Field +) -> [CardNote; 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 = [zeroed(); 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] = CardNote::new(strength, points, owner_address); + } + + cards +} \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr new file mode 100644 index 000000000000..3ec618e338d0 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -0,0 +1,106 @@ +mod cards; + +use dep::aztec::{ + context::{PrivateContext, PublicContext}, + state_vars::{ + map::Map, + }, +}; + +use dep::std::option::Option; + +use cards::{Deck}; + +struct Storage { + collections: Map, +} + +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, + ) + }, + ) + } + } +} + +contract CardGame { + use dep::std::option::Option; + use dep::aztec::abi::Hasher; + use dep::value_note::{ + balance_utils, + value_note::{ + ValueNoteMethods, + VALUE_NOTE_LEN, + }, + }; + + use dep::aztec::{ + abi, + constants_gen::{MAX_NOTES_PER_PAGE}, + abi::PrivateContextInputs, + context::PrivateContext, + note::{ + note_header::NoteHeader, + utils as note_utils, + }, + }; + + use crate::Storage; + use crate::cards::{ + PACK_CARDS, + Deck, + Card, + CardNote, + get_pack_cards + }; + + + #[aztec(private)] + fn constructor() {} + + #[aztec(private)] + fn buyPack( + seed: Field, // The randomness used to generate the cards. Passed in for now. + ) { + let storage = Storage::init(Option::some(&mut context), Option::none()); + let mut cards = get_pack_cards(seed, context.msg_sender()); + + let mut collection = storage.collections.at(context.msg_sender()); + let _inserted_cards = collection.add_cards(cards, context.msg_sender()); + } + + unconstrained fn getCollectionCards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + let storage = Storage::init(Option::none(), Option::none()); + let collection = storage.collections.at(owner); + let mut cards = [Option::none(); MAX_NOTES_PER_PAGE]; + let card_notes = collection.view_cards(offset); + + for i in 0..MAX_NOTES_PER_PAGE { + cards[i] = card_notes[i].map(|card_note: CardNote| card_note.card); + } + + cards + } + + // 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) + } +} From 2995f9ecc2458c3c4b8618329cad4a648f11d735 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 11:31:28 +0000 Subject: [PATCH 02/14] feat: first e2e version of card game --- yarn-project/acir-simulator/src/acvm/acvm.ts | 2 +- .../end-to-end/src/e2e_card_game.test.ts | 176 +++++++++++++++- .../contracts/card_game_contract/src/cards.nr | 114 +++++++++-- .../contracts/card_game_contract/src/game.nr | 172 ++++++++++++++++ .../contracts/card_game_contract/src/main.nr | 188 +++++++++++++++++- 5 files changed, 622 insertions(+), 30 deletions(-) create mode 100644 yarn-project/noir-contracts/src/contracts/card_game_contract/src/game.nr diff --git a/yarn-project/acir-simulator/src/acvm/acvm.ts b/yarn-project/acir-simulator/src/acvm/acvm.ts index 505d0e039d42..82c0e53e22b7 100644 --- a/yarn-project/acir-simulator/src/acvm/acvm.ts +++ b/yarn-project/acir-simulator/src/acvm/acvm.ts @@ -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. diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 9aae82eb0987..7defb852a20c 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -7,18 +7,40 @@ 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; +} + describe('e2e_card_game', () => { let aztecNode: AztecNodeService | undefined; let aztecRpcServer: AztecRPC; let wallet: Wallet; let logger: DebugLogger; let firstPlayer: AztecAddress; - // let secondPlayer: AztecAddress; + let secondPlayer: AztecAddress; let contract: CardGameContract; @@ -26,7 +48,8 @@ describe('e2e_card_game', () => { let accounts: CompleteAddress[]; ({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup(2)); firstPlayer = accounts[0].address; - // secondPlayer = accounts[1].address; + secondPlayer = accounts[1].address; + await deployContract(); }, 100_000); afterEach(async () => { @@ -44,25 +67,158 @@ describe('e2e_card_game', () => { const firstPlayerCollection: Card[] = [ { - points: 7074n, - strength: 45778n, + points: 45778n, + strength: 7074n, }, { - points: 53787n, - strength: 60338n, + points: 60338n, + strength: 53787n, }, { - points: 45778n, - strength: 13035n, + points: 13035n, + strength: 45778n, }, ]; it('should be able to buy packs', async () => { - await deployContract(); await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); - const collection = await contract.methods.getCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); + const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); expect(collection.filter((option: any) => option._is_some).map((option: any) => option._value)).toEqual( firstPlayerCollection, ); }, 30_000); + + describe('game join', () => { + beforeEach(async () => { + await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); + await contract.methods.buyPack(27n).send({ origin: secondPlayer }).wait(); + }, 30_000); + + it('should be able to join games', async () => { + await contract.methods + .joinGame(42, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .send({ origin: firstPlayer }) + .wait(); + + const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); + expect(collection.filter((option: any) => option._is_some).map((option: any) => option._value)).toEqual([ + { + points: 60338n, + strength: 53787n, + }, + ]); + + expect((await contract.methods.viewGame(42).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 = ( + await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }) + ) + .filter((option: any) => option._is_some) + .map((option: any) => option._value); + + await contract.methods + .joinGame(42, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .send({ origin: firstPlayer }) + .wait(); + + await contract.methods + .joinGame(42, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) + .send({ origin: secondPlayer }) + .wait(); + + await contract.methods.startGame(42).send({ origin: firstPlayer }).wait(); + + expect((await contract.methods.viewGame(42).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.only('game play', () => { + let players: AztecAddress[]; + let cards: Card[][]; + + beforeEach(async () => { + players = [firstPlayer, secondPlayer]; + await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); + await contract.methods.buyPack(27n).send({ origin: secondPlayer }).wait(); + + const secondPlayerCollection = ( + await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }) + ) + .filter((option: any) => option._is_some) + .map((option: any) => option._value); + + cards = [ + [firstPlayerCollection[0], firstPlayerCollection[2]], + [secondPlayerCollection[1], secondPlayerCollection[2]], + ]; + + await contract.methods.joinGame(42, cards[0].map(cardToField)).send({ origin: firstPlayer }).wait(); + + await contract.methods.joinGame(42, cards[1].map(cardToField)).send({ origin: secondPlayer }).wait(); + + await contract.methods.startGame(42).send({ origin: firstPlayer }).wait(); + }, 60_000); + + it('should play a game and claim the winned cards', async () => { + 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.playCard(42, card).send({ origin: player }).wait(); + } + } + + const game = (await contract.methods.viewGame(42).view({ from: firstPlayer })) as Game; + + expect(game.finished).toBe(true); + + const winner = AztecAddress.fromBigInt( + game.players.reduce((currentWinner, player) => (player.points > currentWinner.points ? player : currentWinner)) + .address, + ); + + await contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + const winnerCollection = (await contract.methods.viewCollectionCards(winner, 0).view({ from: winner })) + .filter((option: any) => option._is_some) + .map((option: any) => option._value); + + expect(winnerCollection).toEqual(expect.arrayContaining(cards.flat())); + }, 120_000); + }); }); diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr index b53cbe6f6833..38c87abec086 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -1,6 +1,6 @@ use dep::aztec::{ context::{PrivateContext, PublicContext}, - constants_gen::{MAX_NOTES_PER_PAGE}, + constants_gen::{MAX_NOTES_PER_PAGE, MAX_READ_REQUESTS_PER_CALL}, log::emit_encrypted_log, note::{ note_getter_options::NoteGetterOptions, @@ -28,6 +28,34 @@ struct Card { 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, @@ -39,25 +67,23 @@ impl CardNote { 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: Card { - strength, - points, - }, - note: ValueNote::new(strength as Field + (points as Field)*65536, owner), + card, + note: ValueNote::new(card.to_field(), owner), } } fn from_note(note: ValueNote) -> CardNote { - let value_bytes = note.value.to_le_bytes(32); - let points = (value_bytes[0] as u16) + (value_bytes[1] as u16) * 256; - let strength = (value_bytes[2] as u16) + (value_bytes[3] as u16) * 256; - CardNote { - card: Card { - strength, - points, - }, + card: Card::from_field(note.value), note, } } @@ -67,6 +93,30 @@ struct Deck { set: Set, } +fn filter_cards(notes: [Option; MAX_READ_REQUESTS_PER_CALL], desired_cards: [Card; N]) -> [Option; 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; + } + } + } + + } + + selected +} + + impl Deck { fn new( private_context: Option<&mut PrivateContext>, @@ -105,6 +155,36 @@ impl Deck { inserted_cards } + fn remove_cards(&mut self, cards: [Card; N], owner: Field) { + let options = NoteGetterOptions::with_filter(filter_cards, cards); + let maybe_notes = self.set.get_notes(options); + let mut removed = [false; 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); + + // Removes the note from the owner's set of notes. + self.set.remove(card_note.note); + // set the removed flag to true + for j in 0..cards.len() { + if !removed[j] & (cards[j].strength == card_note.card.strength) & (cards[j].points == card_note.card.points) { + removed[j] = true; + } + } + + } + } + + for i in 0..removed.len() { + assert(removed[i], "Could not find card"); + } + } + unconstrained fn view_cards(self, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let options = NoteViewerOptions::new().set_offset(offset); let opt_notes = self.set.view_notes(options); @@ -140,4 +220,10 @@ fn get_pack_cards( } cards +} + +fn compute_deck_strength(cards: [Card; N]) -> Field { + cards.fold(0, |acc, card: Card| { + acc + card.strength as Field + }) } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/game.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/game.nr new file mode 100644 index 000000000000..e68e8799f6e3 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/game.nr @@ -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, +}; \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr index 3ec618e338d0..fe800795fe47 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -1,18 +1,23 @@ 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, + game_decks: Map>, + games: Map>, } impl Storage { @@ -32,6 +37,38 @@ impl Storage { 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, + ) + }, ) } } @@ -39,7 +76,6 @@ impl Storage { contract CardGame { use dep::std::option::Option; - use dep::aztec::abi::Hasher; use dep::value_note::{ balance_utils, value_note::{ @@ -51,12 +87,15 @@ contract CardGame { use dep::aztec::{ abi, constants_gen::{MAX_NOTES_PER_PAGE}, - abi::PrivateContextInputs, + abi::{ + Hasher, PrivateContextInputs, + }, context::PrivateContext, note::{ note_header::NoteHeader, utils as note_utils, }, + oracle::compute_selector::compute_selector }; use crate::Storage; @@ -65,9 +104,16 @@ contract CardGame { Deck, Card, CardNote, - get_pack_cards + get_pack_cards, + compute_deck_strength, + }; + use crate::game::{ + NUMBER_OF_PLAYERS, + NUMBER_OF_CARDS_DECK, + PLAYABLE_CARDS, + PlayerEntry, + Game }; - #[aztec(private)] fn constructor() {} @@ -83,7 +129,122 @@ contract CardGame { let _inserted_cards = collection.add_cards(cards, context.msg_sender()); } - unconstrained fn getCollectionCards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + #[aztec(private)] + fn joinGame( + 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.map(|card: Card| { + CardNote::new(card.strength, card.points, player) + }), player); + let selector = compute_selector("onGameJoined(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 onGameJoined( + 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 startGame(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 playCard( + 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("onCardPlayed(u32,Field,Field)"); + context.call_public_function(context.this_address(), selector, [game as Field, player, card.to_field()]); + } + + #[aztec(public)] + internal fn onCardPlayed(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 claimCards( + game: u32, + cards_fields: [Field; PLAYABLE_CARDS], + ) { + let storage = Storage::init(Option::some(&mut context), Option::none()); + let player = context.msg_sender(); + let cards_notes = cards_fields.map(|card_field| CardNote::from_card(Card::from_field(card_field), player)); + + let mut collection = storage.collections.at(player); + let _inserted_cards = collection.add_cards(cards_notes, player); + + let selector = compute_selector("onCardsClaimed(u32,Field,[Field;4])"); + context.call_public_function( + context.this_address(), + selector, + [game as Field, player, cards_fields[0], cards_fields[1], cards_fields[2], cards_fields[3]] + ); + } + + #[aztec(public)] + internal fn onCardsClaimed(game: u32, player: Field, cards_fields: [Field; PLAYABLE_CARDS]) { + 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; + + for i in 0..cards_fields.len() { + let game_card_field = game_data.rounds_cards[i].to_field(); + assert_eq(game_card_field, cards_fields[i], "Claim card mismatch"); + } + + let winner = game_data.winner(); + assert_eq(player, winner.address, "Not the winner"); + + game_storage.write(game_data); + } + + unconstrained fn viewCollectionCards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let storage = Storage::init(Option::none(), Option::none()); let collection = storage.collections.at(owner); let mut cards = [Option::none(); MAX_NOTES_PER_PAGE]; @@ -96,6 +257,23 @@ contract CardGame { cards } + unconstrained fn viewGameCards(game: u32, player: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + let storage = Storage::init(Option::none(), Option::none()); + let game_deck = storage.game_decks.at(game as Field).at(player); + let mut cards = [Option::none(); MAX_NOTES_PER_PAGE]; + let card_notes = game_deck.view_cards(offset); + + for i in 0..MAX_NOTES_PER_PAGE { + cards[i] = card_notes[i].map(|card_note: CardNote| card_note.card); + } + + cards + } + + unconstrained fn viewGame(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. From e5daa40f652d43378b21968fa0595f387f609667 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 13:27:14 +0000 Subject: [PATCH 03/14] refactor: pass hash of cards --- .../end-to-end/src/e2e_card_game.test.ts | 139 +++++++++++++----- .../contracts/card_game_contract/src/main.nr | 16 +- 2 files changed, 108 insertions(+), 47 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 7defb852a20c..421c01756ff9 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -34,6 +34,15 @@ interface Game { current_round: bigint; } +interface NoirOption { + _is_some: boolean; + _value: T; +} + +function unwrapOptions(options: NoirOption[]): T[] { + return options.filter((option: any) => option._is_some).map((option: any) => option._value); +} + describe('e2e_card_game', () => { let aztecNode: AztecNodeService | undefined; let aztecRpcServer: AztecRPC; @@ -41,14 +50,16 @@ describe('e2e_card_game', () => { 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(2)); + ({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup(3)); firstPlayer = accounts[0].address; secondPlayer = accounts[1].address; + thirdPlayer = accounts[2].address; await deployContract(); }, 100_000); @@ -83,9 +94,7 @@ describe('e2e_card_game', () => { it('should be able to buy packs', async () => { await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); - expect(collection.filter((option: any) => option._is_some).map((option: any) => option._value)).toEqual( - firstPlayerCollection, - ); + expect(unwrapOptions(collection)).toEqual(firstPlayerCollection); }, 30_000); describe('game join', () => { @@ -101,7 +110,7 @@ describe('e2e_card_game', () => { .wait(); const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); - expect(collection.filter((option: any) => option._is_some).map((option: any) => option._value)).toEqual([ + expect(unwrapOptions(collection)).toEqual([ { points: 60338n, strength: 53787n, @@ -129,11 +138,11 @@ describe('e2e_card_game', () => { }, 30_000); it('should start games', async () => { - const secondPlayerCollection = ( - await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }) - ) - .filter((option: any) => option._is_some) - .map((option: any) => option._value); + const secondPlayerCollection = unwrapOptions( + (await contract.methods + .viewCollectionCards(secondPlayer, 0) + .view({ from: secondPlayer })) as NoirOption[], + ); await contract.methods .joinGame(42, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) @@ -168,57 +177,109 @@ describe('e2e_card_game', () => { }, 30_000); }); - describe.only('game play', () => { - let players: AztecAddress[]; - let cards: Card[][]; + describe('game play', () => { + let playerCards: { address: AztecAddress; cards: Card[] }[]; + + let secondPlayerCollection: Card[]; + let thirdPlayerCOllection: Card[]; beforeEach(async () => { - players = [firstPlayer, secondPlayer]; await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); await contract.methods.buyPack(27n).send({ origin: secondPlayer }).wait(); + await contract.methods.buyPack(27n).send({ origin: thirdPlayer }).wait(); - const secondPlayerCollection = ( - await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }) - ) - .filter((option: any) => option._is_some) - .map((option: any) => option._value); + secondPlayerCollection = unwrapOptions( + await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }), + ); - cards = [ - [firstPlayerCollection[0], firstPlayerCollection[2]], - [secondPlayerCollection[1], secondPlayerCollection[2]], - ]; + thirdPlayerCOllection = unwrapOptions( + await contract.methods.viewCollectionCards(thirdPlayer, 0).view({ from: thirdPlayer }), + ); - await contract.methods.joinGame(42, cards[0].map(cardToField)).send({ origin: firstPlayer }).wait(); + playerCards = [ + { address: firstPlayer, cards: [firstPlayerCollection[0], firstPlayerCollection[2]] }, + { address: secondPlayer, cards: [secondPlayerCollection[0], secondPlayerCollection[2]] }, + { address: thirdPlayer, cards: [thirdPlayerCOllection[0], thirdPlayerCOllection[1]] }, + ]; + }, 60_000); - await contract.methods.joinGame(42, cards[1].map(cardToField)).send({ origin: secondPlayer }).wait(); + async function joinGame(playerAddress: AztecAddress, cards: Card[], id = 42) { + await contract.methods.joinGame(id, cards.map(cardToField)).send({ origin: playerAddress }).wait(); + } - await contract.methods.startGame(42).send({ origin: firstPlayer }).wait(); - }, 60_000); + async function playGame(id = 42) { + const initialGameState = (await contract.methods.viewGame(id).view({ from: firstPlayer })) as Game; + const players = initialGameState.players.map(player => player.address); + const cards = players.map( + player => playerCards.find(playerCardsEntry => playerCardsEntry.address.toBigInt() === player)!.cards, + ); - it('should play a game and claim the winned cards', async () => { 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.playCard(42, card).send({ origin: player }).wait(); + await contract.methods + .playCard(id, card) + .send({ origin: AztecAddress.fromBigInt(player) }) + .wait(); } } - const game = (await contract.methods.viewGame(42).view({ from: firstPlayer })) as Game; + const finalGameState = (await contract.methods.viewGame(id).view({ from: firstPlayer })) as Game; - expect(game.finished).toBe(true); + expect(finalGameState.finished).toBe(true); + + console.log(finalGameState.players); - const winner = AztecAddress.fromBigInt( - game.players.reduce((currentWinner, player) => (player.points > currentWinner.points ? player : currentWinner)) - .address, + return finalGameState; + } + + it('should play a game and claim the winned cards', async () => { + await joinGame(playerCards[0].address, playerCards[0].cards); + await joinGame(playerCards[1].address, playerCards[1].cards); + await contract.methods.startGame(42).send({ origin: playerCards[0].address }).wait(); + + const game = await playGame(); + + 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.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(), + ).rejects.toThrow(/Not the winner/); + + await contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + const winnerCollection = unwrapOptions( + await contract.methods.viewCollectionCards(winner, 0).view({ from: winner }), ); + expect(winnerCollection).toEqual(expect.arrayContaining([playerCards[0].cards, playerCards[1].cards].flat())); + }, 120_000); + + it('should allow to play with cards won', async () => { + await joinGame(playerCards[0].address, playerCards[0].cards); + await joinGame(playerCards[1].address, playerCards[1].cards); + await contract.methods.startGame(42).send({ origin: playerCards[0].address }).wait(); + let game = await playGame(); + const sotedByPoints = game.players.sort((a, b) => Number(b.points - a.points)); + const winner = AztecAddress.fromBigInt(sotedByPoints[0].address); await contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); - const winnerCollection = (await contract.methods.viewCollectionCards(winner, 0).view({ from: winner })) - .filter((option: any) => option._is_some) - .map((option: any) => option._value); - expect(winnerCollection).toEqual(expect.arrayContaining(cards.flat())); - }, 120_000); + const winnerCollection = unwrapOptions( + (await contract.methods.viewCollectionCards(winner, 0).view({ from: winner })) as NoirOption[], + ); + + const winnerPlayercards = playerCards.find(playerCardsEntry => playerCardsEntry.address.equals(winner))!; + winnerPlayercards.cards = [winnerCollection[0], winnerCollection[3]]; + + await joinGame(winnerPlayercards.address, winnerPlayercards.cards, 43); + await joinGame(playerCards[2].address, playerCards[2].cards, 43); + await contract.methods.startGame(43).send({ origin: playerCards[0].address }).wait(); + game = await playGame(43); + + expect(game.finished).toBe(true); + }, 180_000); }); }); diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr index fe800795fe47..2aa91cc97046 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -216,27 +216,27 @@ contract CardGame { let mut collection = storage.collections.at(player); let _inserted_cards = collection.add_cards(cards_notes, player); - let selector = compute_selector("onCardsClaimed(u32,Field,[Field;4])"); + let selector = compute_selector("onCardsClaimed(u32,Field,Field)"); context.call_public_function( context.this_address(), selector, - [game as Field, player, cards_fields[0], cards_fields[1], cards_fields[2], cards_fields[3]] + [game as Field, player, dep::std::hash::pedersen(cards_fields)[0]] ); } #[aztec(public)] - internal fn onCardsClaimed(game: u32, player: Field, cards_fields: [Field; PLAYABLE_CARDS]) { + internal fn onCardsClaimed(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; - - for i in 0..cards_fields.len() { - let game_card_field = game_data.rounds_cards[i].to_field(); - assert_eq(game_card_field, cards_fields[i], "Claim card mismatch"); - } + + 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"); From 8c751c93b18e4f9f3395c06a4f649e9bc7f04e22 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 13:39:10 +0000 Subject: [PATCH 04/14] test: improve e2e test --- .../end-to-end/src/e2e_card_game.test.ts | 92 +++++++++++-------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 421c01756ff9..15c190823b65 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -43,6 +43,8 @@ function unwrapOptions(options: NoirOption[]): 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; @@ -105,7 +107,7 @@ describe('e2e_card_game', () => { it('should be able to join games', async () => { await contract.methods - .joinGame(42, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .joinGame(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) .send({ origin: firstPlayer }) .wait(); @@ -117,7 +119,7 @@ describe('e2e_card_game', () => { }, ]); - expect((await contract.methods.viewGame(42).view({ from: firstPlayer })) as Game).toMatchObject({ + expect((await contract.methods.viewGame(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ players: [ { address: firstPlayer.toBigInt(), @@ -145,18 +147,18 @@ describe('e2e_card_game', () => { ); await contract.methods - .joinGame(42, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .joinGame(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) .send({ origin: firstPlayer }) .wait(); await contract.methods - .joinGame(42, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) + .joinGame(GAME_ID, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) .send({ origin: secondPlayer }) .wait(); - await contract.methods.startGame(42).send({ origin: firstPlayer }).wait(); + await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); - expect((await contract.methods.viewGame(42).view({ from: firstPlayer })) as Game).toMatchObject({ + expect((await contract.methods.viewGame(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ players: expect.arrayContaining([ { address: firstPlayer.toBigInt(), @@ -178,8 +180,6 @@ describe('e2e_card_game', () => { }); describe('game play', () => { - let playerCards: { address: AztecAddress; cards: Card[] }[]; - let secondPlayerCollection: Card[]; let thirdPlayerCOllection: Card[]; @@ -195,23 +195,17 @@ describe('e2e_card_game', () => { thirdPlayerCOllection = unwrapOptions( await contract.methods.viewCollectionCards(thirdPlayer, 0).view({ from: thirdPlayer }), ); - - playerCards = [ - { address: firstPlayer, cards: [firstPlayerCollection[0], firstPlayerCollection[2]] }, - { address: secondPlayer, cards: [secondPlayerCollection[0], secondPlayerCollection[2]] }, - { address: thirdPlayer, cards: [thirdPlayerCOllection[0], thirdPlayerCOllection[1]] }, - ]; }, 60_000); - async function joinGame(playerAddress: AztecAddress, cards: Card[], id = 42) { + async function joinGame(playerAddress: AztecAddress, cards: Card[], id = GAME_ID) { await contract.methods.joinGame(id, cards.map(cardToField)).send({ origin: playerAddress }).wait(); } - async function playGame(id = 42) { + async function playGame(playerDecks: { address: AztecAddress; deck: Card[] }[], id = GAME_ID) { const initialGameState = (await contract.methods.viewGame(id).view({ from: firstPlayer })) as Game; const players = initialGameState.players.map(player => player.address); const cards = players.map( - player => playerCards.find(playerCardsEntry => playerCardsEntry.address.toBigInt() === player)!.cards, + player => playerDecks.find(playerDeckEntry => playerDeckEntry.address.toBigInt() === player)!.deck, ); for (let roundIndex = 0; roundIndex < cards.length; roundIndex++) { @@ -228,18 +222,20 @@ describe('e2e_card_game', () => { const finalGameState = (await contract.methods.viewGame(id).view({ from: firstPlayer })) as Game; expect(finalGameState.finished).toBe(true); - - console.log(finalGameState.players); - return finalGameState; } it('should play a game and claim the winned cards', async () => { - await joinGame(playerCards[0].address, playerCards[0].cards); - await joinGame(playerCards[1].address, playerCards[1].cards); - await contract.methods.startGame(42).send({ origin: playerCards[0].address }).wait(); - - const game = await playGame(); + const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]]; + const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; + await joinGame(firstPlayer, firstPlayerGameDeck); + await joinGame(secondPlayer, secondPlayerGameDeck); + await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); + + const game = await playGame([ + { address: firstPlayer, deck: firstPlayerGameDeck }, + { address: secondPlayer, deck: secondPlayerGameDeck }, + ]); const sotedByPoints = game.players.sort((a, b) => Number(b.points - a.points)); @@ -247,37 +243,53 @@ describe('e2e_card_game', () => { const loser = AztecAddress.fromBigInt(sotedByPoints[1].address); await expect( - contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(), + contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(), ).rejects.toThrow(/Not the winner/); - await contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + await contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); const winnerCollection = unwrapOptions( await contract.methods.viewCollectionCards(winner, 0).view({ from: winner }), ); - expect(winnerCollection).toEqual(expect.arrayContaining([playerCards[0].cards, playerCards[1].cards].flat())); + expect(winnerCollection).toEqual(expect.arrayContaining([firstPlayerGameDeck, secondPlayerGameDeck].flat())); }, 120_000); - it('should allow to play with cards won', async () => { - await joinGame(playerCards[0].address, playerCards[0].cards); - await joinGame(playerCards[1].address, playerCards[1].cards); - await contract.methods.startGame(42).send({ origin: playerCards[0].address }).wait(); + it.only('should allow to play with cards won', async () => { + const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]]; + const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; + await joinGame(firstPlayer, firstPlayerGameDeck); + await joinGame(secondPlayer, secondPlayerGameDeck); + await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); + + let game = await playGame([ + { address: firstPlayer, deck: firstPlayerGameDeck }, + { address: secondPlayer, deck: secondPlayerGameDeck }, + ]); - let game = await playGame(); const sotedByPoints = game.players.sort((a, b) => Number(b.points - a.points)); const winner = AztecAddress.fromBigInt(sotedByPoints[0].address); - await contract.methods.claimCards(42, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + await contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); const winnerCollection = unwrapOptions( (await contract.methods.viewCollectionCards(winner, 0).view({ from: winner })) as NoirOption[], ); - const winnerPlayercards = playerCards.find(playerCardsEntry => playerCardsEntry.address.equals(winner))!; - winnerPlayercards.cards = [winnerCollection[0], winnerCollection[3]]; + const winnerGameDeck = [winnerCollection[0], winnerCollection[3]]; + const thirdPlayerGameDeck = [thirdPlayerCOllection[0], thirdPlayerCOllection[2]]; - await joinGame(winnerPlayercards.address, winnerPlayercards.cards, 43); - await joinGame(playerCards[2].address, playerCards[2].cards, 43); - await contract.methods.startGame(43).send({ origin: playerCards[0].address }).wait(); - game = await playGame(43); + await joinGame(winner, winnerGameDeck, GAME_ID + 1); + await joinGame(thirdPlayer, thirdPlayerGameDeck, GAME_ID + 1); + + await contract.methods + .startGame(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); From 6a9c6d7d75b9026df6dcda967427dbf09f721705 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 13:39:49 +0000 Subject: [PATCH 05/14] test: remove only --- yarn-project/end-to-end/src/e2e_card_game.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 15c190823b65..de375a2592d3 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -253,7 +253,7 @@ describe('e2e_card_game', () => { expect(winnerCollection).toEqual(expect.arrayContaining([firstPlayerGameDeck, secondPlayerGameDeck].flat())); }, 120_000); - it.only('should allow to play with cards won', async () => { + it('should allow to play with cards won', async () => { const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]]; const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; await joinGame(firstPlayer, firstPlayerGameDeck); From e89741666214a945b7ccacd4d6a918f6b34dba3c Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 13:49:15 +0000 Subject: [PATCH 06/14] chore: add test --- .circleci/config.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index e00eadcb9db0..121f6339cf5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 From d3a58fc40541d9a532a831682192f22d77649f97 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 13:57:01 +0000 Subject: [PATCH 07/14] refactor: don't use zeroed --- .../src/contracts/card_game_contract/src/cards.nr | 9 +++++---- .../src/contracts/card_game_contract/src/main.nr | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr index 38c87abec086..fb35477f590b 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -16,7 +16,6 @@ use dep::aztec::{ }; use dep::std; use dep::std::{ - unsafe::zeroed, option::Option, }; use dep::value_note::{ @@ -204,19 +203,21 @@ global PACK_CARDS = 3; // Limited by number of write requests (max 4) fn get_pack_cards( seed: Field, owner_address: Field -) -> [CardNote; PACK_CARDS] { +) -> [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 = [zeroed(); PACK_CARDS]; + 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] = CardNote::new(strength, points, owner_address); + cards[i] = Card { + strength, points + }; } cards diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr index 2aa91cc97046..742a30658134 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -123,10 +123,11 @@ contract CardGame { seed: Field, // The randomness used to generate the cards. Passed in for now. ) { let storage = Storage::init(Option::some(&mut context), Option::none()); - let mut cards = get_pack_cards(seed, context.msg_sender()); + let buyer = context.msg_sender(); + let mut cards = get_pack_cards(seed, buyer); - let mut collection = storage.collections.at(context.msg_sender()); - let _inserted_cards = collection.add_cards(cards, context.msg_sender()); + let mut collection = storage.collections.at(buyer); + let _inserted_cards = collection.add_cards(cards.map(|card| CardNote::from_card(card, buyer)), buyer); } #[aztec(private)] From 04f7a0fb8fa9d025d857f556e36a473d55cab840 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 14:26:58 +0000 Subject: [PATCH 08/14] refactor: hide card note to the game --- .../contracts/card_game_contract/src/cards.nr | 14 +++++----- .../contracts/card_game_contract/src/main.nr | 27 +++++-------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr index fb35477f590b..dfb94febaccd 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -133,22 +133,22 @@ impl Deck { } } - fn add_cards(&mut self, cards: [CardNote; N], owner: Field) -> [CardNote]{ + fn add_cards(&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 mutable_card = card; - self.set.insert(&mut mutable_card.note); + 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, - mutable_card.note.serialise(), + card_note.note.serialise(), ); - inserted_cards = inserted_cards.push_back(mutable_card); + inserted_cards = inserted_cards.push_back(card_note); } inserted_cards @@ -184,13 +184,13 @@ impl Deck { } } - unconstrained fn view_cards(self, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + unconstrained fn view_cards(self, offset: u32) -> [Option; 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(CardNote::from_note); + opt_cards[i] = opt_notes[i].map(|note: ValueNote| Card::from_field(note.value)); } opt_cards diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr index 742a30658134..6b35621193e8 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -103,7 +103,6 @@ contract CardGame { PACK_CARDS, Deck, Card, - CardNote, get_pack_cards, compute_deck_strength, }; @@ -127,7 +126,7 @@ contract CardGame { let mut cards = get_pack_cards(seed, buyer); let mut collection = storage.collections.at(buyer); - let _inserted_cards = collection.add_cards(cards.map(|card| CardNote::from_card(card, buyer)), buyer); + let _inserted_cards = collection.add_cards(cards, buyer); } #[aztec(private)] @@ -142,9 +141,7 @@ contract CardGame { 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.map(|card: Card| { - CardNote::new(card.strength, card.points, player) - }), player); + let _added_to_game_deck = game_deck.add_cards(cards, player); let selector = compute_selector("onGameJoined(u32,Field,u32)"); let strength = compute_deck_strength(cards); context.call_public_function(context.this_address(), selector, [game as Field, player, strength]); @@ -212,10 +209,10 @@ contract CardGame { ) { let storage = Storage::init(Option::some(&mut context), Option::none()); let player = context.msg_sender(); - let cards_notes = cards_fields.map(|card_field| CardNote::from_card(Card::from_field(card_field), player)); + 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_notes, player); + let _inserted_cards = collection.add_cards(cards, player); let selector = compute_selector("onCardsClaimed(u32,Field,Field)"); context.call_public_function( @@ -248,27 +245,15 @@ contract CardGame { unconstrained fn viewCollectionCards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let storage = Storage::init(Option::none(), Option::none()); let collection = storage.collections.at(owner); - let mut cards = [Option::none(); MAX_NOTES_PER_PAGE]; - let card_notes = collection.view_cards(offset); - - for i in 0..MAX_NOTES_PER_PAGE { - cards[i] = card_notes[i].map(|card_note: CardNote| card_note.card); - } - cards + collection.view_cards(offset) } unconstrained fn viewGameCards(game: u32, player: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let storage = Storage::init(Option::none(), Option::none()); let game_deck = storage.game_decks.at(game as Field).at(player); - let mut cards = [Option::none(); MAX_NOTES_PER_PAGE]; - let card_notes = game_deck.view_cards(offset); - - for i in 0..MAX_NOTES_PER_PAGE { - cards[i] = card_notes[i].map(|card_note: CardNote| card_note.card); - } - cards + game_deck.view_cards(offset) } unconstrained fn viewGame(game: u32) -> Game { From 782c070781fb7ab77bc5ccdd290ebb9154296c09 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 14:36:45 +0000 Subject: [PATCH 09/14] refactor: separate get from remove in deck --- .../contracts/card_game_contract/src/cards.nr | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr index dfb94febaccd..e50c6c6ed9b8 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -154,10 +154,10 @@ impl Deck { inserted_cards } - fn remove_cards(&mut self, cards: [Card; N], owner: Field) { + fn get_cards(&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 removed = [false; N]; + 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( @@ -167,20 +167,24 @@ impl Deck { // spending someone else's notes). assert(card_note.note.owner == owner); - // Removes the note from the owner's set of notes. - self.set.remove(card_note.note); - // set the removed flag to true for j in 0..cards.len() { - if !removed[j] & (cards[j].strength == card_note.card.strength) & (cards[j].points == card_note.card.points) { - removed[j] = true; + 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); } } - } } - for i in 0..removed.len() { - assert(removed[i], "Could not find card"); + found_cards.map(|card_note: Option| { + assert(card_note.is_some(), "Card not found"); + card_note.unwrap_unchecked() + }) + } + + fn remove_cards(&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); } } From 9e756384339fac8b6e729593f68a48ec3f72bdc1 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 14:39:59 +0000 Subject: [PATCH 10/14] fix: missing flag --- .../noir-contracts/src/contracts/card_game_contract/src/cards.nr | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr index e50c6c6ed9b8..e0bb5a538b73 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/cards.nr @@ -106,6 +106,7 @@ fn filter_cards(notes: [Option; MAX_READ_REQUESTS_PER_CALL], desir 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; } } } From c6b749f7287227f9d5d22dfe26532b9fe399cb62 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 14:51:56 +0000 Subject: [PATCH 11/14] refactor: methods to snake_case --- .../end-to-end/src/e2e_card_game.test.ts | 58 +++++++++---------- .../contracts/card_game_contract/src/main.nr | 28 ++++----- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index de375a2592d3..0bccf4fd746f 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -94,24 +94,24 @@ describe('e2e_card_game', () => { ]; it('should be able to buy packs', async () => { - await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); - const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); + 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 contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); - await contract.methods.buyPack(27n).send({ origin: secondPlayer }).wait(); + await contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(); + await contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(); }, 30_000); it('should be able to join games', async () => { await contract.methods - .joinGame(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) .send({ origin: firstPlayer }) .wait(); - const collection = await contract.methods.viewCollectionCards(firstPlayer, 0).view({ from: firstPlayer }); + const collection = await contract.methods.view_collection_cards(firstPlayer, 0).view({ from: firstPlayer }); expect(unwrapOptions(collection)).toEqual([ { points: 60338n, @@ -119,7 +119,7 @@ describe('e2e_card_game', () => { }, ]); - expect((await contract.methods.viewGame(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ + expect((await contract.methods.view_game(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ players: [ { address: firstPlayer.toBigInt(), @@ -142,23 +142,23 @@ describe('e2e_card_game', () => { it('should start games', async () => { const secondPlayerCollection = unwrapOptions( (await contract.methods - .viewCollectionCards(secondPlayer, 0) + .view_collection_cards(secondPlayer, 0) .view({ from: secondPlayer })) as NoirOption[], ); await contract.methods - .joinGame(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) + .join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) .send({ origin: firstPlayer }) .wait(); await contract.methods - .joinGame(GAME_ID, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) + .join_game(GAME_ID, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) .send({ origin: secondPlayer }) .wait(); - await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); + await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait(); - expect((await contract.methods.viewGame(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ + expect((await contract.methods.view_game(GAME_ID).view({ from: firstPlayer })) as Game).toMatchObject({ players: expect.arrayContaining([ { address: firstPlayer.toBigInt(), @@ -184,25 +184,25 @@ describe('e2e_card_game', () => { let thirdPlayerCOllection: Card[]; beforeEach(async () => { - await contract.methods.buyPack(27n).send({ origin: firstPlayer }).wait(); - await contract.methods.buyPack(27n).send({ origin: secondPlayer }).wait(); - await contract.methods.buyPack(27n).send({ origin: thirdPlayer }).wait(); + await contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(); + await contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(); + await contract.methods.buy_pack(27n).send({ origin: thirdPlayer }).wait(); secondPlayerCollection = unwrapOptions( - await contract.methods.viewCollectionCards(secondPlayer, 0).view({ from: secondPlayer }), + await contract.methods.view_collection_cards(secondPlayer, 0).view({ from: secondPlayer }), ); thirdPlayerCOllection = unwrapOptions( - await contract.methods.viewCollectionCards(thirdPlayer, 0).view({ from: thirdPlayer }), + 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.joinGame(id, cards.map(cardToField)).send({ origin: playerAddress }).wait(); + 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.viewGame(id).view({ from: firstPlayer })) as Game; + 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, @@ -213,13 +213,13 @@ describe('e2e_card_game', () => { const player = players[playerIndex]; const card = cards[playerIndex][roundIndex]; await contract.methods - .playCard(id, card) + .play_card(id, card) .send({ origin: AztecAddress.fromBigInt(player) }) .wait(); } } - const finalGameState = (await contract.methods.viewGame(id).view({ from: firstPlayer })) as Game; + const finalGameState = (await contract.methods.view_game(id).view({ from: firstPlayer })) as Game; expect(finalGameState.finished).toBe(true); return finalGameState; @@ -230,7 +230,7 @@ describe('e2e_card_game', () => { const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; await joinGame(firstPlayer, firstPlayerGameDeck); await joinGame(secondPlayer, secondPlayerGameDeck); - await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); + await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait(); const game = await playGame([ { address: firstPlayer, deck: firstPlayerGameDeck }, @@ -243,12 +243,12 @@ describe('e2e_card_game', () => { const loser = AztecAddress.fromBigInt(sotedByPoints[1].address); await expect( - contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(), + contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: loser }).wait(), ).rejects.toThrow(/Not the winner/); - await contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + await contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); const winnerCollection = unwrapOptions( - await contract.methods.viewCollectionCards(winner, 0).view({ from: winner }), + await contract.methods.view_collection_cards(winner, 0).view({ from: winner }), ); expect(winnerCollection).toEqual(expect.arrayContaining([firstPlayerGameDeck, secondPlayerGameDeck].flat())); }, 120_000); @@ -258,7 +258,7 @@ describe('e2e_card_game', () => { const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; await joinGame(firstPlayer, firstPlayerGameDeck); await joinGame(secondPlayer, secondPlayerGameDeck); - await contract.methods.startGame(GAME_ID).send({ origin: firstPlayer }).wait(); + await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait(); let game = await playGame([ { address: firstPlayer, deck: firstPlayerGameDeck }, @@ -267,10 +267,10 @@ describe('e2e_card_game', () => { const sotedByPoints = game.players.sort((a, b) => Number(b.points - a.points)); const winner = AztecAddress.fromBigInt(sotedByPoints[0].address); - await contract.methods.claimCards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); + await contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); const winnerCollection = unwrapOptions( - (await contract.methods.viewCollectionCards(winner, 0).view({ from: winner })) as NoirOption[], + (await contract.methods.view_collection_cards(winner, 0).view({ from: winner })) as NoirOption[], ); const winnerGameDeck = [winnerCollection[0], winnerCollection[3]]; @@ -280,7 +280,7 @@ describe('e2e_card_game', () => { await joinGame(thirdPlayer, thirdPlayerGameDeck, GAME_ID + 1); await contract.methods - .startGame(GAME_ID + 1) + .start_game(GAME_ID + 1) .send({ origin: winner }) .wait(); game = await playGame( diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr index 6b35621193e8..a1d8b0ce3caf 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/src/main.nr @@ -118,7 +118,7 @@ contract CardGame { fn constructor() {} #[aztec(private)] - fn buyPack( + 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()); @@ -130,7 +130,7 @@ contract CardGame { } #[aztec(private)] - fn joinGame( + fn join_game( game: u32, cards_fields: [Field; 2], ) { @@ -142,13 +142,13 @@ contract CardGame { 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("onGameJoined(u32,Field,u32)"); + 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 onGameJoined( + internal fn on_game_joined( game: u32, player: Field, deck_strength: u32, @@ -163,7 +163,7 @@ contract CardGame { } #[aztec(public)] - fn startGame(game: u32) { + fn start_game(game: u32) { let storage = Storage::init(Option::none(), Option::some(&mut context)); let game_storage = storage.games.at(game as Field); @@ -173,7 +173,7 @@ contract CardGame { } #[aztec(private)] - fn playCard( + fn play_card( game: u32, card: Card, ) { @@ -183,12 +183,12 @@ contract CardGame { let mut game_deck = storage.game_decks.at(game as Field).at(player); game_deck.remove_cards([card], player); - let selector = compute_selector("onCardPlayed(u32,Field,Field)"); + 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 onCardPlayed(game: u32, player: Field, card_as_field: Field) { + 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); @@ -203,7 +203,7 @@ contract CardGame { } #[aztec(private)] - fn claimCards( + fn claim_cards( game: u32, cards_fields: [Field; PLAYABLE_CARDS], ) { @@ -214,7 +214,7 @@ contract CardGame { let mut collection = storage.collections.at(player); let _inserted_cards = collection.add_cards(cards, player); - let selector = compute_selector("onCardsClaimed(u32,Field,Field)"); + let selector = compute_selector("on_cards_claimed(u32,Field,Field)"); context.call_public_function( context.this_address(), selector, @@ -223,7 +223,7 @@ contract CardGame { } #[aztec(public)] - internal fn onCardsClaimed(game: u32, player: Field, cards_hash: Field) { + 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(); @@ -242,21 +242,21 @@ contract CardGame { game_storage.write(game_data); } - unconstrained fn viewCollectionCards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + unconstrained fn view_collection_cards(owner: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let storage = Storage::init(Option::none(), Option::none()); let collection = storage.collections.at(owner); collection.view_cards(offset) } - unconstrained fn viewGameCards(game: u32, player: Field, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { + unconstrained fn view_game_cards(game: u32, player: Field, offset: u32) -> [Option; 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 viewGame(game: u32) -> Game { + unconstrained fn view_game(game: u32) -> Game { Storage::init(Option::none(), Option::none()).games.at(game as Field).read() } From 8c728692f138fdb75d024694d10d984756346858 Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 15:40:53 +0000 Subject: [PATCH 12/14] test: improve e2e card game test --- .../end-to-end/src/e2e_card_game.test.ts | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 0bccf4fd746f..3ebe6570b79a 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -111,6 +111,13 @@ describe('e2e_card_game', () => { .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([ { @@ -225,20 +232,19 @@ describe('e2e_card_game', () => { return finalGameState; } - it('should play a game and claim the winned cards', async () => { + 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 joinGame(firstPlayer, firstPlayerGameDeck); await joinGame(secondPlayer, secondPlayerGameDeck); await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait(); - const game = await playGame([ + 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); @@ -246,27 +252,6 @@ describe('e2e_card_game', () => { 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 }), - ); - expect(winnerCollection).toEqual(expect.arrayContaining([firstPlayerGameDeck, secondPlayerGameDeck].flat())); - }, 120_000); - - it('should allow to play with cards won', async () => { - const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]]; - const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; - await joinGame(firstPlayer, firstPlayerGameDeck); - await 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); await contract.methods.claim_cards(GAME_ID, game.rounds_cards.map(cardToField)).send({ origin: winner }).wait(); const winnerCollection = unwrapOptions( From 5519740a1637354c5c686d99e42b55e1e920c6be Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 15:41:27 +0000 Subject: [PATCH 13/14] chore: remove unnecessary dep --- .../noir-contracts/src/contracts/card_game_contract/Nargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml b/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml index e2491ce5d1fa..40f9e3fbffaa 100644 --- a/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml +++ b/yarn-project/noir-contracts/src/contracts/card_game_contract/Nargo.toml @@ -7,4 +7,3 @@ type = "contract" [dependencies] aztec = { path = "../../../../noir-libs/noir-aztec" } value_note = { path = "../../../../noir-libs/value-note"} -easy_private_state = { path = "../../../../noir-libs/easy-private-state"} \ No newline at end of file From 048cd5bdad2267754b0cf039f3d69d56399adaef Mon Sep 17 00:00:00 2001 From: sirasistant Date: Fri, 8 Sep 2023 15:46:47 +0000 Subject: [PATCH 14/14] test: add some parallel txs to speedup test --- .../end-to-end/src/e2e_card_game.test.ts | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 3ebe6570b79a..cc2c21636301 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -101,8 +101,10 @@ describe('e2e_card_game', () => { describe('game join', () => { beforeEach(async () => { - await contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(); - await contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(); + 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 () => { @@ -153,15 +155,16 @@ describe('e2e_card_game', () => { .view({ from: secondPlayer })) as NoirOption[], ); - await contract.methods - .join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) - .send({ origin: firstPlayer }) - .wait(); - - await contract.methods - .join_game(GAME_ID, [cardToField(secondPlayerCollection[0]), cardToField(secondPlayerCollection[2])]) - .send({ origin: secondPlayer }) - .wait(); + 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(); @@ -191,9 +194,11 @@ describe('e2e_card_game', () => { let thirdPlayerCOllection: Card[]; beforeEach(async () => { - await contract.methods.buy_pack(27n).send({ origin: firstPlayer }).wait(); - await contract.methods.buy_pack(27n).send({ origin: secondPlayer }).wait(); - await contract.methods.buy_pack(27n).send({ origin: thirdPlayer }).wait(); + 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 }), @@ -235,8 +240,7 @@ describe('e2e_card_game', () => { 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 joinGame(firstPlayer, firstPlayerGameDeck); - await joinGame(secondPlayer, secondPlayerGameDeck); + await Promise.all([joinGame(firstPlayer, firstPlayerGameDeck), joinGame(secondPlayer, secondPlayerGameDeck)]); await contract.methods.start_game(GAME_ID).send({ origin: firstPlayer }).wait(); let game = await playGame([ @@ -261,8 +265,10 @@ describe('e2e_card_game', () => { const winnerGameDeck = [winnerCollection[0], winnerCollection[3]]; const thirdPlayerGameDeck = [thirdPlayerCOllection[0], thirdPlayerCOllection[2]]; - await joinGame(winner, winnerGameDeck, GAME_ID + 1); - await joinGame(thirdPlayer, thirdPlayerGameDeck, GAME_ID + 1); + await Promise.all([ + joinGame(winner, winnerGameDeck, GAME_ID + 1), + joinGame(thirdPlayer, thirdPlayerGameDeck, GAME_ID + 1), + ]); await contract.methods .start_game(GAME_ID + 1)