initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
93
Outnumbered.Tests/TestSupport.cs
Normal file
93
Outnumbered.Tests/TestSupport.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System.Collections.Frozen;
|
||||
using Outnumbered.Config;
|
||||
using Outnumbered.Domain;
|
||||
using Xunit;
|
||||
|
||||
namespace Outnumbered.Tests;
|
||||
|
||||
// Shared fixtures for the Domain tests. The config objects' parameterless constructors already carry the SHIPPING code
|
||||
// defaults (the balance-retune values in DomainConfig.cs are those defaults), so `new HandicapConfig()` etc. is exactly the
|
||||
// production config — the live OP testing JSON is deliberately NOT involved (balance source of truth = code defaults).
|
||||
internal static class T
|
||||
{
|
||||
public const int BaseMaxHp = 100; // SnapshotBuilder.BaseMaxHp
|
||||
public const int BaseMaxArmor = 100; // SnapshotBuilder.BaseMaxArmor
|
||||
|
||||
public static HandicapConfig Hcap() => new();
|
||||
public static ProgressionConfig Prog() => new();
|
||||
public static StatsConfig Stats() => new();
|
||||
public static SurvivalConfig Surv() => new();
|
||||
public static AbilitiesConfig Abil() => new();
|
||||
|
||||
public static ResolvedHandicap Resolved() => new(Hcap());
|
||||
public static ResolvedHandicap Resolved(HandicapConfig h) => new(h);
|
||||
|
||||
// Mirrors SnapshotBuilder.RebuildStatDefs + Stats.StatRegistry EXACTLY (same 12 key->StatDef rows, StringComparer.Ordinal).
|
||||
// If StatRegistry changes, this must change with it — the load-time validators in the engine keep the live registry honest;
|
||||
// this is the test-side twin.
|
||||
public static FrozenDictionary<string, StatDef> StatDefs() => StatDefs(Stats());
|
||||
public static FrozenDictionary<string, StatDef> StatDefs(StatsConfig c) => new Dictionary<string, StatDef>(StringComparer.Ordinal)
|
||||
{
|
||||
[StatKeys.Damage] = c.Damage,
|
||||
[StatKeys.CritChance] = c.CritChance,
|
||||
[StatKeys.CritDamage] = c.CritDamage,
|
||||
[StatKeys.HeadshotDamage] = c.HeadshotDamage,
|
||||
[StatKeys.MaxHp] = c.MaxHp,
|
||||
[StatKeys.MaxArmor] = c.MaxArmor,
|
||||
[StatKeys.Lifesteal] = c.Lifesteal,
|
||||
[StatKeys.ArmorLifesteal] = c.ArmorLifesteal,
|
||||
[StatKeys.HpRegen] = c.HpRegen,
|
||||
[StatKeys.ArmorRegen] = c.ArmorRegen,
|
||||
[StatKeys.Thorns] = c.Thorns,
|
||||
[StatKeys.XpBoost] = c.XpBoost,
|
||||
}.ToFrozenDictionary(StringComparer.Ordinal);
|
||||
|
||||
// A PlayerSnapshot factory: neutral by default (kills+deaths<3 so the K/D factor is dormant, full HP, no floor/team buff,
|
||||
// no abilities). Override only the fields a case cares about.
|
||||
public static PlayerSnapshot Snap(
|
||||
int level = 1, int prestige = 0, long xp = 0,
|
||||
int kills = 0, int deaths = 0, int headshotKills = 0, int streak = 0,
|
||||
int health = 100,
|
||||
double progress = 0.0, double floor = -1.0,
|
||||
double teamDeal = 1.0, double teamTake = 1.0,
|
||||
bool overcharge = false, bool berserk = false, bool adrenaline = false,
|
||||
Dictionary<string, int>? upgrades = null, IStatBonusSource? cards = null) => new()
|
||||
{
|
||||
Level = level,
|
||||
Prestige = prestige,
|
||||
Xp = xp,
|
||||
Kills = kills,
|
||||
Deaths = deaths,
|
||||
HeadshotKills = headshotKills,
|
||||
Streak = streak,
|
||||
Health = health,
|
||||
HandicapProgress = progress,
|
||||
HandicapFloor = floor,
|
||||
TeamDealMult = teamDeal,
|
||||
TeamTakeMult = teamTake,
|
||||
OverchargeActive = overcharge,
|
||||
BerserkActive = berserk,
|
||||
AdrenalineActive = adrenaline,
|
||||
Upgrades = upgrades ?? new Dictionary<string, int>(),
|
||||
Cards = cards,
|
||||
};
|
||||
|
||||
// Relative-tolerance double compare. The fractional-t bands route through Math.Pow, whose last ULP can differ from the
|
||||
// independent Python oracle that produced the literals; 1e-9 is microscopic next to any real formula change (which moves a
|
||||
// value by whole percent), so this still catches every regression while ignoring FP noise.
|
||||
public static void Close(double expected, double actual, double rel = 1e-9)
|
||||
{
|
||||
double tol = rel * Math.Max(1.0, Math.Abs(expected));
|
||||
Assert.True(Math.Abs(expected - actual) <= tol,
|
||||
$"expected {expected:R}, got {actual:R} (|diff|={Math.Abs(expected - actual):R} > tol={tol:R})");
|
||||
}
|
||||
}
|
||||
|
||||
// Dictionary-backed IStatBonusSource — stands in for the survival run's per-stat card accumulator. The Domain only ever calls
|
||||
// Bonus(key); the engine-side level*PerPick accumulation isn't Domain logic, so a flat dictionary is the faithful test double.
|
||||
internal sealed class Cards(params (string key, double bonus)[] entries) : IStatBonusSource
|
||||
{
|
||||
private readonly Dictionary<string, double> _b = entries.ToDictionary(e => e.key, e => e.bonus, StringComparer.Ordinal);
|
||||
|
||||
public double Bonus(string statKey) => _b.TryGetValue(statKey, out var v) ? v : 0.0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue