From 2f3044166013797b9e8dfd8e7049df6ea23ef337 Mon Sep 17 00:00:00 2001 From: RecursiveVision <66801010+RecursiveVision@users.noreply.github.com> Date: Thu, 14 Sep 2023 22:50:17 -0400 Subject: [PATCH] Add AI_considerRaze() logic Improve AI city capture & purchase logic Fix some issues with liberation --- CvGameCoreDLL_Expansion2/CvDealAI.cpp | 4 + CvGameCoreDLL_Expansion2/CvDiplomacyAI.cpp | 32 ++-- CvGameCoreDLL_Expansion2/CvPlayer.cpp | 5 +- CvGameCoreDLL_Expansion2/CvPlayerAI.cpp | 191 ++++++++++++++++++++- CvGameCoreDLL_Expansion2/CvPlayerAI.h | 1 + 5 files changed, 214 insertions(+), 19 deletions(-) diff --git a/CvGameCoreDLL_Expansion2/CvDealAI.cpp b/CvGameCoreDLL_Expansion2/CvDealAI.cpp index 67c0732f80..35dab7c757 100644 --- a/CvGameCoreDLL_Expansion2/CvDealAI.cpp +++ b/CvGameCoreDLL_Expansion2/CvDealAI.cpp @@ -2179,6 +2179,10 @@ int CvDealAI::GetCityValueForDeal(CvCity* pCity, PlayerTypes eAssumedOwner) CvPlayer& assumedOwner = GET_PLAYER(eAssumedOwner); bool bPeaceTreatyTrade = assumedOwner.IsAtWarWith(pCity->getOwner()); + // Don't buy any cities that aren't ours if we're unhappy + if (!bPeaceTreatyTrade && pCity->getOwner() != eAssumedOwner && pCity->getOriginalOwner() != eAssumedOwner && GET_PLAYER(eAssumedOwner).IsEmpireUnhappy()) + return INT_MAX; + //if we already own it and trade voluntarily ... if (!bPeaceTreatyTrade && pCity->getOwner() == eAssumedOwner) { diff --git a/CvGameCoreDLL_Expansion2/CvDiplomacyAI.cpp b/CvGameCoreDLL_Expansion2/CvDiplomacyAI.cpp index 11a92b7685..dd4072e73b 100644 --- a/CvGameCoreDLL_Expansion2/CvDiplomacyAI.cpp +++ b/CvGameCoreDLL_Expansion2/CvDiplomacyAI.cpp @@ -54709,7 +54709,7 @@ int CvDiplomacyAIHelpers::GetCityWarmongerValue(CvCity* pCity, PlayerTypes eConq } // Capped at 25% and SHARED_FATE_PERCENT - iWarmongerValue *= range((100 + iWarmongerPoliticalModifier), 25, max(100, /*200*/ GD_INT_GET(WARMONGER_THREAT_SHARED_FATE_PERCENT))); + iWarmongerValue *= range(100 + iWarmongerPoliticalModifier, 25, max(100, /*200*/ GD_INT_GET(WARMONGER_THREAT_SHARED_FATE_PERCENT))); iWarmongerValue /= 100; if (iWarmongerValue <= 0) @@ -54774,6 +54774,10 @@ int CvDiplomacyAIHelpers::GetCityLiberationValue(CvCity* pCity, PlayerTypes eLib if (pDiplo->IsMaster(eLiberator) || pDiplo->IsVassal(eLiberator)) return 0; + // No bonuses for liberating your own team's city. + if (GET_PLAYER(eNewOwner).getTeam() == GET_PLAYER(eLiberator).getTeam()) + return 0; + // Are we at war with the liberated player? No liberation bonuses! if (pDiplo->IsAtWar(eNewOwner)) return 0; @@ -54870,47 +54874,45 @@ int CvDiplomacyAIHelpers::GetCityLiberationValue(CvCity* pCity, PlayerTypes eLib int iWarmongerStrengthModifier = 0; // DECREASE if he's big and nasty, INCREASE if he's not. + // Flip the values used for warmongering switch (pDiplo->GetMilitaryStrengthComparedToUs(eLiberator)) { case NO_STRENGTH_VALUE: UNREACHABLE(); // Strengths are supposed to have been evaluated by this point. case STRENGTH_IMMENSE: - iWarmongerStrengthModifier = /*100*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_IMMENSE); + iWarmongerStrengthModifier = /*-50*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_PATHETIC); break; case STRENGTH_POWERFUL: - iWarmongerStrengthModifier = /*75*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_POWERFUL); + iWarmongerStrengthModifier = /*-25*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_WEAK); break; case STRENGTH_STRONG: - iWarmongerStrengthModifier = /*50*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_STRONG); + iWarmongerStrengthModifier = /*0*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_POOR); break; case STRENGTH_AVERAGE: iWarmongerStrengthModifier = /*33*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_AVERAGE); break; case STRENGTH_POOR: - iWarmongerStrengthModifier = /*0*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_POOR); + iWarmongerStrengthModifier = /*50*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_STRONG); break; case STRENGTH_WEAK: - iWarmongerStrengthModifier = /*-25*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_WEAK); + iWarmongerStrengthModifier = /*75*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_POWERFUL); break; case STRENGTH_PATHETIC: - iWarmongerStrengthModifier = /*-50*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_PATHETIC); + iWarmongerStrengthModifier = /*100*/ GD_INT_GET(WARMONGER_THREAT_ATTACKER_STRENGTH_IMMENSE); break; } - // Flip this for liberation. Higher strength = less bonus. - iWarmongerStrengthModifier *= -1; - // No strength-based reductions if it's our gain... if (bHisGainIsOurOwn && iWarmongerStrengthModifier < 0) iWarmongerStrengthModifier = 0; if (!bHisGainIsOurOwn) { - // Need to check if the opponent is alive, calling this for an unmet player can crash the game - StrengthTypes eOpponentStrength = GET_PLAYER(eNewOwner).isAlive() ? pDiplo->GetMilitaryStrengthComparedToUs(eNewOwner) : STRENGTH_PATHETIC; + // For some reason it's possible for this to be NO_STRENGTH_VALUE for a met, but dead player ... default to PATHETIC to avoid crashing the game + StrengthTypes eLiberatedPlayerStrength = GET_PLAYER(eNewOwner).isAlive() ? pDiplo->GetMilitaryStrengthComparedToUs(eNewOwner) : STRENGTH_PATHETIC; - // DECREASE if opponent is big and nasty. - switch (eOpponentStrength) + // DECREASE if liberated player is big and nasty. + switch (eLiberatedPlayerStrength) { case NO_STRENGTH_VALUE: UNREACHABLE(); // Strengths are supposed to have been evaluated by this point. @@ -55121,7 +55123,7 @@ int CvDiplomacyAIHelpers::GetCityLiberationValue(CvCity* pCity, PlayerTypes eLib } // Capped at 25% and SHARED_FATE_PERCENT - iLiberationValue *= range((100 + iWarmongerPoliticalModifier), 25, max(100, /*200*/ GD_INT_GET(WARMONGER_THREAT_SHARED_FATE_PERCENT))); + iLiberationValue *= range(100 + iWarmongerPoliticalModifier, 25, max(100, /*200*/ GD_INT_GET(WARMONGER_THREAT_SHARED_FATE_PERCENT))); iLiberationValue /= 100; if (iLiberationValue >= 0) diff --git a/CvGameCoreDLL_Expansion2/CvPlayer.cpp b/CvGameCoreDLL_Expansion2/CvPlayer.cpp index 6c899d1cd2..1a78d6c61c 100644 --- a/CvGameCoreDLL_Expansion2/CvPlayer.cpp +++ b/CvGameCoreDLL_Expansion2/CvPlayer.cpp @@ -9939,9 +9939,8 @@ void CvPlayer::DoLiberatePlayer(PlayerTypes ePlayer, int iOldCityID, bool bForce // Bonuses for first liberation only (remove sphere doesn't count) if (!bSphereRemoval && bFirstLiberation) { - // Reduce liberator's warmongering penalties (if any), unless this is their own team's city - if (getTeam() != GET_PLAYER(ePlayer).getTeam()) - CvDiplomacyAIHelpers::ApplyLiberationBonuses(pNewCity, GetID(), ePlayer); + // Reduce liberator's warmongering penalties (if any, and applicable) + CvDiplomacyAIHelpers::ApplyLiberationBonuses(pNewCity, GetID(), ePlayer); // Reduce liberator's war weariness by 25% GetCulture()->SetWarWeariness(GetCulture()->GetWarWeariness() - (GetCulture()->GetWarWeariness() / 4)); diff --git a/CvGameCoreDLL_Expansion2/CvPlayerAI.cpp b/CvGameCoreDLL_Expansion2/CvPlayerAI.cpp index f9543f5669..924cba8cf8 100644 --- a/CvGameCoreDLL_Expansion2/CvPlayerAI.cpp +++ b/CvGameCoreDLL_Expansion2/CvPlayerAI.cpp @@ -132,6 +132,7 @@ void CvPlayerAI::AI_doTurnPre() AI_doResearch(); AI_considerAnnex(); + AI_considerRaze(); } @@ -330,6 +331,35 @@ void CvPlayerAI::AI_conquerCity(CvCity* pCity, bool bGift, bool bAllowSphereRemo return; } + // Special handling when already super unhappy + // We likely won't be able to keep the city, so get rid of it immediately if possible + if (IsEmpireSuperUnhappy()) + { + if (bCanLiberate) + { + if (GET_PLAYER(ePlayerToLiberate).isMinorCiv()) + { + DoLiberatePlayer(ePlayerToLiberate, pCity->GetID(), false, false); + return; + } + + if (GetDiplomacyAI()->DoPossibleMajorLiberation(pCity)) + return; + } + + if (bAllowSphereRemoval) + { + DoLiberatePlayer(ePlayerToLiberate, pCity->GetID(), false, true); + return; + } + + if (bCanRaze) + { + pCity->doTask(TASK_RAZE); + return; + } + } + // What is our happiness situation? bool bUnhappy = false; @@ -369,10 +399,13 @@ void CvPlayerAI::AI_conquerCity(CvCity* pCity, bool bGift, bool bAllowSphereRemo } //only city on a landmass? may be strategically important + bool bOnlyCityOnLandmass = false; CvLandmass* pLandmass = GC.getMap().getLandmassById(pCity->plot()->getLandmass()); if (pLandmass != NULL && pLandmass->getCitiesPerPlayer(GetID()) < 1) { - bKeepCity = true; + bOnlyCityOnLandmass = true; + if (!IsEmpireVeryUnhappy()) + bKeepCity = true; } // If we're going for world conquest, have to keep any capitals we get @@ -446,6 +479,12 @@ void CvPlayerAI::AI_conquerCity(CvCity* pCity, bool bGift, bool bAllowSphereRemo return; } + if (bOnlyCityOnLandmass && !IsEmpireSuperUnhappy()) + { + pCity->DoCreatePuppet(); + return; + } + // If the city is of ok-ish value and we're not too unhappy, let's puppet if (!IsEmpireVeryUnhappy()) { @@ -767,6 +806,156 @@ void CvPlayerAI::AI_considerAnnex() } } +void CvPlayerAI::AI_considerRaze() +{ + if (isHuman()) + return; + + AI_PERF("AI-perf.csv", "AI_ considerRaze"); + + // Only raze when super unhappy + int iNumCities = getNumCities(); + if (!IsEmpireSuperUnhappy() || iNumCities == 1) + return; + + int iRequiredReduction = 0; + + // Look at our Unhappiness situation to see how much Unhappiness we need to remove + if (MOD_BALANCE_CORE_HAPPINESS) + { + int iUnhappyCitizens = GetUnhappinessFromCitizenNeeds(); + int iHappyCitizens = GetHappinessFromCitizenNeeds(); + int iThreshold = /*20*/ GD_INT_GET(SUPER_UNHAPPY_THRESHOLD); + // Protect against modder stupidity + if (iHappyCitizens <= 0 || iThreshold <= 0) + return; + + // Considering the number of happy citizens, how many citizens would we need to remove to get out of super unhappiness? + // Find X, where Happy Citizens * 100 / X / 2 == Threshold + int iTarget = iHappyCitizens * 50 / iThreshold; + iRequiredReduction = iUnhappyCitizens - iTarget; + } + else + { + int iHappyCitizens = GetHappiness(); + int iUnhappyCitizens = GetUnhappiness(); + int iDiff = iHappyCitizens - iUnhappyCitizens; + iRequiredReduction = /*-20*/ GD_INT_GET(SUPER_UNHAPPY_THRESHOLD) + 1 - iDiff; + } + + if (iRequiredReduction <= 0) + return; + + vector EndangeredCities; + CvWeightedVector RazeCandidates; + vector vPlayersAtWarWith = GetPlayersAtWarWith(); + int iLoop = 0; + int iPopulationRemoved = 0; + for (CvCity* pLoopCity = firstCity(&iLoop); pLoopCity != NULL; pLoopCity = nextCity(&iLoop)) + { + // Already razing a city? How much population will be removed? + if (pLoopCity->IsRazing()) + { + iNumCities--; + iPopulationRemoved += pLoopCity->getPopulation(); + // Already enough! Don't burn more! + if (iPopulationRemoved >= iRequiredReduction || iNumCities == 1) + return; + + continue; + } + + // Must be occupied or puppeted + if (!pLoopCity->IsOccupied() && !pLoopCity->IsPuppet()) + continue; + + // Must be able to raze + if (!canRaze(pLoopCity)) + continue; + + // City is in danger of being captured? Raze with higher priority to deny our opponents the ability to capture it. + if (pLoopCity->IsInDangerFromPlayers(vPlayersAtWarWith)) + EndangeredCities.push_back(pLoopCity); + + // Don't raze cities that aren't unhappy + // Special handling for non-Venetian puppets + int iHappinessDelta = pLoopCity->getHappinessDelta(); + + if (pLoopCity->IsPuppet() && (!MOD_BALANCE_VP || !GetPlayerTraits()->IsNoAnnexing())) + iHappinessDelta = pLoopCity->GetUnhappinessAggregated() * -1; + + if (iHappinessDelta >= 0) + continue; + + RazeCandidates.push_back(pLoopCity, iHappinessDelta * -1); + } + + RazeCandidates.StableSortItems(); + + // First priority: burn down endangered cities that we haven't built a courthouse in, and that don't have Wonders + for (int i = 0; i < RazeCandidates.size(); i++) + { + CvCity* pLoopCity = RazeCandidates.GetElement(i); + if (!pLoopCity->IsNoOccupiedUnhappiness() && !pLoopCity->HasAnyWonder() && std::find(EndangeredCities.begin(), EndangeredCities.end(), pLoopCity) != EndangeredCities.end()) + { + iNumCities--; + iPopulationRemoved += pLoopCity->getPopulation(); + pLoopCity->doTask(TASK_RAZE); + // Have we met our goal? + if (iPopulationRemoved >= iRequiredReduction || iNumCities == 1) + return; + } + } + // Second priority: burn down non-endangered cities that we haven't built a courthouse in, and that don't have Wonders + for (int i = 0; i < RazeCandidates.size(); i++) + { + CvCity* pLoopCity = RazeCandidates.GetElement(i); + if (pLoopCity->IsRazing()) + continue; + + if (!pLoopCity->IsNoOccupiedUnhappiness() && !pLoopCity->HasAnyWonder()) + { + iNumCities--; + iPopulationRemoved += pLoopCity->getPopulation(); + pLoopCity->doTask(TASK_RAZE); + // Have we met our goal? + if (iPopulationRemoved >= iRequiredReduction || iNumCities == 1) + return; + } + } + // Third priority: burn down endangered cities that we HAVE built a courthouse in, or that have Wonders + for (int i = 0; i < RazeCandidates.size(); i++) + { + CvCity* pLoopCity = RazeCandidates.GetElement(i); + if (pLoopCity->IsRazing()) + continue; + + if (std::find(EndangeredCities.begin(), EndangeredCities.end(), pLoopCity) != EndangeredCities.end()) + { + iNumCities--; + iPopulationRemoved += pLoopCity->getPopulation(); + pLoopCity->doTask(TASK_RAZE); + // Have we met our goal? + if (iPopulationRemoved >= iRequiredReduction || iNumCities == 1) + return; + } + } + // Fourth priority: burn down other cities + for (int i = 0; i < RazeCandidates.size(); i++) + { + CvCity* pLoopCity = RazeCandidates.GetElement(i); + if (pLoopCity->IsRazing()) + continue; + + iNumCities--; + iPopulationRemoved += pLoopCity->getPopulation(); + pLoopCity->doTask(TASK_RAZE); + // Have we met our goal? + if (iPopulationRemoved >= iRequiredReduction || iNumCities == 1) + return; + } +} + #if defined(MOD_BALANCE_CORE_EVENTS) void CvPlayerAI::AI_DoEventChoice(EventTypes eChosenEvent) { diff --git a/CvGameCoreDLL_Expansion2/CvPlayerAI.h b/CvGameCoreDLL_Expansion2/CvPlayerAI.h index b1f54e8716..381806541f 100644 --- a/CvGameCoreDLL_Expansion2/CvPlayerAI.h +++ b/CvGameCoreDLL_Expansion2/CvPlayerAI.h @@ -48,6 +48,7 @@ class CvPlayerAI : public CvPlayer void AI_chooseResearch(); void AI_considerAnnex(); + void AI_considerRaze(); virtual void AI_launch(VictoryTypes eVictory);