feat(packs): rewrite PackOpenService against per-pack draw table
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>
This commit is contained in:
@@ -5,6 +5,7 @@ using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Pack;
|
||||
using SVSim.Database.Repositories.PackDrawTables;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Pack;
|
||||
@@ -25,7 +26,8 @@ public class PackController : SVSimController
|
||||
|
||||
private readonly IPackRepository _packs;
|
||||
private readonly PackOpenService _opener;
|
||||
private readonly ICardPoolProvider _pools;
|
||||
private readonly IPackDrawTableRepository _drawTables;
|
||||
private readonly ICardFoilLookup _foils;
|
||||
private readonly IRandom _rng;
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly ICardAcquisitionService _acquisition;
|
||||
@@ -36,7 +38,8 @@ public class PackController : SVSimController
|
||||
public PackController(
|
||||
IPackRepository packs,
|
||||
PackOpenService opener,
|
||||
ICardPoolProvider pools,
|
||||
IPackDrawTableRepository drawTables,
|
||||
ICardFoilLookup foils,
|
||||
IRandom rng,
|
||||
SVSimDbContext db,
|
||||
ICardAcquisitionService acquisition,
|
||||
@@ -46,7 +49,8 @@ public class PackController : SVSimController
|
||||
{
|
||||
_packs = packs;
|
||||
_opener = opener;
|
||||
_pools = pools;
|
||||
_drawTables = drawTables;
|
||||
_foils = foils;
|
||||
_rng = rng;
|
||||
_db = db;
|
||||
_acquisition = acquisition;
|
||||
@@ -343,7 +347,28 @@ public class PackController : SVSimController
|
||||
|
||||
// Draw + persist. DAILY single overrides packNumber to 1 (it's a one-card open).
|
||||
int drawCount = child.IsDailySingle ? 1 : packNumber;
|
||||
var draw = _opener.Draw(pack, _pools, drawCount, request.ExcludeCardIds, _rng);
|
||||
|
||||
var drawTable = await _drawTables.GetAsync(pack.Id);
|
||||
if (drawTable is null)
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { error = "pack_draw_table_missing" });
|
||||
|
||||
// Owned card_ids for the rate-less Guaranteed-Leader-Card branch. Project to longs to
|
||||
// avoid pulling viewer.Cards entities into memory. Shadow-FK access (EF.Property) per
|
||||
// the project_ef_nav_include_pitfall memory.
|
||||
var ownedCardIds = await _db.Viewers
|
||||
.Where(v => v.Id == viewerId)
|
||||
.SelectMany(v => v.Cards)
|
||||
.Select(c => (long)EF.Property<int>(c, "CardId"))
|
||||
.ToListAsync();
|
||||
|
||||
var draw = _opener.Draw(
|
||||
drawTable,
|
||||
pack,
|
||||
drawCount,
|
||||
request.ExcludeCardIds ?? Array.Empty<long>(),
|
||||
ownedCardIds,
|
||||
_foils,
|
||||
_rng);
|
||||
var grant = await _acquisition.GrantManyAsync(viewerId, draw.Cards.Select(c => c.CardId));
|
||||
|
||||
// Accrue gacha points (skip tutorial path — the starter pack isn't a real open).
|
||||
|
||||
Reference in New Issue
Block a user