using Outnumbered.Config; using Outnumbered.Domain; using Xunit; namespace Outnumbered.Tests; // SurvivalEconomy: wave population/budget/escalation + the PER-WAVE XP grant. Defaults (DomainConfig.SurvivalConfig): // AliveBase=4, AlivePerWave=2, AliveCap=20; BudgetBase=6, BudgetPerWave=4, BudgetPerPlayer=3; MaxNerfWave=25; // WaveCount=20, WinMult=24. XP is granted per cleared wave: rawWaveXp x prestige x WaveMult(wave) (no cap, no XpBoost, // handicap excluded). WaveMult(w) = WinMult^((w-1)/(WaveCount-1)). public class SurvivalEconomyTests { // AliveForWave = clamp(AliveBase + AlivePerWave*(wave-1), 1, AliveCap). [Theory] [InlineData(1, 4)] // base [InlineData(2, 6)] [InlineData(9, 20)] // 4 + 2*8 = 20 -> hits cap [InlineData(50, 20)] // clamped to AliveCap public void AliveForWave_ramps_then_caps(int wave, int expected) => Assert.Equal(expected, SurvivalEconomy.AliveForWave(wave, T.Surv())); [Fact] public void AliveForWave_never_below_one() => Assert.Equal(1, SurvivalEconomy.AliveForWave(-5, T.Surv())); // lower clamp // WaveBudget = max(1, BudgetBase + BudgetPerWave*(wave-1) + BudgetPerPlayer*aliveHumans). [Theory] [InlineData(1, 1, 9)] // 6 + 0 + 3*1 [InlineData(1, 4, 18)] // 6 + 0 + 3*4 [InlineData(5, 3, 31)] // 6 + 16 + 9 public void WaveBudget_matches(int wave, int aliveHumans, int expected) => Assert.Equal(expected, SurvivalEconomy.WaveBudget(wave, aliveHumans, T.Surv())); [Fact] public void WaveBudget_never_below_one() => Assert.Equal(1, SurvivalEconomy.WaveBudget(-10, 0, T.Surv())); // HandicapFloor = wave<=0 ? -1 : min(1, wave/max(1,MaxNerfWave)). Monotonic escalate-only floor in t-space. [Theory] [InlineData(0, -1.0)] // idle / between runs [InlineData(-3, -1.0)] [InlineData(1, 0.04)] // 1/25 [InlineData(25, 1.0)] // reaches full nerf at MaxNerfWave [InlineData(40, 1.0)] // clamped at 1 public void HandicapFloor_matches(int wave, double expected) => T.Close(expected, SurvivalEconomy.HandicapFloor(wave, T.Surv())); [Fact] public void HandicapFloor_is_monotonic_non_decreasing() { var c = T.Surv(); double prev = SurvivalEconomy.HandicapFloor(1, c); for (int w = 2; w <= c.WaveCount; w++) { double cur = SurvivalEconomy.HandicapFloor(w, c); Assert.True(cur >= prev, $"floor wave {w}={cur} < wave {w - 1}={prev}"); prev = cur; } } // WaveMult(w) = WinMult ^ ((w-1)/(WaveCount-1)). Use a clean config (WinMult 100, WaveCount 11) for exact landmarks. private static SurvivalConfig Clean() => new() { WinMult = 100, WaveCount = 11 }; [Theory] [InlineData(1, 1.0)] // wave 1 -> x1 [InlineData(6, 10.0)] // midpoint -> 100^0.5 = 10 [InlineData(11, 100.0)] // final wave -> xWinMult public void WaveMult_ramps_one_to_winmult(int wave, double expected) => T.Close(expected, SurvivalEconomy.WaveMult(wave, Clean())); [Fact] public void WaveMult_endpoints_on_defaults() { var c = T.Surv(); T.Close(1.0, SurvivalEconomy.WaveMult(1, c)); // wave 1 always x1 T.Close(c.WinMult, SurvivalEconomy.WaveMult(c.WaveCount, c)); // final wave = WinMult } [Fact] public void Retune_pinned_winmult_default() => Assert.Equal(24.0, T.Surv().WinMult); // pins the survival XP knob // WaveXpLump = floor(rawWaveXp * prestigeMult * WaveMult(wave)); NO cap, NO XpBoost, handicap excluded. [Theory] [InlineData(1000, 6, 0, 10000)] // 1000 * 1.0 * 10 [InlineData(1000, 11, 0, 100000)] // 1000 * 1.0 * 100 [InlineData(1000, 1, 5, 1500)] // 1000 * 1.5(prestige) * 1 [InlineData(1000, 6, 10, 20000)] // 1000 * 2.0(prestige) * 10 public void WaveXpLump_matches(double rawWaveXp, int wave, int prestige, long expected) => Assert.Equal(expected, SurvivalEconomy.WaveXpLump(rawWaveXp, wave, prestige, Clean(), T.Prog())); [Theory] [InlineData(0)] [InlineData(-50)] public void WaveXpLump_zero_for_nonpositive(double rawWaveXp) => Assert.Equal(0L, SurvivalEconomy.WaveXpLump(rawWaveXp, 6, 0, Clean(), T.Prog())); // AccrueWaveXp = amount * (1 + xpMultCard/100). [Theory] [InlineData(100, 0, 100)] [InlineData(100, 50, 150)] public void AccrueWaveXp_matches(double amount, double cardPct, double expected) => T.Close(expected, SurvivalEconomy.AccrueWaveXp(amount, cardPct)); // TeamMult: compounding per-level squad card. increase -> (1+p/100)^L ; decrease -> max(0,1-p/100)^L. [Theory] [InlineData(3, 10, true, 1.3310000000000004)] [InlineData(3, 10, false, 0.7290000000000001)] [InlineData(0, 10, true, 1.0)] [InlineData(3, 100, false, 0.0)] // 1-100% = 0, floored [InlineData(2, 50, true, 2.25)] public void TeamMult_matches(int level, double perPick, bool increase, double expected) => T.Close(expected, SurvivalEconomy.TeamMult(level, perPick, increase)); }