[Code Talk] Intelligent Enemies

So there were several problems at hand with what I wanted my monsters to be able to do in Witness to Unity. Some of it came down to the limits of the program I chose to use, but a lot of it seems to be because I'm still pretty dumb when it comes to programming. Oops.

RPG Maker VX Ace is not very flexible about making enemies. By default targets are random, and the pre-conditions for using an action are based on radio buttons. Radio buttons are the this-or-that circular selections.

Back in RPG Maker 2003 you had a drop-down list making you select this-or-that about the conditions for an enemy to act. While this is similarly problematic, you had the possibility of turning a switch on or off after an action. You could at least sort of fake a pattern or sequence with this, by chaining one action to another.

In RPG Maker XP, you got tick-boxes. Tick-boxes that let you require all conditions be met. ... okay, there weren't that many conditions to choose from, but these are some major enough conditions that others could be always handled by switches toggled either in battle of beforehand.

I'm using RMVXAce because it is up-to-date, in terms of the underlying engine, running the smoothest and fastest of all I've tried, especially after jumping from RMXP and coding in basically the same elements. To get around the problem of the radio buttons, I had to resort to using a custom script that allows multiple conditions for an action.

<enemy action: 20, 10>
$game_troop.state?(4) || $game_troop.state?(5) || $game_troop.state?(6);
</enemy action>
<enemy action: 38, 10>
hp higher 90%;
turn 4*;
!state?(13);
</enemy action>

Something as simple as "Ecstatic Nostalgia"'s eagerness for recognition can be coded in with this script. I want the enemy to fly into a rage when it's ignored? I can have it use a skill that self-inflicts Berserk if it still has full HP after a certain number of turns.

Some things haven't been so straight-forward. Enemies still randomly target characters on either side of the field, and still use actions if the die rolls right and conditions are met, even if they shouldn't. I have a heavily modified "smarter AI" script that allows actions to go to the right targets and be disregarded as long as they're not useful. This wasn't terribly straight-forward. This process has involved a lot of shuffling code and redoing and giving up and learning new syntax and getting things confused so I'll try to keep this as straight-forward as possible, since I haven't been writing this as I've worked.

The pictured skill, Devile, cures an ally of heavy bleeding, indigestion, or pain. I wanted this skill in the (giant cardboard) hands of the enemy, to only be used when there is a target with one of those states, and on a target with one of those states.

The original "smarter AI" script appeared to be focused on low-HP enemy-side allies and possessing a status condition. I created a special process that allowed Ecstatic Nostalgia only attempt to cure Indigestion if someone in their party, had Indigestion, via the earlier multiple-conditions script, but this script wasn't made for that.

def state?(state)
  for member in members
    return true if member.state?(state)
  end
  return false
end

Additionally, because by default targets were random, and by this script the target was the one with the least HP, it could not be certain status-curing skills would actually got to the right place. In fact because of this script, if an allly had low HP, but not a status condition, the one with the Indigestion would not get healed. I didn't realise this at the time, yet I still removed this segment during my editing, I later discovered. I don't really know what happened there.

So this script would delete targets from the possible targets array if they didn't meet certain conditions. The early version with my own edits allowed it to ignore targets that had a status condition and were not immune to a certain element. The point to this was to create skills and status conditions that would nullify a character's immunity, something helped by another script again:

def elem_override(element_id)
  # Element immunity override
  for state in self.states
    return nil if state.note_elemoverride.nil?
    if state.note_elemoverride[0] == element_id
      value = state.note_elemoverride[1]
    end
  end
  return value
end

... there's actually more to this script, but it's buried in my generic "Formula Changes" script page. Moving on, this was already an oversight that I didn't realise: I was using "and" to require both conditions to be met to remove an unqualifying target. The script worked at the time because it was looking for a character with a particular element immunity, and didn't have the immunity negation status already.

I eventually reached the problem above, of wanting a beneficial, state removing spell. After "fixing" the problem of randomised ally targets...

# Check if there's a need to use the skill next time
result = check_usefulness(item.id)

# Get ally with least HP
result = friends_unit.alive_members.shuffle.sort { |a,b| a.hp_rate <=> b.hp_rate }
result = [result[0]]

... I decided to attack again what I had given up some time before, of also being able to consider targets with states, not without. This time, I somehow managed to get the hang of a particularly complicated regular expression and scanning of said expression to be able to specify a list of IDs and allow the script to look through them all. Previously the script could only consider one ID.

This would be handled by a separate process, and when it found something that matched (the target has the state matching the ID) it would return true... I actually wanted false because of the way I worded the definitions and objects, which I eventually worked out... . -.

I also fixed the above code... even though I had set out the process to get what targets the skill should be used on (i.e. everyone with heavy bleeding, indigestion, or pain), I was then overriding it with a fresh group of ally targets.

# Check if there's a need to use the skill next time
result = check_usefulness(item.id)

# Get ally with least HP
result = result.shuffle.sort { |a,b| a.hp_rate <=> b.hp_rate }
result = [result[0]]

With the addition of other skill conditions for the Smarter AI script, I could then ... supposedly... have a skill that would only ever be used on sick allies! But of course, I needed to test. I've been very code focused with my project at this time, hoping perhaps I will be able to forget about it later on. I am trying to focus on the way the game works, then the database entries (which I was working on until this cropped up), then exploration and... things like that...

... it sure might be a nice break from scripting.

I think I'm getting distracted again. I was testing. Devile now also required its target to not have Flame Resistance. I know it sounds silly, but it was the first thing I chose that wouldn't get in the way in other things. Now even though Devile was set up as such, the code wasn't behaving as expected. This was because I was misinterpreting the way I expected things to happen. Part of it was the phrasing of my objects, mixing up true and false, and the other part was because of "and".

For a while I wondered why it felt like (true && false) == true. Which isn't true. Sometimes it felt like it was saying it was true even when I "printed" it. I was confusing my booleans because of my phrasing, I wasn't realising I was asking "!false && !false", I didn't need "and"... I was being thrown off by the fact I wasn't asking... "Does this target meet these conditions? Do they have these states, not these, and these immunities?" No. I was asking if they didn't. The process was to remove targets until I was left with the qualifiers.

Devile currently was looking for a target with Indigestion, and without Flame Resist. "Disturbant Past" had Indigestion, but it also had Flame Resist. I expected the system to get rid of them. But it wasn't. It wasn't, because I forgot I was looking for those that didn't meet the conditions, not those that did.

It was about here I changed my "and"s to "or"s. (technically && to ||) If any one of the conditions were met with a true - if the target didn't have a state, did have a state (note the order), or elemental defence wasn't above 0; the system would boot the target out. true && false == false, don't use the skill on that guy. When anyone getting a false is removed, you're left with with true && true == true.

Along the way I made sure that nil values worked. For example, not requiring a state to be absent or an element immunity, as Devile does by default.

# Return if no values
return false if !values

This is false because the target should not be deleted from the array if the value happens to be nil. If that was true, anything that asks for any of the Smarter AI conditions would notice the other two are nil, and disallow the skill. Additionally without the help of the initial multiple conditions script talked about at the start of this article, the skill would activate complete with casting effects, then cancel itself. ... (Mental note: I will need to make sure this cannot happen by remembering usage conditions for skills that use the Smarter AI system.)

...

Now I am sure I made a complete arse of this whole thing. I don't post regularly about my game progress anywhere because I don't know what qualifies enough for a post, especially when it's scripting that cannot be easily posted in a visual form. What this means is when I make a big report like this, it's almost certain I've made a mistake somewhere by misremembering something, or gone on a tangent, repeated things, or just not made any sense. It's very wordy and probably only makes sense if you're familiar with RMVXAce, I'm sorry.

I really don't want to have to deal with this sort of code again, yikes. This seems like such a simple ask: having enemies make smart moves about their targets. Clearly, when those targets are chosen after skill activation and by random, it's quite a challenge to get the system to do something else without making it obvious you made it make a mistake. Or it yells at you about syntax.

PostedSunday, 15 September 02013 Tagsgames, witness to unity.