How do AI's handle items like Orthus' Axe?

davidlallen

Deity
Joined
Apr 28, 2008
Messages
4,743
Location
California
I am starting a new mod and it will have magic items, similar to Orthus' Axe. I do not want to drag in all of the large FFH changes, let alone mod-mods, and I am implementing the items in a very simple way.

But, I am curious to know how the AI handles items such as Orthus' Axe. I understand that some of the mod-mods such as wild mana may do this better. I would like to know what the behavior looks like, and if any of the developers can comment, I would like to know how this is handled at the code level. For example, have item-related changes been made in the unitai's, or only by separate intervention in python?

There are three behaviors which seem needed.

1. Detecting that an item is available, for example dropped after a combat result, and sending a unit to pick it up.

2. Determining which is the best unit to permanently carry an item, and delivering it to that unit.

3. Once the unit has an item, anything special for using the item. For Orthus' Axe, maybe nothing is needed here, since it gives a general +1 combat. But for other items, which have special abillities, the AI needs to position the unit with the item and then use the item at the right time.

Can anybody give any details on this?
 
In base FfH, if they happen to be standing on the tile where the item is, they pick it up. Typically the AI won't move the items between units much, as I recall though the unit who is taking the item CAN grab it from the unit holding the item if they place a significantly higher value on the promotion stats than the unit who currently holds it.

In Fall Further, I have all items drop as treasure chests instead of unique graphical units. Any AI unit who is set on EXPLORE AI type will actively move to the tile which holds the treasure chest to pick up any items there. But due to how explore works, they have to happen to be in range of that tile (about 3-5 tiles as I recall), and of course the AI has to be running explore types, so typically it means only near or beyond the borders of their land, and only early in the game will the AI successfully pick up items.

The base FfH work and Fall Further work mentioned is all in the DLL. FfH method is completely through the routine to choose the best spell to cast for the turn (one of the considerations is checking if it will give you a promotion, and how highly you value that promotion. Though I don't recall it checking if you will remove a promotion from one of your own units and subtracting off how highly THEY value the promotion, which would lead to two warriors tossing the axe back and forth between them...), and the exploration units moving toward items is through UnitAI's.


Setting up special rules once you have a certain item would be best done with a PyPerTurn on the promotion, or otherwise making it so that the promotion itself modifies the unit actions, probably through hefty python for more creative items.
 
Thanks! There may be some room for improvement in FFH and its mods. But, I would still like to think out the most efficient ways to do some of these things.

1. For picking up an item in the same plot, the only way I can think to do this is that each unit should check, all other units in the plot, to see if they are items. For stacks of doom, that becomes an "order of N squared" operation. Each of 20 units must check the other 19 units. You mention that this happens as a choice of the best spell in FFH. But, how does the unit know what spells are available related to items? Is that property placed onto the plot? What if there are multiple items in the plot?

2. For picking up a dropped item, I have seen Orthus' axe lying around inside the cultural borders of AI's. I have not investigated why. Maybe Orthus was with several barbarians, Orthus was killed but the rest of the stack survived, and the barbs didn't pick it up. The easy case is where some unit is in the same plot and can pick it up; but the other case is one I am worried about. I will have dragons, with loot which can be stolen. Think of the Acheron's Hoard item in FFH, only you can steal it, and he will get mad and fly after you. If he kills off the stack carrying the item, the item could be dropped basically in the middle of nowhere. Or it could be dropped inside a cultural control.

Currently, I have an approach somewhat similar to yours where the plot generating an item is like a goody hut, and it "attracts" units with UNITAI_EXPLORE. So a recon type unit must be nearby. An item could sit in cultural borders and no scout might ever wander by.

I would hate to have every unit do this type of search, since the number of unclaimed items is often zero and always small. I was thinking of an inverse operation, where some code on the *item* searches for nearby AI units and attracts them, like a beacon. The good news is any unit could come by; but it may "distract" SOD's on a particular objective. Even so, in a large civ, an item could sit unclaimed inside borders, or visible just outside borders.

Perhaps an unclaimed item inside borders should magically appear at the nearest city. That reduces some micromanagement but may cause some confusion. It is similar to popping a goody hut with a border expansion. Suddenly a message pops up, "The villagers give you gold!" and you don't know quite why.

3. The overall AI could spot unclaimed items which are visible, and detail a unit to go there. This should not be part of the *unit* AI since each unit should not have to check; but I am not sure where this could go in the overall AI scheme.

4. If items need to be retrieved and brought back, the carrying unit should not be distracted, and it may need an escort. I have not watched how Acheron's Hoard is handled by the AI; but basically you need the unit to beeline to a city. If the carrying unit is weak you may want to send escorts to meet it.

In another mod I have handled this "beeline to a city" with a happy accident; the UNITAI_CITY_DEFENDER happens to beeline to the nearest city if it is not in a city. So I had a "refugee" unit which could appear, and it would automatically head for a city. However, it would often get eaten by monsters since the AI never escorted it. In that case the unit had only the one UNITAI choice since it had no other purpose.

I can use the same approach as a starter; if a unit picks up an item and cannot use it effectively (scout picks up magic sword, anybody picks up a Hoard item) I can change their UNITAI to city defender. But, I haven't tried this, what happens if a weak unit is given this UNITAI? Are there conditions under which the overall AI may assign it a different mission? For example, the AI might set a scout back to UNITAI_EXPLORE, instead of letting it complete the city mission.
 
1. One of the potential requirements for a spell is that a specific UnitType is required on the tile. The data of filling that condition isn't placed on the tile itself, but it IS a very short routine to do the check in the DLL. I haven't slipped a profile hook into it directly to verify, but the "mother process" of the spell selection itself doesn't profile out to be a significant chunk of time.

2. If you have forced AI sending the dragon after the treasure holder, then it is pretty trivial to modify it to also send him after the treasure itself and pick it up. In cases where you see the Axe laying on the ground, the issue was destruction of the entire stack of units without moving onto the tile (so the last unit of the stack suicided on your defenders most likely). If there ever is an AI unit on that tile, they WILL pick up the axe, unless they are unable to cast a spell that turn (already did) or value another spell which they are capable of casting more heavily.

If you had the item itself seek out wielders, you could have it seek specific UnitAIs. UNITAI_RESERVE should be available in any city, and it is designed to be able to move around a little bit in the AI's considerations as I recall. So grabbing one of them won't mess up the AI plans too heavily. You could also check the size of the group the unit is in to ensure you don't snag a SoD, and check the threat value for the unit's tile and the item's tile to ensure you aren't placing the unit in danger, or potentially removing a solid defender from a vital tile.

You could have items automatically move 1 tile at a time toward the nearest village. Claim that it is picked up by a merchant caravan and they are ferrying it to market or something. The movement would be a forced thing, leaving the item with 0 movement points and DOMAIN_IMMOBILE still.

3. As I recall, there is a "beacon" function already in CvUnitAI where a unit will request that an escort is provided for it. If that is the case, it would be a simple matter of inventing a UNITAI_ITEM and having it utilize this function to broadcast the need for an escort. The only catch there of course is that the UnitAI functions only check units under the same owner, and items are allowed (in FfH at least) to be picked up by anyone (in FF actually, all dropped items belong to the Barbarians). But you could modify the function to allow this unitAI to look for ANY units, not just allies.

4. In FfH most items benefit the unit instead of wanting to be in a city. The Hoard is actually left IN the city where the dragon was unless you raze it. Putting an item back into a city when it is appropriate to do so relies again on the selection of the ideal spell to cast and hoping there isn't a better choice for the unit who happens to be holding the item, and that they happen to be IN a city sometime to drop the item in the first place. No AI is written to specifically seek to return the item to an appropriate location.

The catch for your escort needs is the same as I mentioned in 3, there is a function used by workers and settlers to seek out an escort party before trying to move. You can also utilize the SafeMove function so that the unit will avoid dangerous tiles. Then only high movement or invisible units will potentially eat the unit.
 
I have a lot of this working, but I am stuck on one part. The AI and human player can now visit the towers and explore to spawn the items. When the items exist as objects, they trigger a python routine at the beginning of the turn called "PickMeUp".

So far the routine doesn't do anything. Here is what the routine will do. (a) If any AI unit which can pick it up is in the same plot, the unit will pick it up. (b) If any unit which can pick it up is adjacent (approximately, the item is visible) the unit will move to the plot. (c) If it is in owned territory, it will request a unit from the nearest city to pick it up. At most one unit per civ will be attracted.

When a unit picks up the item, it may trigger a different python routine called "TakeMeHome". If the unit is an appropriate combat unit, such as a warrior picking up a sword, the routine is not triggered. Otherwise, and in particular for a treasure chest, the unit is directed to move to the nearest friendly city. A chest in a friendly city is automatically opened; any other item is dropped which triggers PickMeUp again, hopefully to attract a more appropriate unit.

The part I am stuck on is that the created item belongs to the civ which found it. Some other civs, including barbarians, are at war with the civ so they attack and destroy the item. If I make the item belong to the barbarian civ and use <Capture>, then moving any other player onto the plot triggers a capture. This seems to delete and partly rebuild the unit, and it displays a message about the item being destroyed.

So my question is, how can I allow any player, including the barb player, to move onto these items without attacking or capturing? Do I need to introduce another fake player to own all the items, and be at peace with everybody? Then I have to keep that fake civ off the scoreboard, the other advisors, etc.
 
As a temporary hack, I added the fake player. Now any unit can enter the plot of the item without causing fights. (I think units with bAlwaysHostile are an exception, I think they destroy the unit anyway. But I have not proved that yet.)

However, pushing the mission does not work. The DebugPrint's tell me the routine is triggering, and pushing the mission to the units. However, those units -- which are mostly scouts, on UNITAI_EXPLORE -- just ignore the push and go on exploring. Here is the code. Note that I call canBuild to make sure that the unit can execute the build mission.
Code:
	def ItemPickMeUp(self, argsList):
		pItem = argsList[0]
		iX = pItem.getX() ; iY = pItem.getY()
		self.DebugPrint("Turn %d: PMU %d,%d" % (CyGame().getElapsedGameTurns(), iX, iY))
		pPlot = pItem.plot()
		for iUnit in xrange(pPlot.getNumUnits()):
			pUnit = pPlot.getUnit(iUnit)
			if pUnit.canBuild(pPlot, self.iBTake, false):
				pUnit.getGroup().pushMission(MissionTypes.MISSION_BUILD, self.iBTake, -1, 0, false, false, MissionAITypes.MISSIONAI_BUILD, pPlot, pUnit)
				self.DebugPrint("Pushed")

I assume that when the unit's turn is processed, it weights all the different possible missions, discards the one I pushed since it doesn't see any benefit in doing it, and then picks some other mission such as exploring again.

How can I force my mission to be executed? Or how can I prove if there is some other problem?
 
the golden rule is to only push missions in the update cycle. If you push a mission somewhere else, the selectiongroup might be busy. Try to set the bAppend parameter to true.
 
Thanks for the info. I am not familiar enough with the overall flow to understand your golden rule. Which functions are involved in the update cycle? The unit which is the item is performing this call as the first step in CvUnit::doTurn. When is a better place to do this, so it will affect the potential carrier unit on the same turn?

The potential carrier unit is often a scout with UNITAI_EXPLORE. If I "append" my mission, that means it will only take effect after the exploration is done. I am not sure exactly when that happens, but I thought it was only when the unit gets stuck and cannot reach any unexplored territory. That may be many, many turns after I push this mission. Is there something about appending which I have missed?

EDIT: easy enough to try changing bAppend to true, but it did not make any difference.
 
Since the issue seems to be that you are sending an order to a unit who is not in their active turn, change how you are approaching it slightly.

The item will find a unit who it wants to pick it up. It sends that unit the coordinates of the item. Unit stores those values in CvUnit.cpp.

At the start of each unit's turn, if the coordinate values are non-negative, then it pushes a mission for itself to travel to those coordinates, and resets them back to -1,-1.
 
The turn cycle is in CvPlayer::setTurnActive. Its CvPlayer::doTurn, CvPlayer::doTurnUnits, etc.

The update cycle is in CvPlayer::AI_unitUpdate and is called in CvGame::updateMoves() (unlike some other AI functions this one is also called for human player).

usually there is no need to append a mission. However if a unit is busy and the mission isn't appended, then nothing will happen.

try to use CySelectionGroup::canStartMission instead of the canbuild command. Maybe that reveals more information
 
I have tried a few things, and I am not able to get the "take item" missions to run. I have tested that canStartMission also returns true when I push the mission. I have added some print statements into CvSelectionGroup:: pushMission and I can see it being called with the expected arguments; "isBusy" is false; and the function goes all the way to the end. So the mission is getting pushed to the target unit.

I have sprinkled more print statements around in CvSelectionGroup.cpp, but I cannot understand the flow of control. I can see CvSelectionGroup::doTurn is called. I have added print statements into CvSelectionGroup::startMission for when the "take item" mission starts, and this is *not* being called. But, I do not understand how startMission gets called for anything. I cannot see the connection from doTurn to startMission. What else can I check? Can anybody explain to me the way that missions get started during the turn?
 
why do you use the MISSION_BUILD to pick up an item? Anyway, if you aren't already at the plot where you want to build something you probably need to push a MISSION_MOVE first. If you want an example, search the cvgameutils.py in FFH or Wildmana for MISSION_BUILD.
 
Thanks for the suggestion. My call to pushMission is same as the first example I found in ffh, AI_Mage_UPGRADE_MANA. Since I am basing on vanilla and there are no "item" or "pick up" mechanics available, I am using build actions for this. So there are build actions for explore-lair, take, drop. They work correctly for the human player. (This seems to be a very inexpensive way to make custom action buttons, and I am a little surprised not to see this in any other mod.)

One of the checks in canBuild for the "take item" build mission is to be on the same plot, and canBuild is passing when I push the mission; so I know I do not need to push a move-to mission first.

I have created a fake player, at least temporarily, since I cannot find a good way to make the item units accessible to all civs otherwise. I want the processing to be on the item unit since these are few, rather than having every unit scan to see if there is an item nearby. So, the call to push the mission is happening during the fake player's doTurn loop.

Compared to the ffh example, or previous code of my own which is working, there are two differences: the pushing unit is a different player and unit, and the push is happening during doTurn rather than during AI_unitUpdate. I can see that the push is working, so I do not think either of those are the problem. Somehow, the mission is getting discarded after it gets pushed, and that is what I cannot figure out.
 
if you don't append, then
pushmission calls insertAtEndMissionQueue, which calls activateHeadMission, which calls startMission. Seems to break down somewhere for you.
 
That is helpful. In my case, the selgroup for which the mission is being pushed, is not active. In fact that player is not even active. So it is unclear whether I should use "append". It doesn't work either way; but what I want is to just put the mission onto the queue without activating anything. And then when the selgroup does become active, I want it to execute this mission.

When the unit becomes active, shouldn't there be a call to startmission if it doesn't have an active mission? If this exists, I can't find the path.
 
When the unit becomes active, shouldn't there be a call to startmission if it doesn't have an active mission? If this exists, I can't find the path.

looks to be
CvSelectionGroup::autoMission()
which is called in the update cycle. Did you check if the missionqueque gets modified in your case.
 
After some quality time in the debugger on a map with a single AI unit, I have learned some more. The reason for the "golden rule" mentioned above now seems clear. When a unit goes to update, it calls CvUnit::AI_update. This is basically a big switch statement around the UnitAIType. Each branch of the switch is highly likely to call pushMission(... append=false ...), without looking to see if the unit was already on some mission. So, there is no way to push any mission from outside. The only way to make a unit do something is to handle it in AI_update (possibly in python), and push the mission from the unit every turn.

I guess there is no such thing as a python AI which really performs a multi-turn mission, even to go from point A to point B. It may look that way. But, every turn the python is called it re-pushes the mission to point B, or whatever point looks good then. So the mission itself does not really last multiple turns. For the human player, it is possible to queue missions. People often do this for workers. But I now believe the AI does not ever do this.

So, with this new understanding, it seems the only way to get the persistent behavior that I want is to add a UNITAI and associated code. Perhaps the beacon unit can set the UNITAI of the desired carrying unit to this new value, such as UNITAI_PICKMEUP. Then, it should be possible to store the destination in a moveto mission. The unitai would allow the moveto mission to continue until it succeeded, and then do whatever else it should do such as changing to UNITAI_TAKEMEHOME.

This is much more complicated than I expected.

Now I am also nervous about the general AI changing the unit's unitaitype underneath me. If the general AI decides that my carrier unit is better off defending a city, it can remove the UNITAI_PICKMEUP and replace it with UNITAI_CITY_DEFENDER, and then again the persistent mission is lost. Since most units can have multiple legal UNITAIs, that must mean the general AI can reassign them as needed.

Can anybody shed light on where/how the general AI reassigns UNITAIs?

EDIT: I can successfully change a unitai and have it "stick". Looking through the code, it *seems* like unitai's are only changed in a few special cases. I guess the cvplayer code must set the unitai when a unit is built, based on its intended function, and then the unitai stays constant for the whole game. So I can build a new unitai for carrying items and it ""should"" work.
 
As you found in your edit, the AI will typically assign a UNITAI at creation of the unit and never switch it. It prefers to build a new unit when it finds a need for more of a specific UNITAI type.


Instead of creating a false player, you can look at Fall Further's code for <bNeverHostile> and <bCommunalProperty> to allow peaceful interaction with the items for all civs, no matter who technically owns it.
 
After some quality time in the debugger on a map with a single AI unit, I have learned some more. The reason for the "golden rule" mentioned above now seems clear. When a unit goes to update, it calls CvUnit::AI_update. This is basically a big switch statement around the UnitAIType. Each branch of the switch is highly likely to call pushMission(... append=false ...), without looking to see if the unit was already on some mission. So, there is no way to push any mission from outside. The only way to make a unit do something is to handle it in AI_update (possibly in python), and push the mission from the unit every turn.
you don't need to push the mission every turn. If a unit has ACTIVITIY_MISSION the AI_update function isn't called. However if you push a MISSION_MOVE_TO the mission might be canceled ,depends on the mission flag you set. if you use MOVE_DIRECT_ATTACK you should be fine.

I handle items by letting the GROUPFLAG_DEFENSE_NEW stack (there is only one per player typically) scan the player area and then if necessary send out a unit to grab the equipment. An adept for a spellbook, non magic units for other equipment like Orthus axe. Not sure if I even defined a new Groupflag for that. But if you don't modmod FFH/Wildmana you won't have that the luxury of groupflags.
 
Thanks for the info on <bNeverHostile> and <bCommunalProperty> and GROUPFLAG_DEFENSE_NEW. I will dig into these and see if I can get working what I want.
 
Top Bottom