Explorar o código

Random-with-history for luck & morale rolls

Ivan Savenko hai 5 meses
pai
achega
87323f08d9

+ 61 - 26
lib/callback/GameRandomizer.cpp

@@ -16,6 +16,7 @@
 #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"
@@ -23,15 +24,26 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-bool BiasedRandomizer::roll(vstd::RNG &generator, int successChance, int biasValue)
+BiasedRandomizer::BiasedRandomizer(int seed)
+	: seed(seed)
 {
-	int failChance = 100 - successChance;
-	int newRoll = generator.nextInt(0,99);
-	bool success = newRoll + accumulatedBias >= successChance;
+}
+
+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 / 100;
+		accumulatedBias -= failChance * biasValue;
 	else
-		accumulatedBias += successChance * biasValue / 100;
+		accumulatedBias += successChance * biasValue;
 
 	return success;
 }
@@ -43,26 +55,43 @@ GameRandomizer::GameRandomizer(const IGameInfoCallback & gameInfo)
 
 GameRandomizer::~GameRandomizer() = default;
 
-//bool GameRandomizer::rollGoodMorale(ObjectInstanceID actor, int moraleValue)
-//{
-//
-//}
-//
-//bool GameRandomizer::rollBadMorale(ObjectInstanceID actor, int moraleValue)
-//{
-//
-//}
-//
-//bool GameRandomizer::rollGoodLuck(ObjectInstanceID actor, int luckValue)
-//{
-//
-//}
-//
-//bool GameRandomizer::rollBadLuck(ObjectInstanceID actor, int luckValue)
-//{
-//
-//}
-//
+
+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, biasValue);
+}
+
+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)
 //{
 //
@@ -199,6 +228,9 @@ void GameRandomizer::setSeed(int 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());
@@ -214,6 +246,9 @@ PrimarySkill GameRandomizer::rollPrimarySkillForLevelup(const CGHeroInstance * h
 
 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)

+ 20 - 9
lib/callback/GameRandomizer.h

@@ -14,6 +14,8 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+enum class EGameSettings;
+
 class CGHeroInstance;
 
 /// Biased randomizer that has following properties:
@@ -24,17 +26,25 @@ class CGHeroInstance;
 /// Its goal is to simulate human expectations of random distributions and reduce frustration from "bad" rolls
 class BiasedRandomizer
 {
-	int accumulatedBias;
+	CRandomGenerator seed;
+	int32_t accumulatedBias = 0;
 public:
+	explicit BiasedRandomizer(int seed);
 	/// Performs coin flip with specified success chance
 	/// Returns true with probability successChance percents, and false with probability 100-successChance percents
-	bool roll(vstd::RNG & generator, int successChance, int biasValue);
+	bool roll(int successChance, int totalWeight, int biasValue);
 };
 
 class DLL_LINKAGE GameRandomizer final : public IGameRandomizer
 {
-	struct HeroSkillGenerator
+	static constexpr int biasValue = 10;
+
+	struct HeroSkillRandomizer
 	{
+		HeroSkillRandomizer(int seed)
+			:seed(seed)
+		{}
+
 		CRandomGenerator seed;
 		int8_t magicSchoolCounter = 1;
 		int8_t wisdomCounter = 1;
@@ -48,7 +58,7 @@ class DLL_LINKAGE GameRandomizer final : public IGameRandomizer
 	/// Stores number of times each artifact was placed on map via randomization
 	std::map<ArtifactID, int> allocatedArtifacts;
 
-	std::map<HeroTypeID, HeroSkillGenerator> heroSkillSeed;
+	std::map<HeroTypeID, HeroSkillRandomizer> heroSkillSeed;
 	std::map<PlayerColor, CRandomGenerator> playerTavern;
 
 	std::map<ObjectInstanceID, BiasedRandomizer> goodMoraleSeed;
@@ -58,17 +68,18 @@ class DLL_LINKAGE GameRandomizer final : public IGameRandomizer
 
 	std::map<ObjectInstanceID, BiasedRandomizer> combatAbilitySeed;
 
+	bool rollMoraleLuck(std::map<ObjectInstanceID, BiasedRandomizer> & seeds, ObjectInstanceID actor, int moraleLuckValue, EGameSettings diceSize, EGameSettings diceWeights);
 public:
 	explicit GameRandomizer(const IGameInfoCallback & gameInfo);
 	~GameRandomizer();
 
 	PrimarySkill rollPrimarySkillForLevelup(const CGHeroInstance * hero) override;
 	SecondarySkill rollSecondarySkillForLevelup(const CGHeroInstance * hero, const std::set<SecondarySkill> & candidates) override;
-//
-//	bool rollGoodMorale(ObjectInstanceID actor, int moraleValue);
-//	bool rollBadMorale(ObjectInstanceID actor, int moraleValue);
-//	bool rollGoodLuck(ObjectInstanceID actor, int luckValue);
-//	bool rollBadLuck(ObjectInstanceID actor, int luckValue);
+
+	bool rollGoodMorale(ObjectInstanceID actor, int moraleValue);
+	bool rollBadMorale(ObjectInstanceID actor, int moraleValue);
+	bool rollGoodLuck(ObjectInstanceID actor, int luckValue);
+	bool rollBadLuck(ObjectInstanceID actor, int luckValue);
 //
 //	bool rollCombatAbility(ObjectInstanceID actor, int percentageChance);
 

+ 5 - 10
server/battles/BattleActionProcessor.cpp

@@ -21,6 +21,7 @@
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/callback/GameRandomizer.h"
 #include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -940,21 +941,15 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
 
 	if(attackerLuck > 0)
 	{
-		auto goodLuckChanceVector = gameHandler->getSettings().getVector(EGameSettings::COMBAT_GOOD_LUCK_CHANCE);
-		int luckDiceSize = gameHandler->getSettings().getInteger(EGameSettings::COMBAT_LUCK_DICE_SIZE);
-		size_t chanceIndex = std::min<size_t>(goodLuckChanceVector.size(), attackerLuck) - 1; // array index, so 0-indexed
-
-		if(goodLuckChanceVector.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, luckDiceSize) <= goodLuckChanceVector[chanceIndex])
+		ObjectInstanceID ownerArmy = battle.getBattle()->getSideArmy(attacker->unitSide())->id;
+		if (gameHandler->randomizer->rollGoodLuck(ownerArmy, attackerLuck))
 			bat.flags |= BattleAttack::LUCKY;
 	}
 
 	if(attackerLuck < 0)
 	{
-		auto badLuckChanceVector = gameHandler->getSettings().getVector(EGameSettings::COMBAT_BAD_LUCK_CHANCE);
-		int luckDiceSize = gameHandler->getSettings().getInteger(EGameSettings::COMBAT_LUCK_DICE_SIZE);
-		size_t chanceIndex = std::min<size_t>(badLuckChanceVector.size(), -attackerLuck) - 1; // array index, so 0-indexed
-
-		if(badLuckChanceVector.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, luckDiceSize) <= badLuckChanceVector[chanceIndex])
+		ObjectInstanceID ownerArmy = battle.getBattle()->getSideArmy(attacker->unitSide())->id;
+		if (gameHandler->randomizer->rollBadLuck(ownerArmy, -attackerLuck))
 			bat.flags |= BattleAttack::UNLUCKY;
 	}
 

+ 5 - 10
server/battles/BattleFlowProcessor.cpp

@@ -19,6 +19,7 @@
 #include "../../lib/IGameSettings.h"
 #include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/IBattleState.h"
+#include "../../lib/callback/GameRandomizer.h"
 #include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -347,11 +348,8 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat
 	int nextStackMorale = next->moraleVal();
 	if(!next->hadMorale && !next->waited() && nextStackMorale < 0)
 	{
-		auto badMoraleChanceVector = gameHandler->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_CHANCE);
-		int moraleDiceSize = gameHandler->getSettings().getInteger(EGameSettings::COMBAT_MORALE_DICE_SIZE);
-		size_t chanceIndex = std::min<size_t>(badMoraleChanceVector.size(), -nextStackMorale) - 1; // array index, so 0-indexed
-
-		if(badMoraleChanceVector.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, moraleDiceSize) <= badMoraleChanceVector[chanceIndex])
+		ObjectInstanceID ownerArmy = battle.getBattle()->getSideArmy(next->unitSide())->id;
+		if (gameHandler->randomizer->rollBadMorale(ownerArmy, -nextStackMorale))
 		{
 			//unit loses its turn - empty freeze action
 			BattleAction ba;
@@ -527,11 +525,8 @@ bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, con
 		&& next->canMove()
 		&& nextStackMorale > 0)
 	{
-		auto goodMoraleChanceVector = gameHandler->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_CHANCE);
-		int moraleDiceSize = gameHandler->getSettings().getInteger(EGameSettings::COMBAT_MORALE_DICE_SIZE);
-		size_t chanceIndex = std::min<size_t>(goodMoraleChanceVector.size(), nextStackMorale) - 1; // array index, so 0-indexed
-
-		if(goodMoraleChanceVector.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, moraleDiceSize) <= goodMoraleChanceVector[chanceIndex])
+		ObjectInstanceID ownerArmy = battle.getBattle()->getSideArmy(next->unitSide())->id;
+		if (gameHandler->randomizer->rollGoodMorale(ownerArmy, nextStackMorale))
 		{
 			BattleTriggerEffect bte;
 			bte.battleID = battle.getBattle()->getBattleID();