diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..01604330 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +obj/** +tu_optimize diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a80241ea --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +MAIN := tuo.exe +SRCS := $(wildcard *.cpp) +OBJS := $(patsubst %.cpp,obj/%.o,$(SRCS)) +INCS := $(wildcard *.h) + +CPPFLAGS := -Wall -Werror -std=gnu++11 -O3 -DNDEBUG +LDFLAGS := -lboost_system -lboost_thread -lboost_filesystem -lboost_regex + +all: $(MAIN) + +obj/%.o: %.cpp $(INCS) + $(CXX) $(CPPFLAGS) -o $@ -c $< + +$(MAIN): $(OBJS) + $(CXX) -o $@ $(OBJS) $(LDFLAGS) + +clean: + del /q $(MAIN).exe obj\*.o diff --git a/Makefile-debug b/Makefile-debug new file mode 100644 index 00000000..fda029e4 --- /dev/null +++ b/Makefile-debug @@ -0,0 +1,18 @@ +MAIN := tuodebug.exe +SRCS := $(wildcard *.cpp) +OBJS := $(patsubst %.cpp,obj-debug/%.o,$(SRCS)) +INCS := $(wildcard *.h) + +CPPFLAGS := -Wall -Werror -std=gnu++11 -O3 +LDFLAGS := -lboost_system -lboost_thread -lboost_filesystem -lboost_regex + +all: $(MAIN) + +obj-debug/%.o: %.cpp $(INCS) + $(CXX) $(CPPFLAGS) -o $@ -c $< + +$(MAIN): $(OBJS) + $(CXX) -o $@ $(OBJS) $(LDFLAGS) + +clean: + del /q $(MAIN).exe obj-debug\*.o diff --git a/Makefile.osx b/Makefile.osx new file mode 100644 index 00000000..7534af94 --- /dev/null +++ b/Makefile.osx @@ -0,0 +1,18 @@ +MAIN := tuo +SRCS := $(wildcard *.cpp) +OBJS := $(patsubst %.cpp,obj/%.o,$(SRCS)) +INCS := $(wildcard *.h) + +CPPFLAGS := -Wall -Werror -std=c++11 -stdlib=libc++ -O3 -I/usr/local/include -Wno-deprecated-register -DNDEBUG +LDFLAGS := lib/libboost_system-mt.a lib/libboost_thread-mt.a lib/libboost_filesystem-mt.a lib/libboost_regex-mt.a -L/usr/local/lib -Bstatic + +all: $(MAIN) + +obj/%.o: %.cpp ${INCS} + $(CXX) $(CPPFLAGS) -o $@ -c $< + +$(MAIN): $(OBJS) + $(CXX) -o $@ $(OBJS) $(LDFLAGS) + +clean: + rm -f $(MAIN) obj/*.o diff --git a/card.h b/card.h new file mode 100644 index 00000000..919b3c20 --- /dev/null +++ b/card.h @@ -0,0 +1,58 @@ +#ifndef CARD_H_INCLUDED +#define CARD_H_INCLUDED + +#include +#include +#include +#include "tyrant.h" + +class Card +{ +public: + unsigned m_attack; + unsigned m_base_id; // The id of the original card if a card is unique and alt/upgraded. The own id of the card otherwise. + unsigned m_delay; + Faction m_faction; + unsigned m_health; + unsigned m_id; + unsigned m_level; + unsigned m_fusion_level; + std::string m_name; + unsigned m_rarity; + unsigned m_set; + std::vector m_skills; + unsigned m_skill_value[num_skills]; + CardType::CardType m_type; + const Card* m_top_level_card; // [TU] corresponding full-level card + unsigned m_recipe_cost; + std::map m_recipe_cards; + std::map m_used_for_cards; + +public: + Card() : + m_attack(0), + m_base_id(0), + m_delay(0), + m_faction(imperial), + m_health(0), + m_id(0), + m_level(1), + m_fusion_level(0), + m_name(""), + m_rarity(1), + m_set(0), + m_skills(), + m_type(CardType::assault), + m_top_level_card(this), + m_recipe_cost(0), + m_recipe_cards(), + m_used_for_cards() + { + std::memset(m_skill_value, 0, sizeof m_skill_value); + } + + void add_skill(Skill id, unsigned x, Faction y, unsigned n, unsigned c, Skill s, Skill s2, bool all); + const Card* upgraded() const { return this == m_top_level_card ? this : m_used_for_cards.begin()->first; } +}; + +#endif diff --git a/cards.cpp b/cards.cpp new file mode 100644 index 00000000..c857d83d --- /dev/null +++ b/cards.cpp @@ -0,0 +1,173 @@ +#include "cards.h" + +#include +#include +#include +#include +#include +#include + +#include "tyrant.h" +#include "card.h" + +std::string simplify_name(const std::string& card_name) +{ + std::string simple_name; + for(auto c : card_name) + { + if(!strchr(";:,\"'! ", c)) + { + simple_name += ::tolower(c); + } + } + return(simple_name); +} + +std::list get_abbreviations(const std::string& name) +{ + std::list abbr_list; + boost::tokenizer> word_token{name, boost::char_delimiters_separator{false, " -", ""}}; + std::string initial; + auto token_iter = word_token.begin(); + for(; token_iter != word_token.end(); ++token_iter) + { + abbr_list.push_back(simplify_name(std::string{token_iter->begin(), token_iter->end()})); + initial += *token_iter->begin(); + } + abbr_list.push_back(simplify_name(initial)); + return(abbr_list); +} + +//------------------------------------------------------------------------------ +Cards::~Cards() +{ + for (Card* c: all_cards) { delete(c); } +} + +const Card* Cards::by_id(unsigned id) const +{ + const auto cardIter = cards_by_id.find(id); + if(cardIter == cards_by_id.end()) + { + throw std::runtime_error("No card with id " + to_string(id)); + } + else + { + return(cardIter->second); + } +} +//------------------------------------------------------------------------------ +void Cards::organize() +{ + cards_by_id.clear(); + player_cards.clear(); + cards_by_name.clear(); + player_commanders.clear(); + player_assaults.clear(); + player_structures.clear(); + // Round 1: set cards_by_id + for(Card* card: all_cards) + { + cards_by_id[card->m_id] = card; + } + // Round 2: depend on cards_by_id / by_id(); update m_name, [TU] m_top_level_card etc.; set cards_by_name; + for(Card* card: all_cards) + { + // Remove delimiters from card names + size_t pos; + while((pos = card->m_name.find_first_of(";:,")) != std::string::npos) + { + card->m_name.erase(pos, 1); + } + // set m_top_level_card for non base cards + card->m_top_level_card = by_id(card->m_base_id)->m_top_level_card; + // Cards available ("visible") to players have priority + std::string base_name = card->m_name; + if (card == card->m_top_level_card) + { + add_card(card, card->m_name + "-" + to_string(card->m_level)); + } + else + { + card->m_name += "-" + to_string(card->m_level); + } + add_card(card, card->m_name); + } +#if 0 // TODO refactor precedence + // Round 3: depend on cards_by_name; set abbreviations + for(Card* card: cards) + { + // generate abbreviations + if(card->m_set > 0) + { + for(auto&& abbr_name : get_abbreviations(card->m_name)) + { + if(abbr_name.length() > 1 && cards_by_name.find(abbr_name) == cards_by_name.end()) + { + player_cards_abbr[abbr_name] = card->m_name; + } + } + } + } +#endif +} + +void Cards::add_card(Card * card, const std::string & name) +{ + std::string simple_name{simplify_name(name)}; + auto card_itr = cards_by_name.find(simple_name); + signed old_visible = card_itr == cards_by_name.end() ? -1 : visible_cardset.count(card_itr->second->m_set); + signed new_visible = visible_cardset.count(card->m_set); + if (card_itr != cards_by_name.end()) + { + if (old_visible == new_visible) + { + ambiguous_names.insert(simple_name); + } + _DEBUG_MSG(2, "Duplicated card name \"%s\" [%u] set=%u (visible=%u) : [%u] set=%u (visible=%u)\n", name.c_str(), card_itr->second->m_id, card_itr->second->m_set, old_visible, card->m_id, card->m_set, new_visible); + } + else if (old_visible < new_visible) + { + ambiguous_names.erase(simple_name); + cards_by_name[simple_name] = card; + if (new_visible) + { + player_cards.push_back(card); + switch(card->m_type) + { + case CardType::commander: { + player_commanders.push_back(card); + break; + } + case CardType::assault: { + player_assaults.push_back(card); + break; + } + case CardType::structure: { + player_structures.push_back(card); + break; + } + case CardType::num_cardtypes: { + throw card->m_type; + break; + } + } + } + } +} + +// class Card +void Card::add_skill(Skill id, unsigned x, Faction y, unsigned n, unsigned c, Skill s, Skill s2, bool all) +{ + for(auto it = m_skills.begin(); it != m_skills.end(); ++ it) + { + if(it->id == id) + { + m_skills.erase(it); + break; + } + } + m_skills.push_back({id, x, y, n, c, s, s2, all}); + m_skill_value[id] = x ? x : n ? n : 1; +} + diff --git a/cards.h b/cards.h new file mode 100644 index 00000000..c2218254 --- /dev/null +++ b/cards.h @@ -0,0 +1,33 @@ +#ifndef CARDS_H_INCLUDED +#define CARDS_H_INCLUDED + +#include +#include +#include +#include + +class Card; + +class Cards +{ +public: + ~Cards(); + + std::vector all_cards; + std::map cards_by_id; + std::vector player_cards; + std::map cards_by_name; + std::vector player_commanders; + std::vector player_assaults; + std::vector player_structures; + std::map player_cards_abbr; + std::unordered_set visible_cardset; + std::unordered_set ambiguous_names; + const Card * by_id(unsigned id) const; + void organize(); + void add_card(Card * card, const std::string & name); +}; + +std::string simplify_name(const std::string& card_name); + +#endif diff --git a/data/raids.xml b/data/raids.xml new file mode 100644 index 00000000..3654cbdd --- /dev/null +++ b/data/raids.xml @@ -0,0 +1,4857 @@ + + + + + + + + 1 + Apocalypse + 1373 + 26 + + + 7695 + 7705 + 7715 + 7725 + 7735 + 7745 + + + 7695 + 7705 + 7715 + 7725 + 7735 + 7745 + + + + + + 2 + Jotun Max + 1413 + 26 + + + 10551 + 10551 + 10521 + 10521 + 10541 + 10541 + 10531 + 10531 + 10511 + 10511 + + + + + + 3 + Forsaken Horde + 1523 + 26 + + + 12051 + 12051 + 12061 + 12061 + 12071 + 12071 + 12081 + 12081 + 12091 + 12091 + + + + + + 4 + Oluth + 1545 + 26 + + + 12294 + 12294 + 12304 + 12304 + 12314 + 12314 + 12324 + 12324 + 12334 + 12334 + + + + + + 5 + Carnifex + 1571 + 26 + + + 13102 + 13112 + 13122 + 13132 + 13142 + 8247 + + + 13102 + 13112 + 13122 + 13132 + 13142 + 8247 + + + + + + 6 + Karkinos + 1620 + 26 + + + 8353 + 8363 + 14167 + 14167 + 14177 + 14177 + 14187 + 14187 + 14197 + 14197 + 14207 + 14207 + + + + + + 7 + Brood Mother + 1652 + 26 + + + 14810 + 14820 + 14830 + 14840 + 14850 + 14860 + + + 14810 + 14820 + 14830 + 14840 + 14850 + 14860 + + + + + + 8 + Lernaean Hydra + 1698 + 26 + + + 8548 + 15480 + 15480 + 15490 + 15500 + 15500 + 15510 + 15510 + 15520 + 15520 + + + + + + 9 + Talos + 1732 + 26 + + + 16136 + 16146 + 16156 + 16166 + 16176 + 8630 + + + 16136 + 16146 + 16156 + 16166 + 16176 + + + + + + 10 + Gore Typhon Raid + 1760 + 26 + + + 30236 + 30246 + 30246 + 30256 + 30256 + 30266 + 30266 + 30276 + 8658 + 8686 + + + + + + 11 + Supremacy Raid + 1815 + 26 + + + 31116 + 31116 + 31126 + 31126 + 31136 + 31136 + 31146 + 31146 + 31156 + 31156 + + + + + + 12 + Intrepid Raid + 1855 + 26 + + + 8774 + 8784 + 31827 + 31827 + 31837 + 31837 + 31847 + 31847 + 31857 + 31857 + + + + + + 13 + Pantheon Raid + 1883 + 26 + + + 32947 + 32947 + 32957 + 32957 + 32967 + 32967 + 32977 + 32977 + 32987 + 32987 + + + + + + 14 + Prometheus Raid + 1912 + 26 + + + 33813 + 33813 + 33823 + 33823 + 33833 + 33833 + 33843 + 33843 + 33853 + 33853 + + + + + + 15 + Lithid Raid + 1952 + 26 + + + 35130 + 35140 + 35150 + 35160 + + + 35150 + 35160 + + + 35130 + 35130 + 35140 + 35140 + 35160 + 35170 + 35170 + + + + + + 16 + Calamity Raid + 1986 + 26 + + + 36344 + 36344 + 36344 + 36354 + 36354 + 36364 + 36364 + 36374 + 36374 + 36384 + + + + + + 17 + Warden Raid + 25026 + 26 + + + 17192 + 37474 + 37474 + 37484 + 37484 + 37494 + 37494 + 37504 + 37504 + 37504 + + + + + + + + + + + 1 + Tartarus Vanguard 1 + 1187 + 3 + + + 4358 + 4616 + 5026 + 5420 + 6869 + 7143 + 8287 + 11029 + 14342 + 14894 + + + + + + 2 + Tartarus Vanguard 2 + 1187 + 3 + + + 4153 + 4232 + 4495 + 5319 + 5641 + 6081 + 6769 + 11613 + 14083 + 14894 + + + + + + 3 + Tartarus Vanguard 3 + 1662 + 3 + + + 2227 + 5896 + 11613 + 11613 + 11859 + 12470 + 12994 + 13398 + 13920 + 14894 + + + + + + 4 + Tartarus Vanguard 4 + 1193 + 3 + + + 4555 + 4640 + 6004 + 6004 + 8329 + 11511 + 13012 + 14396 + 14702 + 14888 + + + + + + 5 + Tartarus Vanguard 5 + 1193 + 3 + + + 4370 + 5044 + 5784 + 5992 + 6615 + 7509 + 7971 + 12778 + 13782 + 14888 + + + + + + 6 + Tartarus Vanguard 6 + 1668 + 3 + + + 4555 + 7047 + 8305 + 10884 + 11102 + 12778 + 13236 + 13638 + 14053 + 14888 + + + + + + 7 + Tartarus Vanguard 7 + 1674 + 3 + + + 7065 + 7293 + 8241 + 11511 + 12204 + 12482 + 13764 + 13999 + 14888 + 14894 + + + + + + 211 + Brim101 + Brimstone Normal 1 + 1358 + 1 + + + 4060 + 5737 + 8402 + 12704 + 12879 + 15065 + 15065 + 15074 + 15134 + 15151 + + + + + + 212 + Brim102 + Brimstone Normal 2 + 1358 + 1 + + + 4060 + 8402 + 12704 + 12879 + 14082 + 15065 + 15065 + 15071 + 15134 + 15151 + + + + + + 213 + Brim103 + Brimstone Normal 3 + 1681 + 1 + + + 4060 + 8402 + 12704 + 12879 + 14082 + 15065 + 15065 + 15071 + 15134 + 15151 + + + + + + 214 + Brim104 + Brimstone Normal 4 + 1032 + 1 + + + 4060 + 5737 + 8402 + 12704 + 12879 + 15065 + 15065 + 15074 + 15134 + 15151 + + + + + + 215 + Brim105 + Brimstone Normal 5 + 1032 + 1 + + + 4811 + 8065 + 11004 + 12601 + 13325 + 15037 + 15065 + 15071 + 15071 + 15151 + + + + + + 216 + Brim106 + Brimstone Normal 6 + 1687 + 1 + + + 4811 + 8286 + 11312 + 11642 + 11948 + 12607 + 15065 + 15071 + 15071 + 15151 + + + + + + 217 + Brim107 + Brimstone Normal 7 + 1692 + 1 + + + 7688 + 8286 + 10174 + 12391 + 14299 + 14959 + 15065 + 15071 + 15151 + 15151 + + + + + + 221 + Brim201 + Brimstone Heroic 1 + 1360 + 1 + + + 5743 + 8408 + 10180 + 10860 + 12710 + 15067 + 15067 + 15073 + 15153 + 15197 + + + + + + 222 + Brim202 + Brimstone Heroic 2 + 1360 + 1 + + + 7688 + 8408 + 10180 + 12710 + 14088 + 15067 + 15067 + 15073 + 15153 + 15197 + + + + + + 223 + Brim203 + Brimstone Heroic 3 + 1683 + 1 + + + 4158 + 7688 + 8408 + 12710 + 12885 + 14088 + 15067 + 15067 + 15073 + 15153 + + + + + + 224 + Brim204 + Brimstone Heroic 4 + 1034 + 1 + + + 7148 + 7688 + 8071 + 11010 + 13331 + 15043 + 15067 + 15073 + 15073 + 15153 + + + + + + 225 + Brim205 + Brimstone Heroic 5 + 1034 + 1 + + + 5743 + 7148 + 8071 + 11954 + 12607 + 15043 + 15067 + 15073 + 15073 + 15153 + + + + + + 226 + Brim206 + Brimstone Heroic 6 + 1689 + 1 + + + 8292 + 10180 + 11648 + 12397 + 14088 + 14971 + 15067 + 15073 + 15073 + 15153 + + + + + + 227 + Brim207 + Brimstone Heroic 7 + 1694 + 1 + + + 7688 + 8292 + 10180 + 11618 + 12397 + 14971 + 15067 + 15073 + 15153 + 15153 + + + + + + 231 + Brim301 + Brimstone Mythic 1 + 1360 + 1 + + + 5749 + 8408 + 10048 + 10186 + 10438 + 15068 + 15068 + 15074 + 15156 + 15197 + + + + + + 232 + Brim302 + Brimstone Mythic 2 + 1360 + 1 + + + 8408 + 10186 + 10438 + 10871 + 11498 + 12710 + 15068 + 15068 + 15074 + 15156 + + + + + + 233 + Brim303 + Brimstone Mythic 3 + 1685 + 1 + + + 7694 + 8408 + 10186 + 11408 + 12891 + 14094 + 15068 + 15068 + 15074 + 15156 + + + + + + 234 + Brim304 + Brimstone Mythic 4 + 1034 + 1 + + + 5749 + 8071 + 10048 + 10438 + 11016 + 13337 + 15068 + 15074 + 15074 + 15156 + + + + + + 235 + Brim305 + Brimstone Mythic 5 + 1034 + 1 + + + 7148 + 7694 + 8071 + 11960 + 12891 + 14305 + 15068 + 15074 + 15074 + 15156 + + + + + + 236 + Brim306 + Brimstone Mythic 6 + 1691 + 1 + + + 7382 + 7694 + 8292 + 10186 + 10734 + 14971 + 15068 + 15074 + 15074 + 15156 + + + + + + 237 + Brim307 + Brimstone Mythic 7 + 1697 + 1 + + + 7382 + 8292 + 10186 + 11618 + 12397 + 14743 + 15068 + 15074 + 15156 + 15156 + + + + + + 311 + Secr101 + Secrets Revealed Normal 1 + 1018 + 1 + + + 4468 + 4468 + 6329 + 6644 + 6644 + 12520 + 14103 + 15910 + 15914 + 15920 + + + + + + 312 + Secr102 + Secrets Revealed Normal 2 + 1018 + 1 + + + 517 + 517 + 951 + 5502 + 6335 + 6896 + 6896 + 15910 + 15914 + 15920 + + + + + + 313 + Secr103 + Secrets Revealed Normal 3 + 1714 + 1 + + + 7406 + 11468 + 12523 + 13487 + 13565 + 13644 + 15913 + 15913 + 15916 + 15920 + + + + + + 314 + Secr104 + Secrets Revealed Normal 4 + 1018 + 1 + + + 8173 + 11143 + 11143 + 11999 + 12716 + 13469 + 15113 + 15908 + 15916 + 15920 + + + + + + 315 + Secr105 + Secrets Revealed Normal 5 + 1018 + 1 + + + 6644 + 6644 + 6644 + 12008 + 14007 + 14100 + 14672 + 15913 + 15916 + 15920 + + + + + + 316 + Secr106 + Secrets Revealed Normal 6 + 1720 + 1 + + + 543 + 11239 + 13433 + 14008 + 14100 + 14617 + 15913 + 15919 + 15919 + 15920 + + + + + + 317 + Secr107 + Secrets Revealed Normal 7 + 1726 + 1 + + + 4921 + 10758 + 11239 + 12722 + 13029 + 14010 + 15913 + 15919 + 15920 + 15920 + + + + + + 321 + Secr201 + Secrets Revealed Heroic 1 + 1044 + 1 + + + 6332 + 7671 + 12523 + 13481 + 14106 + 14674 + 15601 + 15913 + 15919 + 15922 + + + + + + 322 + Secr202 + Secrets Revealed Heroic 2 + 1044 + 1 + + + 4289 + 4738 + 5508 + 6339 + 11828 + 12526 + 12526 + 15913 + 15919 + 15922 + + + + + + 323 + Secr203 + Secrets Revealed Heroic 3 + 1716 + 1 + + + 7412 + 11828 + 12526 + 13493 + 13571 + 13652 + 15913 + 15913 + 15919 + 15922 + + + + + + 324 + Secr204 + Secrets Revealed Heroic 4 + 1201 + 1 + + + 11149 + 11149 + 11245 + 14013 + 14617 + 15116 + 15245 + 15913 + 15919 + 15922 + + + + + + 325 + Secr205 + Secrets Revealed Heroic 5 + 1201 + 1 + + + 8462 + 12722 + 13475 + 14317 + 14674 + 15395 + 15907 + 15913 + 15919 + 15922 + + + + + + 326 + Secr206 + Secrets Revealed Heroic 6 + 1722 + 1 + + + 854 + 11245 + 13439 + 14013 + 14106 + 14620 + 15913 + 15919 + 15919 + 15922 + + + + + + 327 + Secr207 + Secrets Revealed Heroic 7 + 1728 + 1 + + + 10764 + 11245 + 11531 + 12728 + 13032 + 14016 + 15913 + 15919 + 15922 + 15922 + + + + + + 331 + Secr301 + Secrets Revealed Mythic 1 + 1046 + 1 + + + 6344 + 12529 + 12535 + 13571 + 13571 + 13649 + 14112 + 15913 + 15919 + 15925 + + + + + + 332 + Secr302 + Secrets Revealed Mythic 2 + 1046 + 1 + + + 4741 + 6344 + 7676 + 11444 + 11444 + 12499 + 13655 + 15913 + 15919 + 15925 + + + + + + 333 + Secr303 + Secrets Revealed Mythic 3 + 1719 + 1 + + + 7418 + 11834 + 12535 + 13499 + 13571 + 13661 + 15913 + 15913 + 15919 + 15925 + + + + + + 334 + Secr304 + Secrets Revealed Mythic 4 + 1204 + 1 + + + 12643 + 12795 + 14112 + 14112 + 14677 + 14677 + 15913 + 15919 + 15925 + 16299 + + + + + + 335 + Secr305 + Secrets Revealed Mythic 5 + 1204 + 1 + + + 7166 + 12646 + 12801 + 14413 + 14677 + 14779 + 15913 + 15919 + 15925 + 16299 + + + + + + 336 + Secr306 + Secrets Revealed Mythic 6 + 1725 + 1 + + + 4177 + 11251 + 13445 + 14022 + 14112 + 14623 + 15913 + 15919 + 15919 + 15925 + + + + + + 337 + Secr307 + Secrets Revealed Mythic 7 + 1731 + 1 + + + 7178 + 7532 + 10770 + 11251 + 14022 + 14022 + 15913 + 15919 + 15925 + 15925 + + + + + + 411 + Deid101 + Deidon Normal 1 + 1022 + 1 + + + 4472 + 4472 + 6648 + 6648 + 12822 + 16054 + 16746 + 16768 + 16774 + 16780 + + + + + + 412 + Deid102 + Deidon Normal 2 + 1022 + 1 + + + 443 + 443 + 6652 + 6652 + 7322 + 7907 + 12810 + 16768 + 16774 + 16780 + + + + + + 413 + Deid103 + Deidon Normal 3 + 1742 + 1 + + + 7322 + 11537 + 12807 + 14420 + 14426 + 16752 + 16773 + 16773 + 16774 + 16780 + + + + + + 414 + Deid104 + Deidon Normal 4 + 1022 + 1 + + + 443 + 443 + 2292 + 13958 + 16033 + 16120 + 16587 + 16768 + 16774 + 16780 + + + + + + 415 + Deid105 + Deidon Normal 5 + 1022 + 1 + + + 2292 + 4476 + 4476 + 4476 + 10773 + 11324 + 15323 + 16768 + 16774 + 16780 + + + + + + 416 + Deid106 + Deidon Normal 6 + 1748 + 1 + + + 5384 + 8346 + 10210 + 10780 + 12038 + 13815 + 16768 + 16779 + 16779 + 16780 + + + + + + 417 + Deid107 + Deidon Normal 7 + 1754 + 1 + + + 2578 + 6602 + 12439 + 13181 + 14034 + 15078 + 16768 + 16774 + 16780 + 16780 + + + + + + 421 + Deid201 + Deidon Heroic 1 + 1049 + 1 + + + 6929 + 7914 + 7920 + 12825 + 14426 + 16051 + 16749 + 16770 + 16776 + 16782 + + + + + + 422 + Deid202 + Deidon Heroic 2 + 1049 + 1 + + + 6932 + 6932 + 7328 + 12813 + 13418 + 14425 + 16755 + 16770 + 16776 + 16782 + + + + + + 423 + Deid203 + Deidon Heroic 3 + 1744 + 1 + + + 12813 + 12951 + 13418 + 13418 + 16057 + 16761 + 16773 + 16773 + 16776 + 16782 + + + + + + 424 + Deid204 + Deidon Heroic 4 + 1051 + 1 + + + 2572 + 11816 + 14023 + 14136 + 15703 + 16045 + 16045 + 16770 + 16776 + 16782 + + + + + + 425 + Deid205 + Deidon Heroic 5 + 1051 + 1 + + + 14635 + 15320 + 15329 + 15709 + 16194 + 16449 + 16770 + 16776 + 16782 + 16921 + + + + + + 426 + Deid206 + Deidon Heroic 6 + 1750 + 1 + + + 247 + 10213 + 12445 + 15709 + 16551 + 16770 + 16779 + 16779 + 16782 + 16959 + + + + + + 427 + Deid207 + Deidon Heroic 7 + 1756 + 1 + + + 2578 + 10782 + 13823 + 14032 + 14136 + 15084 + 16770 + 16776 + 16782 + 16782 + + + + + + 431 + Deid301 + Deidon Mythic 1 + 1051 + 1 + + + 7922 + 12837 + 12957 + 14431 + 14431 + 16063 + 16761 + 16773 + 16779 + 16785 + + + + + + 432 + Deid302 + Deidon Mythic 2 + 1051 + 1 + + + 7334 + 7922 + 11546 + 11546 + 12819 + 12957 + 12957 + 16773 + 16779 + 16785 + + + + + + 433 + Deid303 + Deidon Mythic 3 + 1747 + 1 + + + 7922 + 12963 + 13421 + 14437 + 15757 + 16599 + 16773 + 16773 + 16779 + 16785 + + + + + + 434 + Deid304 + Deidon Mythic 4 + 1052 + 1 + + + 2578 + 2578 + 12939 + 13901 + 14569 + 14569 + 16045 + 16773 + 16779 + 16785 + + + + + + 435 + Deid305 + Deidon Mythic 5 + 1052 + 1 + + + 12451 + 13385 + 15709 + 15709 + 15811 + 16203 + 16203 + 16773 + 16779 + 16785 + + + + + + 436 + Deid306 + Deidon Mythic 6 + 1753 + 1 + + + 10788 + 13679 + 14269 + 15547 + 15709 + 16773 + 16779 + 16779 + 16785 + 16929 + + + + + + 437 + Deid307 + Deidon Mythic 7 + 1759 + 1 + + + 11822 + 13823 + 14148 + 14269 + 14275 + 15092 + 16773 + 16779 + 16785 + 16785 + + + + + + 511 + Kali101 + Kalihmah Normal 1 + 1150 + 1 + + + 4460 + 4460 + 4460 + 6636 + 10369 + 10647 + 12768 + 30450 + 30454 + 30460 + + + + + + 512 + Kali102 + Kalihmah Normal 2 + 1150 + 1 + + + 4460 + 4460 + 6636 + 6636 + 12197 + 12278 + 15712 + 30450 + 30454 + 30460 + + + + + + 513 + Kali103 + Kalihmah Normal 3 + 1776 + 1 + + + 650 + 8648 + 11414 + 12898 + 13005 + 16512 + 30450 + 30450 + 30454 + 30460 + + + + + + 514 + Kali104 + Kalihmah Normal 4 + 1150 + 1 + + + 6632 + 6632 + 7220 + 8648 + 12508 + 14695 + 14977 + 30448 + 30456 + 30460 + + + + + + 515 + Kali105 + Kalihmah Normal 5 + 1150 + 1 + + + 4506 + 6636 + 6636 + 6636 + 12894 + 14389 + 16401 + 30448 + 30456 + 30460 + + + + + + 516 + Kali106 + Kalihmah Normal 6 + 1782 + 1 + + + 5276 + 5279 + 11840 + 12281 + 15937 + 30348 + 30448 + 30456 + 30456 + 30460 + + + + + + 517 + Kali107 + Kalihmah Normal 7 + 1788 + 1 + + + 11275 + 11502 + 13349 + 14593 + 15631 + 16989 + 30448 + 30454 + 30460 + 30460 + + + + + + 521 + Kali201 + Kalihmah Heroic 1 + 1796 + 1 + + + 8435 + 10372 + 10372 + 15014 + 15571 + 15724 + 16266 + 30453 + 30456 + 30462 + + + + + + 522 + Kali202 + Kalihmah Heroic 2 + 1797 + 1 + + + 2283 + 7058 + 10378 + 10384 + 10384 + 10656 + 16887 + 30453 + 30456 + 30462 + + + + + + 523 + Kali203 + Kalihmah Heroic 3 + 1778 + 1 + + + 10749 + 10749 + 11420 + 14707 + 14983 + 30309 + 30453 + 30453 + 30456 + 30462 + + + + + + 524 + Kali204 + Kalihmah Heroic 4 + 1213 + 1 + + + 5652 + 7628 + 10749 + 12511 + 13011 + 14704 + 14704 + 30450 + 30459 + 30462 + + + + + + 525 + Kali205 + Kalihmah Heroic 5 + 1213 + 1 + + + 4512 + 11843 + 11852 + 12900 + 13017 + 14395 + 16521 + 30450 + 30459 + 30462 + + + + + + 526 + Kali206 + Kalihmah Heroic 6 + 1784 + 1 + + + 5282 + 11504 + 12287 + 12287 + 15679 + 16357 + 30450 + 30459 + 30459 + 30462 + + + + + + 527 + Kali207 + Kalihmah Heroic 7 + 1793 + 1 + + + 4083 + 11912 + 13349 + 14049 + 14605 + 16989 + 30450 + 30456 + 30462 + 30462 + + + + + + 531 + Kali301 + Kalihmah Mythic 1 + 1799 + 1 + + + 7292 + 10384 + 15019 + 15019 + 15727 + 30453 + 30459 + 30465 + + + + + + 532 + Kali302 + Kalihmah Mythic 2 + 1799 + 1 + + + 10662 + 10883 + 10883 + 11227 + 11227 + 15583 + 15583 + 30453 + 30459 + 30465 + + + + + + 533 + Kali303 + Kalihmah Mythic 3 + 1781 + 1 + + + 11426 + 11426 + 12909 + 13553 + 14707 + 14707 + 30453 + 30453 + 30459 + 30465 + + + + + + 534 + Kali304 + Kalihmah Mythic 4 + 1216 + 1 + + + 8657 + 10752 + 14707 + 14707 + 14989 + 14989 + 30453 + 30459 + 30465 + 30597 + + + + + + 535 + Kali305 + Kalihmah Mythic 5 + 1216 + 1 + + + 4512 + 4560 + 4560 + 16527 + 16527 + 30315 + 30315 + 30453 + 30459 + 30465 + + + + + + 536 + Kali306 + Kalihmah Mythic 6 + 1787 + 1 + + + 5288 + 5288 + 5789 + 10883 + 16947 + 16989 + 30453 + 30459 + 30459 + 30465 + + + + + + 531 + Kali301 + Kalihmah Mythic 1 + 1799 + 1 + + + 7292 + 10384 + 15019 + 15019 + 15727 + 30453 + 30459 + 30465 + + + + + + 532 + Kali302 + Kalihmah Mythic 2 + 1799 + 1 + + + 10662 + 10883 + 10883 + 11227 + 11227 + 15583 + 15583 + 30453 + 30459 + 30465 + + + + + + 533 + Kali303 + Kalihmah Mythic 3 + 1781 + 1 + + + 11426 + 11426 + 12909 + 13553 + 14707 + 14707 + 30453 + 30453 + 30459 + 30465 + + + + + + 534 + Kali304 + Kalihmah Mythic 4 + 1216 + 1 + + + 8657 + 10752 + 14707 + 14707 + 14989 + 14989 + 30453 + 30459 + 30465 + 30597 + + + + + + 535 + Kali305 + Kalihmah Mythic 5 + 1216 + 1 + + + 4512 + 4560 + 4560 + 16527 + 16527 + 30315 + 30315 + 30453 + 30459 + 30465 + + + + + + 536 + Kali306 + Kalihmah Mythic 6 + 1787 + 1 + + + 5288 + 5288 + 5789 + 10883 + 16947 + 16989 + 30453 + 30459 + 30459 + 30465 + + + + + + 537 + Kali307 + Kalihmah Mythic 7 + 1793 + 1 + + + 7298 + 7976 + 13355 + 13355 + 14052 + 15745 + 30453 + 30459 + 30465 + 30465 + + + + + + 611 + Ward101 + Warden Normal 1 + 1570 + 1 + + + 4439 + 4439 + 6556 + 6556 + 30058 + 30600 + 30708 + 31415 + 31419 + 31425 + + + + + + 612 + Ward102 + Warden Normal 2 + 1570 + 1 + + + 4444 + 4444 + 6560 + 6560 + 16762 + 16788 + 31415 + 31419 + 31425 + 31472 + + + + + + 613 + Ward103 + Warden Normal 3 + 1837 + 1 + + + 4598 + 6434 + 6434 + 31271 + 31415 + 31415 + 31419 + 31425 + 31469 + 31601 + + + + + + 614 + Ward104 + Warden Normal 4 + 1570 + 1 + + + 4439 + 4439 + 10587 + 12476 + 31271 + 31413 + 31421 + 31425 + 31598 + 31598 + + + + + + 615 + Ward105 + Warden Normal 5 + 1570 + 1 + + + 4444 + 4444 + 6560 + 6658 + 12218 + 15823 + 16972 + 31413 + 31421 + 31425 + + + + + + 616 + Ward106 + Warden Normal 6 + 1843 + 1 + + + 4095 + 7772 + 13065 + 13292 + 30097 + 30609 + 31413 + 31421 + 31421 + 31425 + + + + + + 617 + Ward107 + Warden Normal 7 + 1849 + 1 + + + 6750 + 12843 + 12966 + 13505 + 15299 + 31274 + 31413 + 31419 + 31425 + 31425 + + + + + + 621 + Ward201 + Warden Heroic 1 + 1833 + 1 + + + 30064 + 30502 + 30603 + 30612 + 30612 + 30711 + 30711 + 31415 + 31421 + 31427 + + + + + + 622 + Ward202 + Warden Heroic 2 + 1833 + 1 + + + 16767 + 16797 + 30513 + 30516 + 31190 + 31334 + 31415 + 31421 + 31427 + 31478 + + + + + + 623 + Ward203 + Warden Heroic 3 + 1839 + 1 + + + 6441 + 6441 + 6534 + 16975 + 31280 + 31418 + 31418 + 31421 + 31427 + 31601 + + + + + + 624 + Ward204 + Warden Heroic 4 + 1835 + 1 + + + 10590 + 10590 + 12478 + 12478 + 14073 + 31415 + 31421 + 31424 + 31604 + 31604 + + + + + + 625 + Ward205 + Warden Heroic 5 + 1835 + 1 + + + 6664 + 12224 + 12224 + 13071 + 15829 + 15829 + 16977 + 31415 + 31421 + 31427 + + + + + + 626 + Ward206 + Warden Heroic 6 + 1845 + 1 + + + 7592 + 7772 + 13077 + 13316 + 30103 + 30609 + 31415 + 31424 + 31424 + 31427 + + + + + + 627 + Ward207 + Warden Heroic 7 + 1851 + 1 + + + 8384 + 12855 + 12972 + 13511 + 16689 + 31415 + 31421 + 31427 + 31427 + 31661 + + + + + + 631 + Ward301 + Warden Mythic 1 + 1835 + 1 + + + 16851 + 30073 + 30513 + 30513 + 30615 + 30615 + 30723 + 31418 + 31424 + 31430 + + + + + + 632 + Ward302 + Warden Mythic 2 + 1835 + 1 + + + 16371 + 16767 + 16803 + 16851 + 31190 + 31190 + 31418 + 31424 + 31430 + 31484 + + + + + + 633 + Ward303 + Warden Mythic 3 + 1842 + 1 + + + 6444 + 6548 + 12233 + 14076 + 31280 + 31418 + 31418 + 31424 + 31430 + 31610 + + + + + + 634 + Ward304 + Warden Mythic 4 + 1836 + 1 + + + 10596 + 12481 + 12481 + 14076 + 16977 + 31418 + 31424 + 31430 + 31610 + 31610 + + + + + + 635 + Ward305 + Warden Mythic 5 + 1836 + 1 + + + 7592 + 12233 + 12233 + 15829 + 15829 + 16491 + 16977 + 31418 + 31424 + 31430 + + + + + + 636 + Ward306 + Warden Mythic 6 + 1848 + 1 + + + 8575 + 14725 + 15347 + 30109 + 30519 + 31418 + 31424 + 31424 + 31430 + 31484 + + + + + + 637 + Ward307 + Warden Mythic 7 + 1854 + 1 + + + 7820 + 12981 + 14076 + 16689 + 30519 + 31418 + 31424 + 31430 + 31430 + 31670 + + + + + + 711 + Gate101 + Gateway Normal 1 + 1259 + 1 + + + 4439 + 4439 + 4456 + 4456 + 6801 + 16692 + 16878 + 32299 + 32305 + 32311 + + + + + + 712 + Gate102 + Gateway Normal 2 + 1007 + 1 + + + 4444 + 4444 + 4460 + 4460 + 8832 + 13580 + 32299 + 32305 + 32311 + 32823 + + + + + + 713 + Gate103 + Gateway Normal 3 + 1865 + 1 + + + 8835 + 13580 + 16620 + 16881 + 31343 + 32301 + 32301 + 32305 + 32311 + 32623 + + + + + + 714 + Gate104 + Gateway Normal 4 + 1011 + 1 + + + 4439 + 4439 + 4456 + 30414 + 31601 + 31926 + 32299 + 32305 + 32311 + 32460 + + + + + + 715 + Gate105 + Gateway Normal 5 + 1015 + 1 + + + 4444 + 4460 + 31277 + 31601 + 31926 + 32299 + 32305 + 32311 + 32460 + 32586 + + + + + + 716 + Gate106 + Gateway Normal 6 + 1871 + 1 + + + 31280 + 31310 + 31403 + 31745 + 32160 + 32299 + 32307 + 32307 + 32311 + 32586 + + + + + + 717 + Gate107 + Gateway Normal 7 + 1877 + 1 + + + 15869 + 30561 + 31310 + 31640 + 32163 + 32299 + 32305 + 32311 + 32311 + 32568 + + + + + + 721 + Gate201 + Gateway Heroic 1 + 1501 + 1 + + + 6813 + 6813 + 7799 + 14804 + 16704 + 16890 + 16890 + 32301 + 32307 + 32313 + + + + + + 722 + Gate202 + Gateway Heroic 2 + 1189 + 1 + + + 8844 + 13586 + 14809 + 31352 + 32301 + 32313 + 32631 + 32832 + + + 7799 + 32307 + + + 7802 + 32310 + + + + + + 723 + Gate203 + Gateway Heroic 3 + 1867 + 1 + + + 13583 + 13583 + 16626 + 16893 + 31704 + 32304 + 32304 + 32307 + 32313 + 32628 + + + + + + 724 + Gate204 + Gateway Heroic 4 + 1196 + 1 + + + 14154 + 30420 + 31364 + 31604 + 31604 + 31932 + 32301 + 32307 + 32313 + 32466 + + + + + + 725 + Gate205 + Gateway Heroic 5 + 1484 + 1 + + + 14157 + 30423 + 30531 + 31226 + 31286 + 31364 + 32301 + 32307 + 32313 + 32589 + + + + + + 726 + Gate206 + Gateway Heroic 6 + 1873 + 1 + + + 31313 + 31409 + 31436 + 31745 + 32166 + 32301 + 32310 + 32310 + 32313 + 32589 + + + + + + 727 + Gate207 + Gateway Heroic 7 + 1879 + 1 + + + 15874 + 30564 + 31319 + 31646 + 32163 + 32301 + 32307 + 32313 + 32313 + 32571 + + + + + + 731 + Gate301 + Gateway Mythic 1 + 1503 + 1 + + + 6816 + 12215 + 16893 + 16893 + 31896 + 32304 + 32310 + 32316 + 32640 + 32640 + + + + + + 732 + Gate302 + Gateway Mythic 2 + 1191 + 1 + + + 8847 + 13589 + 31358 + 31358 + 31712 + 31896 + 32304 + 32310 + 32316 + 32838 + + + + + + 733 + Gate303 + Gateway Mythic 3 + 1870 + 1 + + + 13589 + 13589 + 16893 + 31718 + 32304 + 32304 + 32310 + 32316 + 32634 + 32634 + + + + + + 734 + Gate304 + Gateway Mythic 4 + 1198 + 1 + + + 14166 + 31376 + 31610 + 31610 + 31938 + 31938 + 32304 + 32310 + 32316 + 32472 + + + + + + 735 + Gate305 + Gateway Mythic 5 + 1486 + 1 + + + 14166 + 14166 + 30333 + 30429 + 31232 + 31286 + 32304 + 32310 + 32316 + 32598 + + + + + + 736 + Gate306 + Gateway Mythic 6 + 1876 + 1 + + + 30333 + 31412 + 31448 + 31754 + 32304 + 32310 + 32310 + 32316 + 32598 + 32801 + + + + + + 737 + Gate307 + Gateway Mythic 7 + 1882 + 1 + + + 15775 + 30573 + 30793 + 31992 + 32304 + 32310 + 32316 + 32316 + 32580 + 32801 + + + + + + 811 + Emry101 + Emrys Ascended Normal 1 + 1022 + 1 + + + 6648 + 6648 + 10243 + 14115 + 33305 + 33447 + 33453 + 33459 + + + 443 + 471 + 4472 + + + + + + 812 + Emry102 + Emrys Ascended Normal 2 + 1022 + 1 + + + 6652 + 6652 + 7109 + 15314 + 16956 + 33447 + 33453 + 33459 + + + 443 + 4476 + 4552 + 4781 + 12503 + 13419 + + + + + + 813 + Emry103 + Emrys Ascended Normal 3 + 1893 + 1 + + + 7112 + 14118 + 16914 + 16956 + 31397 + 32053 + 33449 + 33449 + 33453 + 33459 + + + + + + 814 + Emry104 + Emrys Ascended Normal 4 + 1022 + 1 + + + 6648 + 8489 + 14629 + 15700 + 30871 + 33447 + 33453 + 33459 + + + 443 + 471 + 4472 + 4552 + + + + + + 815 + Emry105 + Emrys Ascended Normal 5 + 1022 + 1 + + + 6652 + 14629 + 15700 + 15799 + 30871 + 31947 + 33447 + 33453 + 33459 + + + 443 + 4476 + 4552 + + + + + + 816 + Emry106 + Emrys Ascended Normal 6 + 1900 + 1 + + + 15796 + 15802 + 31947 + 32475 + 33164 + 33447 + 33455 + 33455 + 33459 + 33704 + + + + + + 817 + Emry107 + Emrys Ascended Normal 7 + 1906 + 1 + + + 12673 + 15697 + 33167 + 33308 + 33434 + 33447 + 33453 + 33459 + 33459 + 33704 + + + + + + 821 + Emry201 + Emrys Ascended Heroic 1 + 1049 + 1 + + + 10255 + 14127 + 14127 + 31054 + 32719 + 33317 + 33317 + 33449 + 33455 + 33461 + + + + + + 822 + Emry202 + Emrys Ascended Heroic 2 + 1049 + 1 + + + 7121 + 15323 + 16962 + 31059 + 32061 + 32722 + 33449 + 33455 + 33461 + 16923 + + + + + + 823 + Emry203 + Emrys Ascended Heroic 3 + 1895 + 1 + + + 5871 + 14130 + 16959 + 16959 + 31409 + 32061 + 33452 + 33452 + 33455 + 33461 + + + + + + 824 + Emry204 + Emrys Ascended Heroic 4 + 1050 + 1 + + + 8495 + 14635 + 15703 + 15703 + 16123 + 30877 + 33449 + 33455 + 33461 + 33596 + + + + + + 825 + Emry205 + Emrys Ascended Heroic 5 + 1050 + 1 + + + 8498 + 15802 + 16123 + 30681 + 31460 + 31956 + 33449 + 33455 + 33461 + 33599 + + + + + + 826 + Emry206 + Emrys Ascended Heroic 6 + 1902 + 1 + + + 7880 + 15802 + 15802 + 32487 + 33170 + 33449 + 33458 + 33458 + 33461 + 33707 + + + + + + 827 + Emry207 + Emrys Ascended Heroic 7 + 1908 + 1 + + + 12678 + 15703 + 33167 + 33311 + 33437 + 33449 + 33455 + 33461 + 33461 + 33713 + + + + + + 831 + Emry301 + Emrys Ascended Mythic 1 + 1051 + 1 + + + 14130 + 14130 + 16557 + 31065 + 31065 + 32728 + 33320 + 33452 + 33458 + 33464 + + + + + + 832 + Emry302 + Emrys Ascended Mythic 2 + 1051 + 1 + + + 5877 + 7124 + 15329 + 16557 + 16929 + 16929 + 16965 + 33452 + 33458 + 33464 + + + + + + 833 + Emry303 + Emrys Ascended Mythic 3 + 1898 + 1 + + + 5883 + 14130 + 16965 + 32064 + 32064 + 33452 + 33458 + 33464 + + 16965 + 33452 + 33458 + + + + + + + 834 + Emry304 + Emrys Ascended Mythic 4 + 32550 + 1 + + + 14641 + 14641 + 15709 + 15709 + 16135 + 30883 + 33452 + 33458 + 33464 + 33608 + + + + + + 835 + Emry305 + Emrys Ascended Mythic 5 + 32550 + 1 + + + 8504 + 15086 + 15811 + 30687 + 31956 + 33452 + 33458 + 33464 + 33608 + 33608 + + + + + + 836 + Emry306 + Emrys Ascended Mythic 6 + 1905 + 1 + + + 7892 + 15086 + 15811 + 32490 + 32820 + 33452 + 33458 + 33464 + + 15811 + 33452 + 33458 + + + + + + + 837 + Emry307 + Emrys Ascended Mythic 7 + 1911 + 1 + + + 6728 + 10698 + 12687 + 13871 + 32820 + 33320 + 33446 + 33452 + 33458 + 33464 + 33464 + + + + + + 911 + Meph101 + Mephisopheles Normal 1 + 1014 + 1 + + + 4456 + 4456 + 6632 + 6632 + 11090 + 16396 + 34229 + 34235 + 34241 + 34663 + + + + + + 912 + Meph102 + Mephisopheles Normal 2 + 1014 + 1 + + + 4460 + 4460 + 6636 + 6636 + 16876 + 31020 + 32821 + 34229 + 34235 + 34241 + + + + + + 913 + Meph103 + Mephisopheles Normal 3 + 1928 + 1 + + + 16398 + 30730 + 31018 + 32137 + 32823 + 34229 + 34229 + 34235 + 34241 + 34717 + + + + + + 914 + Meph104 + Mephisopheles Normal 4 + 1014 + 1 + + + 4456 + 4456 + 6632 + 14690 + 14974 + 15712 + 30224 + 34229 + 34235 + 34241 + + + + + + 915 + Meph105 + Mephisopheles Normal 5 + 1014 + 1 + + + 4460 + 6636 + 14977 + 15712 + 30224 + 33471 + 34229 + 34235 + 34241 + 35078 + + + + + + 916 + Meph106 + Mephisopheles Normal 6 + 1934 + 1 + + + 33399 + 33473 + 33557 + 34229 + 34235 + 34235 + 34241 + 34483 + 34541 + 35081 + + + + + + 917 + Meph107 + Mephisopheles Normal 7 + 1940 + 1 + + + 30799 + 31778 + 31890 + 33563 + 34126 + 34229 + 34235 + 34241 + 34241 + 34486 + + + + + + 921 + Meph201 + Mephisopheles Heroic 1 + 1037 + 1 + + + 11102 + 11102 + 16408 + 16408 + 16518 + 34231 + 34237 + 34243 + 34673 + 34795 + + + + + + 922 + Meph202 + Mephisopheles Heroic 2 + 1037 + 1 + + + 16518 + 16884 + 31024 + 32145 + 32833 + 34231 + 34237 + 34243 + 34727 + 34795 + + + + + + 923 + Meph203 + Mephisopheles Heroic 3 + 1930 + 1 + + + 14482 + 16408 + 30738 + 31020 + 31020 + 34234 + 34234 + 34237 + 34243 + 34723 + + + + + + 924 + Meph204 + Mephisopheles Heroic 4 + 1038 + 1 + + + 14698 + 14980 + 15718 + 30226 + 30226 + 31364 + 33506 + 34231 + 34237 + 34243 + + + + + + 925 + Meph205 + Mephisopheles Heroic 5 + 1038 + 1 + + + 14698 + 31364 + 32535 + 33257 + 33477 + 33509 + 34231 + 34237 + 34243 + 35084 + + + + + + 926 + Meph206 + Mephisopheles Heroic 6 + 1936 + 1 + + + 32439 + 33405 + 33563 + 34231 + 34240 + 34240 + 34243 + 34489 + 34543 + 35082 + + + + + + 927 + Meph207 + Mephisopheles Heroic 7 + 1942 + 1 + + + 30802 + 31781 + 31891 + 33563 + 34129 + 34231 + 34237 + 34243 + 34243 + 34493 + + + + + + 931 + Meph301 + Mephisopheles Mythic 1 + 1039 + 1 + + + 11107 + 16413 + 16413 + 16527 + 16527 + 30931 + 34234 + 34240 + 34246 + 34804 + + + + + + 932 + Meph302 + Mephisopheles Mythic 2 + 1039 + 1 + + + 14491 + 16893 + 30931 + 31029 + 32154 + 32154 + 32838 + 34234 + 34240 + 34246 + + + + + + 933 + Meph303 + Mephisopheles Mythic 3 + 1933 + 1 + + + 14497 + 16413 + 16413 + 31029 + 31029 + 34234 + 34234 + 34240 + 34246 + 34732 + + + + + + 934 + Meph304 + Mephisopheles Mythic 4 + 1040 + 1 + + + 14989 + 14989 + 15727 + 30235 + 30235 + 31376 + 33518 + 34234 + 34240 + 34246 + + + + + + 935 + Meph305 + Mephisopheles Mythic 5 + 1040 + 1 + + + 14707 + 30315 + 33266 + 33482 + 33518 + 33518 + 34234 + 34240 + 34246 + 35093 + + + + + + 936 + Meph306 + Mephisopheles Mythic 6 + 1939 + 1 + + + 30315 + 32454 + 33068 + 33410 + 34234 + 34240 + 34240 + 34246 + 34552 + 35093 + + + + + + 937 + Meph307 + Mephisopheles Mythic 7 + 1945 + 1 + + + 11516 + 14058 + 30811 + 31902 + 33068 + 34138 + 34234 + 34240 + 34246 + 34246 + 34354 + + + + + + 1011 + Nexu101 + Nexus Arachis Normal 1 + 1018 + 1 + + + 4464 + 4464 + 6640 + 6640 + 34735 + 35504 + 35510 + 35516 + 35704 + 36504 + + + + + + 1012 + Nexu102 + Nexus Arachis Normal 2 + 1018 + 1 + + + 4468 + 4468 + 6644 + 6644 + 33269 + 33683 + 35332 + 35504 + 35510 + 35516 + + + + + + 1013 + Nexu103 + Nexus Arachis Normal 3 + 1962 + 1 + + + 14612 + 33686 + 35332 + 35506 + 35506 + 35510 + 35516 + 35866 + 36004 + 36507 + + + + + + 1014 + Nexu104 + Nexus Arachis Normal 4 + 1018 + 1 + + + 4464 + 4464 + 6640 + 16530 + 30190 + 33182 + 35275 + 35504 + 35510 + 35516 + + + + + + 1015 + Nexu105 + Nexus Arachis Normal 5 + 1018 + 1 + + + 4468 + 6644 + 30190 + 32770 + 33182 + 35275 + 35458 + 35504 + 35510 + 35516 + + + + + + 1016 + Nexu106 + Nexus Arachis Normal 6 + 1968 + 1 + + + 16018 + 32268 + 32770 + 33940 + 35461 + 35504 + 35512 + 35512 + 35516 + 36010 + + + + + + 1017 + Nexu107 + Nexus Arachis Normal 7 + 1974 + 1 + + + 15761 + 32268 + 32568 + 33943 + 34834 + 35347 + 35504 + 35510 + 35516 + 35516 + + + + + + 1021 + Nexu201 + Nexus Arachis Heroic 1 + 1043 + 1 + + + 34747 + 34974 + 35506 + 35512 + 35518 + 35638 + 35716 + 35716 + 36516 + 36516 + + + + + + 1022 + Nexu202 + Nexus Arachis Heroic 2 + 1043 + 1 + + + 14620 + 33278 + 33695 + 34979 + 35338 + 35506 + 35512 + 35518 + 35641 + 36013 + + + + + + 1023 + Nexu203 + Nexus Arachis Heroic 3 + 1964 + 1 + + + 14620 + 34612 + 35335 + 35335 + 35509 + 35509 + 35512 + 35518 + 35878 + 36519 + + + + + + 1024 + Nexu204 + Nexus Arachis Heroic 4 + 1102 + 1 + + + 16536 + 30193 + 30193 + 33188 + 33578 + 33994 + 35281 + 35506 + 35512 + 35518 + + + + + + 1025 + Nexu205 + Nexus Arachis Heroic 5 + 1102 + 1 + + + 16539 + 32773 + 32940 + 33578 + 33997 + 35467 + 35506 + 35512 + 35518 + 36211 + + + + + + 1026 + Nexu206 + Nexus Arachis Heroic 6 + 1970 + 1 + + + 16024 + 32271 + 32699 + 32773 + 33946 + 35506 + 35515 + 35515 + 35518 + 36010 + + + + + + 1027 + Nexu207 + Nexus Arachis Heroic 7 + 1976 + 1 + + + 15766 + 32277 + 32571 + 33943 + 34837 + 35353 + 35506 + 35512 + 35518 + 35518 + + + + + + 1031 + Nexu301 + Nexus Arachis Mythic 1 + 1045 + 1 + + + 30333 + 34985 + 35509 + 35515 + 35521 + 35647 + 35719 + 35719 + 36519 + 36519 + + + + + + 1032 + Nexu302 + Nexus Arachis Mythic 2 + 1045 + 1 + + + 30333 + 33284 + 33698 + 34618 + 35341 + 35509 + 35515 + 35521 + 36019 + 36019 + + + + + + 1033 + Nexu303 + Nexus Arachis Mythic 3 + 1967 + 1 + + + 14623 + 14623 + 34624 + 35341 + 35341 + 35509 + 35509 + 35515 + 35521 + 36519 + + + + + + 1034 + Nexu304 + Nexus Arachis Mythic 4 + 1112 + 1 + + + 30199 + 30199 + 33194 + 33590 + 34006 + 35287 + 35287 + 35509 + 35515 + 35521 + + + + + + 1035 + Nexu305 + Nexus Arachis Mythic 5 + 1112 + 1 + + + 16545 + 32782 + 32946 + 33008 + 34006 + 34006 + 35467 + 35509 + 35515 + 35521 + + + + + + 1036 + Nexu306 + Nexus Arachis Mythic 6 + 1973 + 1 + + + 16027 + 32782 + 33008 + 35509 + 35515 + 35515 + 35521 + 35881 + 36019 + + + + + + + 1037 + Nexu307 + Nexus Arachis Mythic 7 + 1979 + 1 + + + 15775 + 17077 + 32580 + 34606 + 35509 + 35515 + 35521 + 35881 + 36591 + + + + + + diff --git a/deck.cpp b/deck.cpp new file mode 100644 index 00000000..6c2e80c3 --- /dev/null +++ b/deck.cpp @@ -0,0 +1,606 @@ +#include "deck.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "card.h" +#include "cards.h" +#include "read.h" + +template +void partial_shuffle(RandomAccessIterator first, RandomAccessIterator middle, + RandomAccessIterator last, + UniformRandomNumberGenerator&& g) +{ + typedef typename std::iterator_traits::difference_type diff_t; + typedef typename std::make_unsigned::type udiff_t; + typedef typename std::uniform_int_distribution distr_t; + typedef typename distr_t::param_type param_t; + + distr_t D; + diff_t m = middle - first; + diff_t n = last - first; + for (diff_t i = 0; i < m; ++i) + { + std::swap(first[i], first[D(g, param_t(i, n-1))]); + } +} + +//------------------------------------------------------------------------------ +const char* base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +const char* wmt_b64_magic_chars = "-.~!*"; + +// Converts cards in `hash' to a deck. +// Stores resulting card IDs in `ids'. +void hash_to_ids_wmt_b64(const char* hash, std::vector& ids) +{ + unsigned int last_id = 0; + const char* pc = hash; + + while(*pc) + { + unsigned id_plus = 0; + const char* pmagic = strchr(wmt_b64_magic_chars, *pc); + if(pmagic) + { + ++ pc; + id_plus = 4000 * (pmagic - wmt_b64_magic_chars + 1); + } + if(!*pc || !*(pc + 1)) + { + throw std::runtime_error("Invalid hash length"); + } + const char* p0 = strchr(base64_chars, *pc); + const char* p1 = strchr(base64_chars, *(pc + 1)); + if (!p0 || !p1) + { + throw std::runtime_error("Invalid hash character"); + } + pc += 2; + size_t index0 = p0 - base64_chars; + size_t index1 = p1 - base64_chars; + unsigned int id = (index0 << 6) + index1; + + if (id < 4001) + { + id += id_plus; + ids.push_back(id); + last_id = id; + } + else for (unsigned int j = 0; j < id - 4001; ++j) + { + ids.push_back(last_id); + } + } +} + +void encode_id_wmt_b64(std::stringstream &ios, unsigned card_id) +{ + if(card_id > 4000) + { + ios << wmt_b64_magic_chars[(card_id - 1) / 4000 - 1]; + card_id = (card_id - 1) % 4000 + 1; + } + ios << base64_chars[card_id / 64]; + ios << base64_chars[card_id % 64]; +} + +void encode_deck_wmt_b64(std::stringstream &ios, const Card* commander, std::vector cards) +{ + if (commander) + { + encode_id_wmt_b64(ios, commander->m_id); + } + unsigned last_id = 0; + unsigned num_repeat = 0; + for(const Card* card: cards) + { + auto card_id = card->m_id; + if(card_id == last_id) + { + ++ num_repeat; + } + else + { + if(num_repeat > 1) + { + ios << base64_chars[(num_repeat + 4000) / 64]; + ios << base64_chars[(num_repeat + 4000) % 64]; + } + last_id = card_id; + num_repeat = 1; + encode_id_wmt_b64(ios, card_id); + } + } + if(num_repeat > 1) + { + ios << base64_chars[(num_repeat + 4000) / 64]; + ios << base64_chars[(num_repeat + 4000) % 64]; + } +} + +void hash_to_ids_ext_b64(const char* hash, std::vector& ids) +{ + const char* pc = hash; + while (*pc) + { + unsigned id = 0; + unsigned factor = 1; + const char* p = strchr(base64_chars, *pc); + if (!p) + { throw std::runtime_error("Invalid hash character"); } + size_t d = p - base64_chars; + while (d < 32) + { + id += factor * d; + factor *= 32; + ++ pc; + p = strchr(base64_chars, *pc); + if (!p) + { throw std::runtime_error("Invalid hash character"); } + d = p - base64_chars; + } + id += factor * (d - 32); + ++ pc; + ids.push_back(id); + } +} + +void encode_id_ext_b64(std::stringstream &ios, unsigned card_id) +{ + while (card_id >= 32) + { + ios << base64_chars[card_id % 32]; + card_id /= 32; + } + ios << base64_chars[card_id + 32]; +} + +void encode_deck_ext_b64(std::stringstream &ios, const Card* commander, std::vector cards) +{ + if (commander) + { + encode_id_ext_b64(ios, commander->m_id); + } + for (const Card* card: cards) + { + encode_id_ext_b64(ios, card->m_id); + } +} + +void hash_to_ids_ddd_b64(const char* hash, std::vector& ids) +{ + const char* pc = hash; + while(*pc) + { + if(!*pc || !*(pc + 1) || !*(pc + 2)) + { + throw std::runtime_error("Invalid hash length"); + } + const char* p0 = strchr(base64_chars, *pc); + const char* p1 = strchr(base64_chars, *(pc + 1)); + const char* p2 = strchr(base64_chars, *(pc + 2)); + if (!p0 || !p1 || !p2) + { + throw std::runtime_error("Invalid hash character"); + } + pc += 3; + size_t index0 = p0 - base64_chars; + size_t index1 = p1 - base64_chars; + size_t index2 = p2 - base64_chars; + unsigned int id = (index0 << 12) + (index1 << 6) + index2; + ids.push_back(id); + } +} + +void encode_id_ddd_b64(std::stringstream &ios, unsigned card_id) +{ + ios << base64_chars[card_id / 4096]; + ios << base64_chars[card_id % 4096 / 64]; + ios << base64_chars[card_id % 64]; +} + +void encode_deck_ddd_b64(std::stringstream &ios, const Card* commander, std::vector cards) +{ + if (commander) + { + encode_id_ddd_b64(ios, commander->m_id); + } + for (const Card* card: cards) + { + encode_id_ddd_b64(ios, card->m_id); + } +} + +DeckDecoder hash_to_ids = hash_to_ids_ext_b64; +DeckEncoder encode_deck = encode_deck_ext_b64; + +namespace range = boost::range; + +void Deck::set(const std::vector& ids, const std::map &marks) +{ + commander = nullptr; + strategy = DeckStrategy::random; + for(auto id: ids) + { + const Card* card{all_cards.by_id(id)}; + if(card->m_type == CardType::commander) + { + if (commander == nullptr) + { + commander = card; + } + else + { + std::cerr << "WARNING: Ignoring additional commander " << card->m_name << " (" << commander->m_name << " already in deck)\n"; + } + } + else + { + cards.emplace_back(card); + } + } + if (commander == nullptr) + { + throw std::runtime_error("While constructing a deck: no commander found"); + } + commander_max_level = commander->m_top_level_card->m_level; + deck_size = cards.size(); + card_marks = marks; +} + +void Deck::set(const std::string& deck_string_) +{ + deck_string = deck_string_; +} + +void Deck::resolve() +{ + if (commander != nullptr) + { + return; + } + auto && id_marks = string_to_ids(all_cards, deck_string, short_description()); + set(id_marks.first, id_marks.second); + deck_string.clear(); +} + +void Deck::shrink(const unsigned deck_len) +{ + if (cards.size() > deck_len) + { + cards.resize(deck_len); + } +} + +void Deck::set_vip_cards(const std::string& deck_string) +{ + auto && id_marks = string_to_ids(all_cards, deck_string, "vip"); + for (const auto & cid : id_marks.first) + { + vip_cards.insert(cid); + } +} + +void Deck::set_given_hand(const std::string& deck_string) +{ + auto && id_marks = string_to_ids(all_cards, deck_string, "hand"); + given_hand = id_marks.first; +} + +void Deck::add_forts(const std::string& deck_string) +{ + auto && id_marks = string_to_ids(all_cards, deck_string, "fort_cards"); + for (auto id: id_marks.first) + { + fort_cards.push_back(all_cards.by_id(id)); + } +} + +std::string Deck::hash() const +{ + std::stringstream ios; + if (strategy == DeckStrategy::random) + { + auto sorted_cards = cards; + std::sort(sorted_cards.begin(), sorted_cards.end(), [](const Card* a, const Card* b) { return a->m_id < b->m_id; }); + encode_deck(ios, commander, sorted_cards); + } + else + { + encode_deck(ios, commander, cards); + } + return ios.str(); +} + +std::string Deck::short_description() const +{ + std::stringstream ios; + ios << decktype_names[decktype]; + if(id > 0) { ios << " #" << id; } + if(!name.empty()) { ios << " \"" << name << "\""; } + if(deck_string.empty()) + { + if(variable_cards.empty()) { ios << ": " << hash(); } + } + else + { + ios << ": " << deck_string; + } + return ios.str(); +} + +std::string Deck::medium_description() const +{ + std::stringstream ios; + ios << short_description() << std::endl; + if (commander) + { + ios << commander->m_name; + } + else + { + ios << "No commander"; + } + for (const Card * card: fort_cards) + { + ios << ", " << card->m_name; + } + for(const Card * card: cards) + { + ios << ", " << card->m_name; + } + unsigned num_pool_cards = 0; + for(auto& pool: variable_cards) + { + num_pool_cards += std::get<0>(pool) * std::get<1>(pool); + } + if(num_pool_cards > 0) + { + ios << ", and " << num_pool_cards << " cards from pool"; + } + if (upgrade_points > 0) + { + ios << " +" << upgrade_points << "/" << upgrade_opportunities; + } + return ios.str(); +} + +extern std::string card_description(const Cards& all_cards, const Card* c); + +std::string Deck::long_description() const +{ + std::stringstream ios; + ios << medium_description() << "\n"; + if (commander) + { + show_upgrades(ios, commander, commander_max_level, ""); + } + else + { + ios << "No commander\n"; + } + for (const Card * card: fort_cards) + { + show_upgrades(ios, card, card->m_top_level_card->m_level, ""); + } + for(const Card* card: cards) + { + show_upgrades(ios, card, card->m_top_level_card->m_level, " "); + } + for(auto& pool: variable_cards) + { + if (std::get<1>(pool) > 1) + { + ios << std::get<1>(pool) << " copies of each of "; + } + ios << std::get<0>(pool) << " in:\n"; + for(auto& card: std::get<2>(pool)) + { + show_upgrades(ios, card, card->m_top_level_card->m_level, " "); + } + } + return ios.str(); +} + +void Deck::show_upgrades(std::stringstream &ios, const Card* card, unsigned card_max_level, const char * leading_chars) const +{ + ios << leading_chars << card_description(all_cards, card) << "\n"; + if (upgrade_points == 0 || card->m_level == card_max_level) + { + return; + } + if (debug_print < 2 && decktype != DeckType::raid) + { + while (card->m_level != card_max_level) + { card = card->upgraded(); } + ios << leading_chars << "-> " << card_description(all_cards, card) << "\n"; + return; + } + // nCm * p^m / q^(n-m) + double p = 1.0 * upgrade_points / upgrade_opportunities; + double q = 1.0 - p; + unsigned n = card_max_level - card->m_level; + unsigned m = 0; + double prob = 100.0 * pow(q, n); + ios << leading_chars << std::fixed << std::setprecision(2) << std::setw(5) << prob << "% no up\n"; + while (card->m_level != card_max_level) + { + card = card->upgraded(); + ++m; + prob = prob * (n + 1 - m) / m * p / q; + ios << leading_chars << std::setw(5) << prob << "% -> " << card_description(all_cards, card) << "\n"; + } +} + +Deck* Deck::clone() const +{ + return(new Deck(*this)); +} + +const Card* Deck::next() +{ + if(shuffled_cards.empty()) + { + return(nullptr); + } + else if(strategy == DeckStrategy::random || strategy == DeckStrategy::exact_ordered) + { + const Card* card = shuffled_cards.front(); + shuffled_cards.pop_front(); + return(card); + } + else if(strategy == DeckStrategy::ordered) + { + auto cardIter = std::min_element(shuffled_cards.begin(), shuffled_cards.begin() + std::min(3u, shuffled_cards.size()), [this](const Card* card1, const Card* card2) -> bool + { + auto card1_order = order.find(card1->m_id); + if(!card1_order->second.empty()) + { + auto card2_order = order.find(card2->m_id); + if(!card2_order->second.empty()) + { + return(*card1_order->second.begin() < *card2_order->second.begin()); + } + else + { + return(true); + } + } + else + { + return(false); + } + }); + auto card = *cardIter; + shuffled_cards.erase(cardIter); + auto card_order = order.find(card->m_id); + if(!card_order->second.empty()) + { + card_order->second.erase(card_order->second.begin()); + } + return(card); + } + throw std::runtime_error("Unknown strategy for deck."); +} + +const Card* Deck::upgrade_card(const Card* card, unsigned card_max_level, std::mt19937& re, unsigned &remaining_upgrade_points, unsigned &remaining_upgrade_opportunities) +{ + unsigned oppos = card_max_level - card->m_level; + if (remaining_upgrade_points > 0) + { + for (; oppos > 0; -- oppos) + { + std::mt19937::result_type rnd = re(); + if (rnd % remaining_upgrade_opportunities < remaining_upgrade_points) + { + card = card->upgraded(); + -- remaining_upgrade_points; + } + -- remaining_upgrade_opportunities; + } + } + return card; +} + +void Deck::shuffle(std::mt19937& re) +{ + shuffled_commander = commander; + shuffled_forts.clear(); + boost::insert(shuffled_forts, shuffled_forts.end(), fort_cards); + shuffled_cards.clear(); + boost::insert(shuffled_cards, shuffled_cards.end(), cards); + if(!variable_cards.empty()) + { + if(strategy != DeckStrategy::random) + { + throw std::runtime_error("Support only random strategy for raid/quest deck."); + } + for(auto& card_pool: variable_cards) + { + auto & amount = std::get<0>(card_pool); + auto & replicates = std::get<1>(card_pool); + auto & card_list = std::get<2>(card_pool); + assert(amount <= card_list.size()); + partial_shuffle(card_list.begin(), card_list.begin() + amount, card_list.end(), re); + for (unsigned rep = 0; rep < replicates; ++ rep) + { + shuffled_cards.insert(shuffled_cards.end(), card_list.begin(), card_list.begin() + amount); + } + } + } + if (upgrade_points > 0) + { + unsigned remaining_upgrade_points = upgrade_points; + unsigned remaining_upgrade_opportunities = upgrade_opportunities; + shuffled_commander = upgrade_card(commander, commander_max_level, re, remaining_upgrade_points, remaining_upgrade_opportunities); + for (auto && card: shuffled_forts) + { + card = upgrade_card(card, card->m_top_level_card->m_level, re, remaining_upgrade_points, remaining_upgrade_opportunities); + } + for (auto && card: shuffled_cards) + { + card = upgrade_card(card, card->m_top_level_card->m_level, re, remaining_upgrade_points, remaining_upgrade_opportunities); + } + } + if(strategy == DeckStrategy::ordered) + { + unsigned i = 0; + order.clear(); + for(auto card: cards) + { + order[card->m_id].push_back(i); + ++i; + } + } + if(strategy != DeckStrategy::exact_ordered) + { + auto shufflable_iter = shuffled_cards.begin(); + for(auto hand_card_id: given_hand) + { + auto it = std::find_if(shufflable_iter, shuffled_cards.end(), [hand_card_id](const Card* card) -> bool { return card->m_id == hand_card_id; }); + if(it != shuffled_cards.end()) + { + std::swap(*shufflable_iter, *it); + ++ shufflable_iter; + } + } + std::shuffle(shufflable_iter, shuffled_cards.end(), re); +#if 0 + if(!given_hand.empty()) + { + for(auto card: cards) std::cout << ", " << card->m_name; + std::cout << std::endl; + std::cout << strategy; + for(auto card: shuffled_cards) std::cout << ", " << card->m_name; + std::cout << std::endl; + } +#endif + } +} + +void Deck::place_at_bottom(const Card* card) +{ + shuffled_cards.push_back(card); +} + +void Decks::add_deck(Deck* deck, const std::string& deck_name) +{ + by_name[deck_name] = deck; + by_name[simplify_name(deck_name)] = deck; +} + +Deck* Decks::find_deck_by_name(const std::string& deck_name) +{ + auto it = by_name.find(simplify_name(deck_name)); + return it == by_name.end() ? nullptr : it->second; +} + diff --git a/deck.h b/deck.h new file mode 100644 index 00000000..3ab5f41e --- /dev/null +++ b/deck.h @@ -0,0 +1,152 @@ +#ifndef DECK_H_INCLUDED +#define DECK_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include "tyrant.h" +#include "card.h" + +class Cards; + +//---------------------- $30 Deck: a commander + a sequence of cards ----------- +// Can be shuffled. +// Implementations: random player and raid decks, ordered player decks. +//------------------------------------------------------------------------------ +namespace DeckStrategy +{ +enum DeckStrategy +{ + random, + ordered, + exact_ordered, + num_deckstrategies +}; +} +typedef void (*DeckDecoder)(const char* hash, std::vector& ids); +typedef void (*DeckEncoder)(std::stringstream &ios, const Card* commander, std::vector cards); +void hash_to_ids_wmt_b64(const char* hash, std::vector& ids); +void encode_deck_wmt_b64(std::stringstream &ios, const Card* commander, std::vector cards); +void hash_to_ids_ext_b64(const char* hash, std::vector& ids); +void encode_deck_ext_b64(std::stringstream &ios, const Card* commander, std::vector cards); +void hash_to_ids_ddd_b64(const char* hash, std::vector& ids); +void encode_deck_ddd_b64(std::stringstream &ios, const Card* commander, std::vector cards); +extern DeckDecoder hash_to_ids; +extern DeckEncoder encode_deck; + +//------------------------------------------------------------------------------ +// No support for ordered raid decks +class Deck +{ +public: + const Cards& all_cards; + DeckType::DeckType decktype; + unsigned id; + std::string name; + unsigned upgrade_points; + unsigned upgrade_opportunities; + DeckStrategy::DeckStrategy strategy; + + const Card* commander; + unsigned commander_max_level; + std::vector cards; + std::map card_marks; // : -1 indicating the commander. E.g, used as a mark to be kept in attacking deck when optimizing. + + const Card* shuffled_commander; + std::deque shuffled_forts; + std::deque shuffled_cards; + + // card id -> card order + std::map> order; + std::vector>> variable_cards; // amount, replicates, card pool + unsigned deck_size; + unsigned mission_req; + + std::string deck_string; + std::set vip_cards; + std::vector given_hand; + std::vector fort_cards; + + Deck( + const Cards& all_cards_, + DeckType::DeckType decktype_ = DeckType::deck, + unsigned id_ = 0, + std::string name_ = "", + unsigned upgrade_points_ = 0, + unsigned upgrade_opportunities_ = 0, + DeckStrategy::DeckStrategy strategy_ = DeckStrategy::random) : + all_cards(all_cards_), + decktype(decktype_), + id(id_), + name(name_), + upgrade_points(upgrade_points_), + upgrade_opportunities(upgrade_opportunities_), + strategy(strategy_), + commander(nullptr), + shuffled_commander(nullptr), + deck_size(0), + mission_req(0) + { + } + + ~Deck() {} + + void set( + const Card* commander_, + unsigned commander_max_level_, + const std::vector& cards_, + std::vector>> variable_cards_ = {}, + unsigned mission_req_ = 0) + { + commander = commander_; + commander_max_level = commander_max_level_; + cards = std::vector(std::begin(cards_), std::end(cards_)); + variable_cards = variable_cards_; + deck_size = cards.size(); + for (const auto & pool: variable_cards) + { + deck_size += std::get<0>(pool) * std::get<1>(pool); + } + mission_req = mission_req_; + } + + void set(const std::vector& ids, const std::map &marks); + void set(const std::vector& ids) + { + std::map empty; + set(ids, empty); + } + void set(const std::string& deck_string_); + void resolve(); + void shrink(const unsigned deck_len); + void set_vip_cards(const std::string& deck_string_); + void set_given_hand(const std::string& deck_string_); + void add_forts(const std::string& deck_string_); + + Deck* clone() const; + std::string hash() const; + std::string short_description() const; + std::string medium_description() const; + std::string long_description() const; + void show_upgrades(std::stringstream &ios, const Card* card, unsigned card_max_level, const char * leading_chars) const; + const Card* next(); + const Card* upgrade_card(const Card* card, unsigned card_max_level, std::mt19937& re, unsigned &remaining_upgrade_points, unsigned &remaining_upgrade_opportunities); + void shuffle(std::mt19937& re); + void place_at_bottom(const Card* card); +}; + +typedef std::map DeckList; +class Decks +{ +public: + void add_deck(Deck* deck, const std::string& deck_name); + Deck* find_deck_by_name(const std::string& deck_name); + std::list decks; + std::map, Deck*> by_type_id; + std::map by_name; +}; + +#endif diff --git a/read.cpp b/read.cpp new file mode 100644 index 00000000..607dbd8a --- /dev/null +++ b/read.cpp @@ -0,0 +1,529 @@ +#include "read.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tyrant.h" +#include "card.h" +#include "cards.h" +#include "deck.h" + +template Iterator advance_until(Iterator it, Iterator it_end, Functor f) +{ + while(it != it_end) + { + if(f(*it)) + { + break; + } + ++it; + } + return(it); +} + +// take care that "it" is 1 past current. +template Iterator recede_until(Iterator it, Iterator it_beg, Functor f) +{ + if(it == it_beg) { return(it_beg); } + --it; + do + { + if(f(*it)) + { + return(++it); + } + --it; + } while(it != it_beg); + return(it_beg); +} + +template Iterator read_token(Iterator it, Iterator it_end, Functor f, Token& token) +{ + Iterator token_start = advance_until(it, it_end, [](const char& c){return(c != ' ');}); + Iterator token_end_after_spaces = advance_until(token_start, it_end, f); + if(token_start != token_end_after_spaces) + { + Iterator token_end = recede_until(token_end_after_spaces, token_start, [](const char& c){return(c != ' ');}); + token = boost::lexical_cast(std::string{token_start, token_end}); + } + return(token_end_after_spaces); +} + +DeckList & normalize(DeckList & decklist) +{ + long double factor_sum = 0; + for (const auto & it : decklist) + { + factor_sum += it.second; + } + if (factor_sum > 0) + { + for (auto & it : decklist) + { + it.second /= factor_sum; + } + } + return decklist; +} + +DeckList expand_deck_to_list(std::string deck_name, Decks& decks) +{ + static std::unordered_set expanding_decks; + if (expanding_decks.count(deck_name)) + { + std::cerr << "Warning: circular referred deck: " << deck_name << std::endl; + return DeckList(); + } + auto deck_string = deck_name; + Deck* deck = decks.find_deck_by_name(deck_name); + if (deck != nullptr) + { + deck_string = deck->deck_string; + if (deck_string.find_first_of(";:") != std::string::npos || decks.find_deck_by_name(deck_string) != nullptr) + { + // deck_name refers to a deck list + expanding_decks.insert(deck_name); + auto && decklist = parse_deck_list(deck_string, decks); + expanding_decks.erase(deck_name); + return normalize(decklist); + } + } + + if (deck_string.length() >= 3 && deck_string.front() == '/' && deck_string.back() == '/') + { + // deck_name is, or refers to, a regex + DeckList res; + std::string regex_string(deck_string, 1, deck_string.length() - 2); + boost::regex regex(regex_string); + boost::smatch smatch; + expanding_decks.insert(deck_name); + for (const auto & deck_it: decks.by_name) + { + if (boost::regex_search(deck_it.first, smatch, regex)) + { + auto && decklist = expand_deck_to_list(deck_it.first, decks); + for (const auto & it : decklist) + { + res[it.first] += it.second; + } + } + } + expanding_decks.erase(deck_name); + if (res.size() == 0) + { + std::cerr << "Warning: regular expression matches nothing: /" << regex_string << "/." << std::endl; + } + return normalize(res); + } + else + { + return {{deck_name, 1}}; + } +} + +DeckList parse_deck_list(std::string list_string, Decks& decks) +{ + DeckList res; + boost::tokenizer> list_tokens{list_string, boost::char_delimiters_separator{false, ";", ""}}; + for(const auto & list_token : list_tokens) + { + boost::tokenizer> deck_tokens{list_token, boost::char_delimiters_separator{false, ":", ""}}; + auto deck_token = deck_tokens.begin(); + auto deck_name = *deck_token; + double factor = 1.0; + ++ deck_token; + if (deck_token != deck_tokens.end()) + { + try + { + factor = boost::lexical_cast(*deck_token); + } + catch (const boost::bad_lexical_cast & e) + { + std::cerr << "Warning: Is ':' a typo? Skip deck [" << list_token << "]\n"; + continue; + } + } + auto && decklist = expand_deck_to_list(deck_name, decks); + for (const auto & it : decklist) + { + res[it.first] += it.second * factor; + } + } + return res; +} + +void parse_card_spec(const Cards& all_cards, const std::string& card_spec, unsigned& card_id, unsigned& card_num, char& num_sign, char& mark) +{ +// static std::set recognized_abbr; + auto card_spec_iter = card_spec.begin(); + card_id = 0; + card_num = 1; + num_sign = 0; + mark = 0; + std::string card_name; + card_spec_iter = read_token(card_spec_iter, card_spec.end(), [](char c){return(c=='#' || c=='(' || c=='\r');}, card_name); + if(card_name[0] == '!') + { + mark = card_name[0]; + card_name.erase(0, 1); + } + // If card name is not found, try find card id quoted in '[]' in name, ignoring other characters. + std::string simple_name{simplify_name(card_name)}; + const auto && abbr_it = all_cards.player_cards_abbr.find(simple_name); + if(abbr_it != all_cards.player_cards_abbr.end()) + { +// if(recognized_abbr.count(card_name) == 0) +// { +// std::cout << "Recognize abbreviation " << card_name << ": " << abbr_it->second << std::endl; +// recognized_abbr.insert(card_name); +// } + simple_name = simplify_name(abbr_it->second); + } + auto card_it = all_cards.cards_by_name.find(simple_name); + auto card_id_iter = advance_until(simple_name.begin(), simple_name.end(), [](char c){return(c=='[');}); + if (card_it != all_cards.cards_by_name.end()) + { + card_id = card_it->second->m_id; + if (all_cards.ambiguous_names.count(simple_name)) + { + std::cerr << "Warning: There are multiple cards named " << card_name << " in cards.xml. [" << card_id << "] is used.\n"; + } + } + else if(card_id_iter != simple_name.end()) + { + ++ card_id_iter; + card_id_iter = read_token(card_id_iter, simple_name.end(), [](char c){return(c==']');}, card_id); + } + if(card_spec_iter != card_spec.end() && (*card_spec_iter == '#' || *card_spec_iter == '(')) + { + ++card_spec_iter; + if(card_spec_iter != card_spec.end()) + { + if(strchr("+-$", *card_spec_iter)) + { + num_sign = *card_spec_iter; + ++card_spec_iter; + } + } + card_spec_iter = read_token(card_spec_iter, card_spec.end(), [](char c){return(c < '0' || c > '9');}, card_num); + } + if(card_id == 0) + { + throw std::runtime_error("Unknown card: " + card_name); + } +} + +const std::pair, std::map> string_to_ids(const Cards& all_cards, const std::string& deck_string, const std::string & description) +{ + std::vector card_ids; + std::map card_marks; + std::vector error_list; + boost::tokenizer> deck_tokens{deck_string, boost::char_delimiters_separator{false, ":,", ""}}; + auto token_iter = deck_tokens.begin(); + signed p = -1; + for(; token_iter != deck_tokens.end(); ++token_iter) + { + std::string card_spec(*token_iter); + unsigned card_id{0}; + unsigned card_num{1}; + char num_sign{0}; + char mark{0}; + try + { + parse_card_spec(all_cards, card_spec, card_id, card_num, num_sign, mark); + assert(num_sign == 0); + for(unsigned i(0); i < card_num; ++i) + { + card_ids.push_back(card_id); + if(mark) { card_marks[p] = mark; } + ++ p; + } + } + catch(std::exception& e) + { + error_list.push_back(e.what()); + continue; + } + } + if (! card_ids.empty()) + { + if (! error_list.empty()) + { + std::cerr << "Warning: Ignore some cards while resolving " << description << ": "; + for (auto error: error_list) + { + std::cerr << '[' << error << ']'; + } + std::cerr << std::endl; + } + return {card_ids, card_marks}; + } + try + { + hash_to_ids(deck_string.c_str(), card_ids); + for (auto & card_id: card_ids) + { + try + { + all_cards.by_id(card_id); + } + catch(std::exception& e) + { + throw std::runtime_error(std::string("Deck not found. Error to treat as hash: ") + e.what()); + } + } + } + catch(std::exception& e) + { + std::cerr << "Error: Failed to resolve " << description << ": " << e.what() << std::endl; + throw; + } + return {card_ids, card_marks}; +} + +unsigned read_card_abbrs(Cards& all_cards, const std::string& filename) +{ + if(!boost::filesystem::exists(filename)) + { + return(0); + } + std::ifstream abbr_file(filename); + if(!abbr_file.is_open()) + { + std::cerr << "Error: Card abbreviation file " << filename << " could not be opened\n"; + return(2); + } + unsigned num_line(0); + abbr_file.exceptions(std::ifstream::badbit); + try + { + while(abbr_file && !abbr_file.eof()) + { + std::string abbr_string; + getline(abbr_file, abbr_string); + ++num_line; + if(abbr_string.size() == 0 || strncmp(abbr_string.c_str(), "//", 2) == 0) + { + continue; + } + std::string abbr_name; + auto abbr_string_iter = read_token(abbr_string.begin(), abbr_string.end(), [](char c){return(c == ':');}, abbr_name); + if(abbr_string_iter == abbr_string.end() || abbr_name.empty()) + { + std::cerr << "Error in card abbreviation file " << filename << " at line " << num_line << ", could not read the name.\n"; + continue; + } + abbr_string_iter = advance_until(abbr_string_iter + 1, abbr_string.end(), [](const char& c){return(c != ' ');}); + if(all_cards.cards_by_name.find(abbr_name) != all_cards.cards_by_name.end()) + { + std::cerr << "Warning in card abbreviation file " << filename << " at line " << num_line << ": ignored because the name has been used by an existing card." << std::endl; + } + else + { + all_cards.player_cards_abbr[simplify_name(abbr_name)] = std::string{abbr_string_iter, abbr_string.end()}; + } + } + } + catch (std::exception& e) + { + std::cerr << "Exception while parsing the card abbreviation file " << filename; + if(num_line > 0) + { + std::cerr << " at line " << num_line; + } + std::cerr << ": " << e.what() << ".\n"; + return(3); + } + return(0); +} + + +// Error codes: +// 2 -> file not readable +// 3 -> error while parsing file +unsigned load_custom_decks(Decks& decks, Cards& all_cards, const std::string & filename) +{ + if (!boost::filesystem::exists(filename)) + { + return 0; + } + std::ifstream decks_file(filename); + if (!decks_file.is_open()) + { + std::cerr << "Error: Custom deck file " << filename << " could not be opened\n"; + return 2; + } + unsigned num_line(0); + decks_file.exceptions(std::ifstream::badbit); + try + { + while(decks_file && !decks_file.eof()) + { + std::string deck_string; + getline(decks_file, deck_string); + ++num_line; + if(deck_string.size() == 0 || strncmp(deck_string.c_str(), "//", 2) == 0) + { + continue; + } + std::string deck_name; + auto deck_string_iter = read_token(deck_string.begin(), deck_string.end(), [](char c){return(strchr(":,", c));}, deck_name); + if(deck_string_iter == deck_string.end() || deck_name.empty()) + { + std::cerr << "Error in custom deck file " << filename << " at line " << num_line << ", could not read the deck name.\n"; + continue; + } + deck_string_iter = advance_until(deck_string_iter + 1, deck_string.end(), [](const char& c){return(c != ' ');}); + Deck* deck = decks.find_deck_by_name(deck_name); + if (deck != nullptr) + { + std::cerr << "Warning in custom deck file " << filename << " at line " << num_line << ", name conflicts, overrides " << deck->short_description() << std::endl; + } + decks.decks.push_back(Deck{all_cards, DeckType::custom_deck, num_line, deck_name}); + deck = &decks.decks.back(); + deck->set(std::string{deck_string_iter, deck_string.end()}); + decks.add_deck(deck, deck_name); + std::stringstream alt_name; + alt_name << decktype_names[deck->decktype] << " #" << deck->id; + decks.add_deck(deck, alt_name.str()); + } + } + catch (std::exception& e) + { + std::cerr << "Exception while parsing the custom deck file " << filename; + if(num_line > 0) + { + std::cerr << " at line " << num_line; + } + std::cerr << ": " << e.what() << ".\n"; + return 3; + } + return(0); +} + +void add_owned_card(Cards& all_cards, std::map& owned_cards, std::string& card_spec) +{ + unsigned card_id{0}; + unsigned card_num{1}; + char num_sign{0}; + char mark{0}; + parse_card_spec(all_cards, card_spec, card_id, card_num, num_sign, mark); + all_cards.by_id(card_id); // check that the id is valid + assert(mark == 0); + if(num_sign == 0) + { + owned_cards[card_id] = card_num; + } + else if(num_sign == '+') + { + owned_cards[card_id] += card_num; + } + else if(num_sign == '-') + { + owned_cards[card_id] = owned_cards[card_id] > card_num ? owned_cards[card_id] - card_num : 0; + } +} + +void read_owned_cards(Cards& all_cards, std::map& owned_cards, const std::string & filename) +{ + std::ifstream owned_file{filename}; + if(!owned_file.good()) + { + // try parse the string as a cards instead of as a filename + try + { + std::string card_list(filename); + boost::tokenizer> card_tokens{card_list, boost::char_delimiters_separator{false, ",", ""}}; + auto token_iter = card_tokens.begin(); + for (; token_iter != card_tokens.end(); ++token_iter) + { + std::string card_spec(*token_iter); + add_owned_card(all_cards, owned_cards, card_spec); + } + } + catch (std::exception& e) + { + std::cerr << "Error: Failed to parse owned cards: '" << filename << "' is neither a file nor a valid set of cards (" << e.what() << ")" << std::endl; + exit(0); + } + return; + } + unsigned num_line(0); + while(owned_file && !owned_file.eof()) + { + std::string card_spec; + getline(owned_file, card_spec); + ++num_line; + if(card_spec.size() == 0 || strncmp(card_spec.c_str(), "//", 2) == 0) + { + continue; + } + try + { + add_owned_card(all_cards, owned_cards, card_spec); + } + catch(std::exception& e) + { + std::cerr << "Error in owned cards file " << filename << " at line " << num_line << " while parsing card '" << card_spec << "': " << e.what() << "\n"; + } + } +} + +unsigned read_bge_aliases(std::unordered_map & bge_aliases, const std::string& filename) +{ + if(!boost::filesystem::exists(filename)) + { + return(0); + } + std::ifstream bgefile(filename); + if(!bgefile.is_open()) + { + std::cerr << "Error: BGE file " << filename << " could not be opened\n"; + return(2); + } + unsigned num_line(0); + bgefile.exceptions(std::ifstream::badbit); + try + { + while(bgefile && !bgefile.eof()) + { + std::string bge_string; + getline(bgefile, bge_string); + ++num_line; + if(bge_string.size() == 0 || strncmp(bge_string.c_str(), "//", 2) == 0) + { + continue; + } + std::string bge_name; + auto bge_string_iter = read_token(bge_string.begin(), bge_string.end(), [](char c){return(c == ':');}, bge_name); + if(bge_string_iter == bge_string.end() || bge_name.empty()) + { + std::cerr << "Error in BGE file " << filename << " at line " << num_line << ", could not read the name.\n"; + continue; + } + bge_string_iter = advance_until(bge_string_iter + 1, bge_string.end(), [](const char& c){return(c != ' ');}); + bge_aliases[simplify_name(bge_name)] = std::string{bge_string_iter, bge_string.end()}; + } + } + catch (std::exception& e) + { + std::cerr << "Exception while parsing the BGE file " << filename; + if(num_line > 0) + { + std::cerr << " at line " << num_line; + } + std::cerr << ": " << e.what() << ".\n"; + return(3); + } + return(0); +} + diff --git a/read.h b/read.h new file mode 100644 index 00000000..edc642c0 --- /dev/null +++ b/read.h @@ -0,0 +1,21 @@ +#ifndef READ_H_INCLUDED +#define READ_H_INCLUDED + +#include +#include + +#include "deck.h" + +class Cards; +class Decks; +class Deck; + +DeckList parse_deck_list(std::string list_string, Decks& decks); +void parse_card_spec(const Cards& cards, const std::string& card_spec, unsigned& card_id, unsigned& card_num, char& num_sign, char& mark); +const std::pair, std::map> string_to_ids(const Cards& all_cards, const std::string& deck_string, const std::string & description); +unsigned load_custom_decks(Decks& decks, Cards& cards, const std::string & filename); +void read_owned_cards(Cards& cards, std::map& owned_cards, const std::string & filename); +unsigned read_card_abbrs(Cards& cards, const std::string& filename); +unsigned read_bge_aliases(std::unordered_map & bge_aliases, const std::string & filename); + +#endif diff --git a/sim.cpp b/sim.cpp new file mode 100644 index 00000000..17ecdb30 --- /dev/null +++ b/sim.cpp @@ -0,0 +1,1950 @@ +#include "sim.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "tyrant.h" +#include "card.h" +#include "cards.h" +#include "deck.h" + +//------------------------------------------------------------------------------ +inline std::string status_description(const CardStatus* status) +{ + return status->description(); +} +//------------------------------------------------------------------------------ +template +inline unsigned Field::make_selection_array(CardsIter first, CardsIter last, Functor f) +{ + this->selection_array.clear(); + for(auto c = first; c != last; ++c) + { + if (f(*c)) + { + this->selection_array.push_back(*c); + } + } + return(this->selection_array.size()); +} +inline const std::vector Field::adjacent_assaults(const CardStatus * status) +{ + std::vector res; + auto & assaults = this->players[status->m_player]->assaults; + if (status->m_index > 0) + { + auto left_status = &assaults[status->m_index - 1]; + if (left_status->m_hp > 0) + { + res.push_back(left_status); + } + } + if (status->m_index + 1 < assaults.size()) + { + auto right_status = &assaults[status->m_index + 1]; + if (right_status->m_hp > 0) + { + res.push_back(right_status); + } + } + return res; +} +inline void Field::print_selection_array() +{ +#ifndef NDEBUG + for(auto c: this->selection_array) + { + _DEBUG_MSG(2, "+ %s\n", status_description(c).c_str()); + } +#endif +} +//------------------------------------------------------------------------------ +inline unsigned CardStatus::skill_base_value(Skill skill_id) const +{ + return m_card->m_skill_value[skill_id + m_primary_skill_offset[skill_id]]; +} +//------------------------------------------------------------------------------ +inline unsigned CardStatus::skill(Skill skill_id) const +{ + return skill_base_value(skill_id) + enhanced(skill_id); +} +//------------------------------------------------------------------------------ +inline bool CardStatus::has_skill(Skill skill_id) const +{ + return skill_base_value(skill_id); +} +//------------------------------------------------------------------------------ +inline unsigned CardStatus::enhanced(Skill skill_id) const +{ + return m_enhanced_value[skill_id + m_primary_skill_offset[skill_id]]; +} +//------------------------------------------------------------------------------ +inline unsigned CardStatus::protected_value() const +{ + return m_protected; +} +//------------------------------------------------------------------------------ +inline void CardStatus::set(const Card* card) +{ + this->set(*card); +} +//------------------------------------------------------------------------------ +inline void CardStatus::set(const Card& card) +{ + m_card = &card; + m_index = 0; + m_player = 0; + m_delay = card.m_delay; + m_faction = card.m_faction; + m_attack = card.m_attack; + m_hp = m_max_hp = card.m_health; + m_step = CardStep::none; + + m_corroded_rate = 0; + m_corroded_weakened = 0; + m_enfeebled = 0; + m_evaded = 0; + m_inhibited = 0; + m_jammed = false; + m_overloaded = false; + m_paybacked = 0; + m_poisoned = 0; + m_protected = 0; + m_rallied = 0; + m_rush_attempted = false; + m_sundered = false; + m_weakened = 0; + + std::memset(m_primary_skill_offset, 0, sizeof m_primary_skill_offset); + std::memset(m_evolved_skill_offset, 0, sizeof m_evolved_skill_offset); + std::memset(m_enhanced_value, 0, sizeof m_enhanced_value); + std::memset(m_skill_cd, 0, sizeof m_skill_cd); +} +//------------------------------------------------------------------------------ +inline unsigned attack_power(const CardStatus* att) +{ + return(safe_minus(att->m_attack + att->m_rallied, att->m_weakened + att->m_corroded_weakened)); +} +//------------------------------------------------------------------------------ +std::string skill_description(const Cards& cards, const SkillSpec& s) +{ + return skill_names[s.id] + + (s.all ? " all" : s.n == 0 ? "" : std::string(" ") + to_string(s.n)) + + (s.y == allfactions ? "" : std::string(" ") + faction_names[s.y]) + + (s.s == no_skill ? "" : std::string(" ") + skill_names[s.s]) + + (s.s2 == no_skill ? "" : std::string(" ") + skill_names[s.s2]) + + (s.x == 0 ? "" : std::string(" ") + to_string(s.x)) + + (s.c == 0 ? "" : std::string(" every ") + to_string(s.c)); +} +std::string skill_short_description(const SkillSpec& s) +{ + // NOTE: not support summon + return skill_names[s.id] + + (s.s == no_skill ? "" : std::string(" ") + skill_names[s.s]) + + (s.s2 == no_skill ? "" : std::string(" ") + skill_names[s.s2]) + + (s.x == 0 ? "" : std::string(" ") + to_string(s.x)); +} +//------------------------------------------------------------------------------ +std::string card_description(const Cards& cards, const Card* c) +{ + std::string desc; + desc = c->m_name; + switch(c->m_type) + { + case CardType::assault: + desc += ": " + to_string(c->m_attack) + "/" + to_string(c->m_health) + "/" + to_string(c->m_delay); + break; + case CardType::structure: + desc += ": " + to_string(c->m_health) + "/" + to_string(c->m_delay); + break; + case CardType::commander: + desc += ": hp:" + to_string(c->m_health); + break; + case CardType::num_cardtypes: + assert(false); + break; + } + if(c->m_rarity >= 4) { desc += " " + rarity_names[c->m_rarity]; } + if(c->m_faction != allfactions) { desc += " " + faction_names[c->m_faction]; } + for(auto& skill: c->m_skills) { desc += ", " + skill_description(cards, skill); } + return(desc); +} +//------------------------------------------------------------------------------ +std::string CardStatus::description() const +{ + std::string desc = "P" + to_string(m_player) + " "; + switch(m_card->m_type) + { + case CardType::commander: desc += "Commander "; break; + case CardType::assault: desc += "Assault " + to_string(m_index) + " "; break; + case CardType::structure: desc += "Structure " + to_string(m_index) + " "; break; + case CardType::num_cardtypes: assert(false); break; + } + desc += "[" + m_card->m_name; + switch(m_card->m_type) + { + case CardType::assault: + desc += " att:" + to_string(m_attack); + { + std::string att_desc; + if(m_rallied > 0) { att_desc += "+" + to_string(m_rallied) + "(rallied)"; } + if(m_weakened > 0) { att_desc += "-" + to_string(m_weakened) + "(weakened)"; } + if(m_corroded_weakened > 0) { att_desc += "-" + to_string(m_corroded_weakened) + "(corroded)"; } + if(!att_desc.empty()) { desc += att_desc + "=" + to_string(attack_power(this)); } + } + case CardType::structure: + case CardType::commander: + desc += " hp:" + to_string(m_hp); + break; + case CardType::num_cardtypes: + assert(false); + break; + } + if(m_delay > 0) { + desc += " cd:" + to_string(m_delay); + } + // Status w/o value + if(m_jammed) { desc += ", jammed"; } + if(m_overloaded) { desc += ", overloaded"; } + if(m_sundered) { desc += ", sundered"; } + // Status w/ value + if(m_corroded_rate > 0) { desc += ", corroded " + to_string(m_corroded_rate); } + if(m_enfeebled > 0) { desc += ", enfeebled " + to_string(m_enfeebled); } + if(m_inhibited > 0) { desc += ", inhibited " + to_string(m_inhibited); } + if(m_poisoned > 0) { desc += ", poisoned " + to_string(m_poisoned); } + if(m_protected > 0) { desc += ", protected " + to_string(m_protected); } +// if(m_step != CardStep::none) { desc += ", Step " + to_string(static_cast(m_step)); } + for (const auto & ss: m_card->m_skills) + { + std::string skill_desc; + if (m_evolved_skill_offset[ss.id] != 0) { skill_desc += "->" + skill_names[ss.id + m_evolved_skill_offset[ss.id]]; } + if (m_enhanced_value[ss.id] != 0) { skill_desc += " +" + to_string(m_enhanced_value[ss.id]); } + if (!skill_desc.empty()) { desc += ", " + skill_names[ss.id] + skill_desc; } + } + desc += "]"; + return(desc); +} +//------------------------------------------------------------------------------ +void Hand::reset(std::mt19937& re) +{ + assaults.reset(); + structures.reset(); + deck->shuffle(re); + commander.set(deck->shuffled_commander); +} +//---------------------- $40 Game rules implementation ------------------------- +// Everything about how a battle plays out, except the following: +// the implementation of the attack by an assault card is in the next section; +// the implementation of the active skills is in the section after that. +unsigned turn_limit{50}; +//------------------------------------------------------------------------------ +inline unsigned opponent(unsigned player) +{ + return((player + 1) % 2); +} +//------------------------------------------------------------------------------ +SkillSpec apply_evolve(const SkillSpec& s, signed offset) +{ + SkillSpec evolved_s = s; + evolved_s.id = static_cast(evolved_s.id + offset); + return(evolved_s); +} +//------------------------------------------------------------------------------ +SkillSpec apply_enhance(const SkillSpec& s, unsigned enhanced_value) +{ + SkillSpec enahnced_s = s; + enahnced_s.x += enhanced_value; + return(enahnced_s); +} +//------------------------------------------------------------------------------ +void prepend_on_death(Field* fd) +{ + if (fd->killed_units.empty()) + { + return; + } + std::vector> od_skills; + auto & assaults = fd->players[fd->killed_units[0]->m_player]->assaults; + unsigned stacked_poison_value = 0; + unsigned last_index = 99; + CardStatus * left_virulence_victim = nullptr; + for (auto status: fd->killed_units) + { + if (status->m_card->m_type == CardType::assault) + { + // Avenge + for (auto && adj_status: fd->adjacent_assaults(status)) + { + unsigned avenge_value = adj_status->skill(avenge); + if (avenge_value > 0) + { + _DEBUG_MSG(1, "%s activates Avenge %u\n", status_description(adj_status).c_str(), avenge_value); + if (! adj_status->m_sundered) + { adj_status->m_attack += avenge_value; } + adj_status->m_max_hp += avenge_value; + adj_status->m_hp += avenge_value; + } + } + // Virulence + if (fd->bg_effects.count(virulence)) + { + if (status->m_index != last_index + 1) + { + stacked_poison_value = 0; + left_virulence_victim = nullptr; + if (status->m_index > 0) + { + auto left_status = &assaults[status->m_index - 1]; + if (left_status->m_hp > 0) + { + left_virulence_victim = left_status; + } + } + } + if (status->m_poisoned > 0) + { + if (left_virulence_victim != nullptr) + { + _DEBUG_MSG(1, "Virulence: %s spreads left poison +%u to %s\n", status_description(status).c_str(), status->m_poisoned, status_description(left_virulence_victim).c_str()); + left_virulence_victim->m_poisoned += status->m_poisoned; + } + stacked_poison_value += status->m_poisoned; + _DEBUG_MSG(1, "Virulence: %s spreads right poison +%u = %u\n", status_description(status).c_str(), status->m_poisoned, stacked_poison_value); + } + if (status->m_index + 1 < assaults.size()) + { + auto right_status = &assaults[status->m_index + 1]; + if (right_status->m_hp > 0) + { + _DEBUG_MSG(1, "Virulence: spreads stacked poison +%u to %s\n", stacked_poison_value, status_description(right_status).c_str()); + right_status->m_poisoned += stacked_poison_value; + } + } + last_index = status->m_index; + } + } + // Revenge + if (fd->bg_effects.count(revenge)) + { + SkillSpec ss_heal{heal, fd->bg_effects.at(revenge), allfactions, 0, 0, no_skill, no_skill, true,}; + SkillSpec ss_rally{rally, fd->bg_effects.at(revenge), allfactions, 0, 0, no_skill, no_skill, true,}; + CardStatus * commander = &fd->players[status->m_player]->commander; + _DEBUG_MSG(2, "Revenge: Preparing skill %s and %s\n", skill_description(fd->cards, ss_heal).c_str(), skill_description(fd->cards, ss_rally).c_str()); + od_skills.emplace_back(commander, ss_heal); + od_skills.emplace_back(commander, ss_rally); + } + } + fd->skill_queue.insert(fd->skill_queue.begin(), od_skills.begin(), od_skills.end()); + fd->killed_units.clear(); +} +//------------------------------------------------------------------------------ +void(*skill_table[num_skills])(Field*, CardStatus* src, const SkillSpec&); +void resolve_skill(Field* fd) +{ + while(!fd->skill_queue.empty()) + { + auto skill_instance(fd->skill_queue.front()); + auto& status(std::get<0>(skill_instance)); + const auto& ss(std::get<1>(skill_instance)); + fd->skill_queue.pop_front(); + if (status->m_jammed) + { + _DEBUG_MSG(2, "%s failed to %s because it is Jammed.", status_description(status).c_str(), skill_description(fd->cards, ss).c_str()); + continue; + } + signed evolved_offset = status->m_evolved_skill_offset[ss.id]; + auto& evolved_s = status->m_evolved_skill_offset[ss.id] != 0 ? apply_evolve(ss, evolved_offset) : ss; + unsigned enhanced_value = status->enhanced(evolved_s.id); + auto& enhanced_s = enhanced_value > 0 ? apply_enhance(evolved_s, enhanced_value) : evolved_s; + auto& modified_s = enhanced_s; + skill_table[modified_s.id](fd, status, modified_s); + } +} +//------------------------------------------------------------------------------ +inline bool has_attacked(CardStatus* c) { return(c->m_step == CardStep::attacked); } +inline bool can_act(CardStatus* c) { return(c->m_hp > 0 && !c->m_jammed); } +inline bool is_active(CardStatus* c) { return(can_act(c) && c->m_delay == 0); } +inline bool is_active_next_turn(CardStatus* c) { return(can_act(c) && c->m_delay <= 1); } +// Can be healed / repaired +inline bool can_be_healed(CardStatus* c) { return(c->m_hp > 0 && c->m_hp < c->m_max_hp); } +//------------------------------------------------------------------------------ +bool attack_phase(Field* fd); +template +bool check_and_perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s, bool is_evadable, bool & has_counted_quest); +bool check_and_perform_valor(Field* fd, CardStatus* src); +template +void evaluate_skills(Field* fd, CardStatus* status, const std::vector& skills, bool* attacked=nullptr) +{ + assert(status); + unsigned num_actions(1); + for (unsigned action_index(0); action_index < num_actions; ++ action_index) + { + assert(fd->skill_queue.size() == 0); + for (auto & ss: skills) + { + // check if activation skill, assuming activation skills can be evolved from only activation skills + if (skill_table[ss.id] == nullptr) + { + continue; + } + if (status->m_skill_cd[ss.id] > 0) + { + continue; + } + _DEBUG_MSG(2, "Evaluating %s skill %s\n", status_description(status).c_str(), skill_description(fd->cards, ss).c_str()); + fd->skill_queue.emplace_back(status, ss); + resolve_skill(fd); + if(__builtin_expect(fd->end, false)) { break; } + } + if (type == CardType::assault) + { + // Attack + if (can_act(status)) + { + if (attack_phase(fd) && !*attacked) + { + *attacked = true; + if (__builtin_expect(fd->end, false)) { break; } + } + } + else + { + _DEBUG_MSG(2, "%s cannot take attack.\n", status_description(status).c_str()); + } + } + // Flurry + if (can_act(status) && fd->tip->commander.m_hp > 0 && status->has_skill(flurry) && status->m_skill_cd[flurry] == 0) + { + if (status->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, flurry); + } + _DEBUG_MSG(1, "%s activates Flurry\n", status_description(status).c_str()); + num_actions = 2; + for (const auto & ss : skills) + { + Skill evolved_skill_id = static_cast(ss.id + status->m_evolved_skill_offset[ss.id]); + if (evolved_skill_id == flurry) + { + status->m_skill_cd[ss.id] = ss.c; + } + } + } + } +} + +struct PlayCard +{ + const Card* card; + Field* fd; + CardStatus* status; + Storage* storage; + + PlayCard(const Card* card_, Field* fd_) : + card{card_}, + fd{fd_}, + status{nullptr}, + storage{nullptr} + {} + + template + bool op() + { + setStorage(); + placeCard(); + return(true); + } + + template + void setStorage() + { + } + + template + void placeCard() + { + status = &storage->add_back(); + status->set(card); + status->m_index = storage->size() - 1; + status->m_player = fd->tapi; + if (status->m_player == 0) + { + if (status->m_card->m_type == CardType::assault) + { + fd->inc_counter(QuestType::faction_assault_card_use, card->m_faction); + } + fd->inc_counter(QuestType::type_card_use, type); + } + _DEBUG_MSG(1, "%s plays %s %u [%s]\n", status_description(&fd->tap->commander).c_str(), cardtype_names[type].c_str(), static_cast(storage->size() - 1), card_description(fd->cards, card).c_str()); + if (status->m_delay == 0) + { + check_and_perform_valor(fd, status); + } + } +}; +// assault +template <> +void PlayCard::setStorage() +{ + storage = &fd->tap->assaults; +} +// structure +template <> +void PlayCard::setStorage() +{ + storage = &fd->tap->structures; +} + +// Check if a skill actually proc'ed. +template +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ return(true); } + +template<> +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ + return(c->m_player != ref->m_player); +} + +template<> +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ + return(can_be_healed(c)); +} + +template<> +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ + return(is_active(c)); +} + +template<> +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ + return(ref->m_card->m_type == CardType::assault && ref->m_hp > 0); +} + +template<> +inline bool skill_check(Field* fd, CardStatus* c, CardStatus* ref) +{ + return(can_be_healed(c)); +} + +void remove_hp(Field* fd, CardStatus* status, unsigned dmg) +{ + assert(status->m_hp > 0); + _DEBUG_MSG(2, "%s takes %u damage\n", status_description(status).c_str(), dmg); + status->m_hp = safe_minus(status->m_hp, dmg); + if(status->m_hp == 0) + { + if (status->m_player == 1) + { + if (status->m_card->m_type == CardType::assault) + { + fd->inc_counter(QuestType::faction_assault_card_kill, status->m_card->m_faction); + } + fd->inc_counter(QuestType::type_card_kill, status->m_card->m_type); + } + _DEBUG_MSG(1, "%s dies\n", status_description(status).c_str()); + if(status->m_card->m_type != CardType::commander) + { + fd->killed_units.push_back(status); + } + if (status->m_player == 0 && fd->players[0]->deck->vip_cards.count(status->m_card->m_id)) + { + fd->players[0]->commander.m_hp = 0; + fd->end = true; + } + } +} + +inline bool is_it_dead(CardStatus& c) +{ + if(c.m_hp == 0) // yes it is + { + _DEBUG_MSG(1, "Dead and removed: %s\n", status_description(&c).c_str()); + return(true); + } + else { return(false); } // nope still kickin' +} +inline void remove_dead(Storage& storage) +{ + storage.remove(is_it_dead); +} +inline void add_hp(Field* fd, CardStatus* target, unsigned v) +{ + target->m_hp = std::min(target->m_hp + v, target->m_max_hp); +} +void cooldown_skills(CardStatus * status) +{ + for (const auto & ss : status->m_card->m_skills) + { + if (status->m_skill_cd[ss.id] > 0) + { + _DEBUG_MSG(2, "%s reduces timer (%u) of skill %s\n", status_description(status).c_str(), status->m_skill_cd[ss.id], skill_names[ss.id].c_str()); + -- status->m_skill_cd[ss.id]; + } + } +} +void turn_start_phase(Field* fd) +{ + // Active player's commander card: + cooldown_skills(&fd->tap->commander); + // Active player's assault cards: + // update index + // reduce delay; reduce skill cooldown + { + auto& assaults(fd->tap->assaults); + for(unsigned index(0), end(assaults.size()); + index < end; + ++index) + { + CardStatus * status = &assaults[index]; + status->m_index = index; + if(status->m_delay > 0) + { + _DEBUG_MSG(1, "%s reduces its timer\n", status_description(status).c_str()); + -- status->m_delay; + if (status->m_delay == 0) + { + check_and_perform_valor(fd, status); + } + } + else + { + cooldown_skills(status); + } + } + } + // Active player's structure cards: + // update index + // reduce delay; reduce skill cooldown + { + auto& structures(fd->tap->structures); + for(unsigned index(0), end(structures.size()); + index < end; + ++index) + { + CardStatus * status = &structures[index]; + status->m_index = index; + if(status->m_delay > 0) + { + _DEBUG_MSG(1, "%s reduces its timer\n", status_description(status).c_str()); + --status->m_delay; + } + else + { + cooldown_skills(status); + } + } + } + // Defending player's assault cards: + // update index + { + auto& assaults(fd->tip->assaults); + for(unsigned index(0), end(assaults.size()); + index < end; + ++index) + { + CardStatus& status(assaults[index]); + status.m_index = index; + } + } + // Defending player's structure cards: + // update index + { + auto& structures(fd->tip->structures); + for(unsigned index(0), end(structures.size()); + index < end; + ++index) + { + CardStatus& status(structures[index]); + status.m_index = index; + } + } +} +void turn_end_phase(Field* fd) +{ + // Inactive player's assault cards: + { + auto& assaults(fd->tip->assaults); + for(unsigned index(0), end(assaults.size()); + index < end; + ++index) + { + CardStatus& status(assaults[index]); + if (status.m_hp <= 0) + { + continue; + } + status.m_enfeebled = 0; + status.m_protected = 0; + std::memset(status.m_primary_skill_offset, 0, sizeof status.m_primary_skill_offset); + std::memset(status.m_evolved_skill_offset, 0, sizeof status.m_evolved_skill_offset); + std::memset(status.m_enhanced_value, 0, sizeof status.m_enhanced_value); + status.m_evaded = 0; // so far only useful in Inactive turn + status.m_paybacked = 0; // ditto + } + } + // Inactive player's structure cards: + { + auto& structures(fd->tip->structures); + for(unsigned index(0), end(structures.size()); + index < end; + ++index) + { + CardStatus& status(structures[index]); + if (status.m_hp <= 0) + { + continue; + } + status.m_evaded = 0; // so far only useful in Inactive turn + } + } + + // Active player's assault cards: + { + auto& assaults(fd->tap->assaults); + for(unsigned index(0), end(assaults.size()); + index < end; + ++index) + { + CardStatus& status(assaults[index]); + if (status.m_hp <= 0) + { + continue; + } + unsigned refresh_value = status.skill(refresh); + if (refresh_value > 0 && skill_check(fd, &status, nullptr)) + { + _DEBUG_MSG(1, "%s refreshes %u health\n", status_description(&status).c_str(), refresh_value); + add_hp(fd, &status, refresh_value); + } + if (status.m_poisoned > 0) + { + unsigned poison_dmg = safe_minus(status.m_poisoned + status.m_enfeebled, status.protected_value()); + if (poison_dmg > 0) + { + if (status.m_player == 1) + { + fd->inc_counter(QuestType::skill_damage, poison, 0, poison_dmg); + } + _DEBUG_MSG(1, "%s takes poison damage %u\n", status_description(&status).c_str(), poison_dmg); + remove_hp(fd, &status, poison_dmg); // simultaneous + } + } + // end of the opponent's next turn for enemy units + status.m_jammed = false; + status.m_rallied = 0; + status.m_sundered = false; + status.m_weakened = 0; + status.m_inhibited = 0; + status.m_overloaded = false; + status.m_step = CardStep::none; + } + } + // Active player's structure cards: + // nothing so far + + prepend_on_death(fd); // poison + resolve_skill(fd); + remove_dead(fd->tap->assaults); + remove_dead(fd->tap->structures); + remove_dead(fd->tip->assaults); + remove_dead(fd->tip->structures); +} +//---------------------- $50 attack by assault card implementation ------------- +// Counter damage dealt to the attacker (att) by defender (def) +// pre-condition: only valid if m_card->m_counter > 0 +inline unsigned counter_damage(Field* fd, CardStatus* att, CardStatus* def) +{ + assert(att->m_card->m_type == CardType::assault); + return(safe_minus(def->skill(counter) + att->m_enfeebled, att->protected_value())); +} +inline CardStatus* select_first_enemy_wall(Field* fd) +{ + for(unsigned i(0); i < fd->tip->structures.size(); ++i) + { + CardStatus& c(fd->tip->structures[i]); + if(c.has_skill(wall) && c.m_hp > 0 && skill_check(fd, &c, nullptr)) + { + return(&c); + } + } + return(nullptr); +} + +inline bool alive_assault(Storage& assaults, unsigned index) +{ + return(assaults.size() > index && assaults[index].m_hp > 0); +} + +void remove_commander_hp(Field* fd, CardStatus& status, unsigned dmg, bool count_points) +{ + //assert(status.m_hp > 0); + assert(status.m_card->m_type == CardType::commander); + _DEBUG_MSG(2, "%s takes %u damage\n", status_description(&status).c_str(), dmg); + status.m_hp = safe_minus(status.m_hp, dmg); + if(status.m_hp == 0) + { + _DEBUG_MSG(1, "%s dies\n", status_description(&status).c_str()); + fd->end = true; + } +} +//------------------------------------------------------------------------------ +// implementation of one attack by an assault card, against either an enemy +// assault card, the first enemy wall, or the enemy commander. +struct PerformAttack +{ + Field* fd; + CardStatus* att_status; + CardStatus* def_status; + unsigned att_dmg; + + PerformAttack(Field* fd_, CardStatus* att_status_, CardStatus* def_status_) : + fd(fd_), att_status(att_status_), def_status(def_status_), att_dmg(0) + {} + + template + unsigned op() + { + unsigned pre_modifier_dmg = attack_power(att_status); + + // Evaluation order: + // modify damage + // deal damage + // assaults only: (poison) + // counter, berserk + // assaults only: (leech if still alive) + + modify_attack_damage(pre_modifier_dmg); + if (att_dmg == 0) { return 0; } + + attack_damage(); + if(__builtin_expect(fd->end, false)) { return att_dmg; } + damage_dependant_pre_oa(); + + if (att_status->m_hp > 0 && def_status->has_skill(counter) && skill_check(fd, def_status, att_status)) + { + // perform_skill_counter + unsigned counter_dmg(counter_damage(fd, att_status, def_status)); + if (def_status->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, counter); + fd->inc_counter(QuestType::skill_damage, counter, 0, counter_dmg); + } + _DEBUG_MSG(1, "%s takes %u counter damage from %s\n", status_description(att_status).c_str(), counter_dmg, status_description(def_status).c_str()); + remove_hp(fd, att_status, counter_dmg); + prepend_on_death(fd); + resolve_skill(fd); + if (def_cardtype == CardType::assault && def_status->m_hp > 0 && fd->bg_effects.count(counterflux)) + { + unsigned flux_denominator = fd->bg_effects.at(counterflux) ? fd->bg_effects.at(counterflux) : 4; + unsigned flux_value = (def_status->skill(counter) - 1) / flux_denominator + 1; + _DEBUG_MSG(1, "Counterflux: %s heals itself and berserks for %u\n", status_description(def_status).c_str(), flux_value); + add_hp(fd, def_status, flux_value); + if (! def_status->m_sundered) + { def_status->m_attack += flux_value; } + } + } + unsigned corrosive_value = def_status->skill(corrosive); + if (att_status->m_hp > 0 && corrosive_value > att_status->m_corroded_rate && skill_check(fd, def_status, att_status)) + { + // perform_skill_corrosive + _DEBUG_MSG(1, "%s corrodes %s by %u\n", status_description(def_status).c_str(), status_description(att_status).c_str(), corrosive_value); + att_status->m_corroded_rate = corrosive_value; + } + unsigned berserk_value = att_status->skill(berserk); + if (att_status->m_hp > 0 && ! att_status->m_sundered && berserk_value > 0 && skill_check(fd, att_status, nullptr)) + { + // perform_skill_berserk + att_status->m_attack += berserk_value; + if (att_status->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, berserk); + } + if (fd->bg_effects.count(enduringrage)) + { + unsigned bge_denominator = fd->bg_effects.at(enduringrage) ? fd->bg_effects.at(enduringrage) : 2; + unsigned bge_value = (berserk_value - 1) / bge_denominator + 1; + _DEBUG_MSG(1, "EnduringRage: %s heals and protects itself for %u\n", status_description(att_status).c_str(), bge_value); + add_hp(fd, att_status, bge_value); + att_status->m_protected += bge_value; + } + } + do_leech(); + unsigned valor_value = att_status->skill(valor); + if (valor_value > 0 && ! att_status->m_sundered && fd->bg_effects.count(heroism) && def_cardtype == CardType::assault && def_status->m_hp <= 0) + { + _DEBUG_MSG(1, "Heroism: %s gain %u attack\n", status_description(att_status).c_str(), valor_value); + att_status->m_attack += valor_value; + } + return att_dmg; + } + + template + void modify_attack_damage(unsigned pre_modifier_dmg) + { + assert(att_status->m_card->m_type == CardType::assault); + att_dmg = pre_modifier_dmg; + if (att_dmg == 0) + { return; } + std::string desc; + unsigned legion_value = 0; + if (! att_status->m_sundered) + { + // enhance damage + unsigned legion_base = att_status->skill(legion); + if (legion_base > 0) + { + auto & assaults = fd->tap->assaults; + legion_value += att_status->m_index > 0 && assaults[att_status->m_index - 1].m_hp > 0 && assaults[att_status->m_index - 1].m_faction == att_status->m_faction; + legion_value += att_status->m_index + 1 < assaults.size() && assaults[att_status->m_index + 1].m_hp > 0 && assaults[att_status->m_index + 1].m_faction == att_status->m_faction; + if (legion_value > 0 && skill_check(fd, att_status, nullptr)) + { + legion_value *= legion_base; + if (debug_print > 0) { desc += "+" + to_string(legion_value) + "(legion)"; } + att_dmg += legion_value; + } + } + unsigned rupture_value = att_status->skill(rupture); + if (rupture_value > 0) + { + if (debug_print > 0) { desc += "+" + to_string(rupture_value) + "(rupture)"; } + att_dmg += rupture_value; + } + unsigned venom_value = att_status->skill(venom); + if (venom_value > 0 && def_status->m_poisoned > 0) + { + if (debug_print > 0) { desc += "+" + to_string(venom_value) + "(venom)"; } + att_dmg += venom_value; + } + if (fd->bloodlust_value > 0) + { + if (debug_print > 0) { desc += "+" + to_string(fd->bloodlust_value) + "(bloodlust)"; } + att_dmg += fd->bloodlust_value; + } + if(def_status->m_enfeebled > 0) + { + if(debug_print > 0) { desc += "+" + to_string(def_status->m_enfeebled) + "(enfeebled)"; } + att_dmg += def_status->m_enfeebled; + } + } + // prevent damage + std::string reduced_desc; + unsigned reduced_dmg(0); + unsigned armor_value = def_status->skill(armor); + if (def_status->m_card->m_type == CardType::assault && fd->bg_effects.count(fortification)) + { + for (auto && adj_status: fd->adjacent_assaults(def_status)) + { + armor_value = std::max(armor_value, adj_status->skill(armor)); + } + } + if(armor_value > 0) + { + if(debug_print > 0) { reduced_desc += to_string(armor_value) + "(armor)"; } + reduced_dmg += armor_value; + } + if(def_status->protected_value() > 0) + { + if(debug_print > 0) { reduced_desc += (reduced_desc.empty() ? "" : "+") + to_string(def_status->protected_value()) + "(protected)"; } + reduced_dmg += def_status->protected_value(); + } + unsigned pierce_value = att_status->skill(pierce) + att_status->skill(rupture); + if (reduced_dmg > 0 && pierce_value > 0) + { + if (debug_print > 0) { reduced_desc += "-" + to_string(pierce_value) + "(pierce)"; } + reduced_dmg = safe_minus(reduced_dmg, pierce_value); + } + att_dmg = safe_minus(att_dmg, reduced_dmg); + if(debug_print > 0) + { + if(!reduced_desc.empty()) { desc += "-[" + reduced_desc + "]"; } + if(!desc.empty()) { desc += "=" + to_string(att_dmg); } + _DEBUG_MSG(1, "%s attacks %s for %u%s damage\n", status_description(att_status).c_str(), status_description(def_status).c_str(), pre_modifier_dmg, desc.c_str()); + } + if (legion_value > 0 && can_be_healed(att_status) && fd->bg_effects.count(brigade)) + { + _DEBUG_MSG(1, "Brigade: %s heals itself for %u\n", status_description(att_status).c_str(), legion_value); + add_hp(fd, att_status, legion_value); + } + } + + template + void attack_damage() + { + remove_hp(fd, def_status, att_dmg); + prepend_on_death(fd); + resolve_skill(fd); + } + + template + void damage_dependant_pre_oa() {} + + template + void do_leech() {} +}; + +template<> +void PerformAttack::attack_damage() +{ + remove_commander_hp(fd, *def_status, att_dmg, true); +} + +template<> +void PerformAttack::damage_dependant_pre_oa() +{ + unsigned poison_value = std::max(att_status->skill(poison), att_status->skill(venom)); + if (poison_value > def_status->m_poisoned && skill_check(fd, att_status, def_status)) + { + // perform_skill_poison + if (att_status->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, poison); + } + _DEBUG_MSG(1, "%s poisons %s by %u\n", status_description(att_status).c_str(), status_description(def_status).c_str(), poison_value); + def_status->m_poisoned = poison_value; + } + unsigned inhibit_value = att_status->skill(inhibit); + if (inhibit_value > def_status->m_inhibited && skill_check(fd, att_status, def_status)) + { + // perform_skill_inhibit + _DEBUG_MSG(1, "%s inhibits %s by %u\n", status_description(att_status).c_str(), status_description(def_status).c_str(), inhibit_value); + def_status->m_inhibited = inhibit_value; + } +} + +template<> +void PerformAttack::do_leech() +{ + unsigned leech_value = std::min(att_dmg, att_status->skill(leech)); + if(leech_value > 0 && skill_check(fd, att_status, nullptr)) + { + if (att_status->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, leech); + } + _DEBUG_MSG(1, "%s leeches %u health\n", status_description(att_status).c_str(), leech_value); + add_hp(fd, att_status, leech_value); + } +} + +// General attack phase by the currently evaluated assault, taking into accounts exotic stuff such as flurry, etc. +unsigned attack_commander(Field* fd, CardStatus* att_status) +{ + CardStatus* def_status{select_first_enemy_wall(fd)}; // defending wall + if(def_status != nullptr) + { + return PerformAttack{fd, att_status, def_status}.op(); + } + else + { + return PerformAttack{fd, att_status, &fd->tip->commander}.op(); + } +} +// Return true if actually attacks +bool attack_phase(Field* fd) +{ + CardStatus* att_status(&fd->tap->assaults[fd->current_ci]); // attacking card + Storage& def_assaults(fd->tip->assaults); + + if (attack_power(att_status) == 0) + { + { // Bizarre behavior: Swipe activates if attack is corroded to 0 + CardStatus * def_status = &fd->tip->assaults[fd->current_ci]; + unsigned swipe_value = att_status->skill(swipe); + if (alive_assault(def_assaults, fd->current_ci) && att_status->m_attack + att_status->m_rallied > att_status->m_weakened && swipe_value > 0) + { + for (auto && adj_status: fd->adjacent_assaults(def_status)) + { + unsigned swipe_dmg = safe_minus(swipe_value + def_status->m_enfeebled, def_status->protected_value()); + _DEBUG_MSG(1, "%s swipes %s for %u damage\n", status_description(att_status).c_str(), status_description(adj_status).c_str(), swipe_dmg); + remove_hp(fd, adj_status, swipe_dmg); + } + prepend_on_death(fd); + resolve_skill(fd); + } + } + return false; + } + + unsigned att_dmg = 0; + if (alive_assault(def_assaults, fd->current_ci)) + { + CardStatus * def_status = &fd->tip->assaults[fd->current_ci]; + att_dmg = PerformAttack{fd, att_status, def_status}.op(); + unsigned swipe_value = att_status->skill(swipe); + if (att_dmg > 0 && swipe_value > 0) + { + for (auto && adj_status: fd->adjacent_assaults(def_status)) + { + unsigned swipe_dmg = safe_minus(swipe_value + def_status->m_enfeebled, def_status->protected_value()); + _DEBUG_MSG(1, "%s swipes %s for %u damage\n", status_description(att_status).c_str(), status_description(adj_status).c_str(), swipe_dmg); + remove_hp(fd, adj_status, swipe_dmg); + } + prepend_on_death(fd); + resolve_skill(fd); + } + } + else + { + // might be blocked by walls + att_dmg = attack_commander(fd, att_status); + } + + if (att_dmg > 0 && !fd->assault_bloodlusted && fd->bg_effects.count(bloodlust)) + { + fd->bloodlust_value += fd->bg_effects.at(bloodlust); + fd->assault_bloodlusted = true; + } + + return true; +} + +//---------------------- $65 active skills implementation ---------------------- +template< + bool C + , typename T1 + , typename T2 + > +struct if_ +{ + typedef T1 type; +}; + +template< + typename T1 + , typename T2 + > +struct if_ +{ + typedef T2 type; +}; + +template +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ return dst->m_hp > 0; } + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return dst->has_skill(s.s) && (!(BEGIN_ACTIVATION < s.s && s.s < END_ACTIVATION) || is_active(dst)); +} + +/* + * Target active units: Activation (Mortar) + * Target everything: Defensive (Refresh), Combat-Modifier (Rupture, Venom) + */ +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return dst->has_skill(s.s) && !dst->has_skill(s.s2) && (!(BEGIN_ACTIVATION < s.s2 && s.s2 < END_ACTIVATION) || is_active(dst)); +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ return(can_be_healed(dst)); } + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ return(can_be_healed(dst)); } + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return is_active_next_turn(dst); +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + if (dst->m_overloaded || has_attacked(dst) || !is_active(dst)) + { + return false; + } + bool has_inhibited_unit = false; + for (const auto & c: fd->players[dst->m_player]->assaults.m_indirect) + { + if (c->m_hp > 0 && c->m_inhibited) + { + has_inhibited_unit = true; + break; + } + } + for (const auto & ss: dst->m_card->m_skills) + { + if (dst->m_skill_cd[ss.id] > 0) + { + continue; + } + Skill evolved_skill_id = static_cast(ss.id + dst->m_evolved_skill_offset[ss.id]); + if (BEGIN_ACTIVATION_HARMFUL < evolved_skill_id && evolved_skill_id < END_ACTIVATION_HARMFUL) + { + return true; + } + if (has_inhibited_unit && (evolved_skill_id == heal || evolved_skill_id == protect || evolved_skill_id == rally)) + { + return true; + } + } + return false; +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return ! dst->m_sundered && (fd->tapi == dst->m_player ? is_active(dst) && !has_attacked(dst) : is_active_next_turn(dst)); +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return ! src->m_rush_attempted && dst->m_delay >= (src->m_card->m_type == CardType::assault && dst->m_index < src->m_index ? 2u : 1u); +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return attack_power(dst) > 0 && is_active_next_turn(dst); +} + +template<> +inline bool skill_predicate(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + return attack_power(dst) > 0 && is_active_next_turn(dst); +} + +template +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ assert(false); } + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_enfeebled += s.x; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_enhanced_value[s.s + dst->m_primary_skill_offset[s.s]] += s.x; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + auto primary_s1 = dst->m_primary_skill_offset[s.s] + s.s; + auto primary_s2 = dst->m_primary_skill_offset[s.s2] + s.s2; + dst->m_primary_skill_offset[s.s] = primary_s2 - s.s; + dst->m_primary_skill_offset[s.s2] = primary_s1 - s.s2; + dst->m_evolved_skill_offset[primary_s1] = s.s2 - primary_s1; + dst->m_evolved_skill_offset[primary_s2] = s.s - primary_s2; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + add_hp(fd, dst, s.x); +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_jammed = true; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + add_hp(fd, dst, s.x); +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + if (dst->m_card->m_type == CardType::structure) + { + remove_hp(fd, dst, s.x); + } + else + { + unsigned strike_dmg = safe_minus((s.x + 1) / 2 + dst->m_enfeebled, src->m_overloaded ? 0 : dst->protected_value()); + remove_hp(fd, dst, strike_dmg); + } +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_overloaded = true; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_protected += s.x; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_rallied += s.x; +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_delay -= 1; + if (dst->m_delay == 0) + { + check_and_perform_valor(fd, dst); + } +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + remove_hp(fd, dst, s.x); +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + unsigned strike_dmg = safe_minus(s.x + dst->m_enfeebled, src->m_overloaded ? 0 : dst->protected_value()); + remove_hp(fd, dst, strike_dmg); +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_sundered = true; + dst->m_weakened += std::min(s.x, attack_power(dst)); +} + +template<> +inline void perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s) +{ + dst->m_weakened += std::min(s.x, attack_power(dst)); +} + +template +inline unsigned select_fast(Field* fd, CardStatus* src, const std::vector& cards, const SkillSpec& s) +{ + if (s.y == allfactions || fd->bg_effects.count(metamorphosis)) + { + return(fd->make_selection_array(cards.begin(), cards.end(), [fd, src, s](CardStatus* c){return(skill_predicate(fd, src, c, s));})); + } + else + { + return(fd->make_selection_array(cards.begin(), cards.end(), [fd, src, s](CardStatus* c){return((c->m_faction == s.y || c->m_faction == progenitor) && skill_predicate(fd, src, c, s));})); + } +} + +template<> +inline unsigned select_fast(Field* fd, CardStatus* src, const std::vector& cards, const SkillSpec& s) +{ + fd->selection_array.clear(); + for (auto && adj_status: fd->adjacent_assaults(src)) + { + if (skill_predicate(fd, src, adj_status, s)) + { + fd->selection_array.push_back(adj_status); + } + } + return fd->selection_array.size(); +} + +inline std::vector& skill_targets_hostile_assault(Field* fd, CardStatus* src) +{ + return(fd->players[opponent(src->m_player)]->assaults.m_indirect); +} + +inline std::vector& skill_targets_allied_assault(Field* fd, CardStatus* src) +{ + return(fd->players[src->m_player]->assaults.m_indirect); +} + +inline std::vector& skill_targets_hostile_structure(Field* fd, CardStatus* src) +{ + return(fd->players[opponent(src->m_player)]->structures.m_indirect); +} + +inline std::vector& skill_targets_allied_structure(Field* fd, CardStatus* src) +{ + return(fd->players[src->m_player]->structures.m_indirect); +} + +template +std::vector& skill_targets(Field* fd, CardStatus* src) +{ + std::cerr << "skill_targets: Error: no specialization for " << skill_names[skill] << "\n"; + throw; +} + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_allied_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_structure(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_assault(fd, src)); } + +template<> std::vector& skill_targets(Field* fd, CardStatus* src) +{ return(skill_targets_hostile_assault(fd, src)); } + +template +bool check_and_perform_skill(Field* fd, CardStatus* src, CardStatus* dst, const SkillSpec& s, bool is_evadable, bool & has_counted_quest) +{ + if(skill_check(fd, src, dst)) + { + if (src->m_player == 0 && ! has_counted_quest) + { + fd->inc_counter(QuestType::skill_use, skill_id, dst->m_card->m_id); + has_counted_quest = true; + } + if (is_evadable && + dst->m_evaded < dst->skill(evade) && + skill_check(fd, dst, src)) + { + ++ dst->m_evaded; + _DEBUG_MSG(1, "%s %s on %s but it evades\n", status_description(src).c_str(), skill_short_description(s).c_str(), status_description(dst).c_str()); + return(false); + } + _DEBUG_MSG(1, "%s %s on %s\n", status_description(src).c_str(), skill_short_description(s).c_str(), status_description(dst).c_str()); + perform_skill(fd, src, dst, s); + if (s.c > 0) + { + src->m_skill_cd[skill_id] = s.c; + } + return(true); + } + _DEBUG_MSG(1, "(CANCELLED) %s %s on %s\n", status_description(src).c_str(), skill_short_description(s).c_str(), status_description(dst).c_str()); + return(false); +} + +bool check_and_perform_valor(Field* fd, CardStatus* src) +{ + unsigned valor_value = src->skill(valor); + if (valor_value > 0 && ! src->m_sundered && skill_check(fd, src, nullptr)) + { + unsigned opponent_player = opponent(src->m_player); + const CardStatus * dst = fd->players[opponent_player]->assaults.size() > src->m_index ? + &fd->players[opponent_player]->assaults[src->m_index] : + nullptr; + if (dst == nullptr || dst->m_hp <= 0) + { + _DEBUG_MSG(1, "%s loses Valor (no blocker)\n", status_description(src).c_str()); + return false; + } + else if (attack_power(dst) <= attack_power(src)) + { + _DEBUG_MSG(1, "%s loses Valor (weak blocker %s)\n", status_description(src).c_str(), status_description(dst).c_str()); + return false; + } + if (src->m_player == 0) + { + fd->inc_counter(QuestType::skill_use, valor); + } + _DEBUG_MSG(1, "%s activates Valor %u\n", status_description(src).c_str(), valor_value); + src->m_attack += valor_value; + return true; + } + return false; +} + +template +size_t select_targets(Field* fd, CardStatus* src, const SkillSpec& s) +{ + std::vector& cards(skill_targets(fd, src)); + size_t n_candidates = select_fast(fd, src, cards, s); + if (n_candidates == 0) + { + return n_candidates; + } + _DEBUG_SELECTION("%s", skill_names[skill_id].c_str()); + unsigned n_targets = s.n > 0 ? s.n : 1; + if (s.all || n_targets >= n_candidates || skill_id == mend) // target all or mend + { + return n_candidates; + } + for (unsigned i = 0; i < n_targets; ++i) + { + std::swap(fd->selection_array[i], fd->selection_array[fd->rand(i, n_candidates - 1)]); + } + fd->selection_array.resize(n_targets); + if (n_targets > 1) + { + std::sort(fd->selection_array.begin(), fd->selection_array.end(), [](const CardStatus * a, const CardStatus * b) { return a->m_index < b->m_index; }); + } + return n_targets; +} + +template<> +size_t select_targets(Field* fd, CardStatus* src, const SkillSpec& s) +{ + size_t n_candidates = select_fast(fd, src, skill_targets(fd, src), s); + if (n_candidates == 0) + { + n_candidates = select_fast(fd, src, skill_targets(fd, src), s); + if (n_candidates == 0) + { + return n_candidates; + } + } + _DEBUG_SELECTION("%s", skill_names[mortar].c_str()); + unsigned n_targets = s.n > 0 ? s.n : 1; + if (s.all || n_targets >= n_candidates) + { + return n_candidates; + } + for (unsigned i = 0; i < n_targets; ++i) + { + std::swap(fd->selection_array[i], fd->selection_array[fd->rand(i, n_candidates - 1)]); + } + fd->selection_array.resize(n_targets); + if (n_targets > 1) + { + std::sort(fd->selection_array.begin(), fd->selection_array.end(), [](const CardStatus * a, const CardStatus * b) { return a->m_index < b->m_index; }); + } + return n_targets; +} + +template +void perform_targetted_allied_fast(Field* fd, CardStatus* src, const SkillSpec& s) +{ + select_targets(fd, src, s); + unsigned num_inhibited = 0; + bool has_counted_quest = false; + for (CardStatus * dst: fd->selection_array) + { + if (dst->m_inhibited > 0 && !src->m_overloaded) + { + _DEBUG_MSG(1, "%s %s on %s but it is inhibited\n", status_description(src).c_str(), skill_short_description(s).c_str(), status_description(dst).c_str()); + -- dst->m_inhibited; + ++ num_inhibited; + continue; + } + check_and_perform_skill(fd, src, dst, s, false, has_counted_quest); + } + if (num_inhibited > 0 && fd->bg_effects.count(divert)) + { + SkillSpec diverted_ss = s; + diverted_ss.y = allfactions; + diverted_ss.n = 1; + diverted_ss.all = false; + for (unsigned i = 0; i < num_inhibited; ++ i) + { + select_targets(fd, &fd->tip->commander, diverted_ss); + for (CardStatus * dst: fd->selection_array) + { + if (dst->m_inhibited > 0) + { + _DEBUG_MSG(1, "%s %s (Diverted) on %s but it is inhibited\n", status_description(src).c_str(), skill_short_description(diverted_ss).c_str(), status_description(dst).c_str()); + -- dst->m_inhibited; + continue; + } + _DEBUG_MSG(1, "%s %s (Diverted) on %s\n", status_description(src).c_str(), skill_short_description(diverted_ss).c_str(), status_description(dst).c_str()); + perform_skill(fd, src, dst, diverted_ss); + } + } + } +} + +void perform_targetted_allied_fast_rush(Field* fd, CardStatus* src, const SkillSpec& s) +{ + if (src->m_card->m_type == CardType::commander) + { // BGE skills are casted as by commander + perform_targetted_allied_fast(fd, src, s); + return; + } + if (src->m_rush_attempted) + { + _DEBUG_MSG(2, "%s does not check Rush again.\n", status_description(src).c_str()); + return; + } + _DEBUG_MSG(1, "%s attempts to activate Rush.\n", status_description(src).c_str()); + perform_targetted_allied_fast(fd, src, s); + src->m_rush_attempted = true; +} + +template +void perform_targetted_hostile_fast(Field* fd, CardStatus* src, const SkillSpec& s) +{ + select_targets(fd, src, s); + bool has_counted_quest = false; + std::vector paybackers; + if (fd->bg_effects.count(turningtides) && skill_id == weaken) + { + unsigned turningtides_value = 0; + for (CardStatus * dst: fd->selection_array) + { + unsigned old_attack = attack_power(dst); + if (check_and_perform_skill(fd, src, dst, s, ! src->m_overloaded, has_counted_quest)) + { + turningtides_value = std::max(turningtides_value, safe_minus(old_attack, attack_power(dst))); + // Payback + if(dst->m_paybacked < dst->skill(payback) && skill_check(fd, dst, src) && + skill_predicate(fd, src, src, s) && skill_check(fd, src, dst)) + { + paybackers.push_back(dst); + } + } + } + if (turningtides_value > 0) + { + SkillSpec ss_rally{rally, turningtides_value, allfactions, 0, 0, no_skill, no_skill, s.all,}; + _DEBUG_MSG(1, "TurningTides %u!\n", turningtides_value); + perform_targetted_allied_fast(fd, &fd->players[src->m_player]->commander, ss_rally); + } + for (CardStatus * pb_status: paybackers) + { + ++ pb_status->m_paybacked; + unsigned old_attack = attack_power(src); + _DEBUG_MSG(1, "%s Payback %s on %s\n", status_description(pb_status).c_str(), skill_short_description(s).c_str(), status_description(src).c_str()); + perform_skill(fd, pb_status, src, s); + turningtides_value = std::max(turningtides_value, safe_minus(old_attack, attack_power(src))); + if (turningtides_value > 0) + { + SkillSpec ss_rally{rally, turningtides_value, allfactions, 0, 0, no_skill, no_skill, false,}; + _DEBUG_MSG(1, "Paybacked TurningTides %u!\n", turningtides_value); + perform_targetted_allied_fast(fd, &fd->players[pb_status->m_player]->commander, ss_rally); + } + } + return; + } + for (CardStatus * dst: fd->selection_array) + { + if (check_and_perform_skill(fd, src, dst, s, ! src->m_overloaded, has_counted_quest)) + { + // Payback + if(dst->m_paybacked < dst->skill(payback) && skill_check(fd, dst, src) && + skill_predicate(fd, src, src, s) && skill_check(fd, src, dst)) + { + paybackers.push_back(dst); + } + } + } + prepend_on_death(fd); // skills + for (CardStatus * pb_status: paybackers) + { + ++ pb_status->m_paybacked; + _DEBUG_MSG(1, "%s Payback %s on %s\n", status_description(pb_status).c_str(), skill_short_description(s).c_str(), status_description(src).c_str()); + perform_skill(fd, pb_status, src, s); + } + prepend_on_death(fd); // paybacked skills +} + +//------------------------------------------------------------------------------ +Results play(Field* fd) +{ + fd->players[0]->commander.m_player = 0; + fd->players[1]->commander.m_player = 1; + fd->tapi = fd->gamemode == surge ? 1 : 0; + fd->tipi = opponent(fd->tapi); + fd->tap = fd->players[fd->tapi]; + fd->tip = fd->players[fd->tipi]; + fd->end = false; + + // Play fortresses + for (unsigned _ = 0; _ < 2; ++ _) + { + for (const Card* played_card: fd->tap->deck->shuffled_forts) + { + PlayCard(played_card, fd).op(); + } + std::swap(fd->tapi, fd->tipi); + std::swap(fd->tap, fd->tip); + } + + while(__builtin_expect(fd->turn <= turn_limit && !fd->end, true)) + { + fd->current_phase = Field::playcard_phase; + // Initialize stuff, remove dead cards + _DEBUG_MSG(1, "------------------------------------------------------------------------\n" + "TURN %u begins for %s\n", fd->turn, status_description(&fd->tap->commander).c_str()); + turn_start_phase(fd); + + // Play a card + const Card* played_card(fd->tap->deck->next()); + if(played_card) + { + // Evaluate skill Allegiance + for (CardStatus * status : fd->tap->assaults.m_indirect) + { + unsigned allegiance_value = status->skill(allegiance); + assert(status->m_card); + if (allegiance_value > 0 && status->m_hp > 0 && status->m_card->m_faction == played_card->m_faction) + { + _DEBUG_MSG(1, "%s activates Allegiance %u\n", status_description(status).c_str(), allegiance_value); + if (! status->m_sundered) + { status->m_attack += allegiance_value; } + status->m_max_hp += allegiance_value; + status->m_hp += allegiance_value; + } + } + // End Evaluate skill Allegiance + switch(played_card->m_type) + { + case CardType::assault: + PlayCard(played_card, fd).op(); + break; + case CardType::structure: + PlayCard(played_card, fd).op(); + break; + case CardType::commander: + case CardType::num_cardtypes: + _DEBUG_MSG(0, "Unknown card type: #%u %s: %u\n", played_card->m_id, card_description(fd->cards, played_card).c_str(), played_card->m_type); + assert(false); + break; + } + } + if(__builtin_expect(fd->end, false)) { break; } + + // Evaluate Heroism BGE skills + if (fd->bg_effects.count(heroism)) + { + for (CardStatus * dst: fd->tap->assaults.m_indirect) + { + unsigned bge_value = (dst->skill(valor) + 1) / 2; + if (bge_value <= 0) + { continue; } + SkillSpec ss_protect{protect, bge_value, allfactions, 0, 0, no_skill, no_skill, false,}; + if (dst->m_inhibited > 0) + { + _DEBUG_MSG(1, "Heroism: %s on %s but it is inhibited\n", skill_short_description(ss_protect).c_str(), status_description(dst).c_str()); + -- dst->m_inhibited; + if (fd->bg_effects.count(divert)) + { + SkillSpec diverted_ss = ss_protect; + diverted_ss.y = allfactions; + diverted_ss.n = 1; + diverted_ss.all = false; + // for (unsigned i = 0; i < num_inhibited; ++ i) + { + select_targets(fd, &fd->tip->commander, diverted_ss); + for (CardStatus * dst: fd->selection_array) + { + if (dst->m_inhibited > 0) + { + _DEBUG_MSG(1, "Heroism: %s (Diverted) on %s but it is inhibited\n", skill_short_description(diverted_ss).c_str(), status_description(dst).c_str()); + -- dst->m_inhibited; + continue; + } + _DEBUG_MSG(1, "Heroism: %s (Diverted) on %s\n", skill_short_description(diverted_ss).c_str(), status_description(dst).c_str()); + perform_skill(fd, &fd->tap->commander, dst, diverted_ss); // XXX: the caster + } + } + } + continue; + } + bool has_counted_quest = false; + check_and_perform_skill(fd, &fd->tap->commander, dst, ss_protect, false, has_counted_quest); + } + } + + // Evaluate activation BGE skills + for (const auto & bg_skill: fd->bg_skills[fd->tapi]) + { + _DEBUG_MSG(2, "Evaluating BG skill %s\n", skill_description(fd->cards, bg_skill).c_str()); + fd->skill_queue.emplace_back(&fd->tap->commander, bg_skill); + resolve_skill(fd); + } + if (__builtin_expect(fd->end, false)) { break; } + + // Evaluate commander + fd->current_phase = Field::commander_phase; + evaluate_skills(fd, &fd->tap->commander, fd->tap->commander.m_card->m_skills); + if(__builtin_expect(fd->end, false)) { break; } + + // Evaluate structures + fd->current_phase = Field::structures_phase; + for(fd->current_ci = 0; !fd->end && fd->current_ci < fd->tap->structures.size(); ++fd->current_ci) + { + CardStatus* current_status(&fd->tap->structures[fd->current_ci]); + if (!is_active(current_status)) + { + _DEBUG_MSG(2, "%s cannot take action.\n", status_description(current_status).c_str()); + } + else + { + evaluate_skills(fd, current_status, current_status->m_card->m_skills); + } + } + // Evaluate assaults + fd->current_phase = Field::assaults_phase; + fd->bloodlust_value = 0; + for(fd->current_ci = 0; !fd->end && fd->current_ci < fd->tap->assaults.size(); ++fd->current_ci) + { + // ca: current assault + CardStatus* current_status(&fd->tap->assaults[fd->current_ci]); + bool attacked = false; + if (!is_active(current_status)) + { + _DEBUG_MSG(2, "%s cannot take action.\n", status_description(current_status).c_str()); + } + else + { + fd->assault_bloodlusted = false; + evaluate_skills(fd, current_status, current_status->m_card->m_skills, &attacked); + if (__builtin_expect(fd->end, false)) { break; } + } + if (current_status->m_corroded_rate > 0) + { + if (attacked) + { + unsigned v = std::min(current_status->m_corroded_rate, attack_power(current_status)); + _DEBUG_MSG(1, "%s loses Attack by %u.\n", status_description(current_status).c_str(), v); + current_status->m_corroded_weakened += v; + } + else + { + _DEBUG_MSG(1, "%s loses Status corroded.\n", status_description(current_status).c_str()); + current_status->m_corroded_rate = 0; + current_status->m_corroded_weakened = 0; + } + } + current_status->m_step = CardStep::attacked; + } + fd->current_phase = Field::end_phase; + turn_end_phase(fd); + if(__builtin_expect(fd->end, false)) { break; } + _DEBUG_MSG(1, "TURN %u ends for %s\n", fd->turn, status_description(&fd->tap->commander).c_str()); + std::swap(fd->tapi, fd->tipi); + std::swap(fd->tap, fd->tip); + ++fd->turn; + } + const auto & p = fd->players; + unsigned raid_damage = 0; + unsigned quest_score = 0; + switch (fd->optimization_mode) + { + case OptimizationMode::raid: + raid_damage = 15 + (std::min(p[1]->deck->deck_size, (fd->turn + 1) / 2) - p[1]->assaults.size() - p[1]->structures.size()) - (10 * p[1]->commander.m_hp / p[1]->commander.m_max_hp); + break; + case OptimizationMode::quest: + if (fd->quest.quest_type == QuestType::card_survival) + { + for (const auto & status: p[0]->assaults.m_indirect) + { fd->quest_counter += (fd->quest.quest_key == status->m_card->m_id); } + for (const auto & status: p[0]->structures.m_indirect) + { fd->quest_counter += (fd->quest.quest_key == status->m_card->m_id); } + for (const auto & card: p[0]->deck->shuffled_cards) + { fd->quest_counter += (fd->quest.quest_key == card->m_id); } + } + quest_score = fd->quest.must_fulfill ? (fd->quest_counter >= fd->quest.quest_value ? fd->quest.quest_score : 0) : std::min(fd->quest.quest_score, fd->quest.quest_score * fd->quest_counter / fd->quest.quest_value); + _DEBUG_MSG(1, "Quest: %u / %u = %u%%.\n", fd->quest_counter, fd->quest.quest_value, quest_score); + break; + default: + break; + } + // you lose + if(fd->players[0]->commander.m_hp == 0) + { + _DEBUG_MSG(1, "You lose.\n"); + switch (fd->optimization_mode) + { + case OptimizationMode::raid: return {0, 0, 1, raid_damage}; + case OptimizationMode::brawl: return {0, 0, 1, 5}; + case OptimizationMode::quest: return {0, 0, 1, fd->quest.must_win ? 0 : quest_score}; + default: return {0, 0, 1, 0}; + } + } + // you win + if(fd->players[1]->commander.m_hp == 0) + { + _DEBUG_MSG(1, "You win.\n"); + switch (fd->optimization_mode) + { + case OptimizationMode::brawl: + { + unsigned brawl_score = 57 + - (10 * (p[0]->commander.m_max_hp - p[0]->commander.m_hp) / p[0]->commander.m_max_hp) + + (p[0]->assaults.size() + p[0]->structures.size() + p[0]->deck->shuffled_cards.size()) + - (p[1]->assaults.size() + p[1]->structures.size() + p[1]->deck->shuffled_cards.size()) + - fd->turn / 4; + return {1, 0, 0, brawl_score}; + } + case OptimizationMode::campaign: + { + unsigned campaign_score = 100 - 10 * (std::min(p[0]->deck->cards.size(), (fd->turn + 1) / 2) - p[0]->assaults.size() - p[0]->structures.size()); + return {1, 0, 0, campaign_score}; + } + case OptimizationMode::quest: return {1, 0, 0, fd->quest.win_score + quest_score}; + default: + return {1, 0, 0, 100}; + } + } + if (fd->turn > turn_limit) + { + _DEBUG_MSG(1, "Stall after %u turns.\n", turn_limit); + switch (fd->optimization_mode) + { + case OptimizationMode::defense: return {0, 1, 0, 100}; + case OptimizationMode::raid: return {0, 1, 0, raid_damage}; + case OptimizationMode::brawl: return {0, 1, 0, 5}; + case OptimizationMode::quest: return {0, 1, 0, fd->quest.must_win ? 0 : quest_score}; + default: return {0, 1, 0, 0}; + } + } + + // Huh? How did we get here? + assert(false); + return {0, 0, 0, 0}; +} +//------------------------------------------------------------------------------ +void fill_skill_table() +{ + memset(skill_table, 0, sizeof skill_table); + skill_table[mortar] = perform_targetted_hostile_fast; + skill_table[enfeeble] = perform_targetted_hostile_fast; + skill_table[enhance] = perform_targetted_allied_fast; + skill_table[evolve] = perform_targetted_allied_fast; + skill_table[heal] = perform_targetted_allied_fast; + skill_table[jam] = perform_targetted_hostile_fast; + skill_table[mend] = perform_targetted_allied_fast; + skill_table[overload] = perform_targetted_allied_fast; + skill_table[protect] = perform_targetted_allied_fast; + skill_table[rally] = perform_targetted_allied_fast; + skill_table[rush] = perform_targetted_allied_fast_rush; + skill_table[siege] = perform_targetted_hostile_fast; + skill_table[strike] = perform_targetted_hostile_fast; + skill_table[sunder] = perform_targetted_hostile_fast; + skill_table[weaken] = perform_targetted_hostile_fast; +} diff --git a/sim.h b/sim.h new file mode 100644 index 00000000..404a9625 --- /dev/null +++ b/sim.h @@ -0,0 +1,317 @@ +#ifndef SIM_H_INCLUDED +#define SIM_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tyrant.h" + +class Card; +class Cards; +class Deck; +class Field; +class Achievement; + +extern unsigned turn_limit; + +inline unsigned safe_minus(unsigned x, unsigned y) +{ + return(x - std::min(x, y)); +} + +//---------------------- Represent Simulation Results ---------------------------- +template +struct Results +{ + result_type wins; + result_type draws; + result_type losses; + result_type points; + template + Results& operator+=(const Results& other) + { + wins += other.wins; + draws += other.draws; + losses += other.losses; + points += other.points; + return *this; + } +}; + +typedef std::pair>, unsigned> EvaluatedResults; + +template +struct FinalResults +{ + result_type wins; + result_type draws; + result_type losses; + result_type points; + result_type points_lower_bound; + result_type points_upper_bound; + uint64_t n_sims; +}; + +void fill_skill_table(); +Results play(Field* fd); +// Pool-based indexed storage. +//---------------------- Pool-based indexed storage ---------------------------- +template +class Storage +{ +public: + typedef typename std::vector::size_type size_type; + typedef T value_type; + Storage(size_type size) : + m_pool(sizeof(T)) + { + m_indirect.reserve(size); + } + + inline T& operator[](size_type i) + { + return(*m_indirect[i]); + } + + inline T& add_back() + { + m_indirect.emplace_back((T*) m_pool.malloc()); + return(*m_indirect.back()); + } + + template + void remove(Pred p) + { + size_type head(0); + for(size_type current(0); current < m_indirect.size(); ++current) + { + if(p((*this)[current])) + { + m_pool.free(m_indirect[current]); + } + else + { + if(current != head) + { + m_indirect[head] = m_indirect[current]; + } + ++head; + } + } + m_indirect.erase(m_indirect.begin() + head, m_indirect.end()); + } + + void reset() + { + for(auto index: m_indirect) + { + m_pool.free(index); + } + m_indirect.clear(); + } + + inline size_type size() const + { + return(m_indirect.size()); + } + + std::vector m_indirect; + boost::pool<> m_pool; +}; +//------------------------------------------------------------------------------ +enum class CardStep +{ + none, + attacking, + attacked, +}; +//------------------------------------------------------------------------------ +struct CardStatus +{ + const Card* m_card; + unsigned m_index; + unsigned m_player; + unsigned m_delay; + Faction m_faction; + unsigned m_attack; + unsigned m_hp; + unsigned m_max_hp; + CardStep m_step; + + unsigned m_corroded_rate; + unsigned m_corroded_weakened; + unsigned m_enfeebled; + unsigned m_evaded; + unsigned m_inhibited; + bool m_jammed; + bool m_overloaded; + unsigned m_paybacked; + unsigned m_poisoned; + unsigned m_protected; + unsigned m_rallied; + bool m_rush_attempted; + bool m_sundered; + unsigned m_weakened; + + signed m_primary_skill_offset[num_skills]; + signed m_evolved_skill_offset[num_skills]; + unsigned m_enhanced_value[num_skills]; + unsigned m_skill_cd[num_skills]; + + CardStatus() {} + + void set(const Card* card); + void set(const Card& card); + std::string description() const; + inline unsigned skill_base_value(Skill skill_id) const; + unsigned skill(Skill skill_id) const; + bool has_skill(Skill skill_id) const; + unsigned enhanced(Skill skill) const; + unsigned protected_value() const; +}; +//------------------------------------------------------------------------------ +// Represents a particular draw from a deck. +// Persistent object: call reset to get a new draw. +class Hand +{ +public: + + Hand(Deck* deck_) : + deck(deck_), + assaults(15), + structures(15) + { + } + + void reset(std::mt19937& re); + + Deck* deck; + CardStatus commander; + Storage assaults; + Storage structures; +}; + +struct Quest +{ + QuestType::QuestType quest_type; + unsigned quest_key; + unsigned quest_2nd_key; + unsigned quest_value; + unsigned quest_score; // score for quest goal + unsigned win_score; // score for win regardless quest goal + bool must_fulfill; // true: score iff value is reached; false: score proportion to achieved value + bool must_win; // true: score only if win + Quest() : + quest_type(QuestType::none), + quest_key(0), + quest_value(0), + quest_score(100), + win_score(0), + must_fulfill(false), + must_win(false) + {} +}; + +//------------------------------------------------------------------------------ +// struct Field is the data model of a battle: +// an attacker and a defender deck, list of assaults and structures, etc. +class Field +{ +public: + bool end; + std::mt19937& re; + const Cards& cards; + // players[0]: the attacker, players[1]: the defender + std::array players; + unsigned tapi; // current turn's active player index + unsigned tipi; // and inactive + Hand* tap; + Hand* tip; + std::vector selection_array; + unsigned turn; + gamemode_t gamemode; + OptimizationMode optimization_mode; + const Quest quest; + std::unordered_map bg_effects; // passive BGE + std::vector bg_skills[2]; // active BGE, casted every turn + // With the introduction of on death skills, a single skill can trigger arbitrary many skills. + // They are stored in this, and cleared after all have been performed. + std::deque> skill_queue; + std::vector killed_units; + enum phase + { + playcard_phase, + legion_phase, + commander_phase, + structures_phase, + assaults_phase, + end_phase, + }; + // the current phase of the turn: starts with playcard_phase, then commander_phase, structures_phase, and assaults_phase + phase current_phase; + // the index of the card being evaluated in the current phase. + // Meaningless in playcard_phase, + // otherwise is the index of the current card in players->structures or players->assaults + unsigned current_ci; + + bool assault_bloodlusted; + unsigned bloodlust_value; + unsigned quest_counter; + + Field(std::mt19937& re_, const Cards& cards_, Hand& hand1, Hand& hand2, gamemode_t gamemode_, OptimizationMode optimization_mode_, const Quest & quest_, + std::unordered_map& bg_effects_, std::vector& your_bg_skills_, std::vector& enemy_bg_skills_) : + end{false}, + re(re_), + cards(cards_), + players{{&hand1, &hand2}}, + turn(1), + gamemode(gamemode_), + optimization_mode(optimization_mode_), + quest(quest_), + bg_effects{bg_effects_}, + bg_skills{your_bg_skills_, enemy_bg_skills_}, + assault_bloodlusted(false), + bloodlust_value(0), + quest_counter(0) + { + } + + inline unsigned rand(unsigned x, unsigned y) + { + return(std::uniform_int_distribution(x, y)(re)); + } + + inline unsigned flip() + { + return(this->rand(0,1)); + } + + template + inline T random_in_vector(const std::vector& v) + { + assert(v.size() > 0); + return(v[this->rand(0, v.size() - 1)]); + } + + template + inline unsigned make_selection_array(CardsIter first, CardsIter last, Functor f); + inline const std::vector adjacent_assaults(const CardStatus * status); + inline void print_selection_array(); + + inline void inc_counter(QuestType::QuestType quest_type, unsigned quest_key, unsigned quest_2nd_key = 0, unsigned value = 1) + { + if (quest.quest_type == quest_type && quest.quest_key == quest_key && (quest.quest_2nd_key == 0 || quest.quest_2nd_key == quest_2nd_key)) + { + quest_counter += value; + } + } +}; + +#endif diff --git a/tyrant.cpp b/tyrant.cpp new file mode 100644 index 00000000..ef532eb9 --- /dev/null +++ b/tyrant.cpp @@ -0,0 +1,52 @@ +#include "tyrant.h" + +#include + +const std::string faction_names[Faction::num_factions] = +{ "", "imperial", "raider", "bloodthirsty", "xeno", "righteous", "progenitor" }; + +std::string skill_names[Skill::num_skills] = +{ + // Placeholder for no-skill: + "", + // Attack: + "0", + // Activation: + "", "", + "Enfeeble", "Jam", "Mortar", "Siege", "Strike", "Sunder", "Weaken", + "", + "", + "Enhance", "Evolve", "Heal", "Mend", "Overload", "Protect", "Rally", "Rush", + "", "", + // Defensive: + "", + "Armor", "Avenge", "Corrosive", "Counter", "Evade", "Payback", "Refresh", "Wall", + "", + // Combat-Modifier: + "Legion", "Pierce", "Rupture", "Swipe", "Venom", + // Damage-Dependant: + "Berserk", "Inhibit", "Leech", "Poison", + // Triggered: + "Allegiance", "Flurry", "Valor", + // Pseudo-skill for passive BGEs: + "", + "Bloodlust", "Brigade", "Counterflux", "Divert", "EnduringRage", "Fortification", "Heroism", "Metamorphosis", "Revenge", "TurningTides", "Virulence", + "", +}; + +std::string cardtype_names[CardType::num_cardtypes]{"Commander", "Assault", "Structure", }; + +std::string rarity_names[6]{"", "common", "rare", "epic", "legend", "vindi", }; + +unsigned upgrade_cost[]{0, 5, 15, 30, 75, 150}; +unsigned salvaging_income[][7]{{}, {0, 1, 2, 5}, {0, 5, 10, 15, 20}, {0, 20, 25, 30, 40, 50, 65}, {0, 40, 45, 60, 75, 100, 125}, {0, 80, 85, 100, 125, 175, 250}}; + +signed min_possible_score[]{0, 0, 0, 10, 5, 5, 0, 0}; +signed max_possible_score[]{100, 100, 100, 100, 67, 100, 100, 100}; + +std::string decktype_names[DeckType::num_decktypes]{"Deck", "Mission", "Raid", "Campaign", "Custom Deck", }; + +signed debug_print(0); +unsigned debug_cached(0); +bool debug_line(false); +std::string debug_str(""); diff --git a/tyrant.h b/tyrant.h new file mode 100644 index 00000000..6cd529c0 --- /dev/null +++ b/tyrant.h @@ -0,0 +1,200 @@ +#ifndef TYRANT_H_INCLUDED +#define TYRANT_H_INCLUDED + +#define TYRANT_OPTIMIZER_VERSION "2.19.2" + +#include +#include +#include +#include +#include + +enum Faction +{ + allfactions, + imperial, + raider, + bloodthirsty, + xeno, + righteous, + progenitor, + num_factions +}; +extern const std::string faction_names[num_factions]; + +enum Skill +{ + // Placeholder for no-skill: + no_skill, + // Attack: + attack, + // Activation: + BEGIN_ACTIVATION, BEGIN_ACTIVATION_HARMFUL, // TODO skill traits + enfeeble, jam, mortar, siege, strike, sunder, weaken, + END_ACTIVATION_HARMFUL, + BEGIN_ACTIVATION_HELPFUL, + enhance, evolve, heal, mend, overload, protect, rally, rush, + END_ACTIVATION_HELPFUL, END_ACTIVATION, + // Defensive: + BEGIN_DEFENSIVE, + armor, avenge, corrosive, counter, evade, payback, refresh, wall, + END_DEFENSIVE, + // Combat-Modifier: + legion, pierce, rupture, swipe, venom, + // Damage-Dependent: + berserk, inhibit, leech, poison, + // Triggered: + allegiance, flurry, valor, + // Pseudo-Skill for BGE: + BEGIN_BGE_SKILL, + bloodlust, brigade, counterflux, divert, enduringrage, fortification, heroism, metamorphosis, revenge, turningtides, virulence, + END_BGE_SKILL, + num_skills +}; +extern std::string skill_names[num_skills]; + +namespace CardType { +enum CardType { + commander, + assault, + structure, + num_cardtypes +}; +} + +extern std::string cardtype_names[CardType::num_cardtypes]; + +extern std::string rarity_names[]; + +extern unsigned upgrade_cost[]; +extern unsigned salvaging_income[][7]; + +namespace DeckType { +enum DeckType { + deck, + mission, + raid, + campaign, + custom_deck, + num_decktypes +}; +} + +extern std::string decktype_names[DeckType::num_decktypes]; + +enum gamemode_t +{ + fight, + surge, +}; + +namespace QuestType +{ +enum QuestType +{ + none, + skill_use, + skill_damage, + faction_assault_card_use, + type_card_use, + faction_assault_card_kill, + type_card_kill, + card_survival, + num_objective_types +}; +} + +enum class OptimizationMode +{ + notset, + winrate, + defense, + war, + brawl, + raid, + campaign, + quest, + num_optimization_mode +}; + +extern signed min_possible_score[(size_t)OptimizationMode::num_optimization_mode]; +extern signed max_possible_score[(size_t)OptimizationMode::num_optimization_mode]; + +struct true_ {}; + +struct false_ {}; + +template +struct skillTriggersRegen { typedef false_ T; }; + +template<> +struct skillTriggersRegen { typedef true_ T; }; + +template<> +struct skillTriggersRegen { typedef true_ T; }; + +enum SkillSourceType +{ + source_hostile, + source_allied, + source_global_hostile, + source_global_allied, + source_chaos +}; + +struct SkillSpec +{ + Skill id; + unsigned x; + Faction y; + unsigned n; + unsigned c; + Skill s; + Skill s2; + bool all; +}; + +// -------------------------------------------------------------------------------- +// Common functions +template +std::string to_string(const T val) +{ + std::stringstream s; + s << val; + return s.str(); +} + +//---------------------- Debugging stuff --------------------------------------- +extern signed debug_print; +extern unsigned debug_cached; +extern bool debug_line; +extern std::string debug_str; +#ifndef NDEBUG +#define _DEBUG_MSG(v, format, args...) \ + { \ + if(__builtin_expect(debug_print >= v, false)) \ + { \ + if(debug_line) { printf("%i - " format, __LINE__ , ##args); } \ + else if(debug_cached) { \ + char buf[4096]; \ + snprintf(buf, sizeof(buf), format, ##args); \ + debug_str += buf; \ + } \ + else { printf(format, ##args); } \ + std::cout << std::flush; \ + } \ + } +#define _DEBUG_SELECTION(format, args...) \ + { \ + if(__builtin_expect(debug_print >= 2, 0)) \ + { \ + _DEBUG_MSG(2, "Possible targets of " format ":\n", ##args); \ + fd->print_selection_array(); \ + } \ + } +#else +#define _DEBUG_MSG(v, format, args...) +#define _DEBUG_SELECTION(format, args...) +#endif + +#endif diff --git a/tyrant_optimize.cpp b/tyrant_optimize.cpp index f25bc189..4e3ee05c 100644 --- a/tyrant_optimize.cpp +++ b/tyrant_optimize.cpp @@ -14,4009 +14,2063 @@ //#define NDEBUG #define BOOST_THREAD_USE_LIB #include +#include #include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include #include #include -#include +#include +#include #include -#include // because of 1.51 bug. missing include in range/any_range.hpp ? -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include -#include +#include #include -#include -#include +#include #include -#include -#include -#include "rapidxml.hpp" -//#include "timer.hpp" +#include +#include +#include "card.h" +#include "cards.h" +#include "deck.h" +#include "read.h" +#include "sim.h" +#include "tyrant.h" +#include "xml.h" -using namespace rapidxml; -using namespace std::placeholders; -namespace range = boost::range; -//---------------------- $00 general stuff ------------------------------------- -template -std::string to_string(T val) +struct Requirement { - std::stringstream s; - s << val; - return s.str(); + std::unordered_map num_cards; +}; + +namespace { + gamemode_t gamemode{fight}; + OptimizationMode optimization_mode{OptimizationMode::notset}; + std::map owned_cards; + bool use_owned_cards{true}; + unsigned min_deck_len{1}; + unsigned max_deck_len{10}; + unsigned freezed_cards{0}; + unsigned fund{0}; + long double target_score{100}; + long double min_increment_of_score{0}; + long double confidence_level{0.99}; + bool use_top_level_card{false}; + unsigned use_fused_card_level{0}; + bool show_ci{false}; + bool use_harmonic_mean{false}; + unsigned sim_seed{0}; + Requirement requirement; + Quest quest; } -template -void partial_shuffle(RandomAccessIterator first, RandomAccessIterator middle, - RandomAccessIterator last, - UniformRandomNumberGenerator&& g) +using namespace std::placeholders; +//------------------------------------------------------------------------------ +std::string card_id_name(const Card* card) { - typedef typename std::iterator_traits::difference_type diff_t; - typedef typename std::make_unsigned::type udiff_t; - typedef typename std::uniform_int_distribution distr_t; - typedef typename distr_t::param_type param_t; - - distr_t D; - diff_t m = middle - first; - diff_t n = last - first; - for (diff_t i = 0; i < m; ++i) + std::stringstream ios; + if(card) { - std::swap(first[i], first[D(g, param_t(i, n-1))]); + ios << "[" << card->m_id << "] " << card->m_name; } + else + { + ios << "-void-"; + } + return ios.str(); } - -namespace boost -{ -namespace range -{ -template -void shuffle(Range& range, UniformRandomNumberGenerator&& rand) -{ - std::shuffle(boost::begin(range), boost::end(range), rand); -} -} // namespace range -using range::shuffle; -} // namespace boost -//---------------------- Debugging stuff --------------------------------------- -bool debug_print(false); -bool debug_line(false); -#ifndef NDEBUG -#define _DEBUG_MSG(format, args...) \ - { \ - if(debug_print) \ - { \ - char* format2 = new char[strlen(format) + (8 + 1)*sizeof(char)]; \ - if(debug_line) { strcpy(format2, "%i - "); } \ - else { strcpy(format2, ""); } \ - strcat(format2, format); \ - if(debug_line) { printf(format2, __LINE__ , ##args); } \ - else { printf(format2, ##args); } \ - delete[] format2; \ - std::cout << std::flush; \ - } \ - } -#else -#define _DEBUG_MSG(format, args...) -#endif -// Pool-based indexed storage. -//---------------------- Pool-based indexed storage ---------------------------- -template -class Storage +std::string card_slot_id_names(const std::vector> card_list) { -public: - typedef typename std::vector::size_type size_type; - typedef T value_type; - Storage(size_type size) : - m_pool(sizeof(T)) + if (card_list.empty()) { - m_indirect.reserve(size); + return "-void-"; } - - inline T& operator[](size_type i) + std::stringstream ios; + std::string separator = ""; + for (const auto & card_it : card_list) { - return(*m_indirect[i]); + ios << separator; + separator = ", "; + if (card_it.first >= 0) + { ios << card_it.first << " "; } + ios << "[" << card_it.second->m_id << "] " << card_it.second->m_name; } - - inline T& add_back() + return ios.str(); +} +//------------------------------------------------------------------------------ +Deck* find_deck(Decks& decks, const Cards& all_cards, std::string deck_name) +{ + Deck* deck = decks.find_deck_by_name(deck_name); + if (deck != nullptr) { - m_indirect.emplace_back((T*) m_pool.malloc()); - return(*m_indirect.back()); + deck->resolve(); + return(deck); } - - template - void remove(Pred p) + decks.decks.emplace_back(Deck{all_cards}); + deck = &decks.decks.back(); + deck->set(deck_name); + deck->resolve(); + return(deck); +} +//---------------------- $80 deck optimization --------------------------------- +unsigned get_required_cards_before_upgrade(const std::vector & card_list, std::map & num_cards) +{ + unsigned deck_cost = 0; + std::set unresolved_cards; + for (const Card * card : card_list) + { + ++ num_cards[card]; + unresolved_cards.insert(card); + } + // un-upgrade only if fund is used + while (fund > 0 && !unresolved_cards.empty()) { - size_type head(0); - for(size_type current(0); current < m_indirect.size(); ++current) + auto card_it = unresolved_cards.end(); + auto card = *(-- card_it); + unresolved_cards.erase(card_it); + if ((use_fused_card_level > 0 && card->m_set == 1000 && card->m_rarity <= 2 && card->m_level == 1) || // assume unlimited common/rare level-1 cards (standard set) under endgame 1|2 + (owned_cards[card->m_id] < num_cards[card] && !card->m_recipe_cards.empty())) { - if(p((*this)[current])) + unsigned num_under = num_cards[card] - owned_cards[card->m_id]; + num_cards[card] = owned_cards[card->m_id]; +// std::cout << "-" << num_under << " " << card->m_name << "\n"; // XXX + deck_cost += num_under * card->m_recipe_cost; + for (auto recipe_it : card->m_recipe_cards) { - m_pool.free(m_indirect[current]); - } - else - { - if(current != head) - { - m_indirect[head] = m_indirect[current]; - } - ++head; + num_cards[recipe_it.first] += num_under * recipe_it.second; +// std::cout << "+" << num_under * recipe_it.second << " " << recipe_it.first->m_name << "\n"; // XXX + unresolved_cards.insert(recipe_it.first); } } - m_indirect.erase(m_indirect.begin() + head, m_indirect.end()); } +// std::cout << "\n"; // XXX + return deck_cost; +} - void reset() +unsigned get_deck_cost(const Deck * deck) +{ + if (!use_owned_cards) + { return 0; } + std::map num_in_deck; + unsigned deck_cost = get_required_cards_before_upgrade({deck->commander}, num_in_deck); + deck_cost += get_required_cards_before_upgrade(deck->cards, num_in_deck); + for(auto it: num_in_deck) { - for(auto index: m_indirect) + unsigned card_id = it.first->m_id; + if (it.second > owned_cards[card_id]) { - m_pool.free(index); + return UINT_MAX; } - m_indirect.clear(); - } - - inline size_type size() const - { - return(m_indirect.size()); } - - std::vector m_indirect; - boost::pool<> m_pool; -}; -//--------------------- $10 data model: card properties, etc ------------------- -enum Faction -{ - allfactions, - bloodthirsty, - imperial, - raider, - righteous, - xeno, - num_factions -}; -const std::string faction_names[num_factions] = -{ "", "bloodthirsty", "imperial", "raider", "righteous", "xeno" }; - -enum ActiveSkill -{augment, augment_all, chaos, chaos_all, cleanse, cleanse_all, enfeeble, enfeeble_all, - freeze, freeze_all, heal, heal_all, infuse, jam, jam_all, - mimic, protect, protect_all, rally, rally_all, rush, shock, - siege, siege_all, strike, strike_all, summon, supply, - trigger_regen, // not actually a skill; handles regeneration after strike/siege - weaken, weaken_all, num_skills}; -std::string skill_names[num_skills] = -{"augment", "augment_all", "chaos", "chaos_all", "cleanse", "cleanse_all", "enfeeble", "enfeeble_all", - "freeze", "freeze_all", "heal", "heal_all", "infuse", "jam", "jam_all", - "mimic", "protect", "protect_all", "rally", "rally_all", "rush", "shock", - "siege", "siege_all", "strike", "strike_all", "summon", "supply", - "trigger_regen", - "weaken", "weaken_all"}; - -namespace CardType { -enum CardType { - action, - assault, - commander, - structure, - num_cardtypes -}; + return deck_cost; } -std::string cardtype_names[CardType::num_cardtypes]{"action", "assault", "commander", "structure"}; - -enum gamemode_t -{ - fight, - surge, - tournament -}; -struct true_ {}; - -struct false_ {}; - -template -struct skillTriggersRegen { typedef false_ T; }; - -template<> -struct skillTriggersRegen { typedef true_ T; }; - -template<> -struct skillTriggersRegen { typedef true_ T; }; - -template<> -struct skillTriggersRegen { typedef true_ T; }; - -template<> -struct skillTriggersRegen { typedef true_ T; }; - -enum SkillSourceType -{ - source_hostile, - source_allied, - source_global_hostile, - source_global_allied, - source_chaos -}; - -typedef std::tuple SkillSpec; - -class Card -{ -public: - Card() : - m_antiair(0), - m_armored(0), - m_attack(0), - m_berserk(0), - m_berserk_oa(0), - m_blitz(false), - m_burst(0), - m_counter(0), - m_crush(0), - m_delay(0), - m_disease(false), - m_disease_oa(false), - m_evade(false), - m_faction(imperial), - m_fear(false), - m_flurry(0), - m_flying(false), - m_health(0), - m_id(0), - m_immobilize(false), - m_intercept(false), - m_leech(0), - m_name(""), - m_payback(false), - m_pierce(0), - m_poison(0), - m_poison_oa(0), - m_rarity(1), - m_recharge(false), - m_refresh(false), - m_regenerate(0), - m_set(0), - m_siphon(0), - m_split(false), - m_swipe(false), - m_tribute(false), - m_unique(false), - m_valor(0), - m_wall(false), - m_type(CardType::assault), - m_skills() - { - } - - void add_skill(ActiveSkill v1, unsigned v2, Faction v3) - { m_skills.push_back(std::make_tuple(v1, v2, v3)); } - void add_played_skill(ActiveSkill v1, unsigned v2, Faction v3) - { m_skills_played.push_back(std::make_tuple(v1, v2, v3)); } - void add_died_skill(ActiveSkill v1, unsigned v2, Faction v3) - { m_skills_died.push_back(std::make_tuple(v1, v2, v3)); } - void add_attacked_skill(ActiveSkill v1, unsigned v2, Faction v3) - { m_skills_attacked.push_back(std::make_tuple(v1, v2, v3)); } - - unsigned m_antiair; - unsigned m_armored; - unsigned m_attack; - unsigned m_berserk; - unsigned m_berserk_oa; - bool m_blitz; - unsigned m_burst; - unsigned m_counter; - unsigned m_crush; - unsigned m_delay; - bool m_disease; - bool m_disease_oa; - bool m_evade; - Faction m_faction; - bool m_fear; - unsigned m_flurry; - bool m_flying; - unsigned m_health; - unsigned m_id; - bool m_immobilize; - bool m_intercept; - unsigned m_leech; - std::string m_name; - bool m_payback; - unsigned m_pierce; - unsigned m_poison; - unsigned m_poison_oa; - unsigned m_rarity; - bool m_recharge; - bool m_refresh; - unsigned m_regenerate; - unsigned m_set; - unsigned m_siphon; - bool m_split; - bool m_swipe; - bool m_tribute; - bool m_unique; - unsigned m_valor; - bool m_wall; - std::vector m_skills; - std::vector m_skills_played; - std::vector m_skills_died; - std::vector m_skills_attacked; - CardType::CardType m_type; -}; - -struct Cards +// remove val from oppo if found, otherwise append val to self +template +void append_unless_remove(C & self, C & oppo, typename C::const_reference val) { - ~Cards() + for (auto it = oppo.begin(); it != oppo.end(); ++ it) { - for(Card* c: cards) { delete(c); } + if (*it == val) + { + oppo.erase(it); + return; + } } + self.push_back(val); +} - std::vector cards; - std::map cards_by_id; - std::vector player_cards; - std::map player_cards_by_name; - std::vector player_commanders; - std::vector player_assaults; - std::vector player_structures; - std::vector player_actions; - std::map replace; - const Card * by_id(unsigned id) const; - void organize(); -}; -Cards globalCards; -//------------------------------------------------------------------------------ -struct CardStatus +// insert card at to_slot into deck limited by fund; store deck_cost +// return true if affordable +bool adjust_deck(Deck * deck, const signed from_slot, const signed to_slot, const Card * card, unsigned fund, std::mt19937 & re, unsigned & deck_cost, + std::vector> & cards_out, std::vector> & cards_in) { - const Card* m_card; - unsigned m_index; - unsigned m_player; - unsigned m_augmented; - unsigned m_berserk; - bool blitz; - bool m_chaos; - unsigned m_delay; - bool m_diseased; - unsigned m_enfeebled; - Faction m_faction; - bool m_frozen; - unsigned m_hp; - bool m_immobilized; - bool m_infused; - std::vector infused_skills; - bool m_jammed; - unsigned m_poisoned; - unsigned m_protected; - unsigned m_rallied; - unsigned m_weakened; - - CardStatus() {} - - CardStatus(const Card* card) : - m_card(card), - m_index(0), - m_player(0), - m_augmented(0), - m_berserk(0), - blitz(false), - m_chaos(false), - m_delay(card->m_delay), - m_diseased(false), - m_enfeebled(0), - m_faction(card->m_faction), - m_frozen(false), - m_hp(card->m_health), - m_immobilized(false), - m_infused(false), - m_jammed(false), - m_poisoned(0), - m_protected(0), - m_rallied(0), - m_weakened(0) - { - } - - inline void set(const Card* card) - { - this->set(*card); - } - - inline void set(const Card& card) - { - m_card = &card; - m_index = 0; - m_player = 0; - m_augmented = 0; - m_berserk = 0; - blitz = false; - m_chaos = false; - m_delay = card.m_delay; - m_diseased = false; - m_enfeebled = 0; - m_faction = card.m_faction; - m_frozen = false; - m_hp = card.m_health; - m_immobilized = false; - infused_skills.clear(); - m_infused = false; - m_jammed = false; - m_poisoned = 0; - m_protected = 0; - m_rallied = 0; - m_weakened = 0; + cards_in.clear(); + if (card == nullptr) + { // change commander or remove card + if (to_slot < 0) + { // change commander + cards_in.emplace_back(-1, deck->commander); + } + deck_cost = get_deck_cost(deck); + return (deck_cost <= fund); } -}; -//------------------------------------------------------------------------------ -std::string skill_description(const SkillSpec& s) -{ - return(skill_names[std::get<0>(s)] + - (std::get<2>(s) == allfactions ? "" : std::string(" ") + faction_names[std::get<2>(s)]) + - (std::get<1>(s) == 0 ? "" : std::string(" ") + to_string(std::get<1>(s)))); -} -//------------------------------------------------------------------------------ -std::string status_description(CardStatus* status) -{ - assert(status); - std::string desc; - switch(status->m_card->m_type) + bool is_random = deck->strategy == DeckStrategy::random; + std::vector cards = deck->cards; + card = card->m_top_level_card; { - case CardType::commander: desc = "Commander "; break; - case CardType::action: desc = "Action "; break; - case CardType::assault: desc = "A " + to_string(status->m_index) + " "; break; - case CardType::structure: desc = "S " + to_string(status->m_index) + " "; break; + // try to add new card into the deck, unfuse/downgrade it if necessary + std::stack candidate_cards; + candidate_cards.emplace(card); + while (! candidate_cards.empty()) + { + const Card* card_in = candidate_cards.top(); + candidate_cards.pop(); + deck->cards.clear(); + deck->cards.emplace_back(card_in); + deck_cost = get_deck_cost(deck); + if (use_top_level_card || deck_cost <= fund) + { break; } + for (auto recipe_it : card_in->m_recipe_cards) + { candidate_cards.emplace(recipe_it.first); } + } + if (deck_cost > fund) + { + return false; + } + cards_in.emplace_back(is_random ? -1 : to_slot, deck->cards[0]); } - desc += "[" + status->m_card->m_name + "]"; - return(desc); -} -//---------------------- $20 cards.xml parsing --------------------------------- -// Sets: 1 enclave; 2 nexus; 3 blight; 4 purity; 5 homeworld; -// 6 phobos; 7 phobos aftermath; 8 awakening -// 1000 standard; 5000 rewards; 5001 promotional; 9000 exclusive -// mission only and test cards have no set -using namespace rapidxml; - -std::map sets_counts; - -Faction map_to_faction(unsigned i) -{ - return(i == 1 ? imperial : - i == 3 ? bloodthirsty : - i == 4 ? xeno : - i == 8 ? righteous : - i == 9 ? raider : - allfactions); -} - -Faction skill_faction(xml_node<>* skill) -{ - unsigned unmapped_faction(0); - xml_attribute<>* y(skill->first_attribute("y")); - if(y) { - unmapped_faction = atoi(y->value()); + // try to add commander into the deck, unfuse/downgrade it if necessary + std::stack candidate_cards; + const Card * old_commander = deck->commander; + candidate_cards.emplace(deck->commander); + while (! candidate_cards.empty()) + { + const Card* card_in = candidate_cards.top(); + candidate_cards.pop(); + deck->commander = card_in; + deck_cost = get_deck_cost(deck); + if (deck_cost <= fund) + { break; } + for (auto recipe_it : card_in->m_recipe_cards) + { candidate_cards.emplace(recipe_it.first); } + } + if (deck_cost > fund) + { + deck->commander = old_commander; + return false; + } + else if (deck->commander != old_commander) + { + append_unless_remove(cards_out, cards_in, {-1, old_commander}); + append_unless_remove(cards_in, cards_out, {-1, deck->commander}); + } } - return(unmapped_faction == 0 ? allfactions : map_to_faction(unmapped_faction)); -} - -unsigned skill_value(xml_node<>* skill) -{ - unsigned value(0); - xml_attribute<>* x(skill->first_attribute("x")); - if(x) + if (is_random) + { std::shuffle(cards.begin(), cards.end(), re); } + for (signed i = 0; i < (signed)cards.size(); ++ i) { - value = atoi(x->value()); + // try to add cards[i] into the deck, unfuse/downgrade it if necessary + auto saved_cards = deck->cards; + auto in_it = deck->cards.end() - (i < to_slot); + in_it = deck->cards.insert(in_it, nullptr); + std::stack candidate_cards; + candidate_cards.emplace(cards[i]); + while (! candidate_cards.empty()) + { + const Card* card_in = candidate_cards.top(); + candidate_cards.pop(); + *in_it = card_in; + deck_cost = get_deck_cost(deck); + if (use_top_level_card || deck_cost <= fund) + { break; } + if (i < (signed)freezed_cards) + { return false; } + for (auto recipe_it : card_in->m_recipe_cards) + { candidate_cards.emplace(recipe_it.first); } + } + if (deck_cost > fund) + { + append_unless_remove(cards_out, cards_in, {is_random ? -1 : i + (i >= to_slot), cards[i]}); + deck->cards = saved_cards; + } + else if (*in_it != cards[i]) + { + append_unless_remove(cards_out, cards_in, {is_random ? -1 : i + (i >= from_slot), cards[i]}); + append_unless_remove(cards_in, cards_out, {is_random ? -1 : i + (i >= to_slot), *in_it}); + } } - return(value); -} - -template -struct GlobalSkill -{ - enum { type = 99 }; -}; - -template<> struct GlobalSkill { enum {type = augment_all}; }; -template<> struct GlobalSkill { enum {type = chaos_all}; }; -template<> struct GlobalSkill { enum {type = cleanse_all}; }; -template<> struct GlobalSkill { enum {type = enfeeble_all}; }; -template<> struct GlobalSkill { enum {type = freeze_all}; }; -template<> struct GlobalSkill { enum {type = heal_all}; }; -template<> struct GlobalSkill { enum {type = jam_all}; }; -template<> struct GlobalSkill { enum {type = protect_all}; }; -template<> struct GlobalSkill { enum {type = rally_all}; }; -template<> struct GlobalSkill { enum {type = siege_all}; }; -template<> struct GlobalSkill { enum {type = strike_all}; }; -template<> struct GlobalSkill { enum {type = weaken_all}; }; - -template -bool handle_global_skill(xml_node<>* node, Card* card) -{ - bool played(node->first_attribute("played")); - bool died(node->first_attribute("died")); - bool attacked(node->first_attribute("attacked")); - if(node->first_attribute("all")) - { - if(played) {card->add_played_skill(ActiveSkill(GlobalSkill), skill_value(node), skill_faction(node)); } - else if(died) {card->add_died_skill(ActiveSkill(GlobalSkill), skill_value(node), skill_faction(node)); } - else if(attacked) {card->add_attacked_skill(ActiveSkill(GlobalSkill), skill_value(node), skill_faction(node)); } - else {card->add_skill(ActiveSkill(GlobalSkill), skill_value(node), skill_faction(node)); } - return(true); - } - return(false); -} - -template<> -bool handle_global_skill<99>(xml_node<>* node, Card* card) -{ - return(false); + deck_cost = get_deck_cost(deck); + return !cards_in.empty() || !cards_out.empty(); } -template -void handle_skill(xml_node<>* node, Card* card) -{ - bool played(node->first_attribute("played")); - bool died(node->first_attribute("died")); - bool attacked(node->first_attribute("attacked")); - if(handle_global_skill::type>(node, card)) {} - else - { - if(played) {card->add_played_skill(ActiveSkill(Skill), skill_value(node), skill_faction(node)); } - else if(died) {card->add_died_skill(ActiveSkill(Skill), skill_value(node), skill_faction(node)); } - else if(attacked) {card->add_attacked_skill(ActiveSkill(Skill), skill_value(node), skill_faction(node)); } - else {card->add_skill(ActiveSkill(Skill), skill_value(node), skill_faction(node)); } - } -} -//------------------------------------------------------------------------------ -const Card* Cards::by_id(unsigned id) const +unsigned check_requirement(const Deck* deck, const Requirement & requirement, const Quest & quest) { - std::map::const_iterator cardIter{cards_by_id.find(id)}; - if(cardIter == cards_by_id.end()) + unsigned gap = 0; + if (!requirement.num_cards.empty()) { - throw std::runtime_error("While trying to find the card with id " + to_string(id) + ": no such key in the cards_by_id map."); + std::unordered_map num_cards; + num_cards[deck->commander] = 1; + for (auto card: deck->cards) + { + ++ num_cards[card]; + } + for (auto it: requirement.num_cards) + { + gap += safe_minus(it.second, num_cards[it.first]); + } } - else + if (quest.quest_type != QuestType::none) { - return(cardIter->second); - } -} -//------------------------------------------------------------------------------ -void Cards::organize() -{ - cards_by_id.clear(); - player_cards.clear(); - player_cards_by_name.clear(); - player_commanders.clear(); - player_assaults.clear(); - player_structures.clear(); - player_actions.clear(); - for(Card* card: cards) - { - cards_by_id[card->m_id] = card; - // Card available to players - if(card->m_set != -1) - { - player_cards.push_back(card); - switch(card->m_type) - { - case CardType::commander: { - player_commanders.push_back(card); - break; - } - case CardType::assault: { - player_assaults.push_back(card); + unsigned potential_value = 0; + switch (quest.quest_type) + { + case QuestType::skill_use: + case QuestType::skill_damage: + for (const auto & ss: deck->commander->m_skills) + { + if (quest.quest_key == ss.id) + { + potential_value = quest.quest_value; + break; + } + } break; - } - case CardType::structure: { - player_structures.push_back(card); + case QuestType::faction_assault_card_kill: + case QuestType::type_card_kill: + potential_value = quest.quest_value; break; - } - case CardType::action: { - player_actions.push_back(card); + default: break; - } - } - if(player_cards_by_name.find(card->m_name) != player_cards_by_name.end()) + } + for (auto card: deck->cards) + { + switch (quest.quest_type) { - throw std::runtime_error("While trying to insert the card [" + card->m_name + ", id " + to_string(card->m_id) + "] in the player_cards_by_name map: the key already exists [id " + to_string(player_cards_by_name[card->m_name]->m_id) + "]."); + case QuestType::skill_use: + case QuestType::skill_damage: + for (const auto & ss: card->m_skills) + { + if (quest.quest_key == ss.id) + { + potential_value = quest.quest_value; + break; + } + } + break; + case QuestType::faction_assault_card_use: + potential_value += (quest.quest_key == card->m_faction); + break; + case QuestType::type_card_use: + potential_value += (quest.quest_key == card->m_type); + break; + default: + break; } - else + if (potential_value >= (quest.must_fulfill ? quest.quest_value : 1)) { - player_cards_by_name[card->m_name] = card; + break; } } + gap += safe_minus(quest.must_fulfill ? quest.quest_value : 1, potential_value); } + return gap; } -//------------------------------------------------------------------------------ -void parse_file(const char* filename, std::vector& buffer, xml_document<>& doc) + +void claim_cards(const std::vector & card_list) { - std::ifstream cards_stream(filename, std::ios::binary); - // Get the size of the file - cards_stream.seekg(0,std::ios::end); - std::streampos length = cards_stream.tellg(); - cards_stream.seekg(0,std::ios::beg); - buffer.resize(length + std::streampos(1)); - cards_stream.read(&buffer[0],length); - // zero-terminate - buffer[length] = '\0'; - try - { - doc.parse<0>(&buffer[0]); - } - catch(rapidxml::parse_error& e) + std::map num_cards; + get_required_cards_before_upgrade(card_list, num_cards); + for(const auto & it: num_cards) { - std::cout << "Parse error exception.\n"; - std::cout << e.what(); - throw(e); + const Card * card = it.first; + unsigned num_to_claim = safe_minus(it.second, owned_cards[card->m_id]); + if(num_to_claim > 0) + { + owned_cards[card->m_id] += num_to_claim; + if (debug_print >= 0) + { + std::cerr << "WARNING: Need extra " << num_to_claim << " " << card->m_name << " to build your initial deck: adding to owned card list.\n"; + } + } } } + //------------------------------------------------------------------------------ -void read_cards(Cards& cards) +FinalResults compute_score(const EvaluatedResults& results, std::vector& factors) { - std::vector buffer; - xml_document<> doc; - parse_file("cards.xml", buffer, doc); - xml_node<>* root = doc.first_node(); - bool mission_only(false); - unsigned nb_cards(0); - for(xml_node<>* card = root->first_node(); - card; - card = card->next_sibling()) - { - if(strcmp(card->name(), "unit") == 0) - { - xml_node<>* id_node(card->first_node("id")); - int id(id_node ? atoi(id_node->value()) : -1); - // Replacement art card - xml_node<>* replace_node(card->first_node("replace")); - if(replace_node) - { - cards.replace[id] = atoi(replace_node->value()); - continue; - } - xml_node<>* name_node(card->first_node("name")); - xml_node<>* attack_node(card->first_node("attack")); - xml_node<>* health_node(card->first_node("health")); - xml_node<>* cost_node(card->first_node("cost")); - xml_node<>* unique_node(card->first_node("unique")); - xml_node<>* rarity_node(card->first_node("rarity")); - xml_node<>* type_node(card->first_node("type")); - xml_node<>* set_node(card->first_node("set")); - int set(set_node ? atoi(set_node->value()) : -1); - mission_only = set == -1; - if((mission_only || set >= 0) && name_node && rarity_node) - { - if(!mission_only) - { - nb_cards++; - sets_counts[set]++; - } - Card* c(new Card()); - c->m_id = id; - c->m_name = name_node->value(); - if(id < 1000) - { c->m_type = CardType::assault; } - else if(id < 2000) - { c->m_type = CardType::commander; } - else if(id < 3000) - { c->m_type = CardType::structure; } - else - { c->m_type = CardType::action; } - if(attack_node) { c->m_attack = atoi(attack_node->value()); } - if(health_node) { c->m_health = atoi(health_node->value()); } - if(cost_node) { c->m_delay = atoi(cost_node->value()); } - if(unique_node) { c->m_unique = true; } - c->m_rarity = atoi(rarity_node->value()); - unsigned type(type_node ? atoi(type_node->value()) : 0); - c->m_faction = map_to_faction(type); - c->m_set = set; - for(xml_node<>* skill = card->first_node("skill"); skill; - skill = skill->next_sibling("skill")) - { - if(strcmp(skill->first_attribute("id")->value(), "antiair") == 0) - { c->m_antiair = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "armored") == 0) - { c->m_armored = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "augment") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "berserk") == 0) - { - bool attacked(skill->first_attribute("attacked")); - if(attacked) { c->m_berserk_oa = atoi(skill->first_attribute("x")->value()); } - else {c->m_berserk = atoi(skill->first_attribute("x")->value()); } - } - if(strcmp(skill->first_attribute("id")->value(), "blitz") == 0) - { c->m_blitz = true; } - if(strcmp(skill->first_attribute("id")->value(), "burst") == 0) - { c->m_burst = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "counter") == 0) - { c->m_counter = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "crush") == 0) - { c->m_crush = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "disease") == 0) - { - bool attacked(skill->first_attribute("attacked")); - if(attacked) { c->m_disease_oa = true; } - else {c->m_disease = true; } - } - if(strcmp(skill->first_attribute("id")->value(), "evade") == 0) - { c->m_evade = true; } - if(strcmp(skill->first_attribute("id")->value(), "fear") == 0) - { c->m_fear = true; } - if(strcmp(skill->first_attribute("id")->value(), "flurry") == 0) - { c->m_flurry = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "flying") == 0) - { c->m_flying = true; } - if(strcmp(skill->first_attribute("id")->value(), "immobilize") == 0) - { c->m_immobilize = true; } - if(strcmp(skill->first_attribute("id")->value(), "intercept") == 0) - { c->m_intercept = true; } - if(strcmp(skill->first_attribute("id")->value(), "leech") == 0) - { c->m_leech = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "payback") == 0) - { c->m_payback = true; } - if(strcmp(skill->first_attribute("id")->value(), "pierce") == 0) - { c->m_pierce = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "poison") == 0) - { - bool attacked(skill->first_attribute("attacked")); - if(attacked) { c->m_poison_oa = atoi(skill->first_attribute("x")->value()); } - else {c->m_poison = atoi(skill->first_attribute("x")->value()); } - } - if(strcmp(skill->first_attribute("id")->value(), "recharge") == 0) - { c->m_recharge = true; } - if(strcmp(skill->first_attribute("id")->value(), "refresh") == 0) - { c->m_refresh = true; } - if(strcmp(skill->first_attribute("id")->value(), "regenerate") == 0) - { c->m_regenerate = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "siphon") == 0) - { c->m_siphon = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "split") == 0) - { c->m_split = true; } - if(strcmp(skill->first_attribute("id")->value(), "swipe") == 0) - { c->m_swipe = true; } - if(strcmp(skill->first_attribute("id")->value(), "tribute") == 0) - { c->m_tribute = true; } - if(strcmp(skill->first_attribute("id")->value(), "valor") == 0) - { c->m_valor = atoi(skill->first_attribute("x")->value()); } - if(strcmp(skill->first_attribute("id")->value(), "wall") == 0) - { c->m_wall = true; } - if(strcmp(skill->first_attribute("id")->value(), "chaos") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "cleanse") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "enfeeble") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "freeze") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "heal") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "infuse") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "jam") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "mimic") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "protect") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "rally") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "rush") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "shock") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "siege") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "strike") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "summon") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "supply") == 0) - { handle_skill(skill, c); } - if(strcmp(skill->first_attribute("id")->value(), "weaken") == 0) - { handle_skill(skill, c); } - } - cards.cards.push_back(c); - } + FinalResults final{0, 0, 0, 0, 0, 0, results.second}; + long double max_possible = max_possible_score[(size_t)optimization_mode]; + for (unsigned index(0); index < results.first.size(); ++index) + { + final.wins += results.first[index].wins * factors[index]; + final.draws += results.first[index].draws * factors[index]; + final.losses += results.first[index].losses * factors[index]; + auto lower_bound = boost::math::binomial_distribution<>::find_lower_bound_on_p(results.second, results.first[index].points / max_possible, 1 - confidence_level) * max_possible; + auto upper_bound = boost::math::binomial_distribution<>::find_upper_bound_on_p(results.second, results.first[index].points / max_possible, 1 - confidence_level) * max_possible; + if (use_harmonic_mean) + { + final.points += factors[index] / results.first[index].points; + final.points_lower_bound += factors[index] / lower_bound; + final.points_upper_bound += factors[index] / upper_bound; + } + else + { + final.points += results.first[index].points * factors[index]; + final.points_lower_bound += lower_bound * factors[index]; + final.points_upper_bound += upper_bound * factors[index]; } } - cards.organize(); - // std::cout << "nb cards: " << nb_cards << "\n"; - // for(auto counts: sets_counts) - // { - // std::cout << "set " << counts.first << " (" << sets[counts.first] << ")" << ": " << counts.second << "\n"; - // } - // std::cout << "nb mission cards: " << cards.mission_cards.size() << "\n"; + long double factor_sum = std::accumulate(factors.begin(), factors.end(), 0.); + final.wins /= factor_sum * (long double)results.second; + final.draws /= factor_sum * (long double)results.second; + final.losses /= factor_sum * (long double)results.second; + if (use_harmonic_mean) + { + final.points = factor_sum / ((long double)results.second * final.points); + final.points_lower_bound = factor_sum / final.points_lower_bound; + final.points_upper_bound = factor_sum / final.points_upper_bound; + } + else + { + final.points /= factor_sum * (long double)results.second; + final.points_lower_bound /= factor_sum; + final.points_upper_bound /= factor_sum; + } + return final; } -//---------------------- $30 Deck: a commander + a sequence of cards ----------- -// Can be shuffled. -// Implementations: random player and raid decks, ordered player decks. //------------------------------------------------------------------------------ -struct DeckIface -{ - const Card* commander; - std::vector cards; - - DeckIface() : - commander{nullptr} - {} - - DeckIface(const Card* commander_, - boost::any_range cards_) : - commander(commander_), - cards(std::begin(cards_), std::end(cards_)) - {} - ; - virtual ~DeckIface() {}; - virtual DeckIface* clone() const = 0; - virtual const Card* get_commander() = 0; - virtual const Card* next() = 0; - virtual void shuffle(std::mt19937& re) = 0; - // Special case for recharge (behemoth raid's ability). - virtual void place_at_bottom(const Card*) = 0; -}; +volatile unsigned thread_num_iterations{0}; // written by threads +EvaluatedResults *thread_results{nullptr}; // written by threads +volatile const FinalResults *thread_best_results{nullptr}; +volatile bool thread_compare{false}; +volatile bool thread_compare_stop{false}; // written by threads +volatile bool destroy_threads; //------------------------------------------------------------------------------ -struct DeckRandom : DeckIface +// Per thread data. +// seed should be unique for each thread. +// d1 and d2 are intended to point to read-only process-wide data. +struct SimulationData { - std::vector > > raid_cards; - std::deque shuffled_cards; + std::mt19937 re; + const Cards& cards; + const Decks& decks; + std::shared_ptr your_deck; + Hand your_hand; + std::vector> enemy_decks; + std::vector enemy_hands; + std::vector factors; + gamemode_t gamemode; + Quest quest; + std::unordered_map bg_effects; + std::vector your_bg_skills, enemy_bg_skills; - DeckRandom( - const Card* commander_, - const std::vector& cards_, - std::vector > > raid_cards_ = - std::vector > >()) : - DeckIface(commander_, cards_), - raid_cards(raid_cards_) + SimulationData(unsigned seed, const Cards& cards_, const Decks& decks_, unsigned num_enemy_decks_, std::vector factors_, gamemode_t gamemode_, Quest & quest_, + std::unordered_map& bg_effects_, std::vector& your_bg_skills_, std::vector& enemy_bg_skills_) : + re(seed), + cards(cards_), + decks(decks_), + your_deck(), + your_hand(nullptr), + enemy_decks(num_enemy_decks_), + factors(factors_), + gamemode(gamemode_), + quest(quest_), + bg_effects(bg_effects_), + your_bg_skills(your_bg_skills_), + enemy_bg_skills(enemy_bg_skills_) { + for (size_t i = 0; i < num_enemy_decks_; ++i) + { + enemy_hands.emplace_back(new Hand(nullptr)); + } } - DeckRandom(const DeckIface& other) : - DeckIface(other) + ~SimulationData() { + for(auto hand: enemy_hands) { delete(hand); } } - DeckRandom(const Cards& all_cards, const std::vector& names) + void set_decks(const Deck* const your_deck_, std::vector const & enemy_decks_) { - for(auto name: names) - { - auto card_it(all_cards.player_cards_by_name.find(name)); - if(card_it == all_cards.player_cards_by_name.end()) - { - throw std::runtime_error("While constructing a deck: the card " + name + " was not found."); - } - else - { - const Card* card{card_it->second}; - if(card->m_type == CardType::commander) - { - if(commander == nullptr) - { - commander = card; - } - else - { - throw std::runtime_error("While constructing a deck: two commanders detected (" + name + " and " + commander->m_name + ")"); - } - } - else - { - cards.emplace_back(card); - } - } - } - if(commander == nullptr) + your_deck.reset(your_deck_->clone()); + your_hand.deck = your_deck.get(); + for(unsigned i(0); i < enemy_decks_.size(); ++i) { - throw std::runtime_error("While constructing a deck: no commander found"); + enemy_decks[i].reset(enemy_decks_[i]->clone()); + enemy_hands[i]->deck = enemy_decks[i].get(); } } - DeckRandom(const Cards& all_cards, const std::vector& ids) + inline std::vector> evaluate() { - for(auto id: ids) + std::vector> res; + for(Hand* enemy_hand: enemy_hands) { - const Card* card{all_cards.by_id(id)}; - if(card->m_type == CardType::commander) - { - if(commander == nullptr) - { - commander = card; - } - else - { - throw std::runtime_error("While constructing a deck: two commanders detected (" + card->m_name + " and " + commander->m_name + ")"); - } - } - else - { - cards.emplace_back(card); - } - } - if(commander == nullptr) - { - throw std::runtime_error("While constructing a deck: no commander found"); + your_hand.reset(re); + enemy_hand->reset(re); + Field fd(re, cards, your_hand, *enemy_hand, gamemode, optimization_mode, quest, bg_effects, your_bg_skills, enemy_bg_skills); + Results result(play(&fd)); + res.emplace_back(result); } + return(res); } +}; +//------------------------------------------------------------------------------ +class Process; +void thread_evaluate(boost::barrier& main_barrier, + boost::mutex& shared_mutex, + SimulationData& sim, + const Process& p, + unsigned thread_id); +//------------------------------------------------------------------------------ +class Process +{ +public: + unsigned num_threads; + std::vector threads; + std::vector threads_data; + boost::barrier main_barrier; + boost::mutex shared_mutex; + const Cards& cards; + const Decks& decks; + Deck* your_deck; + const std::vector enemy_decks; + std::vector factors; + gamemode_t gamemode; + Quest quest; + std::unordered_map bg_effects; + std::vector your_bg_skills, enemy_bg_skills; - ~DeckRandom() {} - - virtual DeckIface* clone() const + Process(unsigned num_threads_, const Cards& cards_, const Decks& decks_, Deck* your_deck_, std::vector enemy_decks_, std::vector factors_, gamemode_t gamemode_, Quest & quest_, + std::unordered_map& bg_effects_, std::vector& your_bg_skills_, std::vector& enemy_bg_skills_) : + num_threads(num_threads_), + main_barrier(num_threads+1), + cards(cards_), + decks(decks_), + your_deck(your_deck_), + enemy_decks(enemy_decks_), + factors(factors_), + gamemode(gamemode_), + quest(quest_), + bg_effects(bg_effects_), + your_bg_skills(your_bg_skills_), + enemy_bg_skills(enemy_bg_skills_) { - return(new DeckRandom(*this)); + destroy_threads = false; + unsigned seed(sim_seed ? sim_seed : std::chrono::system_clock::now().time_since_epoch().count() * 2654435761); // Knuth multiplicative hash + if (num_threads_ == 1) + { + std::cout << "RNG seed " << seed << std::endl; + } + for(unsigned i(0); i < num_threads; ++i) + { + threads_data.push_back(new SimulationData(seed + i, cards, decks, enemy_decks.size(), factors, gamemode, quest, bg_effects, your_bg_skills, enemy_bg_skills)); + threads.push_back(new boost::thread(thread_evaluate, std::ref(main_barrier), std::ref(shared_mutex), std::ref(*threads_data.back()), std::ref(*this), i)); + } } - const Card* get_commander() + ~Process() { - return(commander); + destroy_threads = true; + main_barrier.wait(); + for(auto thread: threads) { thread->join(); } + for(auto data: threads_data) { delete(data); } } - const Card* next() + EvaluatedResults & evaluate(unsigned num_iterations, EvaluatedResults & evaluated_results) { - if(!shuffled_cards.empty()) - { - const Card* card = shuffled_cards.front(); - shuffled_cards.pop_front(); - return(card); - } - else + if (num_iterations <= evaluated_results.second) { - return(nullptr); + return evaluated_results; } + thread_num_iterations = num_iterations - evaluated_results.second; + thread_results = &evaluated_results; + thread_compare = false; + // unlock all the threads + main_barrier.wait(); + // wait for the threads + main_barrier.wait(); + return evaluated_results; } - void shuffle(std::mt19937& re) + EvaluatedResults & compare(unsigned num_iterations, EvaluatedResults & evaluated_results, const FinalResults & best_results) { - shuffled_cards.clear(); - boost::insert(shuffled_cards, shuffled_cards.end(), cards); - for(auto& card_pool: raid_cards) + if (num_iterations <= evaluated_results.second) { - assert(card_pool.first <= card_pool.second.size()); - partial_shuffle(card_pool.second.begin(), card_pool.second.begin() + card_pool.first, card_pool.second.end(), re); - shuffled_cards.insert(shuffled_cards.end(), card_pool.second.begin(), card_pool.second.begin() + card_pool.first); + return evaluated_results; } - boost::shuffle(shuffled_cards, re); - } - - void place_at_bottom(const Card* card) - { - shuffled_cards.push_back(card); + thread_num_iterations = num_iterations - evaluated_results.second; + thread_results = &evaluated_results; + thread_best_results = &best_results; + thread_compare = true; + thread_compare_stop = false; + // unlock all the threads + main_barrier.wait(); + // wait for the threads + main_barrier.wait(); + return evaluated_results; } }; - -void print_deck(DeckIface& deck) -{ - std::cout << "Deck:" << std::endl; - if(deck.commander) - { - std::cout << deck.commander->m_name << "\n"; - } - else - { - std::cout << "No commander\n"; - } - for(const Card* card: deck.cards) - { - std::cout << " " << card->m_name << "\n" << std::flush; - } -} //------------------------------------------------------------------------------ -// No support for ordered raid decks -struct DeckOrdered : DeckIface +void thread_evaluate(boost::barrier& main_barrier, + boost::mutex& shared_mutex, + SimulationData& sim, + const Process& p, + unsigned thread_id) { - std::deque shuffled_cards; - // card id -> card order - std::map > order; - - DeckOrdered(const Card* commander_, boost::any_range cards_) : - DeckIface(commander_, cards_), - shuffled_cards(cards.begin(), cards.end()) - { - } - - DeckOrdered(const DeckIface& other) : - DeckIface(other) - { - } - - ~DeckOrdered() {} - - virtual DeckOrdered* clone() const - { - return(new DeckOrdered(*this)); - } - - const Card* get_commander() { return(commander); } - - const Card* next() + while(true) { - if(shuffled_cards.empty()) - { - return(nullptr); - } - else + main_barrier.wait(); + sim.set_decks(p.your_deck, p.enemy_decks); + if(destroy_threads) + { return; } + while(true) { - auto cardIter = std::min_element(shuffled_cards.begin(), shuffled_cards.begin() + std::min(3u, shuffled_cards.size()), [this](const Card* card1, const Card* card2) -> bool - { - auto card1_order = order.find(card1->m_id); - if(!card1_order->second.empty()) - { - auto card2_order = order.find(card2->m_id); - if(!card1_order->second.empty()) - { - return(*card1_order->second.begin() < *card2_order->second.begin()); - } - else - { - return(true); - } - } - else - { - return(false); - } - }); - auto card = *cardIter; - shuffled_cards.erase(cardIter); - auto card_order = order.find(card->m_id); - if(!card_order->second.empty()) + shared_mutex.lock(); //<<<< + if(thread_num_iterations == 0 || (thread_compare && thread_compare_stop)) //! { - card_order->second.erase(card_order->second.begin()); + shared_mutex.unlock(); //>>>> + main_barrier.wait(); + break; + } + else + { + --thread_num_iterations; //! + shared_mutex.unlock(); //>>>> + std::vector> result{sim.evaluate()}; + shared_mutex.lock(); //<<<< + std::vector thread_score_local(thread_results->first.size(), 0u); //! + for(unsigned index(0); index < result.size(); ++index) + { + thread_results->first[index] += result[index]; //! + thread_score_local[index] = thread_results->first[index].points; //! + } + ++thread_results->second; //! + unsigned thread_total_local{thread_results->second}; //! + shared_mutex.unlock(); //>>>> + if(thread_compare && thread_id == 0 && thread_total_local > 1) + { + unsigned score_accum = 0; + // Multiple defense decks case: scaling by factors and approximation of a "discrete" number of events. + if(result.size() > 1) + { + long double score_accum_d = 0.0; + for(unsigned i = 0; i < thread_score_local.size(); ++i) + { + score_accum_d += thread_score_local[i] * sim.factors[i]; + } + score_accum_d /= std::accumulate(sim.factors.begin(), sim.factors.end(), .0); + score_accum = score_accum_d; + } + else + { + score_accum = thread_score_local[0]; + } + bool compare_stop(false); + long double max_possible = max_possible_score[(size_t)optimization_mode]; + // Get a loose (better than no) upper bound. TODO: Improve it. + compare_stop = (boost::math::binomial_distribution<>::find_upper_bound_on_p(thread_total_local, score_accum / max_possible, 1 - confidence_level) * max_possible < + thread_best_results->points + min_increment_of_score); + if(compare_stop) + { + shared_mutex.lock(); //<<<< + //std::cout << thread_total_local << "\n"; + thread_compare_stop = true; //! + shared_mutex.unlock(); //>>>> + } + } } - return(card); - } - } - - void shuffle(std::mt19937& re) - { - unsigned i = 0; - order.clear(); - for(auto card: cards) - { - order[card->m_id].push_back(i); - ++i; } - shuffled_cards.clear(); - range::insert(shuffled_cards, shuffled_cards.end(), cards); - std::shuffle(shuffled_cards.begin(), shuffled_cards.end(), re); } - - void place_at_bottom(const Card* card) - { - shuffled_cards.push_back(card); - } -}; +} //------------------------------------------------------------------------------ -// Represents a particular draw from a deck. -// Persistent object: call reset to get a new draw. -class Hand +void print_score_info(const EvaluatedResults& results, std::vector& factors) { -public: - - Hand(DeckIface* deck_) : - deck(deck_), - assaults(15), - structures(15) + auto final = compute_score(results, factors); + std::cout << final.points << " ("; + if (show_ci) { + std::cout << final.points_lower_bound << "-" << final.points_upper_bound << ", "; } - - void reset(std::mt19937& re) + for(const auto & val: results.first) { - assaults.reset(); - structures.reset(); - commander = CardStatus(deck->get_commander()); - deck->shuffle(re); + switch(optimization_mode) + { + case OptimizationMode::raid: + case OptimizationMode::campaign: + case OptimizationMode::brawl: + case OptimizationMode::war: + case OptimizationMode::quest: + std::cout << val.points << " "; + break; + default: + std::cout << val.points / 100 << " "; + break; + } } - - DeckIface* deck; - CardStatus commander; - Storage assaults; - Storage structures; -}; -//---------------------- $40 Game rules implementation ------------------------- -// Everything about how a battle plays out, except the following: -// the implementation of the attack by an assault card is in the next section; -// the implementation of the active skills is in the section after that. -// struct Field is the data model of a battle: -// an attacker and a defender deck, list of assaults and structures, etc. -unsigned turn_limit{50}; -class Field + std::cout << "/ " << results.second << ")" << std::endl; +} +//------------------------------------------------------------------------------ +void print_results(const EvaluatedResults& results, std::vector& factors) { -public: - bool end; - std::mt19937& re; - const Cards& cards; - // players[0]: the attacker, players[1]: the defender - std::array players; - unsigned tapi; // current turn's active player index - unsigned tipi; // and inactive - Hand* tap; - Hand* tip; - std::array selection_array; - unsigned turn; - gamemode_t gamemode; - // With the introduction of on death skills, a single skill can trigger arbitrary many skills. - // They are stored in this, and cleared after all have been performed. - std::deque > skill_queue; - std::vector killed_with_on_death; - std::vector killed_with_regen; - enum phase - { - playcard_phase, - commander_phase, - structures_phase, - assaults_phase - }; - // the current phase of the turn: starts with playcard_phase, then commander_phase, structures_phase, and assaults_phase - phase current_phase; - // the index of the card being evaluated in the current phase. - // Meaningless in playcard_phase, - // otherwise is the index of the current card in players->structures or players->assaults - unsigned current_ci; - - Field(std::mt19937& re_, const Cards& cards_, Hand& hand1, Hand& hand2, gamemode_t _gamemode) : - end{false}, - re(re_), - cards(cards_), - players{{&hand1, &hand2}}, - turn(1), - gamemode(_gamemode) + auto final = compute_score(results, factors); + std::cout << "win%: " << final.wins * 100.0 << " ("; + for (const auto & val : results.first) { + std::cout << val.wins << " "; } + std::cout << "/ " << results.second << ")" << std::endl; - inline unsigned rand(unsigned x, unsigned y) + std::cout << "stall%: " << final.draws * 100.0 << " ("; + for (const auto & val : results.first) { - return(std::uniform_int_distribution(x, y)(re)); + std::cout << val.draws << " "; } + std::cout << "/ " << results.second << ")" << std::endl; - inline unsigned flip() + std::cout << "loss%: " << final.losses * 100.0 << " ("; + for (const auto & val : results.first) { - return(this->rand(0,1)); + std::cout << val.losses << " "; } + std::cout << "/ " << results.second << ")" << std::endl; - template - inline T& random_in_vector(std::vector& v) - { - assert(v.size() > 0); - return(v[this->rand(0, v.size() - 1)]); - } -}; -//------------------------------------------------------------------------------ -inline unsigned opponent(unsigned player) -{ - return((player + 1) % 2); -} -//------------------------------------------------------------------------------ -void prepend_on_death(Field* fd) -{ - for(auto status: boost::adaptors::reverse(fd->killed_with_on_death)) + if (optimization_mode == OptimizationMode::quest) { - for(auto& skill: boost::adaptors::reverse(status->m_card->m_skills_died)) - { - _DEBUG_MSG("On death skill pushed in front %s %u %s\n", skill_names[std::get<0>(skill)].c_str(), std::get<1>(skill), faction_names[std::get<2>(skill)].c_str()); - fd->skill_queue.emplace_front(status, skill); - } + // points = win% * win_score + (must_win ? win% : 100%) * quest% * quest_score + // quest% = (points - win% * win_score) / (must_win ? win% : 100%) / quest_score + std::cout << "quest%: " << (final.points - final.wins * quest.win_score) / (quest.must_win ? final.wins : 1) / quest.quest_score * 100 << std::endl; } - fd->killed_with_on_death.clear(); -} -//------------------------------------------------------------------------------ -void(*skill_table[num_skills])(Field*, CardStatus* src_status, const SkillSpec&); -void resolve_skill(Field* fd) -{ - while(!fd->skill_queue.empty()) + + switch(optimization_mode) { - auto skill_instance(fd->skill_queue.front()); - auto& status(std::get<0>(skill_instance)); - auto& skill(std::get<1>(skill_instance)); - fd->skill_queue.pop_front(); - skill_table[std::get<0>(skill)](fd, status, skill); + case OptimizationMode::raid: + case OptimizationMode::campaign: + case OptimizationMode::brawl: + case OptimizationMode::war: + case OptimizationMode::quest: + std::cout << "score: " << final.points << " ("; + for(const auto & val: results.first) + { + std::cout << val.points << " "; + } + std::cout << "/ " << results.second << ")" << std::endl; + if (show_ci) + { + std::cout << "ci: " << final.points_lower_bound << " - " << final.points_upper_bound << std::endl; + } + break; + default: + break; } } //------------------------------------------------------------------------------ -void attack_phase(Field* fd); -SkillSpec augmented_skill(CardStatus* status, const SkillSpec& s) -{ - SkillSpec augmented_s = s; - if(std::get<0>(s) != augment && std::get<0>(s) != augment_all && std::get<0>(s) != summon) - { - std::get<1>(augmented_s) += status->m_augmented; - } - return(augmented_s); -} -void evaluate_skills(Field* fd, CardStatus* status, const std::vector& skills) +void print_deck_inline(const unsigned deck_cost, const FinalResults score, Deck * deck) { - assert(status); - assert(fd->skill_queue.size() == 0); - for(auto& skill: skills) + std::cout << deck->cards.size() << " units: "; + if(fund > 0) { - _DEBUG_MSG("Evaluating %s skill %s\n", status_description(status).c_str(), skill_description(skill).c_str()); - fd->skill_queue.emplace_back(status, status->m_augmented == 0 ? skill : augmented_skill(status, skill)); - resolve_skill(fd); + std::cout << "$" << deck_cost << " "; } -} -struct PlayCard -{ - const Card* card; - Field* fd; - CardStatus* status; - Storage* storage; - - PlayCard(const Card* card_, Field* fd_) : - card{card_}, - fd{fd_}, - status{nullptr}, - storage{nullptr} - {} - - template - bool op() + switch(optimization_mode) { - setStorage(); - placeCard(); - onPlaySkills(); - blitz(); + case OptimizationMode::raid: + case OptimizationMode::campaign: + case OptimizationMode::brawl: + case OptimizationMode::war: + case OptimizationMode::quest: + std::cout << "(" << score.wins * 100 << "% win"; + if (optimization_mode == OptimizationMode::quest) + { + std::cout << ", " << (score.points - score.wins * quest.win_score) / (quest.must_win ? score.wins : 1) / quest.quest_score * 100 << "% quest"; + } + if (show_ci) + { + std::cout << ", " << score.points_lower_bound << "-" << score.points_upper_bound; + } + std::cout << ") "; + break; + case OptimizationMode::defense: + std::cout << "(" << score.draws * 100.0 << "% stall) "; + break; + default: + break; } - - // action - template - void setStorage() + std::cout << score.points << ": " << deck->commander->m_name; + if (deck->strategy == DeckStrategy::random) { + std::sort(deck->cards.begin(), deck->cards.end(), [](const Card* a, const Card* b) { return a->m_id < b->m_id; }); } - - // assault + structure - template - void placeCard() + std::string last_name; + unsigned num_repeat(0); + for(const Card* card: deck->cards) { - status = &storage->add_back(); - status->set(card); - status->m_index = storage->size() - 1; - status->m_player = fd->tapi; - if(fd->turn == 1 && fd->gamemode == tournament && status->m_delay > 0) + if(card->m_name == last_name) { - ++status->m_delay; + ++ num_repeat; } - placeDebugMsg(); - } - - // assault + structure - template - void placeDebugMsg() - { - _DEBUG_MSG("Placed [%s] as %s %d\n", card->m_name.c_str(), cardtype_names[type].c_str(), storage->size() - 1); - } - - // all except assault: noop - template - void blitz() - { - } - - // assault + structure - template - void onPlaySkills() - { - for(auto& skill: card->m_skills_played) + else { - fd->skill_queue.emplace_back(status, skill); - resolve_skill(fd); + if(num_repeat > 1) + { + std::cout << " #" << num_repeat; + } + std::cout << ", " << card->m_name; + last_name = card->m_name; + num_repeat = 1; } } -}; -// assault -template <> -void PlayCard::setStorage() -{ - storage = &fd->tap->assaults; -} -// structure -template <> -void PlayCard::setStorage() -{ - storage = &fd->tap->structures; -} -// action -template <> -void PlayCard::placeCard() -{ -} -// assault -template <> -void PlayCard::blitz() -{ - if(card->m_blitz && fd->tip->assaults.size() > status->m_index && fd->tip->assaults[status->m_index].m_hp > 0 && fd->tip->assaults[status->m_index].m_delay == 0) + if(num_repeat > 1) { - status->blitz = true; + std::cout << " #" << num_repeat; } + std::cout << std::endl; } -// action -template <> -void PlayCard::onPlaySkills() +//------------------------------------------------------------------------------ +void hill_climbing(unsigned num_min_iterations, unsigned num_iterations, Deck* d1, Process& proc, Requirement & requirement, Quest & quest) { - for(auto& skill: card->m_skills) + EvaluatedResults zero_results = { EvaluatedResults::first_type(proc.enemy_decks.size()), 0 }; + auto best_deck = d1->hash(); + std::map evaluated_decks{{best_deck, zero_results}}; + EvaluatedResults & results = proc.evaluate(num_min_iterations, evaluated_decks.begin()->second); + print_score_info(results, proc.factors); + auto current_score = compute_score(results, proc.factors); + auto best_score = current_score; + // Non-commander cards + auto non_commander_cards = proc.cards.player_assaults; + non_commander_cards.insert(non_commander_cards.end(), proc.cards.player_structures.begin(), proc.cards.player_structures.end()); + non_commander_cards.insert(non_commander_cards.end(), std::initializer_list{NULL,}); + const Card* best_commander = d1->commander; + std::vector best_cards = d1->cards; + unsigned deck_cost = get_deck_cost(d1); + fund = std::max(fund, deck_cost); + print_deck_inline(deck_cost, best_score, d1); + std::mt19937 & re = proc.threads_data[0]->re; + unsigned best_gap = check_requirement(d1, requirement, quest); + bool deck_has_been_improved = true; + unsigned long skipped_simulations = 0; + std::vector> cards_out, cards_in; + for(unsigned slot_i(0), dead_slot(0); ; slot_i = (slot_i + 1) % std::min(max_deck_len, best_cards.size() + 1)) { - fd->skill_queue.emplace_back(nullptr, skill); - resolve_skill(fd); - // Special case: enemy commander killed by a shock action card - if(fd->tip->commander.m_hp == 0) + if (deck_has_been_improved) { - _DEBUG_MSG("turn's defender dead.\n"); - fd->end = true; - break; + dead_slot = slot_i; + deck_has_been_improved = false; } - } - // Special case: recharge ability - if(card->m_recharge && fd->flip()) - { - fd->tap->deck->place_at_bottom(card); - } -} -//------------------------------------------------------------------------------ -void turn_start_phase(Field* fd); -void prepend_on_death(Field* fd); -// return value : 0 -> attacker wins, 1 -> defender wins -unsigned play(Field* fd) -{ - fd->players[0]->commander.m_player = 0; - fd->players[1]->commander.m_player = 1; - fd->tapi = fd->gamemode == surge ? 1 : 0; - fd->tipi = opponent(fd->tapi); - fd->tap = fd->players[fd->tapi]; - fd->tip = fd->players[fd->tipi]; - fd->end = false; - // Shuffle deck - while(fd->turn < turn_limit && !fd->end) - { - fd->current_phase = Field::playcard_phase; - // Initialize stuff, remove dead cards - _DEBUG_MSG("##### TURN %u #####\n", fd->turn); - turn_start_phase(fd); - // Special case: refresh on commander - if(fd->tip->commander.m_card->m_refresh && fd->tip->commander.m_hp > 0) - { - fd->tip->commander.m_hp = fd->tip->commander.m_card->m_health; - } - // Play a card - const Card* played_card(fd->tap->deck->next()); - if(played_card) - { - switch(played_card->m_type) + else if (slot_i == dead_slot || best_score.points - target_score > -1e-9) + { + if (best_score.n_sims >= num_iterations || best_gap > 0) { - case CardType::action: - // end: handles commander death by shock - PlayCard(played_card, fd).op(); - break; - case CardType::assault: - PlayCard(played_card, fd).op(); - break; - case CardType::structure: - PlayCard(played_card, fd).op(); break; } + auto & prev_results = evaluated_decks[best_deck]; + skipped_simulations += prev_results.second; + // Re-evaluate the best deck + auto evaluate_result = proc.evaluate(std::min(prev_results.second * 10, num_iterations), prev_results); + best_score = compute_score(evaluate_result, proc.factors); + std::cout << "Results refined: "; + print_score_info(evaluate_result, proc.factors); + dead_slot = slot_i; } - // Evaluate commander - fd->current_phase = Field::commander_phase; - evaluate_skills(fd, &fd->tap->commander, fd->tap->commander.m_card->m_skills); - // Evaluate structures - fd->current_phase = Field::structures_phase; - for(fd->current_ci = 0; !fd->end && fd->current_ci < fd->tap->structures.size(); ++fd->current_ci) + if (best_score.points - target_score > -1e-9) { - CardStatus& current_status(fd->tap->structures[fd->current_ci]); - if(current_status.m_delay == 0) - { - evaluate_skills(fd, ¤t_status, current_status.m_card->m_skills); - } + continue; } - // Evaluate assaults - fd->current_phase = Field::assaults_phase; - for(fd->current_ci = 0; !fd->end && fd->current_ci < fd->tap->assaults.size(); ++fd->current_ci) + if (requirement.num_cards.count(best_commander) == 0) { - // ca: current assault - CardStatus& current_status(fd->tap->assaults[fd->current_ci]); - if((current_status.m_delay > 0 && !current_status.blitz) || current_status.m_hp == 0 || current_status.m_jammed || current_status.m_frozen) - { - //_DEBUG_MSG("! Assault %u (%s) hp: %u, jammed %u\n", card_index, current_status.m_card->m_name.c_str(), current_status.m_hp, current_status.m_jammed); - } - else + for(const Card* commander_candidate: proc.cards.player_commanders) { - // Special case: check for split (tartarus swarm raid) - if(current_status.m_card->m_split && fd->tap->assaults.size() + fd->tap->structures.size() < 100) + // Various checks to check if the card is accepted + assert(commander_candidate->m_type == CardType::commander); + if (commander_candidate->m_name == best_commander->m_name) + { continue; } + d1->cards = best_cards; + // Place it in the deck and restore other cards + cards_out.clear(); + cards_out.emplace_back(-1, best_commander); + cards_out = {{-1, best_commander}}; + d1->commander = commander_candidate; + if (! adjust_deck(d1, -1, -1, nullptr, fund, re, deck_cost, cards_out, cards_in)) + { continue; } + unsigned new_gap = check_requirement(d1, requirement, quest); + if (new_gap > 0 && new_gap >= best_gap) + { continue; } + auto && cur_deck = d1->hash(); + auto && emplace_rv = evaluated_decks.insert({cur_deck, zero_results}); + auto & prev_results = emplace_rv.first->second; + if (!emplace_rv.second) { - CardStatus& status_split(fd->tap->assaults.add_back()); - status_split.set(current_status.m_card); - _DEBUG_MSG("Split assault %d (%s)\n", fd->tap->assaults.size() - 1, current_status.m_card->m_name.c_str()); + skipped_simulations += prev_results.second; } - // Evaluate skills - // Special case: Gore Typhon's infuse - evaluate_skills(fd, ¤t_status, current_status.m_infused ? current_status.infused_skills : current_status.m_card->m_skills); - // Attack - if(!current_status.m_immobilized && current_status.m_hp > 0) + // Evaluate new deck + auto compare_results = proc.compare(best_score.n_sims, prev_results, best_score); + current_score = compute_score(compare_results, proc.factors); + // Is it better ? + if (new_gap < best_gap || current_score.points > best_score.points + min_increment_of_score) { - attack_phase(fd); + // Then update best score/commander, print stuff + std::cout << "Deck improved: " << d1->hash() << ": " << card_slot_id_names(cards_out) << " -> " << card_slot_id_names(cards_in) << ": "; + best_gap = new_gap; + best_score = current_score; + best_deck = cur_deck; + best_commander = d1->commander; + best_cards = d1->cards; + deck_has_been_improved = true; + print_score_info(compare_results, proc.factors); + print_deck_inline(deck_cost, best_score, d1); } } + // Now that all commanders are evaluated, take the best one + d1->commander = best_commander; + d1->cards = best_cards; + } + std::shuffle(non_commander_cards.begin(), non_commander_cards.end(), re); + for(const Card* card_candidate: non_commander_cards) + { + if (card_candidate && (card_candidate->m_fusion_level < use_fused_card_level || (use_top_level_card && card_candidate->m_level < card_candidate->m_top_level_card->m_level))) + { continue; } + d1->commander = best_commander; + d1->cards = best_cards; + if (card_candidate ? + (slot_i < best_cards.size() && card_candidate->m_name == best_cards[slot_i]->m_name) // Omega -> Omega + : + (slot_i == best_cards.size())) // void -> void + { continue; } + cards_out.clear(); + if (slot_i < d1->cards.size()) + { + cards_out.emplace_back(-1, d1->cards[slot_i]); + d1->cards.erase(d1->cards.begin() + slot_i); + } + if (! adjust_deck(d1, slot_i, slot_i, card_candidate, fund, re, deck_cost, cards_out, cards_in) || + d1->cards.size() < min_deck_len) + { continue; } + unsigned new_gap = check_requirement(d1, requirement, quest); + if (new_gap > 0 && new_gap >= best_gap) + { continue; } + auto && cur_deck = d1->hash(); + auto && emplace_rv = evaluated_decks.insert({cur_deck, zero_results}); + auto & prev_results = emplace_rv.first->second; + if (!emplace_rv.second) + { + skipped_simulations += prev_results.second; + } + // Evaluate new deck + auto compare_results = proc.compare(best_score.n_sims, prev_results, best_score); + current_score = compute_score(compare_results, proc.factors); + // Is it better ? + if (new_gap < best_gap || current_score.points > best_score.points + min_increment_of_score) + { + // Then update best score/slot, print stuff + std::cout << "Deck improved: " << d1->hash() << ": " << card_slot_id_names(cards_out) << " -> " << card_slot_id_names(cards_in) << ": "; + best_gap = new_gap; + best_score = current_score; + best_deck = cur_deck; + best_commander = d1->commander; + best_cards = d1->cards; + deck_has_been_improved = true; + print_score_info(compare_results, proc.factors); + print_deck_inline(deck_cost, best_score, d1); + } + if(best_score.points - target_score > -1e-9) + { break; } } - std::swap(fd->tapi, fd->tipi); - std::swap(fd->tap, fd->tip); - ++fd->turn; + d1->commander = best_commander; + d1->cards = best_cards; } - // defender wins - if(fd->players[0]->commander.m_hp == 0) { _DEBUG_MSG("Defender wins.\n"); return(1); } - // attacker wins - if(fd->players[1]->commander.m_hp == 0) { _DEBUG_MSG("Attacker wins.\n"); return(0); } - if(fd->turn >= turn_limit) { return(1); } + unsigned simulations = 0; + for(auto evaluation: evaluated_decks) + { simulations += evaluation.second.second; } + std::cout << "Evaluated " << evaluated_decks.size() << " decks (" << simulations << " + " << skipped_simulations << " simulations)." << std::endl; + std::cout << "Optimized Deck: "; + print_deck_inline(get_deck_cost(d1), best_score, d1); } //------------------------------------------------------------------------------ -// All the stuff that happens at the beginning of a turn, before a card is played -inline unsigned safe_minus(unsigned x, unsigned y) -{ - return(x - std::min(x, y)); -} -// returns true iff the card died. -bool remove_hp(Field* fd, CardStatus& status, unsigned dmg) +void hill_climbing_ordered(unsigned num_min_iterations, unsigned num_iterations, Deck* d1, Process& proc, Requirement & requirement, Quest & quest) { - assert(status.m_hp > 0); - status.m_hp = safe_minus(status.m_hp, dmg); - const bool just_died(status.m_hp == 0); - if(just_died) + EvaluatedResults zero_results = { EvaluatedResults::first_type(proc.enemy_decks.size()), 0 }; + auto best_deck = d1->hash(); + std::map evaluated_decks{{best_deck, zero_results}}; + EvaluatedResults & results = proc.evaluate(num_min_iterations, evaluated_decks.begin()->second); + print_score_info(results, proc.factors); + auto current_score = compute_score(results, proc.factors); + auto best_score = current_score; + // Non-commander cards + auto non_commander_cards = proc.cards.player_assaults; + non_commander_cards.insert(non_commander_cards.end(), proc.cards.player_structures.begin(), proc.cards.player_structures.end()); + non_commander_cards.insert(non_commander_cards.end(), std::initializer_list{NULL,}); + const Card* best_commander = d1->commander; + std::vector best_cards = d1->cards; + unsigned deck_cost = get_deck_cost(d1); + fund = std::max(fund, deck_cost); + print_deck_inline(deck_cost, best_score, d1); + std::mt19937 & re = proc.threads_data[0]->re; + unsigned best_gap = check_requirement(d1, requirement, quest); + bool deck_has_been_improved = true; + unsigned long skipped_simulations = 0; + std::vector> cards_out, cards_in; + for(unsigned from_slot(freezed_cards), dead_slot(freezed_cards); ; from_slot = (from_slot + 1) % std::min(max_deck_len, d1->cards.size() + 1)) { - _DEBUG_MSG("Card %u (%s) dead\n", status.m_index, status.m_card->m_name.c_str()); - if(status.m_card->m_skills_died.size() > 0) + if (from_slot < freezed_cards) { - fd->killed_with_on_death.push_back(&status); + continue; } - if(status.m_card->m_regenerate) + if(deck_has_been_improved) { - fd->killed_with_regen.push_back(&status); + dead_slot = from_slot; + deck_has_been_improved = false; } - } - return(just_died); -} -inline bool is_it_dead(CardStatus& c) -{ - if(c.m_hp == 0) // yes it is - { - _DEBUG_MSG("Dead: %s\n", status_description(&c).c_str()); - return(true); - } - else { return(false); } // nope still kickin' -} -inline void remove_dead(Storage& storage) -{ - storage.remove(is_it_dead); -} -void check_regeneration(Field* fd) -{ - for(unsigned i(0); i < fd->killed_with_regen.size(); ++i) - { - CardStatus& status = *fd->killed_with_regen[i]; - if(status.m_hp == 0 && status.m_card->m_regenerate > 0 && !status.m_diseased) + else if (from_slot == dead_slot || best_score.points - target_score > -1e-9) { - status.m_hp = fd->flip() ? status.m_card->m_regenerate : 0; + if (best_score.n_sims >= num_iterations || best_gap > 0) + { + break; + } + auto & prev_results = evaluated_decks[best_deck]; + skipped_simulations += prev_results.second; + // Re-evaluate the best deck + auto evaluate_result = proc.evaluate(std::min(prev_results.second * 10, num_iterations), prev_results); + best_score = compute_score(evaluate_result, proc.factors); + std::cout << "Results refined: "; + print_score_info(evaluate_result, proc.factors); + dead_slot = from_slot; } - if(status.m_hp > 0) + if (best_score.points - target_score > -1e-9) { - _DEBUG_MSG("Card %s regenerated, hp 0 -> %u\n", status.m_card->m_name.c_str(), status.m_hp); + continue; } - - } - fd->killed_with_regen.clear(); -} -void turn_start_phase(Field* fd) -{ - remove_dead(fd->tap->assaults); - remove_dead(fd->tap->structures); - remove_dead(fd->tip->assaults); - remove_dead(fd->tip->structures); - // Active player's assault cards: - // update index - // remove enfeeble, protect; apply poison damage, reduce delay - { - auto& assaults(fd->tap->assaults); - for(unsigned index(0), end(assaults.size()); - index < end; - ++index) - { - CardStatus& status(assaults[index]); - status.m_index = index; - status.m_enfeebled = 0; - status.m_protected = 0; - remove_hp(fd, status, status.m_poisoned); - if(status.m_delay > 0 && !status.m_frozen) { --status.m_delay; } - } - } - // Active player's structure cards: - // update index - // reduce delay - { - auto& structures(fd->tap->structures); - for(unsigned index(0), end(structures.size()); - index < end; - ++index) - { - CardStatus& status(structures[index]); - status.m_index = index; - if(status.m_delay > 0) { --status.m_delay; } - } - } - // Defending player's assault cards: - // update index - // remove augment, chaos, freeze, immobilize, jam, rally, weaken, apply refresh - { - auto& assaults(fd->tip->assaults); - for(unsigned index(0), end(assaults.size()); - index < end; - ++index) - { - CardStatus& status(assaults[index]); - status.m_index = index; - status.m_augmented = 0; - status.blitz = false; - status.m_chaos = false; - status.m_frozen = false; - status.m_immobilized = false; - status.m_jammed = false; - status.m_rallied = 0; - status.m_weakened = 0; - if(status.m_card->m_refresh && !status.m_diseased) + if (requirement.num_cards.count(best_commander) == 0) + { + for(const Card* commander_candidate: proc.cards.player_commanders) { -#ifndef NDEBUG - if(status.m_hp < status.m_card->m_health) + if(best_score.points - target_score > -1e-9) + { break; } + // Various checks to check if the card is accepted + assert(commander_candidate->m_type == CardType::commander); + if (commander_candidate->m_name == best_commander->m_name) + { continue; } + d1->cards = best_cards; + // Place it in the deck + cards_out.clear(); + cards_out.emplace_back(-1, best_commander); + d1->commander = commander_candidate; + if (! adjust_deck(d1, -1, -1, nullptr, fund, re, deck_cost, cards_out, cards_in)) + { continue; } + unsigned new_gap = check_requirement(d1, requirement, quest); + if (new_gap > 0 && new_gap >= best_gap) + { continue; } + auto && cur_deck = d1->hash(); + auto && emplace_rv = evaluated_decks.insert({cur_deck, zero_results}); + auto & prev_results = emplace_rv.first->second; + if (!emplace_rv.second) { - _DEBUG_MSG("%u %s refreshed. hp %u -> %u.\n", index, status_description(&status).c_str(), status.m_hp, status.m_card->m_health); + skipped_simulations += prev_results.second; + } + // Evaluate new deck + auto compare_results = proc.compare(best_score.n_sims, prev_results, best_score); + current_score = compute_score(compare_results, proc.factors); + // Is it better ? + if (new_gap < best_gap || current_score.points > best_score.points + min_increment_of_score) + { + std::cout << "Deck improved: " << d1->hash() << ": " << card_slot_id_names(cards_out) << " -> " << card_slot_id_names(cards_in) << ": "; + // Then update best score/commander, print stuff + best_gap = new_gap; + best_score = current_score; + best_deck = cur_deck; + best_commander = commander_candidate; + best_cards = d1->cards; + deck_has_been_improved = true; + print_score_info(compare_results, proc.factors); + print_deck_inline(deck_cost, best_score, d1); } -#endif - status.m_hp = status.m_card->m_health; } - } - } - // Defending player's structure cards: - // update index - // apply refresh - { - auto& structures(fd->tip->structures); - for(unsigned index(0), end(structures.size()); - index < end; - ++index) - { - CardStatus& status(structures[index]); - status.m_index = index; - if(status.m_card->m_refresh && status.m_hp < status.m_card->m_health) + // Now that all commanders are evaluated, take the best one + d1->commander = best_commander; + d1->cards = best_cards; + } + std::shuffle(non_commander_cards.begin(), non_commander_cards.end(), re); + for(const Card* card_candidate: non_commander_cards) + { + if (card_candidate && (card_candidate->m_fusion_level < use_fused_card_level || (use_top_level_card && card_candidate->m_level < card_candidate->m_top_level_card->m_level))) + { continue; } + // Various checks to check if the card is accepted + assert(!card_candidate || card_candidate->m_type != CardType::commander); + for(unsigned to_slot(card_candidate ? freezed_cards : best_cards.size() - 1); to_slot < best_cards.size() + (from_slot < best_cards.size() ? 0 : 1); ++to_slot) { -#ifndef NDEBUG - _DEBUG_MSG("%s refreshed. hp %u -> %u.\n", index, status_description(&status).c_str(), status.m_hp, status.m_card->m_health); -#endif - status.m_hp = status.m_card->m_health; + d1->commander = best_commander; + d1->cards = best_cards; + if (card_candidate ? + (from_slot < best_cards.size() && (from_slot == to_slot && card_candidate->m_name == best_cards[to_slot]->m_name)) // 2 Omega -> 2 Omega + : + (from_slot == best_cards.size())) // void -> void + { continue; } + cards_out.clear(); + if (from_slot < d1->cards.size()) + { + cards_out.emplace_back(from_slot, d1->cards[from_slot]); + d1->cards.erase(d1->cards.begin() + from_slot); + } + if (! adjust_deck(d1, from_slot, to_slot, card_candidate, fund, re, deck_cost, cards_out, cards_in) || + d1->cards.size() < min_deck_len) + { continue; } + unsigned new_gap = check_requirement(d1, requirement, quest); + if (new_gap > 0 && new_gap >= best_gap) + { continue; } + auto && cur_deck = d1->hash(); + auto && emplace_rv = evaluated_decks.insert({cur_deck, zero_results}); + auto & prev_results = emplace_rv.first->second; + if (!emplace_rv.second) + { + skipped_simulations += prev_results.second; + } + // Evaluate new deck + auto compare_results = proc.compare(best_score.n_sims, prev_results, best_score); + current_score = compute_score(compare_results, proc.factors); + // Is it better ? + if (new_gap < best_gap || current_score.points > best_score.points + min_increment_of_score) + { + // Then update best score/slot, print stuff + std::cout << "Deck improved: " << d1->hash() << ": " << card_slot_id_names(cards_out) << " -> " << card_slot_id_names(cards_in) << ": "; + best_gap = new_gap; + best_score = current_score; + best_deck = cur_deck; + best_commander = d1->commander; + best_cards = d1->cards; + deck_has_been_improved = true; + print_score_info(compare_results, proc.factors); + print_deck_inline(deck_cost, best_score, d1); + } } + if(best_score.points - target_score > -1e-9) + { break; } } + d1->commander = best_commander; + d1->cards = best_cards; } - // Perform on death skills (from cards killed by poison damage) - prepend_on_death(fd); - resolve_skill(fd); - // Regen from poison - check_regeneration(fd); -} -//---------------------- $50 attack by assault card implementation ------------- -inline void add_hp(CardStatus* target, unsigned v) -{ - target->m_hp = std::min(target->m_hp + v, target->m_card->m_health); -} -inline void apply_poison(CardStatus* target, unsigned v) -{ - target->m_poisoned = std::max(target->m_poisoned, v); -} -inline int attack_power(CardStatus* att) -{ - return(safe_minus(att->m_card->m_attack + att->m_berserk + att->m_rallied, att->m_weakened)); + unsigned simulations = 0; + for(auto evaluation: evaluated_decks) + { simulations += evaluation.second.second; } + std::cout << "Evaluated " << evaluated_decks.size() << " decks (" << simulations << " + " << skipped_simulations << " simulations)." << std::endl; + std::cout << "Optimized Deck: "; + print_deck_inline(get_deck_cost(d1), best_score, d1); } -// Counter damage dealt to the attacker (att) by defender (def) -// pre-condition: only valid if m_card->m_counter > 0 -inline unsigned counter_damage(CardStatus* att, CardStatus* def) -{ - assert(att->m_card->m_type == CardType::assault); - assert(def->m_card->m_type != CardType::action); - return(safe_minus(def->m_card->m_counter + att->m_enfeebled, att->m_protected)); +//------------------------------------------------------------------------------ +enum Operation { + noop, + simulate, + climb, + reorder, + debug, + debuguntil, +}; +//------------------------------------------------------------------------------ +extern void(*skill_table[num_skills])(Field*, CardStatus* src_status, const SkillSpec&); +void print_available_effects() +{ + std::cout << "Available effects besides activation skills:\n" + " Bloodlust X\n" + " Brigade\n" + " Counterflux\n" + " Divert\n" + " EnduringRage\n" + " Fortification\n" + " Heroism\n" + " Metamorphosis\n" + " Revenge X\n" + " TurningTides\n" + " Virulence\n" + ; } -inline CardStatus* select_first_enemy_wall(Field* fd) +void usage(int argc, char** argv) { - for(unsigned i(0); i < fd->tip->structures.size(); ++i) - { - CardStatus& card(fd->tip->structures[i]); - if(card.m_card->m_wall && card.m_hp > 0) { return(&card); } - } - return(nullptr); + std::cout << "Tyrant Unleashed Optimizer (TUO) " << TYRANT_OPTIMIZER_VERSION << "\n" + "usage: " << argv[0] << " Your_Deck Enemy_Deck [Flags] [Operations]\n" + "\n" + "Your_Deck:\n" + " the name/hash/cards of a custom deck.\n" + "\n" + "Enemy_Deck:\n" + " semicolon separated list of defense decks, syntax:\n" + " deck1[:factor1];deck2[:factor2];...\n" + " where deck is the name/hash/cards of a mission, raid, quest or custom deck, and factor is optional. The default factor is 1.\n" + " example: \'fear:0.2;slowroll:0.8\' means fear is the defense deck 20% of the time, while slowroll is the defense deck 80% of the time.\n" + "\n" + "Flags:\n" + " -e \"\": set the battleground effect; you may use -e multiple times.\n" + " -r: the attack deck is played in order instead of randomly (respects the 3 cards drawn limit).\n" + " -s: use surge (default is fight).\n" + " -t : set the number of threads, default is 4.\n" + " win: simulate/optimize for win rate. default for non-raids.\n" + " defense: simulate/optimize for win rate + stall rate. can be used for defending deck or win rate oriented raid simulations.\n" + " raid: simulate/optimize for average raid damage (ARD). default for raids.\n" + "Flags for climb:\n" + " -c: don't try to optimize the commander.\n" + " -L : restrict deck size between and .\n" + " -o: restrict to the owned cards listed in \"data/ownedcards.txt\".\n" + " -o=: restrict to the owned cards listed in .\n" + " fund : invest SP to upgrade cards.\n" + " target : stop as soon as the score reaches .\n" + "\n" + "Operations:\n" + " sim : simulate battles to evaluate a deck.\n" + " climb : perform hill-climbing starting from the given attack deck, using up to battles to evaluate a deck.\n" + " reorder : optimize the order for given attack deck, using up to battles to evaluate an order.\n" +#ifndef NDEBUG + " debug: testing purpose only. very verbose output. only one battle.\n" + " debuguntil : testing purpose only. fight until the last fight results in range [, ]. recommend to redirect output.\n" +#endif + ; } -inline unsigned valor_damage(Field* fd, CardStatus& status) +std::string skill_description(const Cards& cards, const SkillSpec& s); + +int main(int argc, char** argv) { - if(status.m_card->m_valor > 0) + if (argc == 2 && strcmp(argv[1], "-version") == 0) { - unsigned count_ta(0); - unsigned count_td(0); - for(unsigned i(0); i < fd->tap->assaults.size(); ++i) - { - if(fd->tap->assaults[i].m_hp > 0) { ++count_ta; } - } - for(unsigned i(0); i < fd->tip->assaults.size(); ++i) - { - if(fd->tip->assaults[i].m_hp > 0) { ++count_td; } - } - if(count_ta < count_td) { return(status.m_card->m_valor); } + std::cout << "Tyrant Unleashed Optimizer " << TYRANT_OPTIMIZER_VERSION << std::endl; + return 0; } - return(0); -} - -inline unsigned attack_damage_against_non_assault(Field* fd, CardStatus& att_status) -{ - const Card& att_card(*att_status.m_card); - assert(att_card.m_type == CardType::assault); - // pre modifier damage - unsigned damage(attack_power(&att_status)); - // - if(damage > 0) + if (argc <= 2) { - damage += valor_damage(fd, att_status); + usage(argc, argv); + return 0; } - return(damage); -} - -inline unsigned attack_damage_against_assault(Field* fd, CardStatus& att_status, CardStatus& def_status) -{ - const Card& att_card(*att_status.m_card); - const Card& def_card(*def_status.m_card); - assert(att_card.m_type == CardType::assault); - assert(def_card.m_type == CardType::assault); - // pre modifier damage - unsigned damage(attack_power(&att_status)); - // - if(damage > 0) - { - damage = safe_minus( - damage // pre-modifier damage - + valor_damage(fd, att_status) // valor - + def_status.m_enfeebled // enfeeble - + (def_card.m_flying ? att_card.m_antiair : 0) // anti-air - + (att_card.m_burst > 0 ? (def_status.m_hp == def_card.m_health ? att_card.m_burst : 0) : 0) // burst - // armor + protect + pierce - , safe_minus(def_card.m_armored + def_status.m_protected, att_card.m_pierce)); - } - return(damage); -} - -inline bool alive_assault(Storage& assaults, unsigned index) -{ - return(index >= 0 && assaults.size() > index && assaults[index].m_hp > 0); -} - -void remove_commander_hp(Field* fd, CardStatus& status, unsigned dmg) -{ - assert(status.m_hp > 0); - assert(status.m_card->m_type == CardType::commander); - status.m_hp = safe_minus(status.m_hp, dmg); - if(status.m_hp == 0) { fd->end = true; } -} -//------------------------------------------------------------------------------ -// implementation of one attack by an assault card, against either an enemy -// assault card, the first enemy wall, or the enemy commander. -struct PerformAttack -{ - Field* fd; - CardStatus* att_status; - CardStatus* def_status; - unsigned att_dmg; - bool killed_by_attack; - PerformAttack(Field* fd_, CardStatus* att_status_, CardStatus* def_status_) : - fd(fd_), att_status(att_status_), def_status(def_status_), att_dmg(0), killed_by_attack(false) - {} + unsigned opt_num_threads(4); + DeckStrategy::DeckStrategy opt_your_strategy(DeckStrategy::random); + DeckStrategy::DeckStrategy opt_enemy_strategy(DeckStrategy::random); + std::string opt_forts, opt_enemy_forts; + std::string opt_hand, opt_enemy_hand; + std::string opt_vip; + std::string opt_quest; + std::string opt_target_score; + std::vector fn_suffix_list{"",}; + std::vector opt_owned_cards_str_list; + bool opt_do_optimization(false); + bool opt_keep_commander{false}; + std::vector> opt_todo; + std::vector opt_effects[3]; // 0-you; 1-enemy; 2-global + std::unordered_map opt_bg_effects; + std::vector opt_bg_skills[2]; - template - void op() + for(int argIndex = 3; argIndex < argc; ++argIndex) { - if(attack_power(att_status) > 0) + // Codec + if (strcmp(argv[argIndex], "ext_b64") == 0) { - const bool fly_check(!def_status->m_card->m_flying || att_status->m_card->m_flying || att_status->m_card->m_antiair > 0 || fd->flip()); - if(fly_check) // unnecessary check for structures, commander -> fix later ? - { - // Evaluation order: - // assaults only: fly check - // assaults only: immobilize - // deal damage - // assaults only: (siphon, poison, disease) - // oa: poison, disease, assaults only: berserk, skills - // counter, berserk - // assaults only: (crush, leech if still alive) - // check regeneration - att_dmg = calculate_attack_damage(); - if(att_dmg > 0) - { - immobilize(); - attack_damage(); - siphon_poison_disease(); - } - oa(); - if(att_dmg > 0) - { - if(att_status->m_hp > 0) - { - counter_berserk(); - } - crush_leech(); - } - prepend_on_death(fd); - resolve_skill(fd); - check_regeneration(fd); - } + hash_to_ids = hash_to_ids_ext_b64; + encode_deck = encode_deck_ext_b64; } - } - - template - unsigned calculate_attack_damage() - { - return(attack_damage_against_non_assault(fd, *att_status)); - } - - template - void immobilize() {} - - template - void attack_damage() - { - remove_hp(fd, *def_status, att_dmg); - killed_by_attack = def_status->m_hp == 0; - _DEBUG_MSG("%s attack damage %u\n", status_description(att_status).c_str(), att_dmg); - } - - template - void siphon_poison_disease() {} - - template - void oa() - { - if(def_status->m_card->m_poison_oa > 0) + else if (strcmp(argv[argIndex], "wmt_b64") == 0) { - apply_poison(att_status, def_status->m_card->m_poison_oa); + hash_to_ids = hash_to_ids_wmt_b64; + encode_deck = encode_deck_wmt_b64; } - if(def_status->m_card->m_disease_oa) + else if (strcmp(argv[argIndex], "ddd_b64") == 0) { - att_status->m_diseased = true; + hash_to_ids = hash_to_ids_ddd_b64; + encode_deck = encode_deck_ddd_b64; } - oa_berserk(); - for(auto& oa_skill: def_status->m_card->m_skills_attacked) + // Base Game Mode + else if (strcmp(argv[argIndex], "fight") == 0) { - fd->skill_queue.emplace_back(def_status, oa_skill); - resolve_skill(fd); + gamemode = fight; } - } - - template - void oa_berserk() {} - - template - void counter_berserk() - { - if(def_status->m_card->m_counter > 0) + else if (strcmp(argv[argIndex], "-s") == 0 || strcmp(argv[argIndex], "surge") == 0) { - unsigned counter_dmg(counter_damage(att_status, def_status)); - remove_hp(fd, *att_status, counter_dmg); - _DEBUG_MSG("%s counter %u by %s\n", status_description(att_status).c_str(), counter_dmg, status_description(def_status).c_str()); + gamemode = surge; } - att_status->m_berserk += att_status->m_card->m_berserk; - } - - template - void crush_leech() {} -}; - -template<> -unsigned PerformAttack::calculate_attack_damage() -{ - return(attack_damage_against_assault(fd, *att_status, *def_status)); -} - -template<> -void PerformAttack::immobilize() -{ - if(att_status->m_card->m_immobilize) - { - def_status->m_immobilized |= fd->flip(); - } -} - -template<> -void PerformAttack::attack_damage() -{ - remove_commander_hp(fd, *def_status, att_dmg); - _DEBUG_MSG("%s attack damage %u to commander; commander hp %u\n", status_description(att_status).c_str(), att_dmg, fd->tip->commander.m_hp); -} - -template<> -void PerformAttack::siphon_poison_disease() -{ - if(att_status->m_card->m_siphon > 0) - { - add_hp(&fd->tap->commander, std::min(att_dmg, att_status->m_card->m_siphon)); - _DEBUG_MSG(" \033[1;32m%s siphon %u; hp %u\033[0m\n", status_description(att_status).c_str(), std::min(att_dmg, att_status->m_card->m_siphon), fd->tap->commander.m_hp); - } - if(att_status->m_card->m_poison > 0) - { - apply_poison(def_status, att_status->m_card->m_poison); - } - if(att_status->m_card->m_disease) - { - def_status->m_diseased = true; - } -} - -template<> -void PerformAttack::oa_berserk() { def_status->m_berserk += def_status->m_card->m_berserk_oa; } - -template<> -void PerformAttack::crush_leech() -{ - if(att_status->m_card->m_crush > 0 && killed_by_attack) - { - CardStatus* def_status{select_first_enemy_wall(fd)}; // defending wall - if (def_status != nullptr) + // Base Scoring Mode + else if (strcmp(argv[argIndex], "win") == 0) { - remove_hp(fd, *def_status, att_status->m_card->m_crush); - _DEBUG_MSG("%s crush %u; wall %s hp %u\n", status_description(att_status).c_str(), att_status->m_card->m_crush, status_description(def_status).c_str(), def_status->m_hp); + optimization_mode = OptimizationMode::winrate; } - else + else if (strcmp(argv[argIndex], "defense") == 0) { - remove_commander_hp(fd, fd->tip->commander, att_status->m_card->m_crush); - _DEBUG_MSG("%s crush %u; commander hp %u\n", status_description(att_status).c_str(), att_status->m_card->m_crush, fd->tip->commander.m_hp); + optimization_mode = OptimizationMode::defense; } - } - if(att_status->m_card->m_leech > 0 && att_status->m_hp > 0 && !att_status->m_diseased) - { - add_hp(att_status, std::min(att_dmg, att_status->m_card->m_leech)); - _DEBUG_MSG("%s leech %u; hp: %u.\n", status_description(att_status).c_str(), std::min(att_dmg, att_status->m_card->m_leech), att_status->m_hp); - } -} - -// General attack phase by the currently evaluated assault, taking into accounts exotic stuff such as flurry,swipe,etc. -void attack_phase(Field* fd) -{ - CardStatus* att_status(&fd->tap->assaults[fd->current_ci]); // attacking card - Storage& def_assaults(fd->tip->assaults); - unsigned num_attacks(att_status->m_card->m_flurry > 0 && fd->flip() ? att_status->m_card->m_flurry + 1 : 1); - for(unsigned attack_index(0); attack_index < num_attacks && att_status->m_hp > 0 && fd->tip->commander.m_hp > 0; ++attack_index) - { - // 3 possibilities: - // - 1. attack against the assault in front - // - 2. swipe attack the assault in front and adjacent assaults if any - // - 3. attack against the commander or walls (if there is no assault or if the attacker has the fear attribute) - // Check if attack mode is 1. or 2. (there is a living assault card in front, and no fear) - if(alive_assault(def_assaults, fd->current_ci) && !att_status->m_card->m_fear) - { - // attack mode 1. - if(!att_status->m_card->m_swipe) - { - PerformAttack{fd, att_status, &fd->tip->assaults[fd->current_ci]}.op(); - } - // attack mode 2. - else - { - // attack the card on the left - if(alive_assault(def_assaults, fd->current_ci - 1)) - { - PerformAttack{fd, att_status, &fd->tip->assaults[fd->current_ci-1]}.op(); - } - // stille alive? attack the card in front - if(fd->tip->commander.m_hp > 0 && att_status->m_hp > 0 && alive_assault(def_assaults, fd->current_ci)) - { - PerformAttack{fd, att_status, &fd->tip->assaults[fd->current_ci]}.op(); - } - // still alive? attack the card on the right - if(fd->tip->commander.m_hp > 0 && att_status->m_hp > 0 && alive_assault(def_assaults, fd->current_ci + 1)) - { - PerformAttack{fd, att_status, &fd->tip->assaults[fd->current_ci+1]}.op(); - } - } + else if (strcmp(argv[argIndex], "raid") == 0) + { + optimization_mode = OptimizationMode::raid; } - // attack mode 3. - else + // Mode Package + else if (strcmp(argv[argIndex], "campaign") == 0) { - CardStatus* def_status{select_first_enemy_wall(fd)}; // defending wall - if(def_status != nullptr) - { - PerformAttack{fd, att_status, def_status}.op(); - } - else - { - PerformAttack{fd, att_status, &fd->tip->commander}.op(); - } + gamemode = surge; + optimization_mode = OptimizationMode::campaign; } - } -} - -//---------------------- $65 active skills implementation ---------------------- -unsigned strike_damage(CardStatus* target, unsigned v) -{ - return(safe_minus(v + target->m_enfeebled, target->m_protected)); -} - -template< - bool C - , typename T1 - , typename T2 - > -struct if_ -{ - typedef T1 type; -}; - -template< - typename T1 - , typename T2 - > -struct if_ -{ - typedef T2 type; -}; - -template -inline bool skill_predicate(CardStatus* c) -{ assert(false); return(false); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ - if(c->m_hp > 0 && (c->m_delay == 0 || c->blitz) && !c->m_jammed && !c->m_frozen) - { - for(auto& s: c->m_card->m_skills) + else if (strcmp(argv[argIndex], "pvp") == 0) { - // Any quantifiable skill except augment - if(std::get<1>(s) > 0 && std::get<0>(s) != augment && std::get<0>(s) != augment_all && std::get<0>(s) != summon) { return(true); } + gamemode = fight; + optimization_mode = OptimizationMode::winrate; } - } - return(false); -} - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_delay <= 1 && c->m_hp > 0 && !c->m_chaos); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ - return(c->m_hp > 0 && ( - c->m_chaos || - c->m_diseased || - c->m_enfeebled > 0 || - (c->m_frozen && c->m_delay == 0) || - c->m_jammed || - c->m_poisoned - )); -} - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0 && !c->m_frozen); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0 && c->m_hp < c->m_card->m_health && !c->m_diseased); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_faction != bloodthirsty); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_delay <= 1 && c->m_hp > 0 && !c->m_jammed); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return((c->m_delay == 0 || c->blitz) && c->m_hp > 0 && !c->m_jammed && !c->m_frozen && !c->m_immobilized); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_delay > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_hp > 0 && c->m_hp < c->m_card->m_health && !c->m_diseased); } - -template<> -inline bool skill_predicate(CardStatus* c) -{ return(c->m_delay <= 1 && c->m_hp > 0 && attack_power(c) > 0 && !c->m_jammed && !c->m_frozen && !c->m_immobilized); } - -template -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ assert(false); } - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_augmented += v; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_chaos = true; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_chaos = false; - c->m_diseased = false; - c->m_enfeebled = 0; - c->m_frozen = false; - c->m_immobilized = false; - c->m_jammed = false; - c->m_poisoned = 0; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_enfeebled += v; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_frozen = true; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - add_hp(c, v); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_faction = bloodthirsty; - c->m_infused = true; - c->infused_skills.clear(); - for(auto& skill: c->m_card->m_skills) - { - c->infused_skills.emplace_back(std::get<0>(skill), std::get<1>(skill), std::get<2>(skill) == allfactions ? allfactions : bloodthirsty); - } -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_jammed = fd->flip(); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_protected += v; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_rallied += v; -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_delay = safe_minus(c->m_delay, v); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - _DEBUG_MSG(" \033[1;31mshock %u. hp %u -> %u.\033[0m", v, c->m_hp, safe_minus(c->m_hp, v)); - c->m_hp = safe_minus(c->m_hp, v); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - _DEBUG_MSG(" hp %u -> %u.", c->m_hp, safe_minus(c->m_hp, v)); - remove_hp(fd, *c, v); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - remove_hp(fd, *c, strike_damage(c, v)); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - add_hp(c, v); -} - -template<> -inline void perform_skill(Field* fd, CardStatus* c, unsigned v) -{ - c->m_weakened += v; -} - -template -inline unsigned select_fast(Field* fd, CardStatus* src_status, const std::vector& cards, const SkillSpec& s) -{ - unsigned array_head{0}; - if(std::get<2>(s) == allfactions) - { - for(auto card: cards) + else if (strcmp(argv[argIndex], "pvp-defense") == 0) { - if(skill_predicate(card)) - { - fd->selection_array[array_head] = card; - ++array_head; - } + gamemode = surge; + optimization_mode = OptimizationMode::defense; } - } - else - { - for(auto card: cards) + else if (strcmp(argv[argIndex], "brawl") == 0) { - if(card->m_faction == std::get<2>(s) && - skill_predicate(card)) - { - fd->selection_array[array_head] = card; - ++array_head; - } + gamemode = surge; + optimization_mode = OptimizationMode::brawl; } - } - return(array_head); -} - -template -inline unsigned select_rally_like(Field* fd, CardStatus* src_status, const std::vector& cards, const SkillSpec& s) -{ - unsigned array_head{0}; - unsigned card_index(fd->current_phase == Field::assaults_phase ? fd->current_ci : 0); - if(std::get<2>(s) == allfactions) - { - for(; card_index < cards.size(); ++card_index) + else if (strcmp(argv[argIndex], "brawl-defense") == 0) { - if(skill_predicate(cards[card_index])) - { - fd->selection_array[array_head] = cards[card_index]; - ++array_head; - } + gamemode = fight; + optimization_mode = OptimizationMode::brawl; } - } - else - { - for(; card_index < cards.size(); ++card_index) + else if (strcmp(argv[argIndex], "gw") == 0) { - if(cards[card_index]->m_faction == std::get<2>(s) && - skill_predicate(cards[card_index])) - { - fd->selection_array[array_head] = cards[card_index]; - ++array_head; - } + gamemode = surge; + optimization_mode = OptimizationMode::winrate; } - } - return(array_head); -} - -template<> -inline unsigned select_fast(Field* fd, CardStatus* src_status, const std::vector& cards, const SkillSpec& s) -{ - return(select_rally_like(fd, src_status, cards, s)); -} - -template<> -inline unsigned select_fast(Field* fd, CardStatus* src_status, const std::vector& cards, const SkillSpec& s) -{ - return(select_rally_like(fd, src_status, cards, s)); -} - -template<> -inline unsigned select_fast(Field* fd, CardStatus* src_status, const std::vector& cards, const SkillSpec& s) -{ - // mimiced supply by a structure, etc ? - if(!(src_status && src_status->m_card->m_type == CardType::assault)) { return(0); } - unsigned array_head{0}; - const unsigned min_index(src_status->m_index - (src_status->m_index == 0 ? 0 : 1)); - const unsigned max_index(src_status->m_index + (src_status->m_index == cards.size() - 1 ? 0 : 1)); - for(unsigned card_index(min_index); card_index <= max_index; ++card_index) - { - if(skill_predicate(cards[card_index])) + else if (strcmp(argv[argIndex], "gw-defense") == 0) { - fd->selection_array[array_head] = cards[card_index]; - ++array_head; + gamemode = fight; + optimization_mode = OptimizationMode::defense; } - } - return(array_head); -} - -inline unsigned select_infuse(Field* fd, const SkillSpec& s) -{ - unsigned array_head{0}; - // Select candidates among attacker's assaults - for(auto card_status: fd->tap->assaults.m_indirect) - { - if(skill_predicate(card_status)) + // Others + else if (strcmp(argv[argIndex], "keep-commander") == 0 || strcmp(argv[argIndex], "-c") == 0) { - fd->selection_array[array_head] = card_status; - ++array_head; + opt_keep_commander = true; } - } - // Select candidates among defender's assaults - for(auto card_status: fd->tip->assaults.m_indirect) - { - if(skill_predicate(card_status)) + else if (strcmp(argv[argIndex], "effect") == 0 || strcmp(argv[argIndex], "-e") == 0) { - fd->selection_array[array_head] = card_status; - ++array_head; + opt_effects[2].push_back(argv[argIndex + 1]); + argIndex += 1; } - } - return(array_head); -} - -inline std::vector& skill_targets_hostile_assault(Field* fd, CardStatus* src_status) -{ - return(fd->players[src_status ? (src_status->m_chaos ? src_status->m_player : opponent(src_status->m_player)) : fd->tipi]->assaults.m_indirect); -} - -inline std::vector& skill_targets_allied_assault(Field* fd, CardStatus* src_status) -{ - return(fd->players[src_status ? src_status->m_player : fd->tapi]->assaults.m_indirect); -} - -inline std::vector& skill_targets_hostile_structure(Field* fd, CardStatus* src_status) -{ - return(fd->players[src_status ? (src_status->m_chaos ? src_status->m_player : opponent(src_status->m_player)) : fd->tipi]->structures.m_indirect); -} - -inline std::vector& skill_targets_allied_structure(Field* fd, CardStatus* src_status) -{ - return(fd->players[src_status ? src_status->m_player : fd->tapi]->structures.m_indirect); -} - -template -std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ - std::cout << "skill_targets: Error: no specialization for " << skill_names[skill] << "\n"; - assert(false); -} - -template<> inline std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_allied_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_assault(fd, src_status)); } - -template<> std::vector& skill_targets(Field* fd, CardStatus* src_status) -{ return(skill_targets_hostile_structure(fd, src_status)); } - -template -void maybeTriggerRegen(Field* fd) -{ -} - -template<> -void maybeTriggerRegen(Field* fd) -{ - fd->skill_queue.emplace_front(nullptr, std::make_tuple(trigger_regen, 0, allfactions)); -} - -template -CardStatus* get_target_hostile_fast(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - std::vector& cards(skill_targets(fd, src_status)); - unsigned array_head{select_fast(fd, src_status, cards, s)}; - if(array_head > 0) - { - unsigned rand_index(fd->rand(0, array_head - 1)); - CardStatus* c(fd->selection_array[rand_index]); - // intercept - if(src_status && !src_status->m_chaos) + else if (strcmp(argv[argIndex], "ye") == 0 || strcmp(argv[argIndex], "yeffect") == 0) { - CardStatus* intercept_card(nullptr); - if(rand_index > 0) - { - CardStatus* left_status(fd->selection_array[rand_index-1]); - if(left_status->m_card->m_intercept && left_status->m_index == c->m_index-1) - { - intercept_card = left_status; - } - } - if(rand_index+1 < array_head && !intercept_card) - { - CardStatus* right_status(fd->selection_array[rand_index+1]); - if(right_status->m_card->m_intercept && right_status->m_index == c->m_index+1) - { - intercept_card = right_status; - } - } - if(intercept_card) { c = intercept_card; } + opt_effects[0].push_back(argv[argIndex + 1]); + argIndex += 1; } - return(c); - } - return(nullptr); -} - -template -void perform_targetted_hostile_fast(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - // null status = action card - CardStatus* c(get_target_hostile_fast(fd, src_status, s)); - if(c) - { - // evade - if(!c->m_card->m_evade || fd->flip()) - { - _DEBUG_MSG("%s (%u) from %s on %s.\n", skill_names[skill_id].c_str(), std::get<1>(s), status_description(src_status).c_str(), status_description(c).c_str()); - // skill - perform_skill(fd, c, std::get<1>(s)); - // payback - if(c->m_card->m_payback && - src_status && - src_status->m_card->m_type == CardType::assault && - !src_status->m_chaos && - src_status->m_hp > 0 && - fd->flip()) - { - // payback evade - if(skill_predicate(src_status) && - (!src_status->m_card->m_evade || fd->flip())) - { - _DEBUG_MSG("Payback (%s %u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), src_status->m_card->m_name.c_str()); - // payback skill - perform_skill(fd, src_status, std::get<1>(s)); - } - } + else if (strcmp(argv[argIndex], "ee") == 0 || strcmp(argv[argIndex], "eeffect") == 0) + { + opt_effects[1].push_back(argv[argIndex + 1]); + argIndex += 1; } - } - maybeTriggerRegen::T>(fd); - prepend_on_death(fd); -} - -template -void perform_targetted_allied_fast(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - std::vector& cards(skill_targets(fd, src_status)); - unsigned array_head{select_fast(fd, src_status, cards, s)}; - if(array_head > 0) - { - CardStatus* c(fd->selection_array[fd->rand(0, array_head - 1)]); - _DEBUG_MSG(" \033[1;34m%s: %s on %s\033[0m", status_description(src_status).c_str(), skill_description(s).c_str(), status_description(c).c_str()); - perform_skill(fd, c, std::get<1>(s)); - _DEBUG_MSG("\n"); - if(c->m_card->m_tribute && - src_status && - src_status->m_card->m_type == CardType::assault && - src_status != c && - src_status->m_hp > 0 && - fd->flip()) - { - if(skill_predicate(src_status)) - { - _DEBUG_MSG("Tribute (%s %u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), src_status->m_card->m_name.c_str()); - perform_skill(fd, src_status, std::get<1>(s)); - } + else if (strcmp(argv[argIndex], "freeze") == 0 || strcmp(argv[argIndex], "-F") == 0) + { + freezed_cards = atoi(argv[argIndex + 1]); + argIndex += 1; } - } -} - -template -void perform_global_hostile_fast(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - std::vector& cards(skill_targets(fd, src_status)); - unsigned array_head{select_fast(fd, src_status, cards, s)}; - unsigned payback_count(0); - for(unsigned s_index(0); s_index < array_head; ++s_index) - { - CardStatus* c(fd->selection_array[s_index]); - if(!c->m_card->m_evade || fd->flip()) - { - _DEBUG_MSG("%s (%u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), c->m_card->m_name.c_str()); - perform_skill(fd, c, std::get<1>(s)); - // payback - if(c->m_card->m_payback && - src_status && - src_status->m_card->m_type == CardType::assault && - !src_status->m_chaos && - src_status->m_hp > 0 && - fd->flip()) - { - ++payback_count; - } + else if(strcmp(argv[argIndex], "-L") == 0) + { + min_deck_len = atoi(argv[argIndex + 1]); + max_deck_len = atoi(argv[argIndex + 2]); + argIndex += 2; } - } - for(unsigned i(0); i < payback_count && skill_predicate(src_status); ++i) - { - if((!src_status->m_card->m_evade || fd->flip())) + else if(strcmp(argv[argIndex], "-o-") == 0) { - _DEBUG_MSG("Payback (%s %u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), src_status->m_card->m_name.c_str()); - perform_skill(fd, src_status, std::get<1>(s)); + use_owned_cards = false; } - } - maybeTriggerRegen::T>(fd); - prepend_on_death(fd); -} - -template -void perform_global_allied_fast(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - std::vector& cards(skill_targets(fd, src_status)); - unsigned array_head{select_fast(fd, src_status, cards, s)}; - for(unsigned s_index(0); s_index < array_head; ++s_index) - { - CardStatus* c(fd->selection_array[s_index]); - _DEBUG_MSG("%s (%u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), c->m_card->m_name.c_str()); - perform_skill(fd, c, std::get<1>(s)); - if(c->m_card->m_tribute && - src_status && - src_status->m_card->m_type == CardType::assault && - src_status != c && - src_status->m_hp > 0 && - fd->flip()) - { - if(skill_predicate(src_status)) - { - _DEBUG_MSG("Tribute (%s %u) on (%s)\n", skill_names[skill_id].c_str(), std::get<1>(s), src_status->m_card->m_name.c_str()); - perform_skill(fd, src_status, std::get<1>(s)); - } + else if(strcmp(argv[argIndex], "-o") == 0) + { + opt_owned_cards_str_list.push_back("data/ownedcards.txt"); + use_owned_cards = true; } - } -} - -void perform_infuse(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - unsigned array_head{0}; - // Select candidates among attacker's assaults - for(auto card_status: fd->players[src_status->m_player]->assaults.m_indirect) - { - if(skill_predicate(card_status)) + else if(strncmp(argv[argIndex], "-o=", 3) == 0) { - fd->selection_array[array_head] = card_status; - ++array_head; + opt_owned_cards_str_list.push_back(argv[argIndex] + 3); + use_owned_cards = true; } - } - // Select candidates among defender's assaults - for(auto card_status: fd->players[opponent(src_status->m_player)]->assaults.m_indirect) - { - if(skill_predicate(card_status)) + else if(strncmp(argv[argIndex], "_", 1) == 0) { - fd->selection_array[array_head] = card_status; - ++array_head; + fn_suffix_list.push_back(argv[argIndex]); } - } - if(array_head > 0) - { - CardStatus* c(fd->selection_array[fd->rand(0, array_head - 1)]); - // check evade for enemy assaults only - if(c->m_player == src_status->m_player || !c->m_card->m_evade || fd->flip()) + else if(strcmp(argv[argIndex], "fund") == 0) { - _DEBUG_MSG("%s on (%s).", skill_names[infuse].c_str(), c->m_card->m_name.c_str()); - perform_skill(fd, c, std::get<1>(s)); - _DEBUG_MSG("\n"); + fund = atoi(argv[argIndex+1]); + argIndex += 1; } - } -} - -// a summoned card's on play skills seem to be evaluated before any other skills on the skill queue. -inline void prepend_skills(Field* fd, CardStatus* status) -{ - for(auto& skill: boost::adaptors::reverse(status->m_card->m_skills_played)) - { - fd->skill_queue.emplace_front(status, skill); - } -} -void summon_card(Field* fd, unsigned player, const Card* summoned) -{ - assert(summoned->m_type == CardType::assault || summoned->m_type == CardType::structure); - Hand* hand{fd->players[player]}; - if(hand->assaults.size() + hand->structures.size() < 100) - { - Storage* storage{summoned->m_type == CardType::assault ? &hand->assaults : &hand->structures}; - CardStatus& card_status(storage->add_back()); - card_status.set(summoned); - card_status.m_index = storage->size() - 1; - card_status.m_player = player; - _DEBUG_MSG("Summoned [%s] as %s %d\n", summoned->m_name.c_str(), cardtype_names[summoned->m_type].c_str(), card_status.m_index); - prepend_skills(fd, &card_status); - if(card_status.m_card->m_blitz && - fd->players[opponent(player)]->assaults.size() > card_status.m_index && - fd->players[opponent(player)]->assaults[card_status.m_index].m_hp > 0 && - fd->players[opponent(player)]->assaults[card_status.m_index].m_delay == 0) + else if (strcmp(argv[argIndex], "random") == 0) { - card_status.blitz = true; + opt_your_strategy = DeckStrategy::random; } - } -} -void perform_summon(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - summon_card(fd, src_status ? src_status->m_player : fd->tapi, fd->cards.by_id(std::get<1>(s))); -} - -void perform_trigger_regen(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - check_regeneration(fd); -} - -void perform_shock(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - _DEBUG_MSG("Performing shock on (%s).", fd->tip->commander.m_card->m_name.c_str()); - perform_skill(fd, &fd->tip->commander, std::get<1>(s)); - _DEBUG_MSG("\n"); -} - -void perform_supply(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - perform_global_allied_fast(fd, src_status, s); -} - -// Special rules for mimic : -// cannot mimic mimic, -// structures cannot mimic supply, -// and is not affected by payback. -void perform_mimic(Field* fd, CardStatus* src_status, const SkillSpec& s) -{ - // mimic cannot be triggered by anything. So it should be the only skill in the unresolved skill table. - // so we can probably clear it safely. This is necessary, because mimic calls resolve_skill as well (infinite loop). - fd->skill_queue.clear(); - CardStatus* c(get_target_hostile_fast(fd, src_status, s)); - if(c) - { - _DEBUG_MSG("%s on (%s)\n", skill_names[std::get<0>(s)].c_str(), c->m_card->m_name.c_str()); - for(auto skill: c->m_card->m_skills) + else if(strcmp(argv[argIndex], "-r") == 0 || strcmp(argv[argIndex], "ordered") == 0) { - if(src_status && src_status->m_card->m_type == CardType::assault && src_status->m_hp == 0) - { break; } - if(std::get<0>(skill) != mimic && - (std::get<0>(skill) != supply || (src_status && src_status->m_card->m_type == CardType::assault))) - { - SkillSpec mimic_s(std::get<0>(skill), std::get<1>(skill), allfactions); - fd->skill_queue.emplace_back(src_status, src_status && src_status->m_augmented > 0 ? augmented_skill(src_status, mimic_s) : mimic_s); - resolve_skill(fd); - check_regeneration(fd); - } + opt_your_strategy = DeckStrategy::ordered; } - } -} - -//---------------------- $70 More xml parsing: missions and raids -------------- -// + also the custom decks -struct Decks -{ - std::map custom_decks; - std::list mission_decks; - std::map mission_decks_by_id; - std::map mission_decks_by_name; - std::list raid_decks; - std::map raid_decks_by_id; - std::map raid_decks_by_name; - - ~Decks() - { - for(auto& obj: custom_decks) + else if(strcmp(argv[argIndex], "exact-ordered") == 0) { - delete(obj.second); + opt_your_strategy = DeckStrategy::exact_ordered; } - } -}; - -template Iterator advance_until(Iterator it, Iterator it_end, Functor f) -{ - while(it != it_end) - { - if(f(*it)) + else if(strcmp(argv[argIndex], "enemy:ordered") == 0) { - break; + opt_enemy_strategy = DeckStrategy::ordered; } - ++it; - } - return(it); -} - -// take care that "it" is 1 past current. -template Iterator recede_until(Iterator it, Iterator it_beg, Functor f) -{ - if(it == it_beg) { return(it_beg); } - --it; - do - { - if(f(*it)) + else if(strcmp(argv[argIndex], "enemy:exact-ordered") == 0) { - return(++it); + opt_enemy_strategy = DeckStrategy::exact_ordered; } - --it; - } while(it != it_beg); - return(it_beg); -} - -template Iterator read_token(Iterator it, Iterator it_end, Functor f, boost::optional& token) -{ - Iterator token_start = advance_until(it, it_end, [](const char& c){return(c != ' ');}); - Iterator token_end_after_spaces = advance_until(token_start, it_end, f); - if(token_start != token_end_after_spaces) - { - Iterator token_end = recede_until(token_end_after_spaces, token_start, [](const char& c){return(c != ' ');}); - token = boost::lexical_cast(std::string{token_start, token_end}); - return(token_end_after_spaces); - } - return(token_end_after_spaces); -} - -// Error codes: -// 2 -> file not readable -// 3 -> error while parsing file -unsigned read_custom_decks(Cards& cards, std::string filename, std::map& custom_decks) -{ - std::ifstream decks_file(filename.c_str()); - if(!decks_file.is_open()) - { - std::cerr << "File " << filename << " could not be opened\n"; - return(2); - } - unsigned num_line(0); - decks_file.exceptions(std::ifstream::badbit); - try - { - while(decks_file && !decks_file.eof()) + else if (strcmp(argv[argIndex], "endgame") == 0) { - std::vector card_ids; - std::string deck_string; - getline(decks_file, deck_string); - ++num_line; - if(deck_string.size() > 0) - { - if(strncmp(deck_string.c_str(), "//", 2) == 0) - { - continue; - } - boost::tokenizer > deck_tokens{deck_string, boost::char_delimiters_separator{false, ":,", ""}}; - auto token_iter = deck_tokens.begin(); - boost::optional deck_name; - if(token_iter != deck_tokens.end()) - { - read_token(token_iter->begin(), token_iter->end(), [](char c){return(false);}, deck_name); - if(!deck_name || (*deck_name).size() == 0) - { - std::cerr << "Error in file " << filename << " at line " << num_line << ", could not read the deck name.\n"; - continue; - } - } - else - { - std::cerr << "Error in file " << filename << " at line " << num_line << ", could not read the deck name.\n"; - continue; - } - ++token_iter; - for(; token_iter != deck_tokens.end(); ++token_iter) - { - std::string card_spec(*token_iter); - try - { - auto card_spec_iter = card_spec.begin(); - boost::optional card_name; - card_spec_iter = read_token(card_spec_iter, card_spec.end(), [](char c){return(c=='[' || c=='#' || c=='\r');}, card_name); - if(!card_name) - { - std::cerr << "Error in file " << filename << " at line " << num_line << " while parsing card " << card_spec << " in deck " << deck_name << "\n"; - break; - } - else - { - boost::optional card_id; - if(*card_spec_iter == '[') - { - ++card_spec_iter; - card_spec_iter = read_token(card_spec_iter, card_spec.end(), [](char c){return(c==']');}, card_id); - card_spec_iter = advance_until(card_spec_iter, card_spec.end(), [](char c){return(c!=' ');}); - } - boost::optional card_num; - if(*card_spec_iter == '#') - { - ++card_spec_iter; - card_spec_iter = read_token(card_spec_iter, card_spec.end(), [](char c){return(c < '0' || c > '9');}, card_num); - } - unsigned resolved_id{card_id ? *card_id : 0}; - if(resolved_id == 0) - { - auto card_it = cards.player_cards_by_name.find(*card_name); - if(card_it != cards.player_cards_by_name.end()) - { - resolved_id = card_it->second->m_id; - } - else - { - std::cerr << "Error in file " << filename << " at line " << num_line << " while parsing card " << card_spec << " in deck " << *deck_name << ": card not found\n"; - break; - } - } - for(unsigned i(0); i < (card_num ? *card_num : 1); ++i) - { - card_ids.push_back(resolved_id); - } - } - } - catch(boost::bad_lexical_cast e) - { - std::cerr << "Error in file " << filename << " at line " << num_line << " while parsing card " << card_spec << " in deck " << deck_name << "\n"; - } - } - if(deck_name) - { - custom_decks.insert({*deck_name, new DeckRandom{cards, card_ids}}); - } - } + use_top_level_card = true; + use_fused_card_level = atoi(argv[argIndex+1]); + argIndex += 1; } - } - catch (std::ifstream::failure e) - { - std::cerr << "Exception while parsing the file " << filename << " (badbit is set).\n"; - e.what(); - return(3); - } -} -//------------------------------------------------------------------------------ -void read_missions(Decks& decks, Cards& cards, std::string filename) -{ - std::vector buffer; - xml_document<> doc; - parse_file(filename.c_str(), buffer, doc); - xml_node<>* root = doc.first_node(); - for(xml_node<>* mission_node = root->first_node(); - mission_node; - mission_node = mission_node->next_sibling()) - { - if(strcmp(mission_node->name(), "mission") == 0) - { - std::vector card_ids; - xml_node<>* id_node(mission_node->first_node("id")); - int id(id_node ? atoi(id_node->value()) : -1); - xml_node<>* name_node(mission_node->first_node("name")); - std::string deck_name{name_node->value()}; - xml_node<>* commander_node(mission_node->first_node("commander")); - card_ids.push_back(atoi(commander_node->value())); - xml_node<>* deck_node(mission_node->first_node("deck")); - for(xml_node<>* card_node = deck_node->first_node(); - card_node; - card_node = card_node->next_sibling()) - { - unsigned card_id{atoi(card_node->value())}; - // Handle the replacement art cards - if(cards.replace.find(card_id) != cards.replace.end()) - { - card_id = cards.replace[card_id]; - } - card_ids.push_back(card_id); - } - decks.mission_decks.push_back(DeckRandom{cards, card_ids}); - DeckRandom* deck = &decks.mission_decks.back(); - decks.mission_decks_by_id[id] = deck; - decks.mission_decks_by_name[deck_name] = deck; + else if (strcmp(argv[argIndex], "quest") == 0) + { + opt_quest = argv[argIndex+1]; + argIndex += 1; + } + else if(strcmp(argv[argIndex], "threads") == 0 || strcmp(argv[argIndex], "-t") == 0) + { + opt_num_threads = atoi(argv[argIndex+1]); + argIndex += 1; + } + else if(strcmp(argv[argIndex], "target") == 0) + { + opt_target_score = argv[argIndex+1]; + argIndex += 1; + } + else if(strcmp(argv[argIndex], "turnlimit") == 0) + { + turn_limit = atoi(argv[argIndex+1]); + argIndex += 1; + } + else if(strcmp(argv[argIndex], "mis") == 0) + { + min_increment_of_score = atof(argv[argIndex+1]); + argIndex += 1; + } + else if(strcmp(argv[argIndex], "cl") == 0) + { + confidence_level = atof(argv[argIndex+1]); + argIndex += 1; + } + else if(strcmp(argv[argIndex], "+ci") == 0) + { + show_ci = true; } - } -} -//------------------------------------------------------------------------------ -void read_raids(Decks& decks, Cards& cards, std::string filename) -{ - std::vector buffer; - xml_document<> doc; - parse_file(filename.c_str(), buffer, doc); - xml_node<>* root = doc.first_node(); - for(xml_node<>* raid_node = root->first_node(); - raid_node; - raid_node = raid_node->next_sibling()) - { - if(strcmp(raid_node->name(), "raid") == 0) - { - std::vector always_cards; - std::vector > > some_cards; - xml_node<>* id_node(raid_node->first_node("id")); - int id(id_node ? atoi(id_node->value()) : -1); - xml_node<>* name_node(raid_node->first_node("name")); - std::string deck_name{name_node->value()}; - xml_node<>* commander_node(raid_node->first_node("commander")); - const Card* commander_card{cards.by_id(atoi(commander_node->value()))}; - xml_node<>* deck_node(raid_node->first_node("deck")); - xml_node<>* always_node{deck_node->first_node("always_include")}; - if(always_node) - { - for(xml_node<>* card_node = always_node->first_node(); - card_node; - card_node = card_node->next_sibling()) - { - unsigned card_id{atoi(card_node->value())}; - // Handle the replacement art cards - if(cards.replace.find(card_id) != cards.replace.end()) - { - card_id = cards.replace[card_id]; - } - always_cards.push_back(cards.by_id(card_id)); - } - } - for(xml_node<>* pool_node = always_node->next_sibling(); - pool_node; - pool_node = pool_node->next_sibling()) - { - if(strcmp(pool_node->name(), "card_pool") == 0) - { - unsigned num_cards_from_pool{atoi(pool_node->first_attribute("amount")->value())}; - std::vector cards_from_pool; - - for(xml_node<>* card_node = pool_node->first_node(); - card_node; - card_node = card_node->next_sibling()) - { - unsigned card_id{atoi(card_node->value())}; - // Handle the replacement art cards - if(cards.replace.find(card_id) != cards.replace.end()) - { - card_id = cards.replace[card_id]; - } - cards_from_pool.push_back(cards.by_id(card_id)); - } - some_cards.push_back(std::make_pair(num_cards_from_pool, cards_from_pool)); - } - } - decks.raid_decks.push_back(DeckRandom{commander_card, always_cards, some_cards}); - DeckRandom* deck = &decks.raid_decks.back(); - decks.raid_decks_by_id[id] = deck; - decks.raid_decks_by_name[deck_name] = deck; + else if(strcmp(argv[argIndex], "+hm") == 0) + { + use_harmonic_mean = true; } - } -} -//------------------------------------------------------------------------------ -void load_decks(Decks& decks, Cards& cards) -{ - try - { - read_missions(decks, cards, "missions.xml"); - } - catch(const rapidxml::parse_error& e) - { - std::cout << "\nException while loading decks from file missions.xml\n"; - } - try - { - read_raids(decks, cards, "raids.xml"); - } - catch(const rapidxml::parse_error& e) - { - std::cout << "\nException while loading decks from file raids.xml\n"; - } - if(boost::filesystem::exists("Custom.txt")) - { - try + else if(strcmp(argv[argIndex], "seed") == 0) { - read_custom_decks(cards, std::string{"Custom.txt"}, decks.custom_decks); + sim_seed = atoi(argv[argIndex+1]); + argIndex += 1; } - catch(const std::runtime_error& e) + else if(strcmp(argv[argIndex], "-v") == 0) { - std::cout << "Exception while loading custom decks: " << e.what() << "\n"; + -- debug_print; } - } -} -//------------------------------------------------------------------------------ -DeckIface* find_deck(const Decks& decks, std::string name) -{ - auto it1 = decks.mission_decks_by_name.find(name); - if(it1 != decks.mission_decks_by_name.end()) - { - return(it1->second); - } - auto it2 = decks.raid_decks_by_name.find(name); - if(it2 != decks.raid_decks_by_name.end()) - { - return(it2->second); - } - auto it3 = decks.custom_decks.find(name); - if(it3 != decks.custom_decks.end()) - { - return(it3->second); - } - return(nullptr); -} -//---------------------- $80 deck optimization --------------------------------- -//------------------------------------------------------------------------------ -// Owned cards -//------------------------------------------------------------------------------ -std::map owned_cards; -bool use_owned_cards{false}; -void read_owned_cards(Cards& cards) -{ - std::ifstream owned_file{"ownedcards.txt"}; - std::string owned_str{(std::istreambuf_iterator(owned_file)), std::istreambuf_iterator()}; - boost::tokenizer > tok{owned_str, boost::char_delimiters_separator{false, "()\n", ""}}; - for(boost::tokenizer >::iterator beg=tok.begin(); beg!=tok.end();++beg) - { - std::string name{*beg}; - ++beg; - assert(beg != tok.end()); - unsigned num{atoi((*beg).c_str())}; - auto card_itr = cards.player_cards_by_name.find(name); - if(card_itr == cards.player_cards_by_name.end()) + else if(strcmp(argv[argIndex], "+v") == 0) { - std::cerr << "Error in file ownedcards.txt, the card \"" << name << "\" does not seem to be a valid card.\n"; + ++ debug_print; } - else + else if(strcmp(argv[argIndex], "vip") == 0) { - owned_cards[card_itr->second->m_id] = num; + opt_vip = argv[argIndex + 1]; + argIndex += 1; } - } -} - -// No raid rewards from 500 and 1k honor for ancient raids -// No very hard to get rewards (level >= 150, faction >= 13) -// No WB -// No UB -bool cheap_1(const Card* card) -{ - if(card->m_set == 2 || card->m_set == 3 || card->m_set == 4 || card->m_set == 6 || card->m_set == 5001 || card->m_set == 9000) { return(false); } - // Ancient raids rewards - // pantheon - if(card->m_id == 567 || card->m_id == 566) { return(false); } - // sentinel - if(card->m_id == 572 || card->m_id == 564) { return(false); } - // lithid - if(card->m_id == 570 || card->m_id == 571) { return(false); } - // hydra - if(card->m_id == 565 || card->m_id == 568) { return(false); } - // Arachnous - if(card->m_id == 432) { return(false); } - // Shrouded defiler - if(card->m_id == 434) { return(false); } - // Emergency fire - if(card->m_id == 3021) { return(false); } - // Turbo commando - if(card->m_id == 428) { return(false); } - // Solar powerhouse - if(card->m_id == 530) { return(false); } - // Utopia Beacon - if(card->m_id == 469) { return(false); } - return(true); -} - -// Top commanders -std::set top_commanders{ - // common commanders: - 1105, // Opak - 1121, // Daizon - // uncommon commanders: - 1031, // Dalia - 1017, // Duncan - 1120, // Emanuel - 1102, // Korvald - // rare commanders: - 1021, // Atlas - 1153, // Daedalus - 1045, // Dracorex - 1099, // Empress - 1116, // Gaia - 1182, // Gialdrea - 1050, // IC - 1184, // Kleave - 1004, // LoT - 1123, // LtW - 1171, // Nexor - 1104, // Stavros - 1152, // Svetlana - 1141, // Tabitha - 1172, // Teiffa - // halcyon + terminus: - 1203, // Halcyon the Corrupt shitty artwork - 1204, // Halcyon the Corrupt LE - 1200, // Corra - 1049, // Lord Halcyon - 1198, // Virulentus - 1199, // Lord Silus - }; -//------------------------------------------------------------------------------ -bool suitable_non_commander(DeckIface& deck, unsigned slot, const Card* card) -{ - assert(card->m_type != CardType::commander); - if(use_owned_cards) - { - if(owned_cards.find(card->m_id) == owned_cards.end()) { return(false); } - else + else if(strcmp(argv[argIndex], "hand") == 0) // set initial hand for test { - unsigned num_in_deck{0}; - for(unsigned i(0); i < deck.cards.size(); ++i) - { - if(i != slot && deck.cards[i]->m_id == card->m_id) - { - ++num_in_deck; - } - } - if(owned_cards.find(card->m_id)->second <= num_in_deck) { return(false); } + opt_hand = argv[argIndex + 1]; + argIndex += 1; } - } - if(card->m_rarity == 4) // legendary - 1 per deck - { - for(unsigned i(0); i < deck.cards.size(); ++i) + else if(strcmp(argv[argIndex], "enemy:hand") == 0) // set enemies' initial hand for test { - if(i != slot && deck.cards[i]->m_rarity == 4) - { - return(false); - } + opt_enemy_hand = argv[argIndex + 1]; + argIndex += 1; } - } - if(card->m_unique) // unique - 1 card with same id per deck - { - for(unsigned i(0); i < deck.cards.size(); ++i) + else if (strcmp(argv[argIndex], "yf") == 0 || strcmp(argv[argIndex], "yfort") == 0) // set forts { - if(i != slot && deck.cards[i]->m_id == card->m_id) - { - return(false); - } + opt_forts = std::string(argv[argIndex + 1]); + argIndex += 1; } - } - return(true); -} - -bool keep_commander{false}; -bool suitable_commander(const Card* card) -{ - assert(card->m_type == CardType::commander); - if(keep_commander) { return(false); } - if(use_owned_cards) - { - auto owned_iter = owned_cards.find(card->m_id); - if(owned_iter == owned_cards.end()) { return(false); } - else + else if (strcmp(argv[argIndex], "ef") == 0 || strcmp(argv[argIndex], "efort") == 0) // set enemies' forts { - if(owned_iter->second <= 0) { return(false); } + opt_enemy_forts = std::string(argv[argIndex + 1]); + argIndex += 1; } - } - if(top_commanders.find(card->m_id) == top_commanders.end()) { return(false); } - return(true); -} -//------------------------------------------------------------------------------ -double compute_efficiency(const std::pair , unsigned>& results) -{ - if(results.second == 0) { return(0.); } - double sum{0.}; - for(unsigned index(0); index < results.first.size(); ++index) - { - sum += (double)results.second / results.first[index]; - } - return(results.first.size() / sum); -} -//------------------------------------------------------------------------------ -bool use_efficiency{false}; -double compute_score(const std::pair , unsigned>& results, std::vector& factors) -{ - double score{0.}; - if(use_efficiency) - { - score = compute_efficiency(results); - } - else - { - if(results.second == 0) { score = 0.; } - double sum{0.}; - for(unsigned index(0); index < results.first.size(); ++index) + else if(strcmp(argv[argIndex], "sim") == 0) { - sum += results.first[index] * factors[index]; + opt_todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex + 1]), 0u, simulate)); + if (std::get<0>(opt_todo.back()) < 10) { opt_num_threads = 1; } + argIndex += 1; } - score = sum / std::accumulate(factors.begin(), factors.end(), 0.) / (double)results.second; - } - return(score); -} -//------------------------------------------------------------------------------ -volatile unsigned thread_num_iterations{0}; // written by threads -std::vector thread_score; // written by threads -volatile unsigned thread_total{0}; // written by threads -volatile double thread_prev_score{0.0}; -volatile bool thread_compare{false}; -volatile bool thread_compare_stop{false}; // written by threads -volatile bool destroy_threads; -//------------------------------------------------------------------------------ -// Per thread data. -// seed should be unique for each thread. -// d1 and d2 are intended to point to read-only process-wide data. -struct SimulationData -{ - std::mt19937 re; - const Cards& cards; - const Decks& decks; - std::shared_ptr att_deck; - Hand att_hand; - std::vector > def_decks; - std::vector def_hands; - std::vector factors; - gamemode_t gamemode; - - SimulationData(unsigned seed, const Cards& cards_, const Decks& decks_, unsigned num_def_decks_, std::vector factors_, gamemode_t gamemode_) : - re(seed), - cards(cards_), - decks(decks_), - att_deck(), - att_hand(nullptr), - def_decks(num_def_decks_), - factors(factors_), - gamemode(gamemode_) - { - for(auto def_deck: def_decks) + else if(strcmp(argv[argIndex], "climbex") == 0) { - def_hands.emplace_back(new Hand(nullptr)); + opt_todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex + 1]), (unsigned)atoi(argv[argIndex + 2]), climb)); + if (std::get<1>(opt_todo.back()) < 10) { opt_num_threads = 1; } + opt_do_optimization = true; + argIndex += 2; } - } - - ~SimulationData() - { - for(auto hand: def_hands) { delete(hand); } - } - - void set_decks(const DeckIface* const att_deck_, std::vector const & def_decks_) - { - att_deck.reset(att_deck_->clone()); - att_hand.deck = att_deck.get(); - for(unsigned i(0); i < def_decks_.size(); ++i) + else if(strcmp(argv[argIndex], "climb") == 0) { - def_decks[i].reset(def_decks_[i]->clone()); - def_hands[i]->deck = def_decks[i].get(); + opt_todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex + 1]), (unsigned)atoi(argv[argIndex + 1]), climb)); + if (std::get<1>(opt_todo.back()) < 10) { opt_num_threads = 1; } + opt_do_optimization = true; + argIndex += 1; } - } - - inline std::vector evaluate() - { - std::vector res; - for(Hand* def_hand: def_hands) + else if(strcmp(argv[argIndex], "reorder") == 0) { - att_hand.reset(re); - def_hand->reset(re); - Field fd(re, cards, att_hand, *def_hand, gamemode); - unsigned result(play(&fd)); - res.emplace_back(result); + opt_todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex + 1]), (unsigned)atoi(argv[argIndex + 1]), reorder)); + if (std::get<1>(opt_todo.back()) < 10) { opt_num_threads = 1; } + argIndex += 1; } - return(res); - } -}; -//------------------------------------------------------------------------------ -class Process; -void thread_evaluate(boost::barrier& main_barrier, - boost::mutex& shared_mutex, - SimulationData& sim, - const Process& p); -//------------------------------------------------------------------------------ -class Process -{ -public: - unsigned num_threads; - std::vector threads; - std::vector threads_data; - boost::barrier main_barrier; - boost::mutex shared_mutex; - const Cards& cards; - const Decks& decks; - DeckIface* att_deck; - const std::vector def_decks; - std::vector factors; - gamemode_t gamemode; - - Process(unsigned _num_threads, const Cards& cards_, const Decks& decks_, DeckIface* att_deck_, std::vector _def_decks, std::vector _factors, gamemode_t _gamemode) : - num_threads(_num_threads), - cards(cards_), - decks(decks_), - att_deck(att_deck_), - def_decks(_def_decks), - factors(_factors), - main_barrier(num_threads+1), - gamemode(_gamemode) - { - destroy_threads = false; - unsigned seed(time(0)); - for(unsigned i(0); i < num_threads; ++i) + else if(strcmp(argv[argIndex], "debug") == 0) + { + opt_todo.push_back(std::make_tuple(0u, 0u, debug)); + opt_num_threads = 1; + } + else if(strcmp(argv[argIndex], "debuguntil") == 0) + { + // output the debug info for the first battle that min_score <= score <= max_score. + // E.g., 0 0: lose; 100 100: win (non-raid); 20 100: at least 20 damage (raid). + opt_todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex + 1]), (unsigned)atoi(argv[argIndex + 2]), debuguntil)); + opt_num_threads = 1; + argIndex += 2; + } + else { - threads_data.push_back(new SimulationData(seed + i, cards, decks, def_decks.size(), factors, gamemode)); - threads.push_back(new boost::thread(thread_evaluate, std::ref(main_barrier), std::ref(shared_mutex), std::ref(*threads_data.back()), std::ref(*this))); + std::cerr << "Error: Unknown option " << argv[argIndex] << std::endl; + return 0; } } - ~Process() + Cards all_cards; + Decks decks; + std::unordered_map bge_aliases; + load_skills_set_xml(all_cards, "data/skills_set.xml", true); + for (unsigned section = 0; section <= 10; ++ section) { - destroy_threads = true; - main_barrier.wait(); - for(auto thread: threads) { thread->join(); } - for(auto data: threads_data) { delete(data); } + load_cards_xml(all_cards, "data/cards_section_" + to_string(section) + ".xml", false); } - - std::pair , unsigned> evaluate(unsigned num_iterations) + all_cards.organize(); + for (const auto & suffix: fn_suffix_list) { - thread_num_iterations = num_iterations; - thread_score = std::vector(def_decks.size(), 0u); - thread_total = 0; - thread_compare = false; - // unlock all the threads - main_barrier.wait(); - // wait for the threads - main_barrier.wait(); - return(std::make_pair(thread_score, thread_total)); + load_decks_xml(decks, all_cards, "data/missions" + suffix + ".xml", "data/raids" + suffix + ".xml", suffix.empty()); + load_recipes_xml(all_cards, "data/fusion_recipes_cj2" + suffix + ".xml", suffix.empty()); + read_card_abbrs(all_cards, "data/cardabbrs" + suffix + ".txt"); + } + for (const auto & suffix: fn_suffix_list) + { + load_custom_decks(decks, all_cards, "data/customdecks" + suffix + ".txt"); } + read_bge_aliases(bge_aliases, "data/bges.txt"); + + fill_skill_table(); - std::pair , unsigned> compare(unsigned num_iterations, double prev_score) + if (opt_do_optimization and use_owned_cards) { - thread_num_iterations = num_iterations; - thread_score = std::vector(def_decks.size(), 0u); - thread_total = 0; - thread_prev_score = prev_score; - thread_compare = true; - thread_compare_stop = false; - // unlock all the threads - main_barrier.wait(); - // wait for the threads - main_barrier.wait(); - return(std::make_pair(thread_score, thread_total)); + if (opt_owned_cards_str_list.empty()) + { // load default files only if specify no -o= + for (const auto & suffix: fn_suffix_list) + { + std::string filename = "data/ownedcards" + suffix + ".txt"; + if (boost::filesystem::exists(filename)) + { + opt_owned_cards_str_list.push_back(filename); + } + } + } + for (const auto & oc_str: opt_owned_cards_str_list) + { + read_owned_cards(all_cards, owned_cards, oc_str); + } } -}; -//------------------------------------------------------------------------------ -void thread_evaluate(boost::barrier& main_barrier, - boost::mutex& shared_mutex, - SimulationData& sim, - const Process& p) -{ - while(true) + + for (int player = 0; player <= 2; ++ player) { - main_barrier.wait(); - sim.set_decks(p.att_deck, p.def_decks); - if(destroy_threads) { return; } - while(true) + for (auto && opt_effect: opt_effects[player]) { - shared_mutex.lock(); //<<<< - if(thread_num_iterations == 0 || (thread_compare && thread_compare_stop)) //! + if (opt_effect.empty()) { - shared_mutex.unlock(); //>>>> - main_barrier.wait(); - break; + continue; } - else + try { - --thread_num_iterations; //! - shared_mutex.unlock(); //>>>> - std::vector result{sim.evaluate()}; - shared_mutex.lock(); //<<<< - std::vector thread_score_local(thread_score.size(), 0); //! - for(unsigned index(0); index < result.size(); ++index) + std::vector tokens, skill_name_list; + const auto bge_itr = bge_aliases.find(simplify_name(opt_effect)); + boost::split(tokens, bge_itr == bge_aliases.end() ? opt_effect : bge_itr->second, boost::is_any_of(" -")); + boost::split(skill_name_list, tokens[0], boost::is_any_of("+")); + for (auto && skill_name: skill_name_list) { - thread_score[index] += result[index] == 0 ? 1 : 0; //! - thread_score_local[index] = thread_score[index]; // ! - } - ++thread_total; //! - unsigned thread_total_local{thread_total}; //! - shared_mutex.unlock(); //>>>> - if(thread_compare && thread_total_local >= 1 && thread_total_local % 100 == 0) - { - unsigned score_accum = 0; - // Multiple defense decks case: scaling by factors and approximation of a "discrete" number of events. - if(result.size() > 1) + Skill skill_id = skill_name_to_id(skill_name); + unsigned skill_index = 1; + if (BEGIN_BGE_SKILL < skill_id && skill_id < END_BGE_SKILL) { - double score_accum_d = 0.0; - for(unsigned i = 0; i < thread_score_local.size(); ++i) + // passive BGE (must be global) + if (player != 2) + { + throw std::runtime_error("must be global"); + } + if (skill_index < tokens.size()) + { + opt_bg_effects[skill_id] = boost::lexical_cast(tokens[skill_index]); + } + else + { + opt_bg_effects[skill_id] = 0; + } + } + else if (skill_table[skill_id] != nullptr) + { + // activation BG skill + SkillSpec bg_skill{skill_id, 0, allfactions, 0, 0, no_skill, no_skill, false}; + if (skill_index < tokens.size() && boost::to_lower_copy(tokens[skill_index]) == "all") + { + bg_skill.all = true; + skill_index += 1; + } + else if (skill_index + 1 < tokens.size() && isdigit(*tokens[skill_index].c_str())) + { + bg_skill.n = boost::lexical_cast(tokens[skill_index]); + skill_index += 1; + } + if (skill_index < tokens.size()) + { + bg_skill.s = skill_name_to_id(tokens[skill_index]); + if (bg_skill.s != no_skill) + { + skill_index += 1; + if (skill_index < tokens.size() && (boost::to_lower_copy(tokens[skill_index]) == "to" || boost::to_lower_copy(tokens[skill_index]) == "into")) + { + skill_index += 1; + } + if (skill_index < tokens.size()) + { + bg_skill.s2 = skill_name_to_id(tokens[skill_index]); + if (bg_skill.s2 != no_skill) + { + skill_index += 1; + } + } + } + } + if (skill_index < tokens.size()) + { + if (bg_skill.id == jam || bg_skill.id == overload) + { + bg_skill.n = boost::lexical_cast(tokens[skill_index]); + } + else + { + bg_skill.x = boost::lexical_cast(tokens[skill_index]); + } + } + if (player == 2) { - score_accum_d += thread_score_local[i] * sim.factors[i]; + opt_bg_skills[0].push_back(bg_skill); + opt_bg_skills[1].push_back(bg_skill); + } + else + { + opt_bg_skills[player].push_back(bg_skill); } - score_accum_d /= std::accumulate(sim.factors.begin(), sim.factors.end(), .0d); - score_accum = score_accum_d; } else { - score_accum = thread_score_local[0]; - } - if(boost::math::binomial_distribution<>::find_upper_bound_on_p(thread_total_local, score_accum, 0.01) < thread_prev_score) - { - shared_mutex.lock(); //<<<< - //std::cout << thread_total_local << "\n"; - thread_compare_stop = true; //! - shared_mutex.unlock(); //>>>> + std::cerr << "Error: unrecognized effect \"" << opt_effect << "\".\n"; + print_available_effects(); + return 0; } } } - } - } -} -//------------------------------------------------------------------------------ -void print_score_info(const std::pair , unsigned>& results, std::vector& factors) -{ - std::cout << "win%: " << compute_score(results, factors) * 100.0 << " ("; - for(auto val: results.first) - { - std::cout << val << " "; - } - std::cout << "out of " << results.second << ")\n" << std::flush; -} -//------------------------------------------------------------------------------ -void hill_climbing(unsigned num_iterations, DeckIface* d1, Process& proc) -{ - auto results = proc.evaluate(num_iterations); - print_score_info(results, proc.factors); - double current_score = compute_score(results, proc.factors); - double best_score = current_score; - // Non-commander cards - auto non_commander_cards = boost::join(boost::join(proc.cards.player_assaults, proc.cards.player_structures), proc.cards.player_actions); - const Card* best_commander = d1->commander; - std::vector best_cards = d1->cards; - bool deck_has_been_improved = true; - bool eval_commander = true; - while(deck_has_been_improved && best_score < 1.0) - { - deck_has_been_improved = false; - for(unsigned slot_i(0); slot_i < d1->cards.size(); ++slot_i) - { - if(eval_commander && !keep_commander) + catch (const boost::bad_lexical_cast & e) { - for(const Card* commander_candidate: proc.cards.player_commanders) - { - // Various checks to check if the card is accepted - assert(commander_candidate->m_type == CardType::commander); - if(commander_candidate == best_commander) { continue; } - if(!suitable_commander(commander_candidate)) { continue; } - // Place it in the deck - d1->commander = commander_candidate; - // Evaluate new deck - auto compare_results = proc.compare(num_iterations, best_score); - current_score = compute_score(compare_results, proc.factors); - // Is it better ? - if(current_score > best_score) - { - // Then update best score/commander, print stuff - best_score = current_score; - best_commander = commander_candidate; - deck_has_been_improved = true; - std::cout << "Deck improved: commander -> " << commander_candidate->m_name << ": "; - print_score_info(compare_results, proc.factors); - } - } - // Now that all commanders are evaluated, take the best one - d1->commander = best_commander; - eval_commander = false; + std::cerr << "Error: Expect a number in effect \"" << opt_effect << "\".\n"; + return 0; } - for(const Card* card_candidate: non_commander_cards) + catch (std::exception & e) { - // Various checks to check if the card is accepted - assert(card_candidate->m_type != CardType::commander); - if(card_candidate == best_cards[slot_i]) { continue; } - if(!suitable_non_commander(*d1, slot_i, card_candidate)) { continue; } - // Place it in the deck - d1->cards[slot_i] = card_candidate; - // Evaluate new deck - auto compare_results = proc.compare(num_iterations, best_score); - current_score = compute_score(compare_results, proc.factors); - // Is it better ? - if(current_score > best_score) - { - // Then update best score/slot, print stuff - best_score = current_score; - best_cards[slot_i] = card_candidate; - eval_commander = true; - deck_has_been_improved = true; - std::cout << "Deck improved: slot " << slot_i << " -> " << card_candidate->m_name << ": "; - print_score_info(compare_results, proc.factors); - } + std::cerr << "Error: effect \"" << opt_effect << "\": " << e.what() << ".\n"; + return 0; } - // Now that all cards are evaluated, take the best one - d1->cards[slot_i] = best_cards[slot_i]; } } - std::cout << "Best deck: " << best_score * 100.0 << "%\n"; - std::cout << best_commander->m_name; - for(const Card* card: best_cards) + + std::string your_deck_name{argv[1]}; + std::string enemy_deck_list{argv[2]}; + auto && deck_list_parsed = parse_deck_list(enemy_deck_list, decks); + + Deck* your_deck{nullptr}; + std::vector enemy_decks; + std::vector enemy_decks_factors; + + try { - std::cout << ", " << card->m_name; + your_deck = find_deck(decks, all_cards, your_deck_name)->clone(); } - std::cout << "\n"; -} -//------------------------------------------------------------------------------ -void hill_climbing_ordered(unsigned num_iterations, DeckOrdered* d1, Process& proc) -{ - auto results = proc.evaluate(num_iterations); - print_score_info(results, proc.factors); - double current_score = compute_score(results, proc.factors); - double best_score = current_score; - // Non-commander cards - auto non_commander_cards = boost::join(boost::join(proc.cards.player_assaults, proc.cards.player_structures), proc.cards.player_actions); - const Card* best_commander = d1->commander; - std::vector best_cards = d1->cards; - bool deck_has_been_improved = true; - bool eval_commander = true; - while(deck_has_been_improved && best_score < 1.0) + catch(const std::runtime_error& e) { - deck_has_been_improved = false; - std::set remaining_cards; - for(unsigned i = 0; i < best_cards.size(); ++i) - { - remaining_cards.insert(i); - } - while(!remaining_cards.empty()) - { - unsigned current_slot(*remaining_cards.begin()); - remaining_cards.erase(remaining_cards.begin()); - if(eval_commander && !keep_commander) - { - for(const Card* commander_candidate: proc.cards.player_commanders) - { - if(best_score == 1.0) { break; } - // Various checks to check if the card is accepted - assert(commander_candidate->m_type == CardType::commander); - if(commander_candidate == best_commander) { continue; } - if(!suitable_commander(commander_candidate)) { continue; } - // Place it in the deck - d1->commander = commander_candidate; - // Evaluate new deck - auto compare_results = proc.compare(num_iterations, best_score); - current_score = compute_score(compare_results, proc.factors); - // Is it better ? - if(current_score > best_score) - { - // Then update best score/commander, print stuff - best_score = current_score; - best_commander = commander_candidate; - deck_has_been_improved = true; - std::cout << "Deck improved: commander -> " << commander_candidate->m_name << ": "; - print_score_info(compare_results, proc.factors); - } - } - // Now that all commanders are evaluated, take the best one - d1->commander = best_commander; - eval_commander = false; - } - for(const Card* card_candidate: non_commander_cards) - { - if(best_score == 1.0) { break; } - // Various checks to check if the card is accepted - assert(card_candidate->m_type != CardType::commander); - for(unsigned slot_i(0); slot_i < d1->cards.size(); ++slot_i) - { - // Various checks to check if the card is accepted - if(card_candidate == best_cards[slot_i]) { continue; } - if(!suitable_non_commander(*d1, current_slot, card_candidate)) { continue; } - // Place it in the deck - d1->cards.erase(d1->cards.begin() + current_slot); - d1->cards.insert(d1->cards.begin() + slot_i, card_candidate); - // Evaluate new deck - auto compare_results = proc.compare(num_iterations, best_score); - current_score = compute_score(compare_results, proc.factors); - // Is it better ? - if(current_score > best_score) - { - // Then update best score/slot, print stuff - std::cout << "Deck improved: " << current_slot << " " << best_cards[current_slot]->m_name << " -> " << slot_i << " " << card_candidate->m_name << ": "; - best_score = current_score; - best_cards.erase(best_cards.begin() + current_slot); - best_cards.insert(best_cards.begin() + slot_i, card_candidate); - eval_commander = true; - deck_has_been_improved = true; - print_score_info(compare_results, proc.factors); - } - d1->cards = best_cards; - } - } - // Now that all cards are evaluated, take the best one - // d1->cards[slot_i] = best_cards[slot_i]; - } + std::cerr << "Error: Deck " << your_deck_name << ": " << e.what() << std::endl; + return 0; } - std::cout << "Best deck: " << best_score * 100.0 << "%\n"; - std::cout << best_commander->m_name; - for(const Card* card: best_cards) + if(your_deck == nullptr) { - std::cout << ", " << card->m_name; + std::cerr << "Error: Invalid attack deck name/hash " << your_deck_name << ".\n"; } - std::cout << "\n"; -} -//------------------------------------------------------------------------------ -// Implements iteration over all combination of k elements from n elements. -// parameter firstIndexLimit: this is a ugly hack used to implement the special condition that -// a deck could be expected to contain at least 1 assault card. Thus the first element -// will be chosen among the assault cards only, instead of all cards. -// It works on the condition that the assault cards are sorted first in the list of cards, -// thus have indices 0..firstIndexLimit-1. -class Combination -{ -public: - Combination(unsigned all_, unsigned choose_, unsigned firstIndexLimit_ = 0) : - all(all_), - choose(choose_), - firstIndexLimit(firstIndexLimit_ == 0 ? all_ - choose_ : firstIndexLimit_), - indices(choose_, 0), - indicesLimits(choose_, 0) + else if(!your_deck->variable_cards.empty()) + { + std::cerr << "Error: Invalid attack deck " << your_deck_name << ": has optional cards.\n"; + your_deck = nullptr; + } + if(your_deck == nullptr) + { + usage(argc, argv); + return 0; + } + + your_deck->strategy = opt_your_strategy; + if (!opt_forts.empty()) { - assert(choose > 0); - assert(choose <= all); - assert(firstIndexLimit <= all); - indicesLimits[0] = firstIndexLimit; - for(unsigned i(1); i < choose; ++i) + try + { + your_deck->add_forts(opt_forts + ","); + } + catch(const std::runtime_error& e) { - indices[i] = i; - indicesLimits[i] = all - choose + i; + std::cerr << "Error: yf " << opt_forts << ": " << e.what() << std::endl; + return 0; } } - const std::vector& getIndices() + try + { + your_deck->set_vip_cards(opt_vip); + } + catch(const std::runtime_error& e) { - return(indices); + std::cerr << "Error: vip " << opt_vip << ": " << e.what() << std::endl; + return 0; } - bool next() + if (!opt_quest.empty()) { - for(index = choose - 1; index >= 0; --index) + try { - if(indices[index] < indicesLimits[index]) + optimization_mode = OptimizationMode::quest; + std::vector tokens; + boost::split(tokens, opt_quest, boost::is_any_of(" -")); + if (tokens.size() < 3) + { + throw std::runtime_error("Expect one of: su n skill; sd n skill; cu n faction/strcture; ck n structure"); + } + auto type_str = boost::to_lower_copy(tokens[0]); + quest.quest_value = boost::lexical_cast(tokens[1]); + auto key_str = boost::to_lower_copy(tokens[2]); + unsigned quest_index = 3; + if (type_str == "su" || type_str == "sd") { - ++indices[index]; - for(nextIndex = index + 1; nextIndex < choose; nextIndex++) + Skill skill_id = skill_name_to_id(key_str); + if (skill_id == no_skill) { - indices[nextIndex] = indices[index] - index + nextIndex; + std::cerr << "Error: Expect skill in quest \"" << opt_quest << "\".\n"; + return 0; } - return(false); + quest.quest_type = type_str == "su" ? QuestType::skill_use : QuestType::skill_damage; + quest.quest_key = skill_id; } - } - return(true); - } - -private: - unsigned all; - unsigned choose; - unsigned firstIndexLimit; - std::vector indices; - std::vector indicesLimits; - int index; - int nextIndex; -}; -//------------------------------------------------------------------------------ -static unsigned total_num_combinations_test(0); -inline void try_all_ratio_combinations(unsigned deck_size, unsigned var_k, unsigned num_iterations, const std::vector& card_indices, std::vector& cards, const Card* commander, Process& proc, double& best_score, boost::optional& best_deck) -{ - assert(card_indices.size() > 0); - assert(card_indices.size() <= deck_size); - unsigned num_cards_to_combine(deck_size); - std::vector unique_cards; - std::vector cards_to_combine; - bool legendary_found(false); - for(unsigned card_index: card_indices) - { - const Card* card(cards[card_index]); - if(card->m_unique || card->m_rarity == 4) - { - if(card->m_rarity == 4) + else if (type_str == "cu" || type_str == "ck") { - if(legendary_found) { return; } - legendary_found = true; + if (key_str == "assault") + { + quest.quest_type = type_str == "cu" ? QuestType::type_card_use : QuestType::type_card_kill; + quest.quest_key = CardType::assault; + } + else if (key_str == "structure") + { + quest.quest_type = type_str == "cu" ? QuestType::type_card_use : QuestType::type_card_kill; + quest.quest_key = CardType::structure; + } + else + { + for (unsigned i = 1; i < Faction::num_factions; ++ i) + { + if (key_str == boost::to_lower_copy(faction_names[i])) + { + quest.quest_type = type_str == "cu" ? QuestType::faction_assault_card_use : QuestType::faction_assault_card_kill; + quest.quest_key = i; + break; + } + } + if (quest.quest_key == 0) + { + std::cerr << "Error: Expect assault, structure or faction in quest \"" << opt_quest << "\".\n"; + return 0; + } + } } - --num_cards_to_combine; - unique_cards.push_back(card); - } - else - { - cards_to_combine.push_back(card); - } - } - // all unique or legendaries, quit - if(cards_to_combine.size() == 0) { return; } - if(cards_to_combine.size() == 1) - { - std::vector deck_cards = unique_cards; - std::vector combined_cards(num_cards_to_combine, cards_to_combine[0]); - deck_cards.insert(deck_cards.end(), combined_cards.begin(), combined_cards.end()); - DeckRandom deck(commander, deck_cards); - (*dynamic_cast(proc.att_deck)) = deck; - auto new_results = proc.compare(num_iterations, best_score); - double new_score = compute_score(new_results, proc.factors); - if(new_score > best_score) - { - best_score = new_score; - best_deck = deck; - print_score_info(new_results, proc.factors); - print_deck(deck); - std::cout << std::flush; - } - //++num; - // num_cards = num_cards_to_combine ... - } - else - { - var_k = cards_to_combine.size() - 1; - Combination cardAmounts(num_cards_to_combine-1, var_k); - bool finished(false); - while(!finished) - { - const std::vector& indices = cardAmounts.getIndices(); - std::vector num_cards(var_k+1, 0); - num_cards[0] = indices[0] + 1; - for(unsigned i(1); i < var_k; ++i) + else if (type_str == "cs") { - num_cards[i] = indices[i] - indices[i-1]; + unsigned card_id; + unsigned card_num; + char num_sign; + char mark; + try + { + parse_card_spec(all_cards, key_str, card_id, card_num, num_sign, mark); + quest.quest_type = QuestType::card_survival; + quest.quest_key = card_id; + } + catch (const std::runtime_error& e) + { + std::cerr << "Error: Expect a card in quest \"" << opt_quest << "\".\n"; + return 0; + } + } + else if (type_str == "suoc" && tokens.size() >= 4) + { + Skill skill_id = skill_name_to_id(key_str); + if (skill_id == no_skill) + { + std::cerr << "Error: Expect skill in quest \"" << opt_quest << "\".\n"; + return 0; + } + unsigned card_id; + unsigned card_num; + char num_sign; + char mark; + try + { + parse_card_spec(all_cards, boost::to_lower_copy(tokens[3]), card_id, card_num, num_sign, mark); + quest_index += 1; + quest.quest_type = QuestType::skill_use; + quest.quest_key = skill_id; + quest.quest_2nd_key = card_id; + } + catch (const std::runtime_error& e) + { + std::cerr << "Error: Expect a card in quest \"" << opt_quest << "\".\n"; + return 0; + } } - num_cards[var_k] = num_cards_to_combine - (indices[var_k-1] + 1); - std::vector deck_cards = unique_cards; - //std::cout << "num cards: "; - for(unsigned num_index(0); num_index < num_cards.size(); ++num_index) + else { - //std::cout << num_cards[num_index] << " "; - for(unsigned i(0); i < num_cards[num_index]; ++i) { deck_cards.push_back(cards[card_indices[num_index]]); } + throw std::runtime_error("Expect one of: su n skill; sd n skill; cu n faction/strcture; ck n structure"); } - //std::cout << "\n" << std::flush; - //std::cout << std::flush; - assert(deck_cards.size() == deck_size); - DeckRandom deck(commander, deck_cards); - *proc.att_deck = deck; - auto new_results = proc.compare(num_iterations, best_score); - double new_score = compute_score(new_results, proc.factors); - if(new_score > best_score) + quest.quest_score = quest.quest_value; + for (unsigned i = quest_index; i < tokens.size(); ++ i) { - best_score = new_score; - best_deck = deck; - print_score_info(new_results, proc.factors); - print_deck(deck); - std::cout << std::flush; + const auto & token = tokens[i]; + if (token == "each") + { + quest.must_fulfill = true; + quest.quest_score = 100; + } + else if (token == "win") + { quest.must_win = true; } + else if (token.substr(0, 2) == "q=") + { quest.quest_score = boost::lexical_cast(token.substr(2)); } + else if (token.substr(0, 2) == "w=") + { quest.win_score = boost::lexical_cast(token.substr(2)); } + else + { throw std::runtime_error("Cannot recognize " + token); } } - ++total_num_combinations_test; - finished = cardAmounts.next(); + max_possible_score[(size_t)optimization_mode] = quest.quest_score + quest.win_score; } - } -} -//------------------------------------------------------------------------------ -void exhaustive_k(unsigned num_iterations, unsigned var_k, Process& proc) -{ - std::vector ass_structs; - for(const Card* card: proc.cards.player_assaults) - { - if(card->m_rarity >= 3) { ass_structs.push_back(card); } - } - for(const Card* card: proc.cards.player_structures) - { - if(card->m_rarity >= 3) { ass_structs.push_back(card); } - } - //std::vector ass_structs; = cards.player_assaults; - //ass_structs.insert(ass_structs.end(), cards.player_structures.begin(), cards.player_structures.end()); - unsigned var_n = ass_structs.size(); - assert(var_k <= var_n); - unsigned num(0); - Combination cardIndices(var_n, var_k); - const std::vector& indices = cardIndices.getIndices(); - bool finished(false); - double best_score{0}; - boost::optional best_deck; - unsigned num_cards = ((DeckRandom*)proc.att_deck)->cards.size(); - while(!finished) - { - if(keep_commander) + catch (const boost::bad_lexical_cast & e) { - try_all_ratio_combinations(num_cards, var_k, num_iterations, indices, ass_structs, ((DeckRandom*)proc.att_deck)->commander, proc, best_score, best_deck); + std::cerr << "Error: Expect a number in quest \"" << opt_quest << "\".\n"; + return 0; } - else + catch (const std::runtime_error& e) { - // Iterate over all commanders - for(unsigned commanderIndex(0); commanderIndex < proc.cards.player_commanders.size() && !finished; ++commanderIndex) - { - const Card* commander(proc.cards.player_commanders[commanderIndex]); - if(!suitable_commander(commander)) { continue; } - try_all_ratio_combinations(num_cards, var_k, num_iterations, indices, ass_structs, commander, proc, best_score, best_deck); - } + std::cerr << "Error: quest " << opt_quest << ": " << e.what() << std::endl; + return 0; } - finished = cardIndices.next(); } - std::cout << "done " << num << "\n"; -} -//------------------------------------------------------------------------------ -enum Operation { - bruteforce, - climb, - fightonce -}; -//------------------------------------------------------------------------------ -// void print_raid_deck(DeckRandom& deck) -// { -// std::cout << "--------------- Raid ---------------\n"; -// std::cout << "Commander:\n"; -// std::cout << " " << deck.m_commander->m_name << "\n"; -// std::cout << "Always include:\n"; -// for(auto& card: deck.m_cards) -// { -// std::cout << " " << card->m_name << "\n"; -// } -// for(auto& pool: deck.m_raid_cards) -// { -// std::cout << pool.first << " from:\n"; -// for(auto& card: pool.second) -// { -// std::cout << " " << card->m_name << "\n"; -// } -// } -// } -//------------------------------------------------------------------------------ -void print_available_decks(const Decks& decks) -{ - std::cout << "Mission decks:\n"; - for(auto it: decks.mission_decks_by_name) + + try { - std::cout << " " << it.first << "\n"; + your_deck->set_given_hand(opt_hand); } - std::cout << "Raid decks:\n"; - for(auto it: decks.raid_decks_by_name) + catch(const std::runtime_error& e) { - std::cout << " " << it.first << "\n"; + std::cerr << "Error: hand " << opt_hand << ": " << e.what() << std::endl; + return 0; } - std::cout << "Custom decks:\n"; - for(auto it: decks.custom_decks) + + if (opt_keep_commander) { - std::cout << " " << it.first << "\n"; + requirement.num_cards[your_deck->commander] = 1; } -} - -std::vector > parse_deck_list(std::string list_string) -{ - std::vector > res; - boost::tokenizer > list_tokens{list_string, boost::char_delimiters_separator{false, ";", ""}}; - for(auto list_token = list_tokens.begin(); list_token != list_tokens.end(); ++list_token) + for (auto && card_mark: your_deck->card_marks) { - boost::tokenizer > deck_tokens{*list_token, boost::char_delimiters_separator{false, ":", ""}}; - auto deck_token = deck_tokens.begin(); - res.push_back(std::make_pair(*deck_token, 1.0d)); - ++deck_token; - if(deck_token != deck_tokens.end()) + auto && card = card_mark.first < 0 ? your_deck->commander : your_deck->cards[card_mark.first]; + auto mark = card_mark.second; + if (mark == '!') { - res.back().second = boost::lexical_cast(*deck_token); + requirement.num_cards[card] += 1; } } - return(res); -} - -void usage(int argc, char** argv) -{ - std::cout << "usage: " << argv[0] << " [optional flags] [brute ] [climb ]\n"; - std::cout << "\n"; - std::cout << ": the deck name of a custom deck.\n"; - std::cout << ": semicolon separated list of defense decks, syntax:\n"; - std::cout << " deckname1[:factor1];deckname2[:factor2];...\n"; - std::cout << " where deckname is the name of a mission, raid, or custom deck, and factor is optional. The default factor is 1.\n"; - std::cout << " example: \'fear:0.2;slowroll:0.8\' means fear is the defense deck 20% of the time, while slowroll is the defense deck 80% of the time.\n"; - std::cout << "\n"; - std::cout << "Flags:\n"; - std::cout << " -c: don't try to optimize the commander.\n"; - std::cout << " -o: restrict hill climbing to the owned cards listed in \"ownedcards.txt\".\n"; - std::cout << " -r: the attack deck is played in order instead of randomly (respects the 3 cards drawn limit).\n"; - std::cout << " -s: use surge (default is fight).\n"; - std::cout << " -t : set the number of threads, default is 4.\n"; - std::cout << " -turnlimit : set the number of turns in a battle, default is 50 (can be used for speedy achievements).\n"; - std::cout << "Operations:\n"; - std::cout << "brute : find the best combination of different cards, using up to battles to evaluate a deck.\n"; - std::cout << "climb : perform hill-climbing starting from the given attack deck, using up to battles to evaluate a deck.\n"; -} - -int main(int argc, char** argv) -{ - if(argc == 1) { usage(argc, argv); return(0); } - debug_print = getenv("DEBUG_PRINT"); - unsigned num_threads = (debug_print || getenv("DEBUG")) ? 1 : 4; - gamemode_t gamemode = fight; - bool ordered = false; - Cards cards; - read_cards(cards); - read_owned_cards(cards); - Decks decks; - load_decks(decks, cards); - skill_table[augment] = perform_targetted_allied_fast; - skill_table[augment_all] = perform_global_allied_fast; - skill_table[chaos] = perform_targetted_hostile_fast; - skill_table[chaos_all] = perform_global_hostile_fast; - skill_table[cleanse] = perform_targetted_allied_fast; - skill_table[cleanse_all] = perform_global_allied_fast; - skill_table[enfeeble] = perform_targetted_hostile_fast; - skill_table[enfeeble_all] = perform_global_hostile_fast; - skill_table[freeze] = perform_targetted_hostile_fast; - skill_table[freeze_all] = perform_global_hostile_fast; - skill_table[heal] = perform_targetted_allied_fast; - skill_table[heal_all] = perform_global_allied_fast; - skill_table[infuse] = perform_infuse; - skill_table[jam] = perform_targetted_hostile_fast; - skill_table[jam_all] = perform_global_hostile_fast; - skill_table[mimic] = perform_mimic; - skill_table[protect] = perform_targetted_allied_fast; - skill_table[protect_all] = perform_global_allied_fast; - skill_table[rally] = perform_targetted_allied_fast; - skill_table[rally_all] = perform_global_allied_fast; - skill_table[rush] = perform_targetted_allied_fast; - skill_table[shock] = perform_shock; - skill_table[siege] = perform_targetted_hostile_fast; - skill_table[siege_all] = perform_global_hostile_fast; - skill_table[supply] = perform_supply; - skill_table[strike] = perform_targetted_hostile_fast; - skill_table[strike_all] = perform_global_hostile_fast; - skill_table[summon] = perform_summon; - skill_table[trigger_regen] = perform_trigger_regen; - skill_table[weaken] = perform_targetted_hostile_fast; - skill_table[weaken_all] = perform_global_hostile_fast; + target_score = opt_target_score.empty() ? max_possible_score[(size_t)optimization_mode] : boost::lexical_cast(opt_target_score); - if(argc <= 2) - { - print_available_decks(decks); - return(4); - } - std::string att_deck_name{argv[1]}; - std::vector def_decks; - std::vector def_decks_factors; - auto deck_list_parsed = parse_deck_list(argv[2]); for(auto deck_parsed: deck_list_parsed) { - DeckIface* def_deck = find_deck(decks, deck_parsed.first); - if(def_deck != nullptr) + Deck* enemy_deck{nullptr}; + try { - def_decks.push_back(def_deck); - def_decks_factors.push_back(deck_parsed.second); + enemy_deck = find_deck(decks, all_cards, deck_parsed.first); } - else + catch(const std::runtime_error& e) { - std::cout << "The deck " << deck_parsed.first << " was not found. Available decks:\n"; - print_available_decks(decks); - return(5); + std::cerr << "Error: Deck " << deck_parsed.first << ": " << e.what() << std::endl; + return 0; } - } - std::vector > todo; - for(unsigned argIndex(3); argIndex < argc; ++argIndex) - { - if(strcmp(argv[argIndex], "-c") == 0) + if(enemy_deck == nullptr) { - keep_commander = true; + std::cerr << "Error: Invalid defense deck name/hash " << deck_parsed.first << ".\n"; + usage(argc, argv); + return 0; } - else if(strcmp(argv[argIndex], "-o") == 0) + if (optimization_mode == OptimizationMode::notset) { - use_owned_cards = true; + if (enemy_deck->decktype == DeckType::raid) + { + optimization_mode = OptimizationMode::raid; + } + else if (enemy_deck->decktype == DeckType::campaign) + { + gamemode = surge; + optimization_mode = OptimizationMode::campaign; + } + else + { + optimization_mode = OptimizationMode::winrate; + } } - else if(strcmp(argv[argIndex], "-r") == 0) + enemy_deck->strategy = opt_enemy_strategy; + if (!opt_enemy_forts.empty()) { - ordered = true; + try + { + enemy_deck->add_forts(opt_enemy_forts + ","); + } + catch(const std::runtime_error& e) + { + std::cerr << "Error: ef " << opt_enemy_forts << ": " << e.what() << std::endl; + return 0; + } } - else if(strcmp(argv[argIndex], "-s") == 0) + try { - gamemode = surge; + enemy_deck->set_given_hand(opt_enemy_hand); } - else if(strcmp(argv[argIndex], "-t") == 0) + catch(const std::runtime_error& e) { - num_threads = atoi(argv[argIndex+1]); - argIndex += 1; + std::cerr << "Error: enemy:hand " << opt_enemy_hand << ": " << e.what() << std::endl; + return 0; } - else if(strcmp(argv[argIndex], "-turnlimit") == 0) + enemy_decks.push_back(enemy_deck); + enemy_decks_factors.push_back(deck_parsed.second); + } + + // Force to claim cards in your initial deck. + if (opt_do_optimization and use_owned_cards) + { + claim_cards({your_deck->commander}); + claim_cards(your_deck->cards); + } + + // shrink any oversized deck to maximum of 10 cards + commander + // NOTE: do this AFTER the call to claim_cards so that passing an initial deck of >10 cards + // can be used as a "shortcut" for adding them to owned cards. Also this allows climb + // to figure out which are the best 10, rather than restricting climb to the first 10. + if (your_deck->cards.size() > max_deck_len) + { + your_deck->shrink(max_deck_len); + if (debug_print >= 0) { - turn_limit = atoi(argv[argIndex+1]); - argIndex += 1; + std::cerr << "WARNING: Too many cards in your deck. Trimmed.\n"; } - else if(strcmp(argv[argIndex], "brute") == 0) + } + freezed_cards = std::min(freezed_cards, your_deck->cards.size()); + + if (debug_print >= 0) + { + std::cout << "Your Deck: " << (debug_print > 0 ? your_deck->long_description() : your_deck->medium_description()) << std::endl; + for (const auto & bg_skill: opt_bg_skills[0]) { - todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex+1]), (unsigned)atoi(argv[argIndex+2]), bruteforce)); - argIndex += 2; + std::cout << "Your BG Skill: " << skill_description(all_cards, bg_skill) << std::endl; } - else if(strcmp(argv[argIndex], "climb") == 0) + + for (unsigned i(0); i < enemy_decks.size(); ++i) { - todo.push_back(std::make_tuple((unsigned)atoi(argv[argIndex+1]), 0u, climb)); - argIndex += 1; + std::cout << "Enemy's Deck:" << enemy_decks_factors[i] << ": " << (debug_print > 0 ? enemy_decks[i]->long_description() : enemy_decks[i]->medium_description()) << std::endl; } - else if(strcmp(argv[argIndex], "debug") == 0) + for (const auto & bg_skill: opt_bg_skills[1]) { - debug_print = true; - num_threads = 1; - todo.push_back(std::make_tuple(0u, 0u, fightonce)); + std::cout << "Enemy's BG Skill: " << skill_description(all_cards, bg_skill) << std::endl; } - } - - unsigned attacker_wins(0); - unsigned defender_wins(0); - - DeckIface* att_deck{nullptr}; - auto custom_deck_it = decks.custom_decks.find(att_deck_name); - if(custom_deck_it != decks.custom_decks.end()) - { - att_deck = custom_deck_it->second; - } - else - { - std::cout << "The deck " << att_deck_name << " was not found. Available decks:\n"; - std::cout << "Custom decks:\n"; - for(auto it: decks.custom_decks) + for (const auto & bg_effect: opt_bg_effects) { - std::cout << " " << it.first << "\n"; + if (bg_effect.second == 0) + { + std::cout << "BG Effect: " << skill_names[bg_effect.first] << std::endl; + } + else + { + std::cout << "BG Effect: " << skill_names[bg_effect.first] << " " << bg_effect.second << std::endl; + } } - return(5); } - print_deck(*att_deck); - std::shared_ptr att_deck_ordered; - if(ordered) - { - att_deck_ordered = std::make_shared(*att_deck); - } + Process p(opt_num_threads, all_cards, decks, your_deck, enemy_decks, enemy_decks_factors, gamemode, quest, opt_bg_effects, opt_bg_skills[0], opt_bg_skills[1]); - Process p(num_threads, cards, decks, ordered ? att_deck_ordered.get() : att_deck, def_decks, def_decks_factors, gamemode); + for(auto op: opt_todo) { - //ScopeClock timer; - for(auto op: todo) + switch(std::get<2>(op)) { - switch(std::get<2>(op)) + case noop: + break; + case simulate: { + EvaluatedResults results = { EvaluatedResults::first_type(enemy_decks.size()), 0 }; + results = p.evaluate(std::get<0>(op), results); + print_results(results, p.factors); + break; + } + case climb: { + switch (opt_your_strategy) { - case bruteforce: { - exhaustive_k(std::get<1>(op), std::get<0>(op), p); + case DeckStrategy::random: + hill_climbing(std::get<0>(op), std::get<1>(op), your_deck, p, requirement, quest); break; - } - case climb: { - if(!ordered) - { - hill_climbing(std::get<0>(op), att_deck, p); - } - else - { - hill_climbing_ordered(std::get<0>(op), att_deck_ordered.get(), p); - } +// case DeckStrategy::ordered: +// case DeckStrategy::exact_ordered: + default: + hill_climbing_ordered(std::get<0>(op), std::get<1>(op), your_deck, p, requirement, quest); break; } - case fightonce: { - p.evaluate(1); - break; + break; + } + case reorder: { + your_deck->strategy = DeckStrategy::ordered; + use_owned_cards = true; + if (min_deck_len == 1 && max_deck_len == 10) + { + min_deck_len = max_deck_len = your_deck->cards.size(); } + fund = 0; + debug_print = -1; + owned_cards.clear(); + claim_cards({your_deck->commander}); + claim_cards(your_deck->cards); + hill_climbing_ordered(std::get<0>(op), std::get<1>(op), your_deck, p, requirement, quest); + break; + } + case debug: { + ++ debug_print; + debug_str.clear(); + EvaluatedResults results{EvaluatedResults::first_type(enemy_decks.size()), 0}; + results = p.evaluate(1, results); + print_results(results, p.factors); + -- debug_print; + break; + } + case debuguntil: { + ++ debug_print; + ++ debug_cached; + while(1) + { + debug_str.clear(); + EvaluatedResults results{EvaluatedResults::first_type(enemy_decks.size()), 0}; + results = p.evaluate(1, results); + auto score = compute_score(results, p.factors); + if(score.points >= std::get<0>(op) && score.points <= std::get<1>(op)) + { + std::cout << debug_str << std::flush; + print_results(results, p.factors); + break; + } } + -- debug_cached; + -- debug_print; + break; + } } } - return(0); + return 0; } diff --git a/update_xml.sh b/update_xml.sh new file mode 100755 index 00000000..e5ecc778 --- /dev/null +++ b/update_xml.sh @@ -0,0 +1,4 @@ +#!/bin/bash +for fn in fusion_recipes_cj2 missions skills_set `seq -f cards_section_%g 1 9` ; do + curl http://mobile$1.tyrantonline.com/assets/${fn}.xml -R -z data/${fn}.xml -o data/${fn}.xml +done diff --git a/xml.cpp b/xml.cpp new file mode 100644 index 00000000..f61876ac --- /dev/null +++ b/xml.cpp @@ -0,0 +1,489 @@ +#include "xml.h" + +#include +#include +#include +#include +#include +#include +#include +#include "rapidxml.hpp" +#include "card.h" +#include "cards.h" +#include "deck.h" +#include "tyrant.h" +//---------------------- $20 cards.xml parsing --------------------------------- +// Sets: 1 enclave; 2 nexus; 3 blight; 4 purity; 5 homeworld; +// 6 phobos; 7 phobos aftermath; 8 awakening +// 1000 standard; 5000 rewards; 5001 promotional; 9000 exclusive +// mission only and test cards have no set +using namespace rapidxml; + +Skill skill_name_to_id(const std::string & name) +{ + static std::map skill_map; + if(skill_map.empty()) + { + for(unsigned i(0); i < Skill::num_skills; ++i) + { + std::string skill_id = boost::to_lower_copy(skill_names[i]); + skill_map[skill_id] = i; + } + skill_map["armored"] = skill_map["armor"]; // Special case for Armor: id and name differ + skill_map["besiege"] = skill_map["mortar"]; // Special case for Mortar: id and name differ + } + auto x = skill_map.find(boost::to_lower_copy(name)); + if (x == skill_map.end()) + { + return no_skill; + } + else + { + return (Skill)x->second; + } +} + +Faction skill_faction(xml_node<>* skill) +{ + xml_attribute<>* y(skill->first_attribute("y")); + if (y) + { + return static_cast(atoi(y->value())); + } + return allfactions; +} + +unsigned node_value(xml_node<>* skill, const char* attribute, unsigned default_value = 0) +{ + xml_attribute<>* value_node(skill->first_attribute(attribute)); + return value_node ? atoi(value_node->value()) : default_value; +} + +Skill skill_target_skill(xml_node<>* skill, const char* attribute) +{ + Skill s(no_skill); + xml_attribute<>* x(skill->first_attribute(attribute)); + if(x) + { + s = skill_name_to_id(x->value()); + } + return(s); +} + +//------------------------------------------------------------------------------ +void load_decks_xml(Decks& decks, const Cards& all_cards, const std::string & mission_filename, const std::string & raid_filename, bool do_warn_on_missing=true) +{ + try + { + read_missions(decks, all_cards, mission_filename, do_warn_on_missing); + } + catch (const rapidxml::parse_error& e) + { + std::cerr << "\nFailed to parse file [" << mission_filename << "]. Skip it.\n"; + } + try + { + read_raids(decks, all_cards, raid_filename, do_warn_on_missing); + } + catch(const rapidxml::parse_error& e) + { + std::cerr << "\nFailed to parse file [" << raid_filename << "]. Skip it.\n"; + } +} + +//------------------------------------------------------------------------------ +void parse_file(const std::string & filename, std::vector& buffer, xml_document<>& doc, bool do_warn_on_missing=true) +{ + std::ifstream cards_stream(filename, std::ios::binary); + if (!cards_stream.good()) + { + if (do_warn_on_missing) + { + std::cerr << "Warning: The file '" << filename << "' does not exist. Proceeding without reading from this file.\n"; + } + buffer.resize(1); + buffer[0] = 0; + doc.parse<0>(&buffer[0]); + return; + } + // Get the size of the file + cards_stream.seekg(0,std::ios::end); + std::streampos length = cards_stream.tellg(); + cards_stream.seekg(0,std::ios::beg); + buffer.resize(length + std::streampos(1)); + cards_stream.read(&buffer[0],length); + // zero-terminate + buffer[length] = '\0'; + try + { + doc.parse<0>(&buffer[0]); + } + catch(rapidxml::parse_error& e) + { + std::cerr << "Parse error exception.\n"; + std::cout << e.what(); + throw(e); + } +} +//------------------------------------------------------------------------------ +void parse_card_node(Cards& all_cards, Card* card, xml_node<>* card_node) +{ + xml_node<>* id_node(card_node->first_node("id")); + xml_node<>* card_id_node = card_node->first_node("card_id"); + assert(id_node || card_id_node); + xml_node<>* name_node(card_node->first_node("name")); + xml_node<>* attack_node(card_node->first_node("attack")); + xml_node<>* health_node(card_node->first_node("health")); + xml_node<>* cost_node(card_node->first_node("cost")); + xml_node<>* rarity_node(card_node->first_node("rarity")); + xml_node<>* type_node(card_node->first_node("type")); + xml_node<>* set_node(card_node->first_node("set")); + int set(set_node ? atoi(set_node->value()) : card->m_set); + xml_node<>* level_node(card_node->first_node("level")); + xml_node<>* fusion_level_node(card_node->first_node("fusion_level")); + if (id_node) { card->m_base_id = card->m_id = atoi(id_node->value()); } + else if (card_id_node) { card->m_id = atoi(card_id_node->value()); } + if (name_node) { card->m_name = name_node->value(); } + if (level_node) { card->m_level = atoi(level_node->value()); } + if (fusion_level_node) { card->m_fusion_level = atoi(fusion_level_node->value()); } + if (attack_node) { card->m_attack = atoi(attack_node->value()); } + if (health_node) { card->m_health = atoi(health_node->value()); } + if (cost_node) { card->m_delay = atoi(cost_node->value()); } + if (id_node) + { + if (card->m_id < 1000) + { card->m_type = CardType::assault; } + else if (card->m_id < 2000) + { card->m_type = CardType::commander; } + else if (card->m_id < 3000) + { card->m_type = CardType::structure; } + else if (card->m_id < 8000) + { card->m_type = CardType::assault; } + else if (card->m_id < 10000) + { card->m_type = CardType::structure; } + else if (card->m_id < 17000) + { card->m_type = CardType::assault; } + else if (card->m_id < 25000) + { card->m_type = CardType::structure; } + else if (card->m_id < 30000) + { card->m_type = CardType::commander; } + else + { card->m_type = CardType::assault; } + } + if(rarity_node) { card->m_rarity = atoi(rarity_node->value()); } + if(type_node) { card->m_faction = static_cast(atoi(type_node->value())); } + card->m_set = set; + + if (card_node->first_node("skill")) + { // inherit no skill if there is skill node + card->m_skills.clear(); + memset(card->m_skill_value, 0, sizeof card->m_skill_value); + } + for(xml_node<>* skill_node = card_node->first_node("skill"); + skill_node; + skill_node = skill_node->next_sibling("skill")) + { + Skill skill_id = skill_name_to_id(skill_node->first_attribute("id")->value()); + if(skill_id == no_skill) { continue; } + auto x = node_value(skill_node, "x", 0); + auto y = skill_faction(skill_node); + auto n = node_value(skill_node, "n", 0); + auto c = node_value(skill_node, "c", 0); + auto s = skill_target_skill(skill_node, "s"); + auto s2 = skill_target_skill(skill_node, "s2"); + bool all(skill_node->first_attribute("all")); + card->add_skill(skill_id, x, y, n, c, s, s2, all); + } + all_cards.all_cards.push_back(card); + Card * top_card = card; + for(xml_node<>* upgrade_node = card_node->first_node("upgrade"); + upgrade_node; + upgrade_node = upgrade_node->next_sibling("upgrade")) + { + Card * pre_upgraded_card = top_card; + top_card = new Card(*top_card); + parse_card_node(all_cards, top_card, upgrade_node); + if (top_card->m_type == CardType::commander) + { + // Commanders cost twice and cannot be salvaged. + top_card->m_recipe_cost = 2 * upgrade_cost[pre_upgraded_card->m_level]; + } + else + { + // Salvaging income counts? + top_card->m_recipe_cost = upgrade_cost[pre_upgraded_card->m_level]; // + salvaging_income[top_card->m_rarity][pre_upgraded_card->m_level] - salvaging_income[top_card->m_rarity][top_card->m_level]; + } + top_card->m_recipe_cards.clear(); + top_card->m_recipe_cards[pre_upgraded_card] = 1; + pre_upgraded_card->m_used_for_cards[top_card] = 1; + } + card->m_top_level_card = top_card; +} + +void load_cards_xml(Cards & all_cards, const std::string & filename, bool do_warn_on_missing) +{ + std::vector buffer; + xml_document<> doc; + parse_file(filename, buffer, doc, do_warn_on_missing); + xml_node<>* root = doc.first_node(); + + if(!root) + { + return; + } + for (xml_node<>* card_node = root->first_node("unit"); + card_node; + card_node = card_node->next_sibling("unit")) + { + auto card = new Card(); + parse_card_node(all_cards, card, card_node); + } +} + +void load_skills_set_xml(Cards & all_cards, const std::string & filename, bool do_warn_on_missing) +{ + std::vector buffer; + xml_document<> doc; + parse_file(filename, buffer, doc, do_warn_on_missing); + xml_node<>* root = doc.first_node(); + + if(!root) + { + return; + } + for (xml_node<>* set_node = root->first_node("cardSet"); + set_node; + set_node = set_node->next_sibling("cardSet")) + { + xml_node<>* id_node(set_node->first_node("id")); + xml_node<>* visible_node = set_node->first_node("visible"); + if (id_node && visible_node && atoi(visible_node->value())) + { + all_cards.visible_cardset.insert(atoi(id_node->value())); + } + } +} +//------------------------------------------------------------------------------ +Deck* read_deck(Decks& decks, const Cards& all_cards, xml_node<>* node, DeckType::DeckType decktype, unsigned id, std::string base_deck_name) +{ + xml_node<>* commander_node(node->first_node("commander")); + const Card* card = all_cards.by_id(atoi(commander_node->value())); + const Card* commander_card{card}; + xml_node<>* commander_max_level_node(node->first_node("commander_max_level")); + unsigned commander_max_level = commander_max_level_node ? atoi(commander_max_level_node->value()) : commander_card->m_top_level_card->m_level; + unsigned upgrade_opportunities = commander_max_level - card->m_level; + std::vector fort_cards; + for (xml_node<>* fortress_card_node = node->first_node("fortress_card"); + fortress_card_node; + fortress_card_node = fortress_card_node->next_sibling("fortress_card")) + { + const Card * card = all_cards.by_id(atoi(fortress_card_node->first_attribute("id")->value())); + fort_cards.push_back(card); + upgrade_opportunities += card->m_top_level_card->m_level - card->m_level; + } + std::vector always_cards; + std::vector>> some_cards; + xml_node<>* deck_node(node->first_node("deck")); + xml_node<>* levels_node(node->first_node("levels")); + unsigned max_level = levels_node ? atoi(levels_node->value()) : 10; + xml_node<>* always_node{deck_node->first_node("always_include")}; + for(xml_node<>* card_node = (always_node ? always_node : deck_node)->first_node("card"); + card_node; + card_node = card_node->next_sibling("card")) + { + card = all_cards.by_id(atoi(card_node->value())); + always_cards.push_back(card); + upgrade_opportunities += card->m_top_level_card->m_level - card->m_level; + } + for(xml_node<>* pool_node = deck_node->first_node("card_pool"); + pool_node; + pool_node = pool_node->next_sibling("card_pool")) + { + unsigned num_cards_from_pool(atoi(pool_node->first_attribute("amount")->value())); + unsigned replicates(pool_node->first_attribute("replicates") ? atoi(pool_node->first_attribute("replicates")->value()) : 1); + std::vector cards_from_pool; + unsigned upgrade_points = 0; + for(xml_node<>* card_node = pool_node->first_node("card"); + card_node; + card_node = card_node->next_sibling("card")) + { + card = all_cards.by_id(atoi(card_node->value())); + cards_from_pool.push_back(card); + upgrade_points += card->m_top_level_card->m_level - card->m_level; + } + some_cards.push_back(std::make_tuple(num_cards_from_pool, replicates, cards_from_pool)); + upgrade_opportunities += upgrade_points * num_cards_from_pool * replicates / cards_from_pool.size(); + } + xml_node<>* mission_req_node(node->first_node(decktype == DeckType::mission ? "req" : "mission_req")); + unsigned mission_req(mission_req_node ? atoi(mission_req_node->value()) : 0); + + for (unsigned level = 1; level < max_level; ++ level) + { + std::string deck_name = base_deck_name + "-" + to_string(level); + decks.decks.push_back(Deck{all_cards, decktype, id, deck_name, (upgrade_opportunities + 1) * (level - 1) / (max_level - 1), upgrade_opportunities}); + Deck* deck = &decks.decks.back(); + deck->set(commander_card, commander_max_level, always_cards, some_cards, mission_req); + deck->fort_cards = fort_cards; + decks.add_deck(deck, deck_name); + decks.add_deck(deck, decktype_names[decktype] + " #" + to_string(id) + "-" + to_string(level)); + } + + decks.decks.push_back(Deck{all_cards, decktype, id, base_deck_name}); + Deck* deck = &decks.decks.back(); + deck->set(commander_card, commander_max_level, always_cards, some_cards, mission_req); + deck->fort_cards = fort_cards; + + // upgrade cards for full-level missions/raids + if (max_level > 1) + { + while (deck->commander->m_level < commander_max_level) + { deck->commander = deck->commander->upgraded(); } + for (auto && card: deck->fort_cards) + { card = card->m_top_level_card; } + for (auto && card: deck->cards) + { card = card->m_top_level_card; } + for (auto && pool: deck->variable_cards) + { + for (auto && card: std::get<2>(pool)) + { card = card->m_top_level_card; } + } + } + + decks.add_deck(deck, base_deck_name); + decks.add_deck(deck, base_deck_name + "-" + to_string(max_level)); + decks.add_deck(deck, decktype_names[decktype] + " #" + to_string(id)); + decks.add_deck(deck, decktype_names[decktype] + " #" + to_string(id) + "-" + to_string(max_level)); + decks.by_type_id[{decktype, id}] = deck; + return deck; +} +//------------------------------------------------------------------------------ +void read_missions(Decks& decks, const Cards& all_cards, const std::string & filename, bool do_warn_on_missing=true) +{ + std::vector buffer; + xml_document<> doc; + parse_file(filename.c_str(), buffer, doc, do_warn_on_missing); + xml_node<>* root = doc.first_node(); + + if(!root) + { + return; + } + + for(xml_node<>* mission_node = root->first_node("mission"); + mission_node; + mission_node = mission_node->next_sibling("mission")) + { + std::vector card_ids; + xml_node<>* id_node(mission_node->first_node("id")); + assert(id_node); + unsigned id(id_node ? atoi(id_node->value()) : 0); + xml_node<>* name_node(mission_node->first_node("name")); + std::string deck_name{name_node->value()}; + try + { + read_deck(decks, all_cards, mission_node, DeckType::mission, id, deck_name); + } + catch (const std::runtime_error& e) + { + std::cerr << "Warning: Failed to parse mission [" << deck_name << "] in file " << filename << ": [" << e.what() << "]. Skip the mission.\n"; + continue; + } + } +} +//------------------------------------------------------------------------------ +void read_raids(Decks& decks, const Cards& all_cards, const std::string & filename, bool do_warn_on_missing=true) +{ + std::vector buffer; + xml_document<> doc; + parse_file(filename.c_str(), buffer, doc, do_warn_on_missing); + xml_node<>* root = doc.first_node(); + + if(!root) + { + return; + } + + for(xml_node<>* raid_node = root->first_node("raid"); + raid_node; + raid_node = raid_node->next_sibling("raid")) + { + xml_node<>* id_node(raid_node->first_node("id")); + assert(id_node); + unsigned id(id_node ? atoi(id_node->value()) : 0); + xml_node<>* name_node(raid_node->first_node("name")); + std::string deck_name{name_node->value()}; + try + { + read_deck(decks, all_cards, raid_node, DeckType::raid, id, deck_name); + } + catch (const std::runtime_error& e) + { + std::cerr << "Warning: Failed to parse raid [" << deck_name << "] in file " << filename << ": [" << e.what() << "]. Skip the raid.\n"; + continue; + } + } + + for(xml_node<>* campaign_node = root->first_node("campaign"); + campaign_node; + campaign_node = campaign_node->next_sibling("campaign")) + { + xml_node<>* id_node(campaign_node->first_node("id")); + assert(id_node); + unsigned id(id_node ? atoi(id_node->value()) : 0); + for (auto && name_node = campaign_node->first_node("name"); + name_node; + name_node = name_node->next_sibling("name")) + { + try + { + read_deck(decks, all_cards, campaign_node, DeckType::campaign, id, name_node->value()); + } + catch (const std::runtime_error& e) + { + std::cerr << "Warning: Failed to parse campaign [" << name_node->value() << "] in file " << filename << ": [" << e.what() << "]. Skip the campaign.\n"; + continue; + } + } + } +} + +//------------------------------------------------------------------------------ +void load_recipes_xml(Cards& all_cards, const std::string & filename, bool do_warn_on_missing=true) +{ + std::vector buffer; + xml_document<> doc; + parse_file(filename, buffer, doc, do_warn_on_missing); + xml_node<>* root = doc.first_node(); + + if(!root) + { + return; + } + + for(xml_node<>* recipe_node = root->first_node("fusion_recipe"); + recipe_node; + recipe_node = recipe_node->next_sibling("fusion_recipe")) + { + xml_node<>* card_id_node(recipe_node->first_node("card_id")); + if (!card_id_node) { continue; } + unsigned card_id(atoi(card_id_node->value())); + Card * card = all_cards.cards_by_id[card_id]; + if (!card) { + std::cerr << "Could not find card by id " << card_id << std::endl; + continue; + } + + for(xml_node<>* resource_node = recipe_node->first_node("resource"); + resource_node; + resource_node = resource_node->next_sibling("resource")) + { + unsigned card_id(node_value(resource_node, "card_id")); + unsigned number(node_value(resource_node, "number")); + if (card_id == 0 || number == 0) { continue; } + Card * material_card = all_cards.cards_by_id[card_id]; + card->m_recipe_cards[material_card] += number; + material_card->m_used_for_cards[card] += number; + } + } +} + diff --git a/xml.h b/xml.h new file mode 100644 index 00000000..6c161991 --- /dev/null +++ b/xml.h @@ -0,0 +1,19 @@ +#ifndef XML_H_INCLUDED +#define XML_H_INCLUDED + +#include +#include "tyrant.h" + +class Cards; +class Decks; +class Achievement; + +Skill skill_name_to_id(const std::string & name); +void load_cards_xml(Cards & all_cards, const std::string & filename, bool do_warn_on_missing); +void load_skills_set_xml(Cards & all_cards, const std::string & filename, bool do_warn_on_missing); +void load_decks_xml(Decks& decks, const Cards& all_cards, const std::string & mission_filename, const std::string & raid_filename, bool do_warn_on_missing); +void load_recipes_xml(Cards& all_cards, const std::string & filename, bool do_warn_on_missing); +void read_missions(Decks& decks, const Cards& all_cards, const std::string & filename, bool do_warn_on_missing); +void read_raids(Decks& decks, const Cards& all_cards, const std::string & filename, bool do_warn_on_missing); + +#endif