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 ;
218mod game_round_note ;
319mod 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}
0 commit comments