diff --git a/data/magic.mse-game/packs b/data/magic.mse-game/packs index ef77ae82..26fc5569 100644 --- a/data/magic.mse-game/packs +++ b/data/magic.mse-game/packs @@ -3,7 +3,7 @@ pack type: name: basic land - select: cyclic + select: equal filter: card.rarity == "basic land" and not is_token_card() # can be shifted pack type: name: common @@ -114,23 +114,22 @@ pack type: name: common sometimes shifted or special selectable: false # TODO: Perhaps use some kind of proportional system here as well? - select: cyclic - item: common - item: common - item: shifted common or else common - item: common - item: common - item: shifted common or else common - item: common - item: common - item: shifted common or else common - item: special or else common + select: equal + item: + name: common + weight: 6 + item: + name: shifted common or else common + weight: 3 + item: + name: special or else common + weight: 1 # of the uncommon slots, 1/3 will be shifted, 1/4 of that will be shifted rares instead pack type: name: uncommon sometimes shifted selectable: false - select: cyclic + select: equal item: uncommon item: uncommon item: shifted uncommon or rare or else uncommon diff --git a/src/data/pack.cpp b/src/data/pack.cpp index 027f0158..a69e70b9 100644 --- a/src/data/pack.cpp +++ b/src/data/pack.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #if !USE_NEW_PACK_SYSTEM // =================================================================================================== OLD @@ -167,14 +168,16 @@ DECLARE_TYPEOF_CONST(map); IMPLEMENT_REFLECTION_ENUM(PackSelectType) { - VALUE_N("auto", SELECT_AUTO); - VALUE_N("all", SELECT_ALL); - VALUE_N("replace", SELECT_REPLACE); - VALUE_N("no replace", SELECT_NO_REPLACE); - VALUE_N("cyclic", SELECT_CYCLIC); - VALUE_N("proportional",SELECT_PROPORTIONAL); - VALUE_N("nonempty", SELECT_NONEMPTY); - VALUE_N("first", SELECT_FIRST); + VALUE_N("auto", SELECT_AUTO); + VALUE_N("all", SELECT_ALL); + VALUE_N("no replace", SELECT_NO_REPLACE); + VALUE_N("replace", SELECT_REPLACE); + VALUE_N("proportional", SELECT_PROPORTIONAL); + VALUE_N("nonempty", SELECT_NONEMPTY); + VALUE_N("equal", SELECT_EQUAL); + VALUE_N("equal proportional", SELECT_EQUAL_PROPORTIONAL); + VALUE_N("equal nonempty", SELECT_NONEMPTY); + VALUE_N("first", SELECT_FIRST); } IMPLEMENT_REFLECTION(PackType) { @@ -264,32 +267,24 @@ PackInstance::PackInstance(const PackType& pack_type, PackGenerator& parent) } } } - // Count items - if (pack_type.select == SELECT_FIRST) { - // count = count of first nonempty thing - if (!cards.empty()) { - count = 1; - } else { - FOR_EACH_CONST(item, pack_type.items) { - count += parent.get(item->name).count; - if (count > 0) break; - } - } - } else { - count = cards.size(); - FOR_EACH_CONST(item, pack_type.items) { - count += parent.get(item->name).count; - } - } // Sum of weights - total_weight = cards.size(); + if (pack_type.select == SELECT_FIRST) { + total_weight = cards.empty() ? 0 : 1; + } else { + total_weight = cards.size(); + } FOR_EACH_CONST(item, pack_type.items) { - if (pack_type.select == SELECT_PROPORTIONAL) { - total_weight += item->weight * parent.get(item->name).count; - } else if (pack_type.select == SELECT_NONEMPTY) { - if (parent.get(item->name).count > 0) { + if (pack_type.select == SELECT_PROPORTIONAL || pack_type.select == SELECT_EQUAL_PROPORTIONAL) { + total_weight += item->weight * parent.get(item->name).total_weight; + } else if (pack_type.select == SELECT_NONEMPTY || pack_type.select == SELECT_EQUAL_NONEMPTY) { + if (parent.get(item->name).total_weight > 0) { total_weight += item->weight; } + } else if (pack_type.select == SELECT_FIRST) { + if (total_weight <= 0) { + total_weight = item->weight; + break; + } } else { total_weight += item->weight; } @@ -308,14 +303,14 @@ void PackInstance::expect_copy(double copies) { PackInstance& i = parent.get(item->name); if (pack_type.select == SELECT_ALL) { i.expect_copy(copies * item->amount); - } else if (pack_type.select == SELECT_PROPORTIONAL) { - i.expect_copy(copies * item->amount * item->weight * i.count / total_weight); - } else if (pack_type.select == SELECT_NONEMPTY) { - if (i.count > 0) { + } else if (pack_type.select == SELECT_PROPORTIONAL || pack_type.select == SELECT_EQUAL_PROPORTIONAL) { + i.expect_copy(copies * item->amount * item->weight * i.total_weight / total_weight); + } else if (pack_type.select == SELECT_NONEMPTY || pack_type.select == SELECT_EQUAL_NONEMPTY) { + if (i.total_weight > 0) { i.expect_copy(copies * item->amount * item->weight / total_weight); } } else if (pack_type.select == SELECT_FIRST) { - if (i.count > 0 && cards.empty()) { + if (i.total_weight > 0 && cards.empty()) { i.expect_copy(copies * item->amount); break; } @@ -337,55 +332,57 @@ struct RandomRange { Gen& gen; }; +struct WeightedItem { + double weight; + int count; + int tiebreaker; +}; + +struct CompareWeightedItems{ + inline bool operator () (WeightedItem* a, WeightedItem* b) { + // compare (a->count+1)/a->weight <> (b->count+1)/b->weight + // prefer the one where this is lower, return true if b is prefered + double delta = b->weight * (a->count + 1) - a->weight * (b->count + 1); + if (delta < 0) return false; + if (delta > 0) return true; + return b->tiebreaker < a->tiebreaker; + } +}; + +/// Distribute 'total' among the weighted items, higher weight items get chosen more often +void weighted_equal_divide(vector& items, int total) { + assert(!items.empty()); + if (items.size() == 1) { + items.front().count = total; + } else { + priority_queue,CompareWeightedItems> pq; + for (size_t i = 0 ; i < items.size() ; ++i) { + pq.push(&items[i]); + } + while (total > 0) { + // repeatedly pick the item that minimizes, after incrementing count: + // max_wi wi->count/wi->weight + WeightedItem* wi = pq.top();pq.pop(); + wi->count++; + total--; + pq.push(wi); + } + } +} + void PackInstance::generate(vector* out) { card_copies = 0; if (requested_copies == 0) return; if (pack_type.select == SELECT_ALL) { // add all cards - card_copies += requested_copies * cards.size(); - if (out) { - for (size_t i = 0 ; i < requested_copies ; ++i) { - out->insert(out->end(), cards.begin(), cards.end()); - } - } - // and all items - FOR_EACH_CONST(item, pack_type.items) { - PackInstance& i = parent.get(item->name); - i.request_copy(requested_copies * item->amount); - } + generate_all(out, requested_copies); } else if (pack_type.select == SELECT_REPLACE || pack_type.select == SELECT_PROPORTIONAL || pack_type.select == SELECT_NONEMPTY) { // multiple copies for (size_t i = 0 ; i < requested_copies ; ++i) { - double r = parent.gen() * total_weight / parent.gen.max(); - if (r < cards.size()) { - // pick a card - card_copies++; - if (out) { - int i = (int)r; - out->push_back(cards[i]); - } - } else { - // pick an item - r -= cards.size(); - FOR_EACH_CONST(item, pack_type.items) { - PackInstance& i = parent.get(item->name); - if (pack_type.select == SELECT_REPLACE) { - r -= item->weight; - } else if (pack_type.select == SELECT_PROPORTIONAL) { - r -= item->weight * i.count; - } else { // SELECT_NONEMPTY - if (i.count > 0) r -= item->weight; - } - // have we reached the item we were looking for? - if (r < 0) { - i.request_copy(item->amount); - break; - } - } - } + generate_one_random(out); } } else if (pack_type.select == SELECT_NO_REPLACE) { @@ -406,20 +403,53 @@ void PackInstance::generate(vector* out) { } } - } else if (pack_type.select == SELECT_CYCLIC) { - size_t total = cards.size() + pack_type.items.size(); - if (total == 0) return; // prevent div by 0 - size_t div = requested_copies / total; - size_t rem = requested_copies % total; - for (size_t i = 0 ; i < total ; ++i) { - // how many copies of this card/item do we need? - size_t copies = div + (i < rem ? 1 : 0); - if (i < cards.size()) { - card_copies += copies; - if (out) out->insert(out->end(), copies, cards[i]); - } else { - const PackItemP& item = pack_type.items[i]; - parent.get(item->name).request_copy(copies * item->amount); + } else if (pack_type.select == SELECT_EQUAL + || pack_type.select == SELECT_EQUAL_PROPORTIONAL + || pack_type.select == SELECT_EQUAL_NONEMPTY) { + // equal selection instead of random + if (requested_copies == 1) { + // somewhat of a hack to keep things fair: just pick at random + // otherwise we would end up picking the lowest weight item + generate_one_random(out); + } else { + // 1. the weights of each item, and of the cards + vector weighted_items; + FOR_EACH_CONST(item, pack_type.items) { + WeightedItem wi = {0,0,parent.gen()}; + if (pack_type.select == SELECT_EQUAL_PROPORTIONAL) { + wi.weight = item->weight * parent.get(item->name).total_weight; + } else if (pack_type.select == SELECT_EQUAL_NONEMPTY) { + wi.weight = parent.get(item->name).total_weight > 0 ? item->weight : 0; + } else { + wi.weight = item->weight; + } + weighted_items.push_back(wi); + } + WeightedItem wi = {cards.size(),0,parent.gen()}; + weighted_items.push_back(wi); + // 2. divide the requested_copies among the cards and the items, taking the weights into account + weighted_equal_divide(weighted_items, (int)requested_copies); + // 3a. propagate to items + for (size_t j = 0 ; j < pack_type.items.size() ; ++j) { + const PackItem& item = *pack_type.items[j]; + PackInstance& i = parent.get(item.name); + i.request_copy(item.amount * weighted_items[j].count); + } + // 3b. pick some cards + int new_card_copies = weighted_items.back().count; + card_copies += new_card_copies; + if (out && !cards.empty()) { + int div = new_card_copies / (int)cards.size(); + int rem = new_card_copies % (int)cards.size(); + // some copies of all cards + for (int i = 0 ; i < div ; ++i) { + out->insert(out->end(), cards.begin(), cards.end()); + } + // pick the remainder at random + for (int i = 0 ; i < rem ; ++i) { + int nr = parent.gen() % cards.size(); + out->push_back(cards.at(nr)); + } } } @@ -427,12 +457,12 @@ void PackInstance::generate(vector* out) { if (!cards.empty()) { // there is a card, pick it card_copies += requested_copies; - if (out) out->push_back(cards.front()); + if (out) out->insert(out->end(), requested_copies, cards.front()); } else { // pick first nonempty item FOR_EACH_CONST(item, pack_type.items) { PackInstance& i = parent.get(item->name); - if (i.count > 0) { + if (i.total_weight > 0) { i.request_copy(requested_copies * item->amount); break; } @@ -442,6 +472,49 @@ void PackInstance::generate(vector* out) { requested_copies = 0; } +void PackInstance::generate_all(vector* out, size_t copies) { + card_copies += copies * cards.size(); + if (out) { + for (size_t i = 0 ; i < copies ; ++i) { + out->insert(out->end(), cards.begin(), cards.end()); + } + } + // and all items + FOR_EACH_CONST(item, pack_type.items) { + PackInstance& i = parent.get(item->name); + i.request_copy(copies * item->amount); + } +} + +void PackInstance::generate_one_random(vector* out) { + double r = parent.gen() * total_weight / parent.gen.max(); + if (r < cards.size()) { + // pick a card + card_copies++; + if (out) { + int i = (int)r; + out->push_back(cards[i]); + } + } else { + // pick an item + r -= cards.size(); + FOR_EACH_CONST(item, pack_type.items) { + PackInstance& i = parent.get(item->name); + if (pack_type.select == SELECT_PROPORTIONAL || pack_type.select == SELECT_EQUAL_PROPORTIONAL) { + r -= item->weight * i.total_weight; + } else if (pack_type.select == SELECT_NONEMPTY || pack_type.select == SELECT_EQUAL_NONEMPTY) { + if (i.total_weight > 0) r -= item->weight; + } else { + r -= item->weight; + } + // have we reached the item we were looking for? + if (r < 0) { + i.request_copy(item->amount); + break; + } + } + } +} // ----------------------------------------------------------------------------- : PackGenerator @@ -451,6 +524,9 @@ void PackGenerator::reset(const SetP& set, int seed) { max_depth = 0; instances.clear(); } +void PackGenerator::reset(int seed) { + gen.seed((unsigned)seed); +} PackInstance& PackGenerator::get(const String& name) { PackInstanceP& instance = instances[name]; diff --git a/src/data/pack.hpp b/src/data/pack.hpp index e51d1527..f183675a 100644 --- a/src/data/pack.hpp +++ b/src/data/pack.hpp @@ -121,11 +121,13 @@ class PackGenerator; enum PackSelectType { SELECT_AUTO , SELECT_ALL -, SELECT_REPLACE , SELECT_NO_REPLACE -, SELECT_CYCLIC +, SELECT_REPLACE , SELECT_PROPORTIONAL , SELECT_NONEMPTY +, SELECT_EQUAL +, SELECT_EQUAL_PROPORTIONAL +, SELECT_EQUAL_NONEMPTY , SELECT_FIRST }; @@ -200,17 +202,23 @@ class PackInstance : public IntrusivePtrBase { PackGenerator& parent; int depth; //< 0 = no items, otherwise 1+max depth of items refered to vector cards; //< All cards that pass the filter - size_t count; //< Total number of non-empty cards/items double total_weight; //< Sum of item and card weights size_t requested_copies; //< The requested number of copies of this pack size_t card_copies; //< The number of cards that were chosen to come from this pack double expected_copies; + + /// Generate some copies of all cards and items + void generate_all(vector* out, size_t copies); + /// Generate one card/item chosen at random (using the select type) + void generate_one_random(vector* out); }; class PackGenerator { public: /// Reset the generator, possibly switching the set or reseeding void reset(const SetP& set, int seed); + /// Reset the generator, but not the set + void reset(int seed); /// Find the PackInstance for the PackType with the given name PackInstance& get(const String& name); diff --git a/src/gui/set/random_pack_panel.cpp b/src/gui/set/random_pack_panel.cpp index 83eeb6e9..808bce97 100644 --- a/src/gui/set/random_pack_panel.cpp +++ b/src/gui/set/random_pack_panel.cpp @@ -373,7 +373,7 @@ CustomPackDialog::CustomPackDialog(Window* parent, const SetP& set, const PackTy } void CustomPackDialog::updateTotals() { - generator.gen.seed(0); + generator.reset(0); int total_packs = 0; FOR_EACH(pick,pickers) { int copies = pick.value->GetValue(); @@ -636,7 +636,7 @@ void RandomPackPanel::onPackTypeClick(wxCommandEvent& ev) { void RandomPackPanel::updateTotals() { #if USE_NEW_PACK_SYSTEM - generator.gen.seed((unsigned)last_seed); + generator.reset(last_seed); #else totals->clear(); #endif