Seeding reorg
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
namespace SVSim.UnitTests.Services;
|
||||
@@ -25,6 +26,24 @@ public class PackOpenServiceTests
|
||||
public ShadowverseCardEntry? TryGetFoilTwin(long baseCardId) => null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test stub that returns a single pre-built section. Only handles <see cref="PackRateConfig"/>
|
||||
/// (the type <see cref="PackOpenService"/> reads in its ctor); other section types throw so a
|
||||
/// future test that needs them must extend this stub explicitly.
|
||||
/// </summary>
|
||||
private sealed class StubConfig : IGameConfigService
|
||||
{
|
||||
private readonly PackRateConfig _rates;
|
||||
public StubConfig(PackRateConfig rates) { _rates = rates; }
|
||||
public T Get<T>() where T : class, new()
|
||||
{
|
||||
if (typeof(T) == typeof(PackRateConfig)) return (T)(object)_rates;
|
||||
throw new NotImplementedException($"StubConfig: unhandled section type {typeof(T)}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static PackOpenService MakeService(PackRateConfig rates) => new(new StubConfig(rates));
|
||||
|
||||
private static List<ShadowverseCardEntry> MakeFourCards() => new()
|
||||
{
|
||||
new ShadowverseCardEntry { Id = 1, Rarity = Rarity.Bronze },
|
||||
@@ -41,8 +60,7 @@ public class PackOpenServiceTests
|
||||
[Test]
|
||||
public void Draw_returns_eight_cards_for_one_pack()
|
||||
{
|
||||
var rootDefault = new GameConfigRoot(); // canonical SV Classic defaults
|
||||
var svc = new PackOpenService(rootDefault);
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults());
|
||||
var pool = new StubPool(MakeFourCards());
|
||||
var rng = new ScriptedRandom(0.5); // anything in Bronze/Silver range for non-slot-8
|
||||
|
||||
@@ -54,8 +72,9 @@ public class PackOpenServiceTests
|
||||
[Test]
|
||||
public void Draw_slot_8_never_returns_bronze_for_standard_pack()
|
||||
{
|
||||
var rootDefault = new GameConfigRoot(); // canonical SV Classic defaults
|
||||
var svc = new PackOpenService(rootDefault);
|
||||
// PackRateConfig.ShippedDefaults() includes the SV Classic slot-8 "Silver-or-better
|
||||
// guarantee" entry (PerSlot Bronze=0). Same shape the runtime seeder writes to GameConfigs.
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults());
|
||||
var pool = new StubPool(MakeFourCards());
|
||||
|
||||
for (int trial = 0; trial < 1000; trial++)
|
||||
@@ -69,8 +88,7 @@ public class PackOpenServiceTests
|
||||
[Test]
|
||||
public void Draw_legendary_special_forces_slot_8_to_legendary()
|
||||
{
|
||||
var rootDefault = new GameConfigRoot(); // canonical SV Classic defaults
|
||||
var svc = new PackOpenService(rootDefault);
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults());
|
||||
var pool = new StubPool(MakeFourCards());
|
||||
var pack = new PackConfigEntry { Id = 92001, BasePackId = 90001, PackCategory = PackCategory.SpecialCardPack };
|
||||
|
||||
@@ -85,8 +103,7 @@ public class PackOpenServiceTests
|
||||
[Test]
|
||||
public void Draw_distribution_over_10k_slots_1_to_7_matches_declared_rates_within_2_percent()
|
||||
{
|
||||
var rootDefault = new GameConfigRoot(); // canonical SV Classic defaults
|
||||
var svc = new PackOpenService(rootDefault);
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults());
|
||||
var pool = new StubPool(MakeFourCards());
|
||||
var counts = new Dictionary<Rarity, int>
|
||||
{
|
||||
@@ -117,8 +134,7 @@ public class PackOpenServiceTests
|
||||
[Test]
|
||||
public void Draw_excludes_listed_card_ids()
|
||||
{
|
||||
var rootDefault = new GameConfigRoot(); // canonical SV Classic defaults
|
||||
var svc = new PackOpenService(rootDefault);
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults());
|
||||
// Pool with two bronze cards; exclude one — every Bronze slot must pick the other.
|
||||
var pool = new StubPool(new List<ShadowverseCardEntry>
|
||||
{
|
||||
@@ -140,16 +156,16 @@ public class PackOpenServiceTests
|
||||
public void Draw_per_slot_override_is_applied_for_that_slot_and_default_for_others()
|
||||
{
|
||||
// Config: slot 3 is forced to Legendary; everything else uses Default.
|
||||
// PerSlot is a List<SlotRarityWeights> with a Slot string key (EF Core 8 deviation from
|
||||
// the Dictionary<int, T> in the spec — see Task 5 notes).
|
||||
var root = new GameConfigRoot();
|
||||
root.PackRates.PerSlot.Add(new SlotRarityWeights
|
||||
// PerSlot is a List<SlotRarityWeights> with a Slot string key (no Dictionary<int,T> of
|
||||
// complex types under jsonb-friendly serialisation — see Task 5 notes).
|
||||
var rates = PackRateConfig.ShippedDefaults();
|
||||
rates.PerSlot.Add(new SlotRarityWeights
|
||||
{
|
||||
Slot = "3",
|
||||
Bronze = 0, Silver = 0, Gold = 0, Legendary = 1.0,
|
||||
});
|
||||
|
||||
var svc = new PackOpenService(root);
|
||||
var svc = MakeService(rates);
|
||||
var pool = new StubPool(MakeFourCards());
|
||||
|
||||
for (int trial = 0; trial < 50; trial++)
|
||||
@@ -186,8 +202,7 @@ public class PackOpenServiceTests
|
||||
new List<ShadowverseCardEntry> { bronze },
|
||||
new Dictionary<long, ShadowverseCardEntry> { [bronze.Id] = bronzeFoil });
|
||||
|
||||
var root = new GameConfigRoot(); // default AnimatedRate = 0.08
|
||||
var svc = new PackOpenService(root);
|
||||
var svc = MakeService(PackRateConfig.ShippedDefaults()); // default AnimatedRate = 0.08
|
||||
|
||||
const int packs = 1_000; // 8000 slots
|
||||
int foilCount = 0;
|
||||
@@ -211,9 +226,9 @@ public class PackOpenServiceTests
|
||||
new Dictionary<long, ShadowverseCardEntry>()); // no foils
|
||||
|
||||
// Force the animated roll to always hit by setting AnimatedRate = 1.0.
|
||||
var root = new GameConfigRoot();
|
||||
root.PackRates.AnimatedRate = 1.0;
|
||||
var svc = new PackOpenService(root);
|
||||
var rates = PackRateConfig.ShippedDefaults();
|
||||
rates.AnimatedRate = 1.0;
|
||||
var svc = MakeService(rates);
|
||||
|
||||
var r = svc.Draw(StandardPack(), pools, 1, Array.Empty<long>(), new SystemRandom(seed: 1));
|
||||
foreach (var c in r.Cards)
|
||||
@@ -232,9 +247,9 @@ public class PackOpenServiceTests
|
||||
new List<ShadowverseCardEntry> { leg },
|
||||
new Dictionary<long, ShadowverseCardEntry> { [leg.Id] = legFoil });
|
||||
|
||||
var root = new GameConfigRoot();
|
||||
root.PackRates.AnimatedRate = 1.0; // every slot upgrades
|
||||
var svc = new PackOpenService(root);
|
||||
var rates = PackRateConfig.ShippedDefaults();
|
||||
rates.AnimatedRate = 1.0; // every slot upgrades
|
||||
var svc = MakeService(rates);
|
||||
|
||||
var specialPack = new PackConfigEntry { Id = 92001, BasePackId = 90001, PackCategory = PackCategory.SpecialCardPack };
|
||||
var r = svc.Draw(specialPack, pools, 1, Array.Empty<long>(), new SystemRandom(seed: 3));
|
||||
@@ -243,4 +258,22 @@ public class PackOpenServiceTests
|
||||
Assert.That(r.Cards[7].CardId, Is.EqualTo(legFoil.Id),
|
||||
"legendary-special slot 8 must be the foil-legendary when animated rate is forced to 1.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression guard for the 2026-05-24 slot-8-ignores-PerSlot-override bug. Invariant: a
|
||||
/// freshly-constructed <see cref="PackRateConfig"/> has an EMPTY PerSlot list. The original
|
||||
/// trigger (EF Core 8's <c>OwnsMany</c>+<c>ToJson</c> jsonb materialisation appending rows
|
||||
/// onto whatever the parent's ctor produced — leaving two slot-8 entries where the seeded one
|
||||
/// silently won <see cref="PackOpenService.ResolveWeights"/>'s <c>FirstOrDefault</c>) is gone
|
||||
/// now (config goes through <c>IGameConfigService</c> + STJ, which replaces correctly). The
|
||||
/// invariant stays because any future config layer that hydrates into a pre-initialised
|
||||
/// collection (custom deserialiser, ORM, manual Add loop) would resurrect the same failure
|
||||
/// mode. Defaults for collections live in <see cref="PackRateConfig.ShippedDefaults"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PackRateConfig_PerSlot_defaults_to_empty_to_avoid_jsonb_append_bug()
|
||||
{
|
||||
Assert.That(new PackRateConfig().PerSlot, Is.Empty,
|
||||
"PackRateConfig.PerSlot must default to empty — see test docstring for why.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user