diff --git a/README.md b/README.md index 3ddf27a..e71f227 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# mlp-draft +# draft -Libraries and servers for MLP:CCG drafting \ No newline at end of file +Libraries and servers for drafting, with focus on MLP:CCG drafting diff --git a/cube.go b/cube.go new file mode 100644 index 0000000..f2b9936 --- /dev/null +++ b/cube.go @@ -0,0 +1,5 @@ +package draft + +type Cube struct { + Cards []Card +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..97b66de --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.fromouter.space/mcg/draft + +go 1.12 diff --git a/mlp/booster.go b/mlp/booster.go new file mode 100644 index 0000000..9112730 --- /dev/null +++ b/mlp/booster.go @@ -0,0 +1,137 @@ +package mlp + +import ( + "math/rand" + + "git.fromouter.space/mcg/draft" +) + +/* + +(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 + +*/ + +// BoxSchema returns the pack schema from a booster box for a specific set +func (set *Set) BoxSchema() draft.PackSchema { + // Return blank schemas for invalid sets + if set.ID == SetRockNRave || set.ID == SetCelestialSolstice { + return draft.PackSchema{} + } + + var rr []draft.AlternateProvider + var srur []draft.AlternateProvider + + // Check for RR chances + switch set.ID { + case SetEquestrialOdysseys, + SetHighMagic: + rr = []draft.AlternateProvider{ + { + Probability: 1.0 / 108.0, + Provider: set.ProviderByRarity(RarityRoyalRare), + }, + } + case SetMarksInTime, + SetDefendersOfEquestria, + SetSeaquestriaBeyond, + SetFriendsForever: + rr = []draft.AlternateProvider{ + { + Probability: 1.0 / 216.0, + Provider: set.ProviderByRarity(RarityRoyalRare), + }, + } + } + + // Check for SR/UR chances + switch set.ID { + case SetPremiere: + srur = []draft.AlternateProvider{ + { + Probability: 1.0 / 13.0, + Provider: set.ProviderByRarity(RarityUltraRare), + }, + } + case SetCanterlotNights, + SetCrystalGames, + SetAbsoluteDiscord: + srur = []draft.AlternateProvider{ + { + Probability: 1.0 / 11.0, + Provider: set.ProviderByRarity(RarityUltraRare), + }, + } + default: + srur = []draft.AlternateProvider{ + { + Probability: (1.0 / 9.0) * 2.0, + Provider: set.ProviderByRarity(RaritySuperRare), + }, { + Probability: 1.0 / 9.0, + Provider: set.ProviderByRarity(RarityUltraRare), + }, + } + } + + return draft.PackSchema{ + Slots: []draft.PackSlot{ + // Fixed common slots + {Amount: 6, Provider: set.ProviderByRarity(RarityCommon)}, + // Common slot that can be replaced by RR + {Amount: 1, Provider: set.ProviderByRarity(RarityCommon), Alternate: rr}, + // Common slot that can be replaced by SR/UR + {Amount: 1, Provider: set.ProviderByRarity(RarityCommon), Alternate: srur}, + // Fixed rare and uncommon slots + {Amount: 1, Provider: set.ProviderByRarity(RarityRare)}, + {Amount: 3, Provider: set.ProviderByRarity(RarityUncommon)}, + }, + } +} + +// ProviderByRarity returns a provider for a given rarity level from a given set +func (set *Set) ProviderByRarity(rarity Rarity) draft.CardProvider { + var collection []draft.Card + // RR flow is super easy, just pick one from our hardcoded list + if rarity == RarityRoyalRare { + rr, ok := royalRares[set.ID] + // If asking for RR from a set that doesn't have one, exit early + if !ok { + return nil + } + collection = rr + } else { + //TODO Filter cards by rarity + for _, card := range set.Cards { + if card.Rarity == rarity { + collection = append(collection, draft.Card{ID: card.ID}) + } + } + } + return func(n int) []draft.Card { + out := make([]draft.Card, n) + for n := range out { + // Pick a RR at random + idx := rand.Intn(len(collection)) + out[n] = collection[idx] + } + return out + } +} diff --git a/mlp/mlp.go b/mlp/mlp.go new file mode 100644 index 0000000..4eb9c77 --- /dev/null +++ b/mlp/mlp.go @@ -0,0 +1,33 @@ +package mlp + +// Rarity denotes a card's rarity +type Rarity string + +// All card rarities +const ( + RarityCommon Rarity = "C" + RarityUncommon Rarity = "U" + RarityRare Rarity = "R" + RaritySuperRare Rarity = "SR" + RarityUltraRare Rarity = "UR" + RarityRoyalRare Rarity = "RR" +) + +// SetID denotes a card's set +type SetID string + +// All sets +const ( + SetPremiere SetID = "PR" + SetCanterlotNights SetID = "CN" + SetRockNRave SetID = "RR" + SetCelestialSolstice SetID = "CS" + SetCrystalGames SetID = "CG" + SetAbsoluteDiscord SetID = "AD" + SetEquestrialOdysseys SetID = "EO" + SetHighMagic SetID = "HM" + SetMarksInTime SetID = "MT" + SetDefendersOfEquestria SetID = "DE" + SetSeaquestriaBeyond SetID = "SB" + SetFriendsForever SetID = "FF" +) diff --git a/mlp/royalrares.go b/mlp/royalrares.go new file mode 100644 index 0000000..88ce95c --- /dev/null +++ b/mlp/royalrares.go @@ -0,0 +1,30 @@ +package mlp + +import "git.fromouter.space/mcg/draft" + +// Royal rares for each set + +var royalRares = map[SetID][]draft.Card{ + SetEquestrialOdysseys: { + draft.Card{ID: "eo207"}, // Discord, Wrathful + draft.Card{ID: "eo208"}, // Pinkie Pie, Remix Master + }, + SetHighMagic: { + draft.Card{ID: "hm149"}, // Trixie, Highest Level Unicorn + draft.Card{ID: "hm147"}, // Fluttershy, Saddle Rager + draft.Card{ID: "hm145"}, // Rarity, Radiance + }, + SetMarksInTime: { + draft.Card{ID: "mt139"}, // Rainbow Dash, One Winged Warrior + draft.Card{ID: "mt141"}, // Princess Twilight Sparkle, Time Patrol + }, + SetDefendersOfEquestria: { + draft.Card{ID: "de135"}, // Applejack, Captain of the Seven Seas + }, + SetSeaquestriaBeyond: { + draft.Card{ID: "sb135"}, // Tempest Shadow, Stormcaller + }, + SetFriendsForever: { + draft.Card{ID: "ff136"}, // Mistmane, Pillar of Beauty + }, +} diff --git a/mlp/set.go b/mlp/set.go new file mode 100644 index 0000000..2483488 --- /dev/null +++ b/mlp/set.go @@ -0,0 +1,63 @@ +package mlp + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" +) + +// Set is a set/expansion of MLP:CCG +type Set struct { + ID SetID + Name string + Cards []Card +} + +// Card is a single MLP:CCG card in a set +type Card struct { + ID string + Name string + Subname string + Element []string + Keywords []string + Traits []string + Requirement PowerRequirement `json:",omitempty"` + Cost *int `json:",omitempty"` + Power *int `json:",omitempty"` + Type string + Text string + Rarity Rarity + ProblemBonus *int `json:",omitempty"` + ProblemOpponentPower int `json:",omitempty"` + ProblemRequirement PowerRequirement `json:",omitempty"` +} + +// PowerRequirement denotes one or more power requirements, colored or not +type PowerRequirement map[string]int + +// LoadSet loads a set with a specified ID from JSON +func LoadSet(id SetID, setdata []byte) (*Set, error) { + var set Set + err := json.Unmarshal(setdata, &set) + set.ID = id + return &set, err +} + +// LoadSetHTTP loads a set using MCG's remote server +func LoadSetHTTP(id SetID) (*Set, error) { + // Get SetID as string and make it lowercase + setid := strings.ToLower(string(id)) + + resp, err := http.Get("https://mcg.zyg.ovh/setdata/" + setid + ".json") + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return LoadSet(id, data) +} diff --git a/pack.go b/pack.go new file mode 100644 index 0000000..ce1d88a --- /dev/null +++ b/pack.go @@ -0,0 +1,63 @@ +package draft // import "git.fromouter.space/mcg/draft" + +import ( + "math/rand" +) + +// Pack is a collection of cards from a booster pack +type Pack []Card + +// Card is a single card +type Card struct { + ID string +} + +// CardProvider is a function that returns as many cards of a certain types as needed +type CardProvider func(int) []Card + +// PackSchema is all that's needed to generate a certain type of pack +type PackSchema struct { + Slots []PackSlot +} + +// PackSlot is part of how packs are made, one or more providers provide +// cards for X cards of the whole pack +type PackSlot struct { + Amount int + Provider CardProvider + Alternate []AlternateProvider +} + +// AlternateProvider are Card providers that can replace one or more slots +// with special cards (foils, ultra rares) +type AlternateProvider struct { + Probability float32 + Provider CardProvider +} + +// MakePack makes a booster pack from a given schema +func MakePack(schema PackSchema) Pack { + pack := make(Pack, 0) + for _, slot := range schema.Slots { + // Default provider + provider := slot.Provider + + // Check for random alternates + if slot.Alternate != nil { + var currentProb float32 + var chosenProb = rand.Float32() + for _, alt := range slot.Alternate { + currentProb += alt.Probability + if chosenProb > currentProb { + provider = alt.Provider + break + } + } + } + + // Extract cards from provider and add them to the pack + cards := provider(slot.Amount) + pack = append(pack, cards...) + } + return pack +}