Sampler is now driven by PackDrawTable: roll DrawTier per slot by cumulative slot-rate weights, then pick a card within tier by per-card weights renormalized within the tier. Rate-less Guaranteed-Leader-Card rows draw uniform over (pool minus owned), falling back to the full pool when all are owned. Bonus slot fires once at the end of a 10-pack open when HasBonusSlot is set. Slot 8 falls back to the general slot's per-card weights for the rolled tier when slot-8 has only a rarity-level rate quoted (the common shape on normal packs). PackController.Open loads the draw table + viewer owned card ids and passes them to the sampler; the category-based forced-Legendary slot-8 override is gone. ICardFoilLookup replaces ICardPoolProvider for the foil-twin heuristic. Drops the test-fixture pack-draw seed overlay so the production seed flows through the importer tests; controller tests that fabricate their own card sets now call factory.SeedPackDrawTableAsync(...) to install a matching stub draw table. WeightedPick helper handles the cumulative-band roll for both stages. Five sampler tests + four WeightedPick tests + five importer/repo tests; full suite is 653/653 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
171 lines
8.2 KiB
C#
171 lines
8.2 KiB
C#
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models;
|
|
using SVSim.Database.Repositories.PackDrawTables;
|
|
using SVSim.Database.Services;
|
|
using SVSim.EmulatedEntrypoint.Services;
|
|
|
|
namespace SVSim.UnitTests.Services;
|
|
|
|
public class PackOpenServiceTests
|
|
{
|
|
private sealed class ScriptedRandom : IRandom
|
|
{
|
|
private readonly double[] _seq; private int _i;
|
|
public ScriptedRandom(params double[] seq) { _seq = seq; }
|
|
public double NextDouble() => _seq[_i++ % _seq.Length];
|
|
public int Next(int maxExclusive) => (int)(NextDouble() * maxExclusive);
|
|
}
|
|
|
|
private sealed class NoFoil : ICardFoilLookup
|
|
{
|
|
public ShadowverseCardEntry? TryGetFoilTwin(long baseCardId) => null;
|
|
}
|
|
|
|
private static PackConfigEntry StandardPack(int id = 10000) => new()
|
|
{
|
|
Id = id, BasePackId = id, PackCategory = PackCategory.None,
|
|
};
|
|
|
|
private static PackDrawTable AllBronzeTable() => new()
|
|
{
|
|
Config = new PackDrawConfigEntry { Id = 10000, AnimationRatePct = 0 },
|
|
SlotRates = new[]
|
|
{
|
|
new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
new PackDrawSlotRateEntry { PackId = 10000, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
},
|
|
CardWeights = new[]
|
|
{
|
|
new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 1, RatePct = 70 },
|
|
new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 2, RatePct = 30 },
|
|
new PackDrawCardWeightEntry { PackId = 10000, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, CardId = 1, RatePct = 100 },
|
|
},
|
|
};
|
|
|
|
[Test]
|
|
public void Draw_returns_eight_cards_for_one_pack()
|
|
{
|
|
var svc = new PackOpenService();
|
|
var rng = new ScriptedRandom(0.1);
|
|
|
|
var result = svc.Draw(AllBronzeTable(), StandardPack(), 1,
|
|
excludeCardIds: Array.Empty<long>(), ownedCardIds: Array.Empty<long>(),
|
|
new NoFoil(), rng);
|
|
|
|
Assert.That(result.Cards.Count, Is.EqualTo(8));
|
|
Assert.That(result.Cards.All(c => c.CardId == 1), Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void Draw_picks_card_by_per_card_weight_within_tier()
|
|
{
|
|
var svc = new PackOpenService();
|
|
// Tier roll always lands in Bronze (only tier). Card pick rng=0.8 -> within Bronze
|
|
// band > 0.7 -> card 2. Slot 8 has only card 1 in its pool so it always picks card 1.
|
|
var rng = new ScriptedRandom(0.0, 0.8);
|
|
|
|
var result = svc.Draw(AllBronzeTable(), StandardPack(), 1,
|
|
Array.Empty<long>(), Array.Empty<long>(), new NoFoil(), rng);
|
|
|
|
Assert.That(result.Cards.Take(7).All(c => c.CardId == 2), Is.True, "slots 1-7 should pick card 2");
|
|
Assert.That(result.Cards[7].CardId, Is.EqualTo(1), "slot 8 pool only contains card 1");
|
|
}
|
|
|
|
[Test]
|
|
public void Draw_rate_less_branch_picks_only_unowned()
|
|
{
|
|
var pack = new PackConfigEntry { Id = 98001, BasePackId = 98001, PackCategory = PackCategory.SpecialCardPack };
|
|
var table = new PackDrawTable
|
|
{
|
|
Config = new PackDrawConfigEntry { Id = 98001, AnimationRatePct = 0, HasBonusSlot = true, SpecialKind = "leader_card" },
|
|
SlotRates = new[]
|
|
{
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, RatePct = 100.0 },
|
|
},
|
|
CardWeights = new[]
|
|
{
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 10, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, CardId = 10, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 300, RatePct = null, IsLeader = true },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 301, RatePct = null, IsLeader = true },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 302, RatePct = null, IsLeader = true },
|
|
},
|
|
};
|
|
var svc = new PackOpenService();
|
|
var rng = new ScriptedRandom(0.1);
|
|
|
|
var result = svc.Draw(table, pack, packNumber: 10,
|
|
excludeCardIds: Array.Empty<long>(),
|
|
ownedCardIds: new long[] { 300, 301 },
|
|
new NoFoil(), rng);
|
|
|
|
Assert.That(result.Cards.Count, Is.EqualTo(81)); // 10 packs * 8 + 1 bonus
|
|
var bonus = result.Cards[^1];
|
|
Assert.That(bonus.CardId, Is.EqualTo(302));
|
|
}
|
|
|
|
[Test]
|
|
public void Draw_rate_less_falls_back_to_full_pool_when_all_owned()
|
|
{
|
|
var pack = new PackConfigEntry { Id = 98001, BasePackId = 98001 };
|
|
var table = new PackDrawTable
|
|
{
|
|
Config = new PackDrawConfigEntry { Id = 98001, AnimationRatePct = 0, HasBonusSlot = true },
|
|
SlotRates = new[]
|
|
{
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, RatePct = 100.0 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, RatePct = 100.0 },
|
|
},
|
|
CardWeights = new[]
|
|
{
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 10, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, CardId = 10, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 300, RatePct = null, IsLeader = true },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 301, RatePct = null, IsLeader = true },
|
|
},
|
|
};
|
|
var svc = new PackOpenService();
|
|
var rng = new ScriptedRandom(0.1);
|
|
|
|
var result = svc.Draw(table, pack, packNumber: 10,
|
|
excludeCardIds: Array.Empty<long>(),
|
|
ownedCardIds: new long[] { 300, 301 },
|
|
new NoFoil(), rng);
|
|
|
|
var bonus = result.Cards[^1];
|
|
Assert.That(bonus.CardId, Is.AnyOf(300L, 301L));
|
|
}
|
|
|
|
[Test]
|
|
public void Draw_does_not_emit_bonus_for_packNumber_less_than_10()
|
|
{
|
|
var pack = new PackConfigEntry { Id = 98001 };
|
|
var table = new PackDrawTable
|
|
{
|
|
Config = new PackDrawConfigEntry { Id = 98001, HasBonusSlot = true, AnimationRatePct = 0 },
|
|
SlotRates = new[]
|
|
{
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, RatePct = 100 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, RatePct = 100 },
|
|
new PackDrawSlotRateEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, RatePct = 100 },
|
|
},
|
|
CardWeights = new[]
|
|
{
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.General, Tier = DrawTier.Bronze, CardId = 1, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Eighth, Tier = DrawTier.Bronze, CardId = 1, RatePct = 100 },
|
|
new PackDrawCardWeightEntry { PackId = 98001, Slot = DrawSlot.Bonus, Tier = DrawTier.Special, CardId = 999, RatePct = null, IsLeader = true },
|
|
},
|
|
};
|
|
var svc = new PackOpenService();
|
|
var rng = new ScriptedRandom(0.1);
|
|
|
|
var result = svc.Draw(table, pack, packNumber: 1,
|
|
Array.Empty<long>(), Array.Empty<long>(), new NoFoil(), rng);
|
|
|
|
Assert.That(result.Cards.Count, Is.EqualTo(8));
|
|
}
|
|
}
|