Skip to content

Commit 59e495c

Browse files
author
Esau
committed
Address
1 parent fcbd12e commit 59e495c

File tree

11 files changed

+1023
-117
lines changed

11 files changed

+1023
-117
lines changed

Nargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "private_voting_contract"
2+
name = "pod_racing_contract"
33
type = "contract"
44
authors = [ "" ]
55
compiler_version = ">=0.18.0"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "private_voting_codespace",
2+
"name": "pod_racing_codespace",
33
"version": "1.0.0",
44
"main": "index.js",
55
"repository": "https://github.com/critesjosh/private_voting_codespace.git",

src/game_round_note.nr

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
use aztec::{macros::notes::note, protocol_types::{traits::Packable, address::AztecAddress}};
22

3+
// GameRoundNote is a private note that stores a player's point allocation for one round
4+
// These notes remain private until the player calls finish_game to reveal their totals
5+
//
6+
// Privacy model:
7+
// - Each player creates 3 of these notes (one per round) when playing
8+
// - Only the owner can read their own notes
9+
// - During finish_game, the player sums all their notes and reveals the totals publicly
10+
// - This implements a commit-reveal scheme for fair play
311
#[derive(Eq, Packable)]
412
#[note]
513
pub struct GameRoundNote {
14+
// Points allocated to each of the 5 tracks in this round
15+
// Must sum to less than 10 points per round
616
pub track1: u8,
717
pub track2: u8,
818
pub track3: u8,
919
pub track4: u8,
1020
pub track5: u8,
21+
22+
// Which round this note represents (1, 2, or 3)
1123
pub round: u8,
24+
25+
// The player who created this note (only they can read it)
1226
pub owner: AztecAddress,
1327
}
1428

1529
impl GameRoundNote {
30+
// Creates a new note with the player's round choices
31+
// This note gets stored privately and can only be read by the owner
1632
pub fn new(track1: u8, track2: u8, track3: u8, track4: u8, track5: u8, round: u8, owner: AztecAddress) -> Self {
1733
Self {
1834
track1,
@@ -25,6 +41,7 @@ impl GameRoundNote {
2541
}
2642
}
2743

44+
// Helper method to access the note data
2845
pub fn get(self) -> Self {
2946
Self {
3047
track1: self.track1,

src/main.nr

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
// mod test;
1+
// Pod Racing Game Contract
2+
//
3+
// This is a two-player competitive racing game where players allocate points across 5 tracks
4+
// over multiple rounds. The game flow:
5+
// 1. Player 1 creates a game with a time limit
6+
// 2. Player 2 joins the game
7+
// 3. Both players play rounds privately (allocating points across tracks)
8+
// 4. After all rounds, players reveal their total scores per track
9+
// 5. Winner is determined by who won more tracks (best of 5)
10+
//
11+
// Key mechanics:
12+
// - Each round, players distribute up to 9 points across 5 tracks
13+
// - Round choices are private until the finish phase
14+
// - The player with higher total points on a track wins that track
15+
// - The player who wins 3+ tracks wins the game
16+
17+
mod test;
218
mod game_round_note;
319
mod race;
420

@@ -16,14 +32,26 @@ pub contract PodRacing {
1632

1733
use crate::{game_round_note::GameRoundNote, race::Race};
1834

19-
global TOTAL_ROUNDS: u8 = 3;
20-
global GAME_LENGTH: u32 = 300;
35+
// Game configuration constants
36+
global TOTAL_ROUNDS: u8 = 3; // Each game consists of 3 rounds
37+
global GAME_LENGTH: u32 = 300; // Games expire after 300 blocks
2138

2239
#[storage]
2340
struct Storage<Context> {
41+
// Contract administrator address
2442
admin: PublicMutable<AztecAddress, Context>,
43+
44+
// Maps game_id -> Race struct containing public game state
45+
// Stores player addresses, round progress, and final track scores
2546
races: Map<Field, PublicMutable<Race, Context>, Context>,
26-
progress: Map<Field, PrivateSet<GameRoundNote, Context>, Context>,
47+
48+
// Maps game_id -> player_address -> private notes containing that player's round choices
49+
// Each GameRoundNote stores the point allocation for one round
50+
// This data remains private until the player calls finish_game
51+
progress: Map<Field, Map<AztecAddress, PrivateSet<GameRoundNote, Context>, Context>, Context>,
52+
53+
// Maps player address -> total number of wins
54+
// Public leaderboard tracking career victories
2755
win_history: Map<AztecAddress, PublicMutable<u64, Context>, Context>,
2856
}
2957

@@ -33,58 +61,94 @@ pub contract PodRacing {
3361
storage.admin.write(admin);
3462
}
3563

64+
// Creates a new game instance
65+
// The caller becomes player1 and waits for an opponent to join
66+
// Sets the game expiration to current block + GAME_LENGTH
3667
#[external("public")]
3768
fn create_game(game_id: Field) {
69+
// Ensure this game_id hasn't been used yet (player1 must be zero address)
3870
assert(storage.races.at(game_id).read().player1.eq(AztecAddress::zero()));
3971

72+
// Initialize a new Race with the caller as player1
4073
let game = Race::new(context.msg_sender().unwrap(), TOTAL_ROUNDS, context.block_number() + GAME_LENGTH);
4174
storage.races.at(game_id).write(game);
4275
}
4376

77+
// Allows a second player to join an existing game
78+
// After joining, both players can start playing rounds
4479
#[external("public")]
4580
fn join_game(game_id: Field) {
4681
let maybe_existing_game = storage.races.at(game_id).read();
4782

83+
// Add the caller as player2 (validates that player1 exists and player2 is empty)
4884
let joined_game = maybe_existing_game.join(context.msg_sender().unwrap());
4985
storage.races.at(game_id).write(joined_game);
5086
}
5187

88+
// Plays a single round by allocating points across 5 tracks
89+
// This is a PRIVATE function - the point allocation remains hidden from the opponent
90+
// Players must play rounds sequentially (round 1, then 2, then 3)
91+
//
92+
// Parameters:
93+
// - track1-5: Points allocated to each track (must sum to less than 10)
94+
// - round: Which round this is (1, 2, or 3)
5295
#[external("private")]
5396
fn play_round(game_id: Field, round: u8, track1: u8, track2: u8, track3: u8, track4: u8, track5: u8) {
97+
// Validate that total points don't exceed 9 (you can't max out all tracks)
5498
assert(track1 + track2 + track3 + track4 + track5 < 10);
5599

56-
storage.progress.at(game_id).insert(GameRoundNote::new(
100+
let player = context.msg_sender().unwrap();
101+
102+
// Store the round choices privately as a note in the player's own storage
103+
// This creates a private commitment that can only be read by the player
104+
storage.progress.at(game_id).at(player).insert(GameRoundNote::new(
57105
track1,
58106
track2,
59107
track3,
60108
track4,
61109
track5,
62110
round,
63-
context.msg_sender().unwrap(),
64-
)).emit(context.msg_sender().unwrap(), MessageDelivery.CONSTRAINED_ONCHAIN);
111+
player,
112+
)).emit(player, MessageDelivery.CONSTRAINED_ONCHAIN);
65113

66-
PodRacing::at(context.this_address()).validate_and_play_round(context.msg_sender().unwrap(), game_id, round).enqueue(
114+
// Enqueue a public function call to update the round counter
115+
// This reveals that a round was played, but not the point allocation
116+
PodRacing::at(context.this_address()).validate_and_play_round(player, game_id, round).enqueue(
67117
&mut context,
68118
);
69119
}
70120

121+
// Internal public function to validate and record that a player completed a round
122+
// Updates the public game state to track which round each player is on
123+
// Does NOT reveal the point allocation (that remains private)
71124
#[external("public")]
72125
#[internal]
73126
fn validate_and_play_round(player: AztecAddress, game_id: Field, round: u8) {
74127
let game_in_progress = storage.races.at(game_id).read();
128+
// Increment the player's round counter (validates sequential play)
75129
storage.races.at(game_id).write(game_in_progress.increment_player_round(player, round));
76130
}
77131

132+
// Called after all rounds are complete to reveal a player's total scores
133+
// This is PRIVATE - only the caller can read their own GameRoundNotes
134+
// The function sums up all round allocations per track and publishes totals
135+
//
136+
// This is the "reveal" phase where private choices become public
78137
#[external("private")]
79-
fn finish_game(player: AztecAddress, game_id: Field, round: u8) {
80-
let totals = storage.progress.at(game_id).get_notes(NoteGetterOptions::new());
138+
fn finish_game(game_id: Field) {
139+
let player = context.msg_sender().unwrap();
81140

141+
// Retrieve all private notes for this player in this game
142+
let totals = storage.progress.at(game_id).at(player).get_notes(NoteGetterOptions::new());
143+
144+
// Sum up points allocated to each track across all rounds
82145
let mut total_track1: u64= 0;
83146
let mut total_track2: u64= 0;
84147
let mut total_track3: u64= 0;
85148
let mut total_track4: u64= 0;
86149
let mut total_track5: u64= 0;
87150

151+
// Iterate through exactly TOTAL_ROUNDS notes (only this player's notes)
88152
for i in 0..TOTAL_ROUNDS {
89153
total_track1 += totals.get(i as u32).note.track1 as u64;
90154
total_track2 += totals.get(i as u32).note.track2 as u64;
@@ -93,8 +157,10 @@ pub contract PodRacing {
93157
total_track5 += totals.get(i as u32).note.track5 as u64;
94158
}
95159

160+
// Enqueue public function to store the revealed totals on-chain
161+
// Now the revealing player's track totals will be publicly visible
96162
PodRacing::at(context.this_address()).validate_finish_game_and_reveal(
97-
context.msg_sender().unwrap(),
163+
player,
98164
game_id,
99165
total_track1,
100166
total_track2,
@@ -106,6 +172,9 @@ pub contract PodRacing {
106172
);
107173
}
108174

175+
// Internal public function to store a player's revealed track totals
176+
// Validates that the player hasn't already revealed their scores (all must be 0)
177+
// After both players call finish_game, all scores are public and can be compared
109178
#[external("public")]
110179
#[internal]
111180
fn validate_finish_game_and_reveal(
@@ -118,20 +187,31 @@ pub contract PodRacing {
118187
total_track5: u64
119188
) {
120189
let game_in_progress = storage.races.at(game_id).read();
121-
190+
191+
// Store the player's track totals (validates they haven't been set yet)
122192
storage.races.at(game_id).write(game_in_progress.set_player_scores(player, total_track1, total_track2, total_track3, total_track4, total_track5));
123193
}
124194

195+
// Determines the winner after both players have revealed their scores
196+
// Can only be called after the game's end_block (time limit expired)
197+
// Compares track totals and declares the player who won more tracks as winner
198+
//
199+
// Winner determination:
200+
// - Compare each of the 5 tracks
201+
// - Player with higher total on a track wins that track
202+
// - Player who wins 3+ tracks wins the game (best of 5)
203+
// - Updates the winner's career win count
125204
#[external("public")]
126205
fn finalize_game(
127206
game_id: Field
128207
) {
129208
let game_in_progress = storage.races.at(game_id).read();
130209

210+
// Calculate winner by comparing track scores (validates game has ended)
131211
let winner = game_in_progress.calculate_winner(context.block_number());
132212

213+
// Update the winner's total win count in the public leaderboard
133214
let previous_wins = storage.win_history.at(winner).read();
134-
135215
storage.win_history.at(winner).write(previous_wins + 1);
136216
}
137217
}

src/race.nr

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,43 @@ use dep::aztec::protocol_types::{
33
traits::{Deserialize, Serialize, Packable},
44
};
55

6+
// Race struct stores the public state of a game
7+
// This data is visible to everyone and tracks game progress
68
#[derive(Deserialize, Serialize, Eq, Packable)]
79
pub struct Race {
10+
// Player addresses
811
pub player1: AztecAddress,
912
pub player2: AztecAddress,
10-
pub total_rounds: u8,
13+
14+
// Game configuration
15+
pub total_rounds: u8, // Always 3 for this game
16+
17+
// Round progress tracking (which round each player is on)
1118
pub player1_round: u8,
1219
pub player2_round: u8,
20+
21+
// Player 1's final revealed track totals (sum of all rounds)
1322
// Nested structs not working on this version
1423
pub player1_track1_final: u64,
1524
pub player1_track2_final: u64,
1625
pub player1_track3_final: u64,
1726
pub player1_track4_final: u64,
1827
pub player1_track5_final: u64,
28+
29+
// Player 2's final revealed track totals (sum of all rounds)
1930
pub player2_track1_final: u64,
2031
pub player2_track2_final: u64,
2132
pub player2_track3_final: u64,
2233
pub player2_track4_final: u64,
2334
pub player2_track5_final: u64,
35+
36+
// Block number when the game expires (for timeout enforcement)
2437
pub end_block: u32,
2538
}
2639

2740
impl Race {
41+
// Creates a new game with player1 set, waiting for player2 to join
42+
// All track scores initialized to 0, rounds start at 0
2843
pub fn new(player1: AztecAddress, total_rounds: u8, end_block: u32) -> Race {
2944
Self {
3045
player1,
@@ -46,8 +61,11 @@ impl Race {
4661
}
4762
}
4863

64+
// Adds player2 to an existing game
65+
// Validates that player1 exists, player2 slot is empty, and player2 is not the same as player1
4966
pub fn join(self, player2: AztecAddress) -> Race {
5067
assert(!self.player1.eq(AztecAddress::zero()) & self.player2.eq(AztecAddress::zero()));
68+
assert(!self.player1.eq(player2));
5169

5270
Self {
5371
player1: self.player1,
@@ -69,10 +87,15 @@ impl Race {
6987
}
7088
}
7189

90+
// Increments a player's round counter after they complete a round
91+
// Validates:
92+
// - Round number is within bounds (1-3)
93+
// - Player is playing rounds sequentially (can't skip rounds)
7294
pub fn increment_player_round(self, player: AztecAddress, round: u8) -> Race {
7395
assert(round < self.total_rounds + 1);
7496

7597
let ret = if player.eq(self.player1) {
98+
// Ensure player1 is playing the next sequential round
7699
assert(round == self.player1_round + 1);
77100

78101
Option::some(Self {
@@ -94,7 +117,7 @@ impl Race {
94117
end_block: self.end_block
95118
})
96119
} else if player.eq(self.player2) {
97-
assert(round == self.player1_round + 1);
120+
assert(round == self.player2_round + 1);
98121

99122
Option::some(Self {
100123
player1: self.player1,
@@ -121,8 +144,12 @@ impl Race {
121144
ret.unwrap()
122145
}
123146

147+
// Sets a player's final track scores after they reveal them
148+
// Validates that scores haven't been set yet (all must be 0)
149+
// This is called from finish_game after summing private round notes
124150
pub fn set_player_scores(self, player: AztecAddress, track1_final: u64, track2_final: u64, track3_final: u64, track4_final: u64, track5_final: u64) -> Race {
125151
let ret = if player.eq(self.player1) {
152+
// Ensure player1 hasn't already revealed (prevents double-reveal exploit)
126153
assert(
127154
self.player1_track1_final +
128155
self.player1_track2_final +
@@ -183,12 +210,24 @@ impl Race {
183210
ret.unwrap()
184211
}
185212

213+
// Determines the game winner by comparing track scores
214+
// Winner is whoever won more tracks (best of 5)
215+
//
216+
// Game rules:
217+
// - Compare player1 vs player2 score on each of the 5 tracks
218+
// - Higher score wins that track
219+
// - Ties go to player2
220+
// - Player who wins 3+ tracks wins the game
221+
//
222+
// Validates that the game has expired (current block > end_block)
186223
pub fn calculate_winner(self, current_block_number: u32) -> AztecAddress {
224+
// Ensure game time limit has passed before declaring winner
187225
assert(current_block_number > self.end_block);
188226

189227
let mut player1_wins = 0;
190228
let mut player2_wins = 0;
191229

230+
// Count how many tracks each player won
192231
if self.player1_track1_final > self.player2_track1_final {
193232
player1_wins = player1_wins + 1;
194233
} else {
@@ -219,6 +258,7 @@ impl Race {
219258
player2_wins = player2_wins + 1;
220259
};
221260

261+
// Return address of player who won more tracks
222262
if (player1_wins > player2_wins) {
223263
self.player1
224264
} else {

0 commit comments

Comments
 (0)