AREAAI & how often does the AI run through its unitAI logic?

Maniac

Apolyton Sage
Joined
Nov 27, 2004
Messages
5,603
Location
Gent, Belgium
In Planetfall the AI declares much less wars than you'd expect. DanF5771 recently loooked into it and found some causes. Unfortunately that does not yet give me any solutions.

(the spoilered part has been answered)
Spoiler :
The first problem DanF mentioned is the spamming of defensive units. I think I may have found the cause of that. But to be sure I was wondering if you could tell me how C++ does basic algebra.
If you have in the code:
x - y / z
does C++ calculate '(x - y) / z' or 'x - (y / z)' ?

If it's the first one '(x - y) / z', I have accidentally caused the AI to want to produce 33% to 50% more floating defenders or so.


The second problem:

Planetfall allows land units to travel over water inside your cultural territory. This has created the following problem. DanF:
I noticed that Planetfall's special movement rules do indeed get the AIs into trouble, especially the movement of land units over water. On the next turn Deirdre's SoD enters the lake south of Gaia's High Garden, since the pathfinder dictates to go the fastest route (2-moves for land units over water). But the units leaving the landmass results in them going from one area (the continent, for which Deirdre has calculated the AreaAI AREAAI_OFFENSIVE) to a different area (the lake, with AREAAI_NEUTRAL = the default AreaAIType for water areas).

As the AreaAI is crucial for the units' decisions and a new decision is made after the usage of each single movement point, the SoD on the lake tile forgets about all the planned warring and heads straight back to Gaia's Landing. The monster-SoD is caught in a loop, repeating these two movements each turn. :run:

After world buildering in a Mag-Tube the SoD starting from Gaia's Landing chooses the faster route over land (never leaving the area) so that Deirdre DoWs after two turns.

One special problem here is that Deirdre's stack is a mix of 1:move: and 2:move: units. The 2:move: Rover spends 1 of his 2 movement points when entering the lake which triggers a new decision for the whole selection group. If for example you give Santiago a SoD of 4 ATTACK_CITY Walkers (1:move:) in Sparta Command she will move this stack 2 tiles to the SE (x=59;y=36) on a path leading E - SE - S with the first 2 steps over water (she has the tech Pressure Dome for +1:move: over water; movement starts in MY2206 after she has finished her preparations and switches to WARPLAN_TOTAL). But since this movement is just one swift step of the whole 1:move:-group, she doesn't think about her plans while having her SoD on the water and will then continue her assault on University Base.

One thought I had was to, instead of looking at the area AI of the plot the unit is on, look at the CvSelectionGroup::lastMissionPlot if it has one. However if the AI constantly cancels and re-examines its missions, this probably wouldn't make any difference. So I need to know when exactly, and where in the SDK, the AI decides to re-examine what it will do with a unit. Can you guys tell me that?

Is it truly after every movement point?? :confused: How then does the AI ever get anything done? If the AI constantly cancels all its MISSION_MOVE_TO, MISSION_ROUTE_TO etc missions, it seems to me on first sight the AI would always go :run: and change its mind even without area switching.
 
The first problem DanF mentioned is the spamming of defensive units. I think I may have found the cause of that. But to be sure I was wondering if you could tell me how C++ does basic algebra.
If you have in the code:
x - y / z
does C++ calculate '(x - y) / z' or 'x - (y / z)' ?

C++, like most programming languages, follows standard mathematical rules of operator precedence (with modifications for the new operations added by the language). So, it would be x-(y/z).
 
No idea for the real problem, but am i right, that the problem would not occur, if all the units had the same movement speed on water?
If yes: Can't you create a xml tag, that units have only the half speed on water? maybe as a promotion, like the FeaterDoubleMoves from Woodsman, but inverted and for terrain.
 
am i right, that the problem would not occur, if all the units had the same movement speed on water?

I don't think so.

In other news: something I remembered: the first piece of code in the worker AI is that the worker should retreat to a city if it is outside the owner's territory. Yet the AI still sometimes builds roads to connect cities, even if the road has to be built outside the owners territory. So it definitely can't be each turn for every single unit that the AI is recalculated.
 
It's not uncommon for a unit's AI to get called multiple times in one turn. You could do the following to monitor what is happening.

In CvGameUtils.AI_unitUpdate:
pUnit = argsList[0]
display = "name = %s, id = %d, x = %d, y = %d" % (pUnit.getName(), pUnit.getID(), pUnit.getX(), pUnit.getY())
CyInterface().addImmediateMessage(display, "")
CvUtil.pyPrint(display)

The last two lines print the information to the text area at the top of the screen and to the debug file.

You would probably want to do this on a map with only a few units available to the AI or else you will have a lot of text to sort through.
 
Okay, I may have figured it out. The central function is: bool CvSelectionGroupAI::AI_update() - see spoiler

Spoiler :
Code:
bool CvSelectionGroupAI::AI_update()
{
	CLLNode<IDInfo>* pEntityNode;
	CvUnit* pLoopUnit;
	bool bDead;
	bool bFollow;

	PROFILE("CvSelectionGroupAI::AI_update");

	FAssert(getOwnerINLINE() != NO_PLAYER);

	if (!AI_isControlled())
	{
		return false;
	}

	if (getNumUnits() == 0)
	{
		return false;
	}

	if (isForceUpdate())
	{
		clearMissionQueue(); // XXX ???
		setActivityType(ACTIVITY_AWAKE);
		setForceUpdate(false);

		// if we are in the middle of attacking with a stack, cancel it
		AI_cancelGroupAttack();
	}

	FAssert(!(GET_PLAYER(getOwnerINLINE()).isAutoMoves()));

	int iTempHack = 0; // XXX

	bDead = false;

	bool bFailedAlreadyFighting = false;
	while ((m_bGroupAttack && !bFailedAlreadyFighting) || readyToMove())
	{
		iTempHack++;
		if (iTempHack > 100)
		{
			//FAssert(false); // Planetfall Maniac YYY
			CvUnit* pHeadUnit = getHeadUnit();
			if (NULL != pHeadUnit)
			{
				if (GC.getLogging())
				{
					TCHAR szOut[1024];
					CvWString szTempString;
					getUnitAIString(szTempString, pHeadUnit->AI_getUnitAIType());
					sprintf(szOut, "Unit stuck in loop: %S(%S)[%d, %d] (%S)\n", pHeadUnit->getName().GetCString(), GET_PLAYER(pHeadUnit->getOwnerINLINE()).getName(),
						pHeadUnit->getX_INLINE(), pHeadUnit->getY_INLINE(), szTempString.GetCString());
					gDLL->messageControlLog(szOut);
				}

				pHeadUnit->finishMoves();
			}
			break;
		}

		// if we want to force the group to attack, force another attack
		if (m_bGroupAttack)
		{
			m_bGroupAttack = false;

			groupAttack(m_iGroupAttackX, m_iGroupAttackY, MOVE_DIRECT_ATTACK, bFailedAlreadyFighting);
		}
		// else pick AI action
		else
		{
			CvUnit* pHeadUnit = getHeadUnit();

			if (pHeadUnit == NULL || pHeadUnit->isDelayedDeath())
			{
				break;
			}

			resetPath();

			if (pHeadUnit->AI_update())
			{
				// AI_update returns true when we should abort the loop and wait until next slice
				break;
			}
		}

		if (doDelayedDeath())
		{
			bDead = true;
			break;
		}

		// if no longer group attacking, and force separate is true, then bail, decide what to do after group is split up
		// (UnitAI of head unit may have changed)
		if (!m_bGroupAttack && AI_isForceSeparate())
		{
			AI_separate();	// pointers could become invalid...
			return true;
		}
	}

	if (!bDead)
	{
		if (!isHuman())
		{
			bFollow = false;

			// if we not group attacking, then check for follow action
			if (!m_bGroupAttack)
			{
				pEntityNode = headUnitNode();

				while ((pEntityNode != NULL) && readyToMove(true))
				{
					pLoopUnit = ::getUnit(pEntityNode->m_data);
					pEntityNode = nextUnitNode(pEntityNode);

					if (pLoopUnit->canMove())
					{
						resetPath();

						if (pLoopUnit->AI_follow())
						{
							bFollow = true;
							break;
						}
					}
				}
			}

			if (doDelayedDeath())
			{
				bDead = true;
			}

			if (!bDead)
			{
				if (!bFollow && readyToMove(true))
				{
					pushMission(MISSION_SKIP);
				}
			}
		}
	}

	if (bDead)
	{
		return true;
	}

	return (isBusy() || isCargoBusy());
}

If this line returns true
Code:
while ((m_bGroupAttack && !bFailedAlreadyFighting) || readyToMove())
then the SelectionGroup will update its AI.

This is the readyToMove function:
Code:
return (((bAny) ? canAnyMove() : canAllMove()) && (headMissionQueueNode() == NULL) && (getActivityType() == ACTIVITY_AWAKE) && !isBusy() && !isCargoBusy());

For a selectiongroup already on a MOVE_TO mission, I think the headMissionQueueNode() would be != NULL, and the getActivityType() == would be ACTIVITY_MISSION. So I think this function would return false, and the whole while loop gets skipped.

Later on the function however, there is this:

Code:
if (!bFollow && readyToMove(true))
				{
					pushMission(MISSION_SKIP);
				}

Now the readyToMove can return true if ANY unit of the selectiongroup still has movement points left. However I should say I don't understand how it could return true in the case of a selectiongroup in the middle of moving somewhere. But it's the best explanation for the observed behaviour I can think of so far.

Anyway, so if the if condition returns true because there are still units left in the group with movement points, the SKIP mission will be pushed. This clears all missions, and as a consequence, if the group ended its turn on a water plot, might lead to dramatically different AI decisions the next turn.

Now I'm wondering if, to avoid the whole mission queue being cleared, if I can replace 'pushMission(MISSION_SKIP); with simply 'return true;' Or 'return false;' I actually am not sure what I am supposed to write.

Or would it lead to problems if I returned the function while there were still some units without movement points left?
 
Back
Top Bottom