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