浏览代码

feature: improve trading logic within makeTurn pass to avoid thousands of microtransactions; to focus on now's requirements but with long term in mind; to get some gold for the army as well (compared to 0 as before)

Mircea TheHonestCTO 3 月之前
父节点
当前提交
6f33bfe302

+ 36 - 13
AI/Nullkiller2/Analyzers/BuildAnalyzer.cpp

@@ -18,16 +18,29 @@
 namespace NK2AI
 {
 
-TResources BuildAnalyzer::getResourcesRequiredNow() const
+TResources BuildAnalyzer::getMissingResourcesNow(const float armyGoldRatio) const
 {
-	auto result = withoutGold(armyCost) + requiredResources - aiNk->getFreeResources();
+	auto armyGold = goldOnly(armyCost);
+	armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
+	auto result = requiredResources + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
 	result.positive();
 	return result;
 }
 
-TResources BuildAnalyzer::getTotalResourcesRequired() const
+TResources BuildAnalyzer::getMissingResourcesInTotal(const float armyGoldRatio) const
 {
-	auto result = totalDevelopmentCost + withoutGold(armyCost) - aiNk->getFreeResources();
+	auto armyGold = goldOnly(armyCost);
+	armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
+	auto result = totalDevelopmentCost + goldRemove(armyCost) + armyGold - aiNk->getFreeResources();
+	result.positive();
+	return result;
+}
+
+TResources BuildAnalyzer::getFreeResourcesAfterMissingTotal(const float armyGoldRatio) const
+{
+	auto armyGold = goldOnly(armyCost);
+	armyGold[GameResID::GOLD] = armyGoldRatio * armyGold[GameResID::GOLD];
+	auto result = aiNk->getFreeResources() - totalDevelopmentCost - goldRemove(armyCost) - armyGold;
 	result.positive();
 	return result;
 }
@@ -77,8 +90,8 @@ void BuildAnalyzer::update()
 
 	boost::range::sort(developmentInfos, [](const TownDevelopmentInfo & tdi1, const TownDevelopmentInfo & tdi2) -> bool
 	{
-		auto val1 = approximateInGold(tdi1.armyCost) - approximateInGold(tdi1.townDevelopmentCost);
-		auto val2 = approximateInGold(tdi2.armyCost) - approximateInGold(tdi2.townDevelopmentCost);
+		auto val1 = goldApproximate(tdi1.armyCost) - goldApproximate(tdi1.townDevelopmentCost);
+		auto val2 = goldApproximate(tdi2.armyCost) - goldApproximate(tdi2.townDevelopmentCost);
 		return val1 > val2;
 	});
 
@@ -119,7 +132,7 @@ void TownDevelopmentInfo::addBuildingBuilt(const BuildingInfo & bi)
 void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & bi)
 {
 	townDevelopmentCost += bi.buildCostWithPrerequisites;
-	townDevelopmentCost += BuildAnalyzer::withoutGold(bi.armyCost);
+	townDevelopmentCost += BuildAnalyzer::goldRemove(bi.armyCost);
 
 	if (bi.isBuildable)
 	{
@@ -434,19 +447,29 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	return info;
 }
 
-int32_t BuildAnalyzer::approximateInGold(const TResources & res)
+TResource BuildAnalyzer::goldApproximate(const TResources & res)
 {
-	// TODO: Would it make sense to use the marketplace rate of the player?
+	// TODO: Would it make sense to use the marketplace rate of the player? See Nullkiller::handleTrading()
 	return res[EGameResID::GOLD]
 		+ 75 * (res[EGameResID::WOOD] + res[EGameResID::ORE])
 		+ 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
 }
 
-TResources BuildAnalyzer::withoutGold(TResources other)
+TResources BuildAnalyzer::goldRemove(TResources other)
+{
+	TResources copy;
+	for(int i = 0; i < GameResID::COUNT; ++i)
+		copy[i] = other[i];
+
+	copy[GameResID::GOLD] = 0;
+	return copy;
+}
+
+TResources BuildAnalyzer::goldOnly(TResources other)
 {
-	// TODO: Mircea: Potential issue modifying the input directly? To inspect
-	other[GameResID::GOLD] = 0;
-	return other;
+	TResources copy;
+	copy[GameResID::GOLD] = other[GameResID::GOLD];
+	return copy;
 }
 
 }

+ 6 - 4
AI/Nullkiller2/Analyzers/BuildAnalyzer.h

@@ -82,8 +82,9 @@ public:
 	explicit BuildAnalyzer(Nullkiller * aiNk) : aiNk(aiNk) {}
 	void update();
 
-	TResources getResourcesRequiredNow() const;
-	TResources getTotalResourcesRequired() const;
+	TResources getMissingResourcesNow(float armyGoldRatio = 0) const;
+	TResources getMissingResourcesInTotal(float armyGoldRatio = 0) const;
+	TResources getFreeResourcesAfterMissingTotal(float armyGoldRatio = 0) const;
 	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
 	TResources getDailyIncome() const { return dailyIncome; }
 	float getGoldPressure() const { return goldPressure; }
@@ -102,8 +103,9 @@ public:
 		std::unique_ptr<ArmyManager> & armyManager,
 		std::shared_ptr<CCallback> & cc,
 		bool excludeDwellingDependencies = true);
-	static int32_t approximateInGold(const TResources & res);
-	static TResources withoutGold(TResources other);
+	static TResource goldApproximate(const TResources & res);
+	static TResources goldRemove(TResources other);
+	static TResources goldOnly(TResources other);
 };
 
 }

+ 2 - 2
AI/Nullkiller2/Behaviors/BuildingBehavior.cpp

@@ -31,8 +31,8 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * aiNk) const
 {
 	Goals::TGoalVec tasks;
 
-	TResources resourcesRequired = aiNk->buildAnalyzer->getResourcesRequiredNow();
-	TResources totalDevelopmentCost = aiNk->buildAnalyzer->getTotalResourcesRequired();
+	TResources resourcesRequired = aiNk->buildAnalyzer->getMissingResourcesNow();
+	TResources totalDevelopmentCost = aiNk->buildAnalyzer->getMissingResourcesInTotal();
 	TResources availableResources = aiNk->getFreeResources();
 	TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
 

+ 127 - 73
AI/Nullkiller2/Engine/Nullkiller.cpp

@@ -597,9 +597,7 @@ bool Nullkiller::executeTask(const Goals::TTask & task) const
 TResources Nullkiller::getFreeResources() const
 {
 	auto freeRes = cc->getResourceAmount() - lockedResources;
-
 	freeRes.positive();
-
 	return freeRes;
 }
 
@@ -610,98 +608,154 @@ void Nullkiller::lockResources(const TResources & res)
 
 bool Nullkiller::handleTrading()
 {
+	// TODO: Mircea: Maybe include based on how close danger is: X as default + proportion of close danger or something around that
+	constexpr float ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS = 0.1f;
+	constexpr float EXPENDABLE_BULK_RATIO = 0.25f;
 	bool haveTraded = false;
-	bool shouldTryToTrade = true;
 	ObjectInstanceID marketId;
-	for (const auto town : cc->getTownsInfo())
+
+	// TODO: Mircea: What about outside town markets that have better rates than a single town for example?
+	// Are those used anywhere? To inspect.
+	for (const auto * const town : cc->getTownsInfo())
 	{
 		if (town->hasBuiltSomeTradeBuilding())
 		{
 			marketId = town->id;
+			break;
 		}
 	}
+
 	if (!marketId.hasValue())
 		return false;
-	if (const CGObjectInstance* obj = cc->getObj(marketId, false))
+
+	const CGObjectInstance * obj = cc->getObj(marketId, false);
+	assert(obj);
+	// if (!obj)
+		// return false;
+
+	const auto * market = dynamic_cast<const IMarket *>(obj);
+	assert(market);
+	// if (!market)
+		// return false;
+
+	bool shouldTryToTrade = true;
+	while(shouldTryToTrade)
 	{
-		if (const auto* m = dynamic_cast<const IMarket*>(obj))
-		{
-			while (shouldTryToTrade)
-			{
-				shouldTryToTrade = false;
-				buildAnalyzer->update();
-				TResources required = buildAnalyzer->getTotalResourcesRequired();
-				TResources income = buildAnalyzer->getDailyIncome();
-				TResources available = cc->getResourceAmount();
+		shouldTryToTrade = false;
+		buildAnalyzer->update();
+
+		// if we favor getResourcesRequiredNow is better on short term, if we favor getTotalResourcesRequired is better on long term
+		TResources missingNow = buildAnalyzer->getMissingResourcesNow(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
+		if(missingNow.empty())
+			break;
+
+		TResources income = buildAnalyzer->getDailyIncome();
+		// We don't want to sell something that's necessary later on, though that could make short term a bit harder sometimes
+		TResources freeAfterMissingTotal = buildAnalyzer->getFreeResourcesAfterMissingTotal(ARMY_GOLD_RATIO_PER_MAKE_TURN_PASS);
+
 #if NK2AI_TRACE_LEVEL >= 2
-				logAi->debug("Available %s", available.toString());
-				logAi->debug("Required  %s", required.toString());
+		logAi->info("Nullkiller::handleTrading Free %s. FreeAfterMissingTotal %s. Required  %s", getFreeResources().toString(), freeAfterMissingTotal.toString(), missingNow.toString());
 #endif
-				int mostWanted = -1;
-				int mostExpendable = -1;
-				float minRatio = std::numeric_limits<float>::max();
-				float maxRatio = std::numeric_limits<float>::min();
 
-				for (int i = 0; i < required.size(); ++i)
-				{
-					if (required[i] <= 0)
-						continue;
-					float ratio = static_cast<float>(available[i]) / required[i];
-
-					if (ratio < minRatio) {
-						minRatio = ratio;
-						mostWanted = i;
-					}
-				}
+		constexpr int EMPTY = -1;
+		int mostWanted = EMPTY;
+		TResource mostWantedScoreNeg = std::numeric_limits<TResource>::max();
+		int mostExpendable = EMPTY;
+		TResource mostExpendableAmountPos = 0;
 
-				for (int i = 0; i < required.size(); ++i)
-				{
-					float ratio;
-					if (required[i] > 0)
-						ratio = static_cast<float>(available[i]) / required[i];
-					else
-						ratio = available[i];
+		// Find the most wanted resource
+		for(int i = 0; i < missingNow.size(); ++i)
+		{
+			if(missingNow[i] == 0)
+				continue;
 
-					bool okToSell = false;
+			const TResource score = income[i] - missingNow[i];
+			if(score < mostWantedScoreNeg)
+			{
+				mostWanted = i;
+				mostWantedScoreNeg = score;
+			}
+		}
+
+		// Find the most expendable resource
+		for(int i = 0; i < missingNow.size(); ++i)
+		{
+			const TResource amountToSell = freeAfterMissingTotal[i];
+			if (amountToSell == 0)
+				continue;
+
+			bool okToSell = false;
+			if(i == GameResID::GOLD)
+			{
+				// TODO: Mircea: Check if we should negate isGoldPressureOverMax() instead
+				if(income[GameResID::GOLD] > 0 && !buildAnalyzer->isGoldPressureOverMax())
+					okToSell = true;
+			}
+			else
+			{
+				okToSell = true;
+			}
+
+			if(amountToSell > mostExpendableAmountPos && okToSell)
+			{
+				mostExpendable = i;
+				mostExpendableAmountPos = amountToSell;
+			}
+		}
 
-					if (i == GameResID::GOLD)
-					{
-						if (income[i] > 0 && !buildAnalyzer->isGoldPressureOverMax())
-							okToSell = true;
-					}
-					else
-					{
-						if (required[i] <= 0 && income[i] > 0)
-							okToSell = true;
-					}
-
-					if (ratio > maxRatio && okToSell) {
-						maxRatio = ratio;
-						mostExpendable = i;
-					}
-				}
 #if NK2AI_TRACE_LEVEL >= 2
-				logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
+		logAi->trace(
+			"Nullkiller::handleTrading mostWanted: %d, mostWantedScoreNeg %d, mostExpendable: %d, mostExpendableAmountPos %d",
+			mostWanted,
+			mostWantedScoreNeg,
+			mostExpendable,
+			mostExpendableAmountPos
+		);
 #endif
-				if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
-					return false;
-
-				int toGive;
-				int toGet;
-				m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
-				//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
-				//TODO trade only as much as needed
-				if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
-				{
-					cc->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
+
+		if(mostExpendable == mostWanted || mostWanted == EMPTY || mostExpendable == EMPTY)
+			break;
+
+		int givenPerUnit;
+		int receivedUnits;
+		market->getOffer(mostExpendable, mostWanted, givenPerUnit, receivedUnits, EMarketMode::RESOURCE_RESOURCE);
+		if (!givenPerUnit || !receivedUnits)
+		{
+			logGlobal->error(
+				"Nullkiller::handleTrading No offer for %d of %d, given %d, received %d. Should never happen",
+				mostExpendable,
+				mostWanted,
+				givenPerUnit,
+				receivedUnits
+			);
+			break;
+		}
+
+		// TODO: Mircea: if 15 wood and 14 gems, gems can be used a lot more for buying other things
+		if (givenPerUnit > mostExpendableAmountPos)
+			break;
+
+		TResource multiplier = std::min(static_cast<int>(mostExpendableAmountPos * EXPENDABLE_BULK_RATIO / givenPerUnit),
+			missingNow[mostWanted] / receivedUnits); // for gold we have to / receivedUnits, because 1 ore gives many gold units
+		if(multiplier == 0) // could happen for very small values due to EXPENDABLE_BULK_RATIO
+			multiplier = 1;
+
+		const TResource givenMultiplied = givenPerUnit * multiplier;
+		if(givenMultiplied > freeAfterMissingTotal[mostExpendable])
+		{
+			logGlobal->error(
+				"Nullkiller::handleTrading Something went wrong with the multiplier %d",
+				multiplier
+			);
+			break;
+		}
+
+		cc->trade(market->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), givenMultiplied);
 #if NK2AI_TRACE_LEVEL >= 2
-					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
+		logAi->info("Nullkiller::handleTrading Traded %d of %s for %d of %s at %s", givenMultiplied, mostExpendable, receivedUnits, mostWanted, obj->getObjectName());
 #endif
-					haveTraded = true;
-					shouldTryToTrade = true;
-				}
-			}
-		}
+		haveTraded = true;
+		shouldTryToTrade = true;
 	}
 	return haveTraded;
 }

+ 2 - 2
AI/Nullkiller2/Engine/PriorityEvaluator.cpp

@@ -380,7 +380,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
 /// @return between 0-1.0f
 float RewardEvaluator::getNowResourceRequirementStrength(GameResID resType) const
 {
-	TResources requiredResources = aiNk->buildAnalyzer->getResourcesRequiredNow();
+	TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesNow();
 	TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
 
 	if(requiredResources[resType] == 0)
@@ -395,7 +395,7 @@ float RewardEvaluator::getNowResourceRequirementStrength(GameResID resType) cons
 /// @return between 0-1.0f
 float RewardEvaluator::getTotalResourceRequirementStrength(GameResID resType) const
 {
-	TResources requiredResources = aiNk->buildAnalyzer->getTotalResourcesRequired();
+	TResources requiredResources = aiNk->buildAnalyzer->getMissingResourcesInTotal();
 	TResources dailyIncome = aiNk->buildAnalyzer->getDailyIncome();
 
 	if(requiredResources[resType] == 0)

+ 1 - 3
AI/Nullkiller2/Goals/SaveResources.cpp

@@ -25,9 +25,7 @@ bool SaveResources::operator==(const SaveResources & other) const
 void SaveResources::accept(AIGateway * aiGw)
 {
 	aiGw->nullkiller->lockResources(resources);
-
-	logAi->debug("Locked %s resources", resources.toString());
-
+	logAi->debug("Locked resources %s", resources.toString());
 	throw goalFulfilledException(sptr(*this));
 }
 

+ 1 - 0
AI/Nullkiller2/Goals/SaveResources.h

@@ -15,6 +15,7 @@ namespace NK2AI
 {
 namespace Goals
 {
+	// TODO: Mircea: Inspect if it's really in use. See aiNk->getLockedResources()
 	class DLL_EXPORT SaveResources : public ElementarGoal<SaveResources>
 	{
 	private: