• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!

[vJASS] JASS AI problem - GuardSecondary

Status
Not open for further replies.
Level 10
Joined
Nov 23, 2006
Messages
592
Hey,

I am having a problem with making AI build towers to guard an expansion. I have looked through Blizzard's AI files, and they just order to AI to build exp and then use GuardSecondary function to build towers. I tried to replicate it but with no success.

JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
    player user = Player(0)
endglobals


//BUILD ORDER
function BuildOrder takes nothing returns nothing
    local integer mines = GetMinesOwned()


    call SetBuildUnitEx( 10,10,10, 'opeo' )
    call SetBuildUnitEx( 5, 5, 5, 'otrb' )
    call BasicExpansion( mines < 2, 'ogre' )

//this is important
    call GuardSecondary( 1, 2, 'owtw'   )   // This doesn't work
    call GuardSecondary( 0, 2, 'owtw'   )  //This works AI builds the towers in the main base
//this is important


    call SetBuildUnitEx( 1, 1, 1, 'oalt' )
endfunction

//MAIN
function main takes nothing returns nothing
    call CampaignAI('otrb', null)
    call DoCampaignFarms(false)
    call DisplayTextToPlayer(user,0,0,"AI started")
    call BuildOrder()
    call CampaignDefenderEx(4,4,4, 'ogru')
endfunction

Any help would be highly appreciated. I have also added the test map i am using. Thanks!
 

Attachments

  • testAI_orc.w3m
    240.2 KB · Views: 56
Last edited:
Level 12
Joined
Jun 15, 2016
Messages
472
The reason the guard towers are not being built is because of how GuardSecondary actually works:

JASS:
function GuardSecondary takes integer townid, integer qty, integer unitid returns nothing
    if TownHasHall(townid) and TownHasMine(townid) then
        call SecondaryTown( townid, qty, unitid )
    endif
endfunction

Notice that what you want to build will be added to the building order only if you have both a mine and a town hall at the town. Since this function runs a few seconds after the script starts, both TownHasHall(townid) and TownHasMine(townid) are false, and the towers are not added to the build order. The melee AIs are able to use this function because the build order function is being called and updated constantly throughout the game, while that is not the case in Campaign AI.

To prove the point, I changed the script a bit so it will display whether or not the building requirements for towers are met, and added a simple AI command: every time you press ESC the AI will attempt to build two towers in the second town. Here is the modified code:


JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
    player user = Player(0)
endglobals

//-------------------------------------------------------------------------------------------------
// DEBUG FUNCTIONS
//-------------------------------------------------------------------------------------------------

function B2S takes boolean b returns string
    if b then
        return "true"
    else
        return "false"
    endif
endfunction

function Dig2Str takes integer i returns string
    if i == 1 then
        return "1"
    elseif i == 2 then
        return "2"
    elseif i == 3 then
        return "3"
    elseif i == 4 then
        return "4"
    elseif i == 5 then
        return "5"
    elseif i == 6 then
        return "6"
    elseif i == 7 then
        return "7"
    elseif i == 8 then
        return "8"
    elseif i == 9 then
        return "9"
    else
        return "0"
    endif
endfunction

// Courtesy of AIAndy and Tommi. See source:
// http://www.hiveworkshop.com/threads/two-custom-campaign-ais-examples.8939/
function Int2Str takes integer ic returns string
    local string s = ""
    local integer i = ic
    local integer ialt = 0
    local boolean neg = false

    if i == 0 then
      return "0"
    endif
    if i < 0 then
      set neg = true
      set i = (-1)*i
    endif
    loop
      exitwhen i == 0
      set ialt = i
      set i = i / 10
      set s = Dig2Str( ialt - 10*i ) + s
    endloop
    if neg then
      set s = "-"+s
    endif
    return s
endfunction

//-------------------------------------------------------------------------------------------------

function GuardSecondaryEx takes integer townid, integer qty, integer unitid returns nothing
    call DisplayTextToPlayer(user,0,0,("Town " + Int2Str(townid) + " has hall " + B2S(TownHasHall(townid))))
    call DisplayTextToPlayer(user,0,0,("Town " + Int2Str(townid) + " has mine " + B2S(TownHasMine(townid))))
    if TownHasHall(townid) and TownHasMine(townid) then
        call SecondaryTown( townid, qty, unitid )
    endif
endfunction

//BUILD ORDER
function BuildOrder takes nothing returns nothing
    local integer mines = GetMinesOwned()


    call SetBuildUnitEx( 10,10,10, 'opeo' )
    call SetBuildUnitEx( 5, 5, 5, 'otrb' )
    call BasicExpansion( mines < 2, 'ogre' )
    call GuardSecondaryEx( 1, 2, 'owtw'   )
    call GuardSecondaryEx( 0, 2, 'owtw'   )
    call SetBuildUnitEx( 1, 1, 1, 'oalt' )
endfunction

//WATCH THIS
function WatchThis takes nothing returns nothing
    loop
        loop
            exitwhen CommandsWaiting() > 0
            call Sleep(2.0)
        endloop
       
        call GuardSecondaryEx( 1, 2, 'owtw'   )
        call PopLastCommand()
    endloop
endfunction

//MAIN
function main takes nothing returns nothing
    call CampaignAI('otrb', null)
    call DoCampaignFarms(false)
    call DisplayTextToPlayer(user,0,0,"AI started")
    call BuildOrder()
    call CampaignDefenderEx(4,4,4, 'ogru')
    call StartThread(function WatchThis)
endfunction
 

Attachments

  • testAI_orc.w3m
    241 KB · Views: 60
Level 10
Joined
Nov 23, 2006
Messages
592
Thank you a lot! I've read your campaign AI workflow tutorial and for some reason I thought that main function in campaign AI works as a thread as well, I must've misunderstood. Need to re-read it.

I've added a thread that periodically calls BuildOrder function, tested it and it works now:

JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
    player user = Player(0)
endglobals

//GUARDSECONDARY
function GuardSecondaryEx takes integer townid, integer qty, integer unitid returns nothing
    if TownHasHall(townid) and TownHasMine(townid) then
        call SecondaryTown( townid, qty, unitid )
    endif
endfunction


//BUILD ORDER
function BuildOrder takes nothing returns nothing
    local integer mines = GetMinesOwned()


    call SetBuildUnitEx( 10,10,10, 'opeo' )
    call SetBuildUnitEx( 5, 5, 5, 'otrb' )
    call BasicExpansion( mines < 2, 'ogre' )
    call GuardSecondaryEx( 1, 2, 'owtw'   )   // This doesn't work
    call GuardSecondaryEx( 0, 2, 'owtw'   )  //This works AI builds the towers in the main base
    call SetBuildUnitEx( 1, 1, 1, 'oalt' )
endfunction

//BUILD LOOP
function Guardloop takes nothing returns nothing
    call BuildOrder()
    call StaggerSleep(1,2)
    loop
        call BuildOrder()
        call Sleep(2)
    endloop
endfunction

//MAIN
function main takes nothing returns nothing
    call CampaignAI('otrb', null)
    call DoCampaignFarms(false)
    call DisplayTextToPlayer(user,0,0,"AI started")
    call BuildOrder()
    call CampaignDefenderEx(4,4,4, 'ogru')
    call StartThread(function Guardloop)
endfunction

Thanks again for help

EDIT: guess I could just use GuardSecondary instead of GuardSecondaryEx
 
Last edited:
Level 12
Joined
Jun 15, 2016
Messages
472
EDIT: guess I could just use GuardSecondary instead of GuardSecondaryEx

Yes. Actually GuardSecondaryEx is a copy of GuardSecondary with debug functions. You can just remove it.

I've added a thread that periodically calls BuildOrder function

Alright, just add the function InitBuildArray at the start of BuildOrder, because as it is now every 2 seconds or so you add another copy of the build order. Should this go on long enough, the array length of build order will be above 8192 and you'll have a potential crash.

Thank you a lot! I've read your campaign AI workflow tutorial and for some reason I thought that main function in campaign AI works as a thread as well, I must've misunderstood. Need to re-read it.

No, you understood it correctly. The building priorities are stored in a set of arrays, as can be seen in SetBuildAll:

JASS:
function SetBuildAll takes integer t, integer qty, integer unitid, integer town returns nothing
    if qty > 0 then
        set build_qty[build_length] = qty
        set build_type[build_length] = t
        set build_item[build_length] = unitid
        set build_town[build_length] = town
        set build_length = build_length + 1
    endif
endfunction

So this array is in the background and the AI script goes over it every couple of seconds, and builds everything missing. Above that, melee scripts (and now your script too) also empty and recreate the build arrays every couple of seconds to update it (it's a mess, honestly). Are things a bit clearer now? :gg:
 
Level 10
Joined
Nov 23, 2006
Messages
592
Alright, just add the function InitBuildArray at the start of BuildOrder, because as it is now every 2 seconds or so you add another copy of the build order. Should this go on long enough, the array length of build order will be above 8192 and you'll have a potential crash.

Will do

No, you understood it correctly. The building priorities are stored in a set of arrays, as can be seen in SetBuildAll:

JASS:
function SetBuildAll takes integer t, integer qty, integer unitid, integer town returns nothing
    if qty > 0 then
        set build_qty[build_length] = qty
        set build_type[build_length] = t
        set build_item[build_length] = unitid
        set build_town[build_length] = town
        set build_length = build_length + 1
    endif
endfunction

So this array is in the background and the AI script goes over it every couple of seconds, and builds everything missing. Above that, melee scripts (and now your script too) also empty and recreate the build arrays every couple of seconds to update it (it's a mess, honestly). Are things a bit clearer now? :gg:

So the campaign main function rebuilds what missing but no longer checks the conditions (so it does not build tower at exp), whereas my script does it...i guess? :D

Also while I have you here, I would like to ask one more thing, I've created an attack loop, but SuicidePlayerEx always attack the closest base (expansion). So I tried to implement it so that with one wave it checks for player's base first and then attack there. That way the AI wouldn't attack the same spot all the time. The problem is, it does not work (surprise). I tried to play around with it, but with no success:

JASS:
function Attackloop takes nothing returns nothing
    local unit base
    call Sleep( M2 )
    loop
         //FIRST WAVE
        call DisplayTextToPlayer(user,0,0,"setting up the first attack wave")    
        call InitAssaultGroup()
        call CampaignAttackerEx( 4, 4, 4, 'ogru' ) //GRUNTS
        call DisplayTextToPlayer(user,0,0,"units added to the first wave, now waiting")
        call SuicideOnPlayerEx(M2,M2,M2,user)
        call DisplayTextToPlayer(user,0,0,"first wave attack finished")

       //SECOND WAVE
        call StartGetEnemyBase()
            loop
                exitwhen (not WaitGetEnemyBase())
                call SuicideSleep(1)
            endloop
        set base = GetEnemyBase()
        call DisplayTextToPlayer(user,0,0,"Enemy base acquired")
        call InitAssaultGroup()
        call DisplayTextToPlayer(user,0,0,"setting up the second attack wave")
        call CampaignAttackerEx( 2, 3, 3, 'ohun' ) //TROLLS
        call DisplayTextToPlayer(user,0,0,"units added to the second wave, now waiting")
        call Sleep( M2 )
        call AttackMoveKillA(base)
        call DisplayTextToPlayer(user,0,0,"second wave attack fnished")
      
    endloop
endfunction

AI build the trolls, but they do not attack, and then the first wave also becomes disjointed, with grunts attacking the moment they are built.
I would like to ask for your help again.
Function attackloop() is called from main function
 
Last edited:
Level 12
Joined
Jun 15, 2016
Messages
472
So the campaign main function rebuilds what missing but no longer checks the conditions (so it does not build tower at exp), whereas my script does it...i guess? :D

Precisely.

AI build the trolls, but they do not attack, and then the first wave also becomes disjointed, with grunts attacking the moment they are built.
I would like to ask for your help again.
Function attackloop() is called from main function

Unfortunately I won't be able to test that problem until monday, but I have a pretty good idea of what might be wrong here. The trolls you are building are not actually assigned to the attack captain,therefore they do not attack. These are your attacker building functions:

JASS:
function CampaignAttacker takes integer level, integer qty, integer unitid returns nothing
    if qty > 0 and difficulty >= level then
        call SetAssaultGroup(qty,qty,unitid)
    endif
endfunction

//============================================================================
function CampaignAttackerEx takes integer easy, integer med, integer hard, integer unitid returns nothing
    if difficulty == EASY then
        call CampaignAttacker(EASY,easy,unitid)
    elseif difficulty == NORMAL then
        call CampaignAttacker(NORMAL,med,unitid)
    else
        call CampaignAttacker(HARD,hard,unitid)
    endif
endfunction

As you can see they are very similar to the base building function. The actual assignment of attackers to the attack captain happens inside this function SuicideOnPlayerEx, specifically inside the function FormGroup:


JASS:
function FormGroup takes integer seconds, boolean testReady returns nothing
    local integer index
    local integer count
    local integer unitid
    local integer desire
    local integer readyPercent

   // normally test for CaptainReadiness() of 50%
    if testReady == true then
        set readyPercent = 50
        call Trace("forming group, requiring healthy guys\n") //xxx
    else
        set readyPercent = 0
        call Trace("forming group, unit health not important\n") //xxx
    endif

    call Trace("trying to gather forces\n") //xxx

    loop
        call SuicideSleep(seconds)
        call InitAssault()

        set index = 0
        loop
            exitwhen index == harass_length

            set unitid = harass_units[index]
            set desire = harass_max[index]
            set count  = TownCountDone(unitid)

            call Conversions(desire,unitid)

            if count >= desire then
                call AddAssault(desire,unitid)
            else
                set desire = harass_qty[index]

                if count < desire then
                    call AddAssault(desire,unitid)
                else
                    call AddAssault(count,unitid)
                endif
            endif

            set index = index + 1
        endloop

       //xxx
        if form_group_timeouts and (sleep_seconds < -60) then
            call Trace("exit form group -- timeout\n")
        elseif CaptainInCombat(true) then
            call Trace("exit form group -- can't form while already in combat\n")
        elseif CaptainIsFull() and CaptainReadiness() >= readyPercent then
            call Trace("exit form group -- ready\n")
        endif
       //xxx

       // time out and send group anyway if time has already expired
        exitwhen form_group_timeouts and (sleep_seconds < -60)
        exitwhen CaptainInCombat(true)
        exitwhen CaptainIsFull() and CaptainReadiness() >= readyPercent
    endloop
endfunction


In short, I suggest you change this line call Sleep( M2 ) (after adding units to the second wave), to this line FormGroup(M2,true), and see how that goes. Furthermore, I wrote a more in depth explanation about how SuicideOnPlayerEx works in this tutorial I'll release (once I get to sit and finish the damn thing...)

That's the sucky thing about AI, there's just a lot going inside some of the functions. This page, displaying all functions and variables in the common AI really helps with figuring out those problems.
 
Level 10
Joined
Nov 23, 2006
Messages
592
Your FormGroup solution works perfectly, thanks once again!

New issue I am dealing with is that GetEnemyBase() works weirdly, first time the AI atacked group of footmen in the middle of the map and after that the AI attacked the expansion everytime, ignoring main base. One way of dealing with this could probably be sending coordinates from map to AI via triggers and using AttackMoveXY. Will test it tomorrow.
 
Level 12
Joined
Jun 15, 2016
Messages
472
New issue I am dealing with is that GetEnemyBase() works weirdly, first time the AI atacked group of footmen in the middle of the map and after that the AI attacked the expansion everytime, ignoring main base. One way of dealing with this could probably be sending coordinates from map to AI via triggers and using AttackMoveXY. Will test it tomorrow.

Using orders is one way to do it. I didn't actually test how GetEnemyBase() works, but if you don't mind doing a bit more testing for your result, you could do the following steps:

1. Set yourself as player 3, allied with the AI player.
2. after the line set base = GetEnemyBase() write this line SetAllianceTarget(base), which will make the AI player ping you the location of base.

Hopefully this'll give you more info about the problem.
 
Level 10
Joined
Nov 23, 2006
Messages
592
So I tested it and GetEnemyBase() always find the closest base to AI. Kinda renders GetEnemyExpansion a bit useless. Also makes it difficult to get the AI attack different places, guess sending coordinates is the only way?
 

Dr Super Good

Spell Reviewer
Level 64
Joined
Jan 18, 2005
Messages
27,202
So this array is in the background and the AI script goes over it every couple of seconds, and builds everything missing. Above that, melee scripts (and now your script too) also empty and recreate the build arrays every couple of seconds to update it (it's a mess, honestly).
StarCraft II uses the same system except the build queue is managed natively instead of inside the scripting system. I wonder how many other RTS games do something similar as well...
 
Level 12
Joined
Jun 15, 2016
Messages
472
So I tested it and GetEnemyBase() always find the closest base to AI. Kinda renders GetEnemyExpansion a bit useless. Also makes it difficult to get the AI attack different places, guess sending coordinates is the only way?

I did some more tests and found another way to do it. Basically it stores all enemy town halls into variables, then picks one and attacks it.


JASS:
//GLOBAL DECELERATION
//------------------------------------------------
globals
    player user = Player(0)
    player debug_user = Player(2)
    unit array enemy_halls
    integer enemy_halls_length = 0
endglobals

//-------------------------------------------------------------------------------------------------
// DEBUG FUNCTIONS
//-------------------------------------------------------------------------------------------------

function B2S takes boolean b returns string
    if b then
        return "true"
    else
        return "false"
    endif
endfunction

function Dig2Str takes integer i returns string
    if i == 1 then
        return "1"
    elseif i == 2 then
        return "2"
    elseif i == 3 then
        return "3"
    elseif i == 4 then
        return "4"
    elseif i == 5 then
        return "5"
    elseif i == 6 then
        return "6"
    elseif i == 7 then
        return "7"
    elseif i == 8 then
        return "8"
    elseif i == 9 then
        return "9"
    else
        return "0"
    endif
endfunction

// Courtesy of AIAndy and Tommi. See source:
// http://www.hiveworkshop.com/threads/two-custom-campaign-ais-examples.8939/
function Int2Str takes integer ic returns string
    local string s = ""
    local integer i = ic
    local integer ialt = 0
    local boolean neg = false

    if i == 0 then
      return "0"
    endif
    if i < 0 then
      set neg = true
      set i = (-1)*i
    endif
    loop
      exitwhen i == 0
      set ialt = i
      set i = i / 10
      set s = Dig2Str( ialt - 10*i ) + s
    endloop
    if neg then
      set s = "-"+s
    endif
    return s
endfunction

function Msg takes string message returns nothing
    local integer i = 0
   
    loop
        call DisplayTextToPlayer(Player(i),0,0,message)
        set i = i + 1
        exitwhen i == 12
    endloop
endfunction

//-------------------------------------------------------------------------------------------------

function SaveEnemyHalls takes nothing returns nothing
    local group enemy_units = CreateGroup()
    local unit u
    local integer id
    set enemy_halls_length = 0
   
    call Msg("Checking for enemy halls")
   
    call GroupEnumUnitsOfPlayer(enemy_units, user, null)
    set u = FirstOfGroup(enemy_units)
    loop
        exitwhen u == null
       
        if UnitAlive(u) then
            set id = GetUnitTypeId(u)
           
            if (id == TOWN_HALL or id == KEEP or id == CASTLE) or (id == GREAT_HALL or id == FORTRESS or id == STRONGHOLD) or (id == NECROPOLIS_1 or id == NECROPOLIS_2 or id == NECROPOLIS_3) or (id == TREE_LIFE or id == TREE_AGES or id == TREE_ETERNITY) then
                set enemy_halls_length = enemy_halls_length + 1
                set enemy_halls[enemy_halls_length] = u
            endif
        endif
       
        call GroupRemoveUnit(enemy_units,u)
        set u = FirstOfGroup(enemy_units)
    endloop
   
    call Msg((Int2Str(enemy_halls_length) + " halls in array"))
   
    call DestroyGroup(enemy_units)
    set enemy_units = null
    set u = null
endfunction

function LoadEnemyHall takes nothing returns unit
    local unit u
   
    if enemy_halls_length == 0 then
        call Msg("No enemy halls in array")  
        return null
    else
        set u = enemy_halls[GetRandomInt(1,enemy_halls_length)]
        if UnitAlive(u) then
            call Msg("Enemy base acquired")
            return u
        else
            call Msg("Dead enemy hall")
            return null
        endif
    endif
endfunction

function AttackEnemyHall takes unit hall returns nothing
    local integer timeout_seconds = M3

    if hall == null then
        call Msg("No enemy hall, exiting attack function")
        call Sleep(3)
        return
    endif
   
    call AttackMoveKill(hall)
    call Msg("Starting attack check loop")
    loop
        if not UnitAlive(hall) then
            call Msg("Enemy hall dead, exiting attack loop")
            exitwhen true
        endif
       
        if CaptainGroupSize() == 0 then
            call Msg("All units dead, exiting attack loop")
            exitwhen true
        endif
       
        if (timeout_seconds <= 0) and not CaptainInCombat(true) then
            call Msg("3 minutes timeout passed, exiting attack loop")
            exitwhen true
        endif
       
        call Sleep(2.0)
        set timeout_seconds = timeout_seconds - 2
    endloop
   
    call Sleep(2.0)
    call CaptainGoHome()
endfunction

//ATTACK LOOP
function Attackloop takes nothing returns nothing
    local unit base
    call Sleep( 30 )
    loop
        //FIRST WAVE
        call Msg("setting up the first attack wave")   
        call InitAssaultGroup()
        call CampaignAttackerEx( 4, 4, 4, 'ogru' ) //GRUNTS
        call Msg("units added to the first wave, now waiting")
        call SuicideOnPlayerEx(M2,M2,M2,user)
        call Msg("first wave attack finished")

        //SECOND WAVE
        call Msg("setting up the second attack wave")
        call Sleep(5.0)
        loop
            call SaveEnemyHalls()
            exitwhen enemy_halls_length > 0
            call Sleep(2.0)
        endloop
        call InitAssaultGroup()
        call CampaignAttackerEx( 2, 3, 3, 'ohun' )
        call Msg("units added to the second wave, now waiting")
        call FormGroup( M2, true )
        call AttackEnemyHall(LoadEnemyHall())
        call Msg("second wave attack fnished")
    endloop
endfunction

//BUILD ORDER
function BuildOrder takes nothing returns nothing
    local integer mines = GetMinesOwned()


    call SetBuildUnitEx( 10,10,10, 'opeo' )
    call SetBuildUnitEx( 5, 5, 5, 'otrb' )
    call BasicExpansion( mines < 2, 'ogre' )
    call GuardSecondary( 1, 2, 'owtw'   )
    call GuardSecondary( 0, 2, 'owtw'   )
    call SetBuildUnitEx( 1, 1, 1, 'oalt' )
endfunction

//BUILD LOOP
function Guardloop takes nothing returns nothing
    call BuildOrder()
    call StaggerSleep(1,2)
    loop
        call BuildOrder()
        call Sleep(2)
    endloop
endfunction

//MAIN
function main takes nothing returns nothing
    call CampaignAI('otrb', null)
    call DoCampaignFarms(false)
    call Msg("AI started")
    call BuildOrder()
    call CampaignDefenderEx(4,4,4, 'ogru')
    call SetCaptainHome(ATTACK_CAPTAIN, -5800.,-2000.)
    call StartThread(function Guardloop)
    call Attackloop()
endfunction


StarCraft II uses the same system except the build queue is managed natively instead of inside the scripting system. I wonder how many other RTS games do something similar as well...

That's probably better, WC3 AI cannot handle queues, which makes the build order act really stupid sometimes (need to build 3 abominations and 3 necromancers. waits for the abominations to finish losing over a minute in the process. I tried to create an optimizing function for the build order once but it requires the whole dependency tree of which building trains who.
 

Attachments

  • testAI_orc.w3m
    237.5 KB · Views: 40
Status
Not open for further replies.
Top