Author Topic: Spellcaster AI design  (Read 18743 times)

Aukustus

  • Rogueliker
  • ***
  • Posts: 440
  • Karma: +0/-0
    • View Profile
    • The Temple of Torment
Spellcaster AI design
« on: January 28, 2014, 08:18:28 PM »
I was wondering about some basic AI for spellcaster monsters and I was thinking how to design the AI. On mana based systems do you make the monsters to use mana or do they have infinite mana? And how to they select what spell to cast?

I have quite limited programming skills so I was thinking about a AI that randomly picks one spell and uses it. Some impromevent to that would be if it picks fireball but the player is too close to it it would then choose another spell. Or if it selects confusion and player is already confused it would choose another spell.

I'd like to hear opinions about that AI or what kind of spellcaster AI systems there are.

Quendus

  • Rogueliker
  • ***
  • Posts: 447
  • Karma: +0/-0
  • $@ \in \{1,W\} \times \{1,H\}$
    • View Profile
    • Klein Roguelikes
Re: Spellcaster AI design
« Reply #1 on: January 28, 2014, 09:25:40 PM »
Angband uses a simple system of randomly picking whether to use a spell/breath/ability (with a 1 in N chance for some N), and then selects randomly between the available spells. This provides a reasonable challenge. Some obvious modifications to increase "realism" or "fairness" have been implemented in options and variants, but these don't necessarily improve gameplay.

Programming monsters to make smart decisions which spell to use can introduce big balance problems - a monster with a healing spell becomes almost impossible to kill, and gaining resistance to one of a monster's abilities does little to diminish the danger posed by it.

Giving monsters a mana limit allows rather grindy ways to defeat them - if you withstand their spell use for long enough, then they're reduced to melee combat and low-level abilities and become much less dangerous.

Combining those "easy" and "difficult" variations on the basic system might make one more complex, but not necessarily one more fun or fair. An alternative is to use cooldowns to limit monsters' reuse of spells. This

I think the best way to develop AI for spell use is to start with a simple system and see how it plays. If it results in frustrating enemies, promotes grindy player tactics, or if certain spells need special treatment, then you can improve iteratively. They don't have to use the same spellcasting system as the player and they don't have to play optimally or fairly - they just have to provide a challenge and not look stupid or unfair doing it.

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Spellcaster AI design
« Reply #2 on: January 29, 2014, 02:37:42 AM »
My 2013 7DRL and ARRP game was all about spellcasting. I think it's a ton of fun to work on magic and ai. Magic is also one area where you can run with any crazy ideas you can dream up.

As far as coding goes; I've had good results with moving spellcasting logic out of the creatures and into the spells themselves. Spellcasters would loop through the spells they knew and ask each if it should be cast. If a spell should be cast, then it was told to do it's thing and the creature ended their turn; otherwise, the creature would move. So the ai part as well as how to cast the spells were contained in the code for the spell itself. It was pretty easy to add behavior like heal when injured, throw fireballs at enemies, use telekinesis to grab items far away, blow things up when it will do more damage to enemies than to yourself, freeze yourself if you're on fire, don't use fire on frozen enemies, etc.

As far as gameplay goes; when coding or playing I prefer systems where the monsters, npcs, and player are all following the same set of rules. That's what I did for my game: all spelcasters could use the same spells - and even pick up and use scrolls - and there was no mana or cooldowns or anything like that. What I tried to do instead is make it so each spell has some other drawbacks: magic missiles bounced off walls and split when they hit creatures so it was easy to end up hurting yourself more than others. You start with a spell that fully heals you but slightly reduces your max hp and vision distance. Fire does a lot of damage but doors, trees, grass, and creatures can catch and spread fire to the point that you find yourself in the middle of a burning inferno. The biggest cause of death for me was magic effects getting out of hand and backfiring. It was a very different feel from games where you have to wait for cooldowns or carry a bunch of mana potions.

CrowdedTrousers

  • Newcomer
  • Posts: 9
  • Karma: +0/-0
    • View Profile
Re: Spellcaster AI design
« Reply #3 on: January 29, 2014, 09:03:34 AM »
All my attacks are modular, atomic functions with a weighting.  Each monster maintains a list of attacks ordered by weight and will select the highest weighted attack in their list that suits their current circumstance. I add spells as attacks to a monsters list - for example, a ghoul gains a cast-slow spell every time they eat a corpse.  With a high weighting, the ghoul will always cast slow on its quarry. This removes the cast slow from its list, leaving it with only melee in its attack list after that.  Some spells/attacks are permanent, but have a cooldown, so I remove after use, but use a scheduler to reinsert that attack back into the respective critter's attack_list after the cooldown period.

So that is very deterministic - the AI plays to the best of its ability, including unleashing whatever it has at its disposal. Simply: beware the well fed ghoul.




Aukustus

  • Rogueliker
  • ***
  • Posts: 440
  • Karma: +0/-0
    • View Profile
    • The Temple of Torment
Re: Spellcaster AI design
« Reply #4 on: January 29, 2014, 09:50:48 AM »
Thanks for the answers.

The cooldowns didn't enter my mind at all. I like the idea about cooldowns but I think the rules should be the same to everyone, player and monsters. Maybe something like a general spell cooldown so that for example every 2 turns a spell could be cast. All spells have the same cooldown. Mana limit would be only for player. I think I'll weight the spells so that often caster uses regular spells and rarely special spells. And leave out too difficult spells such as monsters healing themselves.

Like a combination of what all you three said.

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Spellcaster AI design
« Reply #5 on: January 29, 2014, 06:00:48 PM »
So that is very deterministic - the AI plays to the best of its ability, including unleashing whatever it has at its disposal. Simply: beware the well fed ghoul.

That sounds similar to how I've been doing things. Is there a playable version or source code somewhere?

CrowdedTrousers

  • Newcomer
  • Posts: 9
  • Karma: +0/-0
    • View Profile
Re: Spellcaster AI design
« Reply #6 on: January 30, 2014, 09:17:46 PM »
That sounds similar to how I've been doing things. Is there a playable version or source code somewhere?

My code is spawling, poorly documented and in Python. I do have a build but I'm not ready to post it just yet. I'll try to explain it better...

In essence each monster has an attack_list of a class I've called called 'attack tokens'. Each token has attributes of name, range, value and function.  The value is the attack's weighting (higher values mean better attack), the function is a reference of the function to call if this attack is ever used.  Each monster keeps track of the highest valued attack in it's list, which will also give it the range it needs to be. When I process monster AI, and determine something wants to be violent, I check to see if its target is within range of its highest valued attack token - if not move closer, if in range I execute the function referred to in the attack token.

The key is to write attack functions that can be used in this way. Abstracting the attacks from the monster AI gives me lots of flexibility. I can add any kind of attack to any monster, and I'm working on tying this to the items as well - picking up a wand of cold will add a high-valued medium-range cold-ray attack token to a creature's attack_list.

A problem I have with this method is to be interchangeable at run time, all attack functions must have the same arguments.  So far I have standardised down to two -> passing reference to attacker and target.  There are times I want to pass more, but haven't worked out how to abstract that into the attack_token yet.

I've also been musing on incorporating detection conditions as well (which will push this even closer towards state machine, which it was based on initially). So I think that the attack tokens could also have a list of conditions, each is actually a function reference.  Those functions return a boolean, and when considering an attack token, you first process each conditional function in its list and if they are all true, execute the attack. This potentially entirely replaces my AI, because I could have a conditional function list containing [(am I standing next to an aggressor?)(am I less than 10% health?)(is my int>10)] with an 'attack' function of (run away!)

Quendus

  • Rogueliker
  • ***
  • Posts: 447
  • Karma: +0/-0
  • $@ \in \{1,W\} \times \{1,H\}$
    • View Profile
    • Klein Roguelikes
Re: Spellcaster AI design
« Reply #7 on: January 30, 2014, 11:17:17 PM »
Quote
A problem I have with this method is to be interchangeable at run time, all attack functions must have the same arguments.  So far I have standardised down to two -> passing reference to attacker and target.  There are times I want to pass more, but haven't worked out how to abstract that into the attack_token yet.
It should be possible to solve this problem in Python using default arguments/keyword arguments, or a similar construct. I don't know enough about your code to know how they're called or what jobs they have to do, but here's a few samples:

Code: [Select]
def punch(attacker,target,item=None):
target.health -= 1
def throw(attacker,target,item=None):
target.health -= item.throw_damage
punch(a,d)
throw(a,d,a.equipment["weapon"])

Code: [Select]
def punch(attacker,target,**kwargs):
target.health -= 1
def throw(attacker,target,**kwargs):
target.health -= kwargs["item"].throw_damage
punch(a,d)
throw(a,d,item=a.equipment["weapon"])

Code: [Select]
def punch(attacker,target,**kwargs):
target.health -= 1
def throw(attacker,target,**kwargs):
target.health -= kwargs["item"].throw_damage
attack_args={}
punch(a,d)
punch(a,d,**attack_args)

attack_args["item"] = a.equipment["weapon"]
throw(a,d,**attack_args)

Here are a few pages properly describing the syntax involved, and giving better (but less domain-specific) examples:
http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
http://docs.python.org/2/tutorial/controlflow.html#keyword-arguments

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Spellcaster AI design
« Reply #8 on: January 31, 2014, 01:58:00 AM »
A problem I have with this method is to be interchangeable at run time, all attack functions must have the same arguments.  So far I have standardised down to two -> passing reference to attacker and target.  There are times I want to pass more, but haven't worked out how to abstract that into the attack_token yet.

I don't know exactly how you have this implemented, and haven't used python in a long while, but could this be rolled into the attack token or another object? So instead of calling the function with whatever it needs, you ask it "What would you do in this situation" and it returns a function that you can call to do it? That worked for me. Here's an example of my getAi method for one of the spells that returns how strongly something should be done from 0.0 to 1.0 and a function that does it: https://github.com/trystan/PugnaciousWizards2/blob/master/src/spells/PullAndFreeze.as#L38

And here's how it's called: https://github.com/trystan/PugnaciousWizards2/blob/master/src/Hero.as#L30

With python's dictionaries it could be even cleaner to return a weight and function.

CrowdedTrousers

  • Newcomer
  • Posts: 9
  • Karma: +0/-0
    • View Profile
Re: Spellcaster AI design
« Reply #9 on: January 31, 2014, 10:41:44 PM »
thanks for the feedback guys, and sorry your OP has been hijacked, Aukustus!!

So I think you're both still not quite hitting my issue.  The problem is not so much in the attack functions themselves, but calling them in an abstract way.  A monster's AI goes like this

  • attack = highest rated  token in my attack_list
  • attack.attack_function(args)

So the problem is that when I call an attack function, I have no idea what you're calling.  Is it throw() which requires (attacker, target, item); or is it punch() which requires (attacker,target)?  How do I stack out an argument list when I don't know the function I'm calling? 

So there seems to be two options - either pass every possible attribute and let the function work it out (shudder - probably huge considering all attacks) or have a massive case statement for each argument possibility (also huge for same reason).  Neither appeal to me.



Aukustus

  • Rogueliker
  • ***
  • Posts: 440
  • Karma: +0/-0
    • View Profile
    • The Temple of Torment
Re: Spellcaster AI design
« Reply #10 on: January 31, 2014, 10:43:23 PM »
thanks for the feedback guys, and sorry your OP has been hijacked, Aukustus!!

No problem, I think this forum needs much more activity and discussion.

Quendus

  • Rogueliker
  • ***
  • Posts: 447
  • Karma: +0/-0
  • $@ \in \{1,W\} \times \{1,H\}$
    • View Profile
    • Klein Roguelikes
Re: Spellcaster AI design
« Reply #11 on: January 31, 2014, 11:39:51 PM »
thanks for the feedback guys, and sorry your OP has been hijacked, Aukustus!!

So I think you're both still not quite hitting my issue.  The problem is not so much in the attack functions themselves, but calling them in an abstract way.  A monster's AI goes like this

  • attack = highest rated  token in my attack_list
  • attack.attack_function(args)

So the problem is that when I call an attack function, I have no idea what you're calling.  Is it throw() which requires (attacker, target, item); or is it punch() which requires (attacker,target)?  How do I stack out an argument list when I don't know the function I'm calling? 

So there seems to be two options - either pass every possible attribute and let the function work it out (shudder - probably huge considering all attacks) or have a massive case statement for each argument possibility (also huge for same reason).  Neither appeal to me.
The **kwargs options let you pass a dictionary - data - into the function. Store that dictionary with the attack option. Or store a method that generates the dictionary.

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Spellcaster AI design
« Reply #12 on: February 01, 2014, 01:32:48 AM »
So the problem is that when I call an attack function, I have no idea what you're calling.  Is it throw() which requires (attacker, target, item); or is it punch() which requires (attacker,target)?  How do I stack out an argument list when I don't know the function I'm calling? 

Yeah, that's why I suggested that it returns a function that already has all that figured out. In pythonish pseudocode:

Code: [Select]
class LightningSpell
  def getAction(caster):
    if caster.visibleMonsters.length == 0:
      return { percentChance: 0.0,
                    func: lambda: pass }
    else:
      maxDistance = caster.viewDistance
      closestDistance = caster.distanceTo(caster.closestEnemy)
      return { percentChance: 1.0 - closestDistance / maxDistance,
                    func: lambda: castSpell(caster, caster.closestEnemy) }

  def castSpell(caster, enemy):
    caster.world.add(new Lightning(caster.postion, enemy.postition))

I don't remember exactly how to return anonymous functions in python, but I think it's close to that. As long as all spells have the same getAction interface ai code just has to call getAction, check the percentChance, and call func if it thinks it's worth doing.

The user interface can call the castSpell method directly.

So there seems to be two options - either pass every possible attribute and let the function work it out (shudder - probably huge considering all attacks) or have a massive case statement for each argument possibility (also huge for same reason).  Neither appeal to me.

Or the option of having the function decide for itself who to target, what to use, etc. It's much more common in languages that encourage functional programming instead of object oriented, but it should be able to work in python.
« Last Edit: February 01, 2014, 01:36:12 AM by Trystan »

Quendus

  • Rogueliker
  • ***
  • Posts: 447
  • Karma: +0/-0
  • $@ \in \{1,W\} \times \{1,H\}$
    • View Profile
    • Klein Roguelikes
Re: Spellcaster AI design
« Reply #13 on: February 01, 2014, 01:46:46 AM »
Python does provide functional programming... or at least it claims to.

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Spellcaster AI design
« Reply #14 on: February 01, 2014, 02:01:08 AM »
Python does provide functional programming... or at least it claims to.

Probably. Years ago the benevolent dictator was very "meh" about it. Maybe things have changed. It wasn't meant as a jab at python or the community - just comparing python idioms to idoms from haskell or the lisps.