Compare commits

...

10 Commits

Author SHA1 Message Date
Hamcha 198f1a6704 Add boosted cards
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2019-08-27 12:00:12 +02:00
Hamcha 69f54c08cc Add promo as set
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-08-26 16:00:59 +02:00
Hamcha 0e2c613375 Add support for MLP promo cards
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2019-08-23 16:57:53 +02:00
Hamcha e9e5ffdada Allow problem packs to have arbitrary sizes
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2019-07-01 14:43:25 +02:00
Hamcha 3184ce476a
We don't need this as a global
continuous-integration/drone/push Build is passing Details
2019-06-28 22:45:39 +02:00
Hamcha d2a2e4526b
Add LoadAllSets
continuous-integration/drone/tag Build is passing Details
2019-06-28 22:35:47 +02:00
Hamcha 37645e37f7 Make HTTP server and URL customizable
continuous-integration/drone/push Build is passing Details
Tests now do not require internet access anymore

Closes #4
2019-06-28 17:24:47 +02:00
Hamcha c2d3b3b679 Add IDs() method for packs
continuous-integration/drone/tag Build is passing Details
2019-06-28 11:17:51 +02:00
Hamcha ac1ae4f506 Expose "ToDraft"
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2019-06-26 12:14:46 +02:00
Hamcha e4ab093861 Add function to load cards from a list 2019-06-26 12:14:36 +02:00
15 changed files with 491 additions and 59 deletions

View File

@ -108,7 +108,7 @@ func (set *Set) PackSchema() draft.PackSchema {
// 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
collection := []draft.Card{}
// RR flow is super easy, just pick one from our hardcoded list
if rarity == RarityRoyalRare {
rr, ok := royalRares[set.ID]
@ -126,6 +126,10 @@ func (set *Set) ProviderByRarity(rarity Rarity) draft.CardProvider {
}
return func(n int) []draft.Card {
out := make([]draft.Card, n)
if len(collection) < 1 {
// Something really wrong happened here
return out
}
for n := range out {
// Pick a RR at random
idx := rand.Intn(len(collection))

View File

@ -9,7 +9,6 @@ import (
// TestAlternates tries to get alternates (SR/UR/RR)
// This might require a while since it needs to generate MANY packs!
// This test *requires* an internet connection!
func TestAlternates(t *testing.T) {
// Load Premiere/CN as they have their own UR ratios
prSet, err := mlp.LoadSetHTTP(mlp.SetPremiere)

87
mlp/card.go Normal file
View File

@ -0,0 +1,87 @@
package mlp
import (
"errors"
"strings"
"git.fromouter.space/mcg/draft"
)
// 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"`
Boosted *Card `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
type PowerRequirement map[string]int
// Errors involving loading cards
var (
ErrInvalidCardID = errors.New("invalid card id")
ErrCardNotFound = errors.New("card not found in their set")
)
// LoadCardList loads cards from sets, fetching them from HTTP if needed/wanted
func LoadCardList(list []string, fetch bool) (out []Card, err error) {
out = make([]Card, len(list))
for i, card := range list {
if len(card) < 3 {
err = ErrInvalidCardID
return
}
// Get Set ID
setid := SetID(strings.ToUpper(card[:2]))
// Check for promo card
for _, promoid := range promoIDs {
if strings.ToLower(card) == promoid {
card = strings.ToLower(card)
setid = SetPromo
break
}
}
// Get set
var set *Set
if fetch {
set, err = LoadSetHTTP(setid)
} else {
set, err = LoadSetMemory(setid)
}
if err != nil {
return
}
cdata, ok := set.CardData[card]
if !ok {
err = ErrCardNotFound
return
}
out[i] = cdata
}
return
}

58
mlp/card_test.go Normal file
View File

@ -0,0 +1,58 @@
package mlp_test
import (
"testing"
"git.fromouter.space/mcg/draft/mlp"
)
func TestLoadCardList(t *testing.T) {
// Fetch two random cards
cards, err := mlp.LoadCardList([]string{"ff3", "pr54"}, true)
if err != nil {
t.Fatalf("Error while fetching cards: %s", err.Error())
}
// Check that fetched cards are the real deal
if cards[0].Name != "Ocellus" {
t.Fatalf("First card (ff3) is not Ocellus but %s", cards[0].Name)
}
if cards[1].Name != "Comet Tail" {
t.Fatalf("Second card (pr54) is not Comet Tail but %s", cards[1].Name)
}
}
func TestLoadCardListErrors(t *testing.T) {
// Clean all loaded sets
mlp.CleanSetCache()
// Fetch a card from an inexistant set
_, err := mlp.LoadCardList([]string{"E"}, true)
if err == nil {
t.Fatalf("LoadCardList succeeded fetching a card with an invalid ID")
} else if err != mlp.ErrInvalidCardID {
t.Fatalf("[E] Expected ErrInvalidCardID but got: %s", err.Error())
}
// Fetch a card from an inexistant set
_, err = mlp.LoadCardList([]string{"oof3"}, true)
if err == nil {
t.Fatalf("LoadCardList succeeded fetching a card from a non-existant set")
}
// Fetch an non-existant card in a real set
_, err = mlp.LoadCardList([]string{"ff3000"}, true)
if err == nil {
t.Fatalf("LoadCardList succeeded fetching a non-existant card from a real set")
} else if err != mlp.ErrCardNotFound {
t.Fatalf("[ff3000] Expected ErrCardNotFound but got: %s", err.Error())
}
// Try fetching from non-loaded sets
_, err = mlp.LoadCardList([]string{"pr10"}, false)
if err == nil {
t.Fatalf("LoadCardList succeeded fetching from a set that wasn't loaded")
} else if err != mlp.ErrSetNotLoaded {
t.Fatalf("Expected ErrSetNotLoaded but got: %s", err.Error())
}
}

View File

@ -57,10 +57,10 @@ const (
type I8PPool map[I8PType][]Card
// MakeI8PCube takes an organized set of cards and sorts them into a draftable I8PCube
func MakeI8PCube(cards I8PPool, schema I8PSchema) *I8PCube {
func MakeI8PCube(cards I8PPool, schema I8PSchema, problemPackSize int) *I8PCube {
return &I8PCube{
Main: makeMainSet(cards, schema),
Problems: makeProblemSet(cards),
Problems: makeProblemSet(cards, problemPackSize),
}
}
@ -102,7 +102,7 @@ func makeMainSet(cards I8PPool, schema I8PSchema) (set *I8PSet) {
return
}
func makeProblemSet(cards I8PPool) (set *I8PSet) {
func makeProblemSet(cards I8PPool, problemPackSize int) (set *I8PSet) {
set = &I8PSet{
Cards: I8PPool{
I8PTypeProblem: cards[I8PTypeProblem],
@ -111,7 +111,7 @@ func makeProblemSet(cards I8PPool) (set *I8PSet) {
set.Schema = draft.PackSchema{
Slots: []draft.PackSlot{
{
Amount: 12,
Amount: problemPackSize,
Provider: set.ProviderByType(I8PTypeProblem),
},
},
@ -126,7 +126,7 @@ func (s *I8PSet) ProviderByType(typ I8PType) draft.CardProvider {
if len(s.Cards[typ]) < n {
n = len(s.Cards[typ])
}
out, s.Cards[typ] = toDraft(s.Cards[typ][:n]), s.Cards[typ][n:]
out, s.Cards[typ] = ToDraft(s.Cards[typ][:n]), s.Cards[typ][n:]
return
}
}

View File

@ -21,7 +21,7 @@ func TestDraftI8PCube(t *testing.T) {
mlp.I8PTypeEntry: mockCards("e1", "e2", "e3", "e4", "e5"),
mlp.I8PTypeProblem: mockCards("P1", "P2"),
}
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema())
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema(), 12)
pack1 := draft.MakePack(cube.Main)
pack2 := draft.MakePack(cube.Main)
@ -57,7 +57,7 @@ func TestDryCube(t *testing.T) {
mlp.I8PTypeMulti: mockCards("m1", "m4"),
mlp.I8PTypeEntry: mockCards("e1", "e4"),
}
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema())
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema(), 12)
pack := draft.MakePack(cube.Main)
if len(pack) != 11 {
t.Fatalf("Expected 11 cards in pack but got %d", len(pack))
@ -93,7 +93,7 @@ func TestPodI8PCube(t *testing.T) {
"Q9", "Q10", "Q11", "Q12",
),
}
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema())
cube := mlp.MakeI8PCube(pool, mlp.DefaultI8PSchema(), 12)
remainingStart := i8pCountRemaining(cube)

View File

@ -30,6 +30,7 @@ const (
SetDefendersOfEquestria SetID = "DE"
SetSeaquestriaBeyond SetID = "SB"
SetFriendsForever SetID = "FF"
SetPromo SetID = "Promo"
)
// BlockID denotes a certain block
@ -41,3 +42,19 @@ const (
BlockOdyssey BlockID = "EO" // Odyssey block - EO/HM/MT
BlockDefenders BlockID = "DE" // Defenders block - DE/SB/FF
)
var allSets = []SetID{
SetPremiere,
SetCanterlotNights,
SetRockNRave,
SetCelestialSolstice,
SetCrystalGames,
SetAbsoluteDiscord,
SetEquestrialOdysseys,
SetHighMagic,
SetMarksInTime,
SetDefendersOfEquestria,
SetSeaquestriaBeyond,
SetFriendsForever,
SetPromo,
}

5
mlp/promo.go Normal file
View File

@ -0,0 +1,5 @@
package mlp
var promoIDs = []string{
"prpf1", "prpf2", "prpf3", "prpf4", "prpf6", "cnpf3", "cnpf5", "cnpf7", "cnpf9", "cnpf11", "cnpf13", "cgpf4", "cgpf6", "cgpf8", "cgpf11", "cgpf12", "cgpf14", "gf1", "gf4", "gf5", "gf7", "gf9",
}

View File

@ -19,32 +19,6 @@ type Set struct {
CardData map[string]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"`
}
// ToDraftCard converts cards to draft.Card
func (c Card) ToDraftCard() draft.Card {
return draft.Card{
ID: c.ID,
}
}
// jsonSet is the set as serialized in the JSON files
type jsonSet struct {
ID SetID
@ -63,9 +37,6 @@ func (j *jsonSet) toSet() (s Set) {
return
}
// PowerRequirement denotes one or more power requirements, colored or not
type PowerRequirement map[string]int
var loadedSets = make(map[SetID]*Set)
// Errors
@ -118,6 +89,12 @@ func LoadSetBytes(id SetID, setdata []byte) (*Set, error) {
return &set, err
}
// HTTPClient is the HTTP client used in LoadSetHTTP
var HTTPClient = http.DefaultClient
// HTTPSource is the base URL for fetching sets remotely via HTTP
var HTTPSource = "https://mcg.zyg.ovh/setdata/"
// LoadSetHTTP loads a set using MCG's remote server
func LoadSetHTTP(id SetID) (*Set, error) {
// Check if set is already loaded
@ -128,7 +105,7 @@ 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")
resp, err := HTTPClient.Get(HTTPSource + setid + ".json")
if err != nil || resp.StatusCode != 200 {
if err == nil {
err = fmt.Errorf("server returned non-200 response code (%d)", resp.StatusCode)
@ -143,3 +120,14 @@ func LoadSetHTTP(id SetID) (*Set, error) {
return LoadSetBytes(id, data)
}
// LoadAllSets just loads all sets from the web
func LoadAllSets() error {
for _, set := range allSets {
_, err := LoadSetHTTP(set)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,25 +1,29 @@
package mlp_test
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"git.fromouter.space/mcg/draft"
"git.fromouter.space/mcg/draft/mlp"
"git.fromouter.space/mcg/draft/mlp/testdata"
)
// TestSet retrieves a set online and generates a couple packs with it
// This test *requires* an internet connection!
func TestSet(t *testing.T) {
// Clean all loaded sets
mlp.CleanSetCache()
deSet, err := mlp.LoadSetHTTP(mlp.SetDefendersOfEquestria)
prSet, err := mlp.LoadSetHTTP(mlp.SetPremiere)
if err != nil {
t.Fatalf("Could not fetch set data: %s", err.Error())
}
pack1 := draft.MakePack(deSet)
pack2 := draft.MakePack(deSet)
pack1 := draft.MakePack(prSet)
pack2 := draft.MakePack(prSet)
// Make sure both packs have the exact number of cards
if len(pack1) != 12 {
@ -34,7 +38,6 @@ func TestSet(t *testing.T) {
}
// TestWrongSet tries to fetch a set that doesn't exist
// This test *requires* an internet connection!
func TestWrongSet(t *testing.T) {
_, err := mlp.LoadSetHTTP("nopenope")
if err == nil {
@ -43,13 +46,12 @@ func TestWrongSet(t *testing.T) {
}
// TestAllLoads tests all the LoadSet* functions
// This test *requires* an internet connection!
func TestAllLoads(t *testing.T) {
// Clean all loaded sets
mlp.CleanSetCache()
// Test LoadSetHTTP
_, err := mlp.LoadSetHTTP(mlp.SetAbsoluteDiscord)
_, err := mlp.LoadSetHTTP(mlp.SetFriendsForever)
if err != nil {
t.Fatalf("[LoadSetHTTP] Could not fetch set data from the internet: %s", err.Error())
}
@ -73,6 +75,12 @@ func TestAllLoads(t *testing.T) {
if err != nil {
t.Fatalf("[LoadSetMemory] Could not load set: %s", err.Error())
}
// Load all remaining sets
err = mlp.LoadAllSets()
if err != nil {
t.Fatalf("[LoadAllSets] Could not load remaining sets: %s", err.Error())
}
}
// TestNotLoadedErr tests that LoadSetMemory fails if set is not cached
@ -157,4 +165,29 @@ func TestMalformedJSONLoad(t *testing.T) {
if err == nil {
t.Fatalf("LoadSetData with invalid data succeeded but shouldn't have")
}
_, err = mlp.LoadSetHTTP(mlp.SetID("broken"))
if err == nil {
t.Fatalf("LoadSetHTTP with invalid JSON succeded but shouldn't have")
}
}
func TestMain(m *testing.M) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/nopenope.json":
// 404
http.Error(res, "Not found", http.StatusNotFound)
case "/broken.json":
// Broken response
fmt.Fprintf(res, "{{{{")
case "/pr.json":
fmt.Fprintf(res, testdata.SetPremiere)
default:
fmt.Fprintf(res, testdata.SetFriendForever)
}
}))
mlp.HTTPSource = testServer.URL + "/"
defer testServer.Close()
os.Exit(m.Run())
}

212
mlp/testdata/sets.go vendored Normal file
View File

@ -0,0 +1,212 @@
package testdata
// SetFriendForever is an excerpt from Friend Forever's JSON set data
const SetFriendForever = `{
"Name": "Friends Forever",
"Cards": [
{
"ID": "ff1",
"Name": "Gallus",
"Subname": "Full of Surprises",
"Element": [
"Loyalty"
],
"Keywords": [
"Home Limit 3"
],
"Traits": [
"Griffon"
],
"Power": 1,
"Type": "Mane Character",
"Text": "When you confront this card's Problem, turn this card over.",
"Rarity": "U"
},
{
"ID": "ff3",
"Name": "Ocellus",
"Subname": "Knowledge is Power",
"Element": [
"Generosity"
],
"Keywords": [
"Home Limit 3"
],
"Traits": [
"Changeling"
],
"Power": 1,
"Type": "Mane Character",
"Text": "When you confront this card's Problem, turn this card over.",
"Rarity": "U"
},
{
"ID": "ff4",
"Name": "Chipcutter",
"Subname": "Sculptor",
"Element": [
"Loyalty"
],
"Keywords": [
"Swift"
],
"Traits": [
"Pegasus",
"Foal"
],
"Cost": 3,
"Power": 2,
"Type": "Friend",
"Text": "",
"Rarity": "C"
},
{
"ID": "ff5",
"Name": "Daybreaker",
"Subname": "Blinding Light",
"Element": [
"Loyalty"
],
"Keywords": [
"Competitive 3"
],
"Traits": [
"Alicorn",
"Royalty"
],
"Requirement": {
"Loyalty": 3
},
"Cost": 3,
"Power": 3,
"Type": "Friend",
"Text": "When you win a faceoff involving this card, score a point.",
"Rarity": "SR"
},
{
"ID": "ff131",
"Name": "Silverstream",
"Subname": "Everything's New!",
"Element": [
"Laughter"
],
"Keywords": [
"Home Limit 3"
],
"Traits": [
"Ally",
"Hippogriff"
],
"Power": 1,
"Type": "Mane Character",
"Text": "Immediate: While this card is at a Problem with 2 or more other characters, turn this card over.",
"Rarity": "UR"
}
]
}`
// SetPremiere is an excerpt from Premiere's JSON set data
const SetPremiere = `{
"Name": "Premiere",
"Cards": [
{
"ID": "pr54",
"Name": "Comet Tail",
"Subname": "Hale Bopper",
"Element": [
"Magic"
],
"Keywords": [],
"Traits": [
"Unicorn"
],
"Cost": 1,
"Power": 1,
"Type": "Friend",
"Text": "",
"Rarity": "C"
},
{
"ID": "pr55",
"Name": "Mint Jewelup",
"Subname": "A Cut Above",
"Element": [
"Magic"
],
"Keywords": [],
"Traits": [
"Unicorn"
],
"Requirement": {
"Magic": 2
},
"Cost": 2,
"Power": 1,
"Type": "Friend",
"Text": "Studious.",
"Rarity": "C"
},
{
"ID": "pr56",
"Name": "Gyro",
"Subname": "Poindexter",
"Element": [
"Magic"
],
"Keywords": [],
"Traits": [
"Earth Pony"
],
"Requirement": {
"Magic": 3
},
"Cost": 1,
"Power": 1,
"Type": "Friend",
"Text": "When you play this card, you may search your deck for an Event, reveal it, put it into your hand, and shuffle your deck.",
"Rarity": "R"
},
{
"ID": "pr77",
"Name": "Rarity",
"Subname": "Nest Weaver",
"Element": [
"Generosity"
],
"Keywords": [
"Inspired"
],
"Traits": [
"Unicorn"
],
"Requirement": {
"Generosity": 3
},
"Cost": 3,
"Power": 2,
"Type": "Friend",
"Text": "When you play this card, you may search your discard pile for a card and put it into your hand.",
"Rarity": "U"
},
{
"ID": "pr211",
"Name": "Fluttershy",
"Subname": "Monster Tamer",
"Element": [
"Kindness"
],
"Keywords": [],
"Traits": [
"Pegasus"
],
"Requirement": {
"Kindness": 4
},
"Cost": 4,
"Power": 2,
"Type": "Friend",
"Text": "When you play this card to a Problem, you may banish a Troublemaker there. When this card leaves that Problem, put that banished Troublemaker into play at a Problem and uncover it.",
"Rarity": "UR"
}
]
}`

View File

@ -2,8 +2,8 @@ package mlp
import "git.fromouter.space/mcg/draft"
// Converts multiple mlp.Card to draft.Card
func toDraft(cards []Card) []draft.Card {
// ToDraft 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()

View File

@ -78,3 +78,12 @@ func (p Pack) String() (str string) {
str = strings.TrimSpace(str)
return
}
// IDs unwraps all the IDs from the cards in the pack
func (p Pack) IDs() []string {
out := make([]string, len(p))
for i, card := range p {
out[i] = card.ID
}
return out
}

30
pack_test.go Normal file
View File

@ -0,0 +1,30 @@
package draft_test
import (
"testing"
"git.fromouter.space/mcg/draft"
)
// 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.Fatalf("Expected \"%s\" but got \"%s\"", expected, p)
}
}
// TestPackIDs makes sure packs are unwrapped correctly
func TestPackIDs(t *testing.T) {
p := draft.Pack{{ID: "a"}, {ID: "b"}, {ID: "c"}}
ids := p.IDs()
if len(ids) < 3 {
t.Fatalf("Expected %d items but got %d", len(p), len(ids))
}
if ids[0] != "a" || ids[1] != "b" || ids[2] != "c" {
t.Fatalf("Contents expected to be [%s] but got %s", p, ids)
}
}

View File

@ -104,16 +104,6 @@ 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.Fatalf("Expected \"%s\" but got \"%s\"", expected, p)
}
}
// ExampleGenericSet is an example usage of the Set APIs to make packs
func ExampleGenericSet() {
// Create a set with some items