it works!
This commit is contained in:
commit
6568ce1b69
7 changed files with 348 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.exe
|
||||||
|
*.json
|
46
cards.go
Normal file
46
cards.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
44
files.go
Normal file
44
files.go
Normal file
|
@ -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
|
||||||
|
}
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
12
go.sum
Normal file
12
go.sum
Normal file
|
@ -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=
|
138
main.go
Normal file
138
main.go
Normal file
|
@ -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
|
||||||
|
}
|
97
price.go
Normal file
97
price.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue