Adding bosses to the game #6

Merged
mico merged 21 commits from feature/boss-mechanic into main 2024-10-31 13:56:05 +00:00
5 changed files with 131 additions and 14 deletions
Showing only changes of commit 2e5d2143a8 - Show all commits

View File

@ -107,7 +107,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
champion: Raider({level: 0}), champion: Raider({level: 0}),
profit_per_second: 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 // 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)); 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) { function getPlayer(address addr) public view returns (Player memory) {
return players[addr]; return players[addr];
} }
// Function to get Army struct // Function to get the Army struct
function getArmy(address addr) public view returns (Army memory) { function getArmy(address addr) public view returns (Army memory) {
return armies[addr]; 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 // Quick fn to check if user is registered
function isRegistered(address addr) public view returns (bool) { function isRegistered(address addr) public view returns (bool) {
return players[addr].created_at != 0; 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 // first perform raid
performRaid(msg.sender); performRaid(msg.sender);
Boss memory boss_to_attack = bosses[msg.sender]; Boss memory boss_to_attack = bosses[msg.sender];
@ -255,15 +260,16 @@ contract RaidGeld is ERC20, Ownable, Constants {
: balanceOf(msg.sender); : balanceOf(msg.sender);
bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao); bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao);
if (hasWonBattle) { 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); _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; players[msg.sender].total_rewards += reward;
daoToken.transferFrom(address(this), msg.sender, reward); daoToken.transferFrom(address(this), msg.sender, reward);
if (boss_to_attack.level == 6) { 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; players[msg.sender].prestige_level += 1;
player_dies(msg.sender); player_dies(msg.sender);
return [hasWonBattle, true /* New prestige level! */ ];
} else { } else {
// else go to next boss // else go to next boss
bosses[msg.sender].level += 1; bosses[msg.sender].level += 1;
@ -272,6 +278,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Whoops u died, boss defeated you // Whoops u died, boss defeated you
player_dies(msg.sender); player_dies(msg.sender);
} }
return [hasWonBattle, false /* hasnt gotten prestige level */ ];
} }
function player_dies(address player) private { function player_dies(address player) private {

View File

@ -26,5 +26,5 @@ struct Player {
struct Boss { struct Boss {
uint8 level; uint8 level;
uint8[7] boss_variants; uint8[7] variants;
} }

View File

@ -2,6 +2,7 @@
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {Army} from "../src/RaidGeldStructs.sol"; import {Army} from "../src/RaidGeldStructs.sol";
import {console} from "forge-std/Test.sol";
library RaidGeldUtils { library RaidGeldUtils {
uint256 public constant PRECISION = 10000; uint256 public constant PRECISION = 10000;
@ -30,6 +31,7 @@ library RaidGeldUtils {
else if (level == 4) return 90000000000; else if (level == 4) return 90000000000;
else if (level == 5) return 900000000000; else if (level == 5) return 900000000000;
else if (level == 6) return 9000000000000; else if (level == 6) return 9000000000000;
else return 0;
} }
function getBossCumulativeChance(uint8 level) internal pure returns (uint256 chance) { function getBossCumulativeChance(uint8 level) internal pure returns (uint256 chance) {
@ -38,11 +40,26 @@ library RaidGeldUtils {
require(level <= 7, "Bosses only go to level 7"); require(level <= 7, "Bosses only go to level 7");
if (level == 0) return 99e16; if (level == 0) return 99e16;
else if (level == 1) return 8811e14; 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 == 3) return 493416e12;
else if (level == 4) return 30591792e10; else if (level == 4) return 30591792e10;
else if (level == 5) return 1560181392e8; else if (level == 5) return 1560181392e8;
else if (level == 6) return 6240725568e7; 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) { 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 // Returns how much Dao Token player is due for winning
function calculateBossReward(uint8 bossLevel) internal pure returns (uint256) { function calculateBossReward(uint8 bossLevel, uint256 baseReward) internal pure returns (uint256) {
return getBossPower(bossLevel) * (getBossCumulativeChance(bossLevel) * 10 / 5) ** 2; // 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 // Calculates whether user survives the fight
function calculateBossFight(uint8 bossLevel, uint256 geldBurnt, uint256 prevrandao) internal pure returns (bool) { function calculateBossFight(uint8 bossLevel, uint256 geldBurnt, uint256 prevrandao) internal pure returns (bool) {
uint256 bossPower = getBossPower(bossLevel); uint256 bossPower = getBossPower(bossLevel);
uint256 bossRoll = getBossChance(bossLevel);
require(geldBurnt <= bossPower, "Cant try to defeat boss with more than what boss power is"); require(geldBurnt <= bossPower, "Cant try to defeat boss with more than what boss power is");
uint256 random_n = random(prevrandao, 1, 100); uint256 random_n = random(prevrandao, 1, 100);
// Relative power as in, you can only put in 800 geld to defeat 900 geld boss, // Relative power as in, you can only put in 800 geld to defeat 900 geld boss,
// but you will get exponentially worse chances // but you will get exponentially worse chances
uint256 relativePower = ((geldBurnt ** 2) * 100) / bossPower ** 2; uint256 relativePower = ((geldBurnt ** 2) * 100) / bossPower ** 2;
uint256 roll = (random_n * relativePower * PRECISION) / (100 * PRECISION); uint256 roll = random_n * relativePower / 1e2;
return roll >= bossPower; return roll < bossRoll;
} }
function generate_boss_variants(uint256 prevrandao) internal pure returns (uint8[7] memory boss_variants) { function generate_boss_variants(uint256 prevrandao) internal pure returns (uint8[7] memory boss_variants) {

View File

@ -3,7 +3,7 @@ pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol"; import {Test, console} from "forge-std/Test.sol";
import {stdStorage, StdStorage} 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 "../src/RaidGeldUtils.sol";
import {Constants} from "../src/Constants.sol"; import {Constants} from "../src/Constants.sol";
@ -285,4 +285,32 @@ contract raid_geldTest is Test, Constants {
assertLt(newBalance, newestBalance); assertLt(newBalance, newestBalance);
assertLt(last_raided_at, last_raided_at_2); 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);
}
} }

View File

@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol"; import {Test, console} from "forge-std/Test.sol";
import {Army, Raider} from "../src/RaidGeldStructs.sol"; import {Army, Raider} from "../src/RaidGeldStructs.sol";
import "../src/RaidGeldUtils.sol"; import "../src/RaidGeldUtils.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
contract raid_geldTest is Test { contract raid_geldTest is Test {
function test_0_unit_price() public pure { function test_0_unit_price() public pure {
@ -51,4 +52,64 @@ contract raid_geldTest is Test {
+ _anLvl * RaidGeldUtils.ANOINTED_PROFIT + _cLvl * RaidGeldUtils.CHAMPION_PROFIT; + _anLvl * RaidGeldUtils.ANOINTED_PROFIT + _cLvl * RaidGeldUtils.CHAMPION_PROFIT;
assertEq(profits_per_second, expected); 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");
}
}
} }