diff --git a/contracts/quest_chain/src/lib.rs b/contracts/quest_chain/src/lib.rs index d937bd1..8411bee 100644 --- a/contracts/quest_chain/src/lib.rs +++ b/contracts/quest_chain/src/lib.rs @@ -42,6 +42,7 @@ pub struct QuestChain { pub total_reward: i128, pub start_time: Option, // None = no time limit pub end_time: Option, // None = no time limit + pub max_participants: Option, // None = no participant limit pub created_at: u64, pub active: bool, } @@ -94,6 +95,7 @@ pub enum DataKey { PlayerProgress(Address, u32), // PlayerProgress - (player, chain_id) CompletionLeaderboard(u32), // Vec - sorted by duration (fastest first) ChainCompletions(u32), // u32 - total completions for chain + ChainParticipants(u32), // u32 - current participant count for chain RewardPool(u32), // i128 - reward pool for chain (if using token rewards) PendingRewards(Address, u32), // i128 - pending rewards for player in chain } @@ -170,6 +172,7 @@ impl QuestChainContract { /// * `quests` - Vector of quests in the chain /// * `start_time` - Optional start time (None for no time limit) /// * `end_time` - Optional end time (None for no time limit) + /// * `max_participants` - Optional maximum participants (None for no limit) pub fn create_chain( env: Env, admin: Address, @@ -178,6 +181,7 @@ impl QuestChainContract { quests: Vec, start_time: Option, end_time: Option, + max_participants: Option, ) -> u32 { admin.require_auth(); Self::assert_admin(&env, &admin); @@ -221,6 +225,7 @@ impl QuestChainContract { total_reward, start_time, end_time, + max_participants, created_at: env.ledger().timestamp(), active: true, }; @@ -230,6 +235,9 @@ impl QuestChainContract { env.storage() .persistent() .set(&DataKey::ChainCompletions(counter), &0u32); + env.storage() + .persistent() + .set(&DataKey::ChainParticipants(counter), &0u32); // Initialize empty leaderboard let leaderboard: Vec = Vec::new(&env); @@ -278,6 +286,18 @@ impl QuestChainContract { } } + // Check participant limit + if let Some(max) = chain.max_participants { + let current_participants: u32 = env + .storage() + .persistent() + .get(&DataKey::ChainParticipants(chain_id)) + .unwrap_or(0); + if current_participants >= max { + panic!("Participant limit reached"); + } + } + // Check if player already has progress if env .storage() @@ -303,6 +323,17 @@ impl QuestChainContract { env.storage() .persistent() .set(&DataKey::PlayerProgress(player.clone(), chain_id), &progress); + + // Increment participant count + let mut participant_count: u32 = env + .storage() + .persistent() + .get(&DataKey::ChainParticipants(chain_id)) + .unwrap_or(0); + participant_count += 1; + env.storage() + .persistent() + .set(&DataKey::ChainParticipants(chain_id), &participant_count); } /// Complete a quest in a chain @@ -528,6 +559,19 @@ impl QuestChainContract { .persistent() .remove(&DataKey::PlayerProgress(player.clone(), chain_id)); + // Decrement participant count + let mut participant_count: u32 = env + .storage() + .persistent() + .get(&DataKey::ChainParticipants(chain_id)) + .unwrap_or(0); + if participant_count > 0 { + participant_count -= 1; + env.storage() + .persistent() + .set(&DataKey::ChainParticipants(chain_id), &participant_count); + } + // Clear pending rewards if any if env .storage() @@ -589,6 +633,14 @@ impl QuestChainContract { .unwrap_or(0) } + /// Get current participant count for a chain + pub fn get_chain_participants(env: Env, chain_id: u32) -> u32 { + env.storage() + .persistent() + .get(&DataKey::ChainParticipants(chain_id)) + .unwrap_or(0) + } + /// Get configuration pub fn get_config(env: Env) -> ChainConfig { env.storage().persistent().get(&DataKey::Config).unwrap() diff --git a/contracts/quest_chain/src/test.rs b/contracts/quest_chain/src/test.rs index ca90091..3f4e786 100644 --- a/contracts/quest_chain/src/test.rs +++ b/contracts/quest_chain/src/test.rs @@ -138,6 +138,7 @@ fn test_create_chain() { &quests, &None, &None, + &None, ); assert_eq!(chain_id, 1); @@ -169,6 +170,7 @@ fn test_create_time_limited_chain() { &quests, &start_time, &end_time, + &None, ); let chain = client.get_chain(&chain_id); @@ -192,6 +194,7 @@ fn test_create_chain_too_few_quests() { &empty_quests, &None, &None, + &None, ); } @@ -211,6 +214,7 @@ fn test_start_chain() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -241,6 +245,7 @@ fn test_start_chain_twice() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -265,6 +270,7 @@ fn test_start_chain_before_start_time() { &quests, &Some(2000u64), &None, + &None, ); let player = Address::generate(&env); @@ -288,6 +294,7 @@ fn test_start_chain_after_end_time() { &quests, &Some(1000u64), &Some(2000u64), + &None, ); let player = Address::generate(&env); @@ -310,6 +317,7 @@ fn test_sequential_quest_completion() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -347,6 +355,7 @@ fn test_complete_quest_without_prerequisites() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -372,6 +381,7 @@ fn test_branching_paths() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -409,6 +419,7 @@ fn test_progress_checkpointing() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -446,6 +457,7 @@ fn test_reset_to_checkpoint() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -489,6 +501,7 @@ fn test_reset_to_checkpoint_no_checkpoint() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -514,6 +527,7 @@ fn test_reset_chain() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -544,6 +558,7 @@ fn test_chain_completion() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -580,6 +595,7 @@ fn test_cumulative_rewards() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -625,6 +641,7 @@ fn test_leaderboard() { &quests, &None, &None, + &None, ); // Player 1 completes quickly @@ -682,6 +699,7 @@ fn test_multiple_players_same_chain() { &quests, &None, &None, + &None, ); let player1 = Address::generate(&env); @@ -726,6 +744,7 @@ fn test_admin_functions() { &quests, &None, &None, + &None, ); client.set_chain_active(&admin, &chain_id, &false); @@ -762,6 +781,7 @@ fn test_complete_quest_twice() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -787,6 +807,7 @@ fn test_complete_unlocked_quest() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -829,6 +850,7 @@ fn test_pending_rewards_tracking() { &quests, &None, &None, + &None, ); let player = Address::generate(&env); @@ -846,3 +868,221 @@ fn test_pending_rewards_tracking() { let pending = client.get_pending_rewards(&player, &chain_id); assert_eq!(pending, 250); // Quest 1 + Quest 2 rewards } + +#[test] +fn test_create_chain_with_participant_limit() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + let max_participants = 5u32; + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Limited Chain"), + &Symbol::new(&env, "A chain with participant limit"), + &quests, + &None, + &None, + &Some(max_participants), + ); + + let chain = client.get_chain(&chain_id); + assert_eq!(chain.max_participants, Some(max_participants)); + + // Initial participant count should be 0 + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 0); +} + +#[test] +fn test_start_chain_within_limit() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + let max_participants = 3u32; + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Limited Chain"), + &Symbol::new(&env, "A chain with participant limit"), + &quests, + &None, + &None, + &Some(max_participants), + ); + + // Add 3 players (at the limit) + for i in 0..3 { + let player = Address::generate(&env); + client.start_chain(&player, &chain_id); + } + + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 3); +} + +#[test] +#[should_panic(expected = "Participant limit reached")] +fn test_start_chain_exceeds_limit() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + let max_participants = 2u32; + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Limited Chain"), + &Symbol::new(&env, "A chain with participant limit"), + &quests, + &None, + &None, + &Some(max_participants), + ); + + // Add 2 players (at the limit) + for _ in 0..2 { + let player = Address::generate(&env); + client.start_chain(&player, &chain_id); + } + + // Try to add a 3rd player (should panic) + let extra_player = Address::generate(&env); + client.start_chain(&extra_player, &chain_id); +} + +#[test] +fn test_chain_without_participant_limit() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + // Create chain with no participant limit + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Unlimited Chain"), + &Symbol::new(&env, "A chain without participant limit"), + &quests, + &None, + &None, + &None, // No participant limit + ); + + let chain = client.get_chain(&chain_id); + assert_eq!(chain.max_participants, None); + + // Should be able to add many players without limit + for _ in 0..10 { + let player = Address::generate(&env); + client.start_chain(&player, &chain_id); + } + + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 10); +} + +#[test] +fn test_reset_chain_decrements_participant_count() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + let max_participants = 5u32; + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Limited Chain"), + &Symbol::new(&env, "A chain with participant limit"), + &quests, + &None, + &None, + &Some(max_participants), + ); + + // Add 3 players + let player1 = Address::generate(&env); + let player2 = Address::generate(&env); + let player3 = Address::generate(&env); + + client.start_chain(&player1, &chain_id); + client.start_chain(&player2, &chain_id); + client.start_chain(&player3, &chain_id); + + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 3); + + // Reset player2's progress + client.reset_chain(&player2, &chain_id); + + // Participant count should decrement + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 2); + + // Should be able to add a new player now + let player4 = Address::generate(&env); + client.start_chain(&player4, &chain_id); + + let participant_count = client.get_chain_participants(&chain_id); + assert_eq!(participant_count, 3); +} + +#[test] +fn test_participant_count_accuracy() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + + let (client, admin) = setup_contract(&env); + let quests = create_test_quests(&env); + + let chain_id = client.create_chain( + &admin, + &Symbol::new(&env, "Test Chain"), + &Symbol::new(&env, "A test quest chain"), + &quests, + &None, + &None, + &None, + ); + + // Initial count + assert_eq!(client.get_chain_participants(&chain_id), 0); + + // Add first player + let player1 = Address::generate(&env); + client.start_chain(&player1, &chain_id); + assert_eq!(client.get_chain_participants(&chain_id), 1); + + // Add second player + let player2 = Address::generate(&env); + client.start_chain(&player2, &chain_id); + assert_eq!(client.get_chain_participants(&chain_id), 2); + + // Try to add same player again (should fail) + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.start_chain(&player1, &chain_id); + })); + assert!(result.is_err()); + assert_eq!(client.get_chain_participants(&chain_id), 2); // Count unchanged + + // Remove one player + client.reset_chain(&player1, &chain_id); + assert_eq!(client.get_chain_participants(&chain_id), 1); + + // Remove another player + client.reset_chain(&player2, &chain_id); + assert_eq!(client.get_chain_participants(&chain_id), 0); +}