Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
This system provides a simple computer AI for arena-type maps. Basic actions for the AI include learning skills, picking up items, buying items, and running away. The default AI can be customized further by the user for specific heroes.
There are also four optional add-ons intended to make coding the AI easier.
One is the priority library that allows the AI to choose targets based on more factors.
The second is the threat library, which allows the AI to perceive threat. This will factor in the AI's decision to run away.
The third is the event response library, intended to allow the AI to react to specific events.
The last is an independent library from Hero AI and allows the AI to have a handicap or advantage based on its difficulty.
The code is quite long and better seen in the test-map with the example AIs.
v4.3.4
Removed FireCode which has been graveyarded. Magtheridon96 is still credited for the logic of evaluating a boolexpr.
Removed GetHeroAIIndex, which should have been pretty useless
Clarified parts of the documentation
v4.3.3
Added a curItem method operator to retrieve the item the hero is trying to buy.
v4.3.2
The methods canBuyItem and buyItem are now accessible.
v4.3.1
Now properly accounts for neutral units with a NEUTRAL_PLAYER constant.
Optionally supports IsUnitChanneling
v4.3.0
GroupUtils is now optional.
weightPriority is now a method instead of being static.
Added a weightThreat method to HeroAIThreat.
Removed some dead code.
v4.2.0
Custom AI are now able to define a shop unit for the AI to go to by setting .shop or calling .canShop().
The AI state variable is now readonly.
SELL_ITEM_RANGE is now public.
v4.1.0
Learnset can now account for morphing heroes.
Learnset now supports multiple skills per level.
v4.0.0
Separated learnset and itemset-related configuration from HeroAI into their own triggers for organization. They are still part of the core system.
Added AIPlayerDifficultySettings. This library is independent of HeroAI and allows the user to handicap or give an advantages to computer players based on their difficulty setting.
The system now requires GetClosestWidget.
Added a new function, RegisterHeroAISkill, to allow the user to define the learnset for a hero in its AI struct
Changes IsSafeUnit to take the hero's owner instead.
SetupItemTypes was renamed to SetupDefaultItemBuild.
Buying items now supports lumber.
Itemset syntax has been changed. The system now requires registration of items to find out its gold and lumber cost. Items can also consider shop ids so that the AI will run to them.
Itemsets function a bit differently. They can hold up to MAX_ITEMSET_SIZE items and will be used to tell how the AI should upgrade its hero's items.
Heroes will refund items when they try to improve their item build.
HeroAIStruct now has a utility method operator to refer to the amount of lumber the owner has. Both this and gold can now be directly set to affect the owner's resouces.
runAway has been deprecated in favor of a state member. This will be used to tell what kind of condition the hero is in.
v3.1.0
Changed RegisterHeroAI so that it won't be inlined.
Changed behavior of the AI to attack enemies or assist allies if the hero is at the safe spot and safeActions is not defined.
Now allows the user to customize the threat threshold factor for running away. THRESHOLD_RUN_FACTOR is used for the default and is based on percent life.
Minor code improvements
[Test-Map] Updated FireCode
v3.0.0
Allowed threat and priority to support specific unit configuration
AI can now consider threat to run away
Includes a THRESHOLD_RUN_FACTOR in HeroAIThreat so that heroes won't easily run away
Allowed AI to find a place to run to instead of always running to static coordinates
Added an on acquire event to HeroAIEventResponse
Fixed USE_ON_ATTACKED
Renamed moveAround to move
Renamed FOUNTAIN_RNG to SAFETY_RANGE
Renamed needHeal to runAway
Improved Learnset to fully reflect that only one instance is created
General code improvements
v2.0.0
Switched to a module interface
AI can no longer be destroyed
Corrected priority library
Added an optional add-on, HeroAIEventResponses
Uses FireCode instead of the function interface
isChanneling now has a text macro to provide easier configuration
Restructured some code
Renamed MAX_ITEM_INVENTORY to MAX_INVENTORY_SIZE
v1.0.0
Released
Please give credit if you use this system in your map.
Credits:
Magtheridon96 for IsUnitChanneling and RegisterPlayerUnitEvent. His feedback was also valuable in improving the system.
Vexorian for TimerUtils
Rising_Dusk for GroupUtils
Bribe for NewTable
Anitarf for PruneGroup, FitnessFunc, and SpellEvent (used only in test-map)
Spinnaker for Get Closest Widget
jim7777 for good feedback and suggestions
DarnYak for EotA which inspired the Assassin AI in the test-map
Any feedback or suggestions to improve this system would be greatly appreciated.
16th Jan 2012
Bribe: This is a great template/building block and I'm thinking to recommend it.
GroupFunctionality is a mostly terrible resource. Half of it was already done in Blizzard.j, the GroupRemoveUnitTimed thing is pretty good though. For...
16th Jan 2012
Bribe: This is a great template/building block and I'm thinking to recommend it.
GroupFunctionality is a mostly terrible resource. Half of it was already done in Blizzard.j, the GroupRemoveUnitTimed thing is pretty good though. For the other stuff you typically want to use FirstOfGroup loops instead.
Your GetAngleDifference function gets the smallest angle between two angles? I wonder if it is more efficient than this:
JASS:
set ang = RAbsBJ(ang - ang2)
if ang > 180 then
set ang = R2I(ang) / 360 * 360 + 360 - ang
endif
I don't see a problem using a local text tag instead of "bj_lastCreatedTextTag", because, keep in mind that JASS is an interpreted language, longer names take longer to process, and that the optimizer will not compress bj_ global names.
You end up with a lot of duplicates of code by using Anitarf's functions interfaces. While I am not aware of any alternatives to fitness functions, http://www.hiveworkshop.com/forums/jass-resources-412/system-spell-struct-204774/ by Nestharus poops all over SpellEvent by Anitarf.
I'm considering approving this and since I could not find glitches I will approve it with an initial rating of 3/5.
private interface AIInterface
method loopActions takes nothing returns nothing defaults nothing // Determines the periodic behavior of the hero. Defined by default.
method assistAlly takes nothing returns boolean defaults false // Actions to take when supporting allies. Needs to return true if a supportive action was taken. Has higher priority than attacking enemies
method assaultEnemy takes nothing returns nothing defaults nothing // Actions to take when attacking enemies. Defined by default.
method runActions takes nothing returns boolean defaults false // Actions to take when running to (RUN_X, RUN_Y). Needs to return true if an action was taken.
method safeActions takes nothing returns nothing defaults nothing // Actions to take while at (RUN_X, RUN_Y)
endinterface
struct HeroAI extends AIInterface
JASS:
group units
group allies
group enemies
ZOMG
You don't have to use an interface, module interfaces are cooler (Cleaner, faster, and they don't create shitcode for teh lulz)
Since you don't need the interface, you can make that struct extend an array.
Also, instead of a group, you can use a linked list of units (LinkedListModule by Dirac and UnitIndexer so you don't only store 32-bytes of data instead of storing ~3072-bytes.) (By this, I mean you should store the Unit Ids.)
Or, LinkedListModule and UnitIndexer because dynamic groups are too old school now
JASS:
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
This method should be in a textmacro somewhere at the top of the script so that it's easier for the user to configure it.
private function interface RegisterHeroAIFunc takes unit h returns nothing
This duplicates a lot of code :/
You could make the user pass code arguments to the functions instead and if you need to store them, convert them to boolexprs with the function Filter(code)
It's a lot cooler ^_^
There are many more things that could be improved, but I'm really tired at the moment.
I want to give this a 5/5, but I can't do that yet
@Magtheridon96
If I used a module interface instead, would the user still be able to call on the AI's actions from a hero without knowing what kind of AI the hero has?
I'll look into using linked lists but it looks like it might complicate things on the user's end.
For the function interface: If I switched to using boolexprs, I would need a trigger to evaluate it and a global variable to store the unit, right?
For the unit, yes.
For the trigger, nah, you can add "FireCode" as a requirement (Jass section)
But, I wouldn't recommend doing that just yet cause I just did a ninja-update and found a major bug in it
I'll tell you when it's safe to use it
edit
FireCode has been updated.
Now that I think about it, a LinkedList wouldn't make much of a difference.
It would just add tons of shitcode to your library.
Use those groups, it's ok.
First, you need to make sure that you have implemented all the required libraries. So basically all the triggers only under the "Needed Libraries" needs to be copied into your map.
Then copy the trigger named HeroAI into your map.
Now, to run the AI for a hero, you need some custom script.
Custom script: call RunHeroAI(YourUnit)
Right now, I'm still updating the system so I'd advise you to wait before implementing this.
Alrighty. Great work on this. I'm really planning on using this after it is finished. Do also make a documentation for noobs (like me) on how to uhm use it properly. Since, I really lack in jass in anyway.
After you fix the stuff up like the interface, I'll do a full review of this line by line for you : ). This is a library that I was thinking about making for a while and one that appears to be quite good ;p, so it's one that I look forward to reviewing ; ).
struct Learnset
private TableArray info
method operator [] takes integer key returns Table
return info[key - 1]
endmethod
static method create takes nothing returns thistype
local thistype this = thistype.allocate()
set .info = TableArray[MAX_SKILL_LVL]
return this
endmethod
endstruct
Well, you aren't destroying any of the instances, so the optimal solution here would be this:
JASS:
struct LearnSet extends array
// This integer does all the allocation for you ;)
private static integer stack = 1
// 3-D Array, FUCK YEAH!
private static TableArray array info
method operator [] takes integer key returns Table
return info[this][key - 1]
endmethod
static method create takes nothing returns thistype
local thistype this = stack
set stack = stack + 1
set info[this] = TableArray[MAX_SKILL_LVL]
return this
endmethod
endstruct
This way, you're producing much less compiled code, making the script more light-weight
I broke compatibility though
I want it to follow Jass convention
And take your time on fixing those Interfaces (I know how hard it is to manage a module interface)
edit
And when you update the code, could you paste it outside of hidden tags?
The font size is horrible
Nah, you can keep it as 1.0.0 since it's still new
My ModeManager system is already up to 6.0.0.0 =p
I broke backwards compatibility with long intervals of time xD
I usually wait 3 days after the first upload before I go to 2.0.0.0 when breaking backwards compatibility (I don't want to fall in the same trap I did while developing ModeManager)
public constant real DEFAULT_PERIOD = 1.7 // The period in which the hero AI will do actions. A very low period can cause strain.
While this makes sense in general coding, it makes more sense to use events for wc3.
public constant real MOVE_DIST = 1000. // The random amount of distance the hero will move
This should probably be different depending on the unit
public constant integer MAX_INVENTORY_SIZE = 6 // The number of items the hero can hold
This is dependent upon unit and possible custom inventories
public constant real FOUNTAIN_RNG = 500. // The range the hero should be in of the fountain.
Should probably be changed to aura and it depends on the aura >.>. This value shouldn't exist... it should go until the unit has the aura on them.
private constant integer MAX_SKILL_LVL = 10 // The level at which the hero learns all of its skills
This is just plain silly as it can be dynamically calculated
return .percentLife <= .35 or (.percentLife <= .55 and .mana / GetUnitState(.hero, UNIT_STATE_MAX_MANA) <= .3) or (.maxLife < 700 and .life <= 250.)
Naive approach. This should depend on the number of enemy units, the number of ally units, the state of the ally units vs the enemy units, and depend on if there is a healer in range or not, in which case the AI should use the healer to heal the hero or w/e.
return .percentLife >= .85 and .mana / GetUnitState(.hero, UNIT_STATE_MAX_MANA) >= .65
Same issue. The return to points should be any point under play control that provides a means of healing.
JASS:
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
Very bad approach. You can easily detect whether a unit is currently channeling or not. Make a seperate resource with an IsUnitChanneling function.
This seems stupid. It should be dependent on the hero and the state of the player and team.
JASS:
private function SetupLearnset takes nothing returns nothing
// Syntax:
// set LearnsetInfo[LEVEL OF HERO][HERO UNIT-TYPE ID] = SKILL ID
// Paladin
set LearnsetInfo[1]['Hpal'] = 'AHhb' // Holy Light
set LearnsetInfo[3]['Hpal'] = 'AHhb'
set LearnsetInfo[5]['Hpal'] = 'AHhb'
set LearnsetInfo[2]['Hpal'] = 'AHds' // Divine Shield
set LearnsetInfo[4]['Hpal'] = 'AHds'
set LearnsetInfo[7]['Hpal'] = 'AHds'
set LearnsetInfo[8]['Hpal'] = 'AHad' // Devotion Aura
set LearnsetInfo[9]['Hpal'] = 'AHad'
set LearnsetInfo[10]['Hpal'] = 'AHad'
set LearnsetInfo[6]['Hpal'] = 'AHre' // Resurrection
// Blood Mage
set LearnsetInfo[1]['Hblm'] = 'AHfs' // Flame Strike
set LearnsetInfo[3]['Hblm'] = 'AHfs'
set LearnsetInfo[5]['Hblm'] = 'AHfs'
// Assassin
set LearnsetInfo[1]['E000'] = 'A001' // Backstab
set LearnsetInfo[3]['E000'] = 'A001'
set LearnsetInfo[5]['E000'] = 'A001'
set LearnsetInfo[7]['E000'] = 'A001'
endfunction
This should depend on the current state of the game >.>. Obviously, if you are fighting undead heroes, you are going to want to go for Holy Light before anything else. If you have a large army and are fighting regular units, devotion aura. If you are fighting heroes regularly Divine Shield.
struct Itemset
This needs to provide a way to interface with a custom inventory
and (IsUnitAlly(u, temp.owner) or IsUnitEnemy(u, temp.owner))
That == true..
private method buyItems takes nothing returns nothing
Hero should buy items straight from the shop, so the set player state thing is silly as is the unit add item by id..
method defaultLoopActions takes nothing returns nothing
Instead of this, a goal needs to be defined for the hero. From there, the AI can follow actions leading to that goal.
method update takes nothing returns nothing
This is rather silly as it should just be tied in with events...
Threats should depend on hero weaknesses and strengths... if a hero is weak vs a specific unit, then obviously that hero should go for that unit first.
USE_ON_ATTACKED
Should use acquisition range rather than on attack
Overall, I wouldn't recommend anyone to use this resource quite yet. It requires a lot of work and a lot of rewriting. Try again ^)^. Also keep in mind that AI actions are largely dependent on the unit and the game that the unit is in as well as the state of the game. This makes general AI pretty much impossible to do, which is why I personally haven't done one. You could sort of get away with unique AI scripts for specific units, but you'd still have to customize them to a specific game (perhaps like a core goal script that effects all of the units).
All in all, I think AI has 3 parts: core AI script that all AI uses, unit specific script, and team specific scripts. You sort of have the core AI, although it isn't that great, as well as minimal support for unit specific (again not that great). This is why I said that you should probably just rewrite the whole thing ; ).
I'm not convinced on this, though I could easily change the system to allow this. How is moving a random distance important enough for a specific unit type?
Should probably be changed to aura and it depends on the aura >.>. This value shouldn't exist... it should go until the unit has the aura on them.
The main purpose is to provide a size for the struct Learnset.
Naive approach. This should depend on the number of enemy units, the number of ally units, the state of the ally units vs the enemy units, and depend on if there is a healer in range or not, in which case the AI should use the healer to heal the hero or w/e.
The Paladin AI shows an example of how a custom inventory can be defined. The latter is really map-specific.
This should depend on the current state of the game >.>. Obviously, if you are fighting undead heroes, you are going to want to go for Holy Light before anything else. If you have a large army and are fighting regular units, devotion aura. If you are fighting heroes regularly Divine Shield.
Either way, checking the player state is still required if you wanted the AI to buy items on triggers alone. The system only mimics buying items though since the AI never directly goes to a shop.
JASS:
method update takes nothing returns nothing
This is rather silly as it should just be tied in with events...
Threats should depend on hero weaknesses and strengths... if a hero is weak vs a specific unit, then obviously that hero should go for that unit first.
Overall, thanks for your feedback. I felt, however, that you expected too much out of this system. My main goal was to create a basic AI that could be considered a template for unit specific ones. Something like team AI was never my intention because it becomes too map-specific and more of a hassle to configure.
Updated to v3.0.0.
Main points (More detailed changelog in first post):
Backwards compatibility was broken by allowing heroes to choose a unit to run to instead of having the static coordinates RUN_X, RUN_Y. Some values and methods were also renamed.
Method interfaces were added to the priority and threat library to allow more specific configuration for a specific AI.
The AI can now factor in threat when deciding to run away. To help with this, a THRESHOLD_RUN_FACTOR is added in the threat library so that heroes won't run away so easily.
An on acquire event was added to the event response library.
Yes, I am planning to allow users to define custom events which can interact with the AI. The problem is that the events need to be specific to the unit so I'm probably going to have to modify Event a bit.
Ah, thanks for pointing that out. The problem is caused by the inliner which ruins the code/boolexpr hack which RegisterHeroAI uses. I didn't notice it since I always had debug mode on which prevented the function from being inlined.
hmm is it possible to write the Hero Skills setup in a respective custom AI? like how you enabled overwriting default item builds?
the library might get long..... but i think this can be optional or it will work like how does item builds setup work, difference is, there's no overwriting part or it can be a support for a skill like Attribute Bonus so you wouldn't write attribute bonus for each hero
EDIT:
i tried using up the test map ai but unfortunately, the Paladain's runActions method isn't working.. the Cast part with the priority one... i can't let it work :/ the firstofgroup always returns null...
but i just used (copied) the same code... and edit the setup part with hero skills, but it always returns null.. i removed the modPriority method
it is assaultEnemy method
the one which will cast Holy bolt, but in my case, i changed it to impale but it isn't casting the skill
EDIT:
nvm managed to fix it just condition problems
EDIT 2:
i think the AI should go to a respective shop when buying or having items, it's kinda weird when they're walking then suddenly they have instant items.
Like renewing the API to something like call DefaultItemBuild.addItemId('ITEM_ID', 'BUILDING_ID', GOLD_COST)
so the AI must go to the building first or at least near the building, well at least the building id can be the id when the unit is pre-placed on the map or detect if any valid bldg ids in the map?
i think the AI should go to a respective shop when buying or having items, it's kinda weird when they're walking then suddenly they have instant items.
Yeah, I'm still thinking of a good way to do this. Would it make sense for the AI to attempt to walk towards the item shop when it has enough gold to purchase something, but if it encounters enemies, it'll start attacking and forget about buying things, unless it's really close to the shop?
Ah, I'm probably going to break backwards compatibility again and move the version up to v4.0.0 when I update the item-related issue.
Somewhat unrelated, but I'm thinking of separating the learnset and itemset related things from HeroAI into two other libraries that get required by HeroAI. Any opinions on this?
I guess you may use either the region or the id of the shop since some shop (like in my map) is wandering around and some are just buildings
the action will AI do when he encounters an enemy when attempting to buy or go to a shop will be up to the user.. OR if it is safe to go to the shop when no enemies are nearby OR any safeCondition given by the user
in short just provide an API for that kind of method
for the libraries, its up to you
EDIT:
oh wait i can't use IsUnitVisible(u,.owner) in the EnemyFilter method (the one in paladin's test AI)
undeclared variable this
hmm.. not possible to use .owner in other methods? or any help ??
temp is a static member provided by the HeroAIStruct module to help with group enumerations. It basically stores the struct instance and is set automatically by the method update (update gets called periodically so you should almost never have to call it).
Itemsets are now done a bit differently along with item registration. Itemsets can now have up to MAX_ITEMSET_SIZE items. The AI can improve their item build and refund bought items.
Items now support a lumber cost.
Heroes can now go to shops to buy items instead of buying them instantly. If you don't want this feature, set up the item to have a shop type id of 0.
runAway has been replaced with a state member.
An independent library was added that can give a handicap or advantage to a computer player based on their difficulty.
I haven't actually changed learnset at all besides moving it and adding that function.
I'm not exactly sure about Engineering Upgrade. I'm guessing that you should be able to make it work as long as you make the learnset choose the correct upgraded ability after an Engineering Upgrade ability has been learned.
EDIT: Yes you do need to do it like that unfortunately. :\
EDIT 2: I just realized that I should make a note for the user to copy the learnset for a hero that can morph in case it levels up while morphed. Or do something so that it won't be such a pain.
I can't make Engineering upgrade to work (the hero won't learn the abilities applied/changed)
just going to edit some stuffs.. lets see what will happen >
EDIT:
Can you have a new way of setting up the Skills?
so it wouldn't overwrite each other since the easiest yet inefficient way of fixing is learning both of the abilities at the same time, but unfortunately, The system learn setup will just overwrite with each other D:
Here's an example of what the Tinker's learnset looks like in the new test map version (4.0.1) that I will probably upload tomorrow:
JASS:
// Tinker
// Engineering Upgrade makes this a bit messy.
// You will have to make sure that the other 3 abilities he learn
// are correctly set to the upgraded ones from Engineering Upgrade.
set LearnsetInfo[1]['Ntin'] = 'ANcs' // Cluster Rockets
set LearnsetInfo[4]['Ntin'] = 'ANc1' // EU upgraded to level 1 at this point
set LearnsetInfo[8]['Ntin'] = 'ANc2' // EU upgraded to level 2 at this point
set LearnsetInfo[2]['Ntin'] = 'ANsy' // Pocket Factory
set LearnsetInfo[5]['Ntin'] = 'ANs1' // EU upgraded to level 1 at this point
set LearnsetInfo[10]['Ntin'] = 'ANs3' // EU upgraded to level 3 at this point
set LearnsetInfo[3]['Ntin'] = 'ANeg' // Engineering Upgrade
set LearnsetInfo[7]['Ntin'] = 'ANeg'
set LearnsetInfo[9]['Ntin'] = 'ANeg'
set LearnsetInfo[6]['Ntin'] = 'ANg1' // Robo-Goblin (EU upgraded to level 1 at this point)
Yeah it's really ugly. :\
EDIT:
Can you have a new way of setting up the Skills?
so it wouldn't overwrite each other since the easiest yet inefficient way of fixing is learning both of the abilities at the same time, but unfortunately, The system learn setup will just overwrite with each other
the hero is in max level, but the hero doesn't have the EU ability but instead will be added via trigger / item..
that's my problem since there's no exact level when to add the EU ability... and the worst thing is the hero will wonder to learn the non-EU or the EU enhanced ability D: and there's no skipping of levels... since the Eu isn't a skill of that hero
Perhaps having a boolean in the API?? that should be one of the best option since using a code like this:
will just let the hero learn the UPGRADED_ID even though he doesn't have the EU ability.. that will result into failure right? 'cause the ABILITY_ID setup will be overwritten by UPGRADED_ID according to your code
Perhaps avoid overwriting of skills? 'cause in that code, the ai will check 2 possible skills at that level which is 16 but the other one can only be learned if the hero has EU which i said earlier that it will be acquired/given via triggers or such
i hope you can understand me
but the shortest point is
disallow overwriting of skills >
'cause the hero might be able to learn two skills at one level (like learning ability 1 and ability 2 at level 4) something like that.. since the current learn set doesn't allow that
Learnset supports an easy way for a hero that has a different unit-type id from another hero type (ex: a morphed hero) and should share the same learnset.
Learnset can now support multiple skills learned per level. If you don't want this feature, you should set MULTIPLE_SKILLS_PER_LVL to false in the HeroAILearnset trigger.
Static if abuse has increased greatly
@jim7777: If the case you describe is only for one hero, you should probably just code a custom learning trigger for it specifically since it would be more efficient than using MULTIPLE_SKILLS_PER_LVL.
oh well... my suggestion isn't for one hero only, almost every hero in my map. LOL
the awkward moment i'm looking for the code.. I just saw paste bin
LOL
EDIT
great >
the last thing is, perhaps add a struct member / variable which will get the AI's current difficulty, so I wouldn't be calling the native so often
(for easier coding LOL)
the last thing is, perhaps add a struct member / variable which will get the AI's current difficulty, so I wouldn't be calling the native so often
(for easier coding LOL)
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.