From 17b04ce3e872f99e75d026c3d8f43ad06b96cf9c Mon Sep 17 00:00:00 2001 From: ilteroi Date: Wed, 26 Jun 2024 10:10:24 +0200 Subject: [PATCH] fix AI ability to remember enemy units across turns (breaks savegames) fix healing logic for units without full moves --- CvGameCoreDLL_Expansion2/CvDangerPlots.cpp | 13 ++- CvGameCoreDLL_Expansion2/CvDangerPlots.h | 2 + CvGameCoreDLL_Expansion2/CvHomelandAI.cpp | 17 +++- CvGameCoreDLL_Expansion2/CvPlayer.cpp | 17 +++- CvGameCoreDLL_Expansion2/CvPlayer.h | 1 + CvGameCoreDLL_Expansion2/CvTacticalAI.cpp | 105 ++++++++++++--------- CvGameCoreDLL_Expansion2/CvTacticalAI.h | 2 +- 7 files changed, 100 insertions(+), 57 deletions(-) diff --git a/CvGameCoreDLL_Expansion2/CvDangerPlots.cpp b/CvGameCoreDLL_Expansion2/CvDangerPlots.cpp index 61ed40a5df..0d471c0fb3 100644 --- a/CvGameCoreDLL_Expansion2/CvDangerPlots.cpp +++ b/CvGameCoreDLL_Expansion2/CvDangerPlots.cpp @@ -207,10 +207,12 @@ void CvDangerPlots::UpdateDangerInternal(bool bKeepKnownUnits, const PlotIndexCo CvPlayer& thisPlayer = GET_PLAYER(m_ePlayer); TeamTypes thisTeam = thisPlayer.getTeam(); - //units we know from last turn (maintained only for AI, humans must remember on their own) - UnitSet previousKnownUnits = m_knownUnits; - if (!bKeepKnownUnits || thisPlayer.isHuman()) - m_knownUnits.clear(); + //units we know from last turn are reset on turn change but not on war state change + if (!bKeepKnownUnits) + m_knownUnitsPrevTurn = m_knownUnits; + + //about to be refreshed + m_knownUnits.clear(); // for each opposing civ for(int iPlayer = 0; iPlayer < MAX_PLAYERS; iPlayer++) @@ -270,7 +272,7 @@ void CvDangerPlots::UpdateDangerInternal(bool bKeepKnownUnits, const PlotIndexCo } // now compare the new known units with the previous known units - for (UnitSet::iterator it = previousKnownUnits.begin(); it != previousKnownUnits.end(); ++it) + for (UnitSet::iterator it = m_knownUnitsPrevTurn.begin(); it != m_knownUnitsPrevTurn.end(); ++it) { //might have made peace ... if (ShouldIgnorePlayer(it->first)) @@ -579,6 +581,7 @@ void CvDangerPlots::Serialize(DangerPlots& dangerPlots, Visitor& visitor) if (bLoading) mutDangerPlots.Init(dangerPlots.m_ePlayer, true); visitor(dangerPlots.m_knownUnits); + visitor(dangerPlots.m_knownUnitsPrevTurn); } /// reads in danger plots info diff --git a/CvGameCoreDLL_Expansion2/CvDangerPlots.h b/CvGameCoreDLL_Expansion2/CvDangerPlots.h index 9b83f6058e..89fa4d7807 100644 --- a/CvGameCoreDLL_Expansion2/CvDangerPlots.h +++ b/CvGameCoreDLL_Expansion2/CvDangerPlots.h @@ -129,6 +129,7 @@ class CvDangerPlots void ResetDangerCache(const CvPlot* pCenterPlot, int iRange); bool IsKnownAttacker(const CvUnit* pUnit) const; bool AddKnownAttacker(const CvUnit* pUnit); + const UnitSet& GetPrevTurnKnownEnemyUnits() const { return m_knownUnitsPrevTurn; } void SetDirty(); bool IsDirty() const { return m_bDirty; } @@ -158,6 +159,7 @@ class CvDangerPlots int m_iTurnSliceBuilt; vector m_DangerPlots; //not serialized! UnitSet m_knownUnits; + UnitSet m_knownUnitsPrevTurn; }; FDataStream& operator>>(FDataStream&, CvDangerPlots&); diff --git a/CvGameCoreDLL_Expansion2/CvHomelandAI.cpp b/CvGameCoreDLL_Expansion2/CvHomelandAI.cpp index 2847a22e95..9779b22f95 100644 --- a/CvGameCoreDLL_Expansion2/CvHomelandAI.cpp +++ b/CvGameCoreDLL_Expansion2/CvHomelandAI.cpp @@ -2221,7 +2221,9 @@ bool CvHomelandAI::ExecuteExplorerMoves(CvUnit* pUnit) //this is stupid but we need extra code for scout healing if (pUnit->shouldHeal(true)) { - CvPlot* pPlot = TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit); + CvPlot* pPlot = TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit,true); + if (!pPlot) + pPlot = TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit,false); if (!pPlot) pPlot = TacticalAIHelpers::FindSafestPlotInReach(pUnit, true); @@ -3211,9 +3213,18 @@ void CvHomelandAI::ExecuteHeals() if (!pUnit) continue; - CvPlot* pBestPlot = pUnit->GetDanger()>0 ? TacticalAIHelpers::FindSafestPlotInReach(pUnit,true) : TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit); + //this is not optimal, ideally we would decide on a target plot and then pillage along the way if possible + //but it should be good enough + CvPlot* pBestPlot = TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit, true); + if (!pBestPlot) + pBestPlot = TacticalAIHelpers::FindClosestSafePlotForHealing(pUnit, false); + if (!pBestPlot) + pBestPlot = TacticalAIHelpers::FindSafestPlotInReach(pUnit, true); + if (pBestPlot && pBestPlot!=pUnit->plot()) pUnit->PushMission(CvTypes::getMISSION_MOVE_TO(), pBestPlot->getX(), pBestPlot->getY()); + if (pUnit->shouldPillage(pUnit->plot())) + pUnit->PushMission(CvTypes::getMISSION_PILLAGE()); if (pUnit->canMove()) pUnit->PushMission(CvTypes::getMISSION_SKIP()); UnitProcessed(pUnit->GetID()); @@ -4485,7 +4496,7 @@ void CvHomelandAI::ExecuteMissionaryMoves() pUnit->PushMission(CvTypes::getMISSION_SKIP()); //disband (captured) missionaries with the wrong religion - if (pUnit->plot()->getOwner()==pUnit->getOwner() && pUnit->canScrap() && pUnit->GetReligionData()->GetReligion() != m_pPlayer->GetReligionAI()->GetReligionToSpread(true)) + if (pUnit->canScrap() && pUnit->GetReligionData()->GetReligion() != m_pPlayer->GetReligionAI()->GetReligionToSpread(true)) pUnit->scrap(); else UnitProcessed(pUnit->GetID()); diff --git a/CvGameCoreDLL_Expansion2/CvPlayer.cpp b/CvGameCoreDLL_Expansion2/CvPlayer.cpp index b2e073ad90..60a11da770 100644 --- a/CvGameCoreDLL_Expansion2/CvPlayer.cpp +++ b/CvGameCoreDLL_Expansion2/CvPlayer.cpp @@ -11234,7 +11234,7 @@ void CvPlayer::doTurnPostDiplomacy() UpdatePlots(); UpdateAreaEffectUnits(); UpdateAreaEffectPlots(); - UpdateDangerPlots(false); + UpdateDangerPlots(true); GetTacticalAI()->GetTacticalAnalysisMap()->Invalidate(); UpdateMilitaryStats(); GET_TEAM(getTeam()).ClearWarDeclarationCache(); @@ -47175,6 +47175,21 @@ int CvPlayer::GetDangerPlotAge() const return m_pDangerPlots->GetTurnSliceBuilt(); } +std::vector CvPlayer::GetPrevTurnKnownEnemyUnits() const +{ + std::vector result; + + const UnitSet& units = m_pDangerPlots->GetPrevTurnKnownEnemyUnits(); + for (UnitSet::const_iterator it = units.begin(); it != units.end(); ++it) + { + result.push_back(GET_PLAYER(it->first).getUnit(it->second)); + if (result.back() == NULL) + result.pop_back(); + } + + return result; +} + std::vector CvPlayer::GetPossibleAttackers(const CvPlot& Plot, TeamTypes eTeamForVisibilityCheck) { if (m_pDangerPlots->IsDirty()) diff --git a/CvGameCoreDLL_Expansion2/CvPlayer.h b/CvGameCoreDLL_Expansion2/CvPlayer.h index 8528b5fe68..7d9a76b1d0 100644 --- a/CvGameCoreDLL_Expansion2/CvPlayer.h +++ b/CvGameCoreDLL_Expansion2/CvPlayer.h @@ -2504,6 +2504,7 @@ class CvPlayer int GetPlotDanger(const CvPlot& Plot, bool bFixedDamageOnly); void ResetDangerCache(const CvPlot& Plot, int iRange); int GetDangerPlotAge() const; + std::vector GetPrevTurnKnownEnemyUnits() const; std::vector GetPossibleAttackers(const CvPlot& Plot, TeamTypes eTeamForVisibilityCheck); bool IsKnownAttacker(const CvUnit* pAttacker); diff --git a/CvGameCoreDLL_Expansion2/CvTacticalAI.cpp b/CvGameCoreDLL_Expansion2/CvTacticalAI.cpp index 6817ef8e5b..884b721045 100644 --- a/CvGameCoreDLL_Expansion2/CvTacticalAI.cpp +++ b/CvGameCoreDLL_Expansion2/CvTacticalAI.cpp @@ -100,9 +100,7 @@ bool CvTacticalTarget::IsTargetStillAlive(PlayerTypes eAttackingPlayer) const eType == AI_TACTICAL_TARGET_MEDIUM_PRIORITY_UNIT || eType == AI_TACTICAL_TARGET_HIGH_PRIORITY_UNIT) { - CvPlot* pPlot = GC.getMap().plot(m_iTargetX, m_iTargetY); - CvUnit* pUnit = pPlot->getVisibleEnemyDefender(eAttackingPlayer); - if(pUnit != NULL && !pUnit->isDelayedDeath()) + if(GetUnitPtr() != NULL && !GetUnitPtr()->isDelayedDeath()) { bRtnValue = true; } @@ -716,6 +714,27 @@ void CvTacticalAI::FindTacticalTargets() } } + // Now a little bit of memory + vector lastTurnUnits = m_pPlayer->GetPrevTurnKnownEnemyUnits(); + for (vector::iterator it = lastTurnUnits.begin(); it != lastTurnUnits.end(); ++it) + { + CvPlot* pUnitPlot = (*it)->plot(); + //consider enemy units which are almost visible, just like a human would guess their presence + if (!pUnitPlot->isVisible(m_pPlayer->getTeam()) && pUnitPlot->isAdjacentVisible(m_pPlayer->getTeam())) + { + CvTacticalTarget newTarget; + newTarget.SetTargetX(pUnitPlot->getX()); + newTarget.SetTargetY(pUnitPlot->getY()); + newTarget.SetDominanceZone(GetTacticalAnalysisMap()->GetDominanceZoneID(pUnitPlot->GetPlotIndex())); + + //note that the HIGH/MEDIUM/LOW classification is changed later in IdentifyPriorityTargets + newTarget.SetTargetType(AI_TACTICAL_TARGET_LOW_PRIORITY_UNIT); + newTarget.SetUnitPtr(*it); + newTarget.SetAuxIntData(50); + m_AllTargets.push_back(newTarget); + } + } + // POST-PROCESSING ON TARGETS // Mark enemy units threatening our cities (or camps) as priority targets @@ -1148,7 +1167,7 @@ void CvTacticalAI::ExecuteDestroyUnitMoves(AITacticalTargetType targetType, bool { // See what units we have who can reach targets this turn CvPlot* pPlot = GC.getMap().plot(pTarget->GetTargetX(), pTarget->GetTargetY()); - CvUnit* pDefender = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pDefender = pTarget->GetUnitPtr(); if (pDefender && !pDefender->isDelayedDeath()) { //if we're not about to make a kill, stay away from zones we are supposed to avoid @@ -1185,7 +1204,7 @@ void CvTacticalAI::ExecuteDestroyUnitMoves(AITacticalTargetType targetType, bool for (size_t i=0; igetVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pDefender = targets[i].pTarget->GetUnitPtr(); if (!pDefender) continue; @@ -2888,8 +2907,7 @@ void CvTacticalAI::IdentifyPriorityTargets() //todo: should really use FindUnitsWithinStrikingDistance() here but it does not re-use the moveplots ... for (CvTacticalTarget* pTarget = GetFirstUnitTarget(); pTarget!=NULL; pTarget = GetNextUnitTarget()) { - CvPlot* pPlot = GC.getMap().plot(pTarget->GetTargetX(), pTarget->GetTargetY()); - CvUnit* pEnemyUnit = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pEnemyUnit = pTarget->GetUnitPtr(); if(pEnemyUnit) { int iExpectedDamage = 0; @@ -2959,8 +2977,7 @@ void CvTacticalAI::IdentifyPriorityTargets() for (size_t i = 0; i < possibleAttackers.size(); i++) { - CvPlot* pPlot = GC.getMap().plot( possibleAttackers[i]->GetTargetX(), possibleAttackers[i]->GetTargetY() ); - CvUnit* pEnemyUnit = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pEnemyUnit = possibleAttackers[i]->GetUnitPtr(); //the siege units are the dangerous ones! cities should target them first //for counterattack with units the tactical combat simulation chooses attacks based on estimated damage @@ -2998,8 +3015,7 @@ void CvTacticalAI::IdentifyPriorityBarbarianTargets() // Skip if already a priority target (because was able to strike another camp) if(pTarget->GetTargetType() != AI_TACTICAL_TARGET_HIGH_PRIORITY_UNIT) { - CvPlot* pPlot = GC.getMap().plot(pTarget->GetTargetX(), pTarget->GetTargetY()); - CvUnit* pEnemyUnit = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pEnemyUnit = pTarget->GetUnitPtr(); if(pEnemyUnit->IsCanAttackRanged() && pEnemyUnit->GetMaxRangedCombatStrength(NULL, NULL, true) > pEnemyUnit->GetMaxAttackStrength(NULL, pLoopPlot, NULL)) { if(plotDistance(pEnemyUnit->getX(), pEnemyUnit->getY(), pLoopPlot->getX(), pLoopPlot->getY()) <= pEnemyUnit->GetRange()) @@ -3101,37 +3117,19 @@ void CvTacticalAI::UpdateTargetScores() if (pPlot->IsFriendlyUnitAdjacent(m_pPlayer->getTeam(), true)) it->SetAuxIntData(it->GetAuxIntData() + 8); - //try to attack targets close to our cities first - if (m_pPlayer->GetCityDistanceInPlots(pPlot)<4) - it->SetAuxIntData(it->GetAuxIntData() + 3); + //if the target is damaged, give it a further boost + CvUnit* pUnit = it->GetUnitPtr(); + if (pUnit) + it->SetAuxIntData(it->GetAuxIntData() + pUnit->getDamage() / 8); + //note that distance to our cities is already considered in high/mid/low prio if(it->GetTargetType() == AI_TACTICAL_TARGET_MEDIUM_PRIORITY_UNIT) - { //initially all targets were low prio, if it's higher now we give it a boost it->SetAuxIntData( it->GetAuxIntData() + 25 ); - CvPlot* pPlot = GC.getMap().plot(it->GetTargetX(), it->GetTargetY()); - CvUnit* pUnit = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); - if (pUnit) - { - //if the target is damaged, give it a further boost - it->SetAuxIntData( it->GetAuxIntData() + pUnit->getDamage()/10 ); - } - } - if(it->GetTargetType() == AI_TACTICAL_TARGET_HIGH_PRIORITY_UNIT) - { //initially all targets were low prio, if it's higher now we give it a boost it->SetAuxIntData( it->GetAuxIntData() + 50 ); - - CvPlot* pPlot = GC.getMap().plot(it->GetTargetX(), it->GetTargetY()); - CvUnit* pUnit = pPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); - if (pUnit) - { - //if the target is damaged, give it a further boost - it->SetAuxIntData( it->GetAuxIntData() + pUnit->getDamage()/10 ); - } - } } } @@ -3218,6 +3216,14 @@ void CvTacticalAI::ExtractTargetsForZone(CvTacticalDominanceZone* pZone /* Pass //zone boundaries are arbitrary sometimes so include neighboring tiles as well for smaller zones if (pZone && plotDistance(pZone->GetCenterX(), pZone->GetCenterY(), it->GetTargetX(), it->GetTargetY()) <= iMaxRadius) { + //but only allow boundary plots + CvPlot* pZonePlot = GC.getMap().plot(pZone->GetCenterX(), pZone->GetCenterY()); + CvPlot* pTargetPlot = GC.getMap().plot(it->GetTargetX(), it->GetTargetY()); + if (pZone->IsWater() && !pTargetPlot->isWater() && !pTargetPlot->isAdjacentToArea(pZonePlot->getArea())) + continue; + if (!pZone->IsWater() && pTargetPlot->isWater() && !pTargetPlot->isAdjacentToArea(pZonePlot->getArea())) + continue; + m_ZoneTargets.push_back(*it); } } @@ -3620,6 +3626,7 @@ bool CvTacticalAI::ExecuteSpotterMove(const vector& vUnits, CvPlot* pTa case UNITAI_FAST_ATTACK: case UNITAI_SKIRMISHER: case UNITAI_ATTACK_SEA: + case UNITAI_ASSAULT_SEA: case UNITAI_SUBMARINE: case UNITAI_ATTACK: case UNITAI_COUNTER: @@ -4621,7 +4628,7 @@ bool CvTacticalAI::ExecuteAttritionAttacks(CvTacticalTarget& kTarget) //todo: how can we make sure that city and units focus fire on the same targets? CvPlot* pTargetPlot = GC.getMap().plot(kTarget.GetTargetX(), kTarget.GetTargetY()); - CvUnit* pDefender = pTargetPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pDefender = kTarget.GetUnitPtr(); if(pDefender) { if(pDefender->GetCurrHitPoints() < 1) @@ -4653,7 +4660,7 @@ bool CvTacticalAI::ExecuteFlankAttack(CvTacticalTarget& kTarget) kTarget.SetLastAggLvl(aggLevel); // Make attacks - CvUnit* pDefender = pTargetPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pDefender = kTarget.GetUnitPtr(); if (pDefender && FindUnitsWithinStrikingDistance(pTargetPlot) && ComputeTotalExpectedDamage(kTarget)>0) return ExecuteAttackWithUnits(pTargetPlot, aggLevel); @@ -5342,7 +5349,7 @@ int CvTacticalAI::ComputeTotalExpectedDamage(const CvTacticalTarget& kTarget) case AI_TACTICAL_TARGET_MEDIUM_PRIORITY_UNIT: case AI_TACTICAL_TARGET_LOW_PRIORITY_UNIT: { - CvUnit* pDefender = pTargetPlot->getVisibleEnemyDefender(m_pPlayer->GetID()); + CvUnit* pDefender = kTarget.GetUnitPtr(); if (pDefender) { int iSelfDamage = 0; @@ -6614,7 +6621,7 @@ bool TacticalAIHelpers::IsCloseToContestedBorder(CvPlayer* pPlayer, CvPlot* pPlo return bResult; } -CvPlot* TacticalAIHelpers::FindClosestSafePlotForHealing(CvUnit* pUnit) +CvPlot* TacticalAIHelpers::FindClosestSafePlotForHealing(CvUnit* pUnit, bool bConservative) { if (!pUnit) return NULL; @@ -6636,7 +6643,7 @@ CvPlot* TacticalAIHelpers::FindClosestSafePlotForHealing(CvUnit* pUnit) //don't check movement, don't need to heal right now if (pUnit->getDomainType() == DOMAIN_LAND) { - if (!pUnit->canHeal(pPlot, true)) + if (!pUnit->canHeal(pPlot, false)) //might have moved before! continue; //don't mess with (ranged) garrisons if enemies are around @@ -6647,7 +6654,7 @@ CvPlot* TacticalAIHelpers::FindClosestSafePlotForHealing(CvUnit* pUnit) else { //naval units usually must pillage to heal ... - if (!bPillage && !pUnit->canHeal(pPlot, true)) + if (!bPillage && !pUnit->canHeal(pPlot, false)) continue; } @@ -6665,13 +6672,17 @@ CvPlot* TacticalAIHelpers::FindClosestSafePlotForHealing(CvUnit* pUnit) } int iDanger = pUnit->GetDanger(pPlot); - if (iDanger <= pUnit->healRate(pPlot)) //ignore pillage health here, it's a one-time effect and may lead into dead ends - { - int iScore = pUnit->healRate(pPlot) + pPlot->GetNumFriendlyUnitsAdjacent(pUnit->getTeam(), NO_DOMAIN, true, pUnit) * 3 - iDanger; + int iHealRate = pUnit->healRate(pPlot); + int nFriends = pPlot->GetNumFriendlyUnitsAdjacent(pUnit->getTeam(), NO_DOMAIN, true, pUnit); + + //sometimes we want to ignore pillage health, it's a one-time effect and may lead into dead ends + if (!bConservative && bPillage) + iHealRate += /*25*/ GD_INT_GET(PILLAGE_HEAL_AMOUNT); - //can pillage = good - if (bPillage) - iScore += /*25*/ GD_INT_GET(PILLAGE_HEAL_AMOUNT); + //the method is called findSafePlot; this should be safe enough + if (iDanger < iHealRate * nFriends) + { + int iScore = iHealRate*2 + nFriends*3 - iDanger; //tiebreaker iScore -= GET_PLAYER(pUnit->getOwner()).GetCityDistancePathLength(pPlot); @@ -6991,7 +7002,7 @@ bool TacticalAIHelpers::CanKillTarget(const CvUnit* pAttacker, CvPlot* pTarget) } } - CvUnit* pDefender = pTarget->getVisibleEnemyDefender(pAttacker->getOwner()); + CvUnit* pDefender = pTarget->getBestDefender(NO_PLAYER,pAttacker->getOwner(),pAttacker,false,true); if (pDefender) { //see how the attack would go diff --git a/CvGameCoreDLL_Expansion2/CvTacticalAI.h b/CvGameCoreDLL_Expansion2/CvTacticalAI.h index deff3cbf7b..a781e0c0c6 100644 --- a/CvGameCoreDLL_Expansion2/CvTacticalAI.h +++ b/CvGameCoreDLL_Expansion2/CvTacticalAI.h @@ -1010,7 +1010,7 @@ namespace TacticalAIHelpers bool PerformOpportunityAttack(CvUnit* pUnit, bool bAllowMovement = false); bool IsAttackNetPositive(CvUnit* pUnit, const CvPlot* pTarget); CvPlot* FindSafestPlotInReach(const CvUnit* pUnit, bool bAllowEmbark, bool bConsiderPush = false); - CvPlot* FindClosestSafePlotForHealing(CvUnit* pUnit); + CvPlot* FindClosestSafePlotForHealing(CvUnit* pUnit, bool bConservative = true); bool IsGoodPlotForStaging(CvPlayer* pPlayer, CvPlot* pCandidate, DomainTypes eDomain); bool IsCloseToContestedBorder(CvPlayer* pPlayer, CvPlot* pPlot);