I'm currently looking into developing a new Healer AI and some functions to help the AI utilize the new Build Up and other alternative Fortification missions.
I've started by evaluating the City Defense AI routine with the intention to see how and when they are establishing themselves as fortified. Starting from the beginning, though, has the benefit of showing me why some of the stupid decisions the AI are making are taking place.
I wanted to take a moment to point out some of what I THINK I've found may be some flaws (perhaps serious ones) and before making any kind of adjustment, asking the community, particularly those that understand the AI and C++ (Alberts2, Afforess, Koshling, AIAndy etc...) to concur or offer counter-observations and perhaps explanations as to how I may be seeing them wrong. This is a place to be very careful so I'm seeking collaboration on anything I'd look to adjust here.
So far, following up to this point:
Code:
void CvUnitAI::AI_cityDefenseMove()
{
PROFILE_FUNC();
if ( checkSwitchToConstruct() )
{
return;
}
/************************************************************************************************/
/* BETTER_BTS_AI_MOD 08/20/09 jdog5000 */
/* */
/* Unit AI, Efficiency */
/************************************************************************************************/
//bool bDanger = (GET_PLAYER(getOwnerINLINE()).AI_getPlotDanger(plot(), 3) > 0);
bool bDanger = (GET_PLAYER(getOwnerINLINE()).AI_getAnyPlotDanger(plot(), 3));
/************************************************************************************************/
/* BETTER_BTS_AI_MOD END */
/************************************************************************************************/
/************************************************************************************************/
/* BETTER_BTS_AI_MOD 09/18/09 jdog5000 */
/* */
/* Settler AI */
/************************************************************************************************/
if ( MISSIONAI_REGROUP == getGroup()->AI_getMissionAIType() )
{
if (AI_group(UNITAI_SETTLE, 2, -1, -1, false, false, false, 1, true))
{
return;
}
And moving through AI_group(etc...):
Code:
bool CvUnitAI::AI_group(UnitAITypes eUnitAI, int iMaxGroup, int iMaxOwnUnitAI, int iMinUnitAI, bool bIgnoreFaster, bool bIgnoreOwnUnitType, bool bStackOfDoom, int iMaxPath, bool bAllowRegrouping, bool bWithCargoOnly, bool bInCityOnly, MissionAITypes eIgnoreMissionAIType)
{
PROFILE_FUNC();
CvUnit* pLoopUnit;
CvUnit* pBestUnit;
int iPathTurns;
int iValue;
int iBestValue;
int iLoop;
bool bCanDefend = getGroup()->canDefend();
// if we are on a transport, then do not regroup
if (isCargo())
{
return false;
}
if (!bAllowRegrouping)
{
if (getGroup()->getNumUnits() > 1)
{
return false;
}
}
if ((getDomainType() == DOMAIN_LAND) && !canMoveAllTerrain())
{
if (area()->getNumAIUnits(getOwnerINLINE(), eUnitAI) == 0)
{
return false;
}
}
if (!AI_canGroupWithAIType(eUnitAI))
{
return false;
}
if ( GET_PLAYER(getOwnerINLINE()).AI_getNumAIUnits(eUnitAI) == 0 )
{
return false;
}
int iOurImpassableCount = 0;
CLLNode<IDInfo>* pUnitNode = getGroup()->headUnitNode();
while (pUnitNode != NULL)
{
CvUnit* pImpassUnit = ::getUnit(pUnitNode->m_data);
pUnitNode = getGroup()->nextUnitNode(pUnitNode);
iOurImpassableCount = std::max(iOurImpassableCount, GET_PLAYER(getOwnerINLINE()).AI_unitImpassableCount(pImpassUnit->getUnitType()));
}
iBestValue = MAX_INT;
pBestUnit = NULL;
CvReachablePlotSet plotSet(getGroup(), bCanDefend ? 0 : MOVE_OUR_TERRITORY, AI_searchRange(iMaxPath));
// Loop over groups, ai_allowgroup blocks non-head units anyway
CvSelectionGroup* pLoopGroup = NULL;
for(pLoopGroup = GET_PLAYER(getOwnerINLINE()).firstSelectionGroup(&iLoop); pLoopGroup != NULL; pLoopGroup = GET_PLAYER(getOwnerINLINE()).nextSelectionGroup(&iLoop))
{
pLoopUnit = pLoopGroup->getHeadUnit();
if( pLoopUnit == NULL )
{
continue;
}
CvPlot* pPlot = pLoopUnit->plot();
if (/*AI_plotValid(pPlot) &&*/ plotSet.find(pPlot) != plotSet.end())
{
if (iMaxPath > 0 || pPlot == plot())
{
if (!isEnemy(pPlot->getTeam()))
{
if (AI_allowGroup(pLoopUnit, eUnitAI))
To AI_allowGroup(etc...) then following that function:
Code:
bool CvUnitAI::AI_allowGroup(const CvUnit* pUnit, UnitAITypes eUnitAI) const
{
CvSelectionGroup* pGroup = pUnit->getGroup();
CvPlot* pPlot = pUnit->plot();
if (pUnit == this)
{
return false;
}
if (!pUnit->isGroupHead())
{
return false;
}
// Don't join a unit that was itself wondering what to do this turn
if ( (static_cast<const CvUnitAI*>(pUnit))->m_contractsLastEstablishedTurn == GC.getGameINLINE().getGameTurn() &&
(m_contractualState == CONTRACTUAL_STATE_AWAITING_WORK || m_contractualState == CONTRACTUAL_STATE_NO_WORK_FOUND))
{
return false;
}
if (pGroup == getGroup())
{
return false;
}
if (pUnit->isCargo())
{
return false;
}
if (pUnit->AI_getUnitAIType() != eUnitAI)
{
return false;
}
switch (pGroup->AI_getMissionAIType())
{
case MISSIONAI_GUARD_CITY:
// do not join groups that are guarding cities
// intentional fallthrough
case MISSIONAI_LOAD_SETTLER:
case MISSIONAI_LOAD_ASSAULT:
case MISSIONAI_LOAD_SPECIAL:
// do not join groups that are loading into transports (we might not fit and get stuck in loop forever)
return false;
break;
default:
break;
}
if (pGroup->getActivityType() == ACTIVITY_HEAL)
{
// do not attempt to join groups which are healing this turn
// (healing is cleared every turn for automated groups, so we know we pushed a heal this turn)
return false;
}
if (!canJoinGroup(pPlot, pGroup))
to canJoinGroup(etc...) and following that to
Code:
bool CvUnit::canJoinGroup(const CvPlot* pPlot, CvSelectionGroup* pSelectionGroup) const
{
CvUnit* pHeadUnit;
// do not allow someone to join a group that is about to be split apart
// this prevents a case of a never-ending turn
if (pSelectionGroup->AI_isForceSeparate())
{
return false;
}
if (pSelectionGroup->getOwnerINLINE() == NO_PLAYER)
{
pHeadUnit = pSelectionGroup->getHeadUnit();
if (pHeadUnit != NULL)
{
if (pHeadUnit->getOwnerINLINE() != getOwnerINLINE())
{
return false;
}
}
}
else
{
if (pSelectionGroup->getOwnerINLINE() != getOwnerINLINE())
{
return false;
}
}
if (pSelectionGroup->getNumUnits() > 0)
{
if (!(pSelectionGroup->atPlot(pPlot)))
{
return false;
}
// Can't join a group that is loaded onto a transport as this
// would bypass the transport's record of what units it has on
// board
if (pSelectionGroup->getHeadUnit()->isCargo())
{
if(pSelectionGroup->getHeadUnit()->isHuman())
{
if(pSelectionGroup->getHeadUnit()->getTransportUnit() == getTransportUnit())
{
return true;
}
}
return false;
}
if (pSelectionGroup->getDomainType() != getDomainType())
{
return false;
}
}
return true;
Apparently, based on some evaluating I've been doing on how group counts can exceed unit counts, a group can't be eliminated until the end of the round after its last unit has died, at which point it THEN clears out but until then that means there can be a hollow group shell with an AI setting and all based on the unit that died that used to belong to it.
So with this being possible, does this last bit mean that if a group doesn't have any units in it at all then it can easily default to being capable of accepting a new member merging into it?
Could this be one of the explanations as to why we have a number of healers standing around doing very little? Healers may be selected as city defense units (which is one thing I'm also looking to fix by generating a new ai for them) so may find themselves joining a recently killed settler stack (of which there's only ever one unit to guard them?!? No wonder they're getting killed out there with just one healer to guard them as they try to get to the city site!) that doesn't even have a settler in it - so then mulls about wondering how to resolve itself?
Perhaps here we could solve a few issues by making a group with 0 units return a false right away so it is then destined to be cleaned out of the system rather than ever having a unit join it and reinvigorate it with new purposelessness?
Does anyone concur or am I reading anything wrong here?
EDIT: Testing found this to be a somewhat incorrect assertion - it keeps units from being able to join groups they generate for themselves so is a major problem to try to resolve this in this manner. I still think it may be possible that empty groups are potentially being joined here unintentionally but it's probably not often and I'm not sure how one would keep that from happening since an empty group must be joinable for a unit to generate one for itself and join it.
And additionally, I'd like to at least enforce 2 units to protect a settler so would changing this:
Code:
if (AI_group(UNITAI_SETTLE, 2, -1, -1, false, false, false, 1, true))
to this:
Code:
if (AI_group(UNITAI_SETTLE, 3, -1, -1, false, false, false, 1, true))
at least be one of the steps necessary in this?
One thing I'm also wondering and I'll eventually find it with further analysis I'm sure but in general terms, how does a group get set to MISSION_REGROUP? Is this done by the contract broker, when the unit is trained, or only after it's somehow lost it's previous group and it's focus somehow? (or other?)
Anyhow, as I said, I'm being very patient and thorough with this process and I'm only spotting a few things along the way. For the most part I'm just trying to learn the existing structures so I can more readily create a new one that works very appropriately - but if we can fix some issues as we go that'd be cool too considering how desperately awful some of the AI decisions have been of late.