How to force unit to attack a specific unit?

Paolo80

Chieftain
Joined
Dec 20, 2019
Messages
85
Hello guys,

does someone know how to force a AI unit to attack a specific unit?

For example I would want an horse archer to attack a catapult, if it stands alone.

Is it possible doing it via python or is it necessary to change sdk files?
 
I'm not sure if it works for specific unit classes but there's an XML tag to target unit types (mounted, melee, siege, etc). The Ballista Elephant uses it to target mounted units first
 
Maybe I badly explained. I wanted to force a unit to attack another unit near it, that it wouldn't attack.

I try the method pushMission in CyGroup object, as following:

Code:
if pUnit.getUnitType() == gc.getInfoTypeForString('UNIT_HORSE_ARCHER'):
                iUnit = gc.getInfoTypeForString('UNIT_CATAPULT')
                pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                iMission = MissionTypes.MISSION_MOVE_TO_UNIT
                iMissionAIType = MissionAITypes.NO_MISSIONAI
                pUnit.getGroup().pushMission(iMission, 18, iUnit, 0, False, True, iMissionAIType, pPlot, pUnit)

How can I check if it works?
 
MISSION_MOVE_TO_UNIT is only for friendly units. Attack is MISSION_MOVE_TO.
You could try using this simple wrapper:
CySelectionGroup:: pushMoveToMission(int iX, int iY)
You'll need coordinates of the target unit though. I guess you'll have to go through all plots in a range around the Horse Archer (pUnit) and then check if there's a Catapult and if it can be reached in a single move.
How can I check if it works?
For a start, perhaps
print "Forcing attack from (%d,%d) to (%d,%d)" % (pUnit.getX(), pUnit.getY(), iX, iY)
before pushing the mission (prints to PythonDbg.log).
 
No, it doesn't work. The unit doesn't change AI mission. Maybe have I to extract unit from selection group?
 
I don't even see a Python-exported function that would let you do that. Maybe CyUnit::setXY, as a side-effect. A group should be able to carry out a move-to mission, also if it involves an attack, but, if you specifically want a Horse Archer to attack, then it would be better to have that unit in a separate group. Can you get a group or unit to just move to some adjacent tile (without attacking)?
 
I want a single specific unit to attack another specific unit, not group.

Besides horse archer, I've made four new units: heretic, missionary, infector, monatto.

Only missionary can kill heretic and only monatto can kill infector. I want force missionary to attack heretic and monatto to attack infector.
 
Last edited:
So you're adding code to AI_unitUpdate (CvGameInterface.py) I assume. That's what I'd try anyway. The function receives a unit object (let's say 'u') as parameter and can return 0 to let the DLL move the unit. I guess you should return 1 if you push a mission – to make sure that the DLL doesn't push a different mission. When the DLL moves a unit, then it may decide to break the group up, but usually it'll move the entire group. So, to make sure that you check every Horse Archer etc., it would be desirable to go through all units in the group of u; however, it seems that the interface for going through all units in a group isn't exposed to Python. Maybe you could go through all units in the plot instead (CyPlot::getUnit(int iIndex)). At any rate, you can at least catch units that happen to lead a group.

To move u into separate (single-unit) group, u.setXY(u.getX(), u.getY()) might work. That's not how setXY is supposed to be used – the DLL code states through an assertion that the target coordinates are supposed to differ from the unit's current coordinates – but perhaps it'll work anyway.

Though, before breaking up the group, you'd have to look for a nearby target,
for (deltaX in range(-...
check that the target is alone and has the proper unit type, and check CySelectionGroup::generatePath to ensure that the target is reachable on the current turn.
But, before writing code for that, I'd do some simple experiment with pushMoveToMission to verify that you can indeed move units this way.
 
So you're adding code to AI_unitUpdate (CvGameInterface.py) I assume. That's what I'd try anyway. The function receives a unit object (let's say 'u') as parameter and can return 0 to let the DLL move the unit. I guess you should return 1 if you push a mission – to make sure that the DLL doesn't push a different mission. When the DLL moves a unit, then it may decide to break the group up, but usually it'll move the entire group. So, to make sure that you check every Horse Archer etc., it would be desirable to go through all units in the group of u; however, it seems that the interface for going through all units in a group isn't exposed to Python. Maybe you could go through all units in the plot instead (CyPlot::getUnit(int iIndex)). At any rate, you can at least catch units that happen to lead a group.

To move u into separate (single-unit) group, u.setXY(u.getX(), u.getY()) might work. That's not how setXY is supposed to be used – the DLL code states through an assertion that the target coordinates are supposed to differ from the unit's current coordinates – but perhaps it'll work anyway.

Though, before breaking up the group, you'd have to look for a nearby target,
for (deltaX in range(-...
check that the target is alone and has the proper unit type, and check CySelectionGroup::generatePath to ensure that the target is reachable on the current turn.
But, before writing code for that, I'd do some simple experiment with pushMoveToMission to verify that you can indeed move units this way.

Ok, I'll wait for you code, if you'll find it.
 
Oh, sorry. :blush: I didn't mean that I was going to test it – I rather dislike writing Python code. :)
It was only meant as a suggestion: "if I were you, I would experiment with that ..."
If you have Python code that looks like it should work and mysteriously doesn't, then I could try to debug it.
 
The problem using unit.setXY is that unit kills always enemy unit. It doesn't attack enemy unit, it substitutes it in its plot
 
Right, but calling setXY with the current coordinates of the attacking unit could be a way to put the attacking unit into a separate group before making a proper attack (push mission).
 
I've almost solved the problem.

Here the code

Code:
for iPlayer in range(gc.getMAX_PLAYERS()):
            for pUnit in PyPlayer(iPlayer).getUnitList():
                for tUnit in PyPlayer(18).getUnitList():
                    iUnit = gc.getInfoTypeForString('UNIT_HORSE_ARCHER')
                    if pUnit.getUnitType() == iUnit:
                        if tUnit.getUnitType() == gc.getInfoTypeForString('UNIT_INFANTRY'):
                            iX = tUnit.getX()
                            iY = tUnit.getY()
                            tPlot = CyMap().plot(iX, iY)
                            pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                           
                            if tPlot.getOwner() == pUnit.getOwner():
                                if pUnit.getGroup().generatePath(pPlot, tPlot, 0, False, None):
                                     iMission = MissionTypes.MISSION_MOVE_TO
                                    iMissionAIType = MissionAITypes.NO_MISSIONAI
                                    CyMap().resetPathDistance()
                                    iDist = CyMap().calculatePathDistance(pPlot, tPlot) * 60
                                   
                                    iMoves = pUnit.movesLeft()
                                 
                                                              
                                    pUnit.setXY(pUnit.getX(), pUnit.getY())
                                   
                                    if iDist > iMoves:
                                       
                                        pUnit.getGroup().pushMission(iMission, iX, iY, 0, False, True, iMissionAIType, pPlot, pUnit)
                                        pUnit.finishMoves()
                                       
                                                       
                                    else:
                                        pUnit.setXY(iX, iY)
                                        pUnit.finishMoves()

There are still some problems:

- When unit archer is in a city, it doesn't move anymore;
- When unit archer kill unit infantry with setXY method, it has still all movement points
- When unit archer reach one plot distance frim unit infantry, it stops
 
The CvUnitAI::AI_anyAttack function in the DLL (same thing also in AI_cityAttack) uses simply
getGroup()->pushMission(MISSION_MOVE_TO, pBestPlot->getX(), pBestPlot->getY())
where pBestPlot is the plot of the best target unit. This uses the default values for the other parameters:
Code:
int iFlags = 0, bool bAppend = false, bool bManual = false, MissionAITypes eMissionAI = NO_MISSIONAI,
CvPlot* pMissionAIPlot = NULL, CvUnit* pMissionAIUnit = NULL
One can't omit default values in a Python-to-DLL call, but pushMoveToMission(iX, iY) should be equivalent to what AI_anyAttack does. AI_anyAttack is used for opportunistic attacks against units that are reachable immediately or in a couple of turns, i.e. pretty similar to what you're doing. So, unless I'm missing something, there should be no need for the
if iDist > iMoves
distinction. I don't suppose you want Horse Archers to target Infantry at the other end of the map, so some sort of distance check will still be needed, Should perhaps also check whether the target plot is revealed to the Horse Archer's team.

Code:
for pUnit in PyPlayer(iPlayer).getUnitList():
   for tUnit in PyPlayer(18).getUnitList():
This looks sensible for a test. Might end up noticeably slowing down the mod once the map becomes crowded with units. generatePath and calculatePathDistance are computationally expensive. (Just a caveat.)
 
I solved, i put the code in CvGameUtils!

Here the final code:

Code:
def AI_unitUpdate(self,argsList):
        pUnit = argsList[0]
       
        for iPlayer in range(gc.getMAX_PLAYERS()):
            for pUnit in PyPlayer(iPlayer).getUnitList():
               
                if pUnit.getUnitType() == gc.getInfoTypeForString('UNIT_CHRISTIAN_MISSIONARY'):
                    for tUnit in PyPlayer(18).getUnitList():
                        if tUnit.getUnitType() == gc.getInfoTypeForString('UNIT_ERETICO'):
                            iX = tUnit.getX()
                            iY = tUnit.getY()
                            tPlot = CyMap().plot(iX, iY)
                            pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                            if tPlot.getOwner() == pUnit.getOwner():
                                if pUnit.generatePath(tPlot, 0, False, None):
                                    pUnit.getGroup().pushMoveToMission(iX, iY)
                                    return True

                elif pUnit.getUnitType() == gc.getInfoTypeForString('UNIT_MONATTO'):
                    for tUnit in PyPlayer(18).getUnitList():
                        if tUnit.getUnitType() == gc.getInfoTypeForString('UNIT_UNTORE'):
                            iX = tUnit.getX()
                            iY = tUnit.getY()
                            tPlot = CyMap().plot(iX, iY)
                            pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                            if tPlot.getOwner() == pUnit.getOwner():
                                if pUnit.generatePath(tPlot, 0, False, None):
                                    pUnit.getGroup().pushMoveToMission(iX, iY)
                                    return True
       
        return False

Now the problem is fasten the routine
 
Last edited:
Since one can, apparently, not go through all units in a group, the fastest approach might indeed be to go through all units owned by the owner of pUnit. However, that should only be done when AI_unitUpdate is called for the first time each turn (and I guess nothing needs to be done when pUnit has a human owner). Perhaps easier to do the whole thing in onBeginPlayerTurn, which is only called once per turn anyway. But I'm not sure if AI units have already been moved at that point.

For a given unit, a range of surrounding plots should be searched for targets to attack. That said, if the targets can only be Barbarian (PyPlayer(18)), then it should be OK to just go through all Barbarian units and check whether their plot is revealed.
 
Finally I wrote this code

Code:
def AI_unitUpdate(self,argsList):
        pUnit = argsList[0]
        

        if pUnit.getUnitType() == gc.getInfoTypeForString('UNIT_CHRISTIAN_MISSIONARY'):
            for iPlayer in range(gc.getMAX_PLAYERS()):
                if gc.getPlayer(iPlayer).isHuman() == False:
                    for tUnit in PyPlayer(18).getUnitList():
                        if tUnit.getUnitType() == gc.getInfoTypeForString('UNIT_ERETICO'):
                            iX = tUnit.getX()
                            iY = tUnit.getY()
                            tPlot = CyMap().plot(iX, iY)
                            pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                            if tPlot.getOwner() == pUnit.getOwner():
                                if pUnit.generatePath(tPlot, 0, False, None):
                                    pUnit.getGroup().pushMoveToMission(iX, iY)
                                    return True

        elif pUnit.getUnitType() == gc.getInfoTypeForString('UNIT_MONATTO'):
            for iPlayer in range(gc.getMAX_PLAYERS()):
                if gc.getPlayer(iPlayer).isHuman() == False:
                    for tUnit in PyPlayer(18).getUnitList():
                        if tUnit.getUnitType() == gc.getInfoTypeForString('UNIT_UNTORE'):
                            iX = tUnit.getX()
                            iY = tUnit.getY()
                            tPlot = CyMap().plot(iX, iY)
                            pPlot = CyMap().plot(pUnit.getX(), pUnit.getY())
                            if tPlot.getOwner() == pUnit.getOwner():
                                if pUnit.generatePath(tPlot, 0, False, None):
                                    pUnit.getGroup().pushMoveToMission(iX, iY)
                                    return True
        
        return False

It runs faster than the previous one and, above all, it works!

Thank you F1rpo for your suggestions.

I inserted this code in my scenario "Italia comunale", if someone wants to test it.

Here the link https://forums.civfanatics.com/resources/italia-comunale.28437/
 
That'll only work for AI units that lead a group; the others could be moved by the AI without a call to AI_unitUpdate. I don't think missionaries form groups at all, but Horse Archers could be lead by other units. Eh, perhaps not a problem in your scenario.
This looks like an oversight:
for tUnit in PyPlayer(18).getUnitList():
Should probably be ...PyPlayer(iPlayer)... Should perhaps also skip iPlayer if its team isn't at war with pUnit.getTeam. The move-to mission won't result in an attack when not at war.
 
Back
Top Bottom