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.txt", 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 }