using Outnumbered.Domain; using Xunit; namespace Outnumbered.Tests; // HandicapModel: the balance spine. ComputeT -> the deal/take/xp bands. Anchor t/band values were produced by an INDEPENDENT // Python re-implementation of the documented formula against the retuned code defaults (MasterDifficulty 1.6, Curve 1.3, // KdNeutral 1.5/MaxNerf 4.0, weights Kd2/Hs1/Streak1/Level1.5, bands deal 0.033..1.3 / take 0.85..8 / xp 0.1..10), so these // validate the C# implementation rather than merely locking it. Fractional-t rows route through Math.Pow -> compared at 1e-9. public class HandicapModelTests { // level, kills, deaths, headshotKills, streak, floor | expected t, deal, take, xp [Theory] [InlineData(0, 0, 0, 0, 0, -1.0, /**/ 0.0, 1.0, 1.0, 1.0)] // pure neutral [InlineData(100, 40, 5, 24, 20, -1.0, /**/ 1.0, 0.03300000000000003, 8.0, 10.0)] // dominant maxed -> t=1 [InlineData(10, 1, 10, 0, 0, -1.0, /**/ -1.0, 1.3, 0.85, 0.09999999999999998)] // struggling -> t=-1 (comeback) [InlineData(50, 10, 4, 0, 5, -1.0, /**/ 0.4312595622635058, 0.5829720032911898, 4.018816935844541, 4.881336060371552)] // mid lead [InlineData(1, 20, 4, 0, 0, -1.0, /**/ 0.4993925356559294, 0.5170874180207163, 4.495747749591506, 5.494532820903364)] // high K/D, low level [InlineData(100, 3, 2, 0, 0, -1.0, /**/ 0.3402539047656616, 0.6709744740916053, 3.381777333359631, 4.062285142890954)] // maxed level at neutral K/D (residual) [InlineData(10, 1, 10, 0, 0, 0.5, /**/ 0.40612619817811774, 0.6072759663617602, 3.842883387246824, 4.655135783603059)] // struggler RAISED by survival floor 0.5 public void ComputeT_and_bands_match_oracle(int level, int kills, int deaths, int hsk, int streak, double floor, double et, double ed, double etk, double exp) { var s = T.Snap(level: level, kills: kills, deaths: deaths, headshotKills: hsk, streak: streak, floor: floor); var rh = T.Resolved(); double t = HandicapModel.ComputeT(s, rh); T.Close(et, t); HandicapModel.Bands(s, rh, out double deal, out double take, out double xp); T.Close(ed, deal); T.Close(etk, take); T.Close(exp, xp); // Bands must be bit-identical to the individual band functions (the HUD shares one ComputeT via Bands). Assert.Equal(HandicapModel.MDeal(s, rh), deal); Assert.Equal(HandicapModel.MTake(s, rh), take); Assert.Equal(HandicapModel.XpMult(s, rh), xp); } // BandsFromT = the balance API's curve sampler: RAW t in, the same Curve ease applied inside, team multipliers // neutral. Equivalence with the live path is proven by routing the SAME raw t through ComputeT via the survival // floor on an all-zero-factor snapshot (floor >= 0 forces t = floor before the shared ease). [Theory] [InlineData(0.0)] [InlineData(0.25)] [InlineData(0.5)] [InlineData(1.0)] public void BandsFromT_matches_live_bands_at_same_raw_t(double t) { var rh = T.Resolved(); var s = T.Snap(level: 0, floor: t); // level: 0 — Snap's level=1 default is a nonzero factor that would shift t off the floor HandicapModel.BandsFromT(t, rh, out double d, out double tk, out double x); HandicapModel.Bands(s, rh, out double liveDeal, out double liveTake, out double liveXp); Assert.Equal(liveDeal, d); Assert.Equal(liveTake, tk); Assert.Equal(liveXp, x); } [Fact] public void BandsFromT_disabled_is_flat_neutral() // mirrors ComputeT's Enabled short-circuit — sampled curves must be x1 too { var h = T.Hcap(); h.Enabled = false; var rh = T.Resolved(h); foreach (var t in new[] { -1.0, -0.4, 0.0, 0.7, 1.0 }) { HandicapModel.BandsFromT(t, rh, out double d, out double tk, out double x); Assert.Equal(1.0, d); Assert.Equal(1.0, tk); Assert.Equal(1.0, x); } } [Fact] public void BandsFromT_extremes_hit_the_configured_bands() // ease(+-1) = +-1 for ANY Curve, so t=+-1 pins the bands { // T.Close, not Assert.Equal: Lerp(1, band, 1) reconstructs the band with float dust (same reason the oracle // rows above encode 0.03300000000000003 for the 0.033 deal floor). var h = T.Hcap(); var rh = T.Resolved(h); HandicapModel.BandsFromT(1.0, rh, out double d1, out double t1, out double x1); T.Close(h.MDealFloor, d1); T.Close(h.MTakeCeiling, t1); T.Close(h.XpCeiling, x1); HandicapModel.BandsFromT(-1.0, rh, out double d2, out double t2, out double x2); T.Close(h.MDealCeiling, d2); T.Close(h.MTakeFloor, t2); T.Close(h.XpFloor, x2); } [Fact] public void Disabled_handicap_is_neutral() { var h = T.Hcap(); h.Enabled = false; var rh = T.Resolved(h); var dominant = T.Snap(level: 100, kills: 40, deaths: 5, headshotKills: 24, streak: 20); Assert.Equal(0.0, HandicapModel.ComputeT(dominant, rh)); HandicapModel.Bands(dominant, rh, out double deal, out double take, out double xp); Assert.Equal(1.0, deal); Assert.Equal(1.0, take); Assert.Equal(1.0, xp); } [Fact] public void Team_card_multipliers_ride_on_top_of_the_band() { // Neutral t=0 -> dealFromT=takeFromT=1, so the survival squad card shows through directly. XP is NOT team-scaled. var rh = T.Resolved(); var s = T.Snap(level: 0, teamDeal: 1.331, teamTake: 0.729); T.Close(1.331, HandicapModel.MDeal(s, rh)); T.Close(0.729, HandicapModel.MTake(s, rh)); T.Close(1.0, HandicapModel.XpMult(s, rh)); // xp band ignores team mults } [Fact] public void Floor_raises_but_never_lowers_t() { var rh = T.Resolved(); // A dominant player (t would be ~1) with a LOW floor is unaffected; the floor only ever pulls a low t UP. var dominant = T.Snap(level: 100, kills: 40, deaths: 5, headshotKills: 24, streak: 20, floor: 0.2); Assert.Equal(1.0, HandicapModel.ComputeT(dominant, rh)); // floor 0.2 < natural 1.0 -> no effect } }