• Our software update is now concluded. You will need to reset your password to log in. In order to do this, you will have to click "Log in" in the top right corner and then "Forgot your password?".
  • Forum moderator applications are now open! Click here for details.
  • Welcome to PokéCommunity! Register now and join one of the best fan communities on the 'net to talk Pokémon and more! We are not affiliated with The Pokémon Company or Nintendo.

Opposing Trainer AI Question

39
Posts
17
Years
  • Seen Feb 28, 2024
Hey all,

I've been playing around with editing the trainer AI to take more situations into account, and it has been helping it - It's just simple stuff like if the holder has Choice Scarf, they are more likely to Trick, or if the user is an electric type, they are more likely to use Soak. Mostly just stuff copied from the other attacks function codes.

But is there a way to make it ALWAYS use the move I want on the first turn?

Basically, to summarize, I have a Pokemon with Normalize and Skill Swap, that is a ghost type Pokemon. I want the enemy trainer to ALWAYS use Skill Swap first turn if they have Normalize, so I figured just shooting the chance to use it up would basically guarantee it - but it doesn't seem to work that way, no matter how high I put the chance to use it.

Is there a way to see the Debug Logs for how the game does AI Calculations while you are playing? That would help a lot.

Anyway, the code for it is below - Maybe I didn't even do it right. But it definitely worked for some moves. Thanks if anyone knows.

when 0x67
if isConst?(opponent.ability(true),PBAbilities,:WONDERGUARD) ||
isConst?(attacker.ability(true),PBAbilities,:WONDERGUARD)
score-=80
end
if isConst?(attacker.ability(true),PBAbilities,:NORMALIZE)
score+=150
end
if isConst?(opponent.ability(true),PBAbilities,:NORMALIZE)
score-=150
end
if skill>=80
if opponent.ability==attacker.ability ||
(isConst?(opponent.ability(true),PBAbilities,:TRUANT) &&
attacker.pbIsOpposing?(opponent.index)) ||
(isConst?(opponent.ability(true),PBAbilities,:SLOWSTART) &&
attacker.pbIsOpposing?(opponent.index))
score-=90
end
end
score-=40 # don't prefer this move
 

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
Set $INTERNAL=true before the battle, and then do the battle. There should be a debug log created in the Data folder which shows the calculated scores for each opponent's move, along with various other information.

From your code, in your scenario Skill Swap would only gain 110 points. A move is only auto-chosen if the standard deviation of the moves' scores is 100 or greater (and even then, only 90% of the time); otherwise it's simply more likely that higher-scored moves will be chosen (but it's not a certainty). Skill Swap's score isn't high enough to qualify unless the other known moves have really low scores.

It's definitely true that the AI in Essentials needs fixing and improving (and it'd be great if you could help with that!). I never really sat down to figure out all the numbers - there are many calculations involved and many more factors that could/should be taken into account.

Perhaps the standard deviation threshold simply needs to be lowered. Maybe to 75? I don't know if that would be a suitable threshold in the majority of situations, though. Upping the score boost too would be a good idea (to something ridiculous just to make sure it's at least very highly likely to be chosen), so long as it doesn't also happen in situations you don't want it to.
 
39
Posts
17
Years
  • Seen Feb 28, 2024
Basically, here's what I think the AI Should work like.

Stat Boosting moves are very tough to balance. The CPU either never uses them, or spams them and dies before doing anything.

The following factors should be taken into account, if they are possible.

- How much of a threat is the opposing pokemon?
- If they have already increased their attack or are viciously strong and will knock the CPU's pokemon out in one shot, why would they ever waste time with Swords Dance or Barrier? Assuming the CPU is faster, They should at least try to inflict a status (probably the ideal) or get some damage in.

- How much have they already statted up?
- In most cases, unless their attacks really arne't hurting the opponent much, there is no need to go to +6. The AI seems to already have this considered, but it is important! Usually, one swords dance, or two Dragon Dances, are enough. Unless the opponent is completely harmless to you.

That's the biggest thing - When is it a good idea to stat up? When the opponent is no threat to you. If you are behind a Substitute. Things like that. If they can 2HK0 you and are faster hten you, boosting becomes a strange choice. There are of course situations where it can work - If the opponent does 80% of your life, you Swords Dance and your Salac activates, you may be able to OHKO them.

- How much HP do you have left?
- If your health is low, don't buff. Its as simple as that. If the opponent will KO you with their next move, DON'T use Dragon Dance!

- Turn "1" is a good time to stat up. If the playing fields are even, starting out with a Dragon Dance or Swords Dance or similar move is usually a good idea.

Reduce Randomness
- Some randomness is good to catch the opponent off guard with a surprise move - You don't want the CPU to be completely predictable. But why would you ever use, say, a 1/4 effectiveness move against an opponent? That's not a gamble, that's idiocy.

If you want to go really in-depth, think about what the foe could switch to. Let's go simple.

CPU Pokemon
- Honchkrow
- Drill Peck/Night Slash/Superpower

Opposing Pokemon
- Machamp

- Obviously, Drill Peck should be the move chosen most often. However, if the computer decides to go "Random" they should NOT pick Night Slash, but they COULD choose Superpower. Why?

Because if the player is switching, they are probably trying to avoid a bad matchup. Honchkrow's types are dark and flying - So they won't switch in anything weak to those moves. So Night Slash will almost always be a terrible choice. Superpower, though it won't do a LOT to Machamp, will still be neutrally effective.

I like that there's a chance of randomness to catch people off guard, but sometimes it does really dumb things. Maybe when the AI chooses a Random move, make them NOT use a move that will be hilariously ineffective to the opposing Pokemon, or that will fail completely? Not sure how it currently works.

Well, this is all theoretical - I'm not sure how possible any of this stuff is to do. Like I said, I've been working in little quirks in the function codes and that seems to have been helping. Things like increasing the chance to use Light Screen or Reflect if the user holds Light Clay, or increasing the chance to use MudSport if the opponent is an electric type, or increasing the chance to use Seismic Toss if the foe has less HP then the users level. Simple things like that. I'm not worried about Trainer Skill right now.

Another thing is when the AI loses a Pokemon and decides what Pokemon to switch in - This seems generally good, but I think it should take into consideration if you have a pokemon massively statted up - Say a Garchomp with 4 Dragon Dances. When deciding what pokemon to send out, it should try to pick ones that can actually take an action!

The best two choices are of course

Say, Pokemon that are faster than that Garchomp (somehow)
A pokemon that can actually survive a hit from that Garchomp
It should look at things like the Abilities of its own Pokemon. If a Pokemon has Sturdy or a Focus Sash. Things like that.

If all of this is impossible, rather then just sending out Pokemon in a random order, it should send out first hte ones that COULD beat that Garchomp.

For example, a Pokemon with Brightpowder. It COULD get lucky.
Or, and this might be complex to code... If the Garchomps attacking moves are Earthquake and Dragon Rush, send out a Pokemon that it HAS to use Dragon Rush to KO FIRST, because that move is not 100% Accurate - It might miss. This might not be easy to code in, of course.


That's all I can think of right now. Obviously this is really tough to balance, outside of simple stuff like, if Light Screen is currently up, ABSOLUTELY NEVER use Light Screen again - The AI has done that before, too. Though I know how to use RPG Maker (Spent a lot of time on RPG Maker 2000) I'm pretty useless with scripting. I've been trying to figure it out and am learning a LITTLE, but that's just it - a little.

like, I couldn't even figure out how to turn that $INTERNAL on. I tried just making a script comment with it and putting it in an event, but I got an error. I see where it's supposed to write all the data for it in the AI script, though. Still, if you need more ideas I have plenty. I could post ideas to add to a lot of the function codes (I've done it for quite a few in my game, though it's still a ways off being done progress is being made) that would make the AI "Appear" smarter.


As a side note, I guess this should go in the bug report topic, but I'll put it here - You changed the Stealth Rock code in Essentials 12, and now it fails completely. It does no damage, and it actually ends the turn as well when a Pokemon is switched in. Switching it back to the code from the old version works, but no clue if that will cause any other bugs. Spikes and such may also be effected - not confirmed.
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
Like I said, there's obviously a lot going on with AI, and a lot that could go on. Part of my looking around revealed that any score changes due to effects are likely to be completely overshadowed by the score change due to potential damage dealt (a Geodude's Head Smash against my Pikachu got a score of 384, for example, which seems way too extreme a change), and status moves simply get -10 to their score by default so are almost bound to be overshadowed by any damaging move.

I'm not working on Essentials at the moment (I've got something better to play with), but I would welcome any code tweaks you want to offer. An overhaul of the AI system would be a good idea at some point. The first part would be to figure out what ranges of numbers should be treated as "very preferred" or "not preferred", and how much of an effect potential damage (damage calculation including type effectiveness, etc.) should have on the score.

Putting $INTERNAL=true in an event's script command (and then interacting with the event) will work. I've discovered that the log isn't written to a file unless it's long enough, which explains why you may not have seen anything. To make the log print out immediately, go into the script section PBDebug, find the following method and quote out the two lines shown:

Code:
  def PBDebug.log(msg)
    if $DEBUG && $INTERNAL
      @@log.push("#{msg}\r\n")
[COLOR=Green]#      if @@log.length>1024[/COLOR]
        PBDebug.flush
[COLOR=Green]#      end[/COLOR]
    end
  end
The Stealth Rock bug you mentioned is true, and also applies to Rough Skin and a few other effects that inflict damage. I've fixed it now. Thanks for letting me know about it.
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
One thing I want to ask as I want to start improving the AI myself is does it do hard damage calculations? It looks like it doesn't.

The "!" in "!opponent.pbCanSleep?(false)" changes a "TRUE" return to "FALSE" correct?


Code:
if move.basedamage>0 && pbTypeModifier(move.type,attacker,opponent) > 1
           score += 200
else pbTypeModifier(move.type,attacker,opponent) = 1
           score += 50
else pbTypeModifier(move.type,attacker,opponent) > 0 && pbTypeModifier(move.type,attacker,opponent) < 1
           score -= 50
end
Would add 200 to the score if the move is super effective yes?
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
No, it doesn't perform full damage calculations. It ignores various effects like held items and abilities that could affect damage (but does consider stat stages and type effectiveness), and comes up with a rough idea of the damage from that. This is then modified by various considerations, such as halving it if it's a two turn move and a couple of other effects which are part of regular damage calculation. Of course, how in-depth it goes with these effects can be changed.

! means "not". So yes, what you said means "if the opponent can't be inflicted with sleep".
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
Ohhhh, it's just a shortcut instead of typing. I thought it was reversing the return itself.

For

Code:
when 0x19
       party=pbParty(opponent.index)
       statuses=0
       for i in 0...party.length
          statuses+=1 if party[i] && party[i].status!=0
       end
        score = 0 if statuses==0
       ##check

shouldn't "opponent" be "attacker", in order to check the attacker's(the users of the move yes?) party?
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
Yes, I suppose it should be attacker.


Code:
if move.basedamage>0 && pbTypeModifier(move.type,attacker,opponent) > 1
           score += 200
else pbTypeModifier(move.type,attacker,opponent) = 1
           score += 50
else pbTypeModifier(move.type,attacker,opponent) > 0 && pbTypeModifier(move.type,attacker,opponent) < 1
           score -= 50
end
Would add 200 to the score if the move is super effective yes?
pbTypeModifier returns a value of 4 for normal effectiveness. It's 8 for super-effective, and 16 for super-super effective (i.e. against both types). Similarly, 2 is not very effective, and 1 is really not very effective. 0 is immune.

It'd also be a good idea to put the if move.basedamage>0 in an outer if statement, since all three calculations should only apply to damaging moves. Something like this:
Code:
if move.basedamage>0
  eff=pbTypeModifier(move.type,attacker,opponent)
  if eff > 4   # Super-effective
    score += 200
  elsif eff == 4   # Normal effectiveness
    score += 50
  elsif eff > 0   # Not very effective
    score -= 50
  elsif eff == 0   # Immune
    score -= 80
  end
end
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
I wasn't trying to be funny, just making sure I understand how things work. Thanks for improving that, it keep throwing a syntax error for the normal effectiveness check in my (bad) code.

Code:
obsts = Array.new
obsts = opponent.pokemon.baseStats()

should set that to be an array of the opponent's BSTs correct? I'm using this for a Toxic scoring, where Toxic should get a large boost if the HP BST is high.
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
opponent.hp or opponent.totalhp should be fine for that. You might also want to check opponent.defense and opponent.spdef to figure out if they're going to be difficult to hurt, with Toxic being more favourable if those values are higher.
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
You may be right. I was worried about it spamming Toxic on full HP, low total HP but high level pokemon. A current HP % check combined with a BST check may be the thing to do for Toxic. Along with a high def/sdef check.

Code:
			when 0x06
			obsts = Array.new
			obsts = opponent.pokemon.baseStats()
			
			if move.basedamage==0				
				if !opponent.pbCanPoison?(false) 
					score = 0
				elsif opponent.status!=0
					score = 0
				elsif obsts[3] >= 90
					score += 130
				elsif isConst?(opponent.ability,PBAbilities,:GUTS)
					score -= 100
				elsif isConst?(opponent.ability,PBAbilities,:FLAREBOOST)
					score -= 100
				elsif isConst?(opponent.ability,PBAbilities,:TOXICBOOST)  
					score -= 100
				elsif opponent.effects[PBEffects::Yawn]>0
					score-=100
				else
					score += 25
				end		
				
			end
			
			if move.basedamage>0
				eff=pbTypeModifier(move.type,attacker,opponent)
				if eff > 4   # Super-effective
					score += 50
				elsif eff == 4   # Normal effectiveness
					score += 0
				elsif eff > 0   # Not very effective
					score -= 25
				elsif eff == 0   # Immune
					score == 0
				end
			end

That's what my current Toxic effect scoring looks like, with score = 50 as the base. When I look at the debug log, a Magikarp values Tackle at 62, Toxic at 65 and Twave at 65 on a target that don't fit the BST scoring increases.

I'm basically making my way down the list and redoing the scoring to take into account status/abilities(stuff like steadfast and motor drive)/attack effectiveness now
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
The elsif opponent.status!=0 part is a check included in the pbCanPoison method, so you don't need to include it here too.

You also shouldn't be checking the base stats of the Pokémon's species. You want to care about their actual stats (which are much easier to get to and more relevant). As I said, opponent.defense and opponent.spdef.

You don't need to care about Flare Boost, because you're inflicting poison, not a burn.

The type modifier stuff is a separate calculation below, so you don't need it here. This part of the code is solely about the effect and whether it'd be a good or a bad thing to try to to (i.e. inflict toxic).

With that all said, here's my take on the AI:

Code:
when 0x06
  if opponent.pbCanPoison?(false)
    score += 40
    score += 40 if opponent.defense > 90
    score += 40 if opponent.spdef > 90
    score -= 50 if isConst?(opponent.ability,PBAbilities,:GUTS)
    score -= 50 if isConst?(opponent.ability,PBAbilities,:TOXICBOOST) 
    score -= 30 if opponent.effects[PBEffects::Yawn]>0
  else
    score = 0 if move.basedamage==0
  end
The first part to look at is whether the opponent can be poisoned. If they can't, the score is set to 0 if the move does no damage. There's no such move, but if a damaging toxic move existed, it would be a move with no effect if it couldn't do its toxic part, and would need a score of 100 like the rest of the effectless moves.

If it can poison the target, then that's a good thing and you get an automatic score boost. You get an even higher score boost if the opponent is harder to hit (looking at each stat separately, where Bulbapedia says 83 is the average defensive stat value between all fully-evolved species). Conversely, you have a score reduction if the opponent will benefit from being poisoned or you know is going to promptly lose that status.

As far as numbers, they're smaller than what you stated, but they can be scaled to suit all the other numbers and that's not the thing to focus on. What's more important is the size of each boost/reduction compared to each other. I figured that, since toxic is slow to start up, the move's automatic bonus should be outweighed by the benefit the opponent gets from the status. Yawn is a slightly lower drop because (as far as I remember) it'll do at least a bit of damage before the opponent falls asleep.

I didn't include a number of things which would be nice, such as:
  • Boost the score if the target can't switch out (either because of some effect or because there's no other Pokémon to switch to) - the opponent would be stuck with the toxic in that case and couldn't get rid of it by switching out.
  • A check of what the opponent could do to heal itself of toxic (moves/held item/trainer's items) and lower the score if they have a counter.
  • An analysis of which damaging physical/special moves the user has, which would affect the score boosts for each of the two defensive stats (either a bigger boost if the user's attacks will tend to go against a high defensive stat and/or a lower stat value threshold for applying this boost). Said analysis should also include looking at the opponent's stat modifiers.
  • Check the state of the battle, and prefer to inflict toxic if the opponent is well off and needs grinding down - the existing AI does something like this by providing a boost during the first few turns, but that doesn't seem like the best way to go about it.
  • The score changes should depend on the trainer's skill level, e.g. only highly skilled trainers would consider the Guts/Toxic Boost part or some of the other points in this list.
  • Any other factors revolving around poisoning and statuses that I can't think of.
I'm no expert in AI. Those are just my thoughts.
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
That's a much better way. I need to stop reinventing the wheel and read the rest of the class before I do any more. The .sp/defense is just their flat stats and still has my problem of not applying early on. A percentage check(x% higher then your attacking stat) would be better for choosing when to use stat lowing moves. But I'm going to read everything before I continue on.

I see Toxic as being useful against either high HP(that still has it) and high defenses. Otherwise a Burn, Freeze(-SpA instead of incapacitate in my hack), or Paralyze would be more useful for dealing with said pokemon.

But I see evaluating attacking moves with a chance of status effect differently. Unless you're Graced, the effect isn't really why you're using said move so there shouldn't be a bonus. I'll give the AI Toxic/Poison Gas if I care about the status. Having the AI favor moves with an effect chance could mess up TWave/Spore usage as an example. What I'm trying to say is you don't want to favor a neutral move with an effect chance over a same BP neutral move with no effect chance.


Thanks for putting up with me.
 
Last edited:

Maruno

Lead Dev of Pokémon Essentials
5,285
Posts
16
Years
What I'm trying to say is you don't want to favor a neutral move with an effect chance over a same BP neutral move with no effect chance.
I entirely disagree. All other things being equal (type/category/power), you'd want to go with the move that also has a chance of doing another bad thing to your opponent. The whole point of the function code part of the AI is to decide whether these extra effects are worth doing, and to make those moves more/less likely to be chosen.
 

Wootius

Glah
300
Posts
11
Years
  • Seen May 31, 2022
I'm not disagreeing with the +40s on S/Def > 90 checks, as then Poison is good.

I'm disagreeing with the flat +40. I feel if the target doesn't fit "poisoning" criteria then it shouldn't get a bonus.

What if the AI is fighting a speedy pokemon and because the AI weighed the neutral Poison effect move higher then a same range no effect move it was poisoned when the AI's next pokemon had the ability to Paralyze it?

I know the ideal answer to the above is to either switch in to paralyze it or to switch in something that has resisted its so far revealed moves/typing and I will be looking at that.
 
29
Posts
5
Years
  • Age 31
  • Seen Feb 27, 2023
Hello! I think it wouldn't be a problem if I asked it in this thread.

Is there a way to make sure the AI ALWAYS choose the move with the highest score? Or at least super force it to do it (if the score gamble can't be avoided at all)?
I feel bad when I get spared by a Slash instead of being annihilated by a Flamethrower :(

Thanks!
 

StCooler

Mayst thou thy peace discover.
9,286
Posts
3
Years
  • Age 28
  • Seen yesterday
You should have made a new topic.
But to answer your question, you will need to check the function:
Code:
  def pbChooseMoves(idxBattler)
and find the code:
Code:
    # Find any preferred moves and just choose from them
    if !wildBattler && skill>=PBTrainerAI.highSkill && maxScore>100
      stDev = pbStdDev(choices)
      if stDev>=40 && pbAIRandom(100)<90
        preferredMoves = []
        choices.each do |c|
          next if c[1]<200 && c[1]<maxScore*0.8
          preferredMoves.push(c)
          preferredMoves.push(c) if c[1]==maxScore   # Doubly prefer the best move
        end
        if preferredMoves.length>0
          m = preferredMoves[pbAIRandom(preferredMoves.length)]
          PBDebug.log("[AI] #{user.pbThis} (#{user.index}) prefers #{user.moves[m[0]].name}")
          @battle.pbRegisterMove(idxBattler,m[0],false)
          @battle.pbRegisterTarget(idxBattler,m[2]) if m[2]>=0
          return
        end
      end
    end
Comment it, and add this below:
Code:
    if !wildBattler && skill>=PBTrainerAI.highSkill && maxScore>100
      choices.each do |c|
        preferredMoves.push(c) if c[1]==maxScore   # Only prefer the highest score move.
      end
      if preferredMoves.length>0
        m = preferredMoves[pbAIRandom(preferredMoves.length)]
        PBDebug.log("[AI] #{user.pbThis} (#{user.index}) prefers #{user.moves[m[0]].name}")
        @battle.pbRegisterMove(idxBattler,m[0],false)
        @battle.pbRegisterTarget(idxBattler,m[2]) if m[2]>=0
        return
      end
    end

But be aware that this code makes sure only the high skilled strainers use only the best move.
 
29
Posts
5
Years
  • Age 31
  • Seen Feb 27, 2023
Thanks a lot! I'm sorry about not making a new post, this was the only one I encountered that talked about the AI (Essentials only). But it's old, so it makes sense.
Should I create a new one and give you credit for the answer? It's the best solution I can think of...

Edit: preferredMoves = [] #has to be declared too or it will give an "undefined local variable or method" error
Other than that, it's perfect, thank you
 
Last edited:

StCooler

Mayst thou thy peace discover.
9,286
Posts
3
Years
  • Age 28
  • Seen yesterday
Thanks a lot! I'm sorry about not making a new post, this was the only one I encountered that talked about the AI (Essentials only). But it's old, so it makes sense.
Should I create a new one and give you credit for the answer? It's the best solution I can think of...

No need to make a new post, and I don't care about credits lol ^^
 
Back
Top