Compare commits

...

2 Commits

Author SHA1 Message Date
Hamcha 65a4a2e6bd
Add basic implementation of I8PCube 2019-05-31 14:21:50 +02:00
Hamcha 1a3d72a006
Add string serialization and separate tests 2019-05-31 14:07:49 +02:00
6 changed files with 275 additions and 31 deletions

View File

@ -1,11 +1,18 @@
package mlp package mlp
import "git.fromouter.space/mcg/draft" import (
"math/rand"
"git.fromouter.space/mcg/draft"
)
// I8PCube is a cube that uses I8Pages' pack schema // I8PCube is a cube that uses I8Pages' pack schema
// I8Pages' cube uses different kinds of packs, so a single schema is not possible. // I8Pages' cube uses different kinds of packs, so a single schema is not possible.
// Therefore, I8PCube itself is not a valid set, but contains two valid sets for // Therefore, I8PCube itself is not a valid set, but contains two valid sets for
// both types of packs (main deck / problems) // both types of packs (main deck / problems)
//
// Infos on I8Pages' cube:
// https://docs.google.com/spreadsheets/d/1Ufz4QLwLCZ1vLemAhE6cuAmu2lq_VAN7RhLO9p1opNQ
type I8PCube struct { type I8PCube struct {
Main *I8PSet Main *I8PSet
Problems *I8PSet Problems *I8PSet
@ -13,22 +20,140 @@ type I8PCube struct {
// I8PSet is one of the sets of packs contained in a I8PCube // I8PSet is one of the sets of packs contained in a I8PCube
type I8PSet struct { type I8PSet struct {
Cards map[string][]Card Cards I8PPool
Schema draft.PackSchema Schema draft.PackSchema
} }
// I8PType is a category of cards to be seeded into packs
type I8PType string
// All types used for seeding packs
const (
I8PTypeBlue I8PType = "blue"
I8PTypeOrange I8PType = "orange"
I8PTypePink I8PType = "pink"
I8PTypePurple I8PType = "purple"
I8PTypeWhite I8PType = "white"
I8PTypeYellow I8PType = "yellow"
I8PTypeNone I8PType = "none"
I8PTypeMulti I8PType = "multi"
I8PTypeEntry I8PType = "entry"
I8PTypeProblem I8PType = "problem"
)
// I8PPool is a pool of card divided into categories
type I8PPool map[I8PType][]Card
// MakeI8PCube takes an organized set of cards and sorts them into a draftable I8PCube
func MakeI8PCube(cards I8PPool) *I8PCube {
return &I8PCube{
Main: makeMainSet(cards),
Problems: makeProblemSet(cards),
}
}
func makeMainSet(cards I8PPool) (set *I8PSet) {
set = &I8PSet{
Cards: I8PPool{
I8PTypeBlue: cards[I8PTypeBlue],
I8PTypeOrange: cards[I8PTypeOrange],
I8PTypePink: cards[I8PTypePink],
I8PTypePurple: cards[I8PTypePurple],
I8PTypeWhite: cards[I8PTypeWhite],
I8PTypeYellow: cards[I8PTypeYellow],
I8PTypeNone: cards[I8PTypeNone],
I8PTypeMulti: cards[I8PTypeMulti],
I8PTypeEntry: cards[I8PTypeEntry],
},
}
//TODO Make schema more flexible
set.Schema = draft.PackSchema{
Slots: []draft.PackSlot{
{Amount: 1, Provider: set.ProviderByType(I8PTypeBlue)},
{Amount: 1, Provider: set.ProviderByType(I8PTypeOrange)},
{Amount: 1, Provider: set.ProviderByType(I8PTypePink)},
{Amount: 1, Provider: set.ProviderByType(I8PTypePurple)},
{Amount: 1, Provider: set.ProviderByType(I8PTypeWhite)},
{Amount: 1, Provider: set.ProviderByType(I8PTypeYellow)},
{Amount: 1, Provider: set.ProviderByType(I8PTypeNone)},
{Amount: 2, Provider: set.ProviderByType(I8PTypeMulti)},
{Amount: 2, Provider: set.ProviderByType(I8PTypeEntry)},
{Amount: 1, Provider: set.ProviderOther()},
},
}
return
}
func makeProblemSet(cards I8PPool) (set *I8PSet) {
set = &I8PSet{
Cards: I8PPool{
I8PTypeProblem: cards[I8PTypeProblem],
},
}
set.Schema = draft.PackSchema{
Slots: []draft.PackSlot{
{
Amount: 12,
Provider: set.ProviderByType(I8PTypeProblem),
},
},
}
return
}
// ProviderByType provides cards from a specified category
func (s *I8PSet) ProviderByType(typ I8PType) draft.CardProvider {
return func(n int) (out []draft.Card) {
s.shuffle(typ)
if len(s.Cards[typ]) < n {
n = len(s.Cards[typ])
}
out, s.Cards[typ] = toDraft(s.Cards[typ][:n]), s.Cards[typ][n:]
return
}
}
// ProviderOther picks a random card for any available set (except Problems)
func (s *I8PSet) ProviderOther() draft.CardProvider {
choices := []I8PType{
I8PTypeBlue,
I8PTypeOrange,
I8PTypePink,
I8PTypePurple,
I8PTypeWhite,
I8PTypeYellow,
I8PTypeNone,
I8PTypeMulti,
I8PTypeEntry,
}
return func(n int) (out []draft.Card) {
// Filter types that have enough cards for what we need
// n is almost always 1, so this should not introduce bias
available := []I8PType{}
for _, typ := range choices {
if len(s.Cards[typ]) >= n {
available = append(available, typ)
}
}
// Check if there are no pools we can pick stuff from, and exit early
// THIS IS NOT NORMAL, AND YOU SHOULD FIX YOUR CUBE!
if len(available) < 1 {
return
}
typ := available[rand.Intn(len(available))]
return s.ProviderByType(typ)(n)
}
}
// PackSchema returns the pack schema for building packs from a I8PCube // PackSchema returns the pack schema for building packs from a I8PCube
func (s *I8PSet) PackSchema() draft.PackSchema { func (s *I8PSet) PackSchema() draft.PackSchema {
return s.Schema return s.Schema
} }
// MakeI8PCube takes an organized set of cards and sorts them into a draftable I8PCube func (s *I8PSet) shuffle(typ I8PType) {
func MakeI8PCube(cards map[string][]Card) *I8PCube { rand.Shuffle(len(s.Cards[typ]), func(i, j int) {
//TODO Separate problems from main deck s.Cards[typ][i], s.Cards[typ][j] = s.Cards[typ][j], s.Cards[typ][i]
//TODO Make schemas })
return &I8PCube{
Main: &I8PSet{
Cards: cards,
},
}
} }

74
mlp/i8pcube_test.go Normal file
View File

@ -0,0 +1,74 @@
package mlp_test
import (
"fmt"
"testing"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/mcg/draft/mlp"
)
// TestDraftI8PCube sets up a I8PCube and drafts 3 packs from it
func TestDraftI8PCube(t *testing.T) {
pool := mlp.I8PPool{
mlp.I8PTypeBlue: mockCards("b1", "b2", "b3"),
mlp.I8PTypeOrange: mockCards("o1", "o2", "o3"),
mlp.I8PTypePink: mockCards("p1", "p2", "p3"),
mlp.I8PTypePurple: mockCards("u1", "u2", "u3"),
mlp.I8PTypeWhite: mockCards("w1", "w2", "w3"),
mlp.I8PTypeYellow: mockCards("y1", "y2", "y3"),
mlp.I8PTypeNone: mockCards("n1", "n2", "n3"),
mlp.I8PTypeMulti: mockCards("m1", "m2", "m3", "m4"),
mlp.I8PTypeEntry: mockCards("e1", "e2", "e3", "e4"),
mlp.I8PTypeProblem: mockCards("P1", "P2"),
}
cube := mlp.MakeI8PCube(pool)
pack1 := draft.MakePack(cube.Main)
pack2 := draft.MakePack(cube.Main)
pack3 := draft.MakePack(cube.Problems)
if len(pack1) != 12 {
t.Errorf("Expected 12 cards in pack 1 but got %d", len(pack1))
}
if len(pack2) != 12 {
t.Errorf("Expected 12 cards in pack 2 but got %d", len(pack2))
}
if len(pack3) != 2 {
t.Errorf("Expected 2 cards in pack 3 but got %d", len(pack3))
}
fmt.Printf("Cards in pack1: %s\n", pack1)
fmt.Printf("Cards in pack2: %s\n", pack2)
fmt.Printf("Cards in pack3: %s\n", pack3)
}
// TestDryCube tries drying up the cube to make the "OtherProvider" not able to pick a card
// Expected behavior is to have a 11 card pack generate and not crash.
// This should **never** happen! If it does, please go fix your cube!
func TestDryCube(t *testing.T) {
pool := mlp.I8PPool{
mlp.I8PTypeBlue: mockCards("b1"),
mlp.I8PTypeOrange: mockCards("o1"),
mlp.I8PTypePink: mockCards("p1"),
mlp.I8PTypePurple: mockCards("u1"),
mlp.I8PTypeWhite: mockCards("w1"),
mlp.I8PTypeYellow: mockCards("y1"),
mlp.I8PTypeNone: mockCards("n1"),
mlp.I8PTypeMulti: mockCards("m1", "m4"),
mlp.I8PTypeEntry: mockCards("e1", "e4"),
}
cube := mlp.MakeI8PCube(pool)
pack := draft.MakePack(cube.Main)
if len(pack) != 11 {
t.Errorf("Expected 11 cards in pack but got %d", len(pack))
}
}
func mockCards(ids ...string) []mlp.Card {
out := make([]mlp.Card, len(ids))
for i, id := range ids {
out[i] = mlp.Card{ID: id}
}
return out
}

View File

@ -5,6 +5,8 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"git.fromouter.space/mcg/draft"
) )
// Set is a set/expansion of MLP:CCG // Set is a set/expansion of MLP:CCG
@ -33,6 +35,13 @@ type Card struct {
ProblemRequirement PowerRequirement `json:",omitempty"` ProblemRequirement PowerRequirement `json:",omitempty"`
} }
// ToDraftCard converts cards to draft.Card
func (c Card) ToDraftCard() draft.Card {
return draft.Card{
ID: c.ID,
}
}
// PowerRequirement denotes one or more power requirements, colored or not // PowerRequirement denotes one or more power requirements, colored or not
type PowerRequirement map[string]int type PowerRequirement map[string]int

12
mlp/utils.go Normal file
View File

@ -0,0 +1,12 @@
package mlp
import "git.fromouter.space/mcg/draft"
// Converts multiple mlp.Card to draft.Card
func toDraft(cards []Card) []draft.Card {
out := make([]draft.Card, len(cards))
for i, card := range cards {
out[i] = card.ToDraftCard()
}
return out
}

14
pack.go
View File

@ -1,6 +1,9 @@
package draft // import "git.fromouter.space/mcg/draft" package draft // import "git.fromouter.space/mcg/draft"
import "math/rand" import (
"math/rand"
"strings"
)
// Pack is a collection of cards from a booster pack // Pack is a collection of cards from a booster pack
type Pack []Card type Pack []Card
@ -66,3 +69,12 @@ func MakePackWithSchema(schema PackSchema) Pack {
} }
return pack return pack
} }
// String encodes a pack to a list of space-separated IDs
func (p Pack) String() (str string) {
for _, card := range p {
str += " " + card.ID
}
str = strings.TrimSpace(str)
return
}

View File

@ -1,21 +1,23 @@
package draft package draft_test
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"testing" "testing"
"git.fromouter.space/mcg/draft"
) )
// TestSetRepeatable makes sure that a set can generate more cards than it contains // TestSetRepeatable makes sure that a set can generate more cards than it contains
func TestSetRepeatable(t *testing.T) { func TestSetRepeatable(t *testing.T) {
const PACKSIZE = 5 const PACKSIZE = 5
s := &GenericSet{ s := &draft.GenericSet{
Cards: []Card{{ID: "a"}, {ID: "b"}, {ID: "c"}}, Cards: []draft.Card{{ID: "a"}, {ID: "b"}, {ID: "c"}},
PackSize: PACKSIZE, PackSize: PACKSIZE,
} }
// Create a pack // Create a pack
pack := MakePack(s) pack := draft.MakePack(s)
if len(pack) < PACKSIZE { if len(pack) < PACKSIZE {
t.Errorf("Pack expected to contain %d cards, contains %d", PACKSIZE, len(pack)) t.Errorf("Pack expected to contain %d cards, contains %d", PACKSIZE, len(pack))
@ -34,17 +36,17 @@ func TestSetRepeatable(t *testing.T) {
// TestAlternateProviders tests alternate providers // TestAlternateProviders tests alternate providers
func TestAlternateProviders(t *testing.T) { func TestAlternateProviders(t *testing.T) {
customProvider := func(n int) []Card { customProvider := func(n int) []draft.Card {
out := make([]Card, n) out := make([]draft.Card, n)
for n := range out { for n := range out {
out[n] = Card{ID: "x"} out[n] = draft.Card{ID: "x"}
} }
return out return out
} }
s := &GenericSet{ s := &draft.GenericSet{
Cards: []Card{{ID: "a"}, {ID: "b"}, {ID: "c"}}, Cards: []draft.Card{{ID: "a"}, {ID: "b"}, {ID: "c"}},
PackSize: 1, PackSize: 1,
Alternates: []AlternateProvider{ Alternates: []draft.AlternateProvider{
{ {
Probability: 0.3, Probability: 0.3,
Provider: customProvider, Provider: customProvider,
@ -56,7 +58,7 @@ func TestAlternateProviders(t *testing.T) {
nonalternates, alternates := 0, 0 nonalternates, alternates := 0, 0
for i := 0; i < 500000; i++ { for i := 0; i < 500000; i++ {
pack := MakePack(s) pack := draft.MakePack(s)
if pack[0].ID == "x" { if pack[0].ID == "x" {
alternates++ alternates++
} else { } else {
@ -76,16 +78,16 @@ func TestAlternateProviders(t *testing.T) {
// TestCubeOverflow makes sure cubes stop providing cards as they are exhausted instead of going out of bound // TestCubeOverflow makes sure cubes stop providing cards as they are exhausted instead of going out of bound
func TestCubeOverflow(t *testing.T) { func TestCubeOverflow(t *testing.T) {
const PACKSIZE = 5 const PACKSIZE = 5
c := &GenericCube{ c := &draft.GenericCube{
Cards: []Card{ Cards: []draft.Card{
{ID: "a"}, {ID: "b"}, {ID: "c"}, {ID: "d"}, {ID: "e"}, {ID: "a"}, {ID: "b"}, {ID: "c"}, {ID: "d"}, {ID: "e"},
{ID: "f"}, {ID: "g"}, {ID: "h"}, {ID: "i"}, {ID: "f"}, {ID: "g"}, {ID: "h"}, {ID: "i"},
}, },
PackSize: PACKSIZE, PackSize: PACKSIZE,
} }
pack1 := MakePack(c) pack1 := draft.MakePack(c)
pack2 := MakePack(c) pack2 := draft.MakePack(c)
// Pack 2 can only contain 4 cards, as there are not enough cards to fill it // Pack 2 can only contain 4 cards, as there are not enough cards to fill it
if len(pack2) >= PACKSIZE { if len(pack2) >= PACKSIZE {
@ -100,11 +102,21 @@ func TestCubeOverflow(t *testing.T) {
} }
} }
// TestPackString makes sure packs are serialized correctly
func TestPackString(t *testing.T) {
p := draft.Pack{{ID: "a"}, {ID: "b"}, {ID: "c"}}
expected := "a b c"
if p.String() != expected {
t.Errorf("Expected \"%s\" but got \"%s\"", expected, p)
}
}
// ExampleGenericSet is an example usage of the Set APIs to make packs // ExampleGenericSet is an example usage of the Set APIs to make packs
func ExampleGenericSet() { func ExampleGenericSet() {
// Create a set with some items // Create a set with some items
s := &GenericSet{ s := &draft.GenericSet{
Cards: []Card{ Cards: []draft.Card{
{ID: "a"}, {ID: "a"},
{ID: "b"}, {ID: "b"},
{ID: "c"}, {ID: "c"},
@ -113,7 +125,7 @@ func ExampleGenericSet() {
} }
// Create a pack // Create a pack
pack := MakePack(s) pack := draft.MakePack(s)
// Print cards in pack // Print cards in pack
for i, card := range pack { for i, card := range pack {
@ -122,7 +134,7 @@ func ExampleGenericSet() {
} }
// https://gist.github.com/alioygur/16c66b4249cb42715091fe010eec7e33 // https://gist.github.com/alioygur/16c66b4249cb42715091fe010eec7e33
func sliceUniq(s []Card) []Card { func sliceUniq(s []draft.Card) []draft.Card {
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
for i2 := i + 1; i2 < len(s); i2++ { for i2 := i + 1; i2 < len(s); i2++ {
if s[i].ID == s[i2].ID { if s[i].ID == s[i2].ID {