- 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.
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.
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:
Current Issues: (More info here)
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
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
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
I'm looking for a better way for a custom AI struct to call its learnSkills method when the AI gets created.- An alternative to interfaces/stub methods
- The best way to periodically loop with the AI.
- Any other feedback that improves the code or suggestions for the AI.
Last edited: