GameRandomizer.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /*
  2. * GameRandomizer.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "GameRandomizer.h"
  12. #include "IGameInfoCallback.h"
  13. #include "../../lib/CRandomGenerator.h"
  14. #include "../../lib/GameLibrary.h"
  15. #include "../../lib/CCreatureHandler.h"
  16. #include "../../lib/CSkillHandler.h"
  17. #include "../../lib/IGameSettings.h"
  18. #include "../../lib/entities/artifact/CArtHandler.h"
  19. #include "../../lib/entities/artifact/EArtifactClass.h"
  20. #include "../../lib/entities/hero/CHeroClass.h"
  21. #include "../../lib/mapObjects/CGHeroInstance.h"
  22. VCMI_LIB_NAMESPACE_BEGIN
  23. bool RandomizationBias::roll(vstd::RNG & generator, int successChance, int totalWeight, int biasValue)
  24. {
  25. assert(successChance > 0);
  26. assert(totalWeight >= successChance);
  27. int failChance = totalWeight - successChance;
  28. int newRoll = generator.nextInt(1, totalWeight);
  29. // accumulated bias is stored as premultiplied to avoid precision loss on division
  30. // so multiply everything else in equation to compensate
  31. // precision loss is small, and generally insignificant, but better to play it safe
  32. bool success = newRoll * totalWeight - accumulatedBias <= successChance * totalWeight;
  33. if(success)
  34. accumulatedBias -= failChance * biasValue;
  35. else
  36. accumulatedBias += successChance * biasValue;
  37. return success;
  38. }
  39. RandomGeneratorWithBias::RandomGeneratorWithBias(int seed)
  40. : generator(seed)
  41. {
  42. }
  43. bool RandomGeneratorWithBias::roll(int successChance, int totalWeight, int biasValue)
  44. {
  45. return bias.roll(generator, successChance, totalWeight, biasValue);
  46. }
  47. GameRandomizer::GameRandomizer(const IGameInfoCallback & gameInfo)
  48. : gameInfo(gameInfo)
  49. {
  50. }
  51. GameRandomizer::~GameRandomizer() = default;
  52. bool GameRandomizer::rollMoraleLuck(std::map<ObjectInstanceID, RandomGeneratorWithBias> & seeds, ObjectInstanceID actor, int moraleLuckValue, EGameSettings biasValueSetting, EGameSettings diceSize, EGameSettings diceWeights)
  53. {
  54. assert(moraleLuckValue > 0);
  55. auto goodLuckChanceVector = gameInfo.getSettings().getVector(diceWeights);
  56. int luckDiceSize = gameInfo.getSettings().getInteger(diceSize);
  57. int biasValue = gameInfo.getSettings().getInteger(biasValueSetting);
  58. size_t chanceIndex = std::min<size_t>(goodLuckChanceVector.size(), moraleLuckValue) - 1; // array index, so 0-indexed
  59. if(!seeds.count(actor))
  60. seeds.emplace(actor, getDefault().nextInt());
  61. if(goodLuckChanceVector.size() == 0)
  62. return false;
  63. return seeds.at(actor).roll(goodLuckChanceVector[chanceIndex], luckDiceSize, biasValue);
  64. }
  65. bool GameRandomizer::rollGoodMorale(ObjectInstanceID actor, int moraleValue)
  66. {
  67. return rollMoraleLuck(goodMoraleSeed, actor, moraleValue, EGameSettings::COMBAT_MORALE_BIAS, EGameSettings::COMBAT_MORALE_DICE_SIZE, EGameSettings::COMBAT_GOOD_MORALE_CHANCE);
  68. }
  69. bool GameRandomizer::rollBadMorale(ObjectInstanceID actor, int moraleValue)
  70. {
  71. return rollMoraleLuck(badMoraleSeed, actor, moraleValue, EGameSettings::COMBAT_MORALE_BIAS, EGameSettings::COMBAT_MORALE_DICE_SIZE, EGameSettings::COMBAT_BAD_MORALE_CHANCE);
  72. }
  73. bool GameRandomizer::rollGoodLuck(ObjectInstanceID actor, int luckValue)
  74. {
  75. return rollMoraleLuck(goodLuckSeed, actor, luckValue, EGameSettings::COMBAT_LUCK_BIAS, EGameSettings::COMBAT_LUCK_DICE_SIZE, EGameSettings::COMBAT_GOOD_LUCK_CHANCE);
  76. }
  77. bool GameRandomizer::rollBadLuck(ObjectInstanceID actor, int luckValue)
  78. {
  79. return rollMoraleLuck(badLuckSeed, actor, luckValue, EGameSettings::COMBAT_LUCK_BIAS, EGameSettings::COMBAT_LUCK_DICE_SIZE, EGameSettings::COMBAT_BAD_LUCK_CHANCE);
  80. }
  81. bool GameRandomizer::rollCombatAbility(ObjectInstanceID actor, int percentageChance)
  82. {
  83. if(!combatAbilitySeed.count(actor))
  84. combatAbilitySeed.emplace(actor, getDefault().nextInt());
  85. if(percentageChance <= 0)
  86. return false;
  87. if(percentageChance >= 100)
  88. return true;
  89. int biasValue = gameInfo.getSettings().getInteger(EGameSettings::COMBAT_ABILITY_BIAS);
  90. return combatAbilitySeed.at(actor).roll(percentageChance, 100, biasValue);
  91. }
  92. CreatureID GameRandomizer::rollCreature()
  93. {
  94. std::vector<CreatureID> allowed;
  95. for(const auto & creatureID : LIBRARY->creh->getDefaultAllowed())
  96. {
  97. const auto * creaturePtr = creatureID.toCreature();
  98. if(!creaturePtr->excludeFromRandomization)
  99. allowed.push_back(creaturePtr->getId());
  100. }
  101. if(allowed.empty())
  102. throw std::runtime_error("Cannot pick a random creature!");
  103. return *RandomGeneratorUtil::nextItem(allowed, getDefault());
  104. }
  105. CreatureID GameRandomizer::rollCreature(int tier)
  106. {
  107. std::vector<CreatureID> allowed;
  108. for(const auto & creatureID : LIBRARY->creh->getDefaultAllowed())
  109. {
  110. const auto * creaturePtr = creatureID.toCreature();
  111. if(creaturePtr->excludeFromRandomization)
  112. continue;
  113. if(creaturePtr->getLevel() == tier)
  114. allowed.push_back(creaturePtr->getId());
  115. }
  116. if(allowed.empty())
  117. throw std::runtime_error("Cannot pick a random creature!");
  118. return *RandomGeneratorUtil::nextItem(allowed, getDefault());
  119. }
  120. ArtifactID GameRandomizer::rollArtifact()
  121. {
  122. std::set<ArtifactID> potentialPicks;
  123. for(const auto & artifactID : LIBRARY->arth->getDefaultAllowed())
  124. {
  125. if(!LIBRARY->arth->legalArtifact(artifactID))
  126. continue;
  127. potentialPicks.insert(artifactID);
  128. }
  129. return rollArtifact(potentialPicks);
  130. }
  131. ArtifactID GameRandomizer::rollArtifact(EArtifactClass type)
  132. {
  133. std::set<ArtifactID> potentialPicks;
  134. for(const auto & artifactID : LIBRARY->arth->getDefaultAllowed())
  135. {
  136. if(!LIBRARY->arth->legalArtifact(artifactID))
  137. continue;
  138. if(!gameInfo.isAllowed(artifactID))
  139. continue;
  140. const auto * artifact = artifactID.toArtifact();
  141. if(type != artifact->aClass)
  142. continue;
  143. potentialPicks.insert(artifactID);
  144. }
  145. return rollArtifact(potentialPicks);
  146. }
  147. ArtifactID GameRandomizer::rollArtifact(std::set<ArtifactID> potentialPicks)
  148. {
  149. // No allowed artifacts at all - give Grail - this can't be banned (hopefully)
  150. // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior
  151. if(potentialPicks.empty())
  152. {
  153. logGlobal->warn("Failed to find artifact that matches requested parameters!");
  154. return ArtifactID::GRAIL;
  155. }
  156. // Find how many times least used artifacts were picked by randomizer
  157. int leastUsedTimes = std::numeric_limits<int>::max();
  158. for(const auto & artifact : potentialPicks)
  159. if(allocatedArtifacts[artifact] < leastUsedTimes)
  160. leastUsedTimes = allocatedArtifacts[artifact];
  161. // Pick all artifacts that were used least number of times
  162. std::set<ArtifactID> preferredPicks;
  163. for(const auto & artifact : potentialPicks)
  164. if(allocatedArtifacts[artifact] == leastUsedTimes)
  165. preferredPicks.insert(artifact);
  166. assert(!preferredPicks.empty());
  167. ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, getDefault());
  168. allocatedArtifacts[artID] += 1; // record +1 more usage
  169. return artID;
  170. }
  171. std::vector<ArtifactID> GameRandomizer::rollMarketArtifactSet()
  172. {
  173. return {
  174. rollArtifact(EArtifactClass::ART_TREASURE),
  175. rollArtifact(EArtifactClass::ART_TREASURE),
  176. rollArtifact(EArtifactClass::ART_TREASURE),
  177. rollArtifact(EArtifactClass::ART_MINOR),
  178. rollArtifact(EArtifactClass::ART_MINOR),
  179. rollArtifact(EArtifactClass::ART_MINOR),
  180. rollArtifact(EArtifactClass::ART_MAJOR)
  181. };
  182. }
  183. vstd::RNG & GameRandomizer::getDefault()
  184. {
  185. return globalRandomNumberGenerator;
  186. }
  187. void GameRandomizer::setSeed(int newSeed)
  188. {
  189. globalRandomNumberGenerator.setSeed(newSeed);
  190. }
  191. PrimarySkill GameRandomizer::rollPrimarySkillForLevelup(const CGHeroInstance * hero)
  192. {
  193. if(!heroSkillSeed.count(hero->getHeroTypeID()))
  194. heroSkillSeed.emplace(hero->getHeroTypeID(), getDefault().nextInt());
  195. const bool isLowLevelHero = hero->level < GameConstants::HERO_HIGH_LEVEL;
  196. const auto & skillChances = isLowLevelHero ? hero->getHeroClass()->primarySkillLowLevel : hero->getHeroClass()->primarySkillHighLevel;
  197. auto & heroRng = heroSkillSeed.at(hero->getHeroTypeID());
  198. if(hero->isCampaignYog())
  199. {
  200. // Yog can only receive Attack or Defence on level-up
  201. std::vector<int> yogChances = {skillChances[0], skillChances[1]};
  202. return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(yogChances, heroRng.seed));
  203. }
  204. return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(skillChances, heroRng.seed));
  205. }
  206. SecondarySkill GameRandomizer::rollSecondarySkillForLevelup(const CGHeroInstance * hero, const std::set<SecondarySkill> & options)
  207. {
  208. if(!heroSkillSeed.count(hero->getHeroTypeID()))
  209. heroSkillSeed.emplace(hero->getHeroTypeID(), getDefault().nextInt());
  210. auto & heroRng = heroSkillSeed.at(hero->getHeroTypeID());
  211. auto getObligatorySkills = [](CSkill::Obligatory obl)
  212. {
  213. std::set<SecondarySkill> obligatory;
  214. for(auto i = 0; i < LIBRARY->skillh->size(); i++)
  215. if((*LIBRARY->skillh)[SecondarySkill(i)]->obligatory(obl))
  216. obligatory.insert(i); //Always return all obligatory skills
  217. return obligatory;
  218. };
  219. auto intersect = [](const std::set<SecondarySkill> & left, const std::set<SecondarySkill> & right)
  220. {
  221. std::set<SecondarySkill> intersect;
  222. std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), std::inserter(intersect, intersect.begin()));
  223. return intersect;
  224. };
  225. std::set<SecondarySkill> wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR);
  226. std::set<SecondarySkill> schoolList = getObligatorySkills(CSkill::Obligatory::MINOR);
  227. bool wantsWisdom = heroRng.wisdomCounter + 1 >= hero->maxlevelsToWisdom();
  228. bool wantsSchool = heroRng.magicSchoolCounter + 1 >= hero->maxlevelsToMagicSchool();
  229. bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty();
  230. bool selectSchool = wantsSchool && !intersect(options, schoolList).empty();
  231. std::set<SecondarySkill> actualCandidates;
  232. if(selectWisdom)
  233. actualCandidates = intersect(options, wisdomList);
  234. else if(selectSchool)
  235. actualCandidates = intersect(options, schoolList);
  236. else
  237. actualCandidates = options;
  238. assert(!actualCandidates.empty());
  239. std::vector<int> weights;
  240. std::vector<SecondarySkill> skills;
  241. for(const auto & possible : actualCandidates)
  242. {
  243. skills.push_back(possible);
  244. if(hero->getHeroClass()->secSkillProbability.count(possible) != 0)
  245. {
  246. int weight = hero->getHeroClass()->secSkillProbability.at(possible);
  247. weights.push_back(std::max(1, weight));
  248. }
  249. else
  250. weights.push_back(1); // H3 behavior - banned skills have minimal (1) chance to be picked
  251. }
  252. int selectedIndex = RandomGeneratorUtil::nextItemWeighted(weights, heroRng.seed);
  253. SecondarySkill selectedSkill = skills.at(selectedIndex);
  254. //deterministic secondary skills
  255. ++heroRng.magicSchoolCounter;
  256. ++heroRng.wisdomCounter;
  257. if((*LIBRARY->skillh)[selectedSkill]->obligatory(CSkill::Obligatory::MAJOR))
  258. heroRng.wisdomCounter = 0;
  259. if((*LIBRARY->skillh)[selectedSkill]->obligatory(CSkill::Obligatory::MINOR))
  260. heroRng.magicSchoolCounter = 0;
  261. return selectedSkill;
  262. }
  263. VCMI_LIB_NAMESPACE_END