From cd1c26249c5497c8fd6679db625d223499952cef Mon Sep 17 00:00:00 2001 From: Hamcha Date: Wed, 11 Sep 2019 14:42:44 +0200 Subject: [PATCH] Add basic booster pack generation --- src/mlpccg/draft/booster.ts | 212 ++++++++++++++++++++++++++++++++++++ src/mlpccg/draft/types.ts | 20 ++++ 2 files changed, 232 insertions(+) create mode 100644 src/mlpccg/draft/booster.ts create mode 100644 src/mlpccg/draft/types.ts diff --git a/src/mlpccg/draft/booster.ts b/src/mlpccg/draft/booster.ts new file mode 100644 index 0000000..59b5143 --- /dev/null +++ b/src/mlpccg/draft/booster.ts @@ -0,0 +1,212 @@ +import { Card } from "../types"; +import { Pack, PackSchema, AlternateProvider } from "./types"; +import { getCards } from "../database"; + +/* + +(This data was taken from the MLP:CCG wikia at mlpccg.fandom.com and confirmed + by people at the MLP:CCG Discord) + +Distribution rates for packs is usually 8 commons, 3 uncommons and 1 rare. + +No Fixed or Promo cards can be found in packs. + +UR distribution depends on set: + - PR has 1/13 chance of UR replacing a common + - CN->AD has 1/11 chance of UR replacing a common + - EO->FF has 1/3 chance of SR/UR replacing a common + +SR are twice as common as UR, so that's one more thing to keep in mind. + +Lastly, RR can replace another common in the ratio of ~1/2 every 6 boxes, depending +on set. Specifically, this is the RR ratio for each set: + - EO->HM: 1/108 + - MT->FF: 1/216 + +*/ + +// Returns the pack schema for a specific set +async function setSchema(set: string): Promise { + // Force set name to uppercase + set = set.toUpperCase(); + + // Return blank schemas for invalid sets + if (set == "RR" || set == "CS") { + return { slots: [] }; + } + + // Get cards for set + let cards = await getCards({ Sets: [set] }); + let cardMap = spanByRarity(cards); + + let rr: AlternateProvider[] = []; + let srur: AlternateProvider[] = []; + + // Check for RR chances + /* + switch (set) { + case "EO": + case "HM": + rr = [ + { + probability: 1.0 / 108.0, + provider: randomProvider([ + //TODO + ]) + } + ]; + break; + case "MT": + case "HM": + case "SB": + case "FF": + rr = [ + { + probability: 1.0 / 216.0, + provider: randomProvider([ + //TODO + ]) + } + ]; + break; + } + */ + + // Check for SR/UR chances + switch (set) { + case "PR": + srur = [ + { + probability: 1.0 / 13.0, + provider: randomProvider(cardMap["UR"]) + } + ]; + break; + case "CN": + case "CG": + case "AD": + srur = [ + { + probability: 1.0 / 11.0, + provider: randomProvider(cardMap["UR"]) + } + ]; + break; + default: + srur = [ + { + probability: (1.0 / 9.0) * 2.0, + provider: randomProvider(cardMap["SR"]) + }, + { + probability: 1.0 / 9.0, + provider: randomProvider(cardMap["UR"]) + } + ]; + break; + } + + return { + slots: [ + { + amount: 6, + provider: randomProvider(cardMap["C"]), + alternate: [] + }, + { + amount: 1, + provider: randomProvider(cardMap["C"]), + alternate: rr + }, + { + amount: 1, + provider: randomProvider(cardMap["C"]), + alternate: srur + }, + { + amount: 1, + provider: randomProvider(cardMap["R"]), + alternate: [] + }, + { + amount: 3, + provider: randomProvider(cardMap["U"]), + alternate: [] + } + ] + }; +} + +export class PackBuilder { + schema: PackSchema; + constructor(schema: PackSchema) { + this.schema = schema; + } + + buildPack(): Pack { + let pack = []; + for (const slot of this.schema.slots) { + let provider = slot.provider; + + // Check for alternates by generating a random and checking cumulated + // probability. Ie. if one card would show 5% of the time, another would + // show up 10% of the time, the algorithm would do something like this: + // + // With Math.random() == 0.85: + // ALTERNATE NO ALTERNATE + // [0.00-0.05][0.06----0.15][0.16------------1.00] + // ^ 0.85 + // + // With Math.random() == 0.03: + // ALTERNATE NO ALTERNATE + // [0.00-0.05][0.06----0.15][0.16------------1.00] + // ^ 0.03 + + const rnd = Math.random(); + let currentProb = 0; + for (const alternate of slot.alternate) { + currentProb += alternate.probability; + // Alternate matched + if (currentProb > rnd) { + provider = alternate.provider; + break; + } + } + for (let i = 0; i < slot.amount; i++) { + const res = provider.next(); + if (res.done) { + // No more cards to get from this, exit early + break; + } + pack.push(res.value); + } + } + return pack; + } + + static async fromSet(set: string): Promise { + let schema = await setSchema(set); + let builder = new PackBuilder(schema); + return builder; + } +} + +// Yields random cards from a chosen pool +function* randomProvider(pool: Card[]) { + while (true) { + const idx = Math.floor(Math.random() * pool.length); + yield pool[idx]; + } +} + +// Divides a list of card to a map of rarities +// ie. [ff14, ff16, ff17] => { "C" : ["ff14"], "U": ["ff17"], "R": ["ff16"] } +function spanByRarity(pool: Card[]): Record { + return pool.reduce((map, current) => { + if (!(current.Rarity in map)) { + map[current.Rarity] = []; + } + map[current.Rarity] = current; + return map; + }, Object.create(null)); +} diff --git a/src/mlpccg/draft/types.ts b/src/mlpccg/draft/types.ts new file mode 100644 index 0000000..62be937 --- /dev/null +++ b/src/mlpccg/draft/types.ts @@ -0,0 +1,20 @@ +import { Card } from "../types"; + +export type Provider = Iterator; + +export type Pack = Card[]; + +export interface PackSchema { + slots: PackSlot[]; +} + +export interface PackSlot { + amount: number; + provider: Provider; + alternate: AlternateProvider[]; +} + +export interface AlternateProvider { + probability: number; + provider: Provider; +}