Add basic booster pack generation
This commit is contained in:
parent
eade73f9f5
commit
cd1c26249c
2 changed files with 232 additions and 0 deletions
212
src/mlpccg/draft/booster.ts
Normal file
212
src/mlpccg/draft/booster.ts
Normal file
|
@ -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<PackSchema> {
|
||||||
|
// 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<PackBuilder> {
|
||||||
|
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<string, Card[]> {
|
||||||
|
return pool.reduce((map, current) => {
|
||||||
|
if (!(current.Rarity in map)) {
|
||||||
|
map[current.Rarity] = [];
|
||||||
|
}
|
||||||
|
map[current.Rarity] = current;
|
||||||
|
return map;
|
||||||
|
}, Object.create(null));
|
||||||
|
}
|
20
src/mlpccg/draft/types.ts
Normal file
20
src/mlpccg/draft/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Card } from "../types";
|
||||||
|
|
||||||
|
export type Provider = Iterator<Card>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in a new issue