From 92740cce76b77f0a2ddd3318b86df1bd6f651c3a Mon Sep 17 00:00:00 2001 From: Credence Date: Sat, 14 Sep 2024 02:09:14 +0100 Subject: [PATCH 01/12] contracts: battle force start --- client/src/dojo/createSystemCalls.ts | 5 ++++ contracts/src/systems/combat/contracts.cairo | 29 ++++++++++++++++++++ sdk/packages/eternum/src/provider/index.ts | 9 ++++++ sdk/packages/eternum/src/types/provider.ts | 5 ++++ 4 files changed, 48 insertions(+) diff --git a/client/src/dojo/createSystemCalls.ts b/client/src/dojo/createSystemCalls.ts index 413dc6bfe..febe84818 100644 --- a/client/src/dojo/createSystemCalls.ts +++ b/client/src/dojo/createSystemCalls.ts @@ -264,6 +264,10 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { await provider.battle_start(props); }; + const battle_force_start = async (props: SystemProps.BattleForceStartProps) => { + await provider.battle_force_start(props); + }; + const battle_leave = async (props: SystemProps.BattleLeaveProps) => { await provider.battle_leave(props); }; @@ -348,6 +352,7 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { remove_player_from_whitelist: withQueueing(withErrorHandling(remove_player_from_whitelist)), battle_start: withQueueing(withErrorHandling(battle_start)), + battle_force_start: withQueueing(withErrorHandling(battle_force_start)), battle_leave: withQueueing(withErrorHandling(battle_leave)), battle_join: withQueueing(withErrorHandling(battle_join)), battle_claim: withQueueing(withErrorHandling(battle_claim)), diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index dc2a3b558..a1b2d3a53 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -170,6 +170,20 @@ trait ICombatContract { /// * None fn battle_start(ref world: IWorldDispatcher, attacking_army_id: ID, defending_army_id: ID) -> ID; + /// Force start a battle between two armies + /// + /// # Preconditions: + /// - The caller must own the `defending_army_id`. + /// - the army must be on the defensive side + /// - The battle must not have already started + /// + /// # Arguments: + /// * `world` - The game world dispatcher interface. + /// * `battle_id` - The id of the battle to force start. + /// * `defending_army_id` - The id of the defending army. + /// + fn battle_force_start(ref world: IWorldDispatcher, battle_id: ID, defending_army_id: ID); + /// Join an existing battle with the specified army, assigning it to a specific side in the /// battle. /// @@ -732,6 +746,21 @@ mod combat_systems { battle_id } + fn battle_force_start(ref world: IWorldDispatcher, battle_id: ID, defending_army_id: ID) { + get!(world, defending_army_id, EntityOwner).assert_caller_owner(world); + + let mut defending_army: Army = get!(world, defending_army_id, Army); + assert!(defending_army.battle_id == battle_id, "army is not in battle"); + assert!(defending_army.battle_side == BattleSide::Defence, "army is not on defensive"); + + let now = starknet::get_block_timestamp(); + let mut battle: Battle = get!(world, battle_id, Battle); + assert!(now < battle.start_at, "Battle already started"); + + // update battle + battle.start_at = now; + set!(world, (battle)); + } fn battle_join(ref world: IWorldDispatcher, battle_id: ID, battle_side: BattleSide, army_id: ID) { assert!(battle_side != BattleSide::None, "choose correct battle side"); diff --git a/sdk/packages/eternum/src/provider/index.ts b/sdk/packages/eternum/src/provider/index.ts index 0d97a84bf..2a5fde248 100644 --- a/sdk/packages/eternum/src/provider/index.ts +++ b/sdk/packages/eternum/src/provider/index.ts @@ -520,6 +520,15 @@ export class EternumProvider extends EnhancedDojoProvider { }); } + public async battle_force_start(props: SystemProps.BattleForceStartProps) { + const { battle_id, defending_army_id, signer } = props; + + return await this.executeAndCheckTransaction(signer, { + contractAddress: getContractByName(this.manifest, `${NAMESPACE}-combat_systems`), + entrypoint: "battle_force_start", + calldata: [battle_id, defending_army_id], + }); + } public async battle_join(props: SystemProps.BattleJoinProps) { const { battle_id, battle_side, army_id, signer } = props; diff --git a/sdk/packages/eternum/src/types/provider.ts b/sdk/packages/eternum/src/types/provider.ts index 4d45131ab..9cf22d68b 100644 --- a/sdk/packages/eternum/src/types/provider.ts +++ b/sdk/packages/eternum/src/types/provider.ts @@ -297,6 +297,11 @@ export interface BattleStartProps extends SystemSigner { defending_army_id: num.BigNumberish; } +export interface BattleForceStartProps extends SystemSigner { + battle_id: num.BigNumberish; + defending_army_id: num.BigNumberish; +} + export interface BattleJoinProps extends SystemSigner { battle_id: num.BigNumberish; battle_side: num.BigNumberish; From b77a967d54a77fd9c0791c6de1efcec1dd049b24 Mon Sep 17 00:00:00 2001 From: Credence Date: Sat, 14 Sep 2024 06:16:53 +0100 Subject: [PATCH 02/12] contracts: fix troop normalization and minor refactor --- contracts/src/models/combat.cairo | 174 ++++++++++++++++-- contracts/src/models/movable.cairo | 2 +- contracts/src/systems/combat/contracts.cairo | 180 +++++-------------- 3 files changed, 205 insertions(+), 151 deletions(-) diff --git a/contracts/src/models/combat.cairo b/contracts/src/models/combat.cairo index 81325f966..12165458b 100644 --- a/contracts/src/models/combat.cairo +++ b/contracts/src/models/combat.cairo @@ -30,6 +30,7 @@ use eternum::utils::number::NumberTrait; const STRENGTH_PRECISION: u256 = 10_000; + #[derive(IntrospectPacked, Copy, Drop, Serde, Default)] #[dojo::model] pub struct Health { @@ -123,6 +124,37 @@ impl TroopsImpl of TroopsTrait { self.crossbowman_count -= other.crossbowman_count; } + // normalize troop counts to nearest mutiple of RESOURCE_PRECISION + // so that troop units only exists as whole and not decimals + fn normalize_counts(ref self: Troops) { + self.knight_count -= self.knight_count % self.normalization_factor(); + self.paladin_count -= self.paladin_count % self.normalization_factor(); + self.crossbowman_count -= self.crossbowman_count % self.normalization_factor(); + } + + fn normalization_factor(self: Troops) -> u64 { + let resource_precision_u64: u64 = RESOURCE_PRECISION.try_into().unwrap(); + return resource_precision_u64; + } + + fn assert_normalized(self: Troops) { + assert!( + self.knight_count % self.normalization_factor() == 0, + "Knight count is not a multiple of {}", + self.normalization_factor() + ); + assert!( + self.paladin_count % self.normalization_factor() == 0, + "Paladin count is not a multiple of {}", + self.normalization_factor() + ); + assert!( + self.crossbowman_count % self.normalization_factor() == 0, + "Crossbowman count is not a multiple of {}", + self.normalization_factor() + ); + } + fn full_health(self: Troops, troop_config: TroopConfig) -> u128 { let h: u128 = troop_config.health.into(); let total_knight_health: u128 = h * self.knight_count.into(); @@ -161,10 +193,19 @@ impl TroopsImpl of TroopsTrait { self: @Troops, self_health: @Health, enemy_troops: @Troops, enemy_health: @Health, troop_config: TroopConfig ) -> (u64, u64) { let self_delta: i128 = self.strength_against(self_health, enemy_troops, enemy_health, troop_config); - let self_delta_abs: u64 = Into::::into(self_delta.abs()).try_into().unwrap(); + let mut self_delta_abs: u64 = Into::::into(self_delta.abs()).try_into().unwrap(); let enemy_delta: i128 = enemy_troops.strength_against(enemy_health, self, self_health, troop_config); - let enemy_delta_abs: u64 = Into::::into(enemy_delta.abs()).try_into().unwrap(); + let mut enemy_delta_abs: u64 = Into::::into(enemy_delta.abs()).try_into().unwrap(); + + let nmf: u64 = (*self).normalization_factor(); + if self_delta_abs < nmf { + self_delta_abs = nmf; + } + + if enemy_delta_abs < nmf { + enemy_delta_abs = nmf; + } return (enemy_delta_abs, self_delta_abs); } @@ -239,6 +280,7 @@ impl TroopsImpl of TroopsTrait { self.knight_count = self.actual_type_count(TroopType::Knight, @health); self.paladin_count = self.actual_type_count(TroopType::Paladin, @health); self.crossbowman_count = self.actual_type_count(TroopType::Crossbowman, @health); + self.normalize_counts(); // make the new health be the full health of updated troops health.clear(); @@ -283,18 +325,6 @@ impl TroopsImpl of TroopsTrait { .try_into() .unwrap() } - - fn reduce_if_under_precision(mut self: Troops) { - if self.knight_count < RESOURCE_PRECISION.try_into().unwrap() { - self.knight_count = 0; - } - if self.paladin_count < RESOURCE_PRECISION.try_into().unwrap() { - self.paladin_count = 0; - } - if self.crossbowman_count < RESOURCE_PRECISION.try_into().unwrap() { - self.crossbowman_count = 0; - } - } } #[generate_trait] @@ -742,6 +772,122 @@ impl BattleCustomImpl of BattleCustomTrait { // it's possible that both killed each other or battle has not ended return BattleSide::None; } + + // update battle troops and their health to actual values after battle + fn update_troops_and_health(ref self: Battle, side: BattleSide, troop_config: TroopConfig) { + let (mut battle_army, mut battle_army_health) = if side == BattleSide::Defence { + (self.defence_army, self.defence_army_health) + } else { + (self.attack_army, self.attack_army_health) + }; + + if battle_army_health.lifetime.is_non_zero() { + battle_army + .troops + .knight_count = + ((battle_army_health.current * battle_army.troops.knight_count.into()) + / battle_army_health.lifetime) + .try_into() + .unwrap(); + battle_army + .troops + .paladin_count = + ((battle_army_health.current * battle_army.troops.paladin_count.into()) + / battle_army_health.lifetime) + .try_into() + .unwrap(); + battle_army + .troops + .crossbowman_count = + ((battle_army_health.current * battle_army.troops.crossbowman_count.into()) + / battle_army_health.lifetime) + .try_into() + .unwrap(); + battle_army.troops.normalize_counts(); + battle_army_health.current = battle_army.troops.full_health(troop_config); + battle_army_health.lifetime = battle_army.troops.full_health(troop_config); + if side == BattleSide::Defence { + self.defence_army = battle_army; + self.defence_army_health = battle_army_health; + } else { + self.attack_army = battle_army; + self.attack_army_health = battle_army_health; + } + } + } + + fn get_troops_share_left(ref self: Battle, mut army: Army) -> Army { + let (battle_army, battle_army_lifetime) = if army.battle_side == BattleSide::Defence { + (self.defence_army, self.defence_army_lifetime) + } else { + (self.attack_army, self.attack_army_lifetime) + }; + army + .troops + .knight_count = + if army.troops.knight_count == 0 { + 0 + } else { + army.troops.knight_count + * battle_army.troops.knight_count + / battle_army_lifetime.troops.knight_count + }; + + army + .troops + .paladin_count = + if army.troops.paladin_count == 0 { + 0 + } else { + army.troops.paladin_count + * battle_army.troops.paladin_count + / battle_army_lifetime.troops.paladin_count + }; + + army + .troops + .crossbowman_count = + if army.troops.crossbowman_count == 0 { + 0 + } else { + army.troops.crossbowman_count + * battle_army.troops.crossbowman_count + / battle_army_lifetime.troops.crossbowman_count + }; + + // normalize troop count + army.troops.normalize_counts(); + + return army; + } + + fn reduce_battle_army_troops_and_health_by( + ref self: Battle, army_before_normalization: Army, troop_config: TroopConfig + ) { + // update battle army count and health + if army_before_normalization.battle_side == BattleSide::Defence { + self.defence_army.troops.deduct(army_before_normalization.troops); + self.defence_army_health.current = self.defence_army.troops.full_health(troop_config); + self.defence_army_health.lifetime = self.defence_army.troops.full_health(troop_config); + } else { + self.attack_army.troops.deduct(army_before_normalization.troops); + self.attack_army_health.current = self.attack_army.troops.full_health(troop_config); + self.attack_army_health.lifetime = self.attack_army.troops.full_health(troop_config); + } + } + + // reduce battle army lifetime count by the original army count + fn reduce_battle_army_lifetime_by(ref self: Battle, army_before_any_modification: Army) { + if army_before_any_modification.battle_side == BattleSide::Defence { + self.defence_army_lifetime.troops.deduct(army_before_any_modification.troops); + } else { + self.attack_army_lifetime.troops.deduct(army_before_any_modification.troops); + } + } + + fn is_empty(self: Battle) -> bool { + self.attack_army_lifetime.troops.count().is_zero() && self.defence_army_lifetime.troops.count().is_zero() + } } diff --git a/contracts/src/models/movable.cairo b/contracts/src/models/movable.cairo index 0e6c415cb..135895b41 100644 --- a/contracts/src/models/movable.cairo +++ b/contracts/src/models/movable.cairo @@ -29,7 +29,7 @@ impl MovableCustomImpl of MovableCustomTrait { } fn assert_blocked(self: Movable) { - assert!(self.blocked, "Entity is not blocked"); + assert!(self.blocked, "Entity {} is not blocked", self.entity_id); } } diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index a1b2d3a53..c53cd9617 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -392,8 +392,7 @@ mod combat_systems { use core::traits::TryInto; use eternum::alias::ID; use eternum::constants::{ - ResourceTypes, ErrorMessages, get_resources_without_earthenshards, get_resources_without_earthenshards_probs, - RESOURCE_PRECISION + ResourceTypes, ErrorMessages, get_resources_without_earthenshards, get_resources_without_earthenshards_probs }; use eternum::constants::{WORLD_CONFIG_ID, ARMY_ENTITY_TYPE, MAX_PILLAGE_TRIAL_COUNT}; use eternum::models::buildings::{Building, BuildingCustomImpl, BuildingCategory, BuildingQuantityv2,}; @@ -480,6 +479,9 @@ mod combat_systems { fn army_buy_troops(ref world: IWorldDispatcher, army_id: ID, payer_id: ID, mut troops: Troops) { + // ensure troop values are normalized + troops.assert_normalized(); + // ensure caller owns the entity paying get!(world, payer_id, EntityOwner).assert_caller_owner(world); @@ -542,6 +544,9 @@ mod combat_systems { fn army_merge_troops(ref world: IWorldDispatcher, from_army_id: ID, to_army_id: ID, troops: Troops,) { + // ensure troop values are normalized + troops.assert_normalized(); + // ensure caller owns from army let mut from_army_owner: EntityOwner = get!(world, from_army_id, EntityOwner); from_army_owner.assert_caller_owner(world); @@ -563,8 +568,6 @@ mod combat_systems { from_army.troops.deduct(troops); set!(world, (from_army)); - from_army.troops.reduce_if_under_precision(); - // decrease from army health let mut from_army_health: Health = get!(world, from_army_id, Health); let troop_full_health = troops.full_health(troop_config); @@ -575,7 +578,7 @@ mod combat_systems { from_army_health.lifetime -= (troop_full_health); set!(world, (from_army_health)); - // decrease from army quantity + // decrease from army quantity let mut from_army_quantity: Quantity = get!(world, from_army_id, Quantity); from_army_quantity.value -= troops.count().into(); set!(world, (from_army_quantity)); @@ -875,9 +878,6 @@ mod combat_systems { // leave battle let mut battle: Battle = get!(world, battle_id, Battle); - - InternalCombatImpl::leave_battle(world, ref battle, ref caller_army); - battle.update_state(); let battle_was_active = (battle.has_started() && !battle.has_ended()); InternalCombatImpl::leave_battle(world, ref battle, ref caller_army); @@ -1253,18 +1253,10 @@ mod combat_systems { ); attacking_army.troops.reset_count_and_health(ref attacking_army_health, troop_config); - - attacking_army.troops.reduce_if_under_precision(); - - if (attacking_army.troops.count().is_zero()) { - let mut entity_owner: EntityOwner = get!(world, attacking_army, EntityOwner); - InternalCombatImpl::delete_army(world, ref entity_owner, ref attacking_army); - } else { - let attacking_army_quantity = Quantity { - entity_id: attacking_army.entity_id, value: attacking_army.troops.count().into() - }; - set!(world, (attacking_army, attacking_army_health, attacking_army_quantity)); - } + let attacking_army_quantity = Quantity { + entity_id: attacking_army.entity_id, value: attacking_army.troops.count().into() + }; + set!(world, (attacking_army, attacking_army_health, attacking_army_quantity)); // reset structure army health and troop count structure_army_health @@ -1492,13 +1484,11 @@ mod combat_systems { /// Make army leave battle - fn leave_battle(world: IWorldDispatcher, ref battle: Battle, ref army: Army) { - let unmodified_army = army; - + fn leave_battle(world: IWorldDispatcher, ref battle: Battle, ref original_army: Army) { battle.update_state(); // make caller army mobile again - let army_id = army.entity_id; + let army_id = original_army.entity_id; let mut army_protectee: Protectee = get!(world, army_id, Protectee); let mut army_movable: Movable = get!(world, army_id, Movable); if army_protectee.is_none() { @@ -1509,130 +1499,48 @@ mod combat_systems { assert!(battle.has_ended(), "structure can only leave battle after it ends"); } - // get up to date battle troop count and health - let (mut battle_army, mut battle_army_health, mut battle_army_lifetime) = if army - .battle_side == BattleSide::Defence { - (battle.defence_army, battle.defence_army_health, battle.defence_army_lifetime) - } else { - (battle.attack_army, battle.attack_army_health, battle.attack_army_lifetime) - }; + // update battle troops and their health to actual values after battle let troop_config = TroopConfigCustomImpl::get(world); + battle.update_troops_and_health(original_army.battle_side, troop_config); - if battle_army_health.lifetime.is_non_zero() { - battle_army - .troops - .knight_count = - ((battle_army_health.current * battle_army.troops.knight_count.into()) - / battle_army_health.lifetime) - .try_into() - .unwrap(); - battle_army - .troops - .paladin_count = - ((battle_army_health.current * battle_army.troops.paladin_count.into()) - / battle_army_health.lifetime) - .try_into() - .unwrap(); - battle_army - .troops - .crossbowman_count = - ((battle_army_health.current * battle_army.troops.crossbowman_count.into()) - / battle_army_health.lifetime) - .try_into() - .unwrap(); - - battle_army.troops.reduce_if_under_precision(); - - battle_army_health.current = battle_army.troops.full_health(troop_config); - battle_army_health.lifetime = battle_army.troops.full_health(troop_config); - if unmodified_army.battle_side == BattleSide::Defence { - battle.defence_army = battle_army; - battle.defence_army_lifetime = battle_army_lifetime; - battle.defence_army_health = battle_army_health; - } else { - battle.attack_army = battle_army; - battle.attack_army_lifetime = battle_army_lifetime; - battle.attack_army_health = battle_army_health; - } - } - - // reset the army leaving battle - army - .troops - .knight_count = - if army.troops.knight_count == 0 { - 0 - } else { - army.troops.knight_count - * battle_army.troops.knight_count - / battle_army_lifetime.troops.knight_count - }; - - army - .troops - .paladin_count = - if army.troops.paladin_count == 0 { - 0 - } else { - army.troops.paladin_count - * battle_army.troops.paladin_count - / battle_army_lifetime.troops.paladin_count - }; - - army - .troops - .crossbowman_count = - if army.troops.crossbowman_count == 0 { - 0 - } else { - army.troops.crossbowman_count - * battle_army.troops.crossbowman_count - / battle_army_lifetime.troops.crossbowman_count - }; - - army.troops.reduce_if_under_precision(); + // get normalized share of army left after battle + let army_left = battle.get_troops_share_left(original_army); - let army_health = Health { - entity_id: army_id, - current: army.troops.full_health(troop_config), - lifetime: army.troops.full_health(troop_config) - }; - let army_quantity = Quantity { entity_id: army_id, value: army.troops.count().into() }; - set!(world, (army_health, army_quantity)); + // update army quantity to correct value before calling `withdraw_balance_and_reward` + // because it is used to calculate the loot amount sent to army if it wins the battle + // i.e when the `withdraw_balance_and_reward` calls `InternalResourceSystemsImpl::mint_if_adequate_capacity` + let army_quantity_left = Quantity { entity_id: army_id, value: army_left.troops.count().into() }; + set!(world, (army_quantity_left)); // withdraw battle deposit and reward - battle.withdraw_balance_and_reward(world, army, army_protectee); - - // remove army from battle - army.battle_id = 0; - army.battle_side = BattleSide::None; - set!(world, (army)); + battle.withdraw_balance_and_reward(world, army_left, army_protectee); - // update battle army count and health - battle_army.troops.deduct(army.troops); - battle_army_health.current = battle_army.troops.full_health(troop_config); - battle_army_health.lifetime = battle_army.troops.full_health(troop_config); + // update battle army values + battle.reduce_battle_army_troops_and_health_by(army_left, troop_config); + battle.reduce_battle_army_lifetime_by(original_army); - // reduce battle army lifetime count by the original army count - battle_army_lifetime.troops.deduct(unmodified_army.troops); - - if unmodified_army.battle_side == BattleSide::Defence { - battle.defence_army = battle_army; - battle.defence_army_lifetime = battle_army_lifetime; - battle.defence_army_health = battle_army_health; - } else { - battle.attack_army = battle_army; - battle.attack_army_lifetime = battle_army_lifetime; - battle.attack_army_health = battle_army_health; - } - - if (battle.attack_army_lifetime.troops.count().is_zero() - && battle.defence_army_lifetime.troops.count().is_zero()) { + if (battle.is_empty()) { + // delete battle when the last participant leaves delete!(world, (battle)); } else { + // update battle if it still has participants battle.reset_delta(troop_config); set!(world, (battle)); } + + // update army + let mut army_left = army_left; + army_left.battle_id = 0; + army_left.battle_side = BattleSide::None; + set!(world, (army_left)); + + // update army health + let army_left_health = Health { + entity_id: army_id, + current: army_left.troops.full_health(troop_config), + lifetime: army_left.troops.full_health(troop_config) + }; + set!(world, (army_left_health)); } } } From 01013f3dd74140b4a1d35788cd6c1065c80e9520 Mon Sep 17 00:00:00 2001 From: Credence Date: Sat, 14 Sep 2024 07:27:48 +0100 Subject: [PATCH 03/12] contracts: fix troop normalization and minor refactor --- contracts/src/models/combat.cairo | 58 ++++++++++--------- .../combat/tests/battle_leave_test.cairo | 2 +- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/contracts/src/models/combat.cairo b/contracts/src/models/combat.cairo index 12165458b..693c09b70 100644 --- a/contracts/src/models/combat.cairo +++ b/contracts/src/models/combat.cairo @@ -68,22 +68,33 @@ impl HealthCustomImpl of HealthCustomTrait { assert!(self.is_alive(), "{} is dead", entity_name); } - fn steps_to_die(self: @Health, mut deduction: u128) -> u128 { + fn steps_to_die(self: @Health, mut deduction: u128, troop_config: TroopConfig) -> u128 { if *self.current == 0 || deduction == 0 { return 0; }; - let mut num_steps = *self.current / deduction; - if (num_steps % deduction) > 0 { + // 0 < self.current <= deduction + if *self.current <= deduction { + return 1; + } + + // 0 < self.current <= single_troop_health + let single_troop_health = troop_config.health.into(); + if *self.current <= single_troop_health { + return 1; + } + + // we know + // self.current > 0 && self.current > single_troop_health + // and deduction < self.current + let troop_modulo = *self.current % single_troop_health; + let current_w_no_modulo = *self.current - troop_modulo; + let mut num_steps = current_w_no_modulo / deduction; + if (num_steps % deduction) > 0 || troop_modulo > 0 { num_steps += 1; } - // this condition is here in case - // self.current < deduction which would make - // num_steps = 0 but that would cause the - // "inaccurate winner invariant" error so we make it - // at least 1. - max(num_steps, 1) + num_steps } fn percentage_left(self: Health) -> u128 { @@ -193,19 +204,10 @@ impl TroopsImpl of TroopsTrait { self: @Troops, self_health: @Health, enemy_troops: @Troops, enemy_health: @Health, troop_config: TroopConfig ) -> (u64, u64) { let self_delta: i128 = self.strength_against(self_health, enemy_troops, enemy_health, troop_config); - let mut self_delta_abs: u64 = Into::::into(self_delta.abs()).try_into().unwrap(); + let self_delta_abs: u64 = Into::::into(self_delta.abs()).try_into().unwrap(); let enemy_delta: i128 = enemy_troops.strength_against(enemy_health, self, self_health, troop_config); - let mut enemy_delta_abs: u64 = Into::::into(enemy_delta.abs()).try_into().unwrap(); - - let nmf: u64 = (*self).normalization_factor(); - if self_delta_abs < nmf { - self_delta_abs = nmf; - } - - if enemy_delta_abs < nmf { - enemy_delta_abs = nmf; - } + let enemy_delta_abs: u64 = Into::::into(enemy_delta.abs()).try_into().unwrap(); return (enemy_delta_abs, self_delta_abs); } @@ -399,8 +401,8 @@ impl BattleHealthCustomImpl of BattleHealthCustomTrait { Into::::into(self).assert_alive("Army") } - fn steps_to_die(self: @BattleHealth, deduction: u128) -> u128 { - Into::::into(*self).steps_to_die(deduction) + fn steps_to_die(self: @BattleHealth, deduction: u128, troop_config: TroopConfig) -> u128 { + Into::::into(*self).steps_to_die(deduction, troop_config) } fn percentage_left(self: BattleHealth) -> u128 { @@ -704,14 +706,18 @@ impl BattleCustomImpl of BattleCustomTrait { self.defence_delta = defence_delta; // get duration with latest delta - self.duration_left = self.duration(); + self.duration_left = self.duration(troop_config); } - fn duration(self: Battle) -> u64 { - let mut attack_num_seconds_to_death = self.attack_army_health.steps_to_die(self.defence_delta.into()); + fn duration(self: Battle, troop_config: TroopConfig) -> u64 { + let mut attack_num_seconds_to_death = self + .attack_army_health + .steps_to_die(self.defence_delta.into(), troop_config); - let mut defence_num_seconds_to_death = self.defence_army_health.steps_to_die(self.attack_delta.into()); + let mut defence_num_seconds_to_death = self + .defence_army_health + .steps_to_die(self.attack_delta.into(), troop_config); min(defence_num_seconds_to_death, attack_num_seconds_to_death).try_into().unwrap() } diff --git a/contracts/src/systems/combat/tests/battle_leave_test.cairo b/contracts/src/systems/combat/tests/battle_leave_test.cairo index 4fa9577c3..483f17af1 100644 --- a/contracts/src/systems/combat/tests/battle_leave_test.cairo +++ b/contracts/src/systems/combat/tests/battle_leave_test.cairo @@ -247,7 +247,7 @@ fn test_battle_leave_by_winner() { // ensure player_1's army troop count is correct let player_1_army: Army = get!(world, player_1_army_id, Army); - assert_eq!(player_1_army.troops.count(), 8_747_643); + assert_eq!(player_1_army.troops.count(), 8_745_000); // ensure the battle was updated correctly let battle: Battle = get!(world, battle_id, Battle); From 86a958acec421f419c487c9d27d3dc69668747e7 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 02:00:44 +0100 Subject: [PATCH 04/12] contracts: fix troop normalization and minor refactor --- contracts/src/models/combat.cairo | 80 +++++++----- contracts/src/systems/combat/contracts.cairo | 125 ++++++------------- 2 files changed, 88 insertions(+), 117 deletions(-) diff --git a/contracts/src/models/combat.cairo b/contracts/src/models/combat.cairo index 693c09b70..be04157f1 100644 --- a/contracts/src/models/combat.cairo +++ b/contracts/src/models/combat.cairo @@ -78,19 +78,17 @@ impl HealthCustomImpl of HealthCustomTrait { return 1; } - // 0 < self.current <= single_troop_health - let single_troop_health = troop_config.health.into(); - if *self.current <= single_troop_health { - return 1; - } + // check health is a multiple of normalization factor + let single_troop_health = troop_config.health.into() * TroopsImpl::normalization_factor().into(); + assert!(single_troop_health > 0, "single_troop_health is 0"); + assert!( + *self.current % single_troop_health == 0, + "health of entity {} is not a multiple of normalization factor", + self.entity_id + ); - // we know - // self.current > 0 && self.current > single_troop_health - // and deduction < self.current - let troop_modulo = *self.current % single_troop_health; - let current_w_no_modulo = *self.current - troop_modulo; - let mut num_steps = current_w_no_modulo / deduction; - if (num_steps % deduction) > 0 || troop_modulo > 0 { + let mut num_steps = *self.current / deduction; + if (num_steps % deduction) > 0 { num_steps += 1; } @@ -138,31 +136,31 @@ impl TroopsImpl of TroopsTrait { // normalize troop counts to nearest mutiple of RESOURCE_PRECISION // so that troop units only exists as whole and not decimals fn normalize_counts(ref self: Troops) { - self.knight_count -= self.knight_count % self.normalization_factor(); - self.paladin_count -= self.paladin_count % self.normalization_factor(); - self.crossbowman_count -= self.crossbowman_count % self.normalization_factor(); + self.knight_count -= self.knight_count % Self::normalization_factor(); + self.paladin_count -= self.paladin_count % Self::normalization_factor(); + self.crossbowman_count -= self.crossbowman_count % Self::normalization_factor(); } - fn normalization_factor(self: Troops) -> u64 { + fn normalization_factor() -> u64 { let resource_precision_u64: u64 = RESOURCE_PRECISION.try_into().unwrap(); return resource_precision_u64; } fn assert_normalized(self: Troops) { assert!( - self.knight_count % self.normalization_factor() == 0, + self.knight_count % Self::normalization_factor() == 0, "Knight count is not a multiple of {}", - self.normalization_factor() + Self::normalization_factor() ); assert!( - self.paladin_count % self.normalization_factor() == 0, + self.paladin_count % Self::normalization_factor() == 0, "Paladin count is not a multiple of {}", - self.normalization_factor() + Self::normalization_factor() ); assert!( - self.crossbowman_count % self.normalization_factor() == 0, + self.crossbowman_count % Self::normalization_factor() == 0, "Crossbowman count is not a multiple of {}", - self.normalization_factor() + Self::normalization_factor() ); } @@ -670,16 +668,25 @@ impl BattleEscrowImpl of BattleEscrowTrait { #[generate_trait] impl BattleCustomImpl of BattleCustomTrait { + fn get(world: IWorldDispatcher, battle_id: ID) -> Battle { + let mut battle = get!(world, battle_id, Battle); + let troop_config = TroopConfigCustomImpl::get(world); + battle.update_state(troop_config); + return battle; + } + /// This function updated the armies health and duration /// of battle according to the set delta and battle duration /// /// Update state should be called before reading /// battle model values so that the correct values /// are gotten - fn update_state(ref self: Battle) { + fn update_state(ref self: Battle, troop_config: TroopConfig) { let battle_duration_passed = self.duration_passed(); self.attack_army_health.decrease_current_by((self.defence_delta.into() * battle_duration_passed.into())); self.defence_army_health.decrease_current_by((self.attack_delta.into() * battle_duration_passed.into())); + self.update_troops_and_health(BattleSide::Attack, troop_config); + self.update_troops_and_health(BattleSide::Defence, troop_config); } /// This function calculates the delta (rate at which health goes down per second) /// and therefore, the duration of tha battle. @@ -779,6 +786,18 @@ impl BattleCustomImpl of BattleCustomTrait { return BattleSide::None; } + fn join(ref self: Battle, side: BattleSide, troops: Troops, health: u128) { + if side == BattleSide::Defence { + self.defence_army.troops.add(troops); + self.defence_army_lifetime.troops.add(troops); + self.defence_army_health.increase_by(health); + } else { + self.attack_army.troops.add(troops); + self.attack_army_lifetime.troops.add(troops); + self.attack_army_health.increase_by(health); + } + } + // update battle troops and their health to actual values after battle fn update_troops_and_health(ref self: Battle, side: BattleSide, troop_config: TroopConfig) { let (mut battle_army, mut battle_army_health) = if side == BattleSide::Defence { @@ -1017,7 +1036,8 @@ mod tests { // move time up but before battle ends starknet::testing::set_block_timestamp(battle.duration_left - 1); - battle.update_state(); + let troop_config = mock_troop_config(); + battle.update_state(troop_config); assert!(battle.has_ended() == false, "battle should not have ended"); } @@ -1031,7 +1051,8 @@ mod tests { // move time up to battle duration starknet::testing::set_block_timestamp(battle.duration_left); - battle.update_state(); + let troop_config = mock_troop_config(); + battle.update_state(troop_config); assert!(battle.has_ended() == true, "battle should have ended"); } @@ -1196,7 +1217,8 @@ mod tests { // lose battle starknet::testing::set_block_timestamp(battle.duration_left + 1); // original ts was 1 - battle.update_state(); + let troop_config = mock_troop_config(); + battle.update_state(troop_config); assert!(battle.has_ended(), "Battle should have ended"); assert!(battle.winner() == BattleSide::Defence, "unexpected side won"); @@ -1278,7 +1300,8 @@ mod tests { // lose battle starknet::testing::set_block_timestamp(battle.duration_left + 1); // original ts was 1 - battle.update_state(); + let troop_config = mock_troop_config(); + battle.update_state(troop_config); assert!(battle.has_ended(), "Battle should have ended"); assert!(battle.winner() == BattleSide::None, "unexpected side won"); @@ -1375,7 +1398,8 @@ mod tests { // attacker wins battle starknet::testing::set_block_timestamp(battle.duration_left + 1); // original ts was 1 - battle.update_state(); + let troop_config = mock_troop_config(); + battle.update_state(troop_config); assert!(battle.has_ended(), "Battle should have ended"); assert!(battle.winner() == BattleSide::Attack, "unexpected side won"); diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index c53cd9617..e8925b49a 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -461,8 +461,8 @@ mod combat_systems { // ensure army is dead let mut army: Army = get!(world, army_id, Army); if army.is_in_battle() { - let mut battle: Battle = get!(world, army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref army); + let mut battle = BattleCustomImpl::get(world, army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref battle, ref army); set!(world, (battle)); } @@ -502,38 +502,16 @@ mod combat_systems { let mut army: Army = get!(world, army_id, Army); if army.is_in_battle() { - // update army health and troop count - let mut battle: Battle = get!(world, army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref army); - - // add the troops being bought to battle - if !battle.has_ended() { - let battle_side = army.battle_side; - // add troops to battle army troops - let (mut battle_army, mut battle_army_health, mut battle_army_lifetime) = - if battle_side == BattleSide::Defence { - (battle.defence_army, battle.defence_army_health, battle.defence_army_lifetime) - } else { - (battle.attack_army, battle.attack_army_health, battle.attack_army_lifetime) - }; - battle_army.troops.add(troops); - battle_army_lifetime.troops.add(troops); - - // add troop health to battle army health + let mut battle = BattleCustomImpl::get(world, army.battle_id); + if battle.has_ended() { + // if battle has ended, leave the battle + InternalCombatImpl::leave_battle(world, ref battle, ref army); + } else { + // if battle has not ended, add the troops to the battle let troop_config = TroopConfigCustomImpl::get(world); - battle_army_health.increase_by(troops.full_health(troop_config)); - - // update battle - if battle_side == BattleSide::Defence { - battle.defence_army = battle_army; - battle.defence_army_health = battle_army_health; - battle.defence_army_lifetime = battle_army_lifetime; - } else { - battle.attack_army = battle_army; - battle.attack_army_health = battle_army_health; - battle.attack_army_lifetime = battle_army_lifetime; - } + let troops_full_health = troops.full_health(troop_config); + battle.join(army.battle_side, troops, troops_full_health); battle.reset_delta(troop_config); set!(world, (battle)); } @@ -561,8 +539,8 @@ mod combat_systems { // decrease from army troops let mut from_army: Army = get!(world, from_army_id, Army); if from_army.is_in_battle() { - let mut battle: Battle = get!(world, from_army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref from_army); + let mut battle = BattleCustomImpl::get(world, from_army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref battle, ref from_army); } from_army.assert_not_in_battle(); from_army.troops.deduct(troops); @@ -592,8 +570,8 @@ mod combat_systems { // increase to army troops let mut to_army: Army = get!(world, to_army_id, Army); if to_army.is_in_battle() { - let mut battle: Battle = get!(world, to_army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref to_army); + let mut battle = BattleCustomImpl::get(world, to_army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref battle, ref to_army); } to_army.assert_not_in_battle(); @@ -631,8 +609,8 @@ mod combat_systems { // so we want to update the defending army's battle status // to see if the battle has ended. if it has ended, then the // army will be removed from the battle - let mut defending_army_battle: Battle = get!(world, defending_army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref defending_army_battle, ref defending_army); + let mut defending_army_battle = BattleCustomImpl::get(world, defending_army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref defending_army_battle, ref defending_army); set!(world, (defending_army_battle)) } // ensure defending army is not in battle @@ -757,7 +735,7 @@ mod combat_systems { assert!(defending_army.battle_side == BattleSide::Defence, "army is not on defensive"); let now = starknet::get_block_timestamp(); - let mut battle: Battle = get!(world, battle_id, Battle); + let mut battle = BattleCustomImpl::get(world, battle_id); assert!(now < battle.start_at, "Battle already started"); // update battle @@ -771,11 +749,8 @@ mod combat_systems { // ensure caller owns army get!(world, army_id, EntityOwner).assert_caller_owner(world); - // update battle state before any other actions - let mut battle: Battle = get!(world, battle_id, Battle); - battle.update_state(); - // ensure battle has not ended + let mut battle = BattleCustomImpl::get(world, battle_id); assert!(!battle.has_ended(), "Battle has ended"); // ensure caller army is not in battle @@ -783,11 +758,11 @@ mod combat_systems { caller_army.assert_not_in_battle(); // ensure caller army is not dead - let troop_config = TroopConfigCustomImpl::get(world); let mut caller_army_health: Health = get!(world, army_id, Health); caller_army_health.assert_alive("Your army"); // caller army health sanity check + let troop_config = TroopConfigCustomImpl::get(world); assert!( caller_army_health.current == caller_army.troops.full_health(troop_config), "caller health sanity check fail" @@ -814,32 +789,9 @@ mod combat_systems { // lock resources being protected by army battle.deposit_balance(world, caller_army, caller_army_protectee); - // add caller army troops to battle army troops - - let (mut battle_army, mut battle_army_health, mut battle_army_lifetime) = - if battle_side == BattleSide::Defence { - (battle.defence_army, battle.defence_army_health, battle.defence_army_lifetime) - } else { - (battle.attack_army, battle.attack_army_health, battle.attack_army_lifetime) - }; - - battle_army.troops.add(caller_army.troops); - battle_army_lifetime.troops.add(caller_army.troops); - - // add caller army heath to battle army health - battle_army_health.increase_by(caller_army_health.current); - - // update battle - if battle_side == BattleSide::Defence { - battle.defence_army = battle_army; - battle.defence_army_health = battle_army_health; - battle.defence_army_lifetime = battle_army_lifetime; - } else { - battle.attack_army = battle_army; - battle.attack_army_health = battle_army_health; - battle.attack_army_lifetime = battle_army_lifetime; - } - + // add troops to battle army troops + let troop_config = TroopConfigCustomImpl::get(world); + battle.join(battle_side, caller_army.troops, caller_army_health.current); battle.reset_delta(troop_config); set!(world, (battle)); @@ -876,14 +828,17 @@ mod combat_systems { let caller_army_side = caller_army.battle_side; + // get battle + let mut battle = BattleCustomImpl::get(world, battle_id); + + // check if army left early + let army_left_early = (battle.has_started() && !battle.has_ended()); + // leave battle - let mut battle: Battle = get!(world, battle_id, Battle); - battle.update_state(); - let battle_was_active = (battle.has_started() && !battle.has_ended()); InternalCombatImpl::leave_battle(world, ref battle, ref caller_army); // slash army if battle was not concluded before they left - if battle_was_active { + if army_left_early { let troop_config = TroopConfigCustomImpl::get(world); let mut army = get!(world, army_id, Army); let troops_deducted = Troops { @@ -953,8 +908,8 @@ mod combat_systems { if structure_army_id.is_non_zero() { let mut structure_army: Army = get!(world, structure_army_id, Army); if structure_army.is_in_battle() { - let mut battle: Battle = get!(world, structure_army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref structure_army); + let mut battle = BattleCustomImpl::get(world, structure_army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref battle, ref structure_army); } // ensure structure army is dead @@ -1026,8 +981,8 @@ mod combat_systems { if structure_army_id.is_non_zero() { structure_army = get!(world, structure_army_id, Army); if structure_army.is_in_battle() { - let mut battle: Battle = get!(world, structure_army.battle_id, Battle); - InternalCombatImpl::update_battle_and_army(world, ref battle, ref structure_army); + let mut battle = BattleCustomImpl::get(world, structure_army.battle_id); + InternalCombatImpl::leave_battle_if_ended(world, ref battle, ref structure_army); } // get accurate structure army health @@ -1471,11 +1426,8 @@ mod combat_systems { ); } - /// Updates battle and removes army if battle has ended - /// - fn update_battle_and_army(world: IWorldDispatcher, ref battle: Battle, ref army: Army) { + fn leave_battle_if_ended(world: IWorldDispatcher, ref battle: Battle, ref army: Army) { assert!(battle.entity_id == army.battle_id, "army must be in same battle"); - battle.update_state(); if battle.has_ended() { // leave battle to update structure army's health Self::leave_battle(world, ref battle, ref army); @@ -1485,8 +1437,6 @@ mod combat_systems { /// Make army leave battle fn leave_battle(world: IWorldDispatcher, ref battle: Battle, ref original_army: Army) { - battle.update_state(); - // make caller army mobile again let army_id = original_army.entity_id; let mut army_protectee: Protectee = get!(world, army_id, Protectee); @@ -1499,10 +1449,6 @@ mod combat_systems { assert!(battle.has_ended(), "structure can only leave battle after it ends"); } - // update battle troops and their health to actual values after battle - let troop_config = TroopConfigCustomImpl::get(world); - battle.update_troops_and_health(original_army.battle_side, troop_config); - // get normalized share of army left after battle let army_left = battle.get_troops_share_left(original_army); @@ -1515,7 +1461,8 @@ mod combat_systems { // withdraw battle deposit and reward battle.withdraw_balance_and_reward(world, army_left, army_protectee); - // update battle army values + // reduce battle army values + let troop_config = TroopConfigCustomImpl::get(world); battle.reduce_battle_army_troops_and_health_by(army_left, troop_config); battle.reduce_battle_army_lifetime_by(original_army); From b41eabc8c7a1aff9fcf4bfb780c7029f97246466 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 02:08:09 +0100 Subject: [PATCH 05/12] update workflow --- .github/workflows/test-contracts.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index 016d0c0a8..832146b1e 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -58,7 +58,6 @@ jobs: - name: Run Dojo Test for ${{ matrix.test }} run: | cd contracts && sozo test -f ${{ matrix.test }} --print-resource-usage - continue-on-error: true test-scarb-fmt: needs: [setup-environment] From 20ac2bcde970d653be8c5ef29bed466f3e86a397 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 02:12:46 +0100 Subject: [PATCH 06/12] contracts: update test --- contracts/src/systems/combat/tests/battle_leave_test.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/systems/combat/tests/battle_leave_test.cairo b/contracts/src/systems/combat/tests/battle_leave_test.cairo index 483f17af1..cff7a5e8b 100644 --- a/contracts/src/systems/combat/tests/battle_leave_test.cairo +++ b/contracts/src/systems/combat/tests/battle_leave_test.cairo @@ -259,8 +259,8 @@ fn test_battle_leave_by_winner() { assert_eq!(battle.defence_army_health.current, 0); - assert_ne!(battle.defence_army.troops.count(), 0); - assert_ne!(battle.defence_army_lifetime.troops.count(), 0); + assert_eq!(battle.defence_army.troops.count(), 0); + assert_eq!(battle.defence_army_lifetime.troops.count(), 0); } From 1890277502f3ff27f645823e8bf8905073823da3 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 02:39:50 +0100 Subject: [PATCH 07/12] contracts: fix troop normalization --- contracts/src/systems/combat/contracts.cairo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index e8925b49a..8af4e1cf5 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -544,6 +544,7 @@ mod combat_systems { } from_army.assert_not_in_battle(); from_army.troops.deduct(troops); + from_army.troops.assert_normalized(); set!(world, (from_army)); // decrease from army health @@ -850,6 +851,7 @@ mod combat_systems { / troop_config.battle_leave_slash_denom.into(), }; army.troops.deduct(troops_deducted); + army.troops.normalize_counts(); let army_health = Health { entity_id: army_id, From f294a65ebc8811191d6f9e8cfd1fa0bfea999674 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 04:07:38 +0100 Subject: [PATCH 08/12] fix ci --- .github/workflows/test-contracts.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index 016d0c0a8..832146b1e 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -58,7 +58,6 @@ jobs: - name: Run Dojo Test for ${{ matrix.test }} run: | cd contracts && sozo test -f ${{ matrix.test }} --print-resource-usage - continue-on-error: true test-scarb-fmt: needs: [setup-environment] From 4f0210644875c7517c7592c6b73dbb5f05ddc3da Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 04:07:51 +0100 Subject: [PATCH 09/12] fix tests --- .../systems/combat/tests/army_buy_test.cairo | 2 +- .../combat/tests/army_create_test.cairo | 2 +- .../combat/tests/battle_leave_test.cairo | 36 +++++++++++++++++-- .../combat/tests/battle_start_test.cairo | 36 +++++++++++++++++-- contracts/src/systems/realm/tests.cairo | 1 + contracts/src/utils/testing/general.cairo | 1 + 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/contracts/src/systems/combat/tests/army_buy_test.cairo b/contracts/src/systems/combat/tests/army_buy_test.cairo index 4e5a38867..03da6e427 100644 --- a/contracts/src/systems/combat/tests/army_buy_test.cairo +++ b/contracts/src/systems/combat/tests/army_buy_test.cairo @@ -58,7 +58,7 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID) { starknet::testing::set_account_contract_address(contract_address_const::()); let realm_id = realm_system_dispatcher - .create(1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: REALM_COORD_X, y: REALM_COORD_Y }); + .create('Mysticora', 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: REALM_COORD_X, y: REALM_COORD_Y }); mint( world, realm_id, diff --git a/contracts/src/systems/combat/tests/army_create_test.cairo b/contracts/src/systems/combat/tests/army_create_test.cairo index 98fe33d55..f3ceebff0 100644 --- a/contracts/src/systems/combat/tests/army_create_test.cairo +++ b/contracts/src/systems/combat/tests/army_create_test.cairo @@ -54,7 +54,7 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID,) { starknet::testing::set_account_contract_address(contract_address_const::()); let realm_id = realm_system_dispatcher - .create(1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: REALM_COORD_X, y: REALM_COORD_Y }); + .create('Mysticora', 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: REALM_COORD_X, y: REALM_COORD_Y }); mint( world, realm_id, diff --git a/contracts/src/systems/combat/tests/battle_leave_test.cairo b/contracts/src/systems/combat/tests/battle_leave_test.cairo index 4fa9577c3..747d59c5c 100644 --- a/contracts/src/systems/combat/tests/battle_leave_test.cairo +++ b/contracts/src/systems/combat/tests/battle_leave_test.cairo @@ -99,7 +99,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_account_contract_address(contract_address_const::()); let player_1_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_1_REALM_COORD_X, y: PLAYER_1_REALM_COORD_Y } + 'Mysticora', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_1_REALM_COORD_X, y: PLAYER_1_REALM_COORD_Y } ); mint( world, @@ -129,7 +139,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_account_contract_address(contract_address_const::()); let player_2_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_2_REALM_COORD_X, y: PLAYER_2_REALM_COORD_Y } + 'Mysticora', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_2_REALM_COORD_X, y: PLAYER_2_REALM_COORD_Y } ); mint( world, @@ -159,7 +179,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_account_contract_address(contract_address_const::()); let player_3_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_3_REALM_COORD_X, y: PLAYER_3_REALM_COORD_Y } + 'Mysticora', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_3_REALM_COORD_X, y: PLAYER_3_REALM_COORD_Y } ); mint( world, diff --git a/contracts/src/systems/combat/tests/battle_start_test.cairo b/contracts/src/systems/combat/tests/battle_start_test.cairo index 039c8a930..b56791839 100644 --- a/contracts/src/systems/combat/tests/battle_start_test.cairo +++ b/contracts/src/systems/combat/tests/battle_start_test.cairo @@ -103,7 +103,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_contract_address(contract_address_const::()); let player_1_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_1_REALM_COORD_X, y: PLAYER_1_REALM_COORD_Y } + 'Mysticora1', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_1_REALM_COORD_X, y: PLAYER_1_REALM_COORD_Y } ); mint( world, @@ -132,7 +142,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_contract_address(contract_address_const::()); let player_2_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_2_REALM_COORD_X, y: PLAYER_2_REALM_COORD_Y } + 'Mysticora2', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_2_REALM_COORD_X, y: PLAYER_2_REALM_COORD_Y } ); mint( world, @@ -161,7 +181,17 @@ fn setup() -> (IWorldDispatcher, ICombatContractDispatcher, ID, ID, ID, ID, ID, starknet::testing::set_contract_address(contract_address_const::()); let player_3_realm_id = realm_system_dispatcher .create( - 1, 1, 1, 1, 1, 1, 1, 1, 1, Position { entity_id: 0, x: PLAYER_3_REALM_COORD_X, y: PLAYER_3_REALM_COORD_Y } + 'Mysticora3', + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + Position { entity_id: 0, x: PLAYER_3_REALM_COORD_X, y: PLAYER_3_REALM_COORD_Y } ); mint( world, diff --git a/contracts/src/systems/realm/tests.cairo b/contracts/src/systems/realm/tests.cairo index 9b67e5c29..ed2bfee08 100644 --- a/contracts/src/systems/realm/tests.cairo +++ b/contracts/src/systems/realm/tests.cairo @@ -84,6 +84,7 @@ fn test_realm_create() { let realm_entity_id = realm_systems_dispatcher .create( + 'Mysticora', realm_id, resource_types_packed, resource_types_count, diff --git a/contracts/src/utils/testing/general.cairo b/contracts/src/utils/testing/general.cairo index e306150e9..e8f0f3230 100644 --- a/contracts/src/utils/testing/general.cairo +++ b/contracts/src/utils/testing/general.cairo @@ -16,6 +16,7 @@ use eternum::utils::map::biomes::Biome; fn spawn_realm(world: IWorldDispatcher, realm_systems_dispatcher: IRealmSystemsDispatcher, position: Position) -> ID { let realm_entity_id = realm_systems_dispatcher .create( + 'Mysticora', 1, // realm id 0x20309, // resource_types_packed // 2,3,9 // stone, coal, gold 3, // resource_types_count From 720afd8b50069e5cab83fabaf0f2cb11456ca460 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 06:04:24 +0100 Subject: [PATCH 10/12] fix tests --- contracts/src/systems/combat/contracts.cairo | 3 --- contracts/src/systems/map/tests.cairo | 6 +++--- .../trade/tests/trade_systems_tests/accept_order.cairo | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index a1b2d3a53..6258c6acc 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -875,9 +875,6 @@ mod combat_systems { // leave battle let mut battle: Battle = get!(world, battle_id, Battle); - - InternalCombatImpl::leave_battle(world, ref battle, ref caller_army); - battle.update_state(); let battle_was_active = (battle.has_started() && !battle.has_ended()); InternalCombatImpl::leave_battle(world, ref battle, ref caller_army); diff --git a/contracts/src/systems/map/tests.cairo b/contracts/src/systems/map/tests.cairo index e1cc25a88..63fd15f59 100644 --- a/contracts/src/systems/map/tests.cairo +++ b/contracts/src/systems/map/tests.cairo @@ -57,9 +57,9 @@ use eternum::utils::testing::{ use starknet::contract_address_const; -const INITIAL_WHEAT_BALANCE: u128 = 1_000_000; -const INITIAL_FISH_BALANCE: u128 = 1_000_000; -const INITIAL_KNIGHT_BALANCE: u128 = 10_000; +const INITIAL_WHEAT_BALANCE: u128 = 10_000_000; +const INITIAL_FISH_BALANCE: u128 = 10_000_000; +const INITIAL_KNIGHT_BALANCE: u128 = 10_000_000; const TIMESTAMP: u64 = 10_000; diff --git a/contracts/src/systems/trade/tests/trade_systems_tests/accept_order.cairo b/contracts/src/systems/trade/tests/trade_systems_tests/accept_order.cairo index ae3690d99..4f2a51e7a 100644 --- a/contracts/src/systems/trade/tests/trade_systems_tests/accept_order.cairo +++ b/contracts/src/systems/trade/tests/trade_systems_tests/accept_order.cairo @@ -226,7 +226,7 @@ fn test_caller_not_taker() { #[available_gas(3000000000000)] #[should_panic( expected: ( - "not enough resources, Resource (entity id: 3, resource type: DONKEY, balance: 0). deduction: 1000", + "not enough resources, Resource (entity id: 4, resource type: DONKEY, balance: 0). deduction: 1000", 'ENTRYPOINT_FAILED' ) )] From cc8ebef1cf81dc41a7c9ccc93245a83f1861a9ef Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 07:23:28 +0100 Subject: [PATCH 11/12] fix tests --- contracts/src/models/combat.cairo | 32 ++++++++++++------- .../combat/tests/battle_leave_test.cairo | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/contracts/src/models/combat.cairo b/contracts/src/models/combat.cairo index be04157f1..efb17757d 100644 --- a/contracts/src/models/combat.cairo +++ b/contracts/src/models/combat.cairo @@ -929,8 +929,8 @@ mod tests { use eternum::models::config::BattleConfig; use eternum::models::config::BattleConfigCustomTrait; use eternum::models::config::CapacityConfigCategory; + use eternum::models::quantity::{Quantity}; use eternum::models::resources::ResourceCustomTrait; - use eternum::models::resources::ResourceTransferLockCustomTrait; use eternum::models::resources::{Resource, ResourceCustomImpl, ResourceTransferLock}; use eternum::utils::testing::world::spawn_eternum; @@ -1163,7 +1163,7 @@ mod tests { starknet::testing::set_block_timestamp(1); // use small army - let attack_troop_each = 500; + let attack_troop_each = 1_000; let defence_troop_each = 10_000; let mut battle = mock_battle(attack_troop_each, defence_troop_each); assert!(battle.duration_left > 0, "duration should be more than 0 "); @@ -1223,6 +1223,11 @@ mod tests { assert!(battle.winner() == BattleSide::Defence, "unexpected side won"); // withdraw back from escrow + let attack_army_left = battle.get_troops_share_left(attack_army); + let attack_army_left_quantity = Quantity { + entity_id: attack_army.entity_id, value: attack_army_left.troops.count().into() + }; + set!(world, (attack_army_left_quantity)); battle.withdraw_balance_and_reward(world, attack_army, attack_army_protectee); // ensure transfer lock was reenabled @@ -1306,7 +1311,12 @@ mod tests { assert!(battle.winner() == BattleSide::None, "unexpected side won"); // withdraw back from escrow - battle.withdraw_balance_and_reward(world, attack_army, attack_army_protectee); + let attack_army_left = battle.get_troops_share_left(attack_army); + let attack_army_left_quantity = Quantity { + entity_id: attack_army.entity_id, value: attack_army_left.troops.count().into() + }; + set!(world, (attack_army_left_quantity)); + battle.withdraw_balance_and_reward(world, attack_army_left, attack_army_protectee); // ensure transfer lock was reenabled let army_transfer_lock: ResourceTransferLock = get!(world, attack_army.entity_id, ResourceTransferLock); @@ -1316,13 +1326,8 @@ mod tests { let attack_army_wheat: Resource = get!(world, (attack_army.entity_id, ResourceTypes::WHEAT), Resource); let attack_army_coal: Resource = get!(world, (attack_army.entity_id, ResourceTypes::COAL), Resource); - assert!( - attack_army_wheat.balance == attack_army_wheat_resource.balance, - "attacking army wheat balance should be > 0" - ); - assert!( - attack_army_coal.balance == attack_army_coal_resource.balance, "attacking army coal balance should be > 0" - ); + assert_eq!(attack_army_wheat.balance, 0); + assert_eq!(attack_army_coal.balance, 0); // ensure attacker got no reward let attack_army_stone: Resource = get!(world, (attack_army.entity_id, ResourceTypes::STONE), Resource); @@ -1404,7 +1409,12 @@ mod tests { assert!(battle.winner() == BattleSide::Attack, "unexpected side won"); // attacker withdraw back from escrow - battle.withdraw_balance_and_reward(world, attack_army, attack_army_protectee); + let attack_army_left = battle.get_troops_share_left(attack_army); + let attack_army_left_quantity = Quantity { + entity_id: attack_army.entity_id, value: attack_army_left.troops.count().into() + }; + set!(world, (attack_army_left_quantity)); + battle.withdraw_balance_and_reward(world, attack_army_left, attack_army_protectee); // ensure transfer lock was reenabled let army_transfer_lock: ResourceTransferLock = get!(world, attack_army.entity_id, ResourceTransferLock); diff --git a/contracts/src/systems/combat/tests/battle_leave_test.cairo b/contracts/src/systems/combat/tests/battle_leave_test.cairo index 547840c40..bbe1fb403 100644 --- a/contracts/src/systems/combat/tests/battle_leave_test.cairo +++ b/contracts/src/systems/combat/tests/battle_leave_test.cairo @@ -290,7 +290,7 @@ fn test_battle_leave_by_winner() { assert_eq!(battle.defence_army_health.current, 0); assert_eq!(battle.defence_army.troops.count(), 0); - assert_eq!(battle.defence_army_lifetime.troops.count(), 0); + assert_eq!(battle.defence_army_lifetime.troops.count().into(), 204 * RESOURCE_PRECISION); } From 29c2dcf9aad81b6d970b7bc537b535cabc4e3747 Mon Sep 17 00:00:00 2001 From: Credence Date: Sun, 15 Sep 2024 22:47:27 +0100 Subject: [PATCH 12/12] contracts: only lock structure resources when battle starts --- client/src/dojo/contractComponents.ts | 4 +- contracts/src/models/combat.cairo | 47 +++++++++++++++----- contracts/src/models/resources.cairo | 6 +++ contracts/src/systems/combat/contracts.cairo | 15 ++++++- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/client/src/dojo/contractComponents.ts b/client/src/dojo/contractComponents.ts index 939b28a5f..21a413d28 100644 --- a/client/src/dojo/contractComponents.ts +++ b/client/src/dojo/contractComponents.ts @@ -983,12 +983,12 @@ export function defineContractComponents(world: World) { ResourceTransferLock: (() => { return defineComponent( world, - { entity_id: RecsType.Number, release_at: RecsType.BigInt }, + { entity_id: RecsType.Number, start_at: RecsType.BigInt, release_at: RecsType.BigInt }, { metadata: { namespace: "eternum", name: "ResourceTransferLock", - types: ["u32", "u64"], + types: ["u32", "u64", "u64"], customTypes: [], }, }, diff --git a/contracts/src/models/combat.cairo b/contracts/src/models/combat.cairo index efb17757d..a384e4b33 100644 --- a/contracts/src/models/combat.cairo +++ b/contracts/src/models/combat.cairo @@ -466,7 +466,7 @@ impl ProtecteeCustomImpl of ProtecteeCustomTrait { self.protectee_id != 0 } - fn protected_resources_holder(self: Protectee) -> ID { + fn protected_entity(self: Protectee) -> ID { if self.is_other() { self.protectee_id } else { @@ -522,9 +522,30 @@ impl BattleSideIntoFelt252 of Into { #[generate_trait] impl BattleEscrowImpl of BattleEscrowTrait { + fn deposit_lock_immediately(ref self: Battle, world: IWorldDispatcher, army_protectee: Protectee) { + let army_protectee_id = army_protectee.protected_entity(); + let mut army_resource_lock: ResourceTransferLock = get!(world, army_protectee_id, ResourceTransferLock); + army_resource_lock.assert_not_locked(); + + let now = starknet::get_block_timestamp(); + assert!(army_resource_lock.start_at > now, "wrong lock invariant (1)"); + assert!(army_resource_lock.release_at > now, "wrong lock invariant (2)"); + + army_resource_lock.start_at = now; + set!(world, (army_resource_lock)); + } + + fn deposit_balance(ref self: Battle, world: IWorldDispatcher, from_army: Army, from_army_protectee: Protectee) { - let from_army_protectee_id = from_army_protectee.protected_resources_holder(); + let from_army_protectee_id = from_army_protectee.protected_entity(); let from_army_protectee_is_self: bool = !get!(world, from_army_protectee_id, Structure).is_structure(); + + // ensure resources were not previously locked + let mut from_army_resource_lock: ResourceTransferLock = get!( + world, from_army_protectee_id, ResourceTransferLock + ); + from_army_resource_lock.assert_not_locked(); + if from_army_protectee_is_self { // detail items locked in box let escrow_id = match from_army.battle_side { @@ -552,15 +573,18 @@ impl BattleEscrowImpl of BattleEscrowTrait { Option::None => { break; } } }; - } - // lock the resources protected by the army - let mut from_army_resource_lock: ResourceTransferLock = get!( - world, from_army_protectee_id, ResourceTransferLock - ); - from_army_resource_lock.assert_not_locked(); - from_army_resource_lock.release_at = Bounded::MAX; - set!(world, (from_army_resource_lock)); + // lock the resources protected by the army immediately + from_army_resource_lock.start_at = starknet::get_block_timestamp(); + from_army_resource_lock.release_at = Bounded::MAX; + set!(world, (from_army_resource_lock)); + } else { + // lock resources of the entity being protected starting from + // when the battle starts to account for battle.start_at delay + from_army_resource_lock.start_at = self.start_at; + from_army_resource_lock.release_at = Bounded::MAX; + set!(world, (from_army_resource_lock)); + } } fn withdraw_balance_and_reward( @@ -572,7 +596,7 @@ impl BattleEscrowImpl of BattleEscrowTrait { BattleSide::Defence => { (self.defenders_resources_escrow_id, self.attackers_resources_escrow_id) } }; - let to_army_protectee_id = to_army_protectee.protected_resources_holder(); + let to_army_protectee_id = to_army_protectee.protected_entity(); let to_army_protectee_is_self: bool = !get!(world, to_army_protectee_id, Structure).is_structure(); let winner_side: BattleSide = self.winner(); @@ -660,6 +684,7 @@ impl BattleEscrowImpl of BattleEscrowTrait { // release lock on resource let mut to_army_resource_lock: ResourceTransferLock = get!(world, to_army_protectee_id, ResourceTransferLock); to_army_resource_lock.assert_locked(); + to_army_resource_lock.start_at = starknet::get_block_timestamp(); to_army_resource_lock.release_at = starknet::get_block_timestamp(); set!(world, (to_army_resource_lock)); } diff --git a/contracts/src/models/resources.cairo b/contracts/src/models/resources.cairo index e73a4e659..c09a99071 100644 --- a/contracts/src/models/resources.cairo +++ b/contracts/src/models/resources.cairo @@ -90,6 +90,7 @@ pub struct OwnedResourcesTracker { pub struct ResourceTransferLock { #[key] entity_id: ID, + start_at: u64, release_at: u64, } @@ -106,6 +107,11 @@ impl ResourceTransferLockCustomImpl of ResourceTransferLockCustomTrait { fn is_open(self: ResourceTransferLock) -> bool { let now = starknet::get_block_timestamp(); + if self.start_at.is_non_zero() { + if now < self.start_at { + return true; + } + } now >= self.release_at } } diff --git a/contracts/src/systems/combat/contracts.cairo b/contracts/src/systems/combat/contracts.cairo index 8af4e1cf5..d3881d80c 100644 --- a/contracts/src/systems/combat/contracts.cairo +++ b/contracts/src/systems/combat/contracts.cairo @@ -678,8 +678,9 @@ mod combat_systems { battle.defence_army_health = defending_army_health.into(); battle.last_updated = now; battle.start_at = now; + // add battle start time delay when a structure is being attacked. + // if the structure is the attacker, the battle starts immediately if defending_army_protectee.is_other() { - // add delay when a structure is being attacked battle.start_at = now + battle_config.battle_delay_seconds; } @@ -735,12 +736,17 @@ mod combat_systems { assert!(defending_army.battle_id == battle_id, "army is not in battle"); assert!(defending_army.battle_side == BattleSide::Defence, "army is not on defensive"); + let mut defending_army_protectee: Protectee = get!(world, defending_army_id, Protectee); + // this condition should not be possible unless there is a bug in `battle_start` + assert!(defending_army_protectee.is_other(), "only structures can force start"); + let now = starknet::get_block_timestamp(); let mut battle = BattleCustomImpl::get(world, battle_id); assert!(now < battle.start_at, "Battle already started"); // update battle battle.start_at = now; + battle.deposit_lock_immediately(world, defending_army_protectee); set!(world, (battle)); } @@ -1344,7 +1350,12 @@ mod combat_systems { set!(world, (structure_protector, Protectee { army_id, protectee_id: army_owner_id })); // stop the army from sending or receiving resources - set!(world, (ResourceTransferLock { entity_id: army_id, release_at: Bounded::MAX })); + set!( + world, + (ResourceTransferLock { + entity_id: army_id, start_at: starknet::get_block_timestamp(), release_at: Bounded::MAX + }) + ); army_id }