feat(packs): PackDrawTableImporter with fixture tests
Idempotent upsert keyed on pack_id; slot rates and card weights are wiped per pack and reinserted. String slot/tier in the seed translate to enum at import time. Tests: - Imports_config_slot_rates_and_card_weights - Is_idempotent_on_rerun Fixtures live under SVSim.Bootstrap/Data/test-fixtures/seeds/ and overlay the production seeds via the existing csproj rule (test-fixture file beats production file at same Link path). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "bronze", "card_id": 100, "rate_pct": 50.0, "is_leader": false, "is_alt_art": false },
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "bronze", "card_id": 101, "rate_pct": 26.5, "is_leader": false, "is_alt_art": false },
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "legendary", "card_id": 200, "rate_pct": 1.5, "is_leader": false, "is_alt_art": false },
|
||||
{ "pack_id": 98001, "slot": "bonus", "tier": "special", "card_id": 300, "rate_pct": null, "is_leader": true, "is_alt_art": false },
|
||||
{ "pack_id": 98001, "slot": "bonus", "tier": "special", "card_id": 301, "rate_pct": null, "is_leader": true, "is_alt_art": false }
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{ "pack_id": 10000, "short_code": "Basic", "animation_rate_pct": 8.0, "has_bonus_slot": false, "special_kind": null },
|
||||
{ "pack_id": 98001, "short_code": "98ANV", "animation_rate_pct": 8.0, "has_bonus_slot": true, "special_kind": "leader_card" }
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "bronze", "rate_pct": 76.5 },
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "silver", "rate_pct": 16.0 },
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "gold", "rate_pct": 6.0 },
|
||||
{ "pack_id": 10000, "slot": "general", "tier": "legendary", "rate_pct": 1.5 },
|
||||
{ "pack_id": 10000, "slot": "eighth", "tier": "silver", "rate_pct": 92.5 },
|
||||
{ "pack_id": 10000, "slot": "eighth", "tier": "gold", "rate_pct": 6.0 },
|
||||
{ "pack_id": 10000, "slot": "eighth", "tier": "legendary", "rate_pct": 1.5 },
|
||||
{ "pack_id": 98001, "slot": "bonus", "tier": "special", "rate_pct": 100.0 }
|
||||
]
|
||||
102
SVSim.Bootstrap/Importers/PackDrawTableImporter.cs
Normal file
102
SVSim.Bootstrap/Importers/PackDrawTableImporter.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Bootstrap.Models.Seed;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent upsert of the per-pack draw table from
|
||||
/// <c>seeds/pack-draw-config.json</c>, <c>pack-draw-slot-rates.json</c>, and
|
||||
/// <c>pack-draw-card-weights.json</c>. Replaces wholesale per pack (config keyed on
|
||||
/// pack_id; slot rates / card weights wiped and reinserted) — the upstream data is
|
||||
/// post-shutdown closed, so we do not preserve hand-edits on these tables.
|
||||
/// </summary>
|
||||
public class PackDrawTableImporter
|
||||
{
|
||||
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||
{
|
||||
var configs = SeedLoader.LoadList<PackDrawConfigSeed>(Path.Combine(seedDir, "pack-draw-config.json"));
|
||||
var slotRates = SeedLoader.LoadList<PackDrawSlotRateSeed>(Path.Combine(seedDir, "pack-draw-slot-rates.json"));
|
||||
var cardWeights = SeedLoader.LoadList<PackDrawCardWeightSeed>(Path.Combine(seedDir, "pack-draw-card-weights.json"));
|
||||
|
||||
if (configs.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[PackDrawTableImporter] No seed rows; skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var seedPackIds = configs.Select(c => c.PackId).ToHashSet();
|
||||
|
||||
// Full-replace strategy: wipe rows for any pack in the seed, then reinsert.
|
||||
await context.PackDrawCardWeights
|
||||
.Where(w => seedPackIds.Contains(w.PackId))
|
||||
.ExecuteDeleteAsync();
|
||||
await context.PackDrawSlotRates
|
||||
.Where(s => seedPackIds.Contains(s.PackId))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
var existingConfigs = await context.PackDrawConfigs
|
||||
.Where(c => seedPackIds.Contains(c.Id))
|
||||
.ToDictionaryAsync(c => c.Id);
|
||||
|
||||
foreach (var s in configs)
|
||||
{
|
||||
var row = existingConfigs.TryGetValue(s.PackId, out var ex)
|
||||
? ex : new PackDrawConfigEntry { Id = s.PackId };
|
||||
row.AnimationRatePct = s.AnimationRatePct;
|
||||
row.HasBonusSlot = s.HasBonusSlot;
|
||||
row.SpecialKind = s.SpecialKind;
|
||||
row.ShortCode = s.ShortCode;
|
||||
if (ex is null) context.PackDrawConfigs.Add(row);
|
||||
}
|
||||
|
||||
foreach (var s in slotRates)
|
||||
{
|
||||
context.PackDrawSlotRates.Add(new PackDrawSlotRateEntry
|
||||
{
|
||||
PackId = s.PackId,
|
||||
Slot = ParseSlot(s.Slot),
|
||||
Tier = ParseTier(s.Tier),
|
||||
RatePct = s.RatePct,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var s in cardWeights)
|
||||
{
|
||||
context.PackDrawCardWeights.Add(new PackDrawCardWeightEntry
|
||||
{
|
||||
PackId = s.PackId,
|
||||
Slot = ParseSlot(s.Slot),
|
||||
Tier = ParseTier(s.Tier),
|
||||
CardId = s.CardId,
|
||||
RatePct = s.RatePct,
|
||||
IsLeader = s.IsLeader,
|
||||
IsAltArt = s.IsAltArt,
|
||||
});
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[PackDrawTableImporter] {configs.Count} configs / {slotRates.Count} slot rates / {cardWeights.Count} card weights");
|
||||
return configs.Count;
|
||||
}
|
||||
|
||||
private static DrawSlot ParseSlot(string s) => s switch
|
||||
{
|
||||
"general" => DrawSlot.General,
|
||||
"eighth" => DrawSlot.Eighth,
|
||||
"bonus" => DrawSlot.Bonus,
|
||||
_ => throw new InvalidDataException($"PackDrawTableImporter: unknown slot \"{s}\""),
|
||||
};
|
||||
|
||||
private static DrawTier ParseTier(string s) => s switch
|
||||
{
|
||||
"bronze" => DrawTier.Bronze,
|
||||
"silver" => DrawTier.Silver,
|
||||
"gold" => DrawTier.Gold,
|
||||
"legendary" => DrawTier.Legendary,
|
||||
"special" => DrawTier.Special,
|
||||
_ => throw new InvalidDataException($"PackDrawTableImporter: unknown tier \"{s}\""),
|
||||
};
|
||||
}
|
||||
48
SVSim.UnitTests/Importers/PackDrawTableImporterTests.cs
Normal file
48
SVSim.UnitTests/Importers/PackDrawTableImporterTests.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Importers;
|
||||
|
||||
public class PackDrawTableImporterTests
|
||||
{
|
||||
private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
|
||||
|
||||
[Test]
|
||||
public async Task Imports_config_slot_rates_and_card_weights()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new PackDrawTableImporter().ImportAsync(db, SeedDir);
|
||||
|
||||
Assert.That(await db.PackDrawConfigs.CountAsync(), Is.EqualTo(2));
|
||||
Assert.That(await db.PackDrawSlotRates.CountAsync(), Is.EqualTo(8));
|
||||
Assert.That(await db.PackDrawCardWeights.CountAsync(), Is.EqualTo(5));
|
||||
|
||||
var bonus = await db.PackDrawCardWeights
|
||||
.Where(w => w.PackId == 98001 && w.Slot == DrawSlot.Bonus)
|
||||
.ToListAsync();
|
||||
Assert.That(bonus.Count, Is.EqualTo(2));
|
||||
Assert.That(bonus.All(w => w.RatePct == null && w.IsLeader && w.Tier == DrawTier.Special), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Is_idempotent_on_rerun()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
|
||||
await new PackDrawTableImporter().ImportAsync(db, SeedDir);
|
||||
int before = await db.PackDrawCardWeights.CountAsync();
|
||||
await new PackDrawTableImporter().ImportAsync(db, SeedDir);
|
||||
int after = await db.PackDrawCardWeights.CountAsync();
|
||||
|
||||
Assert.That(after, Is.EqualTo(before));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user