• 🏆 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] Need Feedback on AI System

Status
Not open for further replies.
Level 14
Joined
Nov 18, 2007
Messages
1,084
So, I attempted to make a basic hero AI for arena-type maps and would like any kind of suggestions and feedback to improve it.
I designed it in mind so that the default AI can be extended for specific heroes.
JASS:
//==========================================================================================
//  HeroAI vWIP
//      by watermelon_1234
//==========================================================================================
// This library provides a simple computer AI for arena-type maps.
// It provides a struct, HeroAI, which takes care of basic hero actions.
//    * moving around the map
//    * attacking an enemy unit
//    * spending gold on items and picking up nearby items
//    * running to a specified coordinates when health is low
//
// Additionally, the struct can be extended by the user to further customize the AI of a 
// specific hero, such as making it cast certain spells.
//
// Things that can be overridden to customize a specific hero further:
//  * Needs work
//##########################################################################################
// HeroAI struct:
//
//   Members:
//
//     * unit hero         -> The hero the AI is controlling
//     * player owner      -> The owner of hero. It assumed that the owner will be constant
//     * integer hid       -> The handle id of the hero
//     * Itemset itemBuild -> The item build the hero will try to buy. Defaults to DefaultItemBuild
//
//    Values checked periodically: 
//
//     * integer itemCount -> The number of items the hero has
//     * integer gold      -> The amount of gold the owner has
//     * real life         -> Life of hero
//     * real mana         -> Mana of hero 
//     * boolean needHeal  -> Used to tell if the hero needs to run to the fountain
//     * hx                -> x-coordinate of the hero
//     * hy                -> y-coordinate of the hero
//     [* group units       -> All alive units around the hero, excluding the hero and neutral units ] May be deprecated
//     * group allies      -> Derived from group units, only allied units
//     * group enemies     -> Derived from group units, only enemy units
//     * integer allyNum   -> Number of nearby allied units
//     * integer enemyNum  -> Number of nearby enemy units
//
//     * static thistype temp -> Allow easier group enumerations
//  
//    Utility Methods:
//
//     * badCondition takes nothing returns boolean *
//       Condition used to tell when the hero should run to the fountain. 
//  
//     * goodCondition takes nothing returns boolean *
//       Condition used to tell when the hero should leave the fountain.
//
//    Action Methods:
//
//     * moveAround takes nothing returns nothing *
//       Makes the hero wander around.
//
//     * run takes nothing returns nothing *
//         Makes the hero run to (RUN_X, RUN_Y), with some randomness
//------------------------------------------------------------------------------------------
// Function API:
//
//  * RunHeroAI takes unit hero returns nothing *
//      Starts the AI for a unit.
//
//  * function interface RegisterHeroAIFunc takes unit hero returns nothing *
//
//  * RegisterHeroAI takes integer unitTypeId, RegisterHeroAIFunc register returns nothing *
//      Registers an AI creation function for a unit-type id.
//      register should be a function that creates the AI struct for that hero and follow 
//      the RegisterHeroAIFunc interface.
//
//  * GetHeroAI takes unit hero returns integer *
//      Returns the AI struct for the hero. Can be destroyed to stop the AI for that hero.
//
//  * DoesHeroHaveAI takes unit hero returns boolean *
//      Returns true if a hero has an AI.
//------------------------------------------------------------------------------------------
//  Itemset API:
//
//  * HeroAI_Itemset.create takes nothing returns thistype *
//      Makes an itemset
//
//  * method addItemId takes integer itemID, integer cost returns nothing *
//      Adds an item id and its cost to the itemset instance.  
//##########################################################################################
// Required Libraries:
//  * GroupUtils                    [url]http://www.wc3c.net/showthread.php?t=104464[/url]
//  * New Table                        [url]http://www.hiveworkshop.com/forums/jass-functions-413/snippet-new-table-188084/[/url]
//    * RegisterPlayerUnitEvent         [url]http://www.hiveworkshop.com/forums/jass-resources-412/snippet-registerplayerunitevent-203338/[/url]
//    * TimerUtils                    [url]http://www.wc3c.net/showthread.php?t=101322[/url]
//
// Optional:
//  * PruneGroup/FitnessFunc         [url]http://www.wc3c.net/showthread.php?t=106467[/url]
//==========================================================================================

library HeroAI requires GroupUtils, RegisterPlayerUnitEvent, Table, TimerUtils, optional FitnessFunc, optional HeroAIHelper
           
    globals
          public constant real    DEFAULT_PERIOD      = 1.85   // How often the timer should loop to make the hero do actions
        public constant real    SIGHT_RANGE         = 1200. // Determines how the hero looks for items and units.
        public constant real    MOVE_DIST           = 1000.  // The random amount of distance the hero will move
        public constant integer MAX_ITEM_INVENTORY  = 6     // The number of items the hero can hold
        public constant real    FOUNTAIN_RNG          = 500.    // The range the hero should be in of the fountain. 
        
        // The following determines where the AI should run to.
        public constant real    RUN_X               = 0.             
        public constant real    RUN_Y               = 0.
    endglobals
    
    // The following are methods that can be coded to make the hero take better actions
    private interface AIInterface
        method learnSkills      takes nothing returns nothing defaults nothing  // Makes the hero learn skills when it has more than one skill point.
        method loopActions      takes nothing returns nothing defaults nothing  // Determines the periodic behavior of the hero. Defined by default.        
        method assaultEnemy     takes nothing returns nothing defaults nothing  // Actions to take when attacking enemies. Defined by default.
        method assistAlly       takes nothing returns boolean defaults false      // Actions to take when supporting allies. Should return true if a supportive action was taken
        method runActions       takes nothing returns nothing defaults nothing  // Actions to take when running to the fountain. This is called after the hero is issued an order to run.
        method fountainActions  takes nothing returns nothing defaults nothing  // Actions to take while at the fountain.
    endinterface     
    
    public keyword Itemset // Don't touch this
    
    globals
        public Itemset DefaultItemBuild // Don't touch this
    endglobals
    
    // Set up the default item build
    private function SetupItemTypes takes nothing returns nothing
        call DefaultItemBuild.addItemId('gcel', 100)
        call DefaultItemBuild.addItemId('bspd', 150) 
        call DefaultItemBuild.addItemId('rlif', 200)        
        call DefaultItemBuild.addItemId('prvt', 350)
        call DefaultItemBuild.addItemId('rwiz', 400)
        call DefaultItemBuild.addItemId('pmna', 500)
    endfunction
    
//==========================================================================================
// END OF USER CONFIGURATION
//==========================================================================================

    globals
        private Table infoAI          // Tracks custom AI structs defined for specific unit-type ids 
        private Table heroesAI      // Tracks the AI struct a hero has          
    endglobals    
    
    // Data structure for the item builds
    struct Itemset
        readonly integer array itemids[MAX_ITEM_INVENTORY]
        readonly integer array costs[MAX_ITEM_INVENTORY]
        readonly integer total = 0
        
        method addItemId takes integer itemID, integer cost returns nothing
            if total < 6 then
                set itemids[total] = itemID
                set costs[total] = cost
                set total = total + 1
            debug else
                debug call BJDebugMsg("Itemset already has max item ids")
            endif
        endmethod
    endstruct
    
    // Main struct that gets extended
    struct HeroAI extends AIInterface
        unit hero 
        player owner
        integer hid
        Itemset itemBuild = DefaultItemBuild        
        integer itemCount
        integer gold          
        boolean needHeal = false 
        real life
        real maxLife
        real mana           
        real hx            
        real hy             
        group units // Not very useful?         
        group allies        
        group enemies       
        integer allyNum   
        integer enemyNum   
        timer tim
        static thistype temp // Set whenever update is called
        
        // Used to make the hero get items
        private static rect ItemRect = Rect(0, 0, 0, 0)
        private static real TempReal
        private static item TempItem
        
        private static method filtUnits takes nothing returns boolean
            local unit u = GetFilterUnit()
            // Filter out dead units and the hero itself
            if not IsUnitType(u, UNIT_TYPE_DEAD) and u != temp.hero and (IsUnitAlly(u, temp.owner) or IsUnitEnemy(u, temp.owner)) then
                // Filter unit is an ally
                if IsUnitAlly(u, temp.owner) then
                    call GroupAddUnit(temp.allies, u)
                    set temp.allyNum = temp.allyNum + 1
                // Filter unit is an enemy, only enum it if it's visible
                elseif IsUnitVisible(u, temp.owner) then
                    call GroupAddUnit(temp.enemies, u)
                    set temp.enemyNum = temp.enemyNum + 1
                endif
                set u = null
                return true
            endif
            set u = null
            return false
        endmethod
        
        // For recounting enemies
        private static method enumCountEnemies takes nothing returns nothing
            set temp.enemyNum = temp.enemyNum + 1
        endmethod
        
        // For recounting allies
        private static method enumCountAllies takes nothing returns nothing
            set temp.allyNum = temp.allyNum + 1
        endmethod
        
        // Finds the closest item to the hero. If the hero's inventory is full, it will only pick up powerups
        private static method itemFilter takes nothing returns boolean
            local item i = GetFilterItem()
            local real ix
            local real iy
            local real dist
            if GetWidgetLife(i) > 0.405 and IsItemVisible(i) and (IsItemPowerup(i) or temp.itemCount < MAX_ITEM_INVENTORY) then
                set ix = GetWidgetX(i)
                set iy = GetWidgetY(i)
                set dist = (temp.hx-ix)*(temp.hx-ix)+(temp.hy-iy)*(temp.hy-iy)
                if dist < TempReal then
                    set TempReal = dist
                    set TempItem = i
                endif
            endif
            set i = null
            return false
        endmethod
        
        // This method will be called by update
        private method buyItems takes nothing returns nothing
            local integer i = .itemCount
            local integer price
            loop
                set price = .itemBuild.costs[i]
                if price <= .gold then
                    set .gold = .gold - price
                    call SetPlayerState(.owner, PLAYER_STATE_RESOURCE_GOLD, .gold)
                    call UnitAddItemById(.hero, .itemBuild.itemids[i])
                    set .itemCount = .itemCount + 1
                    exitwhen .itemCount >= MAX_ITEM_INVENTORY
                endif
                set i = i + 1
                exitwhen i >= MAX_ITEM_INVENTORY
            endloop
        endmethod   
        
      // Helper methods
        
        method operator percentLife takes nothing returns real
            return .life / .maxLife
        endmethod
        
        // Condition when hero attempts to flee
        stub method operator badCondition takes nothing returns boolean
            return .percentLife <= .35 or (.percentLife <= .55 and .mana / GetUnitState(.hero, UNIT_STATE_MAX_MANA) <= .3) 
        endmethod
        
        // Condition the hero tries to return to
        stub method operator goodCondition takes nothing returns boolean
            return .percentLife >= .85 and .mana / GetUnitState(.hero, UNIT_STATE_MAX_MANA) >= .65
        endmethod
        
        method operator isChanneling takes nothing returns boolean
            local integer o = GetUnitCurrentOrder(.hero)
            return o == 852664 or /* Healing spray
                */ o == 852183 or /* Starfall
                */ o == 852593 or /* Stampede
                */ o == 852488 or /* Flamestrike
                */ o == 852089 or /* Blizzard
                */ o == 852238 // Rain of Fire
        endmethod
        
        method recountEnemies takes nothing returns nothing
            set .enemyNum = 0
            // set temp = this
            call ForGroup(.enemies, function thistype.enumCountEnemies)
        endmethod
        
        method recountAllies takes nothing returns nothing
            set .allyNum = 0
            // set temp = this
            call ForGroup(.allies, function thistype.enumCountAllies)
        endmethod
        
      // Actions that can be called on hero

        method moveAround takes nothing returns nothing      
            call IssuePointOrder(.hero, "attack", .hx + GetRandomReal(-MOVE_DIST, MOVE_DIST), .hy + GetRandomReal(-MOVE_DIST, MOVE_DIST))
        endmethod 
        
        // Attempts to get any nearby items
        method getItems takes nothing returns boolean 
            call SetRect(ItemRect, .hx - SIGHT_RANGE, .hy - SIGHT_RANGE, .hx + SIGHT_RANGE, .hy + SIGHT_RANGE)
            set TempReal = SIGHT_RANGE * SIGHT_RANGE // Will be compared with distance that is squared
            set TempItem = null
            call EnumItemsInRect(ItemRect, Filter(function thistype.itemFilter), null)
            return IssueTargetOrder(.hero, "smart", TempItem)
        endmethod
        
        // Runs to (RUN_X, RUN_Y) with some randomness
        method run takes nothing returns nothing
            call IssuePointOrder(.hero, "move", RUN_X + GetRandomReal(-FOUNTAIN_RNG/2, FOUNTAIN_RNG/2), RUN_Y + GetRandomReal(-FOUNTAIN_RNG/2, FOUNTAIN_RNG/2) )
        endmethod        
        
        implement optional HeroAIPriority
        
        method assaultEnemy takes nothing returns nothing
            static if thistype.setPriorityEnemy.exists then
                call .setPriorityEnemy()
                call IssueTargetOrder(.hero, "attack", .enemy)
            else
                static if LIBRARY_FitnessFunc then
                    local group g = NewGroup() // Works weirdly if a global group is used
                    call GroupAddGroup(.enemies, g)
                    call PruneGroup(g, FitnessFunc_LowLife, 1, NO_FITNESS_LIMIT)
                    call IssueTargetOrder(.hero, "attack", FirstOfGroup(g)) 
                    call ReleaseGroup(g)
                    set g = null
                else // A lazy way to make the hero attack a random unit if Fitness_Func isn't there
                    call IssueTargetOrder(.hero, "attack", GroupPickRandomUnit(.enemies)) 
                endif
            endif
        endmethod
        
        method loopActions takes nothing returns nothing              
            if .needHeal then
                if not IsUnitInRangeXY(.hero, RUN_X, RUN_Y, FOUNTAIN_RNG) then
                    call .run()
                    call .runActions()
                else
                    call .fountainActions()
                endif
            else
                if not isChanneling then
                    if .allyNum > 0 then
                        if .assistAlly() then
                            return // Skip assaulting the enemy
                        endif
                    endif
                    
                    if .enemyNum > 0 then
                        call .assaultEnemy()
                    else
                        if not .getItems() then
                            call .moveAround()
                        endif
                    endif
                endif                      
            endif  
        endmethod
        
        // Updates information about the hero and its surroundings
        method update takes nothing returns nothing            
            set .hx = GetUnitX(.hero)
            set .hy = GetUnitY(.hero)
            set .life = GetWidgetLife(.hero)
            set .mana = GetUnitState(.hero, UNIT_STATE_MANA)
            set .maxLife = GetUnitState(.hero, UNIT_STATE_MAX_LIFE)
            set .itemCount = UnitInventoryCount(.hero)
            set .gold = GetPlayerState(.owner, PLAYER_STATE_RESOURCE_GOLD)
            
            if .itemCount < .itemBuild.total then
                call .buyItems()
            endif
            
            if not .needHeal and .badCondition then
                set .needHeal = true
            elseif .needHeal and .goodCondition then
                set .needHeal = false
            endif
            
            set temp = this
            call GroupClear(.enemies)
            call GroupClear(.allies)
            set .enemyNum = 0
            set .allyNum = 0
            call GroupEnumUnitsInArea(.units, .hx, .hy, SIGHT_RANGE, Filter(function thistype.filtUnits)) 
        endmethod
        
        static method defaultLoop takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            if not IsUnitType(.hero, UNIT_TYPE_DEAD) then
                call .update()
                call .loopActions()
            endif
        endmethod
        
        static method delayedLearning takes nothing returns nothing
            local thistype this = GetTimerData(GetExpiredTimer())
            call ReleaseTimer(GetExpiredTimer())
            call .learnSkills()
        endmethod
        
        static method create takes unit h returns thistype
            local thistype this = thistype.allocate()
            local timer t = NewTimer()
            set .hero = h
            set .owner = GetOwningPlayer(.hero)
            set .hid = GetHandleId(h)
            
            set .units = NewGroup()
            set .enemies = NewGroup()
            set .allies = NewGroup()
            
            set .tim = NewTimer()
            call SetTimerData(.tim, this)
            call TimerStart(.tim, DEFAULT_PERIOD, true, function thistype.defaultLoop)
            
            call SetTimerData(t, this)
            call TimerStart(t, 0, false, function thistype.delayedLearning)
            
            set heroesAI[.hid] = this
            
            set t = null
            return this
        endmethod
        
        method destroy takes nothing returns nothing
            call heroesAI.remove(.hid)
            call ReleaseGroup(.units)
            call ReleaseGroup(.enemies)
            call ReleaseGroup(.allies)            
            call ReleaseTimer(.tim)
            call .deallocate()                   
        endmethod
        
    endstruct
    
    private function interface RegisterHeroAIFunc takes unit h returns nothing    
    
    function RunHeroAI takes unit hero returns nothing
        if heroesAI.has(GetHandleId(hero)) then
            debug call BJDebugMsg(SCOPE_PREFIX + ": Warning! Running an AI for a unit that already has one.\nThe previous one will be destroyed.")
            call HeroAI(heroesAI[GetHandleId(hero)]).destroy()
        endif
        
        if infoAI.has(GetUnitTypeId(hero)) then
            call RegisterHeroAIFunc(infoAI[GetUnitTypeId(hero)]).evaluate(hero)
        else     
            call HeroAI.create(hero)        
        endif
    endfunction
    
    function RegisterHeroAI takes integer unitTypeId, RegisterHeroAIFunc register returns nothing
        debug if infoAI.has(unitTypeId) then
            debug call BJDebugMsg(SCOPE_PREFIX  + ": Warning! Registered an AI struct for a unittype id again.")
        debug endif
        set infoAI[unitTypeId] = register
    endfunction
    
    function GetHeroAI takes unit hero returns integer
        return heroesAI[GetHandleId(hero)]
    endfunction    
    
    function DoesHeroHaveAI takes unit hero returns boolean
        return heroesAI.has(GetHandleId(hero))
    endfunction
    
    private function MakeAILearnSkills takes nothing returns nothing
        local HeroAI ai = GetHeroAI(GetTriggerUnit())
        if ai != 0 and GetHeroSkillPoints(ai.hero) > 0 then
            call ai.learnSkills()
        endif
    endfunction
    
    // Textmacro to simplify registering a hero type's custom AI.
    //! textmacro HeroAI_Register takes HERO_UNIT_TYPEID
    private function RegisterAI takes unit u returns nothing    
        call AI.create(u)
    endfunction
    
    private module M
         static method onInit takes nothing returns nothing
             call RegisterHeroAI($HERO_UNIT_TYPEID$, RegisterAI)
         endmethod
    endmodule
    
    private struct S extends array
        implement M
    endstruct
    //! endtextmacro
    
    private module I 
        static method onInit takes nothing returns nothing
            call RegisterPlayerUnitEvent(EVENT_PLAYER_HERO_LEVEL, function MakeAILearnSkills)
                                              
            set infoAI = Table.create()
            set heroesAI = Table.create()
            set DefaultItemBuild = Itemset.create()
            
            call SetupItemTypes()
        endmethod
    endmodule
    
    private struct A extends array
        implement I
    endstruct
endlibrary
Optional Library: Hero AI Helper
A small add-on in an attempt to make coding custom AI easier.
If this library is included, the AI will prioritize enemy units based on its life, distance, and whether it's an hero. Sacrifices efficiency to make the AI look a bit smarter.
JASS:
library HeroAIHelper requires GroupUtils, FitnessFunc
    
    // Causes the AI to prioritize  enemy units rather than basing it on life alone
    module HeroAIPriority
        unit enemy
        
        static method weightPriority takes unit u returns real
            local real life = GetWidgetLife(u)
            local real maxLife = GetUnitState(u, UNIT_STATE_MAX_LIFE)
            local real lifeWeight = 2 * Pow( (maxLife - life)/maxLife, 1.5 ) + 1.5 * temp.life/life
            local real ux = GetUnitX(u)
            local real uy = GetUnitY(u)
            local real dx = ux - temp.hx
            local real dy = uy - temp.hy
            local real distWeight = 4.25 * Pow( HeroAI_SIGHT_RANGE - SquareRoot(dx*dx + dy*dy)  / HeroAI_SIGHT_RANGE, 1.85)
            local real factor = 1
            if IsUnitType(u, UNIT_TYPE_HERO) then 
                set factor = 3.5
            endif
            return (lifeWeight + distWeight)*factor
        endmethod
        
        method setPriorityEnemy takes nothing returns nothing  
            local group g = NewGroup()
            local unit u
            local real highest = 0
            local real p
            local integer i = 0
            
            call GroupAddGroup(.enemies, g)
            loop
                set u = FirstOfGroup(g)
                exitwhen u == null
                call GroupRemoveUnit(g, u)

                set p = weightPriority(u)
                if p > highest then
                    set highest = p
                    set .enemy = u
                endif
                set i = i + 1
            endloop
            
            call ReleaseGroup(g)            
            set g = null
        endmethod
    endmodule
    
    module HeroAIGetLowLifeAlly
        method getLowLifeAlly takes nothing returns unit
            local group g = NewGroup()
            call GroupAddGroup(.allies, g)
            call PruneGroup(g, FitnessFunc_LowLife, 1, NO_FITNESS_LIMIT)
            set bj_lastCreatedUnit = FirstOfGroup(g)
            call ReleaseGroup(g)
            set g = null
            return bj_lastCreatedUnit
        endmethod
    endmodule
    
endlibrary
Example AI for Paladin to casts Holy Light on a non-undead ally with the lowest life or on an undead enemy with the lowest life:
JASS:
library PaladinAI initializer Init requires HeroAI

    globals
        // An example to show that a Paladin can have different itemsets.
        private HeroAI_Itemset array Itemsets  
    endglobals
    
    private struct AI extends HeroAI
    
        method HolyLightHeal takes nothing returns real
            return 200. * GetUnitAbilityLevel(.hero, 'AHhb')
        endmethod
        
        method learnSkills takes nothing returns nothing
            call SelectHeroSkill(.hero, 'AHhb') // Holy Light
        endmethod
        
        static method HolyLightAllyFilter takes nothing returns nothing
            local unit u = GetEnumUnit()
            if IsUnitType(u, UNIT_TYPE_UNDEAD) or IsUnitType(u, UNIT_TYPE_MECHANICAL) then
                call GroupRemoveUnit(ENUM_GROUP, u)
            endif
            set u = null
        endmethod
        
        static method HolyLightEnemyFilter takes nothing returns nothing
            local unit u = GetEnumUnit()
            if not IsUnitType(u, UNIT_TYPE_UNDEAD) or IsUnitType(u, UNIT_TYPE_MECHANICAL) then
                call GroupRemoveUnit(ENUM_GROUP, u)
            endif
            set u = null
        endmethod  
        
        method assaultEnemy takes nothing returns nothing     
            local unit enemy
            
            call super.assaultEnemy() // So the Paladin will still attack the enemy unit with the weakest life
            
            // Things that should have greater priority should be put last:
            if .mana >= 65. then
                call GroupClear(ENUM_GROUP)
                call GroupAddGroup(.enemies, ENUM_GROUP)
                call ForGroup(ENUM_GROUP, function thistype.HolyLightEnemyFilter)
                call PruneGroup(ENUM_GROUP, FitnessFunc_LowLife, 1, NO_FITNESS_LIMIT)
                set enemy = FirstOfGroup(ENUM_GROUP)
                if enemy != null then
                    call IssueTargetOrder(.hero, "holybolt", enemy)  
                    set enemy = null
                endif
            endif            
            
        endmethod
        
        method assistAlly takes nothing returns boolean
            local unit ally 
            local boolean b = false
            
            if .mana >= 65. then
                call GroupClear(ENUM_GROUP)
                call GroupAddGroup(.allies, ENUM_GROUP)
                call ForGroup(ENUM_GROUP, function thistype.HolyLightAllyFilter)
                call PruneGroup(ENUM_GROUP, FitnessFunc_LowLife, 1, NO_FITNESS_LIMIT)
                set ally = FirstOfGroup(ENUM_GROUP)
                if ally != null then
                    // Cast Holy Light only if the ally has less life than the heal
                    if GetUnitState(ally, UNIT_STATE_MAX_LIFE) - GetWidgetLife(ally) >= HolyLightHeal()  then
                        set b = IssueTargetOrder(.hero, "holybolt", ally)    
                    endif                    
                    set ally = null    
                endif
            endif
            
            return b
        endmethod
        
        // This is where you would define a custom item build
        static method create takes unit hero returns thistype
            local thistype this = thistype.allocate(hero)
            set .itemBuild = Itemsets[GetRandomInt(0, 1)] 
            return this
        endmethod

    endstruct
    
    //! runtextmacro HeroAI_Register("'Hpal'")
    
    private function Init takes nothing returns nothing        
        set Itemsets[0] = HeroAI_Itemset.create()
        call Itemsets[0].addItemId('gcel', 100)
        call Itemsets[0].addItemId('ratc', 500) 
        
        set Itemsets[1] = HeroAI_Itemset.create()
        call Itemsets[1].addItemId('rst1', 100)
        call Itemsets[1].addItemId('rde3', 500)        
    endfunction
    
endlibrary
Current Issues: (More info here)

  1. I'm looking for a better way for a custom AI struct to call its learnSkills method when the AI gets created.
  2. An alternative to interfaces/stub methods
  3. The best way to periodically loop with the AI.
  4. Any other feedback that improves the code or suggestions for the AI.
Test Map
 
Last edited:
Awesome, a hero AI. :) It looks in pretty good shape so far, from what I can tell from the test map.

What other features will this end up supporting? (if any)

I'm also trying to figure out a way to allow the user to make certain heroes "buy" their own item types. Any thoughts?

I'm not sure if I know exactly what you mean. From what I can tell, it seems like something kind of map-specific. But then again I'm not completely sure what your problem is. :p

As far as the AI system goes, it seems to have a really nice interface. I never thought of making it like that. :thumbs_up: Good luck with it.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
First of all, thank you for your feedback. :D

What other features will this end up supporting? (if any)
I might try to attach other events firing to the AI, like when the hero gets damaged or when it finishes casting a spell. I also might make the AI support different places to run to if the arena features more than one fountain.

I'm not sure how useful these features would be though, so I'm hoping for more comments or suggestions.

As far as the AI system goes, it seems to have a really nice interface. I never thought of making it like that.
Yeah, I kind of got the idea to code it like this from the xe system.


About the item problem, the way I coded it, all AI heroes would end up buying the same thing. I want to let the user be able to specify their own items for individual heroes, while still having default items if no items are specified.
 
Level 16
Joined
Aug 7, 2009
Messages
1,403
Well I can imagine two ways:

1, A struct called Itemset. After creating one you can add items + costs wich will be stored in an array. The AI will attempt to buy those items (The buying order equals to the initialization order) if the owner has enough gold. After all the itemsets are initialized the user can attach them to unit types.
You can use J4L's hashing snippet, a table or even an array + an index can do the trick. For more fun you may make the system able to handle more itemsets per unit type, and the used itemset should be selected at the initialization.

2, If you don't like the first one, you can use tables and store the items and costs for each unit type. This is propably more precise, however the first one can do this as well, while multiple unit types can have the same builds attached.

Also, a question: do you plan an AI for AoS maps as well? It'd be fun IMO, I'd use it.
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Thanks for the suggestion! I went with the first method.
I also got rid of the GetItemCostById library.
Also, a question: do you plan an AI for AoS maps as well? It'd be fun IMO, I'd use it.
I don't have any plans for it right now. An AoS AI is much more challenging to code than an Arena AI. ><

EDIT:
Updated.
Followed Luorax's suggestions for items.
Added more documentation and did some changing to the code structure.
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
1,084
Bump.

So I'm still working on this for some reason and would like more feedback. :p


  1. I'm looking for a better way for a custom AI struct to call its learnSkills method when it gets created. If I try to call learnSkills in the default AI (HeroAI)'s create method, I call its learnSkills method which does nothing. Currently, I made a timer that expires in 0 seconds to call it.
  2. I know interfaces and stub methods generate some ugly code, so I want to know if there is a better way to avoid them while still allowing the user to customize those methods.
  3. Right now, I decided that each AI would have its own timer to loop for it via TimerUtils. This would allow the user to change an AI's periodic method by accessing the tim method. I'm wondering if this was a better choice than just using one static timer to loop through all AI.
  4. I'm looking for improvements or optimizations on the two libraries and suggestions for the AI.
 
Last edited:
I was working on a really cool system that forces units to do anything to survive (Look for the closest fountain, see if any nearby shops sell potions, run between trees to escape from enemies, etc..)
It's still a work in progress and I have tons of bugs.
That kind of AI would be epic for Creeps ^_^

This looks like a really good system :D
I was going to work on something like this, but since you just did, I guess that's a huge load off of me :p

What I'm trying to do is find a way to give the AI the ability to learn.
Unfortunately, that's impossible with Warcraft III, so instead, I'll just create arrays of boolexprs with procedures that must be taken in a certain scenario.

For example, one boolexpr would be run whenever the unit has less than 20% HP and the boolexprs taken in scenarios change (I change the array index) depending on what's going on in the map.

For example, a hero with 10% HP remaining out of 4000 wouldn't use a potion, he would go back to base, unlike a hero with 10% HP remaining out of 500.

The closest we can get to 'Learning' is 'Adaptation'.
I'd just have to order the boolexprs in the arrays from 0 to 10 so that the closer you are to 0, the weaker the hero, and the closer you are to 10, the stronger.
Then, I'd define an index that tells how strong the hero is.
(I'd use 1000 as an index internally, but I'll divide it by 100 to use it as an index in the array)
Whenever the hero gets a kill, I'll add 50 to the index. When he dies, I'll remove 30.
When he gets a kill streak, I'll give him an additional 10 points.

But, I'm going to have to monitor his HP over time.
If he got a 3-kill-streak with 80% HP and then, while still in combat, increased his streak to 8 with 50% HP, I'd definitely want him to continue, and I'd add points to his strength index. If he came out with 10% HP, I'd want him to be more cautious, so I'd remove about 100 points.

This sounds like a good system to me ^_^
 
Level 14
Joined
Nov 18, 2007
Messages
1,084
Thanks for the feedback! ^_^

The ideas that you mentioned are really interesting, but they're at a higher level of complexity and more map-specific than what I intended. So you better not stop working on them just because I made a basic system like this. :p

I was mainly influenced by Behavioral Mathematics for Game AI while trying to make this. You might be able to tell from the Pow I have in the priority module, in an attempt to weight the priorities better. Testing the map a lot was really necessary to get the right numbers or numbers close to what I wanted.

Then, I'd define an index that tells how strong the hero is.
(I'd use 1000 as an index internally, but I'll divide it by 100 to use it as an index in the array)
Whenever the hero gets a kill, I'll add 50 to the index. When he dies, I'll remove 30.
When he gets a kill streak, I'll give him an additional 10 points.
This inspires me to make some kind of aggression factor which would make the AI more/less likely to flee when engaging in combat. It would probably be saved for later though.
 
This inspires me to make some kind of aggression factor which would make the AI more/less likely to flee when engaging in combat. It would probably be saved for later though

That would be cool :D

So you better not stop working on them just because I made a basic system like this. :p

I will continue to dev it until it's smarter than me ^.^
 
Status
Not open for further replies.
Top