Individual Unit AIs (making Templars cast Psi Storm)

Started by 1337, March 02, 2010, 08:06:10 PM

Previous topic - Next topic

1337

After downloading and investigating some of the various AIs on this forum, I have determined that most of them focus on the "macro" aspect of gameplay, with build orders and such. I have been trying to unravel the "micro" AI to modify individual unit AIs.

I found this gem of information in the TactTerrAI.galaxy (reaper AI code):

void AIThinkReaper (int player, unit aiUnit, unitgroup scanGroup) {
    // **Reaper AI reproduced in XML**
    //  Melee AI does not call this AIThink routine.


    //  Reaper AI is left in TactTerrAI to serve as an example of two different ways to write the same
    //  AI.  To see the xml equivalent of the Reaper AI, look in TacticalData.xml and TargetFindData.xml.
    //  In general, AI implemented in galaxy scripts will be slower than AI implemented in data.
    //  If it is convenient to express AI in data, one should do so.  In this case, converting reaper AI
    //  to xml resulted in a 2x performance gain.


It appears that there are two different sources for the unit AI, and which source is chosen is completely arbitrary. I have scoured the MPQ files for the locations of the AIThink calls and I can't find any. This means that the decision whether to call the AIThink routine or use the XML version is made in the main SCII executable.


This means that some AIThink routines can be modified in a useful way, whereas others do nothing. For those abilities that are XML-based, limited modification of the behavior can be achieved by modifying values in TargetFindData.xml.



other random thoughts (from a later post):
High Templars will cast Psi Storm, but most AIs just don't research it early enough. I just tested a game with 5 Goose v4 Protoss in it. Of the 5 AIs, only 1 (the winning AI) researched Psi Storm, despite the fact that they all followed the same build order otherwise. I assume the other AI Stock requirements were not met, so it never moved on to upgrading Psi Storm. Only the winner actually had enough units in stock, so the AI continued to upgrade.


The Psi Storm AI data is definitely stored in the XML. By editing the Psi Storm entry in TargetFindData.xml, I think I can change the behavior of the Templars with regards to when they choose to cast Psi Storm. I am going to try modifying the "MinCount" and "MinScore" values to see if it changes the Templars discretion.

Kernel64

This is interesting. It could be possible that the AI is not complete in this release of the beta?

ooni

Quote from: 1337 on March 02, 2010, 08:06:10 PM

After downloading and investigating some of the various AIs on this forum, I have determined that most of them focus on the "macro" aspect of gameplay, with build orders and such. I have been trying to unravel the "micro" AI to modify individual unit AIs. My first project is to code the High Templars to use Psionic Storm. Currently, the AI will build Templars if told to in the build order but they just run toward you dumbly without doing anything.


Here's my first hack, which compiles and runs but does nothing...


Replace the entire AIThinkHighTemplar method with the following in TactProtAI.galaxy:
order PsiStorm (int player, unit aiUnit, unitgroup scanGroup) {
    order ord;
//    aifilter filter;
    int psiStormRadius = 2;
//    unitgroup enemyGroup;
//    int enemyCount;
//    int i = 1;
//    region r;
//    unitfilter f;
//    unit enemy;
//    unit target;
//    unitgroup enemyAreaUnits;
//    region enemyArea;
//    int enemyAreaCount = 0;
//    int maxCount = 0;
   point castPoint;
   unitgroup stormGroup;


    ord = AITacticalOrder(player, aiUnit, c_AB_PsiStorm);
    if (ord == null) {
        return null;
    }


//   f = UnitFilterStr("-;Missile,Dead,Stasis");
//   r = RegionCircle(UnitGetPosition(aiUnit), AIRangeHighTemplar(player, aiUnit));
//   enemyGroup = UnitGroupAlliance(player, c_unitAllianceEnemy, r, null, c_noMaxCount);
//   enemyGroup = UnitGroupFilter(null, c_playerAny, enemyGroup, f, 0);
//   
//   enemyCount = UnitGroupCount(enemyGroup, c_unitCountAll);
//   while(i <= enemyCount)
//   {
//      enemy = UnitGroupUnit(enemyGroup,i);
//      enemyArea = RegionCircle(UnitGetPosition(enemy), psiStormRadius);
//      enemyAreaUnits = UnitGroupAlliance(player, c_unitAllianceEnemy, enemyArea, null, c_noMaxCount);
//      enemyAreaCount = UnitGroupCount(UnitGroupFilter(null, c_playerAny, enemyAreaUnits, f, 0),c_unitCountAll);
//      if (maxCount < enemyAreaCount)
//      {
//         target = enemy;
//         maxCount = enemyAreaCount;
//      }
//      i = i + 1;
//   }


   
//    OrderSetTargetPoint(ord,UnitGetPosition(target));


   stormGroup = AIEffectGroup(player, c_EF_PsiStormArea, scanGroup);
   castPoint = AIBestTargetPoint(
        stormGroup,
        3, // min hits
        40, // damage base
        3.0, // score
        psiStormRadius,
        UnitGetPosition(aiUnit),
        AIRangeHighTemplar(player, aiUnit),
        c_unitAttributeNone
    );


    if (castPoint == null) {
        return null;
    }
   
    OrderSetTargetPoint(ord, castPoint);
    if (!UnitOrderIsValid(aiUnit, ord)) {
        return null;
    }
   return ord;
}


void AIThinkHighTemplar (int player, unit aiUnit, unitgroup scanGroup) {
    marker mark;
    order ord;
    unitfilter f;
    region r;
    unitgroup enemyGroup;
    int enemyCount;


    if (AIEvalTacticalData(aiUnit, null)) {
        return;
    }
   
   if (UnitGetPropertyInt(aiUnit, c_unitPropEnergy, c_unitPropCurrent) < 75)
   {
      return;
   }


    //  If we already have a psi storm order, ignore any new orders since psi storm is more important.
    //  If we already have an PhaseShift order, ignore any new PhaseShift orders.
    if (UnitOrderHasAbil(aiUnit, c_AB_PhaseShift) || UnitOrderHasAbil(aiUnit, c_AB_PsiStorm)) {
        return;
    }
        f = UnitFilterStr("-;Missile,Dead,Stasis");
        r = RegionCircle(UnitGetPosition(aiUnit), AIRangeHighTemplar(player, aiUnit));
   enemyGroup = UnitGroupAlliance(player, c_unitAllianceEnemy, r, null, c_noMaxCount);
   enemyCount = UnitGroupCount(enemyGroup, c_unitCountAlive);
        if ((enemyCount < 2) ||
            AIAllyEnemyRatio(player, UnitGetPosition(aiUnit), f, AIRangeHighTemplar(player, aiUnit), c_MinThreshold) > c_EnemyMultiplierHighTemplar) {
            return;
        }


    ord = PsiStorm(player, aiUnit, scanGroup);
    if (ord != null) {
        AICast(aiUnit, ord, c_noMarker, c_castRetreat);
        return;
    }
}

I based my code off of AIThinkInfestor() and its associated InfestedTerrans() method, which work and *ARE USED IN GAME BY THE AI*. This is very important since Infested Terrans is one of the few abilities that the AI will actually use in game.
The code is also similar to AIThinkMothership() and the Vortex() method.
However, my code appears to have no effect. The commented lines in the PsiStorm() method represent my first attempt, and the uncommented lines are a different method of targetfinding, both of which do nothing.


In my build order I force the AI to build High Templars and research Psi Storm but he just never uses the ability.




My hypothesis as to why this code doesn't work is that it never runs. I found this gem of information in the TactTerrAI.galaxy (reaper AI code):

void AIThinkReaper (int player, unit aiUnit, unitgroup scanGroup) {
    // **Reaper AI reproduced in XML**
    //  Melee AI does not call this AIThink routine.


    //  Reaper AI is left in TactTerrAI to serve as an example of two different ways to write the same
    //  AI.  To see the xml equivalent of the Reaper AI, look in TacticalData.xml and TargetFindData.xml.
    //  In general, AI implemented in galaxy scripts will be slower than AI implemented in data.
    //  If it is convenient to express AI in data, one should do so.  In this case, converting reaper AI
    //  to xml resulted in a 2x performance gain.



It appears that there are two different sources for the unit AI, and which source is chosen is completely arbitrary. I have scoured the MPQ files for the locations of the AIThink calls and I can't find any. This means that the decision whether to call the AIThink routine or use the XML version is made in the main SCII executable.


Here's what really confuses me though - Psionic Storm is listed in the XML with all the proper fields in the AbilData, EffectData, TacticalData, TargetFindData, but it still is never used.
The aforementioned Reaper AI is also listed in the XML files, but Reapers don't have active abilities so...




I thought I would post what I have done so far up here and see if anyone else has had luck in getting the AI to use activated abilities. Also, if anyone knows where the AIThink routines are called from, that would be most helpful.[/code]

O_O did you check if the AIs research storm? Remember most AIs don't even research blink

Kernel64

#3
Also, have you looked at Chrono Boost?

I'm guessing these are run much like how the race0.galaxy are.

edit:

Other Units have their Think. Have you tried using this with your code? <-- this is crazy. Your code is about the Think. Sorry.

DKSlayer

   
psi = TriggerCreate("TriggerPsi");
    TriggerExecute(psi, false, true);
[\CODE]
This above is in the sc2.galaxy file. I am in agreement unit AI is definitly capable 2 ways. The XML file seems to be a quite powerful AI file. Definitly Micro. Also you could check to see if somewhere in the AI something is preventing the ai using PSI. Just a thought. Also yeah, the AI isn't done. As well as functions.

Aeg1s

Uhm, I just saw the AI use storm; did you tell it to research it first?

Kernel64

Quote from: Aeg1s on March 03, 2010, 02:07:41 AM
Uhm, I just saw the AI use storm; did you tell it to research it first?

Cool. Which AI did you use? I want to see this too. And, did it use Chrono boost? Warpgates?

Aeg1s

Quote from: Kernel64 on March 03, 2010, 02:17:59 AM
Quote from: Aeg1s on March 03, 2010, 02:07:41 AM
Uhm, I just saw the AI use storm; did you tell it to research it first?

Cool. Which AI did you use? I want to see this too. And, did it use Chrono boost? Warpgates?


It's my own AI of which I haven't messed with the casting code; It uses warp gates but not chrono boost.

kblood

Anyone here found out where they have hidden the AI for retreating? I think the AI retreats too much, and would like to stop that completely and see how well that would work. I am guessing it is in one of the tact(race)AI.galaxy files.

Kernel64

Thanks Aeg1s.

Kblood it's in meleeAI.galaxy. Currently tweaking times and eval values.

1337

Turns out I was wrong... High Templars will cast Psi Storm. Most AIs just don't research it early enough. I just tested a game with 5 Goose v4 Protoss in it. Of the 5 AIs, only 1 (the winning AI) researched Psi Storm, despite the fact that they all followed the same build order otherwise. I assume the other AI Stock requirements were not met, so it never moved on to upgrading Psi Storm. Only the winner actually had enough units in stock, so the AI continued to upgrade.


The Psi Storm AI data is definitely stored in the XML. By editing the Psi Storm entry in TargetFindData.xml, I think I can change the behavior of the Templars with regards to when they choose to cast Psi Storm. I am going to try modifying the "MinCount" and "MinScore" values to see if it changes the Templars discretion.

Kernel64

#11
Quote from: 1337 on March 03, 2010, 04:28:45 AM
Turns out I was wrong... High Templars will cast Psi Storm. Most AIs just don't research it early enough. I just tested a game with 5 Goose v4 Protoss in it. Of the 5 AIs, only 1 (the winning AI) researched Psi Storm, despite the fact that they all followed the same build order otherwise. I assume the other AI Stock requirements were not met, so it never moved on to upgrading Psi Storm. Only the winner actually had enough units in stock, so the AI continued to upgrade.

Awesome. I'll try to check this out as soon as I'm done with the Attacking section.

The Psi Storm AI data is definitely stored in the XML. By editing the Psi Storm entry in TargetFindData.xml, I think I can change the behavior of the Templars with regards to when they choose to cast Psi Storm. I am going to try modifying the "MinCount" and "MinScore" values to see if it changes the Templars discretion.

Here's some progress on the Attacking function. It kinda works.

Quote
//--------------------------------------------------------------------------------------------------
//  AIWaveAttack
//--------------------------------------------------------------------------------------------------
void AIWaveAttack (int player, wave w) {
    int state = AIState(player, e_attackState);
    int eval;
    int time;
    int count;

    //--- DEFEND ---

    if (AIWaveAttackDefend(player, w, state)) {
        AIDefendTown(player, w);
        return;
    }

    //--- ATTACK ---

    if (state == e_attackState_Attack) {
        time = AIWaveGetTimeInCombat(w);
        eval = AIWaveEvalRatio(w, c_evalRange);
        if (eval >= 96 && eval <= 99 && time <= 60) { // when not fighting and only a little weak   from 3  70 85
            if (AIGetFlag(player, e_flagsRunScared)) { // need to add check for ranges & max eval
                AISetAttackState(player, e_attackState_Scared);
            }
            else {
                AISetAttackState(player, e_attackState_Retreat);
            }
        }
        else if (eval < 96) { // need to add check for faster/retreat blocked
            AISetAttackState(player, e_attackState_Retreat);
        }
        else if (AIWaveState(w) != c_waveStateAttack) {
DebugAIPlayerWave(player, "attack1 merge main -> attack; set attack = attack vs. melee target");
           
            if (time <= 20) { //Kernel64: added time debug only
            AIWaveMerge(player, c_waveMain, c_waveAttack);
            AIWaveSetType(w, c_waveStateAttack, AIWaveTargetMelee(player));
            }
        }
    }

    //--- DROP-ATTACK ---

    if (state == e_attackState_DropAttack) {       
        eval = AIWaveEvalRatio(w, c_evalRange);
        if (eval < 80) { // need to add check for can we retreat successfully
            AISetAttackState(player, e_attackState_DropRetreat);
        }
        if (AIWaveState(w) != c_waveStateDropAttack) {
DebugAIPlayerWave(player, "attack1a In drop attack state, but not drop attack wave state?");
            AISetAttackState(player, e_attackState_DropRetreat);
        }
    }

    //--- SCARED ---

    else if (state == e_attackState_Scared) {
        eval = AIWaveEvalRatio(w, c_evalRange);
        if (eval > 99) { // turn and fight when strong
            AISetAttackState(player, e_attackState_Attack);
        }
        else if (eval < 30) { // retreat entirely if getting picked off        kernel64: from 70
            AISetAttackState(player, e_attackState_Retreat);
        }
        else if (AIWaveState(w) != c_waveStateRetreat) {
DebugAIPlayerWave(player, "attack2 merge main -> attack; set attack = retreat to gather offense");
            AIWaveMerge(player, c_waveMain, c_waveAttack);
            AIWaveSetType(w, c_waveStateRetreat, AIWaveTargetGatherO(player, c_townMain));
        }
    }

    //--- RETREAT ---

    else if (state == e_attackState_Retreat) {       
DebugAIPlayerWave(player, "attack3 set attack = retreat to gather offense");
        AIWaveSetType(w, c_waveStateRetreat, AIWaveTargetGatherO(player, c_townMain));
        AISetAttackState(player, e_attackState_InRetreat);
    }

    //--- DROP RETREAT ---

    else if (state == e_attackState_DropRetreat) {       
DebugAIPlayerWave(player, "attack3drop set attack = drop retreat to gather offense");
        AIWaveSetType(w, c_waveStateDropRetreat, AIWaveTargetGatherO(player, c_townMain));
        AISetAttackState(player, e_attackState_InRetreat);
    }

    //--- IN RETREAT ---

    else if (state == e_attackState_InRetreat) {
        if (AIWaveState(w) != c_waveStateRetreat && AIWaveState(w) != c_waveStateDropRetreat) {
DebugAIPlayerWave(player, "attack4 merge main -> attack");
            AIWaveMerge(player, c_waveMain, c_waveAttack);
            AISetAttackState(player, e_attackState_Idle);
        }
    }
   
    //--- IDLE ---

    else if (state == e_attackState_Idle) {
        count = AIWaveUnitCount(w) + AIWaveUnitCount(AIWaveGet(player, c_waveMain));

        // wait 30 seonds at least after retreating before attacking again
        if (AIWaveGetTimeSinceRetreat(w) > 60) {   // kernel64: from 30
            // always support allies attacking
            if (AIAnyAllyAttacking(player)) {
                AISetAttackState(player, e_attackState_Attack);
            }
            if (PlayerDifficulty(player) >= c_skirVeryHard) {
                if (AIWaveState(w) == c_waveStateIdle) {
                    // wait until the wave is idle before considering attacking again
                    AdvancedIdleAttackLogic(player, w, count);
                }
            }
            else {
                // base logic attack any time we have more than 30 units or it's been a while
                if (count >= 30) {
                    AISetAttackState(player, e_attackState_Attack);
                }
                else if (AIGetFlag(player, e_flagsTimeout) && AIWaveGetTimeSinceCombat(w) >= 300-10*count) {
                    AISetAttackState(player, e_attackState_Attack);
                }
            }
        }
       
        if (AIState(player, e_attackState) == e_attackState_Idle) {
            if (AIWaveState(w) != c_waveStateIdle) {
                DebugAIPlayerWave(player, "attack5 set attack = idle at gather offense");
                AIWaveSetType(w, c_waveStateIdle, AIWaveTargetGatherO(player, c_townMax));
            }
        }
    }
}


What I did was change some numbers and added a time delay to where the AI is forced to attack again after the two if, and else if conditions.

edit: apologies. I have no idea how to put these into a code box.

details:

Quote
    //--- ATTACK ---

    if (state == e_attackState_Attack) {
        time = AIWaveGetTimeInCombat(w);
        eval = AIWaveEvalRatio(w, c_evalRange);
        if (eval >= 96 && eval <= 99 && time <= 60) { // when not fighting and only a little weak   from 3  70 85
            if (AIGetFlag(player, e_flagsRunScared)) { // need to add check for ranges & max eval
                AISetAttackState(player, e_attackState_Scared);
            }
            else {
                AISetAttackState(player, e_attackState_Retreat);
            }
        }
        else if (eval < 96) { // need to add check for faster/retreat blocked
            AISetAttackState(player, e_attackState_Retreat);
        }
        else if (AIWaveState(w) != c_waveStateAttack) {
DebugAIPlayerWave(player, "attack1 merge main -> attack; set attack = attack vs. melee target");
           
            if (time <= 20) { //Kernel64: added time debug only
            AIWaveMerge(player, c_waveMain, c_waveAttack);
            AIWaveSetType(w, c_waveStateAttack, AIWaveTargetMelee(player));
            }
        }
    }

Changed eval values higher for attack and lower for retreat. Also added a time condition to when to attack again: if(time <=20)

Quote
//--- SCARED ---

    else if (state == e_attackState_Scared) {
        eval = AIWaveEvalRatio(w, c_evalRange);
        if (eval > 99) { // turn and fight when strong
            AISetAttackState(player, e_attackState_Attack);
        }
        else if (eval < 30) { // retreat entirely if getting picked off        kernel64: from 70
            AISetAttackState(player, e_attackState_Retreat);
        }
        else if (AIWaveState(w) != c_waveStateRetreat) {
DebugAIPlayerWave(player, "attack2 merge main -> attack; set attack = retreat to gather offense");
            AIWaveMerge(player, c_waveMain, c_waveAttack);
            AIWaveSetType(w, c_waveStateRetreat, AIWaveTargetGatherO(player, c_townMain));
        }
    }

This makes the AI retreat only when eval <30. Forces it to stay in combat longer even if outmatched.

imbalisk

Quote from: Kernel64 on March 03, 2010, 01:21:49 AM
Also, have you looked at Chrono Boost?

I'm guessing these are run much like how the race0.galaxy are.

edit:

Other Units have their Think. Have you tried using this with your code? <-- this is crazy. Your code is about the Think. Sorry.
Chrono Boost cant be found in all of this files, it was added short before Beta came out, I think thats the reason its not implemented yet.
But because you can use it in the game, it has to be there, there is just no AI script triggering it.
Look at AbilData.xml:
<CAbilEffectTarget id="TimeWarp">
        <EditorCategories value="AbilityorEffectType:Units,Race:Terran"/>
        <Cost>
            <Vital index="Energy" value="25"/>
        </Cost>
        <TargetFilters value="-;Neutral,Enemy,Missile,Stasis,Dead,Hidden,Invulnerable"/>
        <Range value="500"/>
        <Arc value="360"/>
        <CmdButtonArray index="Execute" DefaultButtonFace="TimeWarp"/>
        <UninterruptibleArray index="Approach" value="1"/>
        <UninterruptibleArray index="Prep" value="1"/>
        <UninterruptibleArray index="Cast" value="1"/>
        <UninterruptibleArray index="Channel" value="1"/>
        <UninterruptibleArray index="Finish" value="1"/>
        <Effect index="0" value="TimeWarpSet"/>
        <AINotifyEffect value="TimeWarpProduction"/>
    </CAbilEffectTarget>

Chrono Boost takes 25 Energy, maybe it is called TimeWarp in the AI and called ChronoBoost in the game?
I think a way to activate it is to add a AIThinkNexus function into the game:
In the UnitData.xml this can be found:
<TacticalAIThink value="AIThinkHighTemplar"/>
I think this is a reference to the AI data stored in TactProt.galaxy file.
In order to make the Nexus cast this new spell we have to implement a function like AIThinkGateway
void AIThinkGateway (int player, unit aiUnit, unitgroup scanGroup) {
DebugVarInt("GGaTTee", 0);
    order ord;
   
    if (AIEvalTacticalData(aiUnit, null)) {
        return;
    }


    if (AITechCount(player, c_PR_WarpGateResearch, c_techCountCompleteOnly) == 0) {
        return;
    }


    AISetWantsToUpgrade(aiUnit);


    ord = AICreateOrder(player, c_AB_UpgradeToWarpGate, 0);
    if (!UnitOrderIsValid(aiUnit, ord)) {
        return;
    }
   
    AICast(aiUnit, ord, c_noMarker, c_castHold);
}

with calls on the ChronoBoost / TimeWarp function on the nexus itself (for testing, later on gates or forges to speed up research... dunno :) )


However the difficulty is this:
ord = AICreateOrder(player, c_AB_UpgradeToWarpGate, 0);


c_AB_UpgradeToWarpGate is defined in RequirementsAI.galaxy:


const string c_AB_UpgradeToWarpGate         = "UpgradeToWarpGate";
There is no const string c_AB_TimeWarp or c_AB_ChronoBoost, I think it might work with just calling ord = AICreateOrder(player, "TimeWarp", 0); in the Nexus AI script.


However, the reason I write this down and dont test it is because I rage everytime I start SC2 and the AI just down spawn because there is a F***ing bug in my script.


I can't even get an output like "AI Gateway script is called" cause when i put a TriggerOutput function call within the AIThinkGateway function the programm just down't work as mentioned above.


Maybe someone take a look at this, implementing the use of new abilities, like ChronoBoost would make the AI much harder :)