diff --git a/backend/dojo_examples/combat_game/.tool-versions b/backend/dojo_examples/combat_game/.tool-versions index 98d030f..2ae4f76 100644 --- a/backend/dojo_examples/combat_game/.tool-versions +++ b/backend/dojo_examples/combat_game/.tool-versions @@ -1,2 +1,2 @@ -dojo 1.1.1 +dojo 1.2.1 scarb 2.9.2 diff --git a/backend/dojo_examples/combat_game/src/achievements/achievement.cairo b/backend/dojo_examples/combat_game/src/achievements/achievement.cairo index 102501a..1bf9ac9 100644 --- a/backend/dojo_examples/combat_game/src/achievements/achievement.cairo +++ b/backend/dojo_examples/combat_game/src/achievements/achievement.cairo @@ -112,7 +112,8 @@ pub impl AchievementImpl of AchievementTrait { fn tasks(self: Achievement) -> Span { match self { Achievement::None => [].span(), - Achievement::FirstBlood => array![TaskTrait::new('First Blood', 1, "Win a game")].span(), + Achievement::FirstBlood => array![TaskTrait::new('First Blood', 1, "Win a game")] + .span(), Achievement::Warrior => array![TaskTrait::new('Warrior', 5, "Win 5 games")].span(), Achievement::Veteran => array![TaskTrait::new('Veteran', 15, "Win 15 games")].span(), Achievement::Champion => array![TaskTrait::new('Champion', 30, "Win 30 games")].span(), @@ -164,4 +165,4 @@ pub impl IntoU8Achievement of Into { _ => Achievement::None, } } -} \ No newline at end of file +} diff --git a/backend/dojo_examples/combat_game/src/helpers/experience_utils.cairo b/backend/dojo_examples/combat_game/src/helpers/experience_utils.cairo index c15fed1..4cc182c 100644 --- a/backend/dojo_examples/combat_game/src/helpers/experience_utils.cairo +++ b/backend/dojo_examples/combat_game/src/helpers/experience_utils.cairo @@ -2,7 +2,7 @@ pub trait ExperienceCalculator { fn calculate_exp_needed_for_level(level: u8) -> u16; fn should_level_up(current_level: u8, current_exp: u16) -> bool; - + fn remaining_exp_after_level_up(current_level: u8, current_exp: u16) -> u16; } @@ -19,9 +19,7 @@ pub impl ExperienceCalculatorImpl of ExperienceCalculator { fn remaining_exp_after_level_up(current_level: u8, current_exp: u16) -> u16 { if current_exp >= (current_level.into() * current_level.into() * 10_u16) { current_exp - (current_level.into() * current_level.into() * 10_u16) - } - - else { + } else { current_exp } } @@ -29,11 +27,10 @@ pub impl ExperienceCalculatorImpl of ExperienceCalculator { #[cfg(test)] mod test { - use super::ExperienceCalculatorImpl; #[test] - fn test_calculate_exp_needed_for_level(){ + fn test_calculate_exp_needed_for_level() { let level_1: u8 = 1; let level_2: u8 = 2; let level_5: u8 = 5; @@ -46,75 +43,241 @@ mod test { let exp_needed_for_level_10: u16 = 1000; let exp_needed_for_level_20: u16 = 4000; - assert_eq!(ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_1), exp_needed_for_level_1, "Experience needed for level 1 should be 10"); - assert_eq!(ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_2), exp_needed_for_level_2, "Experience needed for level 2 should be 40"); - assert_eq!(ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_5), exp_needed_for_level_5, "Experience needed for level 5 should be 250"); - assert_eq!(ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_10), exp_needed_for_level_10, "Experience needed for level 10 should be 1000"); - assert_eq!(ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_20), exp_needed_for_level_20, "Experience needed for level 10 should be 1000"); + assert_eq!( + ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_1), + exp_needed_for_level_1, + "Experience needed for level 1 should be 10", + ); + assert_eq!( + ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_2), + exp_needed_for_level_2, + "Experience needed for level 2 should be 40", + ); + assert_eq!( + ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_5), + exp_needed_for_level_5, + "Experience needed for level 5 should be 250", + ); + assert_eq!( + ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_10), + exp_needed_for_level_10, + "Experience needed for level 10 should be 1000", + ); + assert_eq!( + ExperienceCalculatorImpl::calculate_exp_needed_for_level(level_20), + exp_needed_for_level_20, + "Experience needed for level 10 should be 1000", + ); } #[test] - fn should_level_up_test(){ + fn should_level_up_test() { let level_1: u8 = 1; let level_2: u8 = 2; let level_5: u8 = 5; let level_10: u8 = 10; let level_20: u8 = 20; + ///For each level instance test there is an instance of the exact exp needed to level up, + ///currentexp greater the exp needed to level up, currentexp is less than what is needed. - ///For each level instance test there is an instance of the exact exp needed to level up, currentexp greater the exp needed to level up, - ///currentexp is less than what is needed. - - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 10), true, "Should pass, exp needed to level up is 10"); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 11), true, "Should pass, exp needed to level up is 10"); - assert_ne!(ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 9), true, "Should pass, exp needed to level up is 10"); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 10), + true, + "Should pass, exp needed to level up is 10", + ); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 11), + true, + "Should pass, exp needed to level up is 10", + ); + assert_ne!( + ExperienceCalculatorImpl::should_level_up(current_level: level_1, current_exp: 9), + true, + "Should pass, exp needed to level up is 10", + ); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 41), true, "Should pass, exp needed to level up is 40"); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 40), true, "Should pass, exp needed to level up is 40"); - assert_ne!(ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 39), true, "Should pass, exp needed to level up is 40"); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 41), + true, + "Should pass, exp needed to level up is 40", + ); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 40), + true, + "Should pass, exp needed to level up is 40", + ); + assert_ne!( + ExperienceCalculatorImpl::should_level_up(current_level: level_2, current_exp: 39), + true, + "Should pass, exp needed to level up is 40", + ); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 250), true, "Should pass, exp needed to level up is 250"); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 251), true, "Should pass, exp needed to level up is 250"); - assert_ne!(ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 249), true, "Should pass, exp needed to level up is 250"); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 250), + true, + "Should pass, exp needed to level up is 250", + ); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 251), + true, + "Should pass, exp needed to level up is 250", + ); + assert_ne!( + ExperienceCalculatorImpl::should_level_up(current_level: level_5, current_exp: 249), + true, + "Should pass, exp needed to level up is 250", + ); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 1000), true, "Should pass, exp needed to level up is 100"); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 1001), true, "Should pass, exp needed to level up is 100"); - assert_ne!(ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 999), true, "Should pass, exp needed to level up is 100"); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 1000), + true, + "Should pass, exp needed to level up is 100", + ); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 1001), + true, + "Should pass, exp needed to level up is 100", + ); + assert_ne!( + ExperienceCalculatorImpl::should_level_up(current_level: level_10, current_exp: 999), + true, + "Should pass, exp needed to level up is 100", + ); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 4000), true, "Should pass, exp needed to level up is 4000"); - assert_eq!(ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 4001), true, "Should pass, exp needed to level up is 4000"); - assert_ne!(ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 3999), true, "Should pass, exp needed to level up is 4000"); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 4000), + true, + "Should pass, exp needed to level up is 4000", + ); + assert_eq!( + ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 4001), + true, + "Should pass, exp needed to level up is 4000", + ); + assert_ne!( + ExperienceCalculatorImpl::should_level_up(current_level: level_20, current_exp: 3999), + true, + "Should pass, exp needed to level up is 4000", + ); } #[test] - fn remaining_exp_after_level_up_test(){ + fn remaining_exp_after_level_up_test() { let level_1: u8 = 1; let level_2: u8 = 2; let level_5: u8 = 5; let level_10: u8 = 10; let level_20: u8 = 20; - + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_1, current_exp: 12, + ), + 2, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_1, current_exp: 17, + ), + 7, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_1, current_exp: 8, + ), + 8, + "The remaining exp should be current_exp - exp_needed", + ); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_1, current_exp: 12), 2, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_1, current_exp: 17), 7, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_1, current_exp:8), 8, "The remaining exp should be current_exp - exp_needed"); - - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_2, current_exp:44), 4, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_2, current_exp:160), 120, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_2, current_exp: 32), 32, "The remaining exp should be current_exp - exp_needed"); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_2, current_exp: 44, + ), + 4, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_2, current_exp: 160, + ), + 120, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_2, current_exp: 32, + ), + 32, + "The remaining exp should be current_exp - exp_needed", + ); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_5, current_exp: 260), 10, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_5, current_exp: 400), 150, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_5, current_exp: 200), 200, "The remaining exp should be current_exp - exp_needed"); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_5, current_exp: 260, + ), + 10, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_5, current_exp: 400, + ), + 150, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_5, current_exp: 200, + ), + 200, + "The remaining exp should be current_exp - exp_needed", + ); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_10, current_exp: 1020), 20, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_10, current_exp: 2000), 1000, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_10, current_exp: 950), 950, "The remaining exp should be current_exp - exp_needed"); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_10, current_exp: 1020, + ), + 20, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_10, current_exp: 2000, + ), + 1000, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_10, current_exp: 950, + ), + 950, + "The remaining exp should be current_exp - exp_needed", + ); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_20, current_exp: 4100), 100, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_20, current_exp: 4700), 700, "The remaining exp should be current_exp - exp_needed"); - assert_eq!(ExperienceCalculatorImpl::remaining_exp_after_level_up(current_level: level_20, current_exp: 3800), 3800, "The remaining exp should be current_exp - exp_needed"); - + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_20, current_exp: 4100, + ), + 100, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_20, current_exp: 4700, + ), + 700, + "The remaining exp should be current_exp - exp_needed", + ); + assert_eq!( + ExperienceCalculatorImpl::remaining_exp_after_level_up( + current_level: level_20, current_exp: 3800, + ), + 3800, + "The remaining exp should be current_exp - exp_needed", + ); } } diff --git a/backend/dojo_examples/combat_game/src/lib.cairo b/backend/dojo_examples/combat_game/src/lib.cairo index 3df95ea..c94b393 100644 --- a/backend/dojo_examples/combat_game/src/lib.cairo +++ b/backend/dojo_examples/combat_game/src/lib.cairo @@ -41,4 +41,6 @@ pub mod achievements { pub mod achievement; } -pub mod tests {} +pub mod tests { + mod test_battle; +} diff --git a/backend/dojo_examples/combat_game/src/models/battle.cairo b/backend/dojo_examples/combat_game/src/models/battle.cairo index 0378e89..fc06e30 100644 --- a/backend/dojo_examples/combat_game/src/models/battle.cairo +++ b/backend/dojo_examples/combat_game/src/models/battle.cairo @@ -2,9 +2,7 @@ use starknet::{ContractAddress, get_block_timestamp}; use core::num::traits::zero::Zero; use combat_game::{ helpers::{pseudo_random::PseudoRandom::generate_random_u8}, - types::{ - battle_status::BattleStatus, - }, + types::{battle_status::BattleStatus}, }; #[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] @@ -19,12 +17,14 @@ pub struct Battle { pub winner_id: ContractAddress, pub battle_type: u8, pub timestamp_start: u64, - pub timestamp_last_action: u64 + pub timestamp_last_action: u64, } #[generate_trait] pub impl BattleImpl of BattleTrait { - fn new(id: u256, player1: ContractAddress, player2: ContractAddress, battle_type: u8) -> Battle { + fn new( + id: u256, player1: ContractAddress, player2: ContractAddress, battle_type: u8, + ) -> Battle { let current_timestamp = get_block_timestamp(); let players = array![player1, player2]; Battle { @@ -39,14 +39,14 @@ pub impl BattleImpl of BattleTrait { .into(), ), status: BattleStatus::Waiting, - winner_id: Zero::zero(), + winner_id: Zero::zero(), battle_type: battle_type, timestamp_start: current_timestamp, - timestamp_last_action: current_timestamp + timestamp_last_action: current_timestamp, } } - fn end(ref self: Battle, winner: ContractAddress ) { + fn end(ref self: Battle, winner: ContractAddress) { self.status = BattleStatus::Finished.into(); self.winner_id = winner; self.timestamp_last_action = get_block_timestamp(); @@ -65,9 +65,14 @@ pub impl BattleImpl of BattleTrait { } fn switch_turn(ref self: Battle) { - self.current_turn = if self.current_turn == self.player1 { self.player2 } else { self.player1 }; + self + .current_turn = + if self.current_turn == self.player1 { + self.player2 + } else { + self.player1 + }; } - } #[cfg(test)] @@ -114,4 +119,3 @@ mod tests { } } - diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index e594ea4..5ef1d04 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -4,7 +4,8 @@ use combat_game::{ constants::SECONDS_PER_DAY, models::{ player::Player, beast::{Beast, BeastTrait}, skill, skill::{Skill, SkillTrait}, - beast_skill::BeastSkill, beast_stats::{BeastStats, BeastStatsActionTrait}, battle::{Battle, BattleTrait}, + beast_skill::BeastSkill, beast_stats::{BeastStats, BeastStatsActionTrait}, + battle::{Battle, BattleTrait}, }, types::{ beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, @@ -20,7 +21,7 @@ struct Store { } #[generate_trait] -impl StoreImpl of StoreTrait { +pub impl StoreImpl of StoreTrait { fn new(world: WorldStorage) -> Store { Store { world: world } } @@ -242,7 +243,9 @@ impl StoreImpl of StoreTrait { fn create_rematch(ref self: Store, battle_id: u256) -> Battle { let battle = self.read_battle(battle_id); - let rematch = BattleTrait::new(battle_id, battle.player1, battle.player2, battle.battle_type); + let rematch = BattleTrait::new( + battle_id, battle.player1, battle.player2, battle.battle_type, + ); self.world.write_model(@rematch); rematch } diff --git a/backend/dojo_examples/combat_game/src/systems/battle.cairo b/backend/dojo_examples/combat_game/src/systems/battle.cairo index 8b13789..2c1178e 100644 --- a/backend/dojo_examples/combat_game/src/systems/battle.cairo +++ b/backend/dojo_examples/combat_game/src/systems/battle.cairo @@ -1 +1,319 @@ +#[starknet::interface] +pub trait IBattle { + fn create_battle(ref self: T, opponent: starknet::ContractAddress, battle_type: u8) -> u256; + fn join_battle(ref self: T, battle_id: u256); + fn attack(ref self: T, battle_id: u256, skill_id: u256) -> (u16, bool, bool); + fn end_battle(ref self: T, battle_id: u256, winner: starknet::ContractAddress); +} + +#[dojo::contract] +pub mod battle_system { + use super::IBattle; + use combat_game::store::{StoreTrait}; + use combat_game::models::{battle::{BattleTrait}, player::{AssertTrait}}; + use combat_game::types::battle_status::BattleStatus; + use combat_game::achievements::achievement::{Achievement, AchievementTrait}; + + use starknet::{get_caller_address, get_block_timestamp, ContractAddress}; + use core::num::traits::zero::Zero; + + use achievement::components::achievable::AchievableComponent; + use achievement::store::{StoreTrait as AchievementStoreTrait, Store as AchievementStore}; + component!(path: AchievableComponent, storage: achievable, event: AchievableEvent); + impl AchievableInternalImpl = AchievableComponent::InternalImpl; + + use dojo::event::EventStorage; + + #[storage] + struct Storage { + #[substorage(v0)] + achievable: AchievableComponent::Storage, + battle_counter: u256, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AchievableEvent: AchievableComponent::Event, + } + + #[derive(Drop, Copy, Serde)] + #[dojo::event] + pub struct BattleCreated { + #[key] + pub battle_id: u256, + #[key] + pub player1: ContractAddress, + pub opponent: ContractAddress, + pub battle_type: u8, + } + + #[derive(Drop, Copy, Serde)] + #[dojo::event] + pub struct BattleJoined { + #[key] + pub battle_id: u256, + #[key] + pub player2: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, Copy, Serde)] + #[dojo::event] + pub struct AttackExecuted { + #[key] + pub battle_id: u256, + #[key] + pub attacker: ContractAddress, + pub attacker_beast_id: u16, + pub defender_beast_id: u16, + pub skill_id: u256, + pub damage_dealt: u16, + } + + #[derive(Drop, Copy, Serde)] + #[dojo::event] + pub struct BattleEnded { + #[key] + pub battle_id: u256, + #[key] + pub winner: ContractAddress, + pub timestamp: u64, + } + + // Constructor + fn dojo_init(ref self: ContractState) { + self.battle_counter.write(1); + } + + #[abi(embed_v0)] + impl BattleImpl of IBattle { + fn create_battle( + ref self: ContractState, opponent: ContractAddress, battle_type: u8, + ) -> u256 { + let mut world = self.world(@"combat_game"); + let mut store = StoreTrait::new(world); + + let caller = get_caller_address(); + + // Validate that caller is not trying to battle themselves + assert!(caller != opponent, "Cannot battle yourself"); + + // Validate that both players exist + let player1 = store.read_player_from_address(caller); + let player2 = store.read_player_from_address(opponent); + player1.assert_exists(); + player2.assert_exists(); + + // Get next battle ID + let battle_id = self.battle_counter.read(); + + // Create battle + let _battle = store.new_battle(battle_id, caller, opponent, battle_type); + + // Increment battle counter + self.battle_counter.write(battle_id + 1); + + // Emit event + world.emit_event(@BattleCreated { battle_id, player1: caller, opponent, battle_type }); + + battle_id + } + + // Method to join a battle + fn join_battle(ref self: ContractState, battle_id: u256) { + let mut world = self.world(@"combat_game"); + let mut store = StoreTrait::new(world); + + let caller = get_caller_address(); + + // Read the battle + let mut battle = store.read_battle(battle_id); + + // Validate battle is in waiting status + assert!(battle.status == BattleStatus::Waiting, "Battle is not waiting for players"); + + // Validate caller is one of the battle participants + assert( + caller == battle.player1 || caller == battle.player2, 'Not a battle participant', + ); + + // Validate both players have beasts + let player1 = store.read_player_from_address(battle.player1); + let player2 = store.read_player_from_address(battle.player2); + assert!(player1.current_beast_id != Zero::zero(), "Player 1 has no beast selected"); + assert!(player2.current_beast_id != Zero::zero(), "Player 2 has no beast selected"); + + // Update battle status to active + battle.status = BattleStatus::Active; + battle.update_timestamp(); + + // Save updated battle + store.write_battle(battle); + + // Emit event + world + .emit_event( + @BattleJoined { battle_id, player2: caller, timestamp: get_block_timestamp() }, + ); + } + + fn attack(ref self: ContractState, battle_id: u256, skill_id: u256) -> (u16, bool, bool) { + let mut world = self.world(@"combat_game"); + let mut store = StoreTrait::new(world); + let achievement_store = AchievementStoreTrait::new(world); + + let caller = get_caller_address(); + + // Read the battle + let mut battle = store.read_battle(battle_id); + + // Validate battle is active + assert!(battle.is_active(), "Battle is not active"); + + // Validate it's the caller's turn + assert!(battle.current_turn == caller, "It's not your turn"); + + // Validate caller is a participant + assert(caller == battle.player1 || caller == battle.player2, 'Non battle participant'); + + // Get attacker and defender + let mut attacker: ContractAddress = Zero::zero(); + let mut defender: ContractAddress = Zero::zero(); + if battle.player1 == caller { + attacker = battle.player1; + defender = battle.player2; + } else { + attacker = battle.player2; + defender = battle.player1; + } + + // Get current beast IDs + let attacker_beast_id = store.read_player_from_address(attacker).current_beast_id; + let defender_beast_id = store.read_player_from_address(defender).current_beast_id; + + // Process attack + let (damage_dealt, is_favored, is_effective) = store + .process_attack(battle_id, attacker_beast_id, defender_beast_id, skill_id); + + // Update battle timestamp and switch turns + battle.update_timestamp(); + battle.switch_turn(); + + // Save the updated battle + store.write_battle(battle); + + // Emit attack event + world + .emit_event( + @AttackExecuted { + battle_id, + attacker: caller, + attacker_beast_id, + defender_beast_id, + skill_id, + damage_dealt, + }, + ); + + // Check if battle ended + let mut updated_battle = store.read_battle(battle_id); + if updated_battle.is_finished() { + // Progress battle victory achievements for the winner + let winner_player = store.read_player_from_address(updated_battle.winner_id); + + // Progress through battle achievement tiers based on wins + let wins = winner_player.battles_won + 1; + progress_achievements(wins, achievement_store, winner_player.address); + + // Emit battle ended event + world + .emit_event( + @BattleEnded { + battle_id, + winner: updated_battle.winner_id, + timestamp: get_block_timestamp(), + }, + ); + } + + (damage_dealt, is_favored, is_effective) + } + + fn end_battle(ref self: ContractState, battle_id: u256, winner: ContractAddress) { + let mut world = self.world(@"combat_game"); + let mut store = StoreTrait::new(world); + let achievement_store = AchievementStoreTrait::new(world); + + let caller = get_caller_address(); + + // Read the battle + let mut battle = store.read_battle(battle_id); + + // Validate battle is not already finished + assert!(!battle.is_finished(), "Battle is already finished"); + + // Validate caller is a participant + assert!( + caller == battle.player1 || caller == battle.player2, + "You are not a participant in this battle", + ); + + // Validate winner is a participant + assert!( + winner == battle.player1 || winner == battle.player2, + "Winner must be a battle participant", + ); + + // End the battle + battle.end(winner); + + // Update player battle results + store.update_player_battle_result(won: true); + + // Save the updated battle + store.write_battle(battle); + + // Progress battle victory achievements for the winner + let winner_player = store.read_player_from_address(winner); + let wins = winner_player.battles_won + 1; + + progress_achievements(wins, achievement_store, winner_player.address); + + // Emit event + world.emit_event(@BattleEnded { battle_id, winner, timestamp: get_block_timestamp() }); + } + } + + fn progress_achievements( + wins: u16, achievement_store: AchievementStore, winner: ContractAddress, + ) { + if wins >= 1 { + let achievement: Achievement = Achievement::FirstBlood; + achievement_store + .progress(winner.into(), achievement.identifier(), 1, get_block_timestamp()); + } + if wins >= 5 { + let achievement: Achievement = Achievement::Warrior; + achievement_store + .progress(winner.into(), achievement.identifier(), 1, get_block_timestamp()); + } + if wins >= 15 { + let achievement: Achievement = Achievement::Veteran; + achievement_store + .progress(winner.into(), achievement.identifier(), 1, get_block_timestamp()); + } + if wins >= 30 { + let achievement: Achievement = Achievement::Champion; + achievement_store + .progress(winner.into(), achievement.identifier(), 1, get_block_timestamp()); + } + if wins >= 50 { + let achievement: Achievement = Achievement::Legend; + achievement_store + .progress(winner.into(), achievement.identifier(), 1, get_block_timestamp()); + } + } +} diff --git a/backend/dojo_examples/combat_game/src/tests/test_battle.cairo b/backend/dojo_examples/combat_game/src/tests/test_battle.cairo index 8b13789..1375d7d 100644 --- a/backend/dojo_examples/combat_game/src/tests/test_battle.cairo +++ b/backend/dojo_examples/combat_game/src/tests/test_battle.cairo @@ -1 +1,472 @@ +#[cfg(test)] +mod battle_system { + use dojo_cairo_test::{ + ContractDef, ContractDefTrait, NamespaceDef, TestResource, WorldStorageTestTrait, + spawn_test_world, + }; + use dojo::world::{WorldStorage, WorldStorageTrait}; + use dojo::model::{ModelStorage}; + use combat_game::systems::battle::{battle_system, IBattleDispatcher, IBattleDispatcherTrait}; + use combat_game::models::{ + player::{Player, m_Player}, battle::{Battle, m_Battle, BattleTrait}, beast::{m_Beast}, + beast_stats::{m_BeastStats}, beast_skill::{m_BeastSkill}, skill::{m_Skill}, skill, + }; + use combat_game::types::{ + battle_status::BattleStatus, beast_type::BeastType, status_condition::StatusCondition, + }; + use combat_game::store::{StoreTrait}; + use starknet::{contract_address_const, ContractAddress}; + use starknet::{testing}; + use core::num::traits::zero::Zero; + + // Constants for testing + const BATTLE_TYPE: u8 = 0; + const BEAST_ID_1: u16 = 1; + const BEAST_ID_2: u16 = 2; + + fn PLAYER1() -> ContractAddress { + contract_address_const::<0x1234>() + } + fn PLAYER2() -> ContractAddress { + contract_address_const::<0x5678>() + } + + pub fn namespace_def() -> NamespaceDef { + let ndef = NamespaceDef { + namespace: "combat_game", + resources: [ + TestResource::Model(m_Player::TEST_CLASS_HASH), + TestResource::Model(m_Battle::TEST_CLASS_HASH), + TestResource::Model(m_Beast::TEST_CLASS_HASH), + TestResource::Model(m_BeastStats::TEST_CLASS_HASH), + TestResource::Model(m_BeastSkill::TEST_CLASS_HASH), + TestResource::Model(m_Skill::TEST_CLASS_HASH), + TestResource::Event(battle_system::e_BattleCreated::TEST_CLASS_HASH), + TestResource::Event(battle_system::e_BattleJoined::TEST_CLASS_HASH), + TestResource::Event(battle_system::e_AttackExecuted::TEST_CLASS_HASH), + TestResource::Event(battle_system::e_BattleEnded::TEST_CLASS_HASH), + TestResource::Event(achievement::events::index::e_TrophyCreation::TEST_CLASS_HASH), + TestResource::Event( + achievement::events::index::e_TrophyProgression::TEST_CLASS_HASH, + ), + TestResource::Contract(battle_system::TEST_CLASS_HASH), + ] + .span(), + }; + + ndef + } + + pub fn contract_defs() -> Span { + [ + ContractDefTrait::new(@"combat_game", @"battle_system") + .with_writer_of([dojo::utils::bytearray_hash(@"combat_game")].span()), + ] + .span() + } + + pub fn setup() -> (WorldStorage, IBattleDispatcher) { + let ndef = namespace_def(); + let mut world: WorldStorage = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + + let (contract_address, _) = world.dns(@"battle_system").unwrap(); + let battle_system = IBattleDispatcher { contract_address }; + + (world, battle_system) + } + + fn setup_players_and_beasts(mut world: WorldStorage) { + let mut store = StoreTrait::new(world); + + // Create player 1 + testing::set_caller_address(PLAYER1()); + let mut player1 = store.new_player(); + player1.current_beast_id = BEAST_ID_1.into(); + store.write_player(player1); + + // Create beast for player 1 + let _beast1 = store.new_beast(BEAST_ID_1, BeastType::Light); + store.init_beast_skills(BEAST_ID_1); + let _beast1_stats = store + .new_beast_stats(BEAST_ID_1, 100, 100, 50, 30, 40, 90, 80, StatusCondition::None); + + // Create player 2 + testing::set_caller_address(PLAYER2()); + let mut player2 = store.new_player(); + player2.current_beast_id = BEAST_ID_2.into(); + store.write_player(player2); + + // Create beast for player 2 + let _beast2 = store.new_beast(BEAST_ID_2, BeastType::Shadow); + store.init_beast_skills(BEAST_ID_2); + let _beast2_stats = store + .new_beast_stats(BEAST_ID_2, 100, 100, 45, 35, 35, 85, 75, StatusCondition::None); + } + + fn setup_active_battle(mut world: WorldStorage, battle_system: IBattleDispatcher) -> u256 { + // Setup players and beasts + setup_players_and_beasts(world); + + // Create battle + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + // Join battle to make it active + testing::set_contract_address(PLAYER2()); + battle_system.join_battle(battle_id); + + battle_id + } + + fn setup_players_without_beasts(mut world: WorldStorage) { + let mut store = StoreTrait::new(world); + + // Create player 1 + testing::set_caller_address(PLAYER1()); + store.new_player(); + + // Create player 2 + testing::set_caller_address(PLAYER2()); + store.new_player(); + } + + fn get_player_skill(player: ContractAddress, current_player: bool) -> u256 { + if player == PLAYER1() && current_player { + skill::SLASH_SKILL_ID + } else { + skill::SMASH_SKILL_ID + } + } + + #[test] + fn test_create_battle_success() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup players and beasts + setup_players_and_beasts(world); + + // Set the caller address for the test + testing::set_contract_address(PLAYER1()); + testing::set_block_timestamp(1000); + + // Create a battle + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + // Verify the battle was created correctly + let battle: Battle = world.read_model((battle_id)); + + assert(battle.id == battle_id, 'Battle ID should match'); + assert(battle.player1 == PLAYER1(), 'Player 1 should match'); + assert(battle.player2 == PLAYER2(), 'Player 2 should match'); + assert(battle.status == BattleStatus::Waiting, 'Battle should be waiting'); + assert(battle.battle_type == BATTLE_TYPE, 'Battle type should match'); + assert(battle.winner_id == Zero::zero(), 'Winner should be unset'); + assert(battle.timestamp_start > 0, 'Start timestamp should be set'); + } + + #[test] + #[should_panic(expected: ("Cannot battle yourself", 'ENTRYPOINT_FAILED'))] + fn test_create_battle_self_battle() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup players + setup_players_and_beasts(world); + + // Set the caller address + testing::set_contract_address(PLAYER1()); + + // Try to create a battle against yourself + battle_system.create_battle(PLAYER1(), BATTLE_TYPE); + } + + #[test] + fn test_join_battle_success() { + // Create test environment + let (mut world, battle_system) = setup(); + + testing::set_block_timestamp(50); + + // Setup players and beasts + setup_players_and_beasts(world); + + // Create a battle as player 1 + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + testing::set_block_timestamp(100); + + // Join the battle as player 2 + testing::set_contract_address(PLAYER2()); + battle_system.join_battle(battle_id); + + // Verify the battle is now active + let battle: Battle = world.read_model((battle_id)); + assert(battle.status == BattleStatus::Active, 'Battle should be active'); + assert( + battle.timestamp_last_action > battle.timestamp_start, 'Last action should be updated', + ); + } + + #[test] + #[should_panic(expected: ("Player 1 has no beast selected", 'ENTRYPOINT_FAILED'))] + fn test_join_battle_without_beast() { + // Create test environment + let (mut world, battle_system) = setup(); + + testing::set_block_timestamp(50); + + // Setup players without beasts + setup_players_without_beasts(world); + + // Create a battle as player 1 + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + testing::set_block_timestamp(100); + + // Join the battle as player 2 + testing::set_contract_address(PLAYER2()); + battle_system.join_battle(battle_id); + + // Verify the battle is now active + let battle: Battle = world.read_model((battle_id)); + assert(battle.status == BattleStatus::Active, 'Battle should be active'); + assert( + battle.timestamp_last_action > battle.timestamp_start, 'Last action should be updated', + ); + } + + #[test] + #[should_panic(expected: ("Battle is not waiting for players", 'ENTRYPOINT_FAILED'))] + fn test_join_battle_when_active() { + // Create test environment + let (mut world, battle_system) = setup(); + + setup_active_battle(world, battle_system); + + testing::set_contract_address(PLAYER1()); + battle_system.join_battle(1); + } + + #[test] + #[should_panic(expected: ('Not a battle participant', 'ENTRYPOINT_FAILED'))] + fn test_join_battle_non_participant() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup players + setup_players_and_beasts(world); + + // Create a battle between player 1 and player 2 + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + // A different player try to join + let other_player = contract_address_const::<0x333>(); + testing::set_contract_address(other_player); + battle_system.join_battle(battle_id); + } + + #[test] + #[should_panic(expected: ("Player 1 has no beast selected", 'ENTRYPOINT_FAILED'))] + fn test_join_battle_no_beast_fails() { + // Create test environment + let (mut world, battle_system) = setup(); + let mut store = StoreTrait::new(world); + + // Setup players without beasts + testing::set_caller_address(PLAYER1()); + store.new_player(); + testing::set_caller_address(PLAYER2()); + store.new_player(); + + // Create a battle + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + // Try to join without having beasts + testing::set_contract_address(PLAYER2()); + battle_system.join_battle(battle_id); + } + + #[test] + fn test_attack_success() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup players, beasts, and active battle + let battle_id = setup_active_battle(world, battle_system); + + // Get the battle to see whose turn it is + let battle: Battle = world.read_model((battle_id)); + let current_player = battle.current_turn; + + // Get the player's skill + let skill = get_player_skill(current_player, true); + + // Set caller to current turn player + testing::set_contract_address(current_player); + + // Execute an attack + let (damage, _, _) = battle_system.attack(battle_id, skill); + + // Verify attack results + assert(damage > 0, 'Damage should be dealt'); + + // Verify battle state was updated + let updated_battle: Battle = world.read_model((battle_id)); + assert(updated_battle.current_turn != current_player, 'Turn should switch'); + } + + #[test] + #[should_panic(expected: ("It's not your turn", 'ENTRYPOINT_FAILED'))] + fn test_attack_wrong_turn() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup active battle + let battle_id = setup_active_battle(world, battle_system); + + // Get the battle to see whose turn it is + let battle: Battle = world.read_model((battle_id)); + let current_player = battle.current_turn; + let other_player = if current_player == PLAYER1() { + PLAYER2() + } else { + PLAYER1() + }; + + // Try to attack when it's not your turn + testing::set_contract_address(other_player); + let skill = get_player_skill(other_player, false); + battle_system.attack(battle_id, skill); + } + + #[test] + fn test_end_battle_success() { + // Create test environment + let (mut world, battle_system) = setup(); + + testing::set_block_timestamp(1000); + + // Setup active battle + let battle_id = setup_active_battle(world, battle_system); + + // End the battle manually as player 1 + testing::set_contract_address(PLAYER1()); + battle_system.end_battle(battle_id, PLAYER1()); + + // Verify battle is finished + let battle: Battle = world.read_model((battle_id)); + assert(battle.status == BattleStatus::Finished, 'Battle should be finished'); + assert(battle.winner_id == PLAYER1(), 'Winner should be set'); + + // Verify player stats were updated + let winner_player: Player = world.read_model((PLAYER1())); + assert(winner_player.battles_won > 0, 'Winner should have battle won'); + } + + #[test] + #[should_panic(expected: ("Winner must be a battle participant", 'ENTRYPOINT_FAILED'))] + fn test_end_battle_invalid_participant() { + // Create test environment + let (mut world, battle_system) = setup(); + + // Setup active battle + let battle_id = setup_active_battle(world, battle_system); + + // Try to end battle with invalid participant + testing::set_contract_address(PLAYER1()); + let invalid_participant = contract_address_const::<0x999>(); + battle_system.end_battle(battle_id, invalid_participant); + } + + #[test] + fn test_multiple_battles() { + // Create test environment + let (mut world, battle_system) = setup(); + + testing::set_block_timestamp(1000); + + // Setup players + setup_players_and_beasts(world); + + // Create multiple battles + testing::set_contract_address(PLAYER1()); + let battle_id_1 = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + let battle_id_2 = battle_system.create_battle(PLAYER2(), 1); + + // Verify battles have different IDs + assert(battle_id_1 != battle_id_2, 'Battle IDs should be different'); + + // Verify both battles exist + let battle_1: Battle = world.read_model((battle_id_1)); + let battle_2: Battle = world.read_model((battle_id_2)); + + assert(battle_1.id == battle_id_1, 'Battle 1 ID should match'); + assert(battle_2.id == battle_id_2, 'Battle 2 ID should match'); + assert(battle_1.battle_type == BATTLE_TYPE, 'Battle 1 type should match'); + assert!(battle_2.battle_type == 1, "Battle 2 type should be different"); + } + + #[test] + fn test_complete_battle_flow() { + // Create test environment + let (mut world, battle_system) = setup(); + + testing::set_block_timestamp(1000); + + // Setup players and beasts + setup_players_and_beasts(world); + + // Create battle + testing::set_contract_address(PLAYER1()); + let battle_id = battle_system.create_battle(PLAYER2(), BATTLE_TYPE); + + // Verify initial state + let battle: Battle = world.read_model((battle_id)); + assert(battle.status == BattleStatus::Waiting, 'Should start as waiting'); + + // Join battle + testing::set_contract_address(PLAYER2()); + battle_system.join_battle(battle_id); + + // Verify active state + let mut battle: Battle = world.read_model((battle_id)); + assert(battle.status == BattleStatus::Active, 'Should be active after join'); + + // Execute some attacks + let mut turn_count: u8 = 0; + while turn_count < 3 { // Simulate a few turns + let mut battle: Battle = world.read_model((battle_id)); + if battle.is_finished() { + break; + } + + let current_player = battle.current_turn; + testing::set_contract_address(current_player); + + let skill = get_player_skill(current_player, true); + let (damage, _, _) = battle_system.attack(battle_id, skill); + + assert(damage > 0, 'Should deal damage'); + turn_count += 1; + }; + + // End battle manually if not finished + let mut battle: Battle = world.read_model((battle_id)); + if !battle.is_finished() { + testing::set_contract_address(PLAYER1()); + battle_system.end_battle(battle_id, PLAYER1()); + } + + // Verify final state + let final_battle: Battle = world.read_model((battle_id)); + assert(final_battle.status == BattleStatus::Finished, 'Should be finished'); + assert(!final_battle.winner_id.is_zero(), 'Should have a winner'); + } +}