diff --git a/CosmosQuest.exe b/CosmosQuest.exe index 45e65e4..703ac9f 100644 Binary files a/CosmosQuest.exe and b/CosmosQuest.exe differ diff --git a/OLDREADME.md b/OLDREADME.md new file mode 100644 index 0000000..02fc7d5 --- /dev/null +++ b/OLDREADME.md @@ -0,0 +1,132 @@ +# Cosmos Quest PvE Instance Solver + +## Summary +I took over the developement out of interest and educational purposes. Well, I also needed a good calculator and David's approach was a good place to start. My goal is to provide a Calculator that is usable even on difficult problems in respect to memory usage and calculation time. + +Game forum and disscusuion thread [here](https://www.kongregate.com/forums/910715-cosmos-quest/topics/988381-version-2-9-1-merry-worldbosses). + +Other Repos and Places you might find interesting: +* [Alternative Input throgh Latas' Graphical User Interface](https://github.com/Wiedmolol/CQMacroCreator) +* [Web-Based Macro File Creator](https://allions.net/macrocreator/) +* [Malizia's version of the Calc](https://github.com/Maliziacat/C-Hero-Calc) +* [Battle Specification and Hero Skill Explanations](https://www.kongregate.com/forums/910715-cosmos-quest/topics/959359-detailed-explanations-of-the-hero-abilities) + +### Features +* C++ based Calcultor for solving PvE instances +* Heroes are implemented, levelable and skills are fully functional +* Completely controllable via Command Line or through external Macro Files +* All Quests predefined and accessable +* Worldboss Mode for maximising damage +* Specify multiple quests and let it work on them uninterupted +* Outputs a battle string that can be used to view the battle ingame from the Tournament Page +* Precompiled exe included (Built on Windows 8.1 (64Bit), no guarantee for other operating systems) + +### What's New? +Bunch of Updates happend quietly in the background. +* Calc will now display its version to make it easier for me to determine if faulty interactions happend because of buggy code or due to a deprecated version being used +* I started to reintroduce some caching for fights to speed up the battles but generally the new code is still quite slow +* -1 as a lower limit no properly works and disables ALL normal Monsters +* **Worldbosses are added**. Having a worldboss in the enemy lineup (same input scheme as heroes) will make the calc enter worldboss mode and make it try to find the maximum damage +* **Ascended Heroes are added.** Names are a + original name. Example: Ascended Geror -> ageror:1 +* **Christmas Heroes are added** including a Hero that will be available soon through the advent calendar + +## Usage + +For most people, just downloading the exe and running it will be enough. For those who are not on Windows they will need to download all files and compile for themselves. + +### Input +**Inputting your heroes**: +Enter all of your heroes that you want the Calc to consider for a solution. + +To enter a Hero type its name and a `:` followed by its level. Press enter to submit. (Example: `geror:23`) +Note: Hero names are always typed as one word, ignoring any special characters. (`Sîgrun` -> `sigrun`, `Jack'o Knight` -> `jackoknight`), Ascended Heroes are marked by an `a` in front of their name. (`Ascended Geror` -> `ageror`) + +Once you have entered all your desired heroes you can either type `done` or press enter twice to finish. + +**Entering a lower Follower Limit**: +This determines how expensive a monster needs to be in order for the calculator to consider it for a solution. +This feature is intended for users with a lot of followers or good heroes to ignore weak monsters. +Example: Entering `215000` will exclude `e8` and cheaper monsters in the solution. + +Special Values are: `0` for ALL monsters considered and `-1` for NO monsters considered. + +**Entering an upper Follower Limit**: +This determines how expensive the entire solution is allowed to be. +I only reluctantly put this option in because a lot of people asked for it. Note that as soon as calculation starts an upper bound is automatically generated by the Calc. +You can enter your followers here if you think that it speeds up calculation but then you won't be able to know how many followers you are missing to beat the lineup. Your choice. + +Special Values are: `-1` for unlimited followers. + +**Entering an enemy Lineup**: +Lineups are what you will modify most often. Input here the Fight against which you want to win. + +You can enter normal monsters by combining their element (Fire -> f, Earth -> e, Air -> a, Water -> w) with their tier number. +Example: `Kodama` -> `a1`, `Kraken` -> `w7` + +You can enter Heroes like you would your own. +You can enter Worldbosses just like Heroes. +If Units belong to the same Lineup they are separated with a `,`. +Full Example: `a13,geror:12,jackoknight:23,f2,groth:1,ladyoftwilight:1` + +Alternatively you can enter quests by tying `quest`, the questnumber, a `-` and the difficulty in a range from `1` to `3`. +Example: `quest34-1` will make the Calc look for a solution to quest 34 with 6 monsters, `quest55-3` will make the Calc look for a solution for quest 55 with 4 monsters. + +If you want to calculate a lot of lineups with the same settings you can do so by entering multiple lineups here and separating them with a space(` `). The Calc will then solve every lineup without stopping inbetween. + +### Compiling +Personally I get it to compile by running: +`g++ -std=c++11 -Ofast -o CosmosQuest main.cpp inputProcessing.cpp cosmosData.cpp battleLogic.cpp base64.cpp` from the command line. + +**Makefile**: Base Makefile provided by BugsyLansky. + +### Macro Files + +What are they? +Macro files are the future! +* Macro files are nothing else than your input in text file form. +* Whenever the Calc needs input it just grabs the next line from the macro file. +* When the last line of the marco file is used, the Calc switches back to manual input. +* A `//` makes the Calc ignore everything in the same line that comes after it. + +If this is too much for you please refer to Latas' UI or the Web Editor that will automatically generate macro files. +If you do want to learn but still feel lost, here is a [Imgur Album](https://imgur.com/a/CXy4A) explaning how to make your own macro files. + +How to use them? Multiple Options: +1. Start the program normally. It will take the `default.cqinput`-file as input +2. Start the program via command line like: `CosmosQuest.exe macroFile` instead of just `CosmosQuest.exe` and the program will read everything from the file you specified. +3. Compile yourself and add your own default macro file name. This will stop you having to start the program via command line. + +### Input via command line +Input via command line is now mostly unavailable. Compiling yourself or removing `default.cqinput` from the folder will still give you access to it though. + +### Control Variables +**If you want to use change any of those values you have to compile the program yourself!** +* `firstDominace` This controls at which army length the calc should start removing suboptimal solutions. Setting this higher _might_ improve the solution. But treat this with extreme caution as it can cause your PC run out of RAM rather quickly. +* `macroFileName` Path to your default macro file +* `useDefaultMacroFile` Whether you want to always use the specified macro file or not +* `showMacroFileInput` If enabled will hide any input promts that are answered by a macro file +* `showBattleReplayString` If disabled will hide the replay strings after every solution + +## Bugs, Warnings and other problems + +### Regarding non-optimal solutions +I feel like i need to put this up here because as many of you noticed, there are some situation where this calculator does in fact not give the optimal solution. +The reason for this is that old optimization code back from when this program was designed for only normal monsters is still in there and has bugs related to hero abilities. However this code is **absolutely vital** to keep the runtime of the program in managable regions. + +On the user end, changing what heroes are enabled, what level they have, etc. can all affect the outcome of the calculation. +Even small changes that I make in the code can be the reason why on one day it might suddenly give a worse or better solution than before. +This is undesirable but will require heavy improvements on other ends or a completely new approach to fix. + +### Regarding RAM usage +The RAM usage as well as computation time heavily scales with available Heroes and Monsters, so I reccomend disabling as many Heroes as possible and setting an appropriate lower limit on Monster Cost. +If you calculate a high DQ-Level you probably wont need those Lv.1 Commons or anything cheaper than monster tier 5 after all. +Having only 15 or 20 Heroes is totally fine but if you enable all of them, your machine will probably run out of RAM. + +### Bugs that I'm aware of +No currently known unintended behaviours. Let me know if you find anything. + +### Potential Errors: +**bad_alloc**: You get this error when the program tries to use more RAM than your computer has available. +There is no fix available for this, but I work hard to try and keep the general RAM usage to a minimum. + +**The programm outputs: "FATAL ERROR"**: If this happens you should leave a comment in the forums because this is not normal behaviour. diff --git a/README.md b/README.md index 02fc7d5..ab2be0d 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,45 @@ -# Cosmos Quest PvE Instance Solver - -## Summary -I took over the developement out of interest and educational purposes. Well, I also needed a good calculator and David's approach was a good place to start. My goal is to provide a Calculator that is usable even on difficult problems in respect to memory usage and calculation time. - -Game forum and disscusuion thread [here](https://www.kongregate.com/forums/910715-cosmos-quest/topics/988381-version-2-9-1-merry-worldbosses). - -Other Repos and Places you might find interesting: -* [Alternative Input throgh Latas' Graphical User Interface](https://github.com/Wiedmolol/CQMacroCreator) -* [Web-Based Macro File Creator](https://allions.net/macrocreator/) -* [Malizia's version of the Calc](https://github.com/Maliziacat/C-Hero-Calc) -* [Battle Specification and Hero Skill Explanations](https://www.kongregate.com/forums/910715-cosmos-quest/topics/959359-detailed-explanations-of-the-hero-abilities) - -### Features -* C++ based Calcultor for solving PvE instances -* Heroes are implemented, levelable and skills are fully functional -* Completely controllable via Command Line or through external Macro Files -* All Quests predefined and accessable -* Worldboss Mode for maximising damage -* Specify multiple quests and let it work on them uninterupted -* Outputs a battle string that can be used to view the battle ingame from the Tournament Page -* Precompiled exe included (Built on Windows 8.1 (64Bit), no guarantee for other operating systems) - -### What's New? -Bunch of Updates happend quietly in the background. -* Calc will now display its version to make it easier for me to determine if faulty interactions happend because of buggy code or due to a deprecated version being used -* I started to reintroduce some caching for fights to speed up the battles but generally the new code is still quite slow -* -1 as a lower limit no properly works and disables ALL normal Monsters -* **Worldbosses are added**. Having a worldboss in the enemy lineup (same input scheme as heroes) will make the calc enter worldboss mode and make it try to find the maximum damage -* **Ascended Heroes are added.** Names are a + original name. Example: Ascended Geror -> ageror:1 -* **Christmas Heroes are added** including a Hero that will be available soon through the advent calendar - -## Usage - -For most people, just downloading the exe and running it will be enough. For those who are not on Windows they will need to download all files and compile for themselves. - -### Input -**Inputting your heroes**: -Enter all of your heroes that you want the Calc to consider for a solution. - -To enter a Hero type its name and a `:` followed by its level. Press enter to submit. (Example: `geror:23`) -Note: Hero names are always typed as one word, ignoring any special characters. (`Sîgrun` -> `sigrun`, `Jack'o Knight` -> `jackoknight`), Ascended Heroes are marked by an `a` in front of their name. (`Ascended Geror` -> `ageror`) - -Once you have entered all your desired heroes you can either type `done` or press enter twice to finish. - -**Entering a lower Follower Limit**: -This determines how expensive a monster needs to be in order for the calculator to consider it for a solution. -This feature is intended for users with a lot of followers or good heroes to ignore weak monsters. -Example: Entering `215000` will exclude `e8` and cheaper monsters in the solution. - -Special Values are: `0` for ALL monsters considered and `-1` for NO monsters considered. - -**Entering an upper Follower Limit**: -This determines how expensive the entire solution is allowed to be. -I only reluctantly put this option in because a lot of people asked for it. Note that as soon as calculation starts an upper bound is automatically generated by the Calc. -You can enter your followers here if you think that it speeds up calculation but then you won't be able to know how many followers you are missing to beat the lineup. Your choice. - -Special Values are: `-1` for unlimited followers. - -**Entering an enemy Lineup**: -Lineups are what you will modify most often. Input here the Fight against which you want to win. - -You can enter normal monsters by combining their element (Fire -> f, Earth -> e, Air -> a, Water -> w) with their tier number. -Example: `Kodama` -> `a1`, `Kraken` -> `w7` - -You can enter Heroes like you would your own. -You can enter Worldbosses just like Heroes. -If Units belong to the same Lineup they are separated with a `,`. -Full Example: `a13,geror:12,jackoknight:23,f2,groth:1,ladyoftwilight:1` - -Alternatively you can enter quests by tying `quest`, the questnumber, a `-` and the difficulty in a range from `1` to `3`. -Example: `quest34-1` will make the Calc look for a solution to quest 34 with 6 monsters, `quest55-3` will make the Calc look for a solution for quest 55 with 4 monsters. - -If you want to calculate a lot of lineups with the same settings you can do so by entering multiple lineups here and separating them with a space(` `). The Calc will then solve every lineup without stopping inbetween. - -### Compiling -Personally I get it to compile by running: -`g++ -std=c++11 -Ofast -o CosmosQuest main.cpp inputProcessing.cpp cosmosData.cpp battleLogic.cpp base64.cpp` from the command line. - -**Makefile**: Base Makefile provided by BugsyLansky. - -### Macro Files - -What are they? -Macro files are the future! -* Macro files are nothing else than your input in text file form. -* Whenever the Calc needs input it just grabs the next line from the macro file. -* When the last line of the marco file is used, the Calc switches back to manual input. -* A `//` makes the Calc ignore everything in the same line that comes after it. - -If this is too much for you please refer to Latas' UI or the Web Editor that will automatically generate macro files. -If you do want to learn but still feel lost, here is a [Imgur Album](https://imgur.com/a/CXy4A) explaning how to make your own macro files. - -How to use them? Multiple Options: -1. Start the program normally. It will take the `default.cqinput`-file as input -2. Start the program via command line like: `CosmosQuest.exe macroFile` instead of just `CosmosQuest.exe` and the program will read everything from the file you specified. -3. Compile yourself and add your own default macro file name. This will stop you having to start the program via command line. - -### Input via command line -Input via command line is now mostly unavailable. Compiling yourself or removing `default.cqinput` from the folder will still give you access to it though. - -### Control Variables -**If you want to use change any of those values you have to compile the program yourself!** -* `firstDominace` This controls at which army length the calc should start removing suboptimal solutions. Setting this higher _might_ improve the solution. But treat this with extreme caution as it can cause your PC run out of RAM rather quickly. -* `macroFileName` Path to your default macro file -* `useDefaultMacroFile` Whether you want to always use the specified macro file or not -* `showMacroFileInput` If enabled will hide any input promts that are answered by a macro file -* `showBattleReplayString` If disabled will hide the replay strings after every solution - -## Bugs, Warnings and other problems - -### Regarding non-optimal solutions -I feel like i need to put this up here because as many of you noticed, there are some situation where this calculator does in fact not give the optimal solution. -The reason for this is that old optimization code back from when this program was designed for only normal monsters is still in there and has bugs related to hero abilities. However this code is **absolutely vital** to keep the runtime of the program in managable regions. - -On the user end, changing what heroes are enabled, what level they have, etc. can all affect the outcome of the calculation. -Even small changes that I make in the code can be the reason why on one day it might suddenly give a worse or better solution than before. -This is undesirable but will require heavy improvements on other ends or a completely new approach to fix. - -### Regarding RAM usage -The RAM usage as well as computation time heavily scales with available Heroes and Monsters, so I reccomend disabling as many Heroes as possible and setting an appropriate lower limit on Monster Cost. -If you calculate a high DQ-Level you probably wont need those Lv.1 Commons or anything cheaper than monster tier 5 after all. -Having only 15 or 20 Heroes is totally fine but if you enable all of them, your machine will probably run out of RAM. - -### Bugs that I'm aware of -No currently known unintended behaviours. Let me know if you find anything. - -### Potential Errors: -**bad_alloc**: You get this error when the program tries to use more RAM than your computer has available. -There is no fix available for this, but I work hard to try and keep the general RAM usage to a minimum. - -**The programm outputs: "FATAL ERROR"**: If this happens you should leave a comment in the forums because this is not normal behaviour. +# Cosmos Quest PvE Instance Solver + +### More information +The old readme is available by clicking OLDREADME.md above. In this one, I'll only list changes, so you may want to read the old one after this one if you're not familiar with the program. + +### Looking at forked versions and other resources +This is the final version I'm releasing. For help finding most updated forks, you can try [here](https://github.com/nls0/C-Hero-Calc/network). + +Last discussion thread I made [here](http://www.kongregate.com/forums/910715-cosmos-quest/topics/1683470-calculator-v3-0-2-0g). You might be able to find a more updated thread for a newer fork at some point. + +* [Alternative Input throgh Latas' Graphical User Interface](https://github.com/Wiedmolol/CQMacroCreator) +* [Battle Specification and Hero Skill Explanations](https://www.kongregate.com/forums/910715-cosmos-quest/topics/959359-detailed-explanations-of-the-hero-abilities) (somewhat out-of-date, imperfect) + +### Files you may want to use (or just download everything) +CosmosQuest.exe +default.cqconfig +cqdata.txt + +### Important changes/notes +The goal of the calculator is to provide accurate calculations, but the game code currently has several [disparities](https://www.kongregate.com/forums/910715-cosmos-quest/topics/1678312-battle-oddities) where one side is treated differently than the other. The calculator currently assumes both sides are treated the same and like the left side. It would be difficult to make two sets of rules for the calculator. + +The game replays currently show different damage scores compared to what it internally calculates. For example, hitting a fire enemy with a water unit of 79 attack will result in 119 damage, but only 118.5 damage score. Other things to look at are ricochet damage and aoe damage. It is possible to fix this by looking at the game code, but I won't be doing it. + +When using CQMacroCreator with send checked, your cqconfig file will not be used. It will still be used if you use the macrocreator to open up a console window. (at time of writing) + +Multithreading has been added. Default number of threads is 6. You can change this in default.cqconfig. + +The calculator now stops on first solution found by default. You have to change the default.cqconfig option STOP\_FIRST\_SOLUTION to false if you want to use the calculator to find the cheapest solution (e.g. when looking for how many more followers you need to complete a quest). + +Gambler heroes (Dicemaster, Luxurius Maximus, Pokerface) are working. The way they work is by considering enemy lineup and order including empty space. For the enemy, calc assumes empty space is in very front. If you get a solution with fewer than 6 units against a lineup with an enemy gambler, you should also put any empty space in the very front. Theoretically it would be possible to look for solutions with alternate empty space placements, but the calc doesn't currently do it. + +The calculator begins depth first searching at max size - 2. This significantly reduces runtime (when a solution exists) and memory usage. You should be able to use more units now, but you should still avoid using bad units, particularly very cheap monsters. + +Dominance checks have been removed. This was necessary as this was resulting in a possibility of failing to find a solution where it existed. It was somewhat arbitrarily pruning possibilities providing a false speed-up. + +Monsters, heroes, hero aliases, and quests can be added and changed by modifying the cqdata.txt file. Adding new heroes in the data file is only possible if they use an existing ability. For the data file, don't use any empty lines, place new entries at the bottom of the section (order is important). For quests, make sure there is a space followed by a + at the end. If the file is not present, hardcoded data will be used. + +### Compiling +You can build it in one command with: +g++ -Ofast -std=c++11 -o CosmosQuest main.cpp inputProcessing.cpp cosmosData.cpp battleLogic.cpp base64.cpp -s -static -static-libstdc++ -static-libgcc -pthread + +The .exe included was built on Windows 10 (64Bit) using [MingW-W64-builds](http://mingw-w64.org/doku.php/download) x86\_64-8.1.0-posix-seh-rt_v6-rev0. + +### Hero aliases +Open up cqdata.txt to see them. You can also add your own. \ No newline at end of file diff --git a/battleLogic.cpp b/battleLogic.cpp index e7a6b18..9dd504b 100644 --- a/battleLogic.cpp +++ b/battleLogic.cpp @@ -1,16 +1,10 @@ #include "battleLogic.h" -int fightsSimulatedDefault; -int * totalFightsSimulated = &fightsSimulatedDefault; - // Prototype function! Currently not used. Function determining if a monster is strictly better than another bool isBetter(Monster * a, Monster * b, bool considerAbilities) { if (a->element == b->element) { return (a->damage >= b->damage) && (a->hp >= b->hp); } else { // a needs to be better than b even when b has elemental advantage, or a is at disadvantage - return !considerAbilities && (a->damage >= (int16_t) ((float) b->damage * elementalBoost)) && (a->hp >= (int16_t) ((float) b->hp * elementalBoost)); + return !considerAbilities && (a->damage >= (int16_t) ((double) b->damage * elementalBoost)) && (a->hp >= (int16_t) ((double) b->hp * elementalBoost)); } -} - -ArmyCondition leftCondition; -ArmyCondition rightCondition; \ No newline at end of file +} diff --git a/battleLogic.h b/battleLogic.h index b99817b..441c539 100644 --- a/battleLogic.h +++ b/battleLogic.h @@ -1,323 +1,721 @@ -#ifndef BATTLE_LOGIC_HEADER -#define BATTLE_LOGIC_HEADER - -#include -#include - -#include "cosmosData.h" - -extern int * totalFightsSimulated; -extern int fightsSimulatedDefault; - -const int VALID_RAINBOW_CONDITION = 15; // Binary 00001111 -> means all elements were added - -// Struct keeping track of everything that is only valid for one turn -struct TurnData { - int baseDamage = 0; - float multiplier = 1; - - int buffDamage = 0; - int protection = 0; - int aoeDamage = 0; - int healing = 0; - float dampFactor = 1; - - float valkyrieMult = 0; - float valkyrieDamage = 0; - bool trampleTriggered = false; - int paoeDamage = 0; - int witherer = -1; -}; - -// Keep track of an army's condition during a fight and save some convinience data -class ArmyCondition { - public: - int armySize; - Monster * lineup[ARMY_MAX_SIZE]; - int remainingHealths[ARMY_MAX_SIZE]; - SkillType skillTypes[ARMY_MAX_SIZE]; - Element skillTargets[ARMY_MAX_SIZE]; - float skillAmounts[ARMY_MAX_SIZE]; - - bool rainbowConditions[ARMY_MAX_SIZE]; // for rainbow ability - int pureMonsters[ARMY_MAX_SIZE]; // for friends ability - - int berserkProcs; // for berserk ability - - int monstersLost; - - TurnData turnData; - - inline void init(const Army & army, const int oldMonstersLost, const int aoeDamage); - inline void afterDeath(); - inline void startNewTurn(); - inline void getDamage(const int turncounter, const Element opposingElement, const int opposingProtection, const float opposingDampFactor); - inline void resolveDamage(TurnData & opposing); - -}; - -// extract and extrapolate all necessary data from an army -inline void ArmyCondition::init(const Army & army, const int oldMonstersLost, const int aoeDamage) { - int i; - HeroSkill * skill; - - int tempRainbowCondition = 0; - int tempPureMonsters = 0; - - armySize = army.monsterAmount; - monstersLost = oldMonstersLost; - berserkProcs = 0; - - for (i = armySize -1; i >= monstersLost; i--) { - lineup[i] = &monsterReference[army.monsters[i]]; - - skill = &(lineup[i]->skill); - skillTypes[i] = skill->skillType; - skillTargets[i] = skill->target; - skillAmounts[i] = skill->amount; - remainingHealths[i] = lineup[i]->hp - aoeDamage; - - rainbowConditions[i] = tempRainbowCondition == VALID_RAINBOW_CONDITION; - pureMonsters[i] = tempPureMonsters; - - tempRainbowCondition |= 1 << lineup[i]->element; - if (skill->skillType == NOTHING) { - tempPureMonsters++; - } - } -} - -// Reset turndata and fill it again with the hero abilities' values -inline void ArmyCondition::startNewTurn() { - int i; - - turnData.buffDamage = 0; - turnData.protection = 0; - turnData.aoeDamage = 0; - turnData.healing = 0; - turnData.dampFactor = 1; - - // Gather all skills that trigger globally - for (i = monstersLost; i < armySize; i++) { - switch (skillTypes[i]) { - default: break; - case PROTECT: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { - turnData.protection += (int) skillAmounts[i]; - } break; - case BUFF: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { - turnData.buffDamage += (int) skillAmounts[i]; - } break; - case CHAMPION: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { - turnData.buffDamage += (int) skillAmounts[i]; - turnData.protection += (int) skillAmounts[i]; - } break; - case HEAL: turnData.healing += (int) skillAmounts[i]; - break; - case AOE: turnData.aoeDamage += (int) skillAmounts[i]; - break; - case LIFESTEAL: turnData.aoeDamage += (int) skillAmounts[i]; - turnData.healing += (int) skillAmounts[i]; - break; - case DAMPEN: turnData.dampFactor *= skillAmounts[i]; - break; - } - } -} - -// Handle all self-centered abilites and other multipliers on damage -// Protection needs to be calculated at this point. -inline void ArmyCondition::getDamage(const int turncounter, const Element opposingElement, const int opposingProtection, const float opposingDampFactor) { - turnData.baseDamage = lineup[monstersLost]->damage; // Get Base damage - - // Handle Monsters with skills that only activate on attack. - turnData.paoeDamage = 0; - turnData.trampleTriggered = false; - turnData.valkyrieMult = 0; - turnData.witherer = -1; - turnData.multiplier = 1; - switch (skillTypes[monstersLost]) { - default: break; - case FRIENDS: turnData.multiplier *= (float) pow(skillAmounts[monstersLost], pureMonsters[monstersLost]); - break; - case TRAINING: turnData.buffDamage += (int) (skillAmounts[monstersLost] * (float) turncounter); - break; - case RAINBOW: if (rainbowConditions[monstersLost]) { - turnData.buffDamage += (int) skillAmounts[monstersLost]; - } break; - case ADAPT: if (opposingElement == skillTargets[monstersLost]) { - turnData.multiplier *= skillAmounts[monstersLost]; - } break; - case BERSERK: turnData.multiplier *= (float) pow(skillAmounts[monstersLost], berserkProcs); berserkProcs++; - break; - case PIERCE: turnData.paoeDamage = (int) ((float) lineup[monstersLost]->damage * skillAmounts[monstersLost]); - break; - case VALKYRIE: turnData.valkyrieMult = skillAmounts[monstersLost]; - break; - case TRAMPLE: turnData.trampleTriggered = true; - break; - } - turnData.valkyrieDamage = (float) turnData.baseDamage * turnData.multiplier + (float) turnData.buffDamage; - - if (counter[opposingElement] == lineup[monstersLost]->element) { - turnData.valkyrieDamage *= elementalBoost; - } - if (turnData.valkyrieDamage > opposingProtection) { // Handle Protection - turnData.valkyrieDamage -= (float) opposingProtection; - } else { - turnData.valkyrieDamage = 0; - } - - turnData.baseDamage = castCeil(turnData.valkyrieDamage); - - // Handle enemy dampen ability and reduce aoe effects - if (opposingDampFactor < 1) { - turnData.valkyrieDamage *= opposingDampFactor; - turnData.aoeDamage = castCeil((float) turnData.aoeDamage * opposingDampFactor); - turnData.healing = castCeil((float) turnData.healing * opposingDampFactor); - turnData.paoeDamage = castCeil((float) turnData.paoeDamage * opposingDampFactor); - } -} - -// Add damage to the opposing side and check for deaths -inline void ArmyCondition::resolveDamage(TurnData & opposing) { - int i; - int frontliner = monstersLost; // save original frontliner - - // Apply normal attack damage to the frontliner - remainingHealths[frontliner] -= opposing.baseDamage; - - if (opposing.trampleTriggered && armySize > frontliner + 1) { - remainingHealths[frontliner + 1] -= opposing.valkyrieDamage; - } - - // Handle aoe Damage for all combatants - for (i = frontliner; i < armySize; i++) { - remainingHealths[i] -= opposing.aoeDamage; - if (i > frontliner) { // Aoe that doesnt affect the frontliner - remainingHealths[i] -= opposing.paoeDamage + castCeil(opposing.valkyrieDamage); - } - if (remainingHealths[i] <= 0) { - if (i == monstersLost) { - monstersLost++; - berserkProcs = 0; - } - skillTypes[i] = NOTHING; // disable dead hero's ability - } else { - remainingHealths[i] += turnData.healing; - if (remainingHealths[i] > lineup[i]->hp) { // Avoid overhealing - remainingHealths[i] = lineup[i]->hp; - } - } - opposing.valkyrieDamage *= opposing.valkyrieMult; - } - // Handle wither ability - if (monstersLost == frontliner && skillTypes[monstersLost] == WITHER) { - remainingHealths[monstersLost] = castCeil((float) remainingHealths[monstersLost] * skillAmounts[monstersLost]); - } -} - -extern ArmyCondition leftCondition; -extern ArmyCondition rightCondition; - -// Simulates One fight between 2 Armies and writes results into left's LastFightData -inline bool simulateFight(Army & left, Army & right, bool verbose = false) { - // left[0] and right[0] are the first monsters to fight - (*totalFightsSimulated)++; - - int turncounter; - - // Ignore lastFightData if either army-affecting heroes were added or for debugging - if (left.lastFightData.valid && !verbose) { - // Set pre-computed values to pick up where we left off - leftCondition.init(left, left.monsterAmount-1, left.lastFightData.leftAoeDamage); - rightCondition.init(right, left.lastFightData.monstersLost, left.lastFightData.rightAoeDamage); - // Check if the new addition died to Aoe - if (leftCondition.remainingHealths[leftCondition.monstersLost] <= 0) { - leftCondition.monstersLost++; - } - - rightCondition.remainingHealths[rightCondition.monstersLost] = left.lastFightData.frontHealth; - rightCondition.berserkProcs = left.lastFightData.berserk; - turncounter = left.lastFightData.turncounter; - } else { - // Load Army data into conditions - leftCondition.init(left, 0, 0); - rightCondition.init(right, 0, 0); - // Reset Potential values in fightresults - left.lastFightData.leftAoeDamage = 0; - left.lastFightData.rightAoeDamage = 0; - turncounter = 0; - } - - // Battle Loop. Continues until one side is out of monsters - while (leftCondition.monstersLost < leftCondition.armySize && rightCondition.monstersLost < rightCondition.armySize) { - leftCondition.startNewTurn(); - rightCondition.startNewTurn(); - - // Get damage with all relevant multipliers - leftCondition.getDamage(turncounter, rightCondition.lineup[rightCondition.monstersLost]->element, rightCondition.turnData.protection, rightCondition.turnData.dampFactor); - rightCondition.getDamage(turncounter, leftCondition.lineup[leftCondition.monstersLost]->element, leftCondition.turnData.protection, leftCondition.turnData.dampFactor); - - // Handle Revenge Damage before anything else. Revenge Damage caused through aoe is ignored - if (leftCondition.skillTypes[leftCondition.monstersLost] == REVENGE && - leftCondition.remainingHealths[leftCondition.monstersLost] <= rightCondition.turnData.baseDamage) { - leftCondition.turnData.aoeDamage += (int) round((float) leftCondition.lineup[leftCondition.monstersLost]->damage * leftCondition.skillAmounts[leftCondition.monstersLost]); - } - if (rightCondition.skillTypes[rightCondition.monstersLost] == REVENGE && - rightCondition.remainingHealths[rightCondition.monstersLost] <= leftCondition.turnData.baseDamage) { - rightCondition.turnData.aoeDamage += (int) round((float) rightCondition.lineup[rightCondition.monstersLost]->damage * rightCondition.skillAmounts[rightCondition.monstersLost]); - } - - left.lastFightData.leftAoeDamage += (int16_t) (rightCondition.turnData.aoeDamage + rightCondition.turnData.paoeDamage); - left.lastFightData.rightAoeDamage += (int16_t) (leftCondition.turnData.aoeDamage + leftCondition.turnData.paoeDamage); - - // Check if anything died as a result - leftCondition.resolveDamage(rightCondition.turnData); - rightCondition.resolveDamage(leftCondition.turnData); - - turncounter++; - - if (verbose) { - std::cout << "After Turn " << turncounter << ":" << std::endl; - - std::cout << "Left:" << std::endl; - std::cout << " Damage: " << std::setw(4) << leftCondition.turnData.baseDamage << std::endl; - std::cout << " Health: "; - for (int i = 0; i < leftCondition.armySize; i++) { - std::cout << std::setw(4) << leftCondition.remainingHealths[i] << " "; - } std::cout << std::endl; - - std::cout << "Right:" << std::endl; - std::cout << " Damage: " << std::setw(4) << rightCondition.turnData.baseDamage << std::endl; - std::cout << " Health: "; - for (int i = 0; i < rightCondition.armySize; i++) { - std::cout << std::setw(4) << rightCondition.remainingHealths[i] << " "; - } std::cout << std::endl; - } - } - - // write all the results into a FightResult - left.lastFightData.dominated = false; - left.lastFightData.turncounter = (int8_t) turncounter; - - if (leftCondition.monstersLost >= leftCondition.armySize) { //draws count as right wins. - left.lastFightData.monstersLost = (int8_t) rightCondition.monstersLost; - left.lastFightData.berserk = (int8_t) rightCondition.berserkProcs; - if (rightCondition.monstersLost < rightCondition.armySize) { - left.lastFightData.frontHealth = (int16_t) (rightCondition.remainingHealths[rightCondition.monstersLost]); - } else { - left.lastFightData.frontHealth = 0; - } - return false; - } else { - left.lastFightData.monstersLost = (int8_t) leftCondition.monstersLost; - left.lastFightData.frontHealth = (int16_t) (leftCondition.remainingHealths[leftCondition.monstersLost]); - left.lastFightData.berserk = (int8_t) leftCondition.berserkProcs; - return true; - } -} - -// Function determining if a monster is strictly better than another -bool isBetter(Monster * a, Monster * b, bool considerAbilities = false); - -#endif \ No newline at end of file +#ifndef BATTLE_LOGIC_HEADER +#define BATTLE_LOGIC_HEADER + +#include +#include + +#include "cosmosData.h" + +const int VALID_RAINBOW_CONDITION = 15; // Binary 00001111 -> means all elements were added + +// Struct keeping track of everything that is only valid for one turn +struct TurnData { + int64_t baseDamage = 0; + double multiplier = 1; + + int buffDamage = 0; + int protection = 0; + int aoeDamage = 0; + int healing = 0; + double dampFactor = 1; + + double counter = 0; + double valkyrieMult = 0; + double valkyrieDamage = 0; + double absorbMult = 0; + double absorbDamage = 0; + int explodeDamage = 0; + bool trampleTriggered = false; + bool guyActive = false; + int direct_target = 0; + int counter_target = 0; + double critMult = 1; + double hate = 0; + double leech = 0; + bool immunity5K = false ; +}; + +// Keep track of an army's condition during a fight and save some convenience data +class ArmyCondition { + public: + int armySize; + Monster * lineup[ARMY_MAX_SIZE]; + int64_t remainingHealths[ARMY_MAX_SIZE]; + int64_t maxHealths[ARMY_MAX_SIZE]; + SkillType skillTypes[ARMY_MAX_SIZE]; + Element skillTargets[ARMY_MAX_SIZE]; + double skillAmounts[ARMY_MAX_SIZE]; + + bool rainbowConditions[ARMY_MAX_SIZE]; // for rainbow ability + //int pureMonsters[ARMY_MAX_SIZE]; // for friends ability + + int dice; + bool booze; // for leprechaun's ability + int aoeZero; // for hawking's ability + + int64_t seed; + + int berserkProcs; // for berserk ability + double evolveTotal; //for evolve ability + + int monstersLost; + + bool worldboss; + + TurnData turnData; + + inline void init(const Army & army, const int oldMonstersLost, const int aoeDamage); + inline void afterDeath(); + inline void startNewTurn(); + inline void getDamage(const int turncounter, const ArmyCondition & opposingCondition); + inline void resolveDamage(TurnData & opposing); + inline int64_t getTurnSeed(int64_t seed, int turncounter) { + // From Alya + for (int i = 0; i < turncounter; ++i) { + seed = (16807 * seed) % 2147483647; + } + return seed; + //return (seed + (101 - turncounter)*(101 - turncounter)*(101 - turncounter)) % (int64_t)round((double)seed / (101 - turncounter) + (101 - turncounter)*(101 - turncounter)); + } + inline int findMaxHP(); + inline int getLuxTarget(const ArmyCondition & opposingCondition, int64_t seed); +}; + +// extract and extrapolate all necessary data from an army +inline void ArmyCondition::init(const Army & army, const int oldMonstersLost, const int aoeDamage) { + HeroSkill * skill; + + int tempRainbowCondition = 0; + int tempPureMonsters = 0; + + seed = army.seed; + armySize = army.monsterAmount; + monstersLost = oldMonstersLost; + berserkProcs = 0; + evolveTotal = 0; + + dice = -1; + booze = false; + worldboss = false; + aoeZero = 0; + + for (int i = armySize -1; i >= monstersLost; i--) { + lineup[i] = &monsterReference[army.monsters[i]]; + + skill = &(lineup[i]->skill); + skillTypes[i] = skill->skillType; + skillTargets[i] = skill->target; + skillAmounts[i] = skill->amount; + remainingHealths[i] = lineup[i]->hp - aoeDamage; + + worldboss |= lineup[i]->rarity == WORLDBOSS; + + maxHealths[i] = lineup[i]->hp; + if (skill->skillType == DICE) dice = i; //assumes only 1 unit per side can have dice ability, have to change to bool and loop at turn zero if this changes + if (skill->skillType == BEER) booze = true; + if (skill->skillType == AOEZERO) aoeZero += skill->amount; + + rainbowConditions[i] = tempRainbowCondition == VALID_RAINBOW_CONDITION; + //pureMonsters[i] = tempPureMonsters; + + tempRainbowCondition |= 1 << lineup[i]->element; + if (skill->skillType == NOTHING) { + tempPureMonsters++; + } + } +} + +// Reset turndata and fill it again with the hero abilities' values +inline void ArmyCondition::startNewTurn() { + int i; + + turnData.buffDamage = 0; + turnData.protection = 0; + turnData.aoeDamage = 0; + turnData.healing = 0; + turnData.dampFactor = 1; + turnData.absorbMult = 0; + turnData.absorbDamage = 0; + + if( skillTypes[monstersLost] == DODGE ) + { + turnData.immunity5K = true ; + } + else + { + turnData.immunity5K = false ; + } + + // Gather all skills that trigger globally + for (i = monstersLost; i < armySize; i++) { + switch (skillTypes[i]) { + default: break; + case PROTECT: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { + turnData.protection += (int) skillAmounts[i]; + } break; + case BUFF: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { + turnData.buffDamage += (int) skillAmounts[i]; + } break; + case CHAMPION: if (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element) { + turnData.buffDamage += (int) skillAmounts[i]; + turnData.protection += (int) skillAmounts[i]; + } break; + case HEAL: turnData.healing += (int) skillAmounts[i]; + break; + case AOE: turnData.aoeDamage += (int) skillAmounts[i]; + break; + case LIFESTEAL: turnData.aoeDamage += (int) skillAmounts[i]; + turnData.healing += (int) skillAmounts[i]; + break; + case DAMPEN: turnData.dampFactor *= skillAmounts[i]; + break; + case ABSORB: if (i != monstersLost) turnData.absorbMult += skillAmounts[i]; + break; + } + } +} + +// Handle all self-centered abilities and other multipliers on damage +// Protection needs to be calculated at this point. +inline void ArmyCondition::getDamage(const int turncounter, const ArmyCondition & opposingCondition) { + + turnData.baseDamage = lineup[monstersLost]->damage; // Get Base damage + + const Element opposingElement = opposingCondition.lineup[opposingCondition.monstersLost]->element; + const int opposingProtection = opposingCondition.turnData.protection; + const double opposingDampFactor = opposingCondition.turnData.dampFactor; + const double opposingAbsorbMult = opposingCondition.turnData.absorbMult; + const bool opposingImmunityDamage = opposingCondition.turnData.immunity5K; + const double opposingDamage = opposingCondition.lineup[opposingCondition.monstersLost]->damage; + ArmyCondition tempArmy; + + // Handle Monsters with skills that only activate on attack. + turnData.trampleTriggered = false; + turnData.explodeDamage = 0; + turnData.valkyrieMult = 0; + turnData.multiplier = 1; // Not used outside this function, does it need to be stored in turnData? + turnData.critMult = 1; // same as above + turnData.hate = 0; // same as above + turnData.counter = 0; + turnData.direct_target = 0; + turnData.counter_target = 0; + turnData.leech = 0; + + double friendsDamage = 0; + + switch (skillTypes[monstersLost]) { + case FRIENDS: friendsDamage = turnData.baseDamage; + for (int i = monstersLost + 1; i < armySize; i++) { + if (skillTypes[i] == NOTHING && remainingHealths[i] > 0) + friendsDamage *= skillAmounts[monstersLost]; + else if ((skillTypes[i] == BUFF || skillTypes[i] == CHAMPION) && (skillTargets[i] == ALL || skillTargets[i] == lineup[monstersLost]->element)) + friendsDamage += skillAmounts[i]; + } + break; + case TRAINING: turnData.buffDamage += (int) (skillAmounts[monstersLost] * (double) turncounter); + break; + case RAINBOW: if (rainbowConditions[monstersLost]) { + turnData.buffDamage += (int) skillAmounts[monstersLost]; + } break; + case ADAPT: if (opposingElement == skillTargets[monstersLost]) { + turnData.multiplier *= skillAmounts[monstersLost]; + } break; + case BERSERK: turnData.multiplier *= (double) pow(skillAmounts[monstersLost], berserkProcs); berserkProcs++; + break; + case VALKYRIE: turnData.valkyrieMult = skillAmounts[monstersLost]; + break; + case TRAMPLE: turnData.trampleTriggered = true; + break; + case COUNTER: turnData.counter = skillAmounts[monstersLost]; + break; + case EXPLODE: turnData.explodeDamage = skillAmounts[monstersLost]; // Explode damage gets added here, but still won't apply unless enemy frontliner dies + break; + case DICE: turnData.baseDamage += opposingCondition.seed % (int)(skillAmounts[monstersLost] + 1); // Only adds dice attack effect if dice is in front, max health is done before battle + break; + // Pick a target, Bubbles currently dampens lux damage if not targeting first according to game code, interaction should be added if this doesn't change + case LUX: turnData.direct_target = getLuxTarget(opposingCondition, getTurnSeed(opposingCondition.seed, 99 -turncounter)); + break; + case CRIT: // turnData.critMult *= getTurnSeed(opposingCondition.seed, turncounter) % 2 == 1 ? skillAmounts[monstersLost] : 1; + turnData.critMult *= getTurnSeed(opposingCondition.seed, 99 - turncounter) % 2 == 0 ? skillAmounts[monstersLost] : 1; + break; + case HATE: turnData.hate = skillAmounts[monstersLost]; + break; + case LEECH: turnData.leech = skillAmounts[monstersLost]; + break; + case EVOLVE: turnData.buffDamage += evolveTotal; + evolveTotal += opposingDamage * skillAmounts[monstersLost]; + break; + case COUNTER_MAX_HP: turnData.counter = skillAmounts[monstersLost]; + tempArmy = opposingCondition; + turnData.counter_target = tempArmy.findMaxHP(); + turnData.guyActive = true; + break; + default: break; + + } + turnData.valkyrieDamage = turnData.baseDamage; + if (friendsDamage == 0) { + if (turnData.multiplier > 1) { + turnData.valkyrieDamage *= turnData.multiplier; + } + if (turnData.buffDamage != 0) { + turnData.valkyrieDamage += turnData.buffDamage; + } + } + else { + turnData.valkyrieDamage = friendsDamage; + } + + if (counter[opposingElement] == lineup[monstersLost]->element) { + turnData.valkyrieDamage *= elementalBoost + turnData.hate; + } + if (turnData.valkyrieDamage > opposingProtection) { // Handle Protection, when this takes place currently varies based on the side the army is on according to game code + turnData.valkyrieDamage -= (double) opposingProtection; + } else { + turnData.valkyrieDamage = 0; + } + + if (turnData.critMult > 1) { + turnData.valkyrieDamage *= turnData.critMult; + } + + //absorb damage, damage rounded up later + if (opposingAbsorbMult != 0) { + turnData.absorbDamage = turnData.valkyrieDamage * opposingAbsorbMult; + turnData.valkyrieDamage = turnData.valkyrieDamage - turnData.absorbDamage; + } + //leech healing based on damage dealt + if (turnData.leech != 0) + turnData.leech *= turnData.valkyrieDamage; + + // for compiling heavyDamage version + if (turnData.valkyrieDamage >= std::numeric_limits::max()) + turnData.baseDamage = static_cast(ceil(turnData.valkyrieDamage)); + else + // turnData.baseDamage = castCeil(turnData.valkyrieDamage); + turnData.baseDamage = round(turnData.valkyrieDamage); + + // Handle enemy dampen ability and reduce aoe effects + if (opposingDampFactor < 1) { + turnData.valkyrieDamage *= opposingDampFactor; + // turnData.explodeDamage = castCeil((double) turnData.explodeDamage * opposingDampFactor); + // turnData.aoeDamage = castCeil((double) turnData.aoeDamage * opposingDampFactor); + // turnData.healing = castCeil((double) turnData.healing * opposingDampFactor); + turnData.explodeDamage = round((double) turnData.explodeDamage * opposingDampFactor); + turnData.aoeDamage = round((double) turnData.aoeDamage * opposingDampFactor); + turnData.healing = round((double) turnData.healing * opposingDampFactor); + } + + if( opposingImmunityDamage && (turnData.valkyrieDamage >= 5000 || turnData.baseDamage >= 5000 ) ) { + turnData.valkyrieDamage = 0 ; + turnData.baseDamage = 0 ; + } +} + +// Add damage to the opposing side and check for deaths +inline void ArmyCondition::resolveDamage(TurnData & opposing) { + int frontliner = monstersLost; // save original frontliner + + // Apply normal attack damage to the frontliner + // If direct_target is non-zero that means Lux is hitting something not the front liner + // Also, needed to handle here is the case where there are dead units behind the frontliner + if(opposing.direct_target > 0) { + int actual_target = frontliner; + int alive = 0; + for(int i = frontliner + 1; i < ARMY_MAX_SIZE; i++) { + // std::cout << "I is " << i << std::endl; + if(remainingHealths[i] > 0) { + alive++; + // std::cout << "Alive incremented to " << alive << std::endl; + } + if(alive >= opposing.direct_target) { + actual_target = i; + // std::cout << "Setting actual target to " << i << std::endl; + break; + } + } + // std::cout << " BASE " << opposing.baseDamage << " to " << actual_target << std::endl; + remainingHealths[actual_target] -= opposing.baseDamage; + } else { + // std::cout << " BASE " << opposing.baseDamage << " to " << frontliner + opposing.direct_target << std::endl; + remainingHealths[frontliner + opposing.direct_target] -= opposing.baseDamage; + } + + // Lee and Fawkes can only counter if they are hit directly, so if they are opposing Lux and Lux + // hits another units, they do not counter + int counter_eligible = 1; + if(skillTypes[monstersLost] == LUX && turnData.direct_target > 0) { + counter_eligible = 0; + // std::cout << "LUX DID NOT HIT FRONTLINER" << std::endl; + } + + // Add opposing.counter_target to handle fawkes not targetting the frontliner + if (opposing.counter && counter_eligible && (worldboss || remainingHealths[frontliner] > 0 || opposing.guyActive)) { + // std::cout << "COUNTER " << static_cast(ceil(turnData.baseDamage * opposing.counter)) << " damage " << " to " << frontliner + opposing.counter_target << std::endl; + // Game has been updated to remove units once dead but remainingHealths still has all units + // So, if the counter target is not the frontliner, find the real one + // So, counter_target has to skip over dead units + if(opposing.counter_target <= 0){ + // std::cout << "COUNTER " << static_cast(ceil(turnData.baseDamage * opposing.counter)) << " damage " << " to " << frontliner << std::endl; + remainingHealths[frontliner] -= static_cast(ceil(turnData.baseDamage * opposing.counter)); + } else { + // Find the (counter_target)th alive monster in remainingHealths + int actual_target = opposing.counter_target; + // std::cout << "Counter target is " << opposing.counter_target << std::endl; + int alive = 0; + for(int i = frontliner + 1; i < ARMY_MAX_SIZE; i++) { + // std::cout << "I is " << i << std::endl; + if(remainingHealths[i] > 0) { + alive++; + // std::cout << "Alive incremented to " << alive << std::endl; + } + if(alive >= opposing.counter_target) { + actual_target = i; + // std::cout << "Setting actual target to " << i << std::endl; + break; + } + } + // std::cout << "COUNTER " << static_cast(ceil(turnData.baseDamage * opposing.counter)) << " damage " << " to " << actual_target << std::endl; + remainingHealths[actual_target] -= static_cast(ceil(turnData.baseDamage * opposing.counter)); + } + + } + + if (opposing.trampleTriggered && armySize > frontliner + 1) { + // std::cout << "TRAMPLE" << std::endl; + remainingHealths[frontliner + 1] -= opposing.valkyrieDamage; + } + + if (opposing.explodeDamage != 0 && remainingHealths[frontliner] <= 0 && !worldboss) { + // std::cout << "EXPLODE" << std::endl; + opposing.aoeDamage += opposing.explodeDamage; + } + + // Handle aoe Damage for all combatants + for (int i = frontliner; i < armySize; i++) { + int aliveAtBeginning = 0; + if(remainingHealths[i] > 0 || i == frontliner) { + aliveAtBeginning = 1; + } + // handle absorbed damage + if (skillTypes[i] == ABSORB && i > frontliner) { + // remainingHealths[i] -= castCeil(opposing.absorbDamage); + remainingHealths[i] -= round(opposing.absorbDamage); + } + + remainingHealths[i] -= opposing.aoeDamage; + + if (i > frontliner) { // Aoe that doesnt affect the frontliner + // remainingHealths[i] -= castCeil(opposing.valkyrieDamage); + remainingHealths[i] -= round(opposing.valkyrieDamage); + } + if (remainingHealths[i] <= 0 && !worldboss) { + if (i == monstersLost) { + monstersLost++; + berserkProcs = 0; + evolveTotal = 0; + } + skillTypes[i] = NOTHING; // disable dead hero's ability + } else { + remainingHealths[i] += turnData.healing; + if (i == frontliner) + remainingHealths[i] += turnData.leech; + if (remainingHealths[i] > maxHealths[i]) { // Avoid overhealing + remainingHealths[i] = maxHealths[i]; + } + } + + // Always apply the valkyrieMult if it is zero. Otherwise, given the way + // that riochet is implemented it will cause melee attacks to turn into + // ricochet + if(opposing.valkyrieMult > 0) { + // Only reduce the damage if it hit an alive unit + if(aliveAtBeginning) { + opposing.valkyrieDamage *= opposing.valkyrieMult; + } + } else { + opposing.valkyrieDamage *= opposing.valkyrieMult; + } + } + // Handle wither ability + if (skillTypes[monstersLost] == WITHER && monstersLost == frontliner) { + // remainingHealths[monstersLost] = castCeil((double) remainingHealths[monstersLost] * skillAmounts[monstersLost]); + remainingHealths[monstersLost] = round((double) remainingHealths[monstersLost] * skillAmounts[monstersLost]); + } +} + +inline int ArmyCondition::findMaxHP() { + // go through alive monsters to determine most hp + // If there is a tie... + // Use the same loop as in ResolveDamage + int max_hp = 0; + int index_max_hp = 0; + for (int i = monstersLost; i < armySize; i++) { + if(remainingHealths[i] > max_hp) { + max_hp = remainingHealths[i]; + index_max_hp = i; + } + } + // This is an absolute index, the rest of the code expects a relative index + // so return it relative to the starting point + // std::cout << std::endl << "Choosing " << index_max_hp << " with " << remainingHealths[index_max_hp] << std::endl; + return index_max_hp - monstersLost; +} + +inline int ArmyCondition::getLuxTarget(const ArmyCondition & opposingCondition, int64_t seed) { + /* Javascript code from 3.2.0.4 + function shuffleBySeed(arr, seed) { + for (var size = arr.length, mapa = new Array(size), x = 0; x < size; x++) mapa[x] = (seed = (9301 * seed + 49297) % 233280) / 233280 * size | 0; + for (var i = size - 1; i > 0; i--) arr[i] = arr.splice(mapa[size - 1 - i], 1, arr[i])[0] +} + called like `else if ("rtrg" == skill.type) shuffleBySeed(turn.atk.damageFactor, seed);` + damageFactor appears to be initialized to an array of one element consisting of [1] + function getTurnData(AL, BL) { + var df = [1]; + ... + damageFactor: df, + ... + So looking at things that probably means that we need an array of size the of number alive monsters + with the first element initialized to 1 + 815500433 + */ + + // Count the number of alive monsters; + int alive_count = 0; + for(int i = 0; i < opposingCondition.armySize; i++) { + // New CQ code removes dead units, so simulate that here by checking for health + // std::cout << i << ". Remaining health is " << opposingCondition.remainingHealths[i] << std::endl; + if(opposingCondition.remainingHealths[i] > 0) { + alive_count++; + } + } + // std::cout << "Alive count is " << alive_count << std::endl; + + // Initialize the arr equivalent + int arr[alive_count]; + arr[0] = 1; + for(int i = 1; i < alive_count; i++) { + arr[i] = 0; + } + + // Initialize mapa + int mapa[alive_count]; + for(int x = 0; x < alive_count; x++) { + // mapa[x] = (seed = (9301 * seed + 49297) % 233280) / 233280 * alive_count | 0; + seed = (9301 * seed + 49297) % 233280; + // std::cout << "Seed is " << seed << std::endl; + // From debugging the JS code the in between operation being a double is important + double temp = seed; + temp = temp/ 233280; + //std::cout << "Temp is " << temp << std::endl; + temp = temp * alive_count; + //std::cout << "Temp is " << temp << std::endl; + temp = int64_t(temp) | 0; + //std::cout << "Temp is " << temp << std::endl; + mapa[x] = temp; + // std::cout << x << " => " << mapa[x] << std::endl << std::endl; + } + + /* std::cout << "IN:"; + for(int j = 0; j < alive_count ; j++) { + std::cout << arr[j] << ","; + } + std::cout << std::endl; +*/ + // Shuffle + for(int i = alive_count - 1; i > 0; i--) { + // arr[i] = arr.splice(mapa[size - 1 - i], 1, arr[i])[0] + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice + // array.splice(start[, deleteCount[, item1[, item2[, ...]]]]) + // so at the value contained in mapa[size -1 -i] in arr, remove one element, put the value + // at arr[i] in its place and set arr[i] to the deleted value + + int mapa_index = alive_count - 1 - i; + int index_to_remove = mapa[mapa_index]; + int removed_value = arr[index_to_remove]; + // overwrite the removed value + // std::cout << "Mapa index: " << mapa_index << std::endl; + arr[index_to_remove] = arr[i]; + arr[i] = removed_value; + + /* + for(int j = 0; j < alive_count ; j++) { + std::cout << arr[j] << ","; + } + std::cout << std::endl; + */ + } + + // now figure out which target to return + int return_value; + for(int i = 0; i < alive_count; i++) { + // std::cout << "Index " << i << " has value " << arr[i] << std::endl; + if(arr[i] > 0) { + // std::cout << "Setting return_value to " << i << std::endl; + return_value = i; + } + } + return return_value; +} +// Simulates One fight between 2 Armies and writes results into left's LastFightData +inline bool simulateFight(Army & left, Army & right, bool verbose = false) { + // left[0] and right[0] are the first monsters to fight + ArmyCondition leftCondition; + ArmyCondition rightCondition; + + int turncounter; + + // Ignore lastFightData if either army-affecting heroes were added or for debugging + if (left.lastFightData.valid && !verbose) { + // Set pre-computed values to pick up where we left off + leftCondition.init(left, left.monsterAmount-1, left.lastFightData.leftAoeDamage); + rightCondition.init(right, left.lastFightData.monstersLost, left.lastFightData.rightAoeDamage); + // Check if the new addition died to Aoe + if (leftCondition.remainingHealths[leftCondition.monstersLost] <= 0) { + leftCondition.monstersLost++; + } + + rightCondition.remainingHealths[rightCondition.monstersLost] = left.lastFightData.frontHealth; + rightCondition.berserkProcs = left.lastFightData.berserk; + turncounter = left.lastFightData.turncounter; + } else { + // Load Army data into conditions + leftCondition.init(left, 0, 0); + rightCondition.init(right, 0, 0); + + //----- turn zero ----- + + // Apply Dicemaster max health bonus here, attack bonus applied during battle + if (leftCondition.dice > -1) { + leftCondition.maxHealths[leftCondition.dice] += rightCondition.seed % ((int)leftCondition.skillAmounts[leftCondition.dice] + 1); + leftCondition.remainingHealths[leftCondition.dice] = leftCondition.maxHealths[leftCondition.dice]; + } + + if (rightCondition.dice > -1) { + rightCondition.maxHealths[rightCondition.dice] += leftCondition.seed % ((int)rightCondition.skillAmounts[rightCondition.dice] + 1); + rightCondition.remainingHealths[rightCondition.dice] = rightCondition.maxHealths[rightCondition.dice]; + } + + // Apply Leprechaun's skill (Beer) + if (leftCondition.booze && leftCondition.armySize < rightCondition.armySize) + for (size_t i = 0; i < ARMY_MAX_SIZE; ++i) { + rightCondition.maxHealths[i] = int(rightCondition.maxHealths[i] * leftCondition.armySize / rightCondition.armySize); + rightCondition.remainingHealths[i] = rightCondition.maxHealths[i]; + } + + if (rightCondition.booze && rightCondition.armySize < leftCondition.armySize) + for (size_t i = 0; i < ARMY_MAX_SIZE; ++i) { + leftCondition.maxHealths[i] = int(leftCondition.maxHealths[i] * rightCondition.armySize / leftCondition.armySize); + leftCondition.remainingHealths[i] = leftCondition.maxHealths[i]; + } + + // Reset Potential values in fightresults + left.lastFightData.leftAoeDamage = 0; + left.lastFightData.rightAoeDamage = 0; + turncounter = 0; + + // Apply Hawking's AOE + if (leftCondition.aoeZero || rightCondition.aoeZero) { + TurnData turnZero; + if (leftCondition.aoeZero) { + left.lastFightData.rightAoeDamage += leftCondition.aoeZero; + turnZero.aoeDamage = leftCondition.aoeZero; + rightCondition.resolveDamage(turnZero); + } + if (rightCondition.aoeZero) { + left.lastFightData.leftAoeDamage += rightCondition.aoeZero; + turnZero.aoeDamage = rightCondition.aoeZero; + leftCondition.resolveDamage(turnZero); + } + } + + //----- turn zero end ----- + } + + // Battle Loop. Continues until one side is out of monsters + //TODO: handle 100 turn limit for non-wb, also handle it for wb better maybe + while (leftCondition.monstersLost < leftCondition.armySize && rightCondition.monstersLost < rightCondition.armySize && turncounter < 100) { + leftCondition.startNewTurn(); + rightCondition.startNewTurn(); + + // Get damage with all relevant multipliers + leftCondition.getDamage(turncounter, rightCondition); + rightCondition.getDamage(turncounter, leftCondition); + + // Handle Revenge Damage before anything else. Revenge Damage caused through aoe is ignored + if (leftCondition.skillTypes[leftCondition.monstersLost] == REVENGE && + leftCondition.remainingHealths[leftCondition.monstersLost] <= rightCondition.turnData.baseDamage) { + leftCondition.turnData.aoeDamage += (int) round((double) leftCondition.lineup[leftCondition.monstersLost]->damage * leftCondition.skillAmounts[leftCondition.monstersLost]); + } + if (rightCondition.skillTypes[rightCondition.monstersLost] == REVENGE && + rightCondition.remainingHealths[rightCondition.monstersLost] <= leftCondition.turnData.baseDamage) { + rightCondition.turnData.aoeDamage += (int) round((double) rightCondition.lineup[rightCondition.monstersLost]->damage * rightCondition.skillAmounts[rightCondition.monstersLost]); + } + // Handle Deathstrike Damage before anything else. Deathstrike Damage caused through aoe is ignored + if (leftCondition.skillTypes[leftCondition.monstersLost] == DEATHSTRIKE && + leftCondition.remainingHealths[leftCondition.monstersLost] <= rightCondition.turnData.baseDamage) { + leftCondition.turnData.baseDamage += leftCondition.skillAmounts[leftCondition.monstersLost]; + } + if (rightCondition.skillTypes[rightCondition.monstersLost] == DEATHSTRIKE && + rightCondition.remainingHealths[rightCondition.monstersLost] <= leftCondition.turnData.baseDamage) { + rightCondition.turnData.baseDamage += rightCondition.skillAmounts[rightCondition.monstersLost]; + } + + left.lastFightData.leftAoeDamage += rightCondition.turnData.aoeDamage; + left.lastFightData.rightAoeDamage += leftCondition.turnData.aoeDamage; + + // Check if anything died as a result + leftCondition.resolveDamage(rightCondition.turnData); + rightCondition.resolveDamage(leftCondition.turnData); + + turncounter++; + + if (verbose) { + std::cout << std::endl << "After Turn " << turncounter << ":" << std::endl; + + std::cout << "Left:" << std::endl; + std::cout << " Damage: " << std::setw(4) << leftCondition.turnData.baseDamage << std::endl; + std::cout << " Health: "; + for (int i = 0; i < leftCondition.armySize; i++) { + std::cout << std::setw(4) << leftCondition.remainingHealths[i] << " "; + } std::cout << std::endl; + + std::cout << "Right:" << std::endl; + std::cout << " Damage: " << std::setw(4) << rightCondition.turnData.baseDamage << std::endl; + std::cout << " Health: "; + for (int i = 0; i < rightCondition.armySize; i++) { + std::cout << std::setw(4) << rightCondition.remainingHealths[i] << " "; + } std::cout << std::endl; + } + } + + // how 100 turn limit is handled for WB + if (turncounter >= 100 && rightCondition.worldboss == true) { + leftCondition.monstersLost = leftCondition.armySize; + } + + // write all the results into a FightResult + left.lastFightData.dominated = false; + left.lastFightData.turncounter = turncounter; + + if (leftCondition.monstersLost >= leftCondition.armySize) { //draws count as right wins. + left.lastFightData.monstersLost = rightCondition.monstersLost; + left.lastFightData.berserk = rightCondition.berserkProcs; + if (rightCondition.monstersLost < rightCondition.armySize) { + left.lastFightData.frontHealth = (int64_t) (rightCondition.remainingHealths[rightCondition.monstersLost]); + } else { + left.lastFightData.frontHealth = 0; + } + return false; + } else { + left.lastFightData.monstersLost = leftCondition.monstersLost; + left.lastFightData.frontHealth = (int64_t) (leftCondition.remainingHealths[leftCondition.monstersLost]); + left.lastFightData.berserk = leftCondition.berserkProcs; + return true; + } +} + +// Function determining if a monster is strictly better than another +bool isBetter(Monster * a, Monster * b, bool considerAbilities = false); + +#endif diff --git a/cosmosData.cpp b/cosmosData.cpp index f323964..1618fc8 100644 --- a/cosmosData.cpp +++ b/cosmosData.cpp @@ -1,7 +1,8 @@ #include "cosmosData.h" +#include "inputProcessing.h" // Private constructor that is called by all public ones. Fully initializes all attributes -Monster::Monster(int someHp, int someDamage, FollowerCount aCost, std::string aName, Element anElement, HeroRarity aRarity, HeroSkill aSkill, int aLevel) : +Monster::Monster(int someHp, int someDamage, FollowerCount aCost, std::string aName, Element anElement, HeroRarity aRarity, HeroSkill aSkill, int aLevel) : hp(someHp), damage(someDamage), cost(aCost), @@ -17,14 +18,16 @@ Monster::Monster(int someHp, int someDamage, FollowerCount aCost, std::string aN if (this->rarity != WORLDBOSS) { int points = this->rarity * (this->level-1); int value = this->hp + this->damage; - this->hp = this->hp + (int) round((float) points * (float) this->hp / (float) value); - this->damage = this->damage + (int) round((float) points * (float) this->damage / (float) value); + + int mult = (aSkill.skillType == GROW) ? aSkill.amount : 1; + this->hp = this->hp + (int) round((double) points * mult * (double) this->hp / (double) value); + this->damage = this->damage + (int) round((double) points * mult * (double) this->damage / (double) value); } } } -// Contructor for normal Monsters -Monster::Monster(int someHp, int someDamage, FollowerCount aCost, std::string aName, Element anElement) : +// Constructor for normal Monsters +Monster::Monster(int someHp, int someDamage, FollowerCount aCost, std::string aName, Element anElement) : Monster(someHp, someDamage, aCost, aName, anElement, NO_HERO, NO_SKILL, 0) {} // Constructor for Heroes @@ -33,50 +36,63 @@ Monster::Monster(int someHp, int someDamage, std::string aName, Element anElemen // Constructor for leveled heroes Monster::Monster(const Monster & baseHero, int aLevel) : - Monster(baseHero.hp, baseHero.damage, baseHero.cost, baseHero.baseName, baseHero.element, baseHero.rarity, baseHero.skill, aLevel) + Monster(baseHero.hp, baseHero.damage, baseHero.cost, baseHero.baseName, baseHero.element, baseHero.rarity, baseHero.skill, aLevel) { + this->index = baseHero.index; + if (baseHero.skill.skillType == BUFF_L) { this->skill.skillType = BUFF; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == PROTECT_L) { this->skill.skillType = PROTECT; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == CHAMPION_L) { this->skill.skillType = CHAMPION; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == HEAL_L) { this->skill.skillType = HEAL; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == AOE_L) { this->skill.skillType = AOE; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == LIFESTEAL_L) { this->skill.skillType = LIFESTEAL; - this->skill.amount = (float) floor((float) aLevel * baseHero.skill.amount); + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } else if (baseHero.skill.skillType == DAMPEN_L) { this->skill.skillType = DAMPEN; - this->skill.amount = 1.0f - (float) aLevel * baseHero.skill.amount; + this->skill.amount = 1.0f - (double) aLevel * baseHero.skill.amount; + } else if (baseHero.skill.skillType == AOEZERO_L) { + this->skill.skillType = AOEZERO; + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); + } else if (baseHero.skill.skillType == EXPLODE_L) { + this->skill.skillType = EXPLODE; + this->skill.amount = (double) floor((double) aLevel * baseHero.skill.amount); } } -HeroSkill::HeroSkill(SkillType aType, Element aTarget, Element aSource, float anAmount) : - skillType(aType), - target(aTarget), - sourceElement(aSource), - amount(anAmount) +HeroSkill::HeroSkill(SkillType aType, Element aTarget, Element aSource, double anAmount) : + skillType(aType), + target(aTarget), + sourceElement(aSource), + amount(anAmount) { this->hasAsymmetricAoe = (aType == VALKYRIE || aType == TRAMPLE); - this->hasHeal = (aType == HEAL || aType == HEAL_L || + this->hasHeal = (aType == HEAL || aType == HEAL_L || aType == LIFESTEAL || aType == LIFESTEAL_L); - this->hasAoe = (aType == AOE || aType == AOE_L || - aType == REVENGE || aType == PIERCE || + // hasAoe should include all things affected by dampen + this->hasAoe = (aType == AOE || aType == AOE_L || + aType == REVENGE || + aType == EXPLODE || this->hasHeal || this->hasAsymmetricAoe); - this->violatesFightResults = (aType == BUFF || aType == BUFF_L || + // For expanding armies, if new hero added to the back might have changed the fight, old result is not valid + this->violatesFightResults = (aType == BUFF || aType == BUFF_L || aType == PROTECT || aType == PROTECT_L || aType == CHAMPION || aType == CHAMPION_L || - aType == AOE || aType == AOE_L || + aType == AOE || aType == AOE_L || aType == HEAL || aType == HEAL_L || - aType == LIFESTEAL || aType == LIFESTEAL_L); + aType == LIFESTEAL || aType == LIFESTEAL_L || + aType == BEER || aType == AOEZERO_L || + aType == AOEZERO || aType == ABSORB); } // JSON Functions to provide results in an easily readable output format. Used my Latas for example @@ -92,24 +108,24 @@ std::string Monster::toJSON() { return s.str(); } -std::string Army::toString() { +std::string Army::toString(int tier) { std::stringstream s; s << "["; int index = isQuest(*this); - + if (index != -1) { - s << "quest" << index << " | "; + s << "quest" << index << "-" << tier << " | "; } - s << "Followers: " << std::setw(7) << this->followerCost << " | "; + s << "Followers: " << std::setw(7) << numberWithSeparators(this->followerCost) << " | "; for (int i = this->monsterAmount-1; i >= 0; i--) { s << monsterReference[this->monsters[i]].name << " "; // Print in reversed Order - } s << "<==]"; + } s << "<==]"; return s.str(); } std::string Army::toJSON() { if (this->isEmpty()) {return "null";} - + std::stringstream s; s << "{"; s << "\"followers\"" << ":" << this->followerCost << ","; @@ -129,30 +145,35 @@ std::string Army::toJSON() { void Instance::setTarget(Army aTarget) { this->target = aTarget; this->targetSize = aTarget.monsterAmount; - this->lowestBossHealth = -1; - + this->lowestBossHealth = 0; + HeroSkill currentSkill; this->hasAoe = false; this->hasHeal = false; this->hasAsymmetricAoe = false; + this->hasBeer = false; + this->hasGambler = false; this->hasWorldBoss = false; for (size_t i = 0; i < this->targetSize; i++) { currentSkill = monsterReference[this->target.monsters[i]].skill; this->hasAoe |= currentSkill.hasAoe; this->hasHeal |= currentSkill.hasHeal; this->hasAsymmetricAoe |= currentSkill.hasAsymmetricAoe; + this->hasBeer |= currentSkill.skillType == BEER; + this->hasGambler |= currentSkill.skillType == DICE || currentSkill.skillType == LUX || currentSkill.skillType == CRIT; this->hasWorldBoss |= monsterReference[this->target.monsters[i]].rarity == WORLDBOSS; } - - // Check which monsters can survive a hit from the final monster on the target. This helps reduce the amoutn of potential soluitons in the last expand - // Heroes with global Abilities also get accepted. + + // Check which monsters can survive a hit from the final monster on the target. This helps reduce the amount of potential solutions in the last expand + // Heroes with global Abilities also get accepted. // This produces only false positives not false negatives -> no correct solutions lost Monster lastMonster = monsterReference[this->target.monsters[this->targetSize - 1]]; for (size_t i = 0; i < monsterReference.size(); i++) { Monster currentMonster = monsterReference[i]; int attack = lastMonster.damage; if (counter[currentMonster.element] == lastMonster.element) { - attack = castCeil((float) attack * elementalBoost); + // attack = castCeil((double) attack * elementalBoost); + attack = round((double) attack * elementalBoost); } this->monsterUsefulLast.push_back(currentMonster.hp > attack || currentMonster.skill.violatesFightResults); } @@ -178,163 +199,223 @@ int isQuest(Army & army) { return -1; } -// Access tools for monsters +// Access tools for monsters std::map monsterMap; // Maps monster Names to their indices in monsterReference std::vector monsterReference; // Global lookup for monster stats indices of monsters here can be used instead of the objects std::vector availableMonsters; // Contains indices of all monsters the user allows. Is affected by filters -std::vector availableHeroes; // Contains all user heroes' indices +std::vector availableHeroes; // Contains all user heroes' indices // Storage for Game Data std::vector monsterBaseList; // Raw Monster Data, holds the actual Objects -std::vector baseHeroes; // Raw, unleveld Hero Data, holds actual Objects +std::vector baseHeroes; // Raw, unleveled Hero Data, holds actual Objects +std::map heroAliases; //Alternate or shorthand names for heroes std::vector> quests; // Quest Lineup from the game -// Fill MonsterBaseList With Monsters Order is important for ReplayStrings +// Converts string to int, int still needs to be cast to enum +std::map stringToEnum = { + {"NOTHING", NOTHING}, + {"BUFF", BUFF}, + {"PROTECT", PROTECT}, + {"CHAMPION", CHAMPION}, + {"AOE", AOE}, + {"HEAL", HEAL}, + {"LIFESTEAL", LIFESTEAL}, + {"DAMPEN", DAMPEN}, + {"AOEZERO", AOEZERO}, + {"AOEZERO_L", AOEZERO_L}, + {"BERSERK", BERSERK}, + {"FRIENDS", FRIENDS}, + {"ADAPT", ADAPT}, + {"RAINBOW", RAINBOW}, + {"TRAINING", TRAINING}, + {"WITHER", WITHER}, + {"REVENGE", REVENGE}, + {"VALKYRIE", VALKYRIE}, + {"TRAMPLE", TRAMPLE}, + {"BUFF_L", BUFF_L}, + {"PROTECT_L", PROTECT_L}, + {"CHAMPION_L", CHAMPION_L}, + {"AOE_L", AOE_L}, + {"HEAL_L", HEAL_L}, + {"LIFESTEAL_L", LIFESTEAL_L}, + {"DAMPEN_L", DAMPEN_L}, + {"BEER", BEER}, + {"GROW", GROW}, + {"COUNTER", COUNTER}, + {"DICE", DICE}, + {"LUX", LUX}, + {"CRIT", CRIT}, + {"EXPLODE", EXPLODE}, + {"ABSORB", ABSORB}, + {"HATE", HATE}, + {"EXPLODE_L", EXPLODE_L}, + {"DODGE", DODGE}, + {"DEATHSTRIKE", DEATHSTRIKE}, + {"LEECH", LEECH}, + {"EVOLVE", EVOLVE}, + {"COUNTER_MAX_HP", COUNTER_MAX_HP}, + + {"EARTH", EARTH}, + {"AIR", AIR}, + {"WATER", WATER}, + {"FIRE", FIRE}, + {"ALL", ALL}, + {"SELF", SELF}, + + {"NO_HERO", NO_HERO}, + {"COMMON", COMMON}, + {"RARE", RARE}, + {"LEGENDARY", LEGENDARY}, + {"ASCENDED", ASCENDED}, + {"WORLDBOSS", WORLDBOSS} +}; + +// Fill MonsterBaseList With Monsters Order is important for ReplayStrings and gambler abilities void initMonsterData() { monsterBaseList.push_back(Monster( 20, 8, 1000, "a1", AIR)); monsterBaseList.push_back(Monster( 44, 4, 1300, "e1", EARTH)); monsterBaseList.push_back(Monster( 16, 10, 1000, "f1", FIRE)); monsterBaseList.push_back(Monster( 30, 6, 1400, "w1", WATER)); - + monsterBaseList.push_back(Monster( 48, 6, 3900, "a2", AIR)); monsterBaseList.push_back(Monster( 30, 8, 2700, "e2", EARTH)); monsterBaseList.push_back(Monster( 18, 16, 3900, "f2", FIRE)); monsterBaseList.push_back(Monster( 24, 12, 3900, "w2", WATER)); - + monsterBaseList.push_back(Monster( 36, 12, 8000, "a3", AIR)); monsterBaseList.push_back(Monster( 26, 16, 7500, "e3", EARTH)); monsterBaseList.push_back(Monster( 54, 8, 8000, "f3", FIRE)); monsterBaseList.push_back(Monster( 18, 24, 8000, "w3", WATER)); - + monsterBaseList.push_back(Monster( 24, 26, 15000, "a4", AIR)); monsterBaseList.push_back(Monster( 72, 10, 18000, "e4", EARTH)); monsterBaseList.push_back(Monster( 52, 16, 23000, "f4", FIRE)); monsterBaseList.push_back(Monster( 36, 20, 18000, "w4", WATER)); - + monsterBaseList.push_back(Monster( 60, 20, 41000, "a5", AIR)); monsterBaseList.push_back(Monster( 36, 40, 54000, "e5", EARTH)); monsterBaseList.push_back(Monster( 42, 24, 31000, "f5", FIRE)); monsterBaseList.push_back(Monster( 78, 18, 52000, "w5", WATER)); - + monsterBaseList.push_back(Monster( 62, 34, 96000, "a6", AIR)); monsterBaseList.push_back(Monster( 72, 24, 71000, "e6", EARTH)); monsterBaseList.push_back(Monster(104, 20, 94000, "f6", FIRE)); monsterBaseList.push_back(Monster( 44, 44, 84000, "w6", WATER)); - + monsterBaseList.push_back(Monster(106, 26, 144000, "a7", AIR)); monsterBaseList.push_back(Monster( 66, 36, 115000, "e7", EARTH)); monsterBaseList.push_back(Monster( 54, 44, 115000, "f7", FIRE)); monsterBaseList.push_back(Monster( 92, 32, 159000, "w7", WATER)); - + monsterBaseList.push_back(Monster( 78, 52, 257000, "a8", AIR)); monsterBaseList.push_back(Monster( 60, 60, 215000, "e8", EARTH)); monsterBaseList.push_back(Monster( 94, 50, 321000, "f8", FIRE)); monsterBaseList.push_back(Monster(108, 36, 241000, "w8", WATER)); - + monsterBaseList.push_back(Monster(116, 54, 495000, "a9", AIR)); monsterBaseList.push_back(Monster(120, 48, 436000, "e9", EARTH)); monsterBaseList.push_back(Monster(102, 58, 454000, "f9", FIRE)); monsterBaseList.push_back(Monster( 80, 70, 418000, "w9", WATER)); - + monsterBaseList.push_back(Monster(142, 60, 785000, "a10", AIR)); monsterBaseList.push_back(Monster(122, 64, 689000, "e10", EARTH)); monsterBaseList.push_back(Monster(104, 82, 787000, "f10", FIRE)); monsterBaseList.push_back(Monster(110, 70, 675000, "w10", WATER)); - + monsterBaseList.push_back(Monster(114, 110, 1403000, "a11", AIR)); monsterBaseList.push_back(Monster(134, 81, 1130000, "e11", EARTH)); monsterBaseList.push_back(Monster(164, 70, 1229000, "f11", FIRE)); monsterBaseList.push_back(Monster(152, 79, 1315000, "w11", WATER)); - + monsterBaseList.push_back(Monster(164, 88, 1733000, "a12", AIR)); monsterBaseList.push_back(Monster(128, 120, 1903000, "e12", EARTH)); monsterBaseList.push_back(Monster(156, 92, 1718000, "f12", FIRE)); monsterBaseList.push_back(Monster(188, 78, 1775000, "w12", WATER)); - + monsterBaseList.push_back(Monster(210, 94, 2772000, "a13", AIR)); monsterBaseList.push_back(Monster(190, 132, 3971000, "e13", EARTH)); monsterBaseList.push_back(Monster(166, 130, 3169000, "f13", FIRE)); monsterBaseList.push_back(Monster(140, 128, 2398000, "w13", WATER)); - + monsterBaseList.push_back(Monster(200, 142, 4785000, "a14", AIR)); monsterBaseList.push_back(Monster(244, 136, 6044000, "e14", EARTH)); monsterBaseList.push_back(Monster(168, 168, 4741000, "f14", FIRE)); monsterBaseList.push_back(Monster(212, 122, 4159000, "w14", WATER)); - + monsterBaseList.push_back(Monster(226, 190, 8897000, "a15", AIR)); monsterBaseList.push_back(Monster(200, 186, 7173000, "e15", EARTH)); monsterBaseList.push_back(Monster(234, 136, 5676000, "f15", FIRE)); monsterBaseList.push_back(Monster(276, 142, 7758000, "w15", WATER)); - + monsterBaseList.push_back(Monster(280, 196, 12855000, "a16", AIR)); monsterBaseList.push_back(Monster(284, 190, 12534000, "e16", EARTH)); monsterBaseList.push_back(Monster(288, 192, 13001000, "f16", FIRE)); - monsterBaseList.push_back(Monster(286, 198, 13475000, "w16", WATER)); - + monsterBaseList.push_back(Monster(286, 198, 13475000, "w16", WATER)); + monsterBaseList.push_back(Monster(318, 206, 16765000, "a17", AIR)); monsterBaseList.push_back(Monster(338, 192, 16531000, "e17", EARTH)); monsterBaseList.push_back(Monster(236, 292, 18090000, "f17", FIRE)); monsterBaseList.push_back(Monster(262, 258, 17573000, "w17", WATER)); - + monsterBaseList.push_back(Monster(280, 280, 21951000, "a18", AIR)); monsterBaseList.push_back(Monster(330, 242, 22567000, "e18", EARTH)); monsterBaseList.push_back(Monster(392, 200, 21951000, "f18", FIRE)); monsterBaseList.push_back(Monster(330, 230, 20909000, "w18", WATER)); - + monsterBaseList.push_back(Monster(440, 206, 27288000, "a19", AIR)); monsterBaseList.push_back(Monster(320, 282, 27107000, "e19", EARTH)); monsterBaseList.push_back(Monster(352, 244, 25170000, "f19", FIRE)); monsterBaseList.push_back(Monster(360, 238, 25079000, "w19", WATER)); - + monsterBaseList.push_back(Monster(378, 268, 32242000, "a20", AIR)); monsterBaseList.push_back(Monster(382, 264, 32025000, "e20", EARTH)); - monsterBaseList.push_back(Monster(388, 266, 33155000, "f20", FIRE)); + monsterBaseList.push_back(Monster(388, 266, 33155600, "f20", FIRE)); monsterBaseList.push_back(Monster(454, 232, 34182000, "w20", WATER)); - + monsterBaseList.push_back(Monster(428, 286, 42826000, "a21", AIR)); monsterBaseList.push_back(Monster(446, 272, 42252000, "e21", EARTH)); monsterBaseList.push_back(Monster(362, 338, 42798000, "f21", FIRE)); monsterBaseList.push_back(Monster(416, 290, 41901000, "w21", WATER)); - + monsterBaseList.push_back(Monster(454, 320, 55373000, "a22", AIR)); monsterBaseList.push_back(Monster(450, 324, 55671000, "e22", EARTH)); monsterBaseList.push_back(Monster(458, 318, 55582000, "f22", FIRE)); monsterBaseList.push_back(Monster(440, 340, 55877000, "w22", WATER)); - + monsterBaseList.push_back(Monster(500, 348, 72580000, "a23", AIR)); monsterBaseList.push_back(Monster(516, 340, 73483000, "e23", EARTH)); monsterBaseList.push_back(Monster(424, 410, 72480000, "f23", FIRE)); monsterBaseList.push_back(Monster(490, 354, 72243000, "w23", WATER)); - + monsterBaseList.push_back(Monster(554, 374, 94312000, "a24", AIR)); monsterBaseList.push_back(Monster(458, 458, 96071000, "e24", EARTH)); monsterBaseList.push_back(Monster(534, 392, 95772000, "f24", FIRE)); monsterBaseList.push_back(Monster(540, 388, 95903000, "w24", WATER)); - monsterBaseList.push_back(Monster(580, 430, 124550000, "a25", AIR)); - monsterBaseList.push_back(Monster(592, 418, 123100000, "e25", EARTH)); - monsterBaseList.push_back(Monster(764, 328, 125440000, "f25", FIRE)); - monsterBaseList.push_back(Monster(500, 506, 127260000, "w25", WATER)); + monsterBaseList.push_back(Monster(580, 430, 124549000, "a25", AIR)); + monsterBaseList.push_back(Monster(592, 418, 123096000, "e25", EARTH)); + monsterBaseList.push_back(Monster(764, 328, 125443000, "f25", FIRE)); + monsterBaseList.push_back(Monster(500, 506, 127256000, "w25", WATER)); - monsterBaseList.push_back(Monster(496, 582, 155100000, "a26", AIR)); - monsterBaseList.push_back(Monster(622, 468, 157060000, "e26", EARTH)); - monsterBaseList.push_back(Monster(638, 462, 160030000, "f26", FIRE)); + monsterBaseList.push_back(Monster(496, 582, 155097000, "a26", AIR)); + monsterBaseList.push_back(Monster(622, 468, 157055000, "e26", EARTH)); + monsterBaseList.push_back(Monster(638, 462, 160026000, "f26", FIRE)); monsterBaseList.push_back(Monster(700, 416, 157140000, "w26", WATER)); - monsterBaseList.push_back(Monster(712, 484, 202290000, "a27", AIR)); - monsterBaseList.push_back(Monster(580, 602, 206320000, "e27", EARTH)); - monsterBaseList.push_back(Monster(690, 498, 201430000, "f27", FIRE)); - monsterBaseList.push_back(Monster(682, 500, 199340000, "w27", WATER)); + monsterBaseList.push_back(Monster(712, 484, 202295000, "a27", AIR)); + monsterBaseList.push_back(Monster(580, 602, 206317000, "e27", EARTH)); + monsterBaseList.push_back(Monster(690, 498, 201426000, "f27", FIRE)); + monsterBaseList.push_back(Monster(682, 500, 199344000, "w27", WATER)); - monsterBaseList.push_back(Monster(644, 642, 265850000, "a28", AIR)); - monsterBaseList.push_back(Monster(770, 540, 268120000, "e28", EARTH)); + monsterBaseList.push_back(Monster(644, 642, 265846000, "a28", AIR)); + monsterBaseList.push_back(Monster(770, 540, 268117000, "e28", EARTH)); monsterBaseList.push_back(Monster(746, 552, 264250000, "f28", FIRE)); - monsterBaseList.push_back(Monster(762, 536, 261020000, "w28", WATER)); + monsterBaseList.push_back(Monster(762, 536, 261023000, "w28", WATER)); monsterBaseList.push_back(Monster(834, 616, 368230000, "a29", AIR)); - monsterBaseList.push_back(Monster(830, 614, 363810000, "e29", EARTH)); - monsterBaseList.push_back(Monster(746, 676, 358120000, "f29", FIRE)); - monsterBaseList.push_back(Monster(1008,512, 370760000, "w29", WATER)); + monsterBaseList.push_back(Monster(830, 614, 363805000, "e29", EARTH)); + monsterBaseList.push_back(Monster(746, 676, 358119000, "f29", FIRE)); + monsterBaseList.push_back(Monster(1008,512, 370761000, "w29", WATER)); monsterBaseList.push_back(Monster(700, 906, 505055000, "a30", AIR)); monsterBaseList.push_back(Monster(1022,614, 497082000, "e30", EARTH)); @@ -342,11 +423,11 @@ void initMonsterData() { monsterBaseList.push_back(Monster(802, 802, 515849000, "w30", WATER)); } -// Fill BaseHeroes with Heroes. Order is important +// Fill BaseHeroes with Heroes. Order is important void initBaseHeroes() { - baseHeroes.push_back(Monster( 45, 20, "ladyoftwilight", AIR, COMMON, {PROTECT, ALL, AIR, 1})); - baseHeroes.push_back(Monster( 70, 30, "tiny", EARTH, RARE, {AOE, ALL, EARTH, 2})); - baseHeroes.push_back(Monster( 90, 40, "nebra", FIRE, LEGENDARY, {BUFF, ALL, FIRE, 8})); + baseHeroes.push_back(Monster( 45, 20, "ladyoftwilight", AIR, COMMON, {CHAMPION, ALL, AIR, 3})); + baseHeroes.push_back(Monster( 70, 30, "tiny", EARTH, RARE, {LIFESTEAL_L, ALL, EARTH, 0.04167f})); + baseHeroes.push_back(Monster(110, 40, "nebra", FIRE, LEGENDARY, {BUFF, ALL, FIRE, 20})); baseHeroes.push_back(Monster( 20, 10, "valor", AIR, COMMON, {PROTECT, AIR, AIR, 1})); baseHeroes.push_back(Monster( 30, 8, "rokka", EARTH, COMMON, {PROTECT, EARTH, EARTH, 1})); @@ -371,12 +452,12 @@ void initBaseHeroes() { baseHeroes.push_back(Monster( 22, 32, "nicte", AIR, RARE, {BUFF, AIR, AIR, 4})); - baseHeroes.push_back(Monster( 50, 12, "james", EARTH, LEGENDARY, {PIERCE, ALL, EARTH, 1})); + baseHeroes.push_back(Monster( 50, 12, "james", EARTH, LEGENDARY, {VALKYRIE, ALL, EARTH, 0.75f})); baseHeroes.push_back(Monster( 28, 16, "k41ry", AIR, COMMON, {BUFF, AIR, AIR, 3})); baseHeroes.push_back(Monster( 46, 20, "t4urus", EARTH, RARE, {BUFF, ALL, EARTH, 1})); baseHeroes.push_back(Monster(100, 20, "tr0n1x", FIRE, LEGENDARY, {AOE, ALL, FIRE, 3})); - + baseHeroes.push_back(Monster( 58, 8, "aquortis", WATER, COMMON, {BUFF, WATER, WATER, 3})); baseHeroes.push_back(Monster( 30, 32, "aeris", AIR, RARE, {HEAL, ALL, AIR, 1})); baseHeroes.push_back(Monster( 75, 2, "geum", EARTH, LEGENDARY, {BERSERK, SELF, EARTH, 2})); @@ -401,9 +482,9 @@ void initBaseHeroes() { baseHeroes.push_back(Monster( 26, 44, "petry", EARTH, RARE, {PROTECT, EARTH, EARTH, 4})); baseHeroes.push_back(Monster( 58, 22, "zaytus", FIRE, RARE, {PROTECT, FIRE, FIRE, 4})); - baseHeroes.push_back(Monster( 75, 45, "spyke", AIR, LEGENDARY, {TRAINING, SELF, AIR, 5})); - baseHeroes.push_back(Monster( 70, 55, "aoyuki", WATER, LEGENDARY, {RAINBOW, SELF, WATER, 50})); - baseHeroes.push_back(Monster( 50,100, "gaiabyte", EARTH, LEGENDARY, {WITHER, SELF, EARTH, 0.5f})); + baseHeroes.push_back(Monster( 75, 45, "spyke", AIR, LEGENDARY, {TRAINING, SELF, AIR, 10})); + baseHeroes.push_back(Monster( 70, 55, "aoyuki", WATER, LEGENDARY, {RAINBOW, SELF, WATER, 100})); + baseHeroes.push_back(Monster( 75,150, "gaiabyte", EARTH, LEGENDARY, {WITHER, SELF, EARTH, 0.5f})); baseHeroes.push_back(Monster( 36, 14, "oymos", AIR, COMMON, {BUFF, AIR, AIR, 4})); baseHeroes.push_back(Monster( 32, 32, "xarth", EARTH, RARE, {CHAMPION, EARTH, EARTH, 2})); @@ -413,25 +494,25 @@ void initBaseHeroes() { baseHeroes.push_back(Monster( 76, 46, "koth", EARTH, LEGENDARY, {REVENGE, ALL, EARTH, 0.15f})); baseHeroes.push_back(Monster( 82, 50, "gurth", AIR, LEGENDARY, {REVENGE, ALL, AIR, 0.2f})); - baseHeroes.push_back(Monster( 35, 25, "werewolf", EARTH, COMMON, {PROTECT_L, ALL, EARTH, 0.112f})); - baseHeroes.push_back(Monster( 55, 35, "jackoknight", AIR, RARE, {BUFF_L, ALL, AIR, 0.112f})); - baseHeroes.push_back(Monster( 75, 45, "dullahan", FIRE, LEGENDARY, {CHAMPION_L, ALL, FIRE, 0.112f})); + baseHeroes.push_back(Monster( 35, 25, "werewolf", EARTH, COMMON, {PROTECT_L, ALL, EARTH, 0.1112f})); + baseHeroes.push_back(Monster( 55, 35, "jackoknight", AIR, RARE, {BUFF_L, ALL, AIR, 0.1112f})); + baseHeroes.push_back(Monster( 75, 45, "dullahan", FIRE, LEGENDARY, {CHAMPION_L, ALL, FIRE, 0.1112f})); baseHeroes.push_back(Monster( 36, 36, "ladyodelith", WATER, RARE, {PROTECT, WATER, WATER, 4})); - baseHeroes.push_back(Monster( 34, 54, "shygu", AIR, LEGENDARY, {PROTECT_L, AIR, AIR, 0.112f})); - baseHeroes.push_back(Monster( 72, 28, "thert", EARTH, LEGENDARY, {PROTECT_L, EARTH, EARTH, 0.112f})); - baseHeroes.push_back(Monster( 32, 64, "lordkirk", FIRE, LEGENDARY, {PROTECT_L, FIRE, FIRE, 0.112f})); - baseHeroes.push_back(Monster( 30, 70, "neptunius", WATER, LEGENDARY, {PROTECT_L, WATER, WATER, 0.112f})); - + baseHeroes.push_back(Monster( 34, 54, "shygu", AIR, LEGENDARY, {PROTECT_L, AIR, AIR, 0.1112f})); + baseHeroes.push_back(Monster( 72, 28, "thert", EARTH, LEGENDARY, {PROTECT_L, EARTH, EARTH, 0.1112f})); + baseHeroes.push_back(Monster( 32, 64, "lordkirk", FIRE, LEGENDARY, {PROTECT_L, FIRE, FIRE, 0.1112f})); + baseHeroes.push_back(Monster( 30, 70, "neptunius", WATER, LEGENDARY, {PROTECT_L, WATER, WATER, 0.1112f})); + baseHeroes.push_back(Monster( 65, 12, "sigrun", FIRE, LEGENDARY, {VALKYRIE, ALL, FIRE, 0.5f})); baseHeroes.push_back(Monster( 70, 14, "koldis", WATER, LEGENDARY, {VALKYRIE, ALL, WATER, 0.5f})); baseHeroes.push_back(Monster( 75, 16, "alvitr", EARTH, LEGENDARY, {VALKYRIE, ALL, EARTH, 0.5f})); - + baseHeroes.push_back(Monster( 30, 18, "hama", WATER, COMMON, {BUFF, WATER, WATER, 4})); baseHeroes.push_back(Monster( 34, 34, "hallinskidi", AIR, RARE, {CHAMPION, AIR, AIR, 2})); baseHeroes.push_back(Monster( 60, 42, "rigr", EARTH, LEGENDARY, {ADAPT, EARTH, EARTH, 2})); - + baseHeroes.push_back(Monster(174, 46, "aalpha", FIRE, ASCENDED, {AOE_L, ALL, FIRE, 0.304f})); baseHeroes.push_back(Monster(162, 60, "aathos", EARTH, ASCENDED, {PROTECT_L, ALL, EARTH, 0.304f})); baseHeroes.push_back(Monster(120,104, "arei", AIR, ASCENDED, {BUFF_L, ALL, AIR, 0.304f})); @@ -439,35 +520,156 @@ void initBaseHeroes() { baseHeroes.push_back(Monster(190, 38, "atr0n1x", FIRE, ASCENDED, {VALKYRIE, ALL, FIRE, 0.75f})); baseHeroes.push_back(Monster(222, 8, "ageum", EARTH, ASCENDED, {BERSERK, SELF, EARTH, 2})); baseHeroes.push_back(Monster(116,116, "ageror", AIR, ASCENDED, {FRIENDS, SELF, AIR, 1.3f})); - + baseHeroes.push_back(Monster(WORLDBOSS_HEALTH, 73, "lordofchaos", FIRE, WORLDBOSS, {AOE, ALL, FIRE, 20})); - - baseHeroes.push_back(Monster( 38, 24, "christmaself", WATER, COMMON, {HEAL_L, ALL, WATER, 0.112f})); - baseHeroes.push_back(Monster( 54, 36, "reindeer", AIR, RARE, {AOE_L, ALL, AIR, 0.112f})); - baseHeroes.push_back(Monster( 72, 48, "santaclaus", FIRE, LEGENDARY, {LIFESTEAL_L, ALL, FIRE, 0.112f})); - baseHeroes.push_back(Monster( 44, 44, "sexysanta", EARTH, RARE, {VALKYRIE, ALL, EARTH, 0.66f})); - + + baseHeroes.push_back(Monster( 38, 24, "christmaself", WATER, COMMON, {HEAL_L, ALL, WATER, 0.1112f})); + baseHeroes.push_back(Monster( 54, 36, "reindeer", AIR, RARE, {AOE_L, ALL, AIR, 0.1112f})); + baseHeroes.push_back(Monster( 72, 48, "santaclaus", FIRE, LEGENDARY, {LIFESTEAL_L, ALL, FIRE, 0.1112f})); + baseHeroes.push_back(Monster( 44, 44, "sexysanta", EARTH, RARE, {VALKYRIE, ALL, EARTH, 0.66f})); + baseHeroes.push_back(Monster( 24, 24, "toth", FIRE, COMMON, {BUFF, FIRE, FIRE, 4})); baseHeroes.push_back(Monster( 40, 30, "ganah", WATER, RARE, {CHAMPION, WATER, WATER, 2})); baseHeroes.push_back(Monster( 58, 46, "dagda", AIR, LEGENDARY, {ADAPT, AIR, AIR, 2})); - - baseHeroes.push_back(Monster(200, 60, "bubbles", WATER, ASCENDED, {DAMPEN_L, ALL, WATER, 0.0025f})); - + + baseHeroes.push_back(Monster(300,110, "bubbles", WATER, ASCENDED, {DAMPEN_L, ALL, WATER, 0.0050f})); + baseHeroes.push_back(Monster(150, 86, "apontus", WATER, ASCENDED, {ADAPT, WATER, WATER, 3})); baseHeroes.push_back(Monster(162, 81, "aatzar", FIRE, ASCENDED, {ADAPT, FIRE, FIRE, 3})); - + baseHeroes.push_back(Monster( 74, 36, "arshen", AIR, LEGENDARY, {TRAMPLE, ALL, AIR, 2})); baseHeroes.push_back(Monster( 78, 40, "rua", FIRE, LEGENDARY, {TRAMPLE, ALL, FIRE, 2})); baseHeroes.push_back(Monster( 82, 44, "dorth", WATER, LEGENDARY, {TRAMPLE, ALL, WATER, 2})); - + baseHeroes.push_back(Monster(141, 99, "arigr", EARTH, ASCENDED, {ADAPT, EARTH, EARTH, 3})); - - baseHeroes.push_back(Monster(WORLDBOSS_HEALTH, 125, "moak", EARTH, WORLDBOSS, {DAMPEN, ALL, EARTH, .5})); - - baseHeroes.push_back(Monster( 42, 50, "hosokawa", AIR, LEGENDARY, {BUFF_L, AIR, AIR, 0.112f})); - baseHeroes.push_back(Monster( 32, 66, "takeda", EARTH, LEGENDARY, {BUFF_L, EARTH, EARTH, 0.112f})); - baseHeroes.push_back(Monster( 38, 56, "hirate", FIRE, LEGENDARY, {BUFF_L, FIRE, FIRE, 0.112f})); - baseHeroes.push_back(Monster( 44, 48, "hattori", WATER, LEGENDARY, {BUFF_L, WATER, WATER, 0.112f})); + + baseHeroes.push_back(Monster(WORLDBOSS_HEALTH, 125, "motherofallkodamas", EARTH, WORLDBOSS, {DAMPEN, ALL, EARTH, 0.5})); + + baseHeroes.push_back(Monster( 42, 50, "hosokawa", AIR, LEGENDARY, {BUFF_L, AIR, AIR, 0.1112f})); + baseHeroes.push_back(Monster( 32, 66, "takeda", EARTH, LEGENDARY, {BUFF_L, EARTH, EARTH, 0.1112f})); + baseHeroes.push_back(Monster( 38, 56, "hirate", FIRE, LEGENDARY, {BUFF_L, FIRE, FIRE, 0.1112f})); + baseHeroes.push_back(Monster( 44, 48, "hattori", WATER, LEGENDARY, {BUFF_L, WATER, WATER, 0.1112f})); + + baseHeroes.push_back(Monster(135, 107,"adagda", AIR, ASCENDED, {ADAPT, AIR, AIR, 3})); + + baseHeroes.push_back(Monster( 30, 20, "bylar", EARTH, COMMON, {BUFF, EARTH, EARTH, 4})); + baseHeroes.push_back(Monster( 36, 36, "boor", FIRE, RARE, {TRAINING, SELF, FIRE, 3})); + baseHeroes.push_back(Monster( 52, 52, "bavah", WATER, LEGENDARY, {CHAMPION, ALL, WATER, 2})); + + baseHeroes.push_back(Monster( 75, 25, "leprechaun", EARTH, LEGENDARY, {BEER, ALL, EARTH, 0})); + + baseHeroes.push_back(Monster( 30, 30, "sparks", FIRE, COMMON, {GROW, ALL, FIRE, 2})); + baseHeroes.push_back(Monster( 48, 42, "leaf", EARTH, RARE, {GROW, ALL, EARTH, 2})); + baseHeroes.push_back(Monster( 70, 48, "flynn", AIR, LEGENDARY, {GROW, ALL, AIR, 2})); + + baseHeroes.push_back(Monster(122,122, "abavah", WATER, ASCENDED, {CHAMPION_L, ALL, ALL, 0.152f})); + + baseHeroes.push_back(Monster( 66, 60, "drhawking", AIR, LEGENDARY, {AOEZERO_L, ALL, AIR, 1})); + + baseHeroes.push_back(Monster(150, 90, "masterlee", AIR, ASCENDED, {COUNTER, AIR, AIR, 0.5f})); + + baseHeroes.push_back(Monster( 70, 38, "kumusan", FIRE, LEGENDARY, {COUNTER, FIRE, FIRE, 0.2f})); + baseHeroes.push_back(Monster( 78, 42, "liucheng", WATER, LEGENDARY, {COUNTER, WATER, WATER, 0.25f})); + baseHeroes.push_back(Monster( 86, 44, "hidoka", EARTH, LEGENDARY, {COUNTER, EARTH, EARTH, 0.3f})); + + baseHeroes.push_back(Monster(WORLDBOSS_HEALTH, 11, "kryton", AIR, WORLDBOSS, {TRAINING, SELF, AIR, 10})); + + baseHeroes.push_back(Monster( 25, 26, "dicemaster", WATER, COMMON, {DICE, SELF, SELF, 20})); + baseHeroes.push_back(Monster( 28, 60, "luxuriusmaximus", FIRE, RARE, {LUX, SELF, EARTH, 1})); + baseHeroes.push_back(Monster( 70, 70, "pokerface", EARTH, LEGENDARY, {CRIT, EARTH, EARTH, 3})); + + baseHeroes.push_back(Monster( 25, 25, "taint", AIR, COMMON, {VALKYRIE, ALL, AIR, 0.5f})); + baseHeroes.push_back(Monster( 48, 50, "putrid", EARTH, RARE, {TRAINING, SELF, EARTH, -3})); + baseHeroes.push_back(Monster( 52, 48, "defile", FIRE, LEGENDARY, {EXPLODE, ALL, FIRE, 50})); + + baseHeroes.push_back(Monster(150, 15, "neil", WATER, LEGENDARY, {ABSORB, SELF, WATER, 0.3})); + + baseHeroes.push_back(Monster( 78, 26, "mahatma", AIR, LEGENDARY, {HATE, WATER, AIR, 0.75})); + baseHeroes.push_back(Monster( 76, 30, "jade", EARTH, LEGENDARY, {HATE, AIR, EARTH, 0.75})); + baseHeroes.push_back(Monster( 72, 36, "edana", FIRE, LEGENDARY, {HATE, EARTH, FIRE, 0.75})); + baseHeroes.push_back(Monster( 80, 30, "dybbuk", WATER, LEGENDARY, {HATE, FIRE, WATER, 0.75})); + + baseHeroes.push_back(Monster( 85, 135, "ashygu", AIR, ASCENDED, {PROTECT_L, AIR, AIR, 0.1819f})); + baseHeroes.push_back(Monster( 180, 70, "athert", EARTH, ASCENDED, {PROTECT_L, EARTH, EARTH, 0.1819f})); + baseHeroes.push_back(Monster( 80, 160, "alordkirk", FIRE, ASCENDED, {PROTECT_L, FIRE, FIRE, 0.1819f})); + baseHeroes.push_back(Monster( 75, 175, "aneptunius", WATER, ASCENDED, {PROTECT_L, WATER, WATER, 0.1819f})); + + baseHeroes.push_back(Monster( 106,124, "ahosokawa", AIR, ASCENDED, {BUFF_L, AIR, AIR, 0.1819f})); + baseHeroes.push_back(Monster( 82, 164, "atakeda", EARTH, ASCENDED, {BUFF_L, EARTH, EARTH, 0.1819f})); + baseHeroes.push_back(Monster( 96, 144, "ahirate", FIRE, ASCENDED, {BUFF_L, FIRE, FIRE, 0.1819f})); + baseHeroes.push_back(Monster( 114,126, "ahattori", WATER, ASCENDED, {BUFF_L, WATER, WATER, 0.1819f})); + + baseHeroes.push_back(Monster(WORLDBOSS_HEALTH, 110, "doyenne", WATER, WORLDBOSS, {DODGE, ALL, ALL, 5000})); + + baseHeroes.push_back(Monster( 30, 40, "billy", EARTH, COMMON, {DEATHSTRIKE, ALL, EARTH, 100})); + baseHeroes.push_back(Monster( 88, 22, "sanqueen", WATER, RARE, {LEECH, SELF, WATER, 0.8})); + baseHeroes.push_back(Monster(150, 60, "cliodhna", AIR, LEGENDARY, {EVOLVE, SELF, AIR, 1})); + + baseHeroes.push_back(Monster( 340, 64, "guy", FIRE, ASCENDED, {COUNTER_MAX_HP, FIRE, FIRE, 1})); + baseHeroes.push_back(Monster( 126,114, "adefile", FIRE, ASCENDED, {EXPLODE, ALL, FIRE, 150})); +} + +void initIndices() { + for (auto i = monsterBaseList.begin(); i != monsterBaseList.end(); i++) { + i->index = getRealIndex(*i); + } + + for (auto i = baseHeroes.begin(); i != baseHeroes.end(); i++) { + i->index = getRealIndex(*i); + } +} + +void initHeroAliases() { + heroAliases["lady"] = "ladyoftwilight"; + heroAliases["lot"] = "ladyoftwilight"; + heroAliases["pyro"] = "pyromancer"; + heroAliases["kairy"] = "k41ry"; + heroAliases["taurus"] = "t4urus"; + heroAliases["tronix"] = "tr0n1x"; + heroAliases["druid"] = "forestdruid"; + heroAliases["veil"] = "veildur"; + heroAliases["bryn"] = "brynhildr"; + heroAliases["gaia"] = "gaiabyte"; + heroAliases["ww"] = "werewolf"; + heroAliases["wolf"] = "werewolf"; + heroAliases["jack"] = "jackoknight"; + heroAliases["jacko"] = "jackoknight"; + heroAliases["dull"] = "dullahan"; + heroAliases["dulla"] = "dullahan"; + heroAliases["odelith"] = "ladyodelith"; + heroAliases["kirk"] = "lordkirk"; + heroAliases["nep"] = "neptunius"; + heroAliases["tak"] = "takeda"; + heroAliases["hall"] = "hallinskidi"; + heroAliases["atronix"] = "atr0n1x"; + heroAliases["elf"] = "christmaself"; + heroAliases["deer"] = "reindeer"; + heroAliases["santa"] = "santaclaus"; + heroAliases["ss"] = "sexysanta"; + heroAliases["lep"] = "leprechaun"; + heroAliases["hawking"] = "drhawking"; + heroAliases["dice"] = "dicemaster"; + heroAliases["lux"] = "luxuriusmaximus"; + heroAliases["luxurious"] = "luxuriusmaximus"; + heroAliases["poker"] = "pokerface"; + heroAliases["akirk"] = "alordkirk"; + heroAliases["anep"] = "aneptunius"; + heroAliases["atak"] = "atakeda"; + heroAliases["san"] = "sanqueen"; + heroAliases["squeen"] = "sanqueen"; + heroAliases["clio"] = "cliodhna"; + heroAliases["cloddy"] = "cliodhna"; + heroAliases["fawkes"] = "guy"; + heroAliases["guyfawkes"] = "guy"; + + heroAliases["loc"] = "lordofchaos"; + heroAliases["fboss"] = "lordofchaos"; + heroAliases["moak"] = "motherofallkodamas"; + heroAliases["eboss"] = "motherofallkodamas"; + heroAliases["kry"] = "kryton"; + heroAliases["aboss"] = "kryton"; + heroAliases["doy"] = "doyenne"; + heroAliases["wboss"] = "doyenne"; } void initQuests() { @@ -567,20 +769,150 @@ void initQuests() { quests.push_back({"e18", "f16", "f16", "f16", "w16", "f18"}); quests.push_back({"a21", "a20", "f20", "a21"}); quests.push_back({"e18", "e17", "a18", "e17", "e17", "e20"}); //95 - quests.push_back({"a19", "a19", "w18", "w18", "f15", "e16"}); - quests.push_back({"w18", "f19", "f19", "e18", "e18", "a19"}); - quests.push_back({"f18", "w19", "w19", "e19", "e19", "f18"}); - quests.push_back({"f19", "a19", "e19", "f20", "a20", "f19"}); + quests.push_back({"a19", "a19", "w18", "w18", "f15", "e16"}); + quests.push_back({"w18", "f19", "f19", "e18", "e18", "a19"}); + quests.push_back({"f18", "w19", "w19", "e19", "e19", "f18"}); + quests.push_back({"f19", "a19", "e19", "f20", "a20", "f19"}); quests.push_back({"a20", "w18", "w18", "a19", "w20", "f20"}); // 100 + quests.push_back({"a22", "e21", "f20", "w20", "f22"}); + quests.push_back({"f23", "w21", "f20", "a20", "a21"}); + quests.push_back({"f22", "w21", "w21", "f21", "e21"}); + quests.push_back({"a20", "f20", "e21", "a21", "a20", "f20"}); + quests.push_back({"f20", "e21", "f20", "w20", "e21", "f20"}); // 105 + quests.push_back({"e21", "w22", "f23", "a23", "a22"}); + quests.push_back({"f21", "a20", "f21", "a21", "w21", "e21"}); + quests.push_back({"w22", "w22", "a22", "f22", "e21", "w21"}); + quests.push_back({"e22", "f22", "a22", "w21", "e22", "w21"}); + quests.push_back({"a22", "w22", "a22", "w21", "e22", "w22"}); // 110 + quests.push_back({"f23", "a22", "e23", "e23", "e22", "w22"}); + quests.push_back({"w22", "w23", "a23", "w22", "f21", "f21"}); + quests.push_back({"w24", "a24", "e24", "f24", "f23", "f23"}); + quests.push_back({"a24", "a25", "a24", "f25", "e23"}); + quests.push_back({"e23", "f23", "e23", "w25", "a24", "a23"}); // 115 + quests.push_back({"e24", "a24", "e24", "f23", "w24", "w23"}); + quests.push_back({"e24", "e24", "a24", "w24", "f24", "w24"}); + quests.push_back({"f24", "a23", "a24", "f24", "f24", "w24"}); + quests.push_back({"f25", "f25", "a26", "a26", "w25"}); + quests.push_back({"e27", "w27", "e27", "w27"}); // 120 + quests.push_back({"w27", "f27", "f27", "w27"}); + quests.push_back({"a27", "e27", "f27", "w27"}); + quests.push_back({"e27", "e27", "w27", "f27"}); + quests.push_back({"a28", "f27", "w27", "f27"}); + quests.push_back({"a28", "a27", "w27", "w28"}); // 125 + quests.push_back({"f26", "w28", "w28", "w28"}); + quests.push_back({"e26", "w27", "w27", "w28"}); + quests.push_back({"f27", "w28", "w29", "w29"}); + quests.push_back({"a27", "w27", "w29", "w29"}); + quests.push_back({"e27", "w28", "w29", "w29"}); // 130 + quests.push_back({"a26", "e26", "w29", "e29", "f27"}); + quests.push_back({"w29", "f29", "a27", "a26", "a28"}); + quests.push_back({"w29", "e29", "a29", "a28"}); + quests.push_back({"a27", "w29", "a27", "f28", "a27"}); + quests.push_back({"w29", "w30", "a26", "a27", "a30"}); // 135 + quests.push_back({"f26", "a30", "f27", "e30", "w28"}); + quests.push_back({"a30", "a27", "a30", "a27", "e30"}); + quests.push_back({"a27", "w30", "f28", "w30", "a27", "a27"}); + quests.push_back({"w27", "w29", "a30", "a27", "e30", "a27"}); + quests.push_back({"f27", "e30", "f28", "a30", "e27", "a27"}); // 140 + quests.push_back({"w29", "a30", "a30", "nicte:1000"}); + quests.push_back({"a30", "w29", "e29", "ladyodelith:1000"}); + quests.push_back({"e30", "w29", "w30", "f25", "ignitor:1000"}); + quests.push_back({"f27", "w28", "a30", "a27", "petry:1000"}); + quests.push_back({"e26", "e30", "w29", "e29", "ignitor:1000"}); // 145 + quests.push_back({"f26", "e30", "f25", "a30", "w29", "undine:1000"}); + quests.push_back({"w29", "a30", "undine:1000", "petry:1000"}); + quests.push_back({"a28", "w30", "ignitor:1000", "chroma:1000"}); + quests.push_back({"a28", "a28", "w28", "undine:1000", "zaytus:1000"}); + quests.push_back({"w28", "w28", "w29", "undine:1000", "ladyodelith:1000"}); // 150 + quests.push_back({"e30", "e30", "w30", "w30", "undine:1000", "chroma:1000"}); + quests.push_back({"f30", "f30", "e30", "a30", "ignitor:1000", "zaytus:1000"}); + quests.push_back({"e30", "w30", "chroma:1000", "nicte:1000", "forestdruid:1000"}); + quests.push_back({"w30", "a30", "undine:1000", "zaytus:1000", "ignitor:1000"}); + quests.push_back({"f30", "zaytus:1000", "chroma:1000", "nicte:1000", "forestdruid:1000"}); // 155 + quests.push_back({"f30", "f30", "undine:1000", "ladyodelith:1000", "forestdruid:1000", "petry:1000"}); + quests.push_back({"e30", "e30", "ignitor:1000", "zaytus:1000", "nicte:1000", "undine:1000"}); + quests.push_back({"w30", "ladyodelith:1000", "ignitor:1000", "zaytus:1000", "chroma:1000", "forestdruid:1000"}); + quests.push_back({"ignitor:1000", "forestdruid:1000", "petry:1000", "chroma:1000", "ladyodelith:1000", "undine:1000"}); + quests.push_back({"f30", "ignitor:1000", "undine:1000", "neptunius:1000"}); // 160 +} + +void readMonsterData(std::vector::iterator it, std::vector::iterator itEnd) { + while (it != itEnd) { + monsterBaseList.push_back(Monster(std::stoi(*(it)), std::stoi(*(it+1)), std::stoi(*(it+2)), *(it+3), (Element)stringToEnum[*(it+4)])); + it += 5; + } +} + +void readBaseHeroes(std::vector::iterator it, std::vector::iterator itEnd) { + while (it != itEnd) { + baseHeroes.push_back(Monster(std::stoi(*(it)), std::stoi(*(it+1)), *(it+2), + (Element)stringToEnum[*(it+3)], (HeroRarity)stringToEnum[*(it+4)], + {(SkillType)stringToEnum[*(it+5)], (Element)stringToEnum[*(it+6)], + (Element)stringToEnum[*(it+7)], std::stod(*(it+8))})); + it += 9; + } +} + +void readHeroAliases(std::vector::iterator it, std::vector::iterator itEnd) { + while (it != itEnd) { + heroAliases[*(it)] = *(it + 1); + it += 2; + } +} + +void readQuests(std::vector::iterator it, std::vector::iterator itEnd) { + quests.push_back({""}); // quest 0 empty + + while (it != itEnd) { + std::vector newQuest; + while (*it != "+") { + newQuest.push_back(*it); + it++; + } + quests.push_back(newQuest); + it++; + } } // Fills all references and storages with real data. // Must be called before any other operation on monsters or input void initGameData() { // Initialize Monster Data - initMonsterData(); - initBaseHeroes(); - initQuests(); + std::ifstream file; + file.open("cqdata.txt"); + // If data file is present use it, else use hardcoded data + // Reading done with minimal validity checking for now + if (file) { + std::string token; + std::vector tokens; + while (file >> token) { + tokens.push_back(token); + } + file.close(); + + std::vector::iterator it = std::find(tokens.begin(), tokens.end(), "*") + 2; + std::vector::iterator itEnd = std::find(it, tokens.end(), "*"); + readMonsterData(it, itEnd); + + it = itEnd + 2; + itEnd = std::find(it, tokens.end(), "*"); + readBaseHeroes(it, itEnd); + + it = itEnd + 2; + itEnd = std::find(it, tokens.end(), "*"); + readHeroAliases(it, itEnd); + + it = itEnd + 2; + itEnd = std::find(it, tokens.end(), "*"); + readQuests(it, itEnd); + } + else { + initMonsterData(); + initBaseHeroes(); + initHeroAliases(); + initQuests(); + } + initIndices(); for (size_t i = 0; i < monsterBaseList.size(); i++) { monsterReference.push_back(monsterBaseList[i]); @@ -593,7 +925,8 @@ void initGameData() { void filterMonsterData(FollowerCount minimumMonsterCost, FollowerCount maximumArmyCost) { std::vector tempMonsterList = monsterBaseList; // Get a temporary list to sort sort(tempMonsterList.begin(), tempMonsterList.end(), isCheaper); - + availableMonsters.clear(); + for (size_t i = 0; i < tempMonsterList.size(); i++) { if (minimumMonsterCost <= tempMonsterList[i].cost && maximumArmyCost >= tempMonsterList[i].cost) { availableMonsters.push_back(monsterMap[tempMonsterList[i].name]); @@ -601,11 +934,25 @@ void filterMonsterData(FollowerCount minimumMonsterCost, FollowerCount maximumAr } } -// Add a leveled hero to the databse and return its corresponding index +// Remove monsters from available monsters higher than the maximum cost +void pruneAvailableMonsters(const FollowerCount maximumArmyCost, std::vector & aMonsters) { + int extra = 0; + for (auto i = aMonsters.rbegin(); i != aMonsters.rend(); i++) { + if (monsterReference[*i].cost > maximumArmyCost) { + extra++; + } + else { + break; + } + } + aMonsters.resize(aMonsters.size()-extra); +} + +// Add a leveled hero to the database and return its corresponding index MonsterIndex addLeveledHero(Monster & hero, int level) { Monster m(hero, level); monsterReference.emplace_back(m); - + return (MonsterIndex) (monsterReference.size() - 1); } @@ -616,7 +963,7 @@ int getRealIndex(Monster & monster) { if (monster.rarity != NO_HERO) { for (i = 0; i < baseHeroes.size(); i++) { if (baseHeroes[i].baseName == monster.baseName) { - index = (int) (-i - 2); + index = (-int(i) - 2); } } } else { diff --git a/cosmosData.h b/cosmosData.h index 0521ba8..ebe789d 100644 --- a/cosmosData.h +++ b/cosmosData.h @@ -10,19 +10,20 @@ #include #include #include +#include +#include // Version number not used anywhere except in output to know immediately which version the user is running -const std::string VERSION = "2.9.5.0"; +const std::string VERSION = "3.2.0.4d"; const size_t GIGABYTE = ((size_t) (1) << 30); // Alias for dataTypes makes Code more readable -// An index describing a spot in the monsterReference. +// An index describing a spot in the monsterReference. using MonsterIndex = uint8_t; -// A type used to denote FollowerCounts. +// A type used to denote FollowerCounts. using FollowerCount = uint32_t; - // Constants defining the basic structure of armies const size_t ARMY_MAX_SIZE = 6; const size_t ARMY_MAX_BRUTEFORCEABLE_SIZE = 4; @@ -32,76 +33,97 @@ const std::string HEROLEVEL_SEPARATOR = ":"; const size_t TOURNAMENT_LINES = 5; const int INDEX_NO_MONSTER = -1; -// Worldboss Health maximum larger values get lost because FightResults only accept int16_t too -const int16_t WORLDBOSS_HEALTH = 32000; +// Worldboss Health maximum larger values, maximum value is decided by DamageType +const int64_t WORLDBOSS_HEALTH = 0; // Define types of HeroSkills, Elements and Rarities enum SkillType { - NOTHING, // Base Skill used by normal monsters - - BUFF, // Increases Damage of own army - PROTECT, // Reduces incoming damage vs the own army - CHAMPION, // This monster has the buff and protect ability at the same time - - AOE, // Damages the entire opposing army every turn - HEAL, // Heals the entire own army every turn - LIFESTEAL, // Combines the Aoe and Heal ability into one - DAMPEN, // Reduces the Effects of AOE percentually - - BERSERK, // Every attack this monster makes multiplies its own damage - FRIENDS, // This monster receives a damage multiplicator for every NORMAL monster behind it - ADAPT, // This monster deals more damage vs certain elements - RAINBOW, // This monster receives a damage buff if monsters of every element are behind it - TRAINING, // This monster receives a damage buff for every turn that passed - - WITHER, // This monster's hp decrease after every attack it survives - - REVENGE, // After this monster dies it damages the entire opposing army - PIERCE, // If this monster attacks it also damages every monster behind the attacked - VALKYRIE, // This monsters damage is done to all monsters, the value beeing reduced for each monster it hits. Hardcoded to 50% - TRAMPLE, // This monsters attack damages n units from the front - - BUFF_L, // Buff ability that scales with level - PROTECT_L, // Protect ability that scales with level - CHAMPION_L, // Champion ability that scales with level - AOE_L, // AOE ability that scales with level - HEAL_L, // Heal ability that scales with level - LIFESTEAL_L,// Lifesteal ability that scales with level - DAMPEN_L, // Dampen Ability that scales with level + NOTHING, // Base Skill used by monsters + + BUFF, // Increases the attack of allies in front of this hero by the stated value + PROTECT, // Reduces the attack damage taken by allies in front of this hero by the stated value + CHAMPION, // This hero has both the BUFF and PROTECT abilities + + AOE, // Damages all enemies after each turn by the stated value + HEAL, // Heals all allies after each turn by the stated value + LIFESTEAL, // Damages all enemies and heals all allies after each turn by the stated value + DAMPEN, // Reduces all enemy AOE damage by a percentage amount + AOEZERO, // AOE damage (undampened) at turn 0 / heal-able / after leprechaun's skill + AOEZERO_L, // AOEZERO skill that scales with level + + BERSERK, // Receives an attack multiplier each time it attacks + FRIENDS, // Receives an attack multiplier for every monster behind it + ADAPT, // Deals increased damage against enemies of certain elements + + RAINBOW, // Receives an attack buff if at least one ally of every element is behind it + TRAINING, // Receives an attack buff after each turn + WITHER, // This hero's hp decreases after each turn + + REVENGE, // When this hero dies it deals a percentage of its base attack to all enemies + VALKYRIE, // This monsters damage is done to all monsters, the value being reduced for each monster it hits. + TRAMPLE, // This hero's attack targets the two frontmost enemies + + BUFF_L, // Buff ability that scales with level + PROTECT_L, // Protect ability that scales with level + CHAMPION_L, // Champion ability that scales with level + AOE_L, // AOE ability that scales with level + HEAL_L, // Heal ability that scales with level + LIFESTEAL_L, // Lifesteal ability that scales with level + DAMPEN_L, // Dampen Ability that scales with level + + BEER, // Scales opponent unit health as well as max health by (no. unit in your lane / no. unit in enemy lane) + GROW, // Increase stats gained when this hero levels up + COUNTER, // Enemies that attack this hero are damaged by a percentage amount of the damage taken + + DICE, // adds attack and defense at the start of battle from 0 to ability strength based on enemy starting lineup + LUX, // attacks an enemy based on turn number, enemy starting lineup, and number of enemies remaining + CRIT, // deals bonus damage based on enemy starting lineup and turn count + + EXPLODE, // deals aoe damage when it kill an enemy + ABSORB, // prevents and takes a percentage of damage + HATE, // has extra elemental bonus, can't be treated as adapt due to order + EXPLODE_L, // explode that scales with level + DODGE, // Dodges attacks completely over a certain amount of damage + + DEATHSTRIKE, // Deals damage after death + LEECH, // Heals based on damage dealt + EVOLVE, // Gains attack from damage taken + + COUNTER_MAX_HP // Reflects damage to the enemy with most HP }; enum Element { EARTH = 0, - AIR = 1, - WATER = 2, - FIRE = 3, - ALL = 4, // Discrete Values needed to quickly determine counters + AIR = 1, + WATER = 2, + FIRE = 3, + ALL = 4, // Discrete Values needed to quickly determine counters SELF // These Values are also used to specify targets of hero skills }; const Element counter [] { FIRE, EARTH, AIR, WATER, SELF, SELF }; // Elemental Advantages Ex.: earth = 0 -> counter[0] = fire -> fire has advantage over earth -const float elementalBoost = 1.5; // Damage Boost if element has advantage over another +const double elementalBoost = 1.5; // Damage Boost if element has advantage over another -enum HeroRarity { +enum HeroRarity { NO_HERO = 0, - COMMON = 1, - RARE = 2, - LEGENDARY = 6, + COMMON = 1, + RARE = 2, + LEGENDARY = 6, ASCENDED = 12, // Values define how many stat points per level a hero of this rarity gets WORLDBOSS }; -// Defines Skills of Heros +// Defines Skills of Heroes struct HeroSkill { SkillType skillType; Element target; Element sourceElement; // Not used anywhere - float amount; // Contains various information dependend on the type - bool violatesFightResults; // True if a hero invalidates the data in FightResults i9f he is added to the army - bool hasAoe; + double amount; // Contains various information depending on the type + bool violatesFightResults; // True if a hero invalidates the data in FightResults if he is added to the army + bool hasAoe; bool hasHeal; - bool hasAsymmetricAoe; // Aoe that doesnt damage the entire enemy army equally Ex.: Valkyrie - - HeroSkill(SkillType aType, Element aTarget, Element aSource, float anAmount); + bool hasAsymmetricAoe; // Aoe that doesn't damage the entire enemy army equally Ex.: Valkyrie + + HeroSkill(SkillType aType, Element aTarget, Element aSource, double anAmount); HeroSkill() {}; }; const HeroSkill NO_SKILL = HeroSkill({NOTHING, AIR, AIR, 1}); // base skill used for normal monsters @@ -110,43 +132,49 @@ const HeroSkill NO_SKILL = HeroSkill({NOTHING, AIR, AIR, 1}); // base skill used class Monster { private: Monster(int hp, int damage, FollowerCount cost, std::string name, Element element, HeroRarity rarity, HeroSkill skill, int level); - + public : int hp; int damage; FollowerCount cost; std::string baseName; // Hero names without levels Element element; - + // Hero Stuff HeroRarity rarity; HeroSkill skill; int level; - + int index; // Index used by game, indices for monsters and heroes are assigned at initIndices() + std::string name; // display name - + Monster(int hp, int damage, FollowerCount cost, std::string name, Element element); Monster(int hp, int damage, std::string name, Element element, HeroRarity rarity, HeroSkill skill); Monster(const Monster & baseHero, int level); Monster() {}; - + std::string toJSON(); }; -// Access tools for monsters +// Access tools for monsters extern std::map monsterMap; // Maps monster Names to their indices in monsterReference used to parse input extern std::vector monsterReference; // Global lookup for monster stats. Enables using indices of monsters instead of the objects. Saves tons of memory. Also consumes less memory than pointers extern std::vector availableMonsters; // Contains indices of all monsters the user allows. Is affected by filters -extern std::vector availableHeroes; // Contains all user heroes' indices +extern std::vector availableHeroes; // Contains all user heroes' indices // Storage for Game Data extern std::vector monsterBaseList; // Raw Monster Data, holds the actual Objects void initMonsters(); -extern std::vector baseHeroes; // Raw, unleveld Hero Data, holds actual Objects +extern std::vector baseHeroes; // Raw, unleveled Hero Data, holds actual Objects void initBaseHeroes(); +void initIndices(); +extern std::map heroAliases; //Alternate or shorthand names for heroes +void initHeroAliases(); extern std::vector> quests; // Quest Lineups from the game void initQuests(); +extern std::map stringToEnum; + // Fills all references and storages with real data. // Must be called before any other operation on monsters or input void initGameData(); @@ -155,30 +183,39 @@ void initGameData(); // Must be called before any instance can be solved void filterMonsterData(FollowerCount minimumMonsterCost, FollowerCount maximumArmyCost); -// Defines the results of a fight between two armies; monstersLost and damage desribe the condition of the winning side -// The idea behind FightResults is to save the data and be able to restore the state when the battle ended easily. +// Remove monsters from available monsters higher than the maximum cost +void pruneAvailableMonsters(const FollowerCount maximumArmyCost, std::vector & aMonsters = availableMonsters); + +// Get the index of a monster corresponding to the unique id it is given ingame +int getRealIndex(Monster & monster); + +// Defines the results of a fight between two armies; monstersLost and damage describe the condition of the winning side +// The idea behind FightResults is to save the data and be able to restore the state when the battle ended easily. // When solving an Instance many armies with the same 5 Monsters fight the target Army again with a different 6th Monster // Using FightResults you only have to calculate the fight with 5 Monsters once and then pick up where you left off with the 6th monster. -// Ideally that would work for any battle. Unfortunately as Abilities grew more complex the amount of data needing to be saved outweight the benefit of not having to run the fight again. -// So a few Abilities Invalidate FightResults. Like BUFF. Since the first 5 Monsters would have had more Attack with the buff, the battle might have played out differently. +// Ideally that would work for any battle. Unfortunately as Abilities grew more complex the amount of data needing to be saved outweigh the benefit of not having to run the fight again. +// So a few Abilities Invalidate FightResults. Like BUFF. Since the first 5 Monsters would have had more Attack with the buff, the battle might have played out differently. // So the data in here can't be used if f.e. a BUFF hero is added to the Army -// Similarly some Abilities like Valkyrie produce a Fight state that can't be efficiently captured in a FightResult. +// Similarly some Abilities like Valkyrie produce a Fight state that can't be efficiently captured in a FightResult. // If one of those heroes is in an Army its FightResult is always invalid // -// In a FightResult it is always implied that the target won against the proposed solution. +// In a FightResult it is always implied that the target won against the proposed solution. + +using DamageType = long long; // change data type here to track large damage; default = short + struct FightResult { - int16_t frontHealth; // how much health remaining to the current leading mob of the winning side + DamageType frontHealth; // how much health remaining to the current leading mob of the winning side int16_t leftAoeDamage; // how much aoe damage left took int16_t rightAoeDamage; // how much aoe damage right took - int8_t berserk; // berserk multiplier, if there is a berserker in the front int8_t monstersLost; // how many mobs lost on the winning side (the other side lost all) int8_t turncounter; // how many turns have passed since the battle started + int8_t berserk; // berserk multiplier, if there is a berserker in the front bool valid; // If the result is valid bool dominated; // If the result is worse than another - + FightResult() : valid(false) {} - - // Comparator for FightResults Used to do dominance. + + // Comparator for FightResults Used to do dominance. bool operator <=(const FightResult & toCompare) const { // both results are expected to not have won against the target if(this->leftAoeDamage < toCompare.leftAoeDamage || this->rightAoeDamage > toCompare.rightAoeDamage) { return false; // left is not certainly worse than right because the AOE damage is different @@ -188,18 +225,21 @@ struct FightResult { } else { return this->monstersLost < toCompare.monstersLost; // less monsters destroyed on the enemy side -> left is worse } - } + } }; // Defines a single lineup of monsters class Army { public: FightResult lastFightData; + int64_t seed; + int64_t strength; FollowerCount followerCost; MonsterIndex monsters[ARMY_MAX_SIZE]; int8_t monsterAmount; - + Army(std::vector someMonsters = {}) : + strength(0), followerCost(0), monsterAmount(0) { @@ -207,18 +247,29 @@ class Army { this->add(someMonsters[i]); } } - + // Add monster to the back of the army void add(const MonsterIndex m) { this->monsters[monsterAmount] = m; this->followerCost += monsterReference[m].cost; this->monsterAmount++; + strength += pow(monsterReference[m].hp * monsterReference[m].damage, 1.5); + + // Seed takes into account empty spaces with lane size 6, recalculated each time monster is added + // Any empty spaces are considered to be contiguous and frontmost as they are in DQ and quests + int64_t newSeed = 1; + for (int i = monsterAmount - 1; i >= 0; i--) { + newSeed = (newSeed * abs(monsterReference[monsters[i]].index) + 1) % 2147483647; + } + // Simplification of loop for empty monsters (id: -1) contiguous and frontmost + newSeed += 6 - monsterAmount; + this->seed = newSeed; } bool isEmpty() { return (this->monsterAmount == 0); } - - std::string toString(); + + std::string toString(int tier = 0); std::string toJSON(); }; const size_t ARMY_BUFFER_MAX_SIZE = GIGABYTE / sizeof(Army); @@ -228,50 +279,67 @@ struct Instance { Army target; size_t targetSize; size_t maxCombatants; // Used for Quest Difficulties - + FollowerCount followerUpperBound; // Contains either a used defined limit or the cost of bestSolution Army bestSolution; - + // Stats for Benchmarking time_t calculationTime; int totalFightsSimulated = 0; - + // Propagates from Hero Abilities bool hasAoe; bool hasHeal; bool hasAsymmetricAoe; + bool hasBeer; + bool hasGambler; bool hasWorldBoss; - int lowestBossHealth; - + int64_t lowestBossHealth; + std::vector monsterUsefulLast; - + void setTarget(Army aTarget); }; // Function for sorting FightResults by followers (ascending) inline bool hasFewerFollowers(const Army & a, const Army & b) { - return ((!a.lastFightData.dominated && b.lastFightData.dominated) || + return ((!a.lastFightData.dominated && b.lastFightData.dominated) || (a.lastFightData.dominated == b.lastFightData.dominated && a.followerCost < b.followerCost)); } +// Function for sorting armies to place better results first, then sorting by army strength for equal results +inline bool isMoreEfficient(const Army & a, const Army & b) { + if (a.lastFightData.monstersLost != b.lastFightData.monstersLost) { + return a.lastFightData.monstersLost > b.lastFightData.monstersLost; + } + else if (a.lastFightData.frontHealth != b.lastFightData.frontHealth) { + return a.lastFightData.frontHealth > b.lastFightData.frontHealth; + } + else if (a.lastFightData.rightAoeDamage != b.lastFightData.rightAoeDamage) { + return a.lastFightData.rightAoeDamage > b.lastFightData.rightAoeDamage; + } + else if (a.lastFightData.leftAoeDamage != b.lastFightData.leftAoeDamage) { + return a.lastFightData.leftAoeDamage < b.lastFightData.leftAoeDamage; + } + else { + return a.strength < b.strength; + } +} + // Function for sorting Monsters by cost (ascending) inline bool isCheaper(const Monster & a, const Monster & b) { return a.cost < b.cost; } -// Add a leveled hero to the databse and return its corresponding index +// Add a leveled hero to the database and return its corresponding index MonsterIndex addLeveledHero(Monster & hero, int level); // Returns the index of a quest if the lineup is the same. Returns -1 if not a quest int isQuest(Army & army); -// Get the index of a monster corresponding to the unique id it is given ingame -int getRealIndex(Monster & monster); - -// Custom ceil function to avoid excessive casting. Hardcoded to be effective on 16Bit ints -// The accuracy breaks down at about 1/10000 but should be accurate enough for most purposes -inline int castCeil(float f) { - return 32768 - (int)(32768.0f - f); +// Custom ceil function to avoid excessive casting. Hardcoded to be effective on 32bit ints +inline int castCeil(double f) { + return 2147483647 - (int)(2147483647.0 - f); } -#endif \ No newline at end of file +#endif diff --git a/cqdata.txt b/cqdata.txt new file mode 100644 index 0000000..d78e97c --- /dev/null +++ b/cqdata.txt @@ -0,0 +1,466 @@ +* MONSTERS +20 8 1000 a1 AIR +44 4 1300 e1 EARTH +16 10 1000 f1 FIRE +30 6 1400 w1 WATER +48 6 3900 a2 AIR +30 8 2700 e2 EARTH +18 16 3900 f2 FIRE +24 12 3900 w2 WATER +36 12 8000 a3 AIR +26 16 7500 e3 EARTH +54 8 8000 f3 FIRE +18 24 8000 w3 WATER +24 26 15000 a4 AIR +72 10 18000 e4 EARTH +52 16 23000 f4 FIRE +36 20 18000 w4 WATER +60 20 41000 a5 AIR +36 40 54000 e5 EARTH +42 24 31000 f5 FIRE +78 18 52000 w5 WATER +62 34 96000 a6 AIR +72 24 71000 e6 EARTH +104 20 94000 f6 FIRE +44 44 84000 w6 WATER +106 26 144000 a7 AIR +66 36 115000 e7 EARTH +54 44 115000 f7 FIRE +92 32 159000 w7 WATER +78 52 257000 a8 AIR +60 60 215000 e8 EARTH +94 50 321000 f8 FIRE +108 36 241000 w8 WATER +116 54 495000 a9 AIR +120 48 436000 e9 EARTH +102 58 454000 f9 FIRE +80 70 418000 w9 WATER +142 60 785000 a10 AIR +122 64 689000 e10 EARTH +104 82 787000 f10 FIRE +110 70 675000 w10 WATER +114 110 1403000 a11 AIR +134 81 1130000 e11 EARTH +164 70 1229000 f11 FIRE +152 79 1315000 w11 WATER +164 88 1733000 a12 AIR +128 120 1903000 e12 EARTH +156 92 1718000 f12 FIRE +188 78 1775000 w12 WATER +210 94 2772000 a13 AIR +190 132 3971000 e13 EARTH +166 130 3169000 f13 FIRE +140 128 2398000 w13 WATER +200 142 4785000 a14 AIR +244 136 6044000 e14 EARTH +168 168 4741000 f14 FIRE +212 122 4159000 w14 WATER +226 190 8897000 a15 AIR +200 186 7173000 e15 EARTH +234 136 5676000 f15 FIRE +276 142 7758000 w15 WATER +280 196 12855000 a16 AIR +284 190 12534000 e16 EARTH +288 192 13001000 f16 FIRE +286 198 13475000 w16 WATER +318 206 16765000 a17 AIR +338 192 16531000 e17 EARTH +236 292 18090000 f17 FIRE +262 258 17573000 w17 WATER +280 280 21951000 a18 AIR +330 242 22567000 e18 EARTH +392 200 21951000 f18 FIRE +330 230 20909000 w18 WATER +440 206 27288000 a19 AIR +320 282 27107000 e19 EARTH +352 244 25170000 f19 FIRE +360 238 25079000 w19 WATER +378 268 32242000 a20 AIR +382 264 32025000 e20 EARTH +388 266 33155600 f20 FIRE +454 232 34182000 w20 WATER +428 286 42826000 a21 AIR +446 272 42252000 e21 EARTH +362 338 42798000 f21 FIRE +416 290 41901000 w21 WATER +454 320 55373000 a22 AIR +450 324 55671000 e22 EARTH +458 318 55582000 f22 FIRE +440 340 55877000 w22 WATER +500 348 72580000 a23 AIR +516 340 73483000 e23 EARTH +424 410 72480000 f23 FIRE +490 354 72243000 w23 WATER +554 374 94312000 a24 AIR +458 458 96071000 e24 EARTH +534 392 95772000 f24 FIRE +540 388 95903000 w24 WATER +580 430 124549000 a25 AIR +592 418 123096000 e25 EARTH +764 328 125443000 f25 FIRE +500 506 127256000 w25 WATER +496 582 155097000 a26 AIR +622 468 157055000 e26 EARTH +638 462 160026000 f26 FIRE +700 416 157140000 w26 WATER +712 484 202295000 a27 AIR +580 602 206317000 e27 EARTH +690 498 201426000 f27 FIRE +682 500 199344000 w27 WATER +644 642 265846000 a28 AIR +770 540 268117000 e28 EARTH +746 552 264250000 f28 FIRE +762 536 261023000 w28 WATER +834 616 368230000 a29 AIR +830 614 363805000 e29 EARTH +746 676 358119000 f29 FIRE +1008 512 370761000 w29 WATER +700 906 505055000 a30 AIR +1022 614 497082000 e30 EARTH +930 690 514040000 f30 FIRE +802 802 515849000 w30 WATER +* HEROES +45 20 ladyoftwilight AIR COMMON CHAMPION ALL AIR 3 +70 30 tiny EARTH RARE LIFESTEAL_L ALL EARTH 0.04167 +110 40 nebra FIRE LEGENDARY BUFF ALL FIRE 20 +20 10 valor AIR COMMON PROTECT AIR AIR 1 +30 8 rokka EARTH COMMON PROTECT EARTH EARTH 1 +24 12 pyromancer FIRE COMMON PROTECT FIRE FIRE 1 +50 6 bewat WATER COMMON PROTECT WATER WATER 1 +22 14 hunter AIR COMMON BUFF AIR AIR 2 +40 20 shaman EARTH RARE PROTECT EARTH EARTH 2 +82 22 alpha FIRE LEGENDARY AOE ALL FIRE 1 +28 12 carl WATER COMMON BUFF WATER WATER 2 +38 22 nimue AIR RARE PROTECT AIR AIR 2 +70 26 athos EARTH LEGENDARY PROTECT ALL EARTH 2 +24 16 jet FIRE COMMON BUFF FIRE FIRE 2 +36 24 geron WATER RARE PROTECT WATER WATER 2 +46 40 rei AIR LEGENDARY BUFF ALL AIR 2 +19 22 ailen EARTH COMMON BUFF EARTH EARTH 2 +50 18 faefyr FIRE RARE PROTECT FIRE FIRE 2 +60 32 auri WATER LEGENDARY HEAL ALL WATER 2 +22 32 nicte AIR RARE BUFF AIR AIR 4 +50 12 james EARTH LEGENDARY VALKYRIE ALL EARTH 0.75 +28 16 k41ry AIR COMMON BUFF AIR AIR 3 +46 20 t4urus EARTH RARE BUFF ALL EARTH 1 +100 20 tr0n1x FIRE LEGENDARY AOE ALL FIRE 3 +58 8 aquortis WATER COMMON BUFF WATER WATER 3 +30 32 aeris AIR RARE HEAL ALL AIR 1 +75 2 geum EARTH LEGENDARY BERSERK SELF EARTH 2 +46 16 forestdruid EARTH RARE BUFF EARTH EARTH 4 +32 24 ignitor FIRE RARE BUFF FIRE FIRE 4 +58 14 undine WATER RARE BUFF WATER WATER 4 +38 12 rudean FIRE COMMON BUFF FIRE FIRE 3 +18 50 aural WATER RARE BERSERK SELF WATER 1.2 +46 46 geror AIR LEGENDARY FRIENDS SELF AIR 1.2 +66 44 veildur EARTH LEGENDARY CHAMPION ALL EARTH 3 +72 48 brynhildr AIR LEGENDARY CHAMPION ALL AIR 4 +78 52 groth FIRE LEGENDARY CHAMPION ALL FIRE 5 +30 16 ourea EARTH COMMON BUFF EARTH EARTH 3 +48 20 erebus FIRE RARE CHAMPION FIRE FIRE 2 +62 36 pontus WATER LEGENDARY ADAPT WATER WATER 2 +52 20 chroma AIR RARE PROTECT AIR AIR 4 +26 44 petry EARTH RARE PROTECT EARTH EARTH 4 +58 22 zaytus FIRE RARE PROTECT FIRE FIRE 4 +75 45 spyke AIR LEGENDARY TRAINING SELF AIR 10 +70 55 aoyuki WATER LEGENDARY RAINBOW SELF WATER 100 +75 150 gaiabyte EARTH LEGENDARY WITHER SELF EARTH 0.5 +36 14 oymos AIR COMMON BUFF AIR AIR 4 +32 32 xarth EARTH RARE CHAMPION EARTH EARTH 2 +76 32 atzar FIRE LEGENDARY ADAPT FIRE FIRE 2 +70 42 zeth WATER LEGENDARY REVENGE ALL WATER 0.1 +76 46 koth EARTH LEGENDARY REVENGE ALL EARTH 0.15 +82 50 gurth AIR LEGENDARY REVENGE ALL AIR 0.2 +35 25 werewolf EARTH COMMON PROTECT_L ALL EARTH 0.1112 +55 35 jackoknight AIR RARE BUFF_L ALL AIR 0.1112 +75 45 dullahan FIRE LEGENDARY CHAMPION_L ALL FIRE 0.1112 +36 36 ladyodelith WATER RARE PROTECT WATER WATER 4 +34 54 shygu AIR LEGENDARY PROTECT_L AIR AIR 0.1112 +72 28 thert EARTH LEGENDARY PROTECT_L EARTH EARTH 0.1112 +32 64 lordkirk FIRE LEGENDARY PROTECT_L FIRE FIRE 0.1112 +30 70 neptunius WATER LEGENDARY PROTECT_L WATER WATER 0.1112 +65 12 sigrun FIRE LEGENDARY VALKYRIE ALL FIRE 0.5 +70 14 koldis WATER LEGENDARY VALKYRIE ALL WATER 0.5 +75 16 alvitr EARTH LEGENDARY VALKYRIE ALL EARTH 0.5 +30 18 hama WATER COMMON BUFF WATER WATER 4 +34 34 hallinskidi AIR RARE CHAMPION AIR AIR 2 +60 42 rigr EARTH LEGENDARY ADAPT EARTH EARTH 2 +174 46 aalpha FIRE ASCENDED AOE_L ALL FIRE 0.304 +162 60 aathos EARTH ASCENDED PROTECT_L ALL EARTH 0.304 +120 104 arei AIR ASCENDED BUFF_L ALL AIR 0.304 +148 78 aauri WATER ASCENDED HEAL_L ALL WATER 0.152 +190 38 atr0n1x FIRE ASCENDED VALKYRIE ALL FIRE 0.75 +222 8 ageum EARTH ASCENDED BERSERK SELF EARTH 2 +116 116 ageror AIR ASCENDED FRIENDS SELF AIR 1.3 +0 73 lordofchaos FIRE WORLDBOSS AOE ALL FIRE 20 +38 24 christmaself WATER COMMON HEAL_L ALL WATER 0.1112 +54 36 reindeer AIR RARE AOE_L ALL AIR 0.1112 +72 48 santaclaus FIRE LEGENDARY LIFESTEAL_L ALL FIRE 0.1112 +44 44 sexysanta EARTH RARE VALKYRIE ALL EARTH 0.66 +24 24 toth FIRE COMMON BUFF FIRE FIRE 4 +40 30 ganah WATER RARE CHAMPION WATER WATER 2 +58 46 dagda AIR LEGENDARY ADAPT AIR AIR 2 +300 110 bubbles WATER ASCENDED DAMPEN_L ALL WATER 0.0050 +150 86 apontus WATER ASCENDED ADAPT WATER WATER 3 +162 81 aatzar FIRE ASCENDED ADAPT FIRE FIRE 3 +74 36 arshen AIR LEGENDARY TRAMPLE ALL AIR 2 +78 40 rua FIRE LEGENDARY TRAMPLE ALL FIRE 2 +82 44 dorth WATER LEGENDARY TRAMPLE ALL WATER 2 +141 99 arigr EARTH ASCENDED ADAPT EARTH EARTH 3 +0 125 motherofallkodamas EARTH WORLDBOSS DAMPEN ALL EARTH 0.5 +42 50 hosokawa AIR LEGENDARY BUFF_L AIR AIR 0.1112 +32 66 takeda EARTH LEGENDARY BUFF_L EARTH EARTH 0.1112 +38 56 hirate FIRE LEGENDARY BUFF_L FIRE FIRE 0.1112 +44 48 hattori WATER LEGENDARY BUFF_L WATER WATER 0.1112 +135 107 adagda AIR ASCENDED ADAPT AIR AIR 3 +30 20 bylar EARTH COMMON BUFF EARTH EARTH 4 +36 36 boor FIRE RARE TRAINING SELF FIRE 3 +52 52 bavah WATER LEGENDARY CHAMPION ALL WATER 2 +75 25 leprechaun EARTH LEGENDARY BEER ALL EARTH 0 +30 30 sparks FIRE COMMON GROW ALL FIRE 2 +48 42 leaf EARTH RARE GROW ALL EARTH 2 +70 48 flynn AIR LEGENDARY GROW ALL AIR 2 +122 122 abavah WATER ASCENDED CHAMPION_L ALL ALL 0.152 +66 60 drhawking AIR LEGENDARY AOEZERO_L ALL AIR 1 +150 90 masterlee AIR ASCENDED COUNTER AIR AIR 0.5 +70 38 kumusan FIRE LEGENDARY COUNTER FIRE FIRE 0.2 +78 42 liucheng WATER LEGENDARY COUNTER WATER WATER 0.25 +86 44 hidoka EARTH LEGENDARY COUNTER EARTH EARTH 0.3 +0 11 kryton AIR WORLDBOSS TRAINING SELF AIR 10 +25 26 dicemaster WATER COMMON DICE SELF SELF 20 +28 60 luxuriusmaximus FIRE RARE LUX SELF EARTH 1 +70 70 pokerface EARTH LEGENDARY CRIT EARTH EARTH 3 +25 25 taint AIR COMMON VALKYRIE ALL AIR 0.5 +48 50 putrid EARTH RARE TRAINING SELF EARTH -3 +52 48 defile FIRE LEGENDARY EXPLODE ALL FIRE 50 +150 15 neil WATER LEGENDARY ABSORB SELF WATER 0.3 +78 26 mahatma AIR LEGENDARY HATE WATER AIR 0.75 +76 30 jade EARTH LEGENDARY HATE AIR EARTH 0.75 +72 36 edana FIRE LEGENDARY HATE EARTH FIRE 0.75 +80 30 dybbuk WATER LEGENDARY HATE FIRE WATER 0.75 +85 135 ashygu AIR ASCENDED PROTECT_L AIR AIR 0.1819 +180 70 athert EARTH ASCENDED PROTECT_L EARTH EARTH 0.1819 +80 160 alordkirk FIRE ASCENDED PROTECT_L FIRE FIRE 0.1819 +75 175 aneptunius WATER ASCENDED PROTECT_L WATER WATER 0.1819 +106 124 ahosokawa AIR ASCENDED BUFF_L AIR AIR 0.1819 +82 164 atakeda EARTH ASCENDED BUFF_L EARTH EARTH 0.1819 +96 144 ahirate FIRE ASCENDED BUFF_L FIRE FIRE 0.1819 +114 126 ahattori WATER ASCENDED BUFF_L WATER WATER 0.1819 +0 110 doyenne WATER WORLDBOSS DODGE ALL WATER 5000 +30 40 billy EARTH COMMON DEATHSTRIKE ALL EARTH 100 +88 22 sanqueen WATER RARE LEECH SELF WATER 0.8 +150 60 cliodhna AIR LEGENDARY EVOLVE SELF AIR 1 +340 64 guy FIRE ASCENDED COUNTER_MAX_HP FIRE FIRE 1 +126 114 adefile FIRE ASCENDED EXPLODE ALL FIRE 150 +* ALIASES +lady ladyoftwilight +lot ladyoftwilight +pyro pyromancer +kairy k41ry +taurus t4urus +tronix tr0n1x +druid forestdruid +veil veildur +bryn brynhildr +gaia gaiabyte +ww werewolf +wolf werewolf +jack jackoknight +jacko jackoknight +dull dullahan +dulla dullahan +odelith ladyodelith +kirk lordkirk +nep neptunius +tak takeda +hall hallinskidi +atronix atr0n1x +elf christmaself +deer reindeer +santa santaclaus +ss sexysanta +lep leprechaun +hawking drhawking +dice dicemaster +lux luxuriusmaximus +luxurious luxuriusmaximus +poker pokerface +akirk alordkirk +anep aneptunius +atak atakeda +san sanqueen +squenn sanqueen +clio cliodhna +cloddy cliodhna +fawkes guy +guyfawkes guy +loc lordofchaos +fboss lordofchaos +moak motherofallkodamas +eboss motherofallkodamas +kry kryton +aboss kryton +doy doyenne +wboss doyenne + +* QUESTS +w5 + +f1 a1 f1 a1 f1 a1 + +f5 a5 + +f2 a2 e2 w2 f3 a3 + +w3 e3 w3 e3 w3 e3 + +w4 e1 a4 f4 w1 e4 + +f5 a5 f4 a3 f2 a1 + +e4 w4 w5 e5 w4 e4 + +w5 f5 e5 a5 w4 f4 + +w5 e5 a5 f5 e5 w5 + +f5 f6 e5 e6 a5 a6 + +e5 w5 f5 e6 f6 w6 + +a8 a7 a6 a5 a4 a3 + +f7 f6 f5 e7 e6 e6 + +w5 e6 w6 e8 w8 + +a9 f8 a8 + +w5 e6 w7 e8 w8 + +f7 f6 a6 f5 a7 a8 + +e7 w9 f9 e9 + +f2 a4 f5 a7 f8 a10 + +w10 a10 w10 + +w9 e10 f10 + +e9 a9 w8 f8 e8 + +f6 a7 f7 a8 f8 a9 + +w8 w7 w8 w8 w7 w8 + +a9 w7 w8 e7 e8 f10 + +e9 f9 w9 f7 w7 w7 + +a10 a8 a9 a10 a9 + +a10 w7 f7 e8 a9 a9 + +e10 e10 e10 f10 + +e9 f10 f9 f9 a10 a7 + +w1 a9 f10 e9 a10 w10 + +e9 a9 a9 f9 a9 f10 + +f8 e9 w9 a9 a10 a10 + +w8 w8 w10 a10 a10 f10 + +a8 a10 f10 a10 a10 a10 + +e8 a10 e10 f10 f10 e10 + +f10 e10 w10 a10 w10 w10 + +w9 a10 w10 e10 a10 a10 + +w10 a10 w10 a10 w10 a10 + +e12 e11 a11 f11 a12 + +a11 a11 e11 a11 e11 a11 + +a8 a11 a10 w10 a12 e12 + +a10 f10 a12 f10 a10 f12 + +w4 e11 a12 a12 w11 a12 + +a11 a12 a11 f11 a11 f10 + +f12 w11 e12 a12 w12 + +a11 a11 e12 a11 a11 a13 + +a13 f13 f13 f13 + +f12 f12 f12 f12 f12 f12 + +a11 e11 a13 a11 e11 a13 + +f13 w13 a13 f12 f12 + +a9 f13 f13 f12 a12 a12 + +a13 a13 a12 a12 f11 f12 + +a11 f10 a11 e14 f13 a11 + +f13 a13 f13 e13 w12 + +e10 a13 w12 f13 f13 f13 + +f7 w11 w13 e14 f13 a14 + +a8 f15 a14 f14 w14 + +f12 w13 a14 f13 a13 e10 + +f13 e13 a13 w12 f12 a12 + +w13 e12 w12 a14 a12 f13 + +e15 f14 w14 a15 + +e12 a14 e14 w13 e12 f13 + +e13 f12 w11 w12 a14 e14 + +a14 e13 a11 a14 f13 e13 + +f13 w13 e14 f13 f14 a14 + +a15 e15 f15 w15 + +f13 a14 e14 f13 a14 f13 + +a11 a14 w13 e14 a14 f14 + +e13 a14 f14 w13 f14 e14 + +w10 a14 a14 a14 a14 w14 + +w13 w13 f14 a15 a15 e13 + +a14 e14 e14 e14 e14 e14 + +w15 w15 e15 w15 f15 + +f14 e15 a15 w14 a14 e15 + +w14 a15 w14 e15 a15 w14 + +w15 w15 w15 w15 f15 f15 + +a15 a15 a15 a15 a15 w14 + +f15 w15 w15 w15 w15 w15 + +f14 e16 e16 e16 e16 + +w14 a15 f15 a16 f16 f15 + +w15 f15 w15 w15 a16 w16 + +a16 w15 a16 e16 a17 + +f15 w15 w15 w15 e17 e16 + +a13 a16 a16 a16 a16 f16 + +e16 f16 f16 f17 a17 + +w15 f16 a16 a16 f16 e17 + +f16 f17 a17 a15 a16 a16 + +f16 f16 f16 f16 f16 a18 + +e16 e16 a17 f17 a17 w15 + +f17 a18 a18 w17 a17 e16 + +e18 f16 f16 f16 w16 f18 + +a21 a20 f20 a21 + +e18 e17 a18 e17 e17 e20 + +a19 a19 w18 w18 f15 e16 + +w18 f19 f19 e18 e18 a19 + +f18 w19 w19 e19 e19 f18 + +f19 a19 e19 f20 a20 f19 + +a20 w18 w18 a19 w20 f20 + +a22 e21 f20 w20 f22 + +f23 w21 f20 a20 a21 + +f22 w21 w21 f21 e21 + +a20 f20 e21 a21 a20 f20 + +f20 e21 f20 w20 e21 f20 + +e21 w22 f23 a23 a22 + +f21 a20 f21 a21 w21 e21 + +w22 w22 a22 f22 e21 w21 + +e22 f22 a22 w21 e22 w21 + +a22 w22 a22 w21 e22 w22 + +f23 a22 e23 e23 e22 w22 + +w22 w23 a23 w22 f21 f21 + +w24 a24 e24 f24 f23 f23 + +a24 a25 a24 f25 e23 + +e23 f23 e23 w25 a24 a23 + +e24 a24 e24 f23 w24 w23 + +e24 e24 a24 w24 f24 w24 + +f24 a23 a24 f24 f24 w24 + +f25 f25 a26 a26 w25 + +e27 w27 e27 w27 + +w27 f27 f27 w27 + +a27 e27 f27 w27 + +e27 e27 w27 f27 + +a28 f27 w27 f27 + +a28 a27 w27 w28 + +f26 w28 w28 w28 + +e26 w27 w27 w28 + +f27 w28 w29 w29 + +a27 w27 w29 w29 + +e27 w28 w29 w29 + +a26 e26 w29 e29 f27 + +w29 f29 a27 a26 a28 + +w29 e29 a29 a28 + +a27 w29 a27 f28 a27 + +w29 w30 a26 a27 a30 + +f26 a30 f27 e30 w28 + +a30 a27 a30 a27 e30 + +a27 w30 f28 w30 a27 a27 + +w27 w29 a30 a27 e30 a27 + +f27 e30 f28 a30 e27 a27 + +w29 a30 a30 nicte:1000 + +a30 w29 e29 ladyodelith:1000 + +e30 w29 w30 f25 ignitor:1000 + +f27 w28 a30 a27 petry:1000 + +e26 e30 w29 e29 ignitor:1000 + +f26 e30 f25 a30 w29 undine:1000 + +w29 a30 undine:1000 petry:1000 + +a28 w30 ignitor:1000 chroma:1000 + +a28 a28 w28 undine:1000 zaytus:1000 + +w28 w28 w29 undine:1000 ladyodelith:1000 + +e30 e30 w30 w30 undine:1000 chroma:1000 + +f30 f30 e30 a30 ignitor:1000 zaytus:1000 + +e30 w30 chroma:1000 nicte:1000 forestdruid:1000 + +w30 a30 undine:1000 zaytus:1000 ignitor:1000 + +f30 zaytus:1000 chroma:1000 nicte:1000 forestdruid:1000 + +f30 f30 undine:1000 ladyodelith:1000 forestdruid:1000 petry:1000 + +e30 e30 ignitor:1000 zaytus:1000 nicte:1000 undine:1000 + +w30 ladyodelith:1000 ignitor:1000 zaytus:1000 chroma:1000 forestdruid:1000 + +ignitor:1000 forestdruid:1000 petry:1000 chroma:1000 ladyodelith:1000 undine:1000 + +f30 ignitor:1000 undine:1000 neptunius:1000 + diff --git a/default.cqconfig b/default.cqconfig index a01ac2f..7d7d8c0 100644 --- a/default.cqconfig +++ b/default.cqconfig @@ -6,6 +6,8 @@ OUTPUT_LEVEL BASIC IGNORE_EXEC_HALT FALSE AUTO_ADJUST_OUTPUT TRUE FIRST_DOMINANCE 4 +STOP_FIRST_SOLUTION TRUE +NUM_THREADS 6 ENTITIES NEXT_FILE default.cqinput \ No newline at end of file diff --git a/inputProcessing.cpp b/inputProcessing.cpp index 713b40a..e3a83fa 100644 --- a/inputProcessing.cpp +++ b/inputProcessing.cpp @@ -13,7 +13,7 @@ UserInterface interface; void UserInterface::printBuffer(OutputLevel urgency) { if (shouldOutput(urgency)) { cout << this->outputStream.str(); - } + } this->outputStream.str(""); this->outputStream.clear(); } @@ -65,7 +65,7 @@ void UserInterface::haltExecution() { // Takes a raw line from an Input file or command line input and splits it into tokens vector UserInterface::parseInput(string input) { - input = split(toLower(input), COMMENT_DELIMITOR)[0]; + input = split(toLower(input), COMMENT_DELIMITOR)[0]; return split(input, TOKEN_SEPARATOR); } @@ -147,11 +147,11 @@ void IOManager::loadInputFiles(string fileName) { this->fileInput.init(fileName); } -// Read from the input and set configuration values accordingly. +// Read from the input and set configuration values accordingly. // Outputs warnings to the command line if something is out of order void IOManager::getConfiguration() { vector tokens; - + if (this->fileInput.checkLine(TOKENS.START_CONFIG)) { while (this->fileInput.hasLine()) { tokens = this->fileInput.getLine(); @@ -171,8 +171,16 @@ void IOManager::getConfiguration() { config.showQueries = parseBool(tokens.at(1)); } else if (tokens[0] == TOKENS.SHOW_REPLAY_STRINGS) { config.showReplayStrings = parseBool(tokens.at(1)); + } else if (tokens[0] == TOKENS.STOP_FIRST_SOLUTION) { + config.stopFirstSolution = parseBool(tokens.at(1)); + } else if (tokens[0] == TOKENS.NUM_THREADS) { + config.numThreads = (int) parseInt(tokens.at(1)); } else if (tokens[0] == TOKENS.IGNORE_EXEC_HALT) { config.ignoreExecutionHalt = parseBool(tokens.at(1)); + } else if (tokens[0] == TOKENS.INDIVIDUAL_BATTLES) { + config.individualBattles = parseBool(tokens.at(1)); + } else if (tokens[0] == TOKENS.SKIP_CONTINUE) { + config.skipContinue = parseBool(tokens.at(1)); } else if (tokens[0] != TOKENS.EMPTY) { interface.outputMessage("Unrecognized option '" + tokens[0] + "'", NOTIFICATION_OUTPUT); } @@ -240,7 +248,7 @@ bool IOManager::askYesNoQuestion(string questionMessage, OutputLevel urgency, st } else { inputString = this->getResistantInput(questionMessage + " (" + TOKENS.YES + "/" + TOKENS.NO + "): ", question)[0]; } - + if (inputString == TOKENS.NO) { return false; } @@ -255,10 +263,10 @@ vector IOManager::takeHerolevelInput() { vector heroes {}; vector input; pair heroData; - + interface.outputMessage("\nEnter your Heroes with levels. Press enter after every Hero.", QUERY_OUTPUT); interface.outputMessage("Press enter twice or type " + TOKENS.HEROES_FINISHED + " to proceed without inputting additional Heroes.", QUERY_OUTPUT); - + int cancelCounter = 0; do { input = this->getResistantInput("Enter Hero " + to_string(heroes.size()+1) + ": ", rawFirst); @@ -282,15 +290,15 @@ vector IOManager::takeHerolevelInput() { }; } } while (input[0] != TOKENS.HEROES_FINISHED && cancelCounter < 2); - + return heroes; } -// Promts the user to input instance(s) to be solved +// Promts the user to input instance(s) to be solved vector IOManager::takeInstanceInput(string prompt) { vector instances; vector tokens; - + while (true) { tokens = this->getResistantInput(prompt, raw); instances.clear(); @@ -322,13 +330,13 @@ string IOManager::getJSONError(InputException e) { string message; string errorType; switch (e) { - case MACROFILE_MISSING: - message = "Could not find Macro File!"; + case MACROFILE_MISSING: + message = "Could not find Macro File!"; errorType = "MACROFILE_MISSING"; break; - case MACROFILE_USED_UP: + case MACROFILE_USED_UP: message = "Macro File does not provide enough input!"; - errorType = "MARCOFILE_USED_UP"; + errorType = "MACROFILE_USED_UP"; break; default: message = "Unexpected Error"; @@ -355,7 +363,7 @@ bool shouldOutput(OutputLevel urgency) { Instance makeInstanceFromString(string instanceString) { Instance instance; int dashPosition = (int) instanceString.find(QUEST_NUMBER_SEPARTOR); - + if (instanceString.compare(0, QUEST_PREFIX.length(), QUEST_PREFIX) == 0) { try { int questNumber = (int) parseInt(instanceString.substr(QUEST_PREFIX.length(), dashPosition-QUEST_PREFIX.length())); @@ -372,11 +380,11 @@ Instance makeInstanceFromString(string instanceString) { return instance; } -// Parse string linup input into actual monsters. If there are heroes in the input, a leveled hero is added to the database +// Parse string lineup input into actual monsters. If there are heroes in the input, a leveled hero is added to the database Army makeArmyFromStrings(vector stringMonsters) { Army army; pair heroData; - + for(size_t i = 0; i < stringMonsters.size(); i++) { if(stringMonsters[i].find(HEROLEVEL_SEPARATOR) != stringMonsters[i].npos) { heroData = parseHeroString(stringMonsters[i]); @@ -401,13 +409,18 @@ pair parseHeroString(string heroString) { } catch (const exception & e) { throw HERO_PARSE; } - - Monster hero; - for (size_t i = 0; i < baseHeroes.size(); i++) { - if (baseHeroes[i].baseName == name) { + + std::map::iterator alias = heroAliases.find(name); + if (alias != heroAliases.end()) { + name = alias->second; + } + + Monster hero; + for (size_t i = 0; i < baseHeroes.size(); i++) { + if (baseHeroes[i].baseName == name) { return pair(baseHeroes[i], level); - } - } + } + } throw HERO_PARSE; } @@ -483,7 +496,7 @@ string makeJSONFromInstance(Instance instance, bool valid) { s << "\"time\"" << ":" << instance.calculationTime << ","; s << "\"fights\"" << ":" << instance.totalFightsSimulated << ","; s << "\"replay\"" << ":" << "\"" << makeBattleReplay(instance.bestSolution, instance.target) << "\""; - + s << "}"; if (!valid) { s << ",\"error\" : {"; @@ -497,28 +510,28 @@ string makeJSONFromInstance(Instance instance, bool valid) { string makeStringFromInstance(Instance instance, bool valid, bool showReplayString) { stringstream s; - - s << endl << "Solution for " << instance.target.toString() << ":" << endl; + + s << endl << "Solution for " << instance.target.toString(ARMY_MAX_SIZE - instance.maxCombatants + 1) << ":" << endl; // Announce the result if (!instance.bestSolution.isEmpty()) { s << " " << instance.bestSolution.toString() << endl; } else { s << " Could not find a solution that beats this lineup." << endl; } s << endl; - + // Aditional Statistics if (instance.hasWorldBoss) { - s << " Boss Damage Done: " << WORLDBOSS_HEALTH - instance.lowestBossHealth << endl; + s << " Boss Damage Done: " << numberWithSeparators(WORLDBOSS_HEALTH - instance.lowestBossHealth) << endl; } s << " " << instance.totalFightsSimulated << " Fights simulated." << endl; s << " Total Calculation Time: " << instance.calculationTime << endl; s << " Calc Version: " << VERSION << endl << endl; - + // Replay for debugging and confirming interactions if (!instance.bestSolution.isEmpty() && showReplayString) { s << "Battle Replay (Use on Ingame Tournament Page):" << endl << makeBattleReplay(instance.bestSolution, instance.target) << endl << endl; } - + // Sanity Check if (!valid) { s << "This does not beat the lineup!!!" << endl; @@ -545,7 +558,7 @@ int64_t parseInt(string toParse) { if (num > numeric_limits::max()) { return numeric_limits::max(); } else { - return num; + return num; } } else { throw invalid_argument("Could not parse number from '" + toParse + "'!"); @@ -584,7 +597,29 @@ vector split(string target, string separator) { // Convert a string to lowercase where available string toLower(string input) { for (size_t i = 0; i < input.length(); i++) { - input[i] = tolower(input[i], locale()); + input[i] = tolower(input[i]); + //input[i] = tolower(input[i], locale()); } return input; -} \ No newline at end of file +} + +// convert a large number into a string with thousand separators +string numberWithSeparators(const uint64_t& largeNumber) +{ + string numStr = to_string(largeNumber); + string newStr; + + if (numStr.size() > 3) { + int numSep = (numStr.size() - 1) / 3; + int beg = (numStr.size() % 3) ? numStr.size() % 3 : 3; // first place to insert a separator + + newStr += numStr.substr(0, beg); + + for (int i = 0; i < numSep; i++) { + newStr += thousandSeparator; + newStr += numStr.substr(beg + 3 * i, 3); + } + return newStr; + } + return numStr; +} diff --git a/inputProcessing.h b/inputProcessing.h index e86b59b..4ca5155 100644 --- a/inputProcessing.h +++ b/inputProcessing.h @@ -56,17 +56,21 @@ struct ParserTokens { const std::string YES = "y"; const std::string NO = "n"; const std::string EMPTY = ""; - + const std::string SHOW_QUERIES = "show_queries"; const std::string FIRST_DOMINANCE = "first_dominance"; const std::string OUTPUT_LEVEL = "output_level"; - const std::string AUTO_ADJUST_OUTPUT = "auto_adjust_output"; - const std::string SHOW_REPLAY_STRINGS = "show_replays"; - const std::string IGNORE_EMPTY = "ignore_empty_lines"; - const std::string IGNORE_EXEC_HALT = "ignore_exec_halt"; - - const std::string T_SOLUTION_OUTPUT = "solution"; - const std::string T_BASIC_OUTPUT = "basic"; + const std::string AUTO_ADJUST_OUTPUT = "auto_adjust_output"; + const std::string SHOW_REPLAY_STRINGS = "show_replays"; + const std::string IGNORE_EMPTY = "ignore_empty_lines"; + const std::string IGNORE_EXEC_HALT = "ignore_exec_halt"; + const std::string STOP_FIRST_SOLUTION = "stop_first_solution"; + const std::string NUM_THREADS = "num_threads"; + const std::string INDIVIDUAL_BATTLES = "individual_battles"; + const std::string SKIP_CONTINUE = "skip_continue"; + + const std::string T_SOLUTION_OUTPUT = "solution"; + const std::string T_BASIC_OUTPUT = "basic"; const std::string T_DETAILED_OUPUT = "detailed"; }; static const ParserTokens TOKENS; @@ -77,12 +81,17 @@ struct Configuration { bool ignoreEmptyLines = false; bool ignoreQuestions = false; // bool ignoreExecutionHalt = false; + bool skipContinue = false; bool JSONOutput = false; // int firstDominance = ARMY_MAX_BRUTEFORCEABLE_SIZE; OutputLevel outputLevel = BASIC_OUTPUT; bool autoAdjustOutputLevel = true; bool individualBattles = false; // bool unlimitedWorldbossHealth = false; // + bool stopFirstSolution = true; // false means continue to search for cheaper solution if non-zero cost solution was found + int numThreads = 6; + + size_t branchwiseExpansionLimit = 20; }; extern Configuration config; @@ -90,18 +99,18 @@ class UserInterface { private: time_t lastTimedOutput = -1; std::ostringstream outputStream; - + void printBuffer(OutputLevel urgency); std::string getIndent(int indent); - - public: + + public: void outputMessage(std::string message, OutputLevel urgency, int indent = 0, bool linebreak = true); void timedOutput(std::string message, OutputLevel urgency, int indent = 0, bool reset = false); void suspendTimedOutputs(OutputLevel urgency); void resumeTimedOutputs(OutputLevel urgency); void finishTimedOutput(OutputLevel urgency); void haltExecution(); - + std::vector parseInput(std::string input); }; extern UserInterface interface; @@ -109,11 +118,11 @@ extern UserInterface interface; class InputFileManager { private: std::queue> inputLines; - - public: + + public: void init(std::string fileName); bool readFile(std::string fileName, bool important); - + bool checkLine(std::string token); std::vector getLine(); bool hasLine(); @@ -122,13 +131,13 @@ class InputFileManager { class IOManager { private: InputFileManager fileInput; - + void handleInputException(InputException e); - + public: void loadInputFiles(std::string fileName); void getConfiguration(); - bool askYesNoQuestion(std::string question, OutputLevel urgency, std::string defaultAnswer); + bool askYesNoQuestion(std::string question, OutputLevel urgency, std::string defaultAnswer); std::vector getResistantInput(std::string query, QueryType queryType = raw); std::vector takeHerolevelInput(); std::vector takeInstanceInput(std::string promt); @@ -165,4 +174,8 @@ std::vector split(std::string target, std::string separator); // Convert a string to lowercase where available std::string toLower(std::string input); -#endif \ No newline at end of file +const char thousandSeparator = ','; +// display a large number with thousand separators +std::string numberWithSeparators(const uint64_t& largeNumber); + +#endif diff --git a/main.cpp b/main.cpp index 3844188..349f01f 100644 --- a/main.cpp +++ b/main.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "inputProcessing.h" #include "cosmosData.h" @@ -13,123 +15,164 @@ using namespace std; IOManager iomanager; -// Simulates fights with all armies against the target. The Fightresults are written to the corresponding structs in armies. +// Simulates fights with all armies against the target. The FightResults are written to the corresponding structs in armies. // If a solution is found, armies that are more expensive than that solution are ignored -void simulateMultipleFights(vector & armies, Instance & instance) { +void simulateMultipleFights(vector & armies, Instance & instance, vector & aMonsters = availableMonsters) { bool newFound = false; size_t armyAmount = armies.size(); - + if (!instance.hasWorldBoss) { for (size_t i = 0; i < armyAmount; i++) { if (armies[i].followerCost < instance.followerUpperBound) { // Ignore if a cheaper solution exists + ++instance.totalFightsSimulated; if (simulateFight(armies[i], instance.target)) { // left (our side) wins: if (!newFound) { - interface.suspendTimedOutputs(DETAILED_OUTPUT); + //interface.suspendTimedOutputs(DETAILED_OUTPUT); } newFound = true; instance.followerUpperBound = armies[i].followerCost; + pruneAvailableMonsters(instance.followerUpperBound, aMonsters); instance.bestSolution = armies[i]; - interface.outputMessage(instance.bestSolution.toString(), DETAILED_OUTPUT, 2); + + if (config.numThreads == 1) { + interface.outputMessage(instance.bestSolution.toString(), DETAILED_OUTPUT, 2); + } } } } if (newFound) { - interface.resumeTimedOutputs(DETAILED_OUTPUT); + //interface.resumeTimedOutputs(DETAILED_OUTPUT); } } else { for (size_t i = 0; i < armyAmount; i++) { + ++instance.totalFightsSimulated; simulateFight(armies[i], instance.target); - if (instance.lowestBossHealth == -1 || armies[i].lastFightData.frontHealth < instance.lowestBossHealth) { + if ( //instance.lowestBossHealth == -1 || + armies[i].lastFightData.frontHealth < instance.lowestBossHealth) { instance.bestSolution = armies[i]; instance.lowestBossHealth = armies[i].lastFightData.frontHealth; } + else if (armies[i].lastFightData.frontHealth > 0) { // reached the limit + instance.bestSolution = armies[i]; + instance.lowestBossHealth = numeric_limits::min(); + } } } } // Take the data from oldArmies and write all armies into newArmies with an additional monster at the end. // Armies that are dominated are ignored. -void expand(vector & newPureArmies, vector & newHeroArmies, - const vector & oldPureArmies, const vector & oldHeroArmies, - const size_t currentArmySize, const Instance & instance) { +void expand(vector & newPureArmies, vector & newHeroArmies, + const vector & oldPureArmies, const vector & oldHeroArmies, + const size_t currentArmySize, const Instance & instance, + const vector & aMonsters = availableMonsters) { FollowerCount remainingFollowers; - size_t availableMonstersSize = availableMonsters.size(); + size_t availableMonstersSize = aMonsters.size(); size_t availableHeroesSize = availableHeroes.size(); size_t oldPureArmiesSize = oldPureArmies.size(); size_t oldHeroArmiesSize = oldHeroArmies.size(); size_t i, m; - - bool removeUseless = currentArmySize == (instance.maxCombatants-1) && !instance.hasWorldBoss; - bool instanceInvalid = instance.hasHeal || instance.hasAsymmetricAoe; - + + newPureArmies.reserve(oldPureArmies.size() * availableMonstersSize); + newHeroArmies.reserve(oldPureArmies.size() * availableHeroesSize + oldHeroArmies.size() * (availableHeroesSize + availableMonstersSize)); + + bool removeUseless = currentArmySize == (instance.maxCombatants-1) && !instance.hasWorldBoss && !instance.hasGambler; + bool instanceInvalid = instance.hasHeal || instance.hasAsymmetricAoe || instance.hasGambler; + + // enemy booze will invalidate FightResults + bool boozeInfluence = instance.hasBeer && currentArmySize >= instance.targetSize; + // Expansion for non-Hero Armies for (i = 0; i < oldPureArmiesSize; i++) { - if (instance.followerUpperBound >= oldPureArmies[i].followerCost && !oldPureArmies[i].lastFightData.dominated) { + if (!oldPureArmies[i].lastFightData.dominated) { remainingFollowers = instance.followerUpperBound - oldPureArmies[i].followerCost; // Add Normal Monsters. Check for Cost - for (m = 0; m < availableMonstersSize; m++) { - if (monsterReference[availableMonsters[m]].cost < remainingFollowers) { - if (!removeUseless || instance.monsterUsefulLast[availableMonsters[m]]) { - newPureArmies.push_back(oldPureArmies[i]); - newPureArmies.back().add(availableMonsters[m]); - newPureArmies.back().lastFightData.valid = !instanceInvalid; - } + for (m = 0; m < availableMonstersSize && monsterReference[aMonsters[m]].cost <= remainingFollowers; m++) { + if (!removeUseless || instance.monsterUsefulLast[aMonsters[m]] || instance.targetSize == oldPureArmies[i].lastFightData.monstersLost) { + newPureArmies.push_back(oldPureArmies[i]); + newPureArmies.back().add(aMonsters[m]); + newPureArmies.back().lastFightData.valid = !instanceInvalid && !boozeInfluence; } } // Add Hero. no check needed because it is the First Added for (m = 0; m < availableHeroesSize; m++) { - if (!removeUseless || instance.monsterUsefulLast[availableHeroes[m]]) { + if (!removeUseless || instance.monsterUsefulLast[availableHeroes[m]] || instance.targetSize == oldPureArmies[i].lastFightData.monstersLost) { newHeroArmies.push_back(oldPureArmies[i]); newHeroArmies.back().add(availableHeroes[m]); - newHeroArmies.back().lastFightData.valid = !instanceInvalid && !monsterReference[availableHeroes[m]].skill.violatesFightResults; + newHeroArmies.back().lastFightData.valid = !instanceInvalid && + !boozeInfluence && + !monsterReference[availableHeroes[m]].skill.violatesFightResults; } } } } - + vector usedHeroes; usedHeroes.resize(monsterReference.size(), false); HeroSkill currentSkill; bool invalidSkill; bool friendsInfluence; bool rainbowInfluence; + int tempRainbowCondition; + Element missingElement; for (i = 0; i < oldHeroArmiesSize; i++) { - if (instance.followerUpperBound >= oldHeroArmies[i].followerCost && !oldHeroArmies[i].lastFightData.dominated) { + if (!oldHeroArmies[i].lastFightData.dominated) { remainingFollowers = instance.followerUpperBound - oldHeroArmies[i].followerCost; friendsInfluence = false; rainbowInfluence = false; invalidSkill = false; + tempRainbowCondition = 0; + missingElement = ALL; // Check for influences that can invalidate fightresults and gather used heroes for (m = 0; m < currentArmySize; m++) { + if (rainbowInfluence) { + tempRainbowCondition |= 1 << monsterReference[oldHeroArmies[i].monsters[m]].element; + } currentSkill = monsterReference[oldHeroArmies[i].monsters[m]].skill; invalidSkill |= currentSkill.hasHeal || currentSkill.hasAsymmetricAoe; friendsInfluence |= currentSkill.skillType == FRIENDS; - rainbowInfluence |= currentSkill.skillType == RAINBOW && currentArmySize > m + 4; // Hardcoded number of elements required to activate rainbow + rainbowInfluence |= currentSkill.skillType == RAINBOW && currentArmySize >= m + 4; // Hardcoded number of elements required to activate rainbow + boozeInfluence |= currentSkill.skillType == BEER && currentArmySize < instance.targetSize; usedHeroes[oldHeroArmies[i].monsters[m]] = true; } + // rainbowInfluence can only invalidate if there are exactly 3 elements fulfilled before adding new unit + if (rainbowInfluence) { + switch (tempRainbowCondition) { + case 7 : missingElement = FIRE; break; + case 11: missingElement = WATER; break; + case 13: missingElement = AIR; break; + case 14: missingElement = EARTH; break; + default: rainbowInfluence = false; break; + } + } + // Add Normal Monster. No checks needed except cost - for (m = 0; m < availableMonstersSize && monsterReference[availableMonsters[m]].cost < remainingFollowers; m++) { - // In case of a draw this could casue problems if no more suitable units are available - if (!removeUseless || instance.monsterUsefulLast[availableMonsters[m]]) { + for (m = 0; m < availableMonstersSize && monsterReference[aMonsters[m]].cost <= remainingFollowers; m++) { + // In case of a draw this could cause problems if no more suitable units are available + if (!removeUseless || instance.monsterUsefulLast[aMonsters[m]] || friendsInfluence || (rainbowInfluence && monsterReference[aMonsters[m]].element == missingElement) || instance.targetSize == oldHeroArmies[i].lastFightData.monstersLost) { newHeroArmies.push_back(oldHeroArmies[i]); - newHeroArmies.back().add(availableMonsters[m]); - newHeroArmies.back().lastFightData.valid = !instanceInvalid && - !friendsInfluence && - !rainbowInfluence && - !invalidSkill; + newHeroArmies.back().add(aMonsters[m]); + newHeroArmies.back().lastFightData.valid = !instanceInvalid && + !invalidSkill && + !friendsInfluence && + !boozeInfluence && + !(rainbowInfluence && monsterReference[aMonsters[m]].element == missingElement); + } } // Add Hero. Check if hero was used before. for (m = 0; m < availableHeroesSize; m++) { if (!usedHeroes[availableHeroes[m]]) { - if (!removeUseless || instance.monsterUsefulLast[availableHeroes[m]]) { + if (!removeUseless || instance.monsterUsefulLast[availableHeroes[m]] || (rainbowInfluence && monsterReference[aMonsters[m]].element == missingElement) || instance.targetSize == oldHeroArmies[i].lastFightData.monstersLost) { newHeroArmies.push_back(oldHeroArmies[i]); newHeroArmies.back().add(availableHeroes[m]); - newHeroArmies.back().lastFightData.valid = !instanceInvalid && - !monsterReference[availableHeroes[m]].skill.violatesFightResults && - !rainbowInfluence && - !(monsterReference[availableHeroes[m]].skill.skillType == DAMPEN && instance.hasAoe) && - !invalidSkill; + newHeroArmies.back().lastFightData.valid = !instanceInvalid && + !invalidSkill && + !monsterReference[availableHeroes[m]].skill.violatesFightResults && + !boozeInfluence && + !(rainbowInfluence && monsterReference[aMonsters[m]].element == missingElement) && + !(monsterReference[availableHeroes[m]].skill.skillType == DAMPEN && instance.hasAoe); + } } // Clean up for the next army @@ -146,20 +189,20 @@ void calculateDominance(Instance & instance, bool optimizable, size_t i, j, si, sj; size_t pureMonsterArmiesSize = pureMonsterArmies.size(); size_t heroMonsterArmiesSize = heroMonsterArmies.size(); - + FollowerCount leftFollowerCost; FightResult * currentFightResult; - + // First Check dominance for non-Hero setups interface.timedOutput("Calculating Dominance for non-heroes... ", DETAILED_OUTPUT, 1, firstDominance == armySize); - + // Preselection based on the information that no monster can beat 2 monsters alone if optimizable is true if (armySize == (instance.maxCombatants - 1) && optimizable) { // Must be optimizable and the last expansion for (i = 0; i < pureMonsterArmiesSize; i++) { pureMonsterArmies[i].lastFightData.dominated = pureMonsterArmies[i].lastFightData.monstersLost < (int) (instance.targetSize - 2); } } - + sort(pureMonsterArmies.begin(), pureMonsterArmies.end(), hasFewerFollowers); for (i = 0; i < pureMonsterArmiesSize; i++) { leftFollowerCost = pureMonsterArmies[i].followerCost; @@ -171,8 +214,8 @@ void calculateDominance(Instance & instance, bool optimizable, // Another pureResults got farther with a less costly lineup for (j = i+1; j < pureMonsterArmiesSize; j++) { if (leftFollowerCost < pureMonsterArmies[j].followerCost) { - break; - } else if (*currentFightResult <= pureMonsterArmies[j].lastFightData) { // currentFightResult has more followers implicitly + break; + } else if (*currentFightResult <= pureMonsterArmies[j].lastFightData) { // currentFightResult has more followers implicitly currentFightResult->dominated = true; break; } @@ -185,28 +228,28 @@ void calculateDominance(Instance & instance, bool optimizable, if (armySize == (instance.maxCombatants - 1) && optimizable) { // Must be optimizable and the last expansion for (i = 0; i < heroMonsterArmiesSize; i++) { currentFightResult = &heroMonsterArmies[i].lastFightData; - + currentFightResult->dominated = currentFightResult->rightAoeDamage == 0 && // make sure there is no interference to the optimized calculation - currentFightResult->monstersLost < (int) (instance.targetSize - 2); // Army left at least 2 enemies alive + currentFightResult->monstersLost < (int) (instance.targetSize - 2); // Army left at least 2 enemies alive (this is actually checking if 3 alive) } } - + sort(heroMonsterArmies.begin(), heroMonsterArmies.end(), hasFewerFollowers); - + vector leftMonsterSet; leftMonsterSet.resize(monsterReference.size()); size_t leftMonsterSetSize = leftMonsterSet.size(); bool usedHeroSubset; for (i = 0; i < leftMonsterSetSize; i++) { // prepare monsterlist leftMonsterSet[i] = monsterReference[i].rarity != NO_HERO; // Normal Monsters are true by default } - + for (i = 0; i < heroMonsterArmiesSize; i++) { leftFollowerCost = heroMonsterArmies[i].followerCost; currentFightResult = &heroMonsterArmies[i].lastFightData; if (currentFightResult->dominated || leftFollowerCost > instance.followerUpperBound) { break; // All dominated results are in the back } - + for (si = 0; si < armySize; si++) { leftMonsterSet[heroMonsterArmies[i].monsters[si]] = true; // Add lefts monsters to set } @@ -226,14 +269,15 @@ void calculateDominance(Instance & instance, bool optimizable, } } if (usedHeroSubset) { + // even with a strict subset, order is important, so pruning a solution can result in failure to find any solution currentFightResult->dominated = true; break; - } + } } } } // Clean up monster set for next iteration - for (si = 0; si < armySize; si++) { + for (si = 0; si < armySize; si++) { leftMonsterSet[heroMonsterArmies[i].monsters[si]] = monsterReference[heroMonsterArmies[i].monsters[si]].rarity == NO_HERO; // Remove only heroes from the set } } @@ -247,9 +291,9 @@ void getQuickSolutions(Instance & instance) { vector greedyHeroes; vector greedyTemp; bool invalid = false; - + interface.outputMessage("Trying to find solutions greedily...", DETAILED_OUTPUT); - + // Create Army that kills as many monsters as the army is big if (instance.targetSize <= instance.maxCombatants) { for (size_t i = 0; i < instance.maxCombatants; i++) { @@ -263,13 +307,13 @@ void getQuickSolutions(Instance & instance) { } invalid = greedy.size() < instance.maxCombatants; // if true it didnt find a monster that drew position i } - + if (!invalid) { if (instance.followerUpperBound > tempArmy.followerCost) { instance.bestSolution = tempArmy; instance.followerUpperBound = tempArmy.followerCost; } - + // Try to replace monsters in the setup with heroes to save followers greedyHeroes = greedy; for (size_t m = 0; m < availableHeroes.size(); m++) { @@ -292,20 +336,40 @@ void getQuickSolutions(Instance & instance) { } } +void threadSolve(Instance instance, vector pureBranchArmies, vector heroBranchArmies, vector aMonsters, promise && p) { + int armySize = pureBranchArmies.empty() ? heroBranchArmies[0].monsterAmount : pureBranchArmies[0].monsterAmount; + instance.totalFightsSimulated = 0; + vector tempArmies, pureBranchArmies2, heroBranchArmies2; + expand(pureBranchArmies2, heroBranchArmies2, pureBranchArmies, heroBranchArmies, armySize, instance, aMonsters); + simulateMultipleFights(pureBranchArmies2, instance, aMonsters); + simulateMultipleFights(heroBranchArmies2, instance, aMonsters); + if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && (config.stopFirstSolution || instance.bestSolution.followerCost == 0)) { + p.set_value(instance); + return; + } + expand(tempArmies, tempArmies, pureBranchArmies2, heroBranchArmies2, armySize + 1, instance, aMonsters); + simulateMultipleFights(tempArmies, instance, aMonsters); + + p.set_value(instance); + return; +} + // Main method for solving an instance. void solveInstance(Instance & instance, size_t firstDominance) { Army tempArmy; time_t startTime; size_t i; - // Get first Upper limit on followers with agreedy algorithm - if (instance.maxCombatants > ARMY_MAX_BRUTEFORCEABLE_SIZE) { - getQuickSolutions(instance); - } - + // Get first Upper limit on followers with a greedy algorithm +// if (instance.maxCombatants > ARMY_MAX_BRUTEFORCEABLE_SIZE) { +// getQuickSolutions(instance); +// } + // Fill two vectors with armies each containing exactly one unique available hero or monster vector pureMonsterArmies; + pureMonsterArmies.reserve(availableMonsters.size()); vector heroMonsterArmies; + heroMonsterArmies.reserve(availableHeroes.size()); for (i = 0; i < availableMonsters.size(); i++) { if (monsterReference[availableMonsters[i]].cost <= instance.followerUpperBound) { pureMonsterArmies.push_back(Army( {availableMonsters[i]} )); @@ -314,51 +378,51 @@ void solveInstance(Instance & instance, size_t firstDominance) { for (i = 0; i < availableHeroes.size(); i++) { // Ignore checking for Hero Cost heroMonsterArmies.push_back(Army( {availableHeroes[i]} )); } - + // Check if a single monster can beat the last two monsters of the target. If not, solutions that can only beat n-2 monsters need not be expanded later - bool optimizable = (instance.targetSize > ARMY_MAX_BRUTEFORCEABLE_SIZE && instance.targetSize > 3); - if (optimizable) { - tempArmy = Army({instance.target.monsters[instance.targetSize - 2], instance.target.monsters[instance.targetSize - 1]}); // Make an army from the last two monsters - } - if (optimizable) { // Check with normal Mobs - for (i = 0; i < pureMonsterArmies.size(); i++) { - if (simulateFight(pureMonsterArmies[i], tempArmy)) { // Monster won the fight - optimizable = false; - break; - } - } - } - if (optimizable) { // Check with Heroes - for (i = 0; i < heroMonsterArmies.size(); i++) { - if (simulateFight(heroMonsterArmies[i], tempArmy)) { // Hero won the fight - optimizable = false; - break; - } - } - } +// bool optimizable = (instance.targetSize > ARMY_MAX_BRUTEFORCEABLE_SIZE && instance.targetSize > 3); +// if (optimizable) { +// tempArmy = Army({instance.target.monsters[instance.targetSize - 2], instance.target.monsters[instance.targetSize - 1]}); // Make an army from the last two monsters +// } +// if (optimizable) { // Check with normal Mobs +// for (i = 0; i < pureMonsterArmies.size(); i++) { +// if (simulateFight(pureMonsterArmies[i], tempArmy)) { // Monster won the fight +// optimizable = false; +// break; +// } +// } +// } +// if (optimizable) { // Check with Heroes +// for (i = 0; i < heroMonsterArmies.size(); i++) { +// if (simulateFight(heroMonsterArmies[i], tempArmy)) { // Hero won the fight +// optimizable = false; +// break; +// } +// } +// } // Run the Bruteforce Loop startTime = time(NULL); for (size_t armySize = 1; armySize <= instance.maxCombatants; armySize++) { // Output Debug Information interface.outputMessage("Starting loop for armies of size " + to_string(armySize), BASIC_OUTPUT); - + // Run Fights for non-Hero setups interface.timedOutput("Simulating " + to_string(pureMonsterArmies.size()) + " non-hero Fights... ", DETAILED_OUTPUT, 1, true); simulateMultipleFights(pureMonsterArmies, instance); - + // Run fights for setups with heroes interface.timedOutput("Simulating " + to_string(heroMonsterArmies.size()) + " hero Fights... ", DETAILED_OUTPUT, 1); simulateMultipleFights(heroMonsterArmies, instance); - + // If we have a valid solution with 0 followers there is no need to continue - if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && instance.bestSolution.followerCost == 0) { break; } - + if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && (config.stopFirstSolution || instance.bestSolution.followerCost == 0)) { break; } + // Start Expansion routine if there is still room - if (armySize < instance.maxCombatants) { - // Manage output format + if (armySize < instance.maxCombatants) { + // Manage output format if (armySize == firstDominance && config.outputLevel == BASIC_OUTPUT && config.autoAdjustOutputLevel) { - config.outputLevel = DETAILED_OUTPUT; // Switch output level after pure brutefore is exhausted + config.outputLevel = DETAILED_OUTPUT; // Switch output level after pure bruteforce is exhausted } if (armySize == firstDominance) { if (!config.autoAdjustOutputLevel) { @@ -369,31 +433,121 @@ void solveInstance(Instance & instance, size_t firstDominance) { interface.outputMessage("Best Solution so far:", DETAILED_OUTPUT); interface.outputMessage(instance.bestSolution.toString(), DETAILED_OUTPUT, 1); if (instance.hasWorldBoss) { - interface.outputMessage("Damage Done: " + to_string(WORLDBOSS_HEALTH - instance.lowestBossHealth), DETAILED_OUTPUT, 1); + interface.outputMessage("Damage Done: " + numberWithSeparators(WORLDBOSS_HEALTH - instance.lowestBossHealth), DETAILED_OUTPUT, 1); } } else { interface.outputMessage("Could not find a solution yet!", DETAILED_OUTPUT); } + instance.calculationTime = time(NULL) - startTime; + if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && (config.stopFirstSolution || instance.bestSolution.followerCost == 0)) break; if (!iomanager.askYesNoQuestion("Continue calculation?", DETAILED_OUTPUT, TOKENS.YES)) {return;} startTime = time(NULL); interface.outputMessage("\nPreparing to work on loop for armies of size " + to_string(armySize+1), DETAILED_OUTPUT); interface.outputMessage("Currently considering " + to_string(pureMonsterArmies.size()) + " normal and " + to_string(heroMonsterArmies.size()) + " hero armies.", DETAILED_OUTPUT); } - + // Calculate which results are strictly better than others (dominance) - if (firstDominance <= armySize && availableMonsters.size() > 0) { - calculateDominance(instance, optimizable, pureMonsterArmies, heroMonsterArmies, armySize, firstDominance); + // Reduces memory, but increases calculation time and can result in completely missing a correct solution +// if (firstDominance <= armySize && availableMonsters.size() > 0) { +// calculateDominance(instance, optimizable, pureMonsterArmies, heroMonsterArmies, armySize, firstDominance); +// } + + if (armySize < instance.maxCombatants - 2) { + // now we expand to add the next monster to all non-dominated armies + interface.timedOutput("Expanding Lineups by one... ", DETAILED_OUTPUT, 1); + vector nextPureArmies; + vector nextHeroArmies; + expand(nextPureArmies, nextHeroArmies, pureMonsterArmies, heroMonsterArmies, armySize, instance); + + interface.timedOutput("Moving Data... ", DETAILED_OUTPUT, 1); + pureMonsterArmies = move(nextPureArmies); + heroMonsterArmies = move(nextHeroArmies); + } + else { + // for the second to last expansion, expand and fight each lineups individually (or in small packets) to keep memory usage low + // some max length solutions will therefore be seen before other solutions of one lower size + // TODO: refactor this to get rid of code repetition someday + interface.finishTimedOutput(DETAILED_OUTPUT); + interface.outputMessage("Starting loop for armies of size " + to_string(armySize + 1) + "+", BASIC_OUTPUT); + interface.timedOutput("Simulating fights by expanding Lineups one by one ...\n", DETAILED_OUTPUT, 1, true); + + sort(pureMonsterArmies.begin(), pureMonsterArmies.end(), isMoreEfficient); + sort(heroMonsterArmies.begin(), heroMonsterArmies.end(), isMoreEfficient); + + vector> futures; + futures.reserve(config.numThreads); + + vector pureBranchArmies, heroBranchArmies; + pureBranchArmies.reserve(config.branchwiseExpansionLimit); + heroBranchArmies.reserve(config.branchwiseExpansionLimit); + + for (size_t i = 0, j = 0; i < pureMonsterArmies.size() || j < heroMonsterArmies.size(); ) { + futures.clear(); + for (int k = 0; k < config.numThreads; k++) { + pureBranchArmies.clear(); + heroBranchArmies.clear(); + while (pureBranchArmies.size() + heroBranchArmies.size() < config.branchwiseExpansionLimit && (i < pureMonsterArmies.size() || j < heroMonsterArmies.size())) { + if (i < pureMonsterArmies.size()) { + if (instance.bestSolution.monsterAmount <= 0 || pureMonsterArmies[i].followerCost < instance.bestSolution.followerCost || instance.hasWorldBoss) { + pureBranchArmies.push_back(pureMonsterArmies[i++]); + } + else { + i++; + } + } + if (j < heroMonsterArmies.size()) { + if (instance.bestSolution.monsterAmount <= 0 || heroMonsterArmies[j].followerCost < instance.bestSolution.followerCost || instance.hasWorldBoss) { + heroBranchArmies.push_back(heroMonsterArmies[j++]); + } + else { + j++; + } + } + } + if (pureBranchArmies.empty() && heroBranchArmies.empty()) break; + if (config.numThreads > 1) { + promise p; + futures.emplace_back(p.get_future()); + thread t(&threadSolve, instance, pureBranchArmies, heroBranchArmies, availableMonsters, move(p)); + t.detach(); + } + else { + vector tempArmies, pureBranchArmies2, heroBranchArmies2; + expand(pureBranchArmies2, heroBranchArmies2, pureBranchArmies, heroBranchArmies, armySize, instance); + simulateMultipleFights(pureBranchArmies2, instance); + simulateMultipleFights(heroBranchArmies2, instance); + if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && (config.stopFirstSolution || instance.bestSolution.followerCost == 0)) break; + expand(tempArmies, tempArmies, pureBranchArmies2, heroBranchArmies2, armySize + 1, instance); + simulateMultipleFights(tempArmies, instance); + } + } + int fightsSimulated = instance.totalFightsSimulated; + bool newSolution = false; + for (future & f : futures) { + Instance result = f.get(); + fightsSimulated += result.totalFightsSimulated; + + if (!instance.hasWorldBoss) { + if (result.followerUpperBound < instance.followerUpperBound) { + instance = result; + newSolution = true; + } + } + else { + if (result.lowestBossHealth < instance.lowestBossHealth) instance = result; + } + } + if (newSolution) interface.outputMessage(instance.bestSolution.toString(), DETAILED_OUTPUT, 2); + + instance.totalFightsSimulated = fightsSimulated; + pruneAvailableMonsters(instance.followerUpperBound); + + if (!instance.hasWorldBoss && instance.bestSolution.monsterAmount > 0 && (config.stopFirstSolution || instance.bestSolution.followerCost == 0)) break; + } + + interface.finishTimedOutput(DETAILED_OUTPUT); + break; } - - // now we expand to add the next monster to all non-dominated armies - interface.timedOutput("Expanding Lineups by one... ", DETAILED_OUTPUT, 1); - vector nextPureArmies; - vector nextHeroArmies; - expand(nextPureArmies, nextHeroArmies, pureMonsterArmies, heroMonsterArmies, armySize, instance); - - interface.timedOutput("Moving Data... ", DETAILED_OUTPUT, 1); - pureMonsterArmies = move(nextPureArmies); - heroMonsterArmies = move(nextHeroArmies); } interface.finishTimedOutput(DETAILED_OUTPUT); } @@ -403,10 +557,11 @@ void solveInstance(Instance & instance, size_t firstDominance) { void outputSolution(Instance instance) { instance.bestSolution.lastFightData.valid = false; bool leftWins = simulateFight(instance.bestSolution, instance.target); // Sanity check on the solution + bool sane; sane = !instance.hasWorldBoss && (leftWins || instance.bestSolution.isEmpty()); sane |= instance.hasWorldBoss && instance.bestSolution.lastFightData.frontHealth == instance.lowestBossHealth; - + if (config.JSONOutput) { interface.outputMessage(makeJSONFromInstance(instance, sane), SOLUTION_OUTPUT); } else { @@ -420,7 +575,7 @@ int main(int argc, char** argv) { FollowerCount userFollowerUpperBound; vector instances; bool userWantsContinue; - + if (argc >= 3 && (string) argv[2] == "-server") { config.showQueries = false; config.ignoreQuestions = true; @@ -429,32 +584,37 @@ int main(int argc, char** argv) { config.ignoreExecutionHalt = true; config.allowConfig = false; } - - interface.outputMessage(welcomeMessage, NOTIFICATION_OUTPUT); + + interface.outputMessage(welcomeMessage + " v" + VERSION, NOTIFICATION_OUTPUT); interface.outputMessage(helpMessage + "\n", NOTIFICATION_OUTPUT); - + // Check if the user provided a filename to be used as an inputfile if (argc >= 2) { iomanager.loadInputFiles(argv[1]); } else { - iomanager.loadInputFiles(""); - } + iomanager.loadInputFiles(""); + } interface.outputMessage("", NOTIFICATION_OUTPUT); iomanager.getConfiguration(); - + // Initialize global Data initGameData(); - - // -------------------------------------------- Program Start -------------------------------------------- - + + // -------------------------------------------- Program Start -------------------------------------------- + if (config.individualBattles) { - interface.outputMessage("Simulating individual Figths", NOTIFICATION_OUTPUT); + interface.outputMessage("Simulating individual Fights", NOTIFICATION_OUTPUT); while (true) { Army left = iomanager.takeInstanceInput("Enter friendly lineup: ")[0].target; Army right = iomanager.takeInstanceInput("Enter hostile lineup: ")[0].target; bool leftWins = simulateFight(left, right, true); interface.outputMessage(to_string(leftWins) + " " + to_string(left.followerCost) + " " + to_string(right.followerCost), SOLUTION_OUTPUT); - + + if(config.showReplayStrings) { + interface.outputMessage("\nReplay:\n", SOLUTION_OUTPUT); + interface.outputMessage(makeBattleReplay(left, right), SOLUTION_OUTPUT); + } + if (!iomanager.askYesNoQuestion("Simulate another Fight?", NOTIFICATION_OUTPUT, TOKENS.NO)) { break; } @@ -465,7 +625,9 @@ int main(int argc, char** argv) { availableHeroes = iomanager.takeHerolevelInput(); int64_t minFollowerTemp = parseInt(iomanager.getResistantInput("Set a lower follower limit on monsters used: ", integer)[0]); int64_t maxFollowerTemp = parseInt(iomanager.getResistantInput("Set an upper follower limit that you want to use: ", integer)[0]); - + + if (maxFollowerTemp == 0) maxFollowerTemp = 1; // 0 will cause issues with finding solutions for pure hero armies + if (minFollowerTemp < 0) { minimumMonsterCost = numeric_limits::max(); } else { @@ -476,34 +638,39 @@ int main(int argc, char** argv) { } else { userFollowerUpperBound = (FollowerCount) maxFollowerTemp; // should not overflow due to parseInt } - - // Fill monster arrays with relevant monsters - filterMonsterData(minimumMonsterCost, userFollowerUpperBound); - + do { + // Fill monster arrays with relevant monsters + filterMonsterData(minimumMonsterCost, userFollowerUpperBound); + instances = iomanager.takeInstanceInput("Enter Enemy Lineup(s): "); - + interface.outputMessage("\nCalculating with " + to_string(availableMonsters.size()) + " available Monsters and " + to_string(availableHeroes.size()) + " enabled Heroes.", BASIC_OUTPUT); - + if (config.outputLevel == DETAILED_OUTPUT && config.autoAdjustOutputLevel) { config.outputLevel = BASIC_OUTPUT; } if (config.outputLevel == BASIC_OUTPUT && instances.size() > 1 && config.autoAdjustOutputLevel) { config.outputLevel = SOLUTION_OUTPUT; } - + for (size_t i = 0; i < instances.size(); i++) { - totalFightsSimulated = &(instances[i].totalFightsSimulated); - instances[i].followerUpperBound = userFollowerUpperBound; - + + // Fill monster arrays with relevant monsters, need to repeat for each instance due to pruning + filterMonsterData(minimumMonsterCost, userFollowerUpperBound); + solveInstance(instances[i], config.firstDominance); outputSolution(instances[i]); } - userWantsContinue = iomanager.askYesNoQuestion("Do you want to calculate more lineups?", NOTIFICATION_OUTPUT, TOKENS.NO); + if(config.skipContinue) { + userWantsContinue = true; + } else { + userWantsContinue = iomanager.askYesNoQuestion("Do you want to calculate more lineups?", NOTIFICATION_OUTPUT, TOKENS.NO); + } } while (userWantsContinue); - + interface.outputMessage("", NOTIFICATION_OUTPUT); interface.haltExecution(); return EXIT_SUCCESS; -} \ No newline at end of file +}