Pack opening
This commit is contained in:
110
SVSim.EmulatedEntrypoint/Services/PackOpenService.cs
Normal file
110
SVSim.EmulatedEntrypoint/Services/PackOpenService.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class PackOpenService
|
||||
{
|
||||
private const int CardsPerPack = 8;
|
||||
|
||||
public DrawResult Draw(
|
||||
PackConfigEntry pack,
|
||||
ICardPoolProvider pools,
|
||||
int packNumber,
|
||||
IReadOnlyCollection<long> 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<DrawnCard>(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<Rarity, List<ShadowverseCardEntry>> 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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user