using SVSim.Database.Enums; using SVSim.Database.Models; namespace SVSim.EmulatedEntrypoint.Services; /// /// Draws cards from a pack's pool using the original Shadowverse Classic rates: /// Slots 1-7: Bronze 67.44% / Silver 25% / Gold 6% / Legendary 1.5% /// Slot 8: Silver 76.92% / Gold 18.46% / Legendary 4.62% (no Bronze) /// Legendary-special packs (category 2/3, base >= 90001): slot 8 forced to Legendary. /// /// The 0.06% slack in slots 1-7 (rates sum to 99.94%) is folded into Bronze so cumulative /// weights add to exactly 1.0 — any RNG roll past the Gold band lands in either Legendary or /// Bronze, and we put it in Bronze to err on the player-unfriendly side of the spec. /// public class PackOpenService { private const int CardsPerPack = 8; public DrawResult Draw( PackConfigEntry pack, ICardPoolProvider pools, int packNumber, IReadOnlyCollection excludeCardIds, IRandom rng) { var pool = pools.GetPool(pack); if (pool.Count == 0) { throw new InvalidOperationException( $"PackOpenService: pool for pack {pack.Id} (category {pack.PackCategory}) is empty."); } var poolByRarity = pool .Where(c => !excludeCardIds.Contains(c.Id)) .GroupBy(c => c.Rarity) .ToDictionary(g => g.Key, g => g.ToList()); bool isLegendarySpecial = pack.PackCategory == PackCategory.SpecialCardPack || pack.PackCategory == PackCategory.LimitedSpecialCardPack; var slots = new List(packNumber * CardsPerPack); for (int p = 0; p < packNumber; p++) { for (int s = 0; s < CardsPerPack; s++) { Rarity rarity; if (s == CardsPerPack - 1) { // Slot 8 if (isLegendarySpecial) { rarity = Rarity.Legendary; } else { rarity = PickSlot8Rarity(rng); } } else { rarity = PickSlot1To7Rarity(rng); } var card = PickCardOfRarity(rarity, poolByRarity, rng); slots.Add(new DrawnCard(card.Id, card.Rarity)); } } return new DrawResult(slots); } private static Rarity PickSlot1To7Rarity(IRandom rng) { double r = rng.NextDouble(); // Build cumulative bands in this order: Legendary -> Gold -> Silver -> Bronze (catch-all). if (r < 0.0150) return Rarity.Legendary; // 1.5% if (r < 0.0750) return Rarity.Gold; // +6% = 7.5% if (r < 0.3250) return Rarity.Silver; // +25% = 32.5% return Rarity.Bronze; // remaining (~67.5%; absorbs 0.06% slack) } private static Rarity PickSlot8Rarity(IRandom rng) { double r = rng.NextDouble(); // Renormalized over 32.5: Legendary 4.62%, Gold 18.46%, Silver 76.92%. if (r < 0.0462) return Rarity.Legendary; if (r < 0.2308) return Rarity.Gold; // 0.0462 + 0.1846 return Rarity.Silver; } private static ShadowverseCardEntry PickCardOfRarity( Rarity rarity, Dictionary> poolByRarity, IRandom rng) { // Fallback if the rolled rarity has no cards (e.g. pool has no Legendaries): // walk down to Gold -> Silver -> Bronze. This is a safety net for sparse master data; // healthy production pools have all four rarities. Rarity[] fallback = { rarity, Rarity.Gold, Rarity.Silver, Rarity.Bronze }; foreach (var r in fallback) { if (poolByRarity.TryGetValue(r, out var list) && list.Count > 0) { return list[rng.Next(list.Count)]; } } throw new InvalidOperationException("PackOpenService: pool empty after exclude filter."); } }