> For the complete documentation index, see [llms.txt](/llms.txt)

# The Contract

Prerequisites

Make sure you've completed the [setup steps](/developers/docs/tutorials/js_tutorials/webapp.md#clone-the-example) before continuing.

In this section, you walk through the Pod Racing smart contract, compile it, and run a standalone script that deploys and plays a full game against a local network.

## Overview

Before looking at code, here is how the contract is structured.

**Storage:**

* `admin`: the address that deployed the contract
* `races`: a public map from game ID to `Race` struct (the shared game state)
* `progress`: a private map storing each player's per-round point allocations as encrypted notes
* `win_history`: a public map tracking each player's lifetime win count
* `finalized`: a public map recording which games have been finalized, so a game's winner is credited once

**Game lifecycle:**

1. `create_game`: player 1 calls this to create a new `Race` in public storage, setting a block deadline
2. `join_game`: player 2 joins, filling the second slot in the `Race`
3. `play_round` (private): each player submits a `GameRoundNote` containing their point allocation for one round; a public follow-up increments the round counter without revealing points
4. `finish_game` (private): reads all of your `GameRoundNote`s, sums totals per track, and publishes the aggregated scores publicly
5. `finalize_game` (public): once both players have revealed, compares their track totals, declares the winner (best of 5), and updates win history, once per game

**Key types:**

* `GameRoundNote`: a private note storing one round's point allocation (5 track values), the round number, and the owner
* `Race`: the public game state: both player addresses, round counters, final per-track scores, block deadline, and winner

The core design principle is that **private functions** (`play_round`, `finish_game`) hide your strategy, while **public functions** (`create_game`, `join_game`, `finalize_game`) coordinate shared state that both players can see.

## Nargo.toml

Open `contracts/Nargo.toml`. This configures the Noir compiler for the contract:

```toml
[package]
name = "pod_racing_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../.aztec-node/tree/noir-projects/aztec-nr/aztec" }
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml#L1-L10](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/Nargo.toml#L1-L10)

note

The `aztec` path points into the aztec-node checkout that the [clone steps](/developers/docs/tutorials/js_tutorials/webapp.md#clone-the-example) create. In your own project, use the git dependency instead, with the tag matching your Aztec CLI version:

```toml
aztec = { git = "https://github.com/aztec-labs-eng/aztec-nr/", tag = "v6.0.0", directory = "aztec" }
```

## GameRoundNote

Open `contracts/src/game_round_note.nr`. This is a private note that stores a player's point allocation for one round:

```rust
use aztec::{macros::notes::note, protocol::{traits::Packable, address::AztecAddress}};

/// A private note storing a player's point allocation for one round.
/// These notes remain private until the player calls finish_game to reveal totals.
#[derive(Eq, Packable)]
#[note]
pub struct GameRoundNote {
    pub track1: u8,
    pub track2: u8,
    pub track3: u8,
    pub track4: u8,
    pub track5: u8,
    pub round: u8,
    pub owner: AztecAddress,
}

impl GameRoundNote {
    pub fn new(track1: u8, track2: u8, track3: u8, track4: u8, track5: u8, round: u8, owner: AztecAddress) -> Self {
        Self { track1, track2, track3, track4, track5, round, owner }
    }

}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/game\_round\_note.nr#L1-L24](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/game_round_note.nr#L1-L24)

The fields `track1` through `track5` store the points allocated to each track for that round. `round` identifies which round (1, 2, or 3) the note belongs to. `owner` is the player's address — only the owner's PXE (Private eXecution Environment) can decrypt and read this note.

### How private notes work

In Aztec, private state is stored as **notes** — encrypted data objects that live in your PXE local database. When a note is created, it is encrypted with the owner's public key so that only the owner's PXE can decrypt and read it. The note is also committed to the network as a hash (called a commitment), which allows for proving the note exists without revealing its contents.

When you call `play_round`, the contract creates a `GameRoundNote` with your allocation. The flow:

1. You call `play_round(gameId, round, 3, 2, 1, 2, 1)` — a **private function**
2. The contract creates a `GameRoundNote` with your allocation, stored privately
3. It then enqueues a **public** call to `validate_and_play_round` which increments your round counter (visible) without revealing your points (hidden). "Enqueues" means the private function schedules a public function to run after the private execution completes. Private functions cannot modify public state directly, so they use this mechanism to trigger public side effects (e.g., state changes or emitting logs).
4. Your opponent's PXE cannot decrypt your notes — they only see that you completed a round

This means:

* **You** know your own allocations
* **Your opponent** only sees that you've played a round (public round counter)
* **The network** only sees encrypted data

## Race struct

Open `contracts/src/race.nr`. This is the public game state:

```rust
use aztec::protocol::{
    address::AztecAddress,
    traits::{Deserialize, Serialize, Packable},
};

/// Public game state: player addresses, round progress, and final track scores.
#[derive(Deserialize, Serialize, Eq, Packable)]
pub struct Race {
    pub player1: AztecAddress,
    pub player2: AztecAddress,
    pub total_rounds: u8,
    pub player1_round: u8,
    pub player2_round: u8,
    pub player1_track1_final: u64,
    pub player1_track2_final: u64,
    pub player1_track3_final: u64,
    pub player1_track4_final: u64,
    pub player1_track5_final: u64,
    pub player2_track1_final: u64,
    pub player2_track2_final: u64,
    pub player2_track3_final: u64,
    pub player2_track4_final: u64,
    pub player2_track5_final: u64,
    pub end_block: u32,
}

impl Race {
    pub fn new(player1: AztecAddress, total_rounds: u8, end_block: u32) -> Race {
        Self {
            player1,
            player2: AztecAddress::zero(),
            total_rounds,
            player1_round: 0,
            player2_round: 0,
            player1_track1_final: 0,
            player1_track2_final: 0,
            player1_track3_final: 0,
            player1_track4_final: 0,
            player1_track5_final: 0,
            player2_track1_final: 0,
            player2_track2_final: 0,
            player2_track3_final: 0,
            player2_track4_final: 0,
            player2_track5_final: 0,
            end_block,
        }
    }

    pub fn join(self, player2: AztecAddress) -> Race {
        assert(!self.player1.eq(AztecAddress::zero()), "game does not exist");
        assert(self.player2.eq(AztecAddress::zero()), "game is full");
        assert(!self.player1.eq(player2), "cannot join your own game");
        Self {
            player1: self.player1,
            player2,
            total_rounds: self.total_rounds,
            player1_round: self.player1_round,
            player2_round: self.player2_round,
            player1_track1_final: 0,
            player1_track2_final: 0,
            player1_track3_final: 0,
            player1_track4_final: 0,
            player1_track5_final: 0,
            player2_track1_final: 0,
            player2_track2_final: 0,
            player2_track3_final: 0,
            player2_track4_final: 0,
            player2_track5_final: 0,
            end_block: self.end_block,
        }
    }

    pub fn increment_player_round(self, player: AztecAddress, round: u8) -> Race {
        assert(round < self.total_rounds + 1, "no such round");
        let ret = if player.eq(self.player1) {
            assert(round == self.player1_round + 1, "rounds must be played in order");
            Option::some(Self {
                player1: self.player1, player2: self.player2,
                total_rounds: self.total_rounds,
                player1_round: round, player2_round: self.player2_round,
                player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final,
                player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final,
                player1_track5_final: self.player1_track5_final,
                player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final,
                player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final,
                player2_track5_final: self.player2_track5_final,
                end_block: self.end_block,
            })
        } else if player.eq(self.player2) {
            assert(round == self.player2_round + 1, "rounds must be played in order");
            Option::some(Self {
                player1: self.player1, player2: self.player2,
                total_rounds: self.total_rounds,
                player1_round: self.player1_round, player2_round: round,
                player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final,
                player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final,
                player1_track5_final: self.player1_track5_final,
                player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final,
                player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final,
                player2_track5_final: self.player2_track5_final,
                end_block: self.end_block,
            })
        } else {
            Option::none()
        };
        ret.unwrap()
    }

    pub fn set_player_scores(self, player: AztecAddress, track1_final: u64, track2_final: u64, track3_final: u64, track4_final: u64, track5_final: u64) -> Race {
        let ret = if player.eq(self.player1) {
            assert(
                self.player1_track1_final + self.player1_track2_final +
                self.player1_track3_final + self.player1_track4_final +
                self.player1_track5_final == 0,
                "scores already revealed",
            );
            Option::some(Self {
                player1: self.player1, player2: self.player2,
                total_rounds: self.total_rounds,
                player1_round: self.player1_round, player2_round: self.player2_round,
                player1_track1_final: track1_final, player1_track2_final: track2_final,
                player1_track3_final: track3_final, player1_track4_final: track4_final,
                player1_track5_final: track5_final,
                player2_track1_final: self.player2_track1_final, player2_track2_final: self.player2_track2_final,
                player2_track3_final: self.player2_track3_final, player2_track4_final: self.player2_track4_final,
                player2_track5_final: self.player2_track5_final,
                end_block: self.end_block,
            })
        } else if player.eq(self.player2) {
            assert(
                self.player2_track1_final + self.player2_track2_final +
                self.player2_track3_final + self.player2_track4_final +
                self.player2_track5_final == 0,
                "scores already revealed",
            );
            Option::some(Self {
                player1: self.player1, player2: self.player2,
                total_rounds: self.total_rounds,
                player1_round: self.player1_round, player2_round: self.player2_round,
                player1_track1_final: self.player1_track1_final, player1_track2_final: self.player1_track2_final,
                player1_track3_final: self.player1_track3_final, player1_track4_final: self.player1_track4_final,
                player1_track5_final: self.player1_track5_final,
                player2_track1_final: track1_final, player2_track2_final: track2_final,
                player2_track3_final: track3_final, player2_track4_final: track4_final,
                player2_track5_final: track5_final,
                end_block: self.end_block,
            })
        } else {
            Option::none()
        };
        ret.unwrap()
    }

    /// A zero sum reads as "not revealed", so a player who put no points on any track cannot be told apart.
    pub fn both_revealed(self) -> bool {
        let player1_sum = self.player1_track1_final + self.player1_track2_final + self.player1_track3_final
            + self.player1_track4_final + self.player1_track5_final;
        let player2_sum = self.player2_track1_final + self.player2_track2_final + self.player2_track3_final
            + self.player2_track4_final + self.player2_track5_final;
        (player1_sum != 0) & (player2_sum != 0)
    }

    pub fn calculate_winner(self, current_block_number: u32) -> AztecAddress {
        assert(current_block_number > self.end_block, "game has not ended");
        let mut player1_wins = 0;
        let mut player2_wins = 0;

        if self.player1_track1_final > self.player2_track1_final { player1_wins += 1; } else { player2_wins += 1; };
        if self.player1_track2_final > self.player2_track2_final { player1_wins += 1; } else { player2_wins += 1; };
        if self.player1_track3_final > self.player2_track3_final { player1_wins += 1; } else { player2_wins += 1; };
        if self.player1_track4_final > self.player2_track4_final { player1_wins += 1; } else { player2_wins += 1; };
        if self.player1_track5_final > self.player2_track5_final { player1_wins += 1; } else { player2_wins += 1; };

        if (player1_wins > player2_wins) { self.player1 } else { self.player2 }
    }
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr#L1-L178](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/race.nr#L1-L178)

The `Race` struct holds both player addresses, per-player round counters, the final aggregated scores for each track (filled in when a player calls `finish_game`), and a block deadline. The winner calculation compares track-by-track totals: whoever wins more of the 5 tracks wins the game.

## Main contract

Open `contracts/src/main.nr`. This is the main contract file that defines the game flow.

### Storage

```rust
#[storage]
struct Storage<Context> {
    admin: PublicMutable<AztecAddress, Context>,
    races: Map<Field, PublicMutable<Race, Context>, Context>,
    progress: Map<Field, Owned<PrivateSet<GameRoundNote, Context>, Context>, Context>,
    win_history: Map<AztecAddress, PublicMutable<u64, Context>, Context>,
    finalized: Map<Field, PublicMutable<bool, Context>, Context>,
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L37-L46](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L37-L46)

The storage maps directly to the overview above: `admin` for the deployer, `races` for game state, `progress` for private round notes, `win_history` for tracking wins, and `finalized` for games already settled.

### Creating and joining a game

```rust
#[external("public")]
fn create_game(game_id: Field) {
    assert(
        self.storage.races.at(game_id).read().player1.eq(AztecAddress::zero()),
        "game already exists",
    );
    let game = Race::new(
        self.msg_sender(),
        TOTAL_ROUNDS,
        self.context.block_number() + GAME_LENGTH,
    );
    self.storage.races.at(game_id).write(game);
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L54-L68](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L54-L68)

```rust
#[external("public")]
fn join_game(game_id: Field) {
    let maybe_existing_game = self.storage.races.at(game_id).read();
    let joined_game = maybe_existing_game.join(self.msg_sender());
    self.storage.races.at(game_id).write(joined_game);
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L70-L77](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L70-L77)

Both are public functions. `create_game` initializes a new `Race` with the caller as player 1 and sets a block deadline. `join_game` fills in player 2.

### Playing a round (private)

```rust
/// Allocates points across 5 tracks for a round.
/// This is a PRIVATE function - the allocation remains hidden from the opponent.
#[external("private")]
fn play_round(
    game_id: Field,
    round: u8,
    track1: u8,
    track2: u8,
    track3: u8,
    track4: u8,
    track5: u8,
) {
    assert(track1 + track2 + track3 + track4 + track5 < 10, "a round allows at most 9 points");

    let player = self.msg_sender();

    self
        .storage
        .progress
        .at(game_id)
        .at(player)
        .insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player))
        .deliver(MessageDelivery::onchain_unconstrained());

    self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round(
        player,
        game_id,
        round,
    ));
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L79-L110](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L79-L110)

This is a **private function** — the point allocation remains hidden from the opponent. It creates a `GameRoundNote`, then enqueues a public call to `validate_and_play_round` to increment the round counter without revealing points.

### Finishing the game (reveal)

```rust
/// Reveals a player's total scores per track.
/// Reads private round notes and publishes aggregated totals.
#[external("private")]
fn finish_game(game_id: Field) {
    let player = self.msg_sender();
    let totals =
        self.storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new());

    let mut total_track1: u64 = 0;
    let mut total_track2: u64 = 0;
    let mut total_track3: u64 = 0;
    let mut total_track4: u64 = 0;
    let mut total_track5: u64 = 0;

    for i in 0..TOTAL_ROUNDS {
        total_track1 += totals.get(i as u32).note.track1 as u64;
        total_track2 += totals.get(i as u32).note.track2 as u64;
        total_track3 += totals.get(i as u32).note.track3 as u64;
        total_track4 += totals.get(i as u32).note.track4 as u64;
        total_track5 += totals.get(i as u32).note.track5 as u64;
    }

    self.enqueue(PodRacing::at(self.context.this_address()).validate_finish_game_and_reveal(
        player,
        game_id,
        total_track1,
        total_track2,
        total_track3,
        total_track4,
        total_track5,
    ));
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L119-L152](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L119-L152)

Another private function. It reads all your `GameRoundNote`s, sums up totals per track, and publishes the aggregated scores to public state. Your per-round choices stay hidden, but your final totals become visible.

### Finalizing the game

```rust
/// Determines the winner after both players have revealed and the game has expired. Each game is finalized once.
#[external("public")]
fn finalize_game(game_id: Field) {
    let game_in_progress = self.storage.races.at(game_id).read();
    assert(!game_in_progress.player2.eq(AztecAddress::zero()), "game has not started");
    assert(game_in_progress.both_revealed(), "both players must reveal");
    assert(!self.storage.finalized.at(game_id).read(), "game already finalized");
    self.storage.finalized.at(game_id).write(true);

    let winner = game_in_progress.calculate_winner(self.context.block_number());
    let previous_wins = self.storage.win_history.at(winner).read();
    self.storage.win_history.at(winner).write(previous_wins + 1);
}
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L176-L190](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/contracts/src/main.nr#L176-L190)

A public function that compares both players' track totals, declares the winner (best of 5 tracks), and updates the win history. It requires that player 2 has joined, that both players have revealed, and that the game's block deadline has passed. It marks the game as finalized, so a second call fails instead of crediting the winner again.

## Compile the contract

With the Aztec CLI installed and the contract source in place:

```bash
# Compile the Noir contract and generate TypeScript bindings
yarn prep
```

This runs `yarn compile && yarn codegen`, producing `src/artifacts/PodRacing.ts` (the typed contract class) and `src/artifacts/pod_racing_contract-PodRacing.json` (the compiled artifact). You'll import from `PodRacing.ts` throughout the app.

## Deploy and interact via script

To verify everything works, run a standalone TypeScript script that deploys the contract and plays a full game against a local network.

Open `scripts/deploy-and-interact.ts`. It follows the same pattern as the [aztec.js Getting Started](/developers/docs/tutorials/js_tutorials/aztecjs-getting-started.md) guide.

### Setup

```typescript
import { EmbeddedWallet } from "@aztec-labs/wallets/embedded";
import { getInitialTestAccountsData } from "@aztec-labs/accounts/testing";
// @ts-ignore — generated artifact, may not exist until compiled
import { PodRacingContract } from "../src/artifacts/PodRacing.js";

const nodeUrl = process.env.AZTEC_NODE_URL ?? "http://localhost:8080";
const wallet = await EmbeddedWallet.create(nodeUrl, { ephemeral: true });

const [alice, bob] = await getInitialTestAccountsData();
await wallet.createSchnorrInitializerlessAccount(alice.secret, alice.salt, alice.signingKey);
await wallet.createSchnorrInitializerlessAccount(bob.secret, bob.salt, bob.signingKey);
console.log(
  "Accounts ready:",
  alice.address.toString(),
  bob.address.toString(),
);
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L1-L18](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L1-L18)

This uses `EmbeddedWallet` from `@aztec-labs/wallets/embedded` — a ready-made embedded wallet that handles PXE creation and account management. `getInitialTestAccountsData` provides the local network's genesis-funded test accounts, created here as initializerless Schnorr accounts. The `{ ephemeral: true }` option means PXE state is not persisted between runs.

### Deploy the contract

```typescript
const { contract } = await PodRacingContract.deploy(wallet, alice.address).send(
  {
    from: alice.address,
  },
);
console.log("Contract deployed at:", contract.address.toString());
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L20-L27](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L20-L27)

### Create and join a game

```typescript
const gameId = 1n;
await contract.methods.create_game(gameId).send({ from: alice.address });
console.log("Game created");

await contract.methods.join_game(gameId).send({ from: bob.address });
console.log("Bob joined the game");
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L29-L36](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L29-L36)

### Play rounds

```typescript
// Round 1
await contract.methods
  .play_round(gameId, 1, 3, 2, 1, 2, 1)
  .send({ from: alice.address });
await contract.methods
  .play_round(gameId, 1, 1, 1, 3, 2, 2)
  .send({ from: bob.address });

// Round 2
await contract.methods
  .play_round(gameId, 2, 2, 3, 1, 1, 2)
  .send({ from: alice.address });
await contract.methods
  .play_round(gameId, 2, 2, 2, 2, 2, 1)
  .send({ from: bob.address });

// Round 3
await contract.methods
  .play_round(gameId, 3, 1, 1, 2, 3, 2)
  .send({ from: alice.address });
await contract.methods
  .play_round(gameId, 3, 3, 1, 1, 1, 3)
  .send({ from: bob.address });
console.log("All rounds played");
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L38-L63](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L38-L63)

Each `play_round` call is a private transaction — the point allocations are encrypted as notes. Alice and Bob each play 3 rounds.

### Finish and finalize

```typescript
await contract.methods.finish_game(gameId).send({ from: alice.address });
await contract.methods.finish_game(gameId).send({ from: bob.address });
console.log("Both players revealed scores");

await contract.methods.finalize_game(gameId).send({ from: alice.address });
console.log("Game finalized! Winner determined.");
```

> [Source code: docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L65-L72](https://github.com/AztecProtocol/aztec-packages/blob/c0643538fa9523cca55eab25f5befd31b4d115b8/docs/lines/v6/examples/webapp-tutorial/scripts/deploy-and-interact.ts#L65-L72)

Both players call `finish_game` to reveal their aggregated scores, then either player calls `finalize_game` to determine the winner.

### Run it

Make sure you have a local network running first:

```bash
# Terminal 1: Start the local network
aztec start --local-network
```

Once the network is ready (you can check with `curl http://localhost:8080/status`), run the script in a separate terminal:

```bash
# Terminal 2: Run the script
yarn interact
```

You should see output showing the contract deployment, game creation, rounds being played, and the winner being determined. The script takes a few minutes to complete as each transaction requires proof generation.

## Next steps

With the contract understood and verified, continue to [project setup](/developers/docs/tutorials/js_tutorials/webapp/project-setup.md) to see how the webapp is structured.
