From d66d1d8c6ec9731e754841a8ddc2d75b4199e841 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 30 May 2026 22:46:12 -0400 Subject: [PATCH] test(packs): statistical sampler + mark PackRateConfig [Obsolete] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 200k-slot statistical test asserts observed tier rates within +/- 0.5pp of the seeded SV Classic shape (Bronze=76.5 / Silver=16 / Gold=6 / Legendary=1.5 on general slots). Marked [Category("Slow")]. PackRateConfig is marked [Obsolete] — no longer consulted by PackOpenService. Internal callers (GameConfigService / DbContext config seeding / its own tests) still reference it; they'll go when v1 stabilizes and PackRateConfig is fully retired. Co-Authored-By: Claude Opus 4.7 --- .../Models/Config/PackRateConfig.cs | 1 + .../Services/PackOpenServiceTests.cs | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/SVSim.Database/Models/Config/PackRateConfig.cs b/SVSim.Database/Models/Config/PackRateConfig.cs index d959c14..a17a116 100644 --- a/SVSim.Database/Models/Config/PackRateConfig.cs +++ b/SVSim.Database/Models/Config/PackRateConfig.cs @@ -6,6 +6,7 @@ namespace SVSim.Database.Models.Config; /// , not in the initialiser — see PerSlot docstring. /// [ConfigSection("PackRates")] +[Obsolete("PackRateConfig is no longer consulted by PackOpenService — per-pack rates come from PackDrawTable. Retire once v1 stabilizes.")] public class PackRateConfig { /// diff --git a/SVSim.UnitTests/Services/PackOpenServiceTests.cs b/SVSim.UnitTests/Services/PackOpenServiceTests.cs index f54d3da..ac05d36 100644 --- a/SVSim.UnitTests/Services/PackOpenServiceTests.cs +++ b/SVSim.UnitTests/Services/PackOpenServiceTests.cs @@ -139,6 +139,62 @@ public class PackOpenServiceTests Assert.That(bonus.CardId, Is.AnyOf(300L, 301L)); } + [Test] + [Category("Slow")] + public void Draw_observed_tier_rates_track_seed_within_half_a_percent() + { + // Synthetic Classic pack: Bronze=76.5/Silver=16/Gold=6/Legendary=1.5 in general slots; + // slot 8 is Silver=92.5/Gold=6/Legendary=1.5 (no Bronze). + var table = new PackDrawTable + { + Config = new PackDrawConfigEntry { Id = 10000, AnimationRatePct = 0 }, + SlotRates = new[] + { + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Bronze, RatePct = 76.5 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Silver, RatePct = 16.0 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Gold, RatePct = 6.0 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Legendary, RatePct = 1.5 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.Eighth, Tier = DrawTier.Silver, RatePct = 92.5 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.Eighth, Tier = DrawTier.Gold, RatePct = 6.0 }, + new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.Eighth, Tier = DrawTier.Legendary, RatePct = 1.5 }, + }, + CardWeights = new[] + { + new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 1, RatePct = 76.5 }, + new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Silver, CardId = 2, RatePct = 16.0 }, + new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Gold, CardId = 3, RatePct = 6.0 }, + new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Legendary, CardId = 4, RatePct = 1.5 }, + }, + }; + var svc = new PackOpenService(); + var rng = new SystemRandom(42); + var pack = new PackConfigEntry { Id = 10000 }; + int totalSlots = 200_000; + int bronze = 0, silver = 0, gold = 0, legendary = 0; + + // 25_000 packs * 7 general slots = 175_000 general-slot observations. + for (int i = 0; i < totalSlots / 8; i++) + { + var r = svc.Draw(table, pack, 1, Array.Empty(), Array.Empty(), new NoFoil(), rng); + for (int s = 0; s < 7; s++) + { + switch (r.Cards[s].Rarity) + { + case Rarity.Bronze: bronze++; break; + case Rarity.Silver: silver++; break; + case Rarity.Gold: gold++; break; + case Rarity.Legendary: legendary++; break; + } + } + } + + double n = bronze + silver + gold + legendary; + Assert.That(100 * bronze / n, Is.EqualTo(76.5).Within(0.5)); + Assert.That(100 * silver / n, Is.EqualTo(16.0).Within(0.5)); + Assert.That(100 * gold / n, Is.EqualTo(6.0).Within(0.5)); + Assert.That(100 * legendary / n, Is.EqualTo(1.5).Within(0.5)); + } + [Test] public void Draw_does_not_emit_bonus_for_packNumber_less_than_10() {