commit 6568ce1b690ed07ace82966ba681215b58f1d454 Author: Hamcha Date: Thu Oct 12 15:54:19 2023 +0200 it works! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e362af --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe +*.json \ No newline at end of file diff --git a/cards.go b/cards.go new file mode 100644 index 0000000..9fb7596 --- /dev/null +++ b/cards.go @@ -0,0 +1,46 @@ +package main + +import "os" + +type Card struct { + AsciiName string `json:"asciiName"` + Name string `json:"name"` + Identifiers struct { + CardKingdomId string `json:"cardKingdomId"` + CardsphereId string `json:"cardsphereId"` + MCMId string `json:"mcmId"` + MCMMetaId string `json:"mcmMetaId"` + MTGOId string `json:"mtgoId"` + MultiverseId string `json:"multiverseId"` + ScryfallId string `json:"scryfallId"` + TCGPlayerProductId string `json:"tcgplayerProductId"` + } `json:"identifiers"` + Rarity string `json:"rarity"` + SetCode string `json:"setCode"` + CardType string `json:"type"` + Types []string `json:"types"` + UUID string `json:"uuid"` +} + +type CardDB struct { + Data map[string]Card `json:"data"` +} + +func loadCardInfos(identifierFile string) func() (map[string]Card, error) { + return func() (map[string]Card, error) { + // Check if identifiers file exists + _, err := os.Stat(identifierFile) + if os.IsNotExist(err) { + // Download identifiers file + err = downloadFile(identifierURL, identifierFile) + } + if err != nil { + return nil, err + } + + // Read identifiers file + var cardDB CardDB + err = readJSONFile(identifierFile, &cardDB) + return cardDB.Data, err + } +} diff --git a/files.go b/files.go new file mode 100644 index 0000000..0778e93 --- /dev/null +++ b/files.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + + jsoniter "github.com/json-iterator/go" +) + +func readJSONFile(filename string, dst any) error { + log.Printf("Reading %s\n", filename) + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + return jsoniter.ConfigFastest.NewDecoder(file).Decode(dst) +} + +func downloadFile(url, filename string) error { + log.Printf("%s not found, downloading from %s \n", filename, url) + response, err := http.Get(url) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return fmt.Errorf("request failed with status code: %d", response.StatusCode) + } + + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, response.Body) + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3bcaa8a --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.fromouter.space/Hamcha/blah + +go 1.20 + +require ( + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ea794b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..af6a4e7 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "flag" + "fmt" + "log" + "math/rand" + "os" + "strings" +) + +const ( + identifierURL = "https://mtgjson.com/api/v5/AllIdentifiers.json" + pricesURL = "https://mtgjson.com/api/v5/AllPricesToday.json" +) + +type CardRecord struct { + card Card `json:"card"` + price CardPriceEntry `json:"price"` +} + +func main() { + identifierFile := flag.String("i", "AllIdentifiers.json", "identifier file (AllIdentifiers.json), if not found it will be downloaded") + pricesFile := flag.String("p", "AllPricesToday.json", "prices file (AllPricesToday.json), if not found it will be downloaded") + priceFilter := flag.Float64("price", 0.04, "Maximum card price allowed (in whole units of any currency)") + typeFilter := flag.String("remove-types", "token,emblem,sticker,card,hero,plane,scheme", "comma-separated list of card types to remove") + cardsPerDraft := flag.Int("packSize", 100, "How many cards to put in each \"dollar store\" pack") + packCount := flag.Int("packCount", 2, "How many \"dollar store\" pack to generate") + landLimit := flag.Int("landLimit", 5, "Limit how many lands to put in each pack (0 to disable)") + flag.Parse() + + priceChan := make(chan []CardPriceEntry) + cardChan := make(chan map[string]Card) + + log.Println(" == Loading data... ==") + go loadAsync(loadCardInfos(*identifierFile), cardChan) + go loadAsync(loadPrices(*pricesFile, *priceFilter), priceChan) + + cards := <-cardChan + prices := <-priceChan + + // Filter cards + forbiddenTypes := make(map[string]struct{}) + for _, t := range strings.Split(*typeFilter, ",") { + forbiddenTypes[t] = struct{}{} + } + + var filtered []CardRecord + for _, price := range prices { + entry := cards[price.UUID] + + // Filter card types we don't want + typeOk := true + for _, t := range entry.Types { + if _, ok := forbiddenTypes[strings.ToLower(t)]; ok { + typeOk = false + break + } + } + if !typeOk { + continue + } + + filtered = append(filtered, CardRecord{ + card: entry, + price: price, + }) + } + + types := make(map[string]int) + for _, record := range filtered { + log.Printf("Price for %s: %.2f %s at %s\n", record.card.Name, record.price.Price, record.price.Currency, record.price.Seller) + for _, t := range record.card.Types { + types[strings.ToUpper(t)[0:1]+t[1:]]++ + } + } + + log.Println(" == Stats ==") + log.Printf("Found %d cards\n", len(filtered)) + for t, count := range types { + log.Printf(" - %s: %d\n", t, count) + } + + log.Println(" == Generating packs... ==") + for i := 0; i < *packCount; i++ { + pack := generatePack(filtered, *cardsPerDraft, *landLimit) + checkErr(os.WriteFile(fmt.Sprintf("pack-%d.json", i), []byte(serializePack(pack)), 0644)) + } + + log.Println(" == Done ==") +} + +func isType(card Card, askingType string) bool { + for _, t := range card.Types { + if strings.ToLower(t) == strings.ToLower(askingType) { + return true + } + } + return false +} + +func serializePack(pack []CardRecord) (out string) { + for _, card := range pack { + out += "1 " + card.card.Name + "\n" + } + return +} + +func generatePack(filtered []CardRecord, cardsPerDraft, landLimit int) []CardRecord { + // Pick random cards + randCards := make([]CardRecord, 0) + landCount := 0 + for len(randCards) < cardsPerDraft { + pick := filtered[rand.Intn(len(filtered))] + if isType(pick.card, "land") { + if landLimit > 0 && landCount >= landLimit { + continue + } else { + landCount++ + } + } + randCards = append(randCards, pick) + } + return randCards +} + +func checkErr(err error) { + if err != nil { + log.Printf("Fatal error: %s\n", err.Error()) + os.Exit(1) + } +} + +func loadAsync[T any](fn func() (T, error), chn chan T) { + out, err := fn() + checkErr(err) + chn <- out +} diff --git a/price.go b/price.go new file mode 100644 index 0000000..3f28c22 --- /dev/null +++ b/price.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" +) + +type PriceRecord struct { + MTGO map[string]PriceList `json:"mtgo,omitempty"` + Paper map[string]PriceList `json:"paper,omitempty"` +} + +type PricePoints struct { + Normal map[string]float64 `json:"normal,omitempty"` + Foil map[string]float64 `json:"foil,omitempty"` +} + +type PriceList struct { + Retail *PricePoints `json:"retail,omitempty"` + Currency string `json:"currency"` + Buylist *PricePoints `json:"buylist,omitempty"` +} + +type PriceDB struct { + Data map[string]PriceRecord `json:"data"` +} + +type CardPriceEntry struct { + UUID string `json:"uuid"` + Price float64 `json:"price"` + Currency string `json:"currency"` + Seller string `json:"seller"` +} + +func loadPrices(pricesFile string, priceFilter float64) func() ([]CardPriceEntry, error) { + return func() ([]CardPriceEntry, error) { + // Check if prices file exists + _, err := os.Stat(pricesFile) + if os.IsNotExist(err) { + // Download prices file + err = downloadFile(pricesURL, pricesFile) + } + if err != nil { + return nil, err + } + + // Read prices file + var priceDB PriceDB + err = readJSONFile(pricesFile, &priceDB) + if err != nil { + return nil, err + } + + // Filter cards we care about + entries := make([]CardPriceEntry, 0) + for uuid, priceCatalog := range priceDB.Data { + // Get all paper retail prices + lowestPrice, currency, seller := getLowestPrice(priceCatalog.Paper) + + if priceFilter < lowestPrice { + continue + } + + entries = append(entries, CardPriceEntry{ + UUID: uuid, + Price: lowestPrice, + Currency: currency, + Seller: seller, + }) + } + + return entries, nil + } +} + +func getLowestPrice(sellers map[string]PriceList) (lowestPrice float64, currency string, seller string) { + lowestPrice = 9999 + for cardSeller, priceList := range sellers { + if priceList.Retail == nil { + continue + } + for _, price := range priceList.Retail.Normal { + if price < lowestPrice { + lowestPrice = price + currency = priceList.Currency + seller = cardSeller + } + } + for _, price := range priceList.Retail.Foil { + if price < lowestPrice { + lowestPrice = price + currency = priceList.Currency + seller = cardSeller + } + } + } + return +}