diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 8529188..7c4e0bc 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -107,7 +107,7 @@ contract RaidGeld is ERC20, Ownable, Constants { champion: Raider({level: 0}), profit_per_second: 0 }); - bosses[addr] = Boss({level: 0, boss_variants: RaidGeldUtils.generate_boss_variants(block.prevrandao)}); + bosses[addr] = Boss({level: 0, variants: RaidGeldUtils.generate_boss_variants(block.prevrandao)}); } // New player want to register with ETH @@ -164,16 +164,21 @@ contract RaidGeld is ERC20, Ownable, Constants { emit RaidPerformed(player, players[player].total_minted, balanceOf(player)); } - // Function to get Player struct + // Function to get the Player struct function getPlayer(address addr) public view returns (Player memory) { return players[addr]; } - // Function to get Army struct + // Function to get the Army struct function getArmy(address addr) public view returns (Army memory) { return armies[addr]; } + // Function to get the Boss struct + function getBoss(address addr) public view returns (Boss memory) { + return bosses[addr]; + } + // Quick fn to check if user is registered function isRegistered(address addr) public view returns (bool) { return players[addr].created_at != 0; @@ -245,7 +250,7 @@ contract RaidGeld is ERC20, Ownable, Constants { ); } - function battle_with_boss() external onlyActiveSession { + function battle_with_boss() external onlyActiveSession returns (bool[2] memory hasWonOrAscended) { // first perform raid performRaid(msg.sender); Boss memory boss_to_attack = bosses[msg.sender]; @@ -255,15 +260,16 @@ contract RaidGeld is ERC20, Ownable, Constants { : balanceOf(msg.sender); bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao); if (hasWonBattle) { - // Burn geld, get some sweet DAO Token and continue + // Burn geld, send some sweet DAO Token and continue _burn(msg.sender, geld_to_burn); - uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level); + uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, BUY_IN_DAO_TOKEN_AMOUNT); players[msg.sender].total_rewards += reward; daoToken.transferFrom(address(this), msg.sender, reward); if (boss_to_attack.level == 6) { - // User ascends! Moloch is defeated + // User ascends! Moloch is defeated, user can start a new run players[msg.sender].prestige_level += 1; player_dies(msg.sender); + return [hasWonBattle, true /* New prestige level! */ ]; } else { // else go to next boss bosses[msg.sender].level += 1; @@ -272,6 +278,7 @@ contract RaidGeld is ERC20, Ownable, Constants { // Whoops u died, boss defeated you player_dies(msg.sender); } + return [hasWonBattle, false /* hasnt gotten prestige level */ ]; } function player_dies(address player) private { diff --git a/src/RaidGeldStructs.sol b/src/RaidGeldStructs.sol index 61d6703..f30deed 100644 --- a/src/RaidGeldStructs.sol +++ b/src/RaidGeldStructs.sol @@ -26,5 +26,5 @@ struct Player { struct Boss { uint8 level; - uint8[7] boss_variants; + uint8[7] variants; } diff --git a/src/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index c8da473..d92f6f0 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.13; import {Army} from "../src/RaidGeldStructs.sol"; +import {console} from "forge-std/Test.sol"; library RaidGeldUtils { uint256 public constant PRECISION = 10000; @@ -30,6 +31,7 @@ library RaidGeldUtils { else if (level == 4) return 90000000000; else if (level == 5) return 900000000000; else if (level == 6) return 9000000000000; + else return 0; } function getBossCumulativeChance(uint8 level) internal pure returns (uint256 chance) { @@ -38,11 +40,26 @@ library RaidGeldUtils { require(level <= 7, "Bosses only go to level 7"); if (level == 0) return 99e16; else if (level == 1) return 8811e14; - else if (level == 2) return 7048e14; + else if (level == 2) return 70488e13; else if (level == 3) return 493416e12; else if (level == 4) return 30591792e10; else if (level == 5) return 1560181392e8; else if (level == 6) return 6240725568e7; + else return 0; + } + + function getBossChance(uint8 level) internal pure returns (uint256 chance) { + // for boss chances (percent) [99, 89, 80, 70, 62, 51, 40] + // where cumulative chance is in the form of 18 precision related to 1e18 + require(level <= 7, "Bosses only go to level 7"); + if (level == 0) return 99; + else if (level == 1) return 89; + else if (level == 2) return 80; + else if (level == 3) return 70; + else if (level == 4) return 62; + else if (level == 5) return 51; + else if (level == 6) return 40; + else return 0; } function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) { @@ -77,20 +94,24 @@ library RaidGeldUtils { } // Returns how much Dao Token player is due for winning - function calculateBossReward(uint8 bossLevel) internal pure returns (uint256) { - return getBossPower(bossLevel) * (getBossCumulativeChance(bossLevel) * 10 / 5) ** 2; + function calculateBossReward(uint8 bossLevel, uint256 baseReward) internal pure returns (uint256) { + // TODO: This could as well just be pre-calculated + uint256 cumulativeChance = getBossCumulativeChance(bossLevel); // 0 - 1e18 range + uint256 rewardMultiplier = ((2 * (1e18 - cumulativeChance)) ** 2) / 1e18; + return (baseReward * rewardMultiplier); } // Calculates whether user survives the fight function calculateBossFight(uint8 bossLevel, uint256 geldBurnt, uint256 prevrandao) internal pure returns (bool) { uint256 bossPower = getBossPower(bossLevel); + uint256 bossRoll = getBossChance(bossLevel); require(geldBurnt <= bossPower, "Cant try to defeat boss with more than what boss power is"); uint256 random_n = random(prevrandao, 1, 100); // Relative power as in, you can only put in 800 geld to defeat 900 geld boss, // but you will get exponentially worse chances uint256 relativePower = ((geldBurnt ** 2) * 100) / bossPower ** 2; - uint256 roll = (random_n * relativePower * PRECISION) / (100 * PRECISION); - return roll >= bossPower; + uint256 roll = random_n * relativePower / 1e2; + return roll < bossRoll; } function generate_boss_variants(uint256 prevrandao) internal pure returns (uint8[7] memory boss_variants) { diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index c0bea26..2bb65bd 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; -import {RaidGeld, Army, Player} from "../src/RaidGeld.sol"; +import {RaidGeld, Army, Player, Boss} from "../src/RaidGeld.sol"; import "../src/RaidGeldUtils.sol"; import {Constants} from "../src/Constants.sol"; @@ -285,4 +285,32 @@ contract raid_geldTest is Test, Constants { assertLt(newBalance, newestBalance); assertLt(last_raided_at, last_raided_at_2); } + + function test_08_attack_boss() public { + // Let some time pass so we dont start at block timestamp 0 + vm.warp(120); + + // Register player 1 + vm.startPrank(player1); + registerPlayer(); + raid_geld.addUnit(0, 1); + + Boss memory boss = raid_geld.getBoss(player1); + // assert boss is initialized + assertEq(boss.level, 0); + // make sure variants shuffled + assertNotEq(boss.variants[1], boss.variants[2]); + + // Make a lot of time pass so user deffo has GELD to attack the boss + vm.warp(1200000); + + bool[2] memory results = raid_geld.battle_with_boss(); + console.log(results[0]); + console.log(results[1]); + + // Should almost always defeat first boss + assertEq(results[0], true); + // First boss doesnt grant a new prestige level (ascension) + assertEq(results[1], false); + } } diff --git a/test/RaidGeldUtils.t.sol b/test/RaidGeldUtils.t.sol index fbda317..7502ba9 100644 --- a/test/RaidGeldUtils.t.sol +++ b/test/RaidGeldUtils.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Army, Raider} from "../src/RaidGeldStructs.sol"; import "../src/RaidGeldUtils.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; contract raid_geldTest is Test { function test_0_unit_price() public pure { @@ -51,4 +52,64 @@ contract raid_geldTest is Test { + _anLvl * RaidGeldUtils.ANOINTED_PROFIT + _cLvl * RaidGeldUtils.CHAMPION_PROFIT; assertEq(profits_per_second, expected); } + + function getConfidenceInterval(uint8 bossLevel) + internal + pure + returns (uint256 lower, uint256 upper) + { + /* + man i struggled getting this math with just integers so its + easier to just do it like this (precalculated): + z = 1.96 (95%) + Probability | Expected | Variance | Margin | Lower Bound | Upper Bound + 99% 990 0.00099 62 928 1052 + 89% 890 0.000979 61 829 951 + 80% 800 0.00016 49 751 849 + 70% 700 0.00021 45 655 745 + 62% 620 0.0002356 48 572 668 + 51% 510 0.0002499 49 461 559 + 40% 400 0.00024 48 352 448 + */ + if (bossLevel == 0) { + return (928, 1000); + } else if (bossLevel == 1) { + return (829, 951); + } else if (bossLevel == 2) { + return (751, 849); + } else if (bossLevel == 3) { + return (655, 745); + } else if (bossLevel == 4) { + return (572, 668); + } else if (bossLevel == 5) { + return (461, 559); + } else if (bossLevel == 6) { + return (352, 448); + } + } + + function test_2_calculateBossFight_probabilities() public { + uint256[7] memory probabilities = + [uint256(99), uint256(89), uint256(80), uint256(70), uint256(62), uint256(51), uint256(40)]; + uint256 totalRuns = 1000; + console.log("Checking boss rolls (1000 times)"); + for (uint8 bossLevel = 0; bossLevel < 7; bossLevel++) { + console.log("Boss level: ", bossLevel); + uint256 bossPower = RaidGeldUtils.getBossPower(bossLevel); + uint256 geldBurnt = bossPower; + uint256 successCount = 0; + for (uint256 testRun = 0; testRun < totalRuns; testRun++) { + uint256 prevrandao = uint256(keccak256(abi.encodePacked(block.prevrandao, testRun * 1e9))); // Unique seed each time + if (RaidGeldUtils.calculateBossFight(bossLevel, geldBurnt, prevrandao)) { + successCount++; + } + } + (uint256 lowerProb, uint256 upperProb) = getConfidenceInterval(bossLevel); + console.log("expected at least wins", lowerProb); + console.log("expected at most wins", upperProb); + console.log("actual wins", successCount); + console.log("----------"); + vm.assertTrue(successCount >= lowerProb && successCount <= upperProb, "Success rate not within expected range"); + } + } }