1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-05-21 22:33:43 +02:00
vcmi/lib/callback/GameRandomizer.cpp
2025-05-19 18:51:42 +03:00

331 lines
10 KiB
C++

/*
* GameRandomizer.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "GameRandomizer.h"
#include "IGameInfoCallback.h"
#include "../../lib/CRandomGenerator.h"
#include "../../lib/GameLibrary.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/CSkillHandler.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/entities/artifact/CArtHandler.h"
#include "../../lib/entities/artifact/EArtifactClass.h"
#include "../../lib/entities/hero/CHeroClass.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
VCMI_LIB_NAMESPACE_BEGIN
BiasedRandomizer::BiasedRandomizer(int seed)
: seed(seed)
{
}
bool BiasedRandomizer::roll(int successChance, int totalWeight, int biasValue)
{
assert(successChance > 0);
assert(totalWeight >= successChance);
int failChance = totalWeight - successChance;
int newRoll = seed.nextInt(1,totalWeight);
// accumulated bias is stored as premultiplied to avoid precision loss on division
// so multiply everything else in equation to compensate
// precision loss is small, and generally insignificant, but better to play it safe
bool success = newRoll * totalWeight - accumulatedBias <= successChance * totalWeight;
if (success)
accumulatedBias -= failChance * biasValue;
else
accumulatedBias += successChance * biasValue;
return success;
}
GameRandomizer::GameRandomizer(const IGameInfoCallback & gameInfo)
: gameInfo(gameInfo)
{
}
GameRandomizer::~GameRandomizer() = default;
bool GameRandomizer::rollMoraleLuck(std::map<ObjectInstanceID, BiasedRandomizer> & seeds, ObjectInstanceID actor, int moraleLuckValue, EGameSettings diceSize, EGameSettings diceWeights)
{
assert(moraleLuckValue > 0);
auto goodLuckChanceVector = gameInfo.getSettings().getVector(diceWeights);
int luckDiceSize = gameInfo.getSettings().getInteger(diceSize);
size_t chanceIndex = std::min<size_t>(goodLuckChanceVector.size(), moraleLuckValue) - 1; // array index, so 0-indexed
if (!seeds.count(actor))
seeds.emplace(actor, getDefault().nextInt());
if(goodLuckChanceVector.size() == 0)
return false;
return seeds.at(actor).roll(goodLuckChanceVector[chanceIndex], luckDiceSize, biasValueLuckMorale);
}
bool GameRandomizer::rollGoodMorale(ObjectInstanceID actor, int moraleValue)
{
return rollMoraleLuck(goodMoraleSeed, actor, moraleValue, EGameSettings::COMBAT_MORALE_DICE_SIZE, EGameSettings::COMBAT_GOOD_MORALE_CHANCE);
}
bool GameRandomizer::rollBadMorale(ObjectInstanceID actor, int moraleValue)
{
return rollMoraleLuck(badMoraleSeed, actor, moraleValue, EGameSettings::COMBAT_MORALE_DICE_SIZE, EGameSettings::COMBAT_BAD_MORALE_CHANCE);
}
bool GameRandomizer::rollGoodLuck(ObjectInstanceID actor, int luckValue)
{
return rollMoraleLuck(goodLuckSeed, actor, luckValue, EGameSettings::COMBAT_LUCK_DICE_SIZE, EGameSettings::COMBAT_GOOD_LUCK_CHANCE);
}
bool GameRandomizer::rollBadLuck(ObjectInstanceID actor, int luckValue)
{
return rollMoraleLuck(badLuckSeed, actor, luckValue, EGameSettings::COMBAT_LUCK_DICE_SIZE, EGameSettings::COMBAT_BAD_LUCK_CHANCE);
}
bool GameRandomizer::rollCombatAbility(ObjectInstanceID actor, int percentageChance)
{
if (!combatAbilitySeed.count(actor))
combatAbilitySeed.emplace(actor, getDefault().nextInt());
if (percentageChance <= 0)
return false;
if (percentageChance >= 100)
return true;
return combatAbilitySeed.at(actor).roll(percentageChance, 100, biasValueAbility);
}
//HeroTypeID GameRandomizer::rollHero(PlayerColor player, FactionID faction)
//{
//
//}
CreatureID GameRandomizer::rollCreature()
{
std::vector<CreatureID> allowed;
for(const auto & creatureID : LIBRARY->creh->getDefaultAllowed())
{
const auto * creaturePtr = creatureID.toCreature();
if(!creaturePtr->excludeFromRandomization)
allowed.push_back(creaturePtr->getId());
}
if(allowed.empty())
throw std::runtime_error("Cannot pick a random creature!");
return *RandomGeneratorUtil::nextItem(allowed, getDefault());
}
CreatureID GameRandomizer::rollCreature(int tier)
{
std::vector<CreatureID> allowed;
for(const auto & creatureID : LIBRARY->creh->getDefaultAllowed())
{
const auto * creaturePtr = creatureID.toCreature();
if(creaturePtr->excludeFromRandomization)
continue;
if(creaturePtr->getLevel() == tier)
allowed.push_back(creaturePtr->getId());
}
if(allowed.empty())
throw std::runtime_error("Cannot pick a random creature!");
return *RandomGeneratorUtil::nextItem(allowed, getDefault());
}
ArtifactID GameRandomizer::rollArtifact()
{
std::set<ArtifactID> potentialPicks;
for(const auto & artifactID : LIBRARY->arth->getDefaultAllowed())
{
if(!LIBRARY->arth->legalArtifact(artifactID))
continue;
potentialPicks.insert(artifactID);
}
return rollArtifact(potentialPicks);
}
ArtifactID GameRandomizer::rollArtifact(EArtifactClass type)
{
std::set<ArtifactID> potentialPicks;
for(const auto & artifactID : LIBRARY->arth->getDefaultAllowed())
{
if(!LIBRARY->arth->legalArtifact(artifactID))
continue;
if(!gameInfo.isAllowed(artifactID))
continue;
const auto * artifact = artifactID.toArtifact();
if(type != artifact->aClass)
continue;
potentialPicks.insert(artifactID);
}
return rollArtifact(potentialPicks);
}
ArtifactID GameRandomizer::rollArtifact(std::set<ArtifactID> potentialPicks)
{
// No allowed artifacts at all - give Grail - this can't be banned (hopefully)
// FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior
if(potentialPicks.empty())
{
logGlobal->warn("Failed to find artifact that matches requested parameters!");
return ArtifactID::GRAIL;
}
// Find how many times least used artifacts were picked by randomizer
int leastUsedTimes = std::numeric_limits<int>::max();
for(const auto & artifact : potentialPicks)
if(allocatedArtifacts[artifact] < leastUsedTimes)
leastUsedTimes = allocatedArtifacts[artifact];
// Pick all artifacts that were used least number of times
std::set<ArtifactID> preferredPicks;
for(const auto & artifact : potentialPicks)
if(allocatedArtifacts[artifact] == leastUsedTimes)
preferredPicks.insert(artifact);
assert(!preferredPicks.empty());
ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, getDefault());
allocatedArtifacts[artID] += 1; // record +1 more usage
return artID;
}
std::vector<ArtifactID> GameRandomizer::rollMarketArtifactSet()
{
std::vector<ArtifactID> out;
for(int j = 0; j < 3; j++)
out.push_back(rollArtifact(EArtifactClass::ART_TREASURE));
for(int j = 0; j < 3; j++)
out.push_back(rollArtifact(EArtifactClass::ART_MINOR));
out.push_back(rollArtifact(EArtifactClass::ART_MAJOR));
return out;
}
vstd::RNG & GameRandomizer::getDefault()
{
return globalRandomNumberGenerator;
}
void GameRandomizer::setSeed(int newSeed)
{
globalRandomNumberGenerator.setSeed(newSeed);
}
PrimarySkill GameRandomizer::rollPrimarySkillForLevelup(const CGHeroInstance * hero)
{
if (!heroSkillSeed.count(hero->getHeroTypeID()))
heroSkillSeed.emplace(hero->getHeroTypeID(), getDefault().nextInt());
const bool isLowLevelHero = hero->level < GameConstants::HERO_HIGH_LEVEL;
const auto & skillChances = isLowLevelHero ? hero->getHeroClass()->primarySkillLowLevel : hero->getHeroClass()->primarySkillHighLevel;
auto & heroRng = heroSkillSeed.at(hero->getHeroTypeID());
if(hero->isCampaignYog())
{
// Yog can only receive Attack or Defence on level-up
std::vector<int> yogChances = {skillChances[0], skillChances[1]};
return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(yogChances, heroRng.seed));
}
return static_cast<PrimarySkill>(RandomGeneratorUtil::nextItemWeighted(skillChances, heroRng.seed));
}
SecondarySkill GameRandomizer::rollSecondarySkillForLevelup(const CGHeroInstance * hero, const std::set<SecondarySkill> & options)
{
if (!heroSkillSeed.count(hero->getHeroTypeID()))
heroSkillSeed.emplace(hero->getHeroTypeID(), getDefault().nextInt());
auto & heroRng = heroSkillSeed.at(hero->getHeroTypeID());
auto getObligatorySkills = [](CSkill::Obligatory obl)
{
std::set<SecondarySkill> obligatory;
for(auto i = 0; i < LIBRARY->skillh->size(); i++)
if((*LIBRARY->skillh)[SecondarySkill(i)]->obligatory(obl))
obligatory.insert(i); //Always return all obligatory skills
return obligatory;
};
auto intersect = [](const std::set<SecondarySkill> & left, const std::set<SecondarySkill> & right)
{
std::set<SecondarySkill> intersect;
std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), std::inserter(intersect, intersect.begin()));
return intersect;
};
std::set<SecondarySkill> wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR);
std::set<SecondarySkill> schoolList = getObligatorySkills(CSkill::Obligatory::MINOR);
bool wantsWisdom = heroRng.wisdomCounter + 1 >= hero->maxlevelsToWisdom();
bool wantsSchool = heroRng.magicSchoolCounter + 1 >= hero->maxlevelsToMagicSchool();
bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty();
bool selectSchool = wantsSchool && !intersect(options, schoolList).empty();
std::set<SecondarySkill> actualCandidates;
if(selectWisdom)
actualCandidates = intersect(options, wisdomList);
else if(selectSchool)
actualCandidates = intersect(options, schoolList);
else
actualCandidates = options;
assert(!actualCandidates.empty());
std::vector<int> weights;
std::vector<SecondarySkill> skills;
for(const auto & possible : actualCandidates)
{
skills.push_back(possible);
if(hero->getHeroClass()->secSkillProbability.count(possible) != 0)
{
int weight = hero->getHeroClass()->secSkillProbability.at(possible);
weights.push_back(std::max(1, weight));
}
else
weights.push_back(1); // H3 behavior - banned skills have minimal (1) chance to be picked
}
int selectedIndex = RandomGeneratorUtil::nextItemWeighted(weights, heroRng.seed);
SecondarySkill selectedSkill = skills.at(selectedIndex);
//deterministic secondary skills
++heroRng.magicSchoolCounter;
++heroRng.wisdomCounter;
if((*LIBRARY->skillh)[selectedSkill]->obligatory(CSkill::Obligatory::MAJOR))
heroRng.wisdomCounter = 0;
if((*LIBRARY->skillh)[selectedSkill]->obligatory(CSkill::Obligatory::MINOR))
heroRng.magicSchoolCounter = 0;
return selectedSkill;
}
VCMI_LIB_NAMESPACE_END