Skip to content

Commit

Permalink
fix AI ability to remember enemy units across turns (breaks savegames)
Browse files Browse the repository at this point in the history
fix healing logic for units without full moves
  • Loading branch information
ilteroi authored and RecursiveVision committed Jun 30, 2024
1 parent bffd218 commit 17b04ce
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 57 deletions.
13 changes: 8 additions & 5 deletions CvGameCoreDLL_Expansion2/CvDangerPlots.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CvGameCoreDLL_Expansion2/CvDangerPlots.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -158,6 +159,7 @@ class CvDangerPlots
int m_iTurnSliceBuilt;
vector<CvDangerPlotContents> m_DangerPlots; //not serialized!
UnitSet m_knownUnits;
UnitSet m_knownUnitsPrevTurn;
};

FDataStream& operator>>(FDataStream&, CvDangerPlots&);
Expand Down
17 changes: 14 additions & 3 deletions CvGameCoreDLL_Expansion2/CvHomelandAI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
17 changes: 16 additions & 1 deletion CvGameCoreDLL_Expansion2/CvPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11234,7 +11234,7 @@ void CvPlayer::doTurnPostDiplomacy()
UpdatePlots();
UpdateAreaEffectUnits();
UpdateAreaEffectPlots();
UpdateDangerPlots(false);
UpdateDangerPlots(true);
GetTacticalAI()->GetTacticalAnalysisMap()->Invalidate();
UpdateMilitaryStats();
GET_TEAM(getTeam()).ClearWarDeclarationCache();
Expand Down Expand Up @@ -47175,6 +47175,21 @@ int CvPlayer::GetDangerPlotAge() const
return m_pDangerPlots->GetTurnSliceBuilt();
}

std::vector<CvUnit*> CvPlayer::GetPrevTurnKnownEnemyUnits() const
{
std::vector<CvUnit*> 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<CvUnit*> CvPlayer::GetPossibleAttackers(const CvPlot& Plot, TeamTypes eTeamForVisibilityCheck)
{
if (m_pDangerPlots->IsDirty())
Expand Down
1 change: 1 addition & 0 deletions CvGameCoreDLL_Expansion2/CvPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CvUnit*> GetPrevTurnKnownEnemyUnits() const;
std::vector<CvUnit*> GetPossibleAttackers(const CvPlot& Plot, TeamTypes eTeamForVisibilityCheck);

bool IsKnownAttacker(const CvUnit* pAttacker);
Expand Down
105 changes: 58 additions & 47 deletions CvGameCoreDLL_Expansion2/CvTacticalAI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -716,6 +714,27 @@ void CvTacticalAI::FindTacticalTargets()
}
}

// Now a little bit of memory
vector<CvUnit*> lastTurnUnits = m_pPlayer->GetPrevTurnKnownEnemyUnits();
for (vector<CvUnit*>::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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1185,7 +1204,7 @@ void CvTacticalAI::ExecuteDestroyUnitMoves(AITacticalTargetType targetType, bool

for (size_t i=0; i<targets.size(); i++)
{
CvUnit* pDefender = targets[i].pPlot->getVisibleEnemyDefender(m_pPlayer->GetID());
CvUnit* pDefender = targets[i].pTarget->GetUnitPtr();
if (!pDefender)
continue;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 );
}
}
}
}

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -3620,6 +3626,7 @@ bool CvTacticalAI::ExecuteSpotterMove(const vector<CvUnit*>& 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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CvGameCoreDLL_Expansion2/CvTacticalAI.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit 17b04ce

Please sign in to comment.