From 8474dd9c8e47e78180447b882d646c4a1be36f51 Mon Sep 17 00:00:00 2001 From: loothero <100039621+loothero@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:24:51 -0500 Subject: [PATCH] reduce start delay blocks (#559) * allows clients to optimistically provide contract with the block hash for start_block + 1 * contract will verify provided hash was correct as part of highscore check and prior to minting collectible beast * contract provides a public slay_invalid_adventurers that can be used to slay adventurers started with wrong hash * this change is backwards compatible with existing clients. Calling attack() after 11 blocks will work same as it did previously but now clients can call set_starting_entropy(adventurer_id, block_hash) when start_block + 1 hash is available and proceed to call attack() immediately after. --- Scarb.lock | 2 +- contracts/adventurer/src/adventurer.cairo | 21 +++ contracts/game/src/game/constants.cairo | 5 + contracts/game/src/game/interfaces.cairo | 5 + contracts/game/src/lib.cairo | 158 ++++++++++++++-- contracts/game/src/tests/test_game.cairo | 209 +++++++++++++++++----- 6 files changed, 339 insertions(+), 61 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index e429338be..5f8efa234 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "openzeppelin" version = "0.9.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#861fc416f87addbe23a3b47f9d19ab27c10d5dc8" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#364db5b1aecc1335d2e65db887291d19aa28937d" [[package]] name = "survivor" diff --git a/contracts/adventurer/src/adventurer.cairo b/contracts/adventurer/src/adventurer.cairo index 150a943e4..a23df1299 100644 --- a/contracts/adventurer/src/adventurer.cairo +++ b/contracts/adventurer/src/adventurer.cairo @@ -1701,6 +1701,17 @@ impl ImplAdventurer of IAdventurer { hash_span.append(start_hash); poseidon_hash_span(hash_span.span()) } + + /// @title invalidate_adventurer + /// @notice This function invalidates an adventurer by setting its xp to 1 and gold to 0. + /// @dev This function directly modifies the state of the adventurer. + /// @param self The Adventurer struct instance to be invalidated. + #[inline(always)] + fn invalidate_game(ref self: Adventurer) { + self.health = 0; + self.xp = 1; + self.gold = 0; + } } @@ -5255,4 +5266,14 @@ mod tests { assert(adventurer.is_ambushed(5), 'should be ambushed 5'); assert(!adventurer.is_ambushed(6), 'should not be ambushed 6'); } + + #[test] + #[available_gas(1000000)] + fn test_invalidate_game() { + let mut adventurer = ImplAdventurer::new(ItemId::Wand); + adventurer.invalidate_game(); + assert(adventurer.health == 0, 'adventurer health should be 0'); + assert(adventurer.xp == 1, 'adventurer xp should be 1'); + assert(adventurer.gold == 0, 'adventurer gold should be 0'); + } } diff --git a/contracts/game/src/game/constants.cairo b/contracts/game/src/game/constants.cairo index 48486e7f6..c5094d1a7 100644 --- a/contracts/game/src/game/constants.cairo +++ b/contracts/game/src/game/constants.cairo @@ -31,6 +31,11 @@ mod messages { const NOT_OWNER_OF_TOKEN: felt252 = 'Not owner of token'; const MA_PERIOD_LESS_THAN_WEEK: felt252 = 'MA period too small'; const TERMINAL_TIME_REACHED: felt252 = 'terminal time reached'; + const STARTING_ENTROPY_ALREADY_SET: felt252 = 'starting entropy already set'; + const STARTING_ENTROPY_ZERO: felt252 = 'block hash should not be zero'; + const GAME_ALREADY_STARTED: felt252 = 'game already started'; + const STARTING_ENTROPY_IS_VALID: felt252 = 'starting entropy is valid'; + const VALID_BLOCK_HASH_UNAVAILABLE: felt252 = 'valid hash not yet available'; } // TODO: Update for mainnet diff --git a/contracts/game/src/game/interfaces.cairo b/contracts/game/src/game/interfaces.cairo index 8ae13d872..16670860e 100644 --- a/contracts/game/src/game/interfaces.cairo +++ b/contracts/game/src/game/interfaces.cairo @@ -16,6 +16,7 @@ trait IGame { fn new_game( ref self: TContractState, client_reward_address: ContractAddress, weapon: u8, name: u128, golden_token_id: u256, interface_camel: bool ); + fn set_starting_entropy(ref self: TContractState, adventurer_id: felt252, block_hash: felt252); fn explore(ref self: TContractState, adventurer_id: felt252, till_beast: bool); fn attack(ref self: TContractState, adventurer_id: felt252, to_the_death: bool); fn flee(ref self: TContractState, adventurer_id: felt252, to_the_death: bool); @@ -29,6 +30,7 @@ trait IGame { items: Array, ); fn slay_idle_adventurers(ref self: TContractState, adventurer_ids: Array); + fn slay_invalid_adventurers(ref self: TContractState, adventurer_ids: Array); fn rotate_game_entropy(ref self: TContractState); fn update_cost_to_play(ref self: TContractState); fn initiate_price_change(ref self: TContractState); @@ -39,6 +41,8 @@ trait IGame { fn get_adventurer(self: @TContractState, adventurer_id: felt252) -> Adventurer; fn get_adventurer_no_boosts(self: @TContractState, adventurer_id: felt252) -> Adventurer; fn get_adventurer_meta(self: @TContractState, adventurer_id: felt252) -> AdventurerMetadata; + fn get_adventurer_starting_entropy(self: @TContractState, adventurer_id: felt252) -> felt252; + fn get_adventurer_entropy(self: @TContractState, adventurer_id: felt252) -> felt252; fn get_health(self: @TContractState, adventurer_id: felt252) -> u16; fn get_xp(self: @TContractState, adventurer_id: felt252) -> u16; fn get_level(self: @TContractState, adventurer_id: felt252) -> u8; @@ -48,6 +52,7 @@ trait IGame { fn get_actions_per_block(self: @TContractState, adventurer_id: felt252) -> u8; fn get_reveal_block(self: @TContractState, adventurer_id: felt252) -> u64; fn is_idle(self: @TContractState, adventurer_id: felt252) -> (bool, u16); + fn get_contract_calculated_entropy(self: @TContractState, adventurer_id: felt252) -> felt252; // adventurer stats (includes boost) fn get_stats(self: @TContractState, adventurer_id: felt252) -> Stats; diff --git a/contracts/game/src/lib.cairo b/contracts/game/src/lib.cairo index 203fb87e5..7fb6cac40 100644 --- a/contracts/game/src/lib.cairo +++ b/contracts/game/src/lib.cairo @@ -117,6 +117,7 @@ mod Game { _cost_to_play: u128, _games_played_snapshot: GamesPlayedSnapshot, _terminal_timestamp: u64, + _starting_entropy: LegacyMap::, } #[event] @@ -224,6 +225,31 @@ mod Game { _start_game(ref self, weapon, name, interface_camel); } + fn set_starting_entropy( + ref self: ContractState, adventurer_id: felt252, block_hash: felt252 + ) { + // only owner of the adventurer can set starting entropy + _assert_ownership(@self, adventurer_id); + + // player can only call this before starting game (defeating starter beast) + assert(_load_adventurer(@self, adventurer_id).xp == 0, messages::GAME_ALREADY_STARTED); + + // prevent client from trying to set start entropy prior to valid block hash being available + _assert_valid_block_hash_available(@self, adventurer_id); + + // verify the block_hash is not zero + assert(block_hash != 0, messages::STARTING_ENTROPY_ZERO); + + // starting entropy can only be set once + assert( + self._starting_entropy.read(adventurer_id) == 0, + messages::STARTING_ENTROPY_ALREADY_SET + ); + + // save starting entropy + self._starting_entropy.write(adventurer_id, block_hash); + } + /// @title Explore Function /// /// @notice Allows an adventurer to explore @@ -433,7 +459,7 @@ mod Game { // if adventurer died while attempting to flee, process death if adventurer.health == 0 { - _process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0); } } else { _apply_idle_penalty(ref self, adventurer_id, ref adventurer, num_blocks); @@ -490,7 +516,7 @@ mod Game { // if adventurer died from counter attack, process death if (adventurer.health == 0) { - _process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0); } } @@ -654,6 +680,19 @@ mod Game { } } + fn slay_invalid_adventurers(ref self: ContractState, adventurer_ids: Array) { + let mut adventurer_index: u32 = 0; + loop { + if adventurer_index == adventurer_ids.len() { + break; + } + let adventurer_id = *adventurer_ids.at(adventurer_index); + _slay_invalid_adventurer(ref self, adventurer_id); + adventurer_index += 1; + } + } + + /// @title Rotate Game Entropy Function /// /// @notice Rotates the game entropy @@ -687,6 +726,19 @@ mod Game { fn get_adventurer_meta(self: @ContractState, adventurer_id: felt252) -> AdventurerMetadata { _load_adventurer_metadata(self, adventurer_id) } + fn get_adventurer_starting_entropy( + self: @ContractState, adventurer_id: felt252 + ) -> felt252 { + self._starting_entropy.read(adventurer_id) + } + fn get_contract_calculated_entropy( + self: @ContractState, adventurer_id: felt252 + ) -> felt252 { + _get_starting_block_hash(self, adventurer_id) + } + fn get_adventurer_entropy(self: @ContractState, adventurer_id: felt252) -> felt252 { + _get_adventurer_entropy(self, adventurer_id) + } fn get_bag(self: @ContractState, adventurer_id: felt252) -> Bag { _load_bag(self, adventurer_id) } @@ -1077,7 +1129,27 @@ mod Game { adventurer.health = 0; // handle adventurer death - _process_adventurer_death(ref self, adventurer, adventurer_id, 0, 0,); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0,); + + // save adventurer (gg) + _save_adventurer_no_boosts(ref self, adventurer, adventurer_id); + } + + fn _slay_invalid_adventurer(ref self: ContractState, adventurer_id: felt252) { + // unpack adventurer from storage (no need for stat boosts) + let mut adventurer = _load_adventurer_no_boosts(@self, adventurer_id); + + // assert adventurer is not already dead + _assert_not_dead(adventurer); + + // assert adventurer is invalid + _assert_invalid_starting_entropy(@self, adventurer_id); + + // slay adventurer by setting health to 0 + adventurer.health = 0; + + // handle adventurer death + _process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0,); // save adventurer (gg) _save_adventurer_no_boosts(ref self, adventurer, adventurer_id); @@ -1150,8 +1222,15 @@ mod Game { owner_address, _load_adventurer_metadata(@self, adventurer_id).interface_camel ); - // adventurers gets the beast - _mint_beast(@self, beast, primary_address); + // check if starting entropy is valid + if _is_starting_entropy_valid(@self, adventurer_id) { + // if starting entropy is valid, mint the beast + _mint_beast(@self, beast, primary_address); + } else { + // if starting entropy is not valid, kill adventurer + adventurer.health = 0; + _process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0); + } } } @@ -1212,7 +1291,7 @@ mod Game { fn _process_adventurer_death( ref self: ContractState, - adventurer: Adventurer, + ref adventurer: Adventurer, adventurer_id: felt252, beast_id: u8, obstacle_id: u8 @@ -1229,8 +1308,47 @@ mod Game { __event_AdventurerDied(ref self, AdventurerDied { adventurer_state, death_details }); - if _is_top_score(@self, adventurer.xp) { - _update_leaderboard(ref self, adventurer_id, adventurer); + // if starting entropy is valid + if _is_starting_entropy_valid(@self, adventurer_id) { + // and adventurer got a top score + if _is_top_score(@self, adventurer.xp) { + // update the leaderboard + _update_leaderboard(ref self, adventurer_id, adventurer); + } + } else { + // if starting entropy is not valid we invalidate adventurer's game + adventurer.invalidate_game(); + } + } + + fn _assert_invalid_starting_entropy(self: @ContractState, adventurer_id: felt252) { + assert( + !_is_starting_entropy_valid(self, adventurer_id), messages::STARTING_ENTROPY_IS_VALID + ); + } + + // @title Assert Valid Block Hash Availability + // @notice This function asserts that a valid block hash is available for the given adventurer. + // @dev If this function is called prior to 2 blocks from the start of the game for the given adventurer, an exception will be thrown. + // @param adventurer_id The ID of the adventurer for which to check the block hash availability. + fn _assert_valid_block_hash_available(self: @ContractState, adventurer_id: felt252) { + let current_block = starknet::get_block_info().unbox().block_number; + let adventurer_start_block = _load_adventurer_metadata(self, adventurer_id).start_block; + let block_hash_available = adventurer_start_block + 2; + assert(current_block >= block_hash_available, messages::VALID_BLOCK_HASH_UNAVAILABLE); + } + + // @title Check if starting entropy is valid + // @notice This function checks if the starting entropy provided to the contract is equal to the entropy generated by the contract based on the block hash of the block after the player committed to playing the game. + // @dev If no manual starting entropy was provided, the starting entropy is considered valid. If a starting entropy was manually provided, it is compared with the block hash of the block after the player committed to playing the game. + // @param adventurer_id The ID of the adventurer for which to check the starting entropy. + // @return Returns true if the starting entropy is valid, false otherwise. + fn _is_starting_entropy_valid(self: @ContractState, adventurer_id: felt252) -> bool { + let starting_entropy = self._starting_entropy.read(adventurer_id); + if starting_entropy == 0 { + true + } else { + starting_entropy == _get_starting_block_hash(self, adventurer_id) } } @@ -1575,7 +1693,7 @@ mod Game { ); __event_AmbushedByBeast(ref self, adventurer, adventurer_id, beast_battle_details); if (adventurer.health == 0) { - _process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0); return; } } else { @@ -1634,7 +1752,7 @@ mod Game { ref self, adventurer, adventurer_id, dodged, obstacle_details ); // process death - _process_adventurer_death(ref self, adventurer, adventurer_id, 0, obstacle.id); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, obstacle.id); // return without granting xp to adventurer or items return; } @@ -1858,7 +1976,7 @@ mod Game { // if adventurer is dead if (adventurer.health == 0) { - _process_adventurer_death(ref self, adventurer, adventurer_id, beast.id, 0); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, beast.id, 0); return; } @@ -2740,7 +2858,7 @@ mod Game { __event_IdleDeathPenalty(ref self, adventurer, adventurer_id, idle_blocks); // process adventurer death - _process_adventurer_death(ref self, adventurer, adventurer_id, 0, 0); + _process_adventurer_death(ref self, ref adventurer, adventurer_id, 0, 0); } #[inline(always)] fn _lords_address(self: @ContractState) -> ContractAddress { @@ -2875,10 +2993,22 @@ mod Game { #[inline(always)] fn _get_adventurer_entropy(self: @ContractState, adventurer_id: felt252) -> felt252 { - // get the block the adventurer started the game on + // if player used optimistic start and manually set starting hash + let starting_entropy = self._starting_entropy.read(adventurer_id); + if starting_entropy != 0 { + // use it + starting_entropy + } else { + // otherwise use block hash + _get_starting_block_hash(self, adventurer_id) + } + } + + fn _get_starting_block_hash(self: @ContractState, adventurer_id: felt252) -> felt252 { + // otherwise we'll get start entropy from the block hash after the start block let start_block = _load_adventurer_metadata(self, adventurer_id).start_block; - // adventurer_ + // don't force block delay on testnet to remove this source of friction let chain_id = starknet::get_execution_info().unbox().tx_info.unbox().chain_id; if chain_id == MAINNET_CHAIN_ID { _get_mainnet_entropy(adventurer_id, start_block) diff --git a/contracts/game/src/tests/test_game.cairo b/contracts/game/src/tests/test_game.cairo index 5869e485f..b3f7acd52 100644 --- a/contracts/game/src/tests/test_game.cairo +++ b/contracts/game/src/tests/test_game.cairo @@ -282,10 +282,17 @@ mod tests { game } - fn new_adventurer_lvl2(starting_block: u64, starting_time: u64) -> IGameDispatcher { + fn new_adventurer_lvl2( + starting_block: u64, starting_time: u64, starting_entropy: felt252 + ) -> IGameDispatcher { // start game let mut game = new_adventurer(starting_block, starting_time); + if (starting_entropy != 0) { + testing::set_block_number(starting_block + 2); + game.set_starting_entropy(ADVENTURER_ID, starting_entropy); + } + // attack starter beast game.attack(ADVENTURER_ID, false); @@ -299,36 +306,33 @@ mod tests { game } - fn new_adventurer_lvl3(starting_block: u64) -> IGameDispatcher { - // start game on lvl 2 - let starting_time = 1696201757; - let mut game = new_adventurer_lvl2(starting_block, starting_time); + fn new_adventurer_lvl3( + starting_block: u64, starting_time: u64, starting_entropy: felt252 + ) -> IGameDispatcher { + let mut game = new_adventurer_lvl2(starting_block, starting_time, starting_entropy); let shopping_cart = ArrayTrait::::new(); let stat_upgrades = Stats { strength: 0, dexterity: 0, vitality: 0, intelligence: 0, wisdom: 0, charisma: 1, luck: 0 }; - game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart); + game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart.clone()); // go explore game.explore(ADVENTURER_ID, true); - game.flee(ADVENTURER_ID, false); - game.explore(ADVENTURER_ID, true); - game.flee(ADVENTURER_ID, false); - game.explore(ADVENTURER_ID, true); - game.flee(ADVENTURER_ID, false); - game.explore(ADVENTURER_ID, true); + game.flee(ADVENTURER_ID, true); let adventurer = game.get_adventurer(ADVENTURER_ID); assert(adventurer.get_level() == 3, 'adventurer should be lvl 3'); + game.upgrade(ADVENTURER_ID, 0, stat_upgrades, shopping_cart); // return game game } fn new_adventurer_lvl4(stat: u8) -> IGameDispatcher { - // start game on lvl 2 - let mut game = new_adventurer_lvl3(123); + // start game on lvl 4 + let starting_time = 1696201757; + let mut game = new_adventurer_lvl3(123, starting_time, 0); // upgrade charisma let shopping_cart = ArrayTrait::::new(); @@ -700,7 +704,7 @@ mod tests { #[available_gas(63000000)] fn test_cant_flee_outside_battle() { // start adventuer and advance to level 2 - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // attempt to flee despite not being in a battle // this should trigger a panic 'Not in battle' which is @@ -712,7 +716,7 @@ mod tests { #[available_gas(13000000000)] fn test_flee() { // start game on level 2 - let mut game = new_adventurer_lvl2(1003, 1696201757); + let mut game = new_adventurer_lvl2(1003, 1696201757, 0); // perform upgrade let shopping_cart = ArrayTrait::::new(); @@ -784,7 +788,7 @@ mod tests { #[available_gas(73000000)] fn test_buy_items_without_stat_upgrade() { // mint adventurer and advance to level 2 - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get valid item from market let market_items = @game.get_items_on_market(ADVENTURER_ID); @@ -810,7 +814,7 @@ mod tests { #[available_gas(62000000)] fn test_buy_duplicate_item_equipped() { // start new game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get items from market let market_items = @game.get_items_on_market_by_tier(ADVENTURER_ID, 5); @@ -834,7 +838,7 @@ mod tests { #[available_gas(61000000)] fn test_buy_duplicate_item_bagged() { // start new game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get items from market let market_items = @game.get_items_on_market(ADVENTURER_ID); @@ -856,7 +860,7 @@ mod tests { #[should_panic(expected: ('Market item does not exist', 'ENTRYPOINT_FAILED'))] #[available_gas(65000000)] fn test_buy_item_not_on_market() { - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); let mut shopping_cart = ArrayTrait::::new(); shopping_cart.append(ItemPurchase { item_id: 255, equip: false }); let stat_upgrades = Stats { @@ -868,7 +872,7 @@ mod tests { #[test] #[available_gas(65000000)] fn test_buy_and_bag_item() { - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); let market_items = @game.get_items_on_market(ADVENTURER_ID); let item_id = *market_items.at(0); let mut shopping_cart = ArrayTrait::::new(); @@ -885,7 +889,7 @@ mod tests { #[available_gas(71000000)] fn test_buy_items() { // start game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get items from market let market_items = @game.get_items_on_market(ADVENTURER_ID); @@ -1029,7 +1033,7 @@ mod tests { #[available_gas(92000000)] fn test_equip() { // start game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1002, 1696201757); + let mut game = new_adventurer_lvl2(1002, 1696201757, 0); // get items from market let market_items = @game.get_items_on_market_by_tier(ADVENTURER_ID, 5); @@ -1149,7 +1153,7 @@ mod tests { #[test] #[available_gas(100000000)] fn test_buy_potions() { - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get updated adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); @@ -1183,7 +1187,7 @@ mod tests { #[should_panic(expected: ('Health already full', 'ENTRYPOINT_FAILED'))] #[available_gas(450000000)] fn test_buy_potions_exceed_max_health() { - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get updated adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); @@ -1210,7 +1214,7 @@ mod tests { #[available_gas(100000000)] fn test_cant_buy_potion_without_stat_upgrade() { // deploy and start new game - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // upgrade adventurer let shopping_cart = ArrayTrait::::new(); @@ -1499,7 +1503,7 @@ mod tests { let adventurer_level = game.get_adventurer(ADVENTURER_ID).get_level(); assert(potion_price == POTION_PRICE * adventurer_level.into(), 'wrong lvl1 potion price'); - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); let potion_price = game.get_potion_price(ADVENTURER_ID); let adventurer_level = game.get_adventurer(ADVENTURER_ID).get_level(); assert(potion_price == POTION_PRICE * adventurer_level.into(), 'wrong lvl2 potion price'); @@ -1786,7 +1790,7 @@ mod tests { #[available_gas(83000000)] fn test_drop_item() { // start new game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get items from market let market_items = @game.get_items_on_market(ADVENTURER_ID); @@ -1840,7 +1844,7 @@ mod tests { #[available_gas(90000000)] fn test_drop_item_without_ownership() { // start new game on level 2 so we have access to the market - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // intialize an array with 20 items in it let mut drop_list = ArrayTrait::::new(); @@ -1856,7 +1860,7 @@ mod tests { #[available_gas(75000000)] fn test_upgrade_stats() { // deploy and start new game - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // get adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); @@ -1885,7 +1889,7 @@ mod tests { #[available_gas(70000000)] fn test_upgrade_stats_not_enough_points() { // deploy and start new game - let mut game = new_adventurer_lvl2(1000, 1696201757); + let mut game = new_adventurer_lvl2(1000, 1696201757, 0); // try to upgrade charisma x2 with only 1 stat available let shopping_cart = ArrayTrait::::new(); @@ -1899,7 +1903,7 @@ mod tests { #[available_gas(75000000)] fn test_upgrade_adventurer() { // deploy and start new game - let mut game = new_adventurer_lvl2(1006, 1696201757); + let mut game = new_adventurer_lvl2(1006, 1696201757, 0); // get original adventurer state let adventurer = game.get_adventurer(ADVENTURER_ID); @@ -1967,7 +1971,7 @@ mod tests { fn test_exceed_rate_limit() { let starting_block = 388630; let starting_time = 1699532315; - let mut game = new_adventurer_lvl2(starting_block, starting_time); + let mut game = new_adventurer_lvl2(starting_block, starting_time, 0); testing::set_block_number(starting_block + 1); testing::set_block_timestamp(starting_time + 15); @@ -2022,7 +2026,7 @@ mod tests { #[available_gas(944417814)] fn test_exceed_rate_limit_block_rotation() { let starting_block = 1003; - let mut game = new_adventurer_lvl2(starting_block, 1696201757); + let mut game = new_adventurer_lvl2(starting_block, 1696201757, 0); let shopping_cart = ArrayTrait::::new(); let stat_upgrades = Stats { strength: 0, dexterity: 0, vitality: 0, intelligence: 0, wisdom: 0, charisma: 1, luck: 0 @@ -2238,9 +2242,7 @@ mod tests { let starting_block = 364063; let starting_timestamp = 1698678554; let terminal_timestamp = 0; - let (mut game, _, _, _) = setup( - starting_block, starting_timestamp, terminal_timestamp - ); + let (mut game, _, _, _) = setup(starting_block, starting_timestamp, terminal_timestamp); add_adventurer_to_game(ref game, 1); testing::set_block_timestamp(starting_timestamp + DAY); add_adventurer_to_game(ref game, 1); @@ -2253,9 +2255,7 @@ mod tests { let starting_block = 364063; let starting_timestamp = 1698678554; let terminal_timestamp = 0; - let (mut game, _, _, _) = setup( - starting_block, starting_timestamp, terminal_timestamp - ); + let (mut game, _, _, _) = setup(starting_block, starting_timestamp, terminal_timestamp); assert(game.can_play(1), 'should be able to play'); add_adventurer_to_game(ref game, golden_token_id); assert(!game.can_play(1), 'should not be able to play'); @@ -2273,9 +2273,7 @@ mod tests { let starting_block = 364063; let starting_timestamp = 1698678554; let terminal_timestamp = 0; - let (mut game, _, _, _) = setup( - starting_block, starting_timestamp, terminal_timestamp - ); + let (mut game, _, _, _) = setup(starting_block, starting_timestamp, terminal_timestamp); add_adventurer_to_game(ref game, golden_token_id); } @@ -2287,9 +2285,7 @@ mod tests { let starting_block = 364063; let starting_timestamp = 1698678554; let terminal_timestamp = 0; - let (mut game, _, _, _) = setup( - starting_block, starting_timestamp, terminal_timestamp - ); + let (mut game, _, _, _) = setup(starting_block, starting_timestamp, terminal_timestamp); add_adventurer_to_game(ref game, golden_token_id); // roll blockchain forward 1 second less than a day @@ -2323,4 +2319,125 @@ mod tests { let (is_idle, _) = game.is_idle(ADVENTURER_ID); assert(is_idle, 'should be idle'); } + + + #[test] + #[should_panic(expected: ('Not authorized to act', 'ENTRYPOINT_FAILED'))] + fn test_set_starting_entropy_not_owner() { + let mut game = new_adventurer(1000, 1696201757); + testing::set_block_number(1002); + // change to different caller + testing::set_contract_address(contract_address_const::<50>()); + // try to set starting entropy, should revert + game.set_starting_entropy(ADVENTURER_ID, 1); + } + + #[test] + #[should_panic(expected: ('game already started', 'ENTRYPOINT_FAILED'))] + fn test_set_starting_entropy_game_started() { + let mut game = new_adventurer(1000, 1696201757); + testing::set_block_number(1002); + // defeat starter beast + game.attack(ADVENTURER_ID, true); + // then attempt to set starting entropy, should revert + game.set_starting_entropy(ADVENTURER_ID, 1); + } + + + #[test] + #[should_panic(expected: ('valid hash not yet available', 'ENTRYPOINT_FAILED'))] + fn test_set_starting_entropy_before_hash_available() { + let mut game = new_adventurer(1000, 1696201757); + // attempt to set starting entropy before hash is available, should revert + game.set_starting_entropy(ADVENTURER_ID, 1); + } + + #[test] + #[should_panic(expected: ('block hash should not be zero', 'ENTRYPOINT_FAILED'))] + fn test_set_starting_entropy_zero_hash() { + let mut game = new_adventurer(1000, 1696201757); + testing::set_block_number(1002); + // attempt to pass in 0 for starting entropy hash, should revert + game.set_starting_entropy(ADVENTURER_ID, 0); + } + + #[test] + #[should_panic(expected: ('starting entropy already set', 'ENTRYPOINT_FAILED'))] + fn test_set_starting_entropy_double_call() { + let mut game = new_adventurer(1000, 1696201757); + testing::set_block_number(1002); + // attempt to set starting entropy twice, should revert + game.set_starting_entropy(ADVENTURER_ID, 1); + game.set_starting_entropy(ADVENTURER_ID, 1); + } + + #[test] + fn test_set_starting_entropy_basic() { + let mut game = new_adventurer(1000, 1696201757); + testing::set_block_number(1002); + game.set_starting_entropy(ADVENTURER_ID, 123); + // verify starting entropy was set + assert( + game.get_adventurer_starting_entropy(ADVENTURER_ID) == 123, 'wrong starting entropy' + ); + // verify adventurer entropy is using starting entropy + assert(game.get_adventurer_entropy(ADVENTURER_ID) == 123, 'wrong adventurer entropy'); + } + + #[test] + fn test_set_starting_entropy_wrong_hash() { + let wrong_starting_entropy = 12345678910112; + let mut game = new_adventurer_lvl3(1000, 1696201757, wrong_starting_entropy); + testing::set_block_number(1002); + + // go out exploring till beast + game.explore(ADVENTURER_ID, true); + + // record adventurer before death + let pre_death_adventurer = game.get_adventurer(ADVENTURER_ID); + assert(pre_death_adventurer.xp > 0, 'adventurer should have xp'); + assert(pre_death_adventurer.gold > 0, 'adventurer should have gold'); + + // attack beast till death + game.attack(ADVENTURER_ID, true); + + // adventurer died attacking beast + let post_death_adventurer = game.get_adventurer(ADVENTURER_ID); + // because starting entropy was wrong, adventurer's xp and gold should be reset + assert(post_death_adventurer.health == 0, 'adventurer should be dead'); + assert(post_death_adventurer.xp == 1, 'adventurer should have 1 xp'); + assert(post_death_adventurer.gold == 0, 'adventurer should have 0 gold'); + } + + // test slay_invalid_adventurers on adventurer who didn't use optimistic start + #[test] + #[should_panic(expected: ('starting entropy is valid', 'ENTRYPOINT_FAILED'))] + fn test_slay_invalid_adventurer_no_manual_entropy() { + let mut game = new_adventurer_lvl3(1000, 1696201757, 0); + let invalid_adventurers = array![ADVENTURER_ID]; + game.slay_invalid_adventurers(invalid_adventurers); + } + + // test slay_invalid_adventurers on adventurer who used optimistic start with correct hash + #[test] + #[should_panic(expected: ('starting entropy is valid', 'ENTRYPOINT_FAILED'))] + fn test_slay_invalid_adventurer_correct_entropy() { + let mut game = new_adventurer_lvl3( + 1000, 1696201757, 0x54b720c8f3876115e2e0fd5c8f0ed2fbaa8fe24f12c402497400043adc7d26e + ); + let invalid_adventurers = array![ADVENTURER_ID]; + game.slay_invalid_adventurers(invalid_adventurers); + } + + // test slay_invalid_adventurers on adventurer who used optimistic start with wrong hash + #[test] + fn test_slay_invalid_adventurer() { + let mut game = new_adventurer_lvl3(1000, 1696201757, 123456789); + let invalid_adventurers = array![ADVENTURER_ID]; + game.slay_invalid_adventurers(invalid_adventurers); + let adventurer = game.get_adventurer(ADVENTURER_ID); + assert(adventurer.health == 0, 'adventurer should be dead'); + assert(adventurer.xp == 1, 'adventurer should have 1 xp'); + assert(adventurer.gold == 0, 'adventurer should have 0 gold'); + } }