Improve the enemy trainer AI - pret/pokecrystal GitHub Wiki

The AI, or the behavior of computer-controlled trainers, is much better in Gen II than it was in Gen I, but it's not flawless. There are many ways to improve it, but this tutorial covers a few relatively simple changes to get the ball rolling.

Contents

  1. Gen II AI For Dummies
  2. Fix bugs
  3. Fix nonsensical move lists
  4. Improve AI item use
  5. Strategically encourage stat-up moves
  6. Dismiss status moves if the player has a Substitute
  7. Only sometimes favor reckless moves
  8. Encourage moves based on weather
  9. Replace Sleep check with Leech Seed check in AI_Status
  10. Delve into Smart AI routines

1. Gen II AI For Dummies

Without getting too far into the weeds here, the gist of how the AI works is that it assigns a score to each of the four moves that its current Pokémon knows, and picks the move with the lowest score. (Tied scores? Pick one of the tied moves at random.) All four moves start with the same score, and the scores are then modified by the AI. How exactly is this achieved?

layers.jpg

...

; Falkner
	db NO_ITEM, NO_ITEM ; items
	db 25 ; base reward
	dw AI_BASIC | AI_SETUP | AI_SMART | AI_AGGRESSIVE | AI_CAUTIOUS | AI_STATUS | AI_RISKY
	dw CONTEXT_USE | SWITCH_SOMETIMES

...

; Firebreather
	db NO_ITEM, NO_ITEM ; items
	db 12 ; base reward
	dw AI_BASIC | AI_SETUP | AI_OFFENSIVE | AI_OPPORTUNIST | AI_STATUS
	dw CONTEXT_USE | SWITCH_SOMETIMES

...

; Twins
	db NO_ITEM, NO_ITEM ; items
	db 5 ; base reward
	dw NO_AI
	dw CONTEXT_USE | SWITCH_OFTEN

...

Those things on the third line of each entry, like AI_BASIC, AI_SETUP, and so on, are what we call different "AI layers." Basically, each layer is a set of code that gets run, potentially modifying the scores of each of the four moves, before moving on to running the next layer. Each trainer class is given a certain set of layers, from the simplest like the Twins who just have NO_AI (which as you can guess, does nothing), to boss trainers like Falkner, who have quite a few layers. That's all you really need to know about it for the purposes of this tutorial, but there's more to be learned if you seek it out.

2. Fix bugs

In some cases, people prefer to leave bugs and glitches in place, to be more authentic to the original game. But if you're looking at this tutorial, you'd probably want to fix them.

At the time of writing, pokecrystal's docs/bugs_and_glitches file lists at least eight bugs related to the AI, conveniently all one right after another in that long list. Might as well start by doing those fixes.

3. Fix nonsensical move lists

The folder data/battle/ai contains several lists of moves which get checked for under certain circumstances. Many of the moves on these lists don't make much sense being where they are, and moves that you might expect to see are missing. This has already been touched on in the bug fixes with the Rain Dance and Sunny Day move lists. Let's take a look at the rest of them.

At data/battle/ai/risky_effects.asm, we see this:

; AI_RISKY will not use these effects at max HP
; even if they would KO the player.

RiskyEffects:
	db EFFECT_SELFDESTRUCT
	db EFFECT_OHKO
	db -1 ; end

Competitive players dislike how luck-based OHKO (one-hit knock out) moves are, but assuming that they exist on AI movesets, they might as well be used logically. That being said, I've wracked my brain on why Game Freak told the AI not to use them when its own Pokémon's HP is full, and I can't come up with anything. Thus, we can simply remove them from this list.

RiskyEffects:
	db EFFECT_SELFDESTRUCT
-	db EFFECT_OHKO
	db -1 ; end

This leaves only one entry in the list, which makes the list an inefficient way to check for a single move effect, but there's a chance that you may want to add other effects in here if you're adding new moves to the game, so we'll leave this structure in place for now.


Similarly, data/battle/ai/reckless_moves.asm:

; AI_AGGRESSIVE does not discourage these moves
; even if a stronger one is available.

RecklessMoves:
	db EFFECT_SELFDESTRUCT
	db EFFECT_RAMPAGE
	db EFFECT_MULTI_HIT
	db EFFECT_DOUBLE_HIT
	db -1 ; end

We'll see later that the AI_AGGRESSIVE layer tries to determine which of the AI's four moves will do the most damage. Since it uses the move's base power for this, moves with an effect like EFFECT_MULTI_HIT (i.e. things like Doubleslap) can't be accounted for correctly, since they strike multiple times, making their overall power a multiple of their official base power. Therefore it seems like the idea behind this list was to create an exception to AI_AGGRESSIVE's damage calculation for such moves, allowing the AI to gamble on getting a high number of hits with such a move instead of using a different move that has a higher base power but might deal less damage overall. Unfortunately, Game Freak for some reason put Selfdestruct and Rampage on here, which don't work the same way. (Rampage, the effect used by moves like Thrash and Petal Dance, hits multiple times over multiple turns, but not in the same turn.) A corrected version of this list might look like this:

RecklessMoves:
-	db EFFECT_SELFDESTRUCT
-	db EFFECT_RAMPAGE
	db EFFECT_MULTI_HIT
	db EFFECT_DOUBLE_HIT
+	db EFFECT_POISON_MULTI_HIT
+	db EFFECT_TRIPLE_KICK
	db -1 ; end

I've left off Beat Up, since it doesn't always get multiple hits, and is probably better handled with a specific AI_SMART routine. We'll look at those later.


We can also look at "residual" moves. These are moves that get discouraged after a specific AI Pokémon's first turn in battle. The idea here seems to be to list effects that would fail if used twice, even though there's a whole other system for discouraging redundant moves in engine/battle/ai/redundant.asm. Regardless, nothing on this list stands out as wrong per se, but we might want to add a few things.

data/battle/ai/residual_moves.asm:

ResidualMoves:
	db MIST
	db LEECH_SEED
	db POISONPOWDER
	db STUN_SPORE
	db THUNDER_WAVE
+	db TOXIC
	db FOCUS_ENERGY
	db BIDE
+	db GLARE
	db POISON_GAS
	db TRANSFORM
	db CONVERSION
	db SUBSTITUTE
+	db SPIDER_WEB
+	db BELLY_DRUM
	db SPIKES
+	db MEAN_LOOK
+	db ATTRACT
	db -1 ; end

This is where things start getting a little subjective. Leech Seed is presumably on there because it will fail if used twice against the same Pokémon, but this list is discouraged after the AI Pokémon's first turn out, even after it scores KOs. What about Bide? It's probably on here because using it at low HP is a bad idea, but what if the AI hasn't lost any HP yet? You're going to have to make your own decisions instead of just blindly copying this tutorial.


Let's see what else we've got... Ah, Encore moves. As noted, the AI tries to use Encore after the player uses these moves. So naturally, it would want to lock the player into using useless moves, like, erm... Flame Wheel and Aeroblast. Okay, to be fair, most of the other ones make sense. But you can decide whether you want stuff like Swords Dance on there—is it wise to lock the player into buffing up that much? Or the other way around—if the player gets locked into Leer and the AI decides to putz around, might it not be at risk of an easy KO after the Encore ends? Super Fang is here because it gets diminishing returns, but it is still doing damage—and so on, and so on.


"Stall" moves are only checked by the AI_OPPORTUNIST layer, and are discouraged if the AI Pokémon's HP is low. So, you might expect this to include things like Leer, which aren't a great option when your HP is low, or maybe things like Solarbeam that require multiple turns. But you could do different things with this feature, such as making the trainer try to go for more consistent moves to be safe, or go for riskier moves than usual to appear panicky. Of course, there's only one AI_OPPORTUNIST layer (unless you make another one!), so whatever you choose will apply to every trainer class that has that layer.


"Useful" moves, if used by the player, will make the AI try to copy them with Mimic or Mirror Move, or disable them with, you guessed it, Disable. Maybe try to think of moves here that are "useful" in the widest context—for example, Earthquake is a strong move, but does nothing against Flying types. Conversely, Confuse Ray may not end up making the player hurt themselves, but no Pokémon is immune to it. Hydro Pump is stronger than Surf, but may miss. Decide what you consider "useful," or "copyable," or "disable-able."

4. Improve AI item use

An enemy trainer's usable items are determined by their trainer class. However, the logic for when the trainer should use the item is the same regardless of class or what the item is (with exceptions for only using healing items when necessary). Part of that logic tells them to only use items on their highest-leveled Pokémon. You may have never noticed this or seen it as a problem, since most prominent trainers tend to send out their highest-leveled Pokémon last. However, Red sends out his Pikachu first, and as a result, if the Pikachu is quickly knocked out, Red will never use his two Full Restores on any of his other Pokémon, giving him an unnecessary disadvantage.

There are a number of different permutations of a solution here, the simplest being to use items on any old party member. But since the highest-leveled Pokémon is usually the strongest and/or most important, it does make sense to prioritize that one. The following solution makes it so that this Pokémon will be favored for item use, but if the AI is down to its last Pokémon, then it will use items regardless of levels.

Edit AI_TryItem in engine/battle/ai/items.asm:

...

AI_TryItem:
	; items are not allowed in the Battle Tower
	ld a, [wInBattleTowerBattle]
	and a
	ret nz

	ld a, [wEnemyTrainerItem1]
	ld b, a
	ld a, [wEnemyTrainerItem2]
	or b
	ret z

+	ld a, [wOTPartyCount]
+	cp 2
+	jr c, .only_one_mon
+
+	ld d, a
+	ld e, 0
+	ld b, 1 << (PARTY_LENGTH - 1)
+	ld c, 0
+	ld hl, wOTPartyMon1HP
+
+.loop_alive
+	ld a, [wCurOTMon]
+	cp e
+	jr z, .next_alive
+
+	push bc
+	ld b, [hl]
+	inc hl
+	ld a, [hld]
+	or b
+	pop bc
+	jr z, .next_alive
+
+	ld a, c
+	or b
+	ld c, a
+
+.next_alive
+	srl b
+	push bc
+	ld bc, PARTYMON_STRUCT_LENGTH
+	add hl, bc
+	pop bc
+	inc e
+	dec d
+	jr nz, .loop_alive
+
+	ld a, c
+	and a
+	jr z, .only_one_mon
+
	call .IsHighestLevel
	ret nc

+.only_one_mon
	ld a, [wTrainerClass]
	dec a

...

This code first checks if the AI has only one Pokémon in its party, then checks if more than one of its Pokémon is still alive, and finally, if it does have more than one still alive, applies its normal logic to only use items on the highest-leveled Pokémon. Otherwise, it will skip the level check and just use the items.

There's a second improvement we can make in this same function! Perish Song is a move that makes both the player and enemy Pokémon faint after 3 turns unless they're switched out. If a Pokémon is counting down to being killed by Perish Song, it wouldn't make sense to use healing or buffing items on it, since those effects will be nullified when it, well, dies. So we can install an exception to tell the AI not to use items on a Pokémon that has a Perish count.

...

.IsHighestLevel:
+	ld a, [wEnemySubStatus1]
+	bit SUBSTATUS_PERISH, a
+	jr nz, .no
+
	ld a, [wOTPartyCount]
	ld d, a
	ld e, 0
	ld hl, wOTPartyMon1Level
	ld bc, PARTYMON_STRUCT_LENGTH
...

-.no ; unreferenced
+.no
	and a
	ret

...

Here we've made use of a previously-unused label, .no, to achieve our goals. Since we're now using it, we can remove the "unreferenced" comment.

By putting the check inside of .IsHighestLevel, the AI may still use items on a Perish-Songed Pokémon if that Pokémon is its last one alive. At that point, it can no longer switch out to cure the Perish count anyway, so an item heal might just stall long enough to let the AI win, as unlikely as that is.

5. Strategically encourage stat-up moves

Many great philosophers of old have posed the question, "If the opposing Pokémon is currently in the air with Fly or underground with Dig, and the Pokémon that you have on the field has a higher Speed stat than the opponent, how do you avoid wasting your turn?" Well, if you have a stat-boosting move like Swords Dance, you could always use that!

Edit AI_Setup in engine/battle/ai/scoring.asm:

...

AI_Setup:
; Use stat-modifying moves on turn 1.

; 50% chance to greatly encourage stat-up moves during the first turn of enemy's Pokemon.
; 50% chance to greatly encourage stat-down moves during the first turn of player's Pokemon.
+; 100% chance to greatly encourage stat-up moves if the player is flying or underground, and the enemy is faster.
; Almost 90% chance to greatly discourage stat-modifying moves otherwise.

...

.statup
+	ld a, [wPlayerSubStatus3]
+	and 1 << SUBSTATUS_FLYING | 1 << SUBSTATUS_UNDERGROUND
+	jr z, .statup_continue
+
+	call AICompareSpeed
+	jr c, .do_encourage
+
+.statup_continue
	ld a, [wEnemyTurnsTaken]
	and a
	jr nz, .discourage

	jr .encourage

.statdown
	ld a, [wPlayerTurnsTaken]
	and a
	jr nz, .discourage

.encourage
	call AI_50_50
	jr c, .checkmove

+.do_encourage
	dec [hl]
	dec [hl]
	jr .checkmove

.discourage
	call Random
	cp 12 percent
	jr c, .checkmove
	inc [hl]
	inc [hl]
	jr .checkmove

...

This trick will only apply to trainer classes who have the AI_SETUP layer. Since that layer was already about when to use stat-up and stat-down moves, it seemed like a fitting place to put this.

6. Dismiss status moves if the player has a Substitute

Unlike in Gen I, where almost any Pokémon can learn Substitute via TM, it's an extremely rare move in Gen II. Nevertheless, we want the AI to know what to do if the player uses it. Currently, the AI does not understand that status moves won't work through a Substitute. We can make a very simple edit to fix this.

Edit AI_Basic in engine/battle/ai/scoring.asm:

...

AI_Basic:
...

+; Dismiss status moves if the player has a Substitute.
+	ld a, [wPlayerSubStatus4]
+	bit SUBSTATUS_SUBSTITUTE, a
+	jr nz, .discourage
+
; Dismiss status moves if the player is Safeguarded.
	ld a, [wPlayerScreens]
	bit SCREENS_SAFEGUARD, a
	jr z, .checkmove

.discourage
	call AIDiscourageMove
	jr .checkmove

...

We can see that the AI already knows not to use status moves if the player has Safeguard up. We've used similar logic to handle Substitute.

That covers moves in the StatusOnlyEffects list, which inflict status problems, but what about stat-lowering moves like Growl and Leer? Those don't work through a Substitute either. Come to think of it, there's an entire move designed to block them, Mist, that the AI doesn't seem to account for either. Let's make a couple more small edits, this time in AI_Setup again:

...

AI_Setup:
; Use stat-modifying moves on turn 1.

; 50% chance to greatly encourage stat-up moves during the first turn of enemy's Pokemon.
; 50% chance to greatly encourage stat-down moves during the first turn of player's Pokemon.
; 100% chance to greatly encourage stat-up moves if the player is flying or underground, and the enemy is faster.
+; 100% chance to greatly discourage stat-down moves if the player has Mist or a Substitute up.
; Almost 90% chance to greatly discourage stat-modifying moves otherwise.

...

.statdown
+	ld a, [wPlayerSubStatus4]
+	bit SUBSTATUS_MIST, a
+	jr nz, .do_discourage
+
+	ld a, [wPlayerSubStatus4]
+	bit SUBSTATUS_SUBSTITUTE, a
+	jr nz, .do_discourage
+
	ld a, [wPlayerTurnsTaken]
	and a
	jr nz, .discourage

.encourage
	call AI_50_50
	jr c, .checkmove

.do_encourage
	dec [hl]
	dec [hl]
	jr .checkmove

.discourage
	call Random
	cp 12 percent
	jr c, .checkmove
+
+.do_discourage
	inc [hl]
	inc [hl]
	jr .checkmove

...

There's also an item called Guard Spec. that has the same effect as Mist. Luckily, in the code it just sets the same exact bit that Mist does, so we don't need to make a separate check for it.

7. Only sometimes favor reckless moves

Remember the "reckless" move effects? Doubleslap and all that? Well, we may have corrected that list to no longer include Selfdestruct or Rampage moves, but let's face it, most multi-hit moves just suck. Gambling for 5 hits isn't worth it when the move has 15 base power and might just miss. As it stands, AI_AGGRESSIVE effectively favors reckless moves by leaving them out of its damage check. We can put something in to make it only do that sometimes.

Edit AI_Aggressive in engine/battle/ai/scoring.asm:

...

AI_Aggressive:
; Use whatever does the most damage.

...

.gotstrongestmove
; Nothing we can do if no attacks did damage.
	ld a, c
	and a
-	jr z, .done
+	ret z

; Discourage moves that do less damage unless they're reckless too.
	ld hl, wEnemyAIMoveScores - 1
	ld de, wEnemyMonMoves
	ld b, 0
.checkmove2
	inc b
	ld a, b
	cp NUM_MOVES + 1
-	jr z, .done
+	ret z

; Ignore this move if it is the highest damaging one.
	cp c
	ld a, [de]
	inc de
	inc hl
	jr z, .checkmove2

	call AIGetEnemyMove

; Ignore this move if its power is 0 or 1.
; Moves such as Seismic Toss, Hidden Power,
; Counter and Fissure have a base power of 1.
	ld a, [wEnemyMoveStruct + MOVE_POWER]
	cp 2
	jr c, .checkmove2

-; Ignore this move if it is reckless.
+; 50% chance to ignore this move if it is reckless.
	push hl
	push de
	push bc
	ld a, [wEnemyMoveStruct + MOVE_EFFECT]
	ld hl, RecklessMoves
	ld de, 1
	call IsInArray
	pop bc
	pop de
	pop hl
-	jr c, .checkmove2
+	jr c, .maybe_discourage

; If we made it this far, discourage this move.
+.discourage
	inc [hl]
	jr .checkmove2

-.done
-	ret
+.maybe_discourage
+	call AI_50_50
+	jr c, .discourage
+	jr .checkmove2

...

This results in a 50% chance to favor reckless moves, and a 50% chance not to. Feel free to adjust the percentages to your liking. This code makes use of an existing AI_50_50 function present at the bottom of this file; there's also an AI_80_20 down there.

8. Encourage moves based on weather

Let's make something clear. AI_AGGRESSIVE is used by boss trainers like Gym Leaders, along with some other layers like AI_BASIC and AI_SMART. But some layers are not designed to work together, but rather instead of. When AI_AGGRESSIVE runs its calculation to compare the damage output of each of its moves, it is taking into account factors like STAB and type effectiveness; multi-hit moves were the only thing that really confused it. Now, there's another layer called AI_TYPES. This works somewhat similarly to how Gen I's "smart AI" did: it simply favors moves that are super effective and discourages moves that are not very effective, but fails to take into account factors such as base power. As such, it's inferior to AI_AGGRESSIVE—but not just that: a trainer who has AI_AGGRESSIVE does not need to also have AI_TYPES, because AI_AGGRESSIVE already took type matchups into account. So what is AI_TYPES even used for, then? Well, basic trainer classes like Schoolboys and Birdkeepers use AI_TYPES instead of AI_AGGRESSIVE, thus making them effectively dumber, but intentionally so. The idea is to give different trainer classes character by assigning them different combinations of AI layers. All the big boss trainers have a certain combination that Game Freak thought was the smartest and toughest, but for random fodder, it's perhaps more interesting if sometimes they don't completely know what they're doing.

All of that to say... AI_AGGRESSIVE already factors in weather boosts to damage, but AI_TYPES doesn't. Assuming you care about the random nobodies on Route 937, here's how to make AI_TYPES consider weather boosts as well as type effectiveness.

Edit AI_Types in engine/battle/ai/scoring.asm:

...

AI_Types:
; Dismiss any move that the player is immune to.
; Encourage super-effective moves.
; Discourage not very effective moves unless
; all damaging moves are of the same type.

	ld hl, wEnemyAIMoveScores - 1
	ld de, wEnemyMonMoves
	ld b, NUM_MOVES + 1
.checkmove
	dec b
-	ret z
+	jr z, .checkrain

	inc hl
	ld a, [de]
	and a
-	ret z
+	jr z, .checkrain

	inc de
	call AIGetEnemyMove

	...

.immune
	call AIDiscourageMove
	jr .checkmove
+
+; Encourage moves in the Rain Dance list if it's raining.
+.checkrain
+	ld a, [wBattleWeather]
+	cp WEATHER_RAIN
+	jr nz, .checksun
+
+	ld hl, wEnemyAIMoveScores - 1
+	ld de, wEnemyMonMoves
+	ld c, NUM_MOVES + 1
+.checkmove3
+	inc hl
+	dec c
+	jr z, .checksun
+
+	ld a, [de]
+	inc de
+	and a
+	jr z, .checksun
+
+	push hl
+	push de
+	push bc
+	ld hl, RainDanceMoves
+	ld de, 1
+	call IsInArray
+
+	pop bc
+	pop de
+	pop hl
+	jr nc, .checkmove3
+
+	dec [hl]
+	jr .checkmove3
+
+; Encourage moves in the Sunny Day list if it's sunny.
+.checksun
+	ld a, [wBattleWeather]
+	cp WEATHER_SUN
+	ret nz
+
+	ld hl, wEnemyAIMoveScores - 1
+	ld de, wEnemyMonMoves
+	ld c, NUM_MOVES + 1
+.checkmove4
+	inc hl
+	dec c
+	ret z
+
+	ld a, [de]
+	inc de
+	and a
+	ret z
+
+	push hl
+	push de
+	push bc
+	ld hl, SunnyDayMoves
+	ld de, 1
+	call IsInArray
+
+	pop bc
+	pop de
+	pop hl
+	jr nc, .checkmove4
+
+	dec [hl]
+	jr .checkmove4

...

This makes use of the same rain and sun move lists we saw in data/battle/ai. Using a list, especially a long one, for more than one purpose is a great way to save space, at least compared to writing out two identical or near-identical lists. Consider what else you could do with lists like the "useful" and "stall" moves. You could really change up AI behavior under different circumstances if you get creative like that!

9. Replace Sleep check with Leech Seed check in AI_Status

Pokémon has a bit of a problem with making up exceptions to its own rules. Have you ever noticed that Ground types are immune to Thunder Wave, but Ghost types aren't immune to Sing? Well, apparently the AI hasn't noticed, because AI_Status contains code to check Sleep-inflicting moves for type immunities. At the same time, it fails to recognize that Grass types are immune to Leech Seed. We can fix both of these issues at once by replacing the one with the other.

Edit AI_Status in

AI_Status:
	...

	ld a, [wEnemyMoveStruct + MOVE_EFFECT]
	cp EFFECT_TOXIC
	jr z, .poisonimmunity
	cp EFFECT_POISON
	jr z, .poisonimmunity
-	cp EFFECT_SLEEP
-	jr z, .typeimmunity
+	cp EFFECT_LEECH_SEED
+	jr z, .leechseedimmunity
	cp EFFECT_PARALYZE
	jr z, .typeimmunity

	ld a, [wEnemyMoveStruct + MOVE_POWER]
	and a
	jr z, .checkmove

	jr .typeimmunity

.poisonimmunity
	ld a, [wBattleMonType1]
	cp POISON
	jr z, .immune
	cp STEEL
	jr z, .immune
	ld a, [wBattleMonType2]
	cp POISON
	jr z, .immune
	cp STEEL
	jr z, .immune
+	jr .typeimmunity
+
+.leechseedimmunity
+	ld a, [wBattleMonType1]
+	cp GRASS
+	jr z, .immune
+	ld a, [wBattleMonType2]
+	cp GRASS
+	jr z, .immune

.typeimmunity
	...

It's important to understand that, because the AI's Pokémon will always be on the enemy side of the battlefield, it can always assume that the enemy Pokémon is the "user" of a move and the player's Pokémon is the "target." Functions outside the context of the AI can't make the same assumption, so they have to check whose turn it is to see who should be the "user" and the "target." The player's Pokémon's types are stored in wBattleMonType1 and wBattleMonType2, while the AI's are in wEnemyMonType1 and wEnemyMonType2. And there are plenty of other bytes that store the rest of their characteristics.

10. Delve into Smart AI routines

The AI_SMART layer is the most complex one because it's composed of a bunch of sub-layers. First, you'll notice this:

AI_Smart_EffectHandlers:
	dbw EFFECT_SLEEP,            AI_Smart_Sleep
	dbw EFFECT_LEECH_HIT,        AI_Smart_LeechHit
	dbw EFFECT_SELFDESTRUCT,     AI_Smart_Selfdestruct
	...
	dbw EFFECT_SOLARBEAM,        AI_Smart_Solarbeam
	dbw EFFECT_THUNDER,          AI_Smart_Thunder
	dbw EFFECT_FLY,              AI_Smart_Fly
	db -1 ; end

Each sub-layer, or subroutine, or whatever you want to call it, is designed to tell the AI how to intelligently use a specific move effect. A move effect may apply to multiple moves, like we've seen with Rampage and Multi-Hit, or just a single move. This table tells the AI which routine goes with which move effect. Let's look at one routine as an example.

AI_Smart_Sleep:
; Greatly encourage sleep inducing moves if the enemy has either Dream Eater or Nightmare.
; 50% chance to greatly encourage sleep inducing moves otherwise.

	ld b, EFFECT_DREAM_EATER
	call AIHasMoveEffect
	jr c, .encourage

	ld b, EFFECT_NIGHTMARE
	call AIHasMoveEffect
	ret nc

.encourage
	call AI_50_50
	ret c
	dec [hl]
	dec [hl]
	ret

The most important thing to understand about Smart AI routines is that they typically end with one or more instances of either dec [hl] or inc [hl]. Dec means decrease, and inc means increase. Remember that explanation at the beginning about each move getting a score, and the AI picking the lowest-scored move? That's what we're increasing or decreasing here, the score of the move in question.

According to the comments here, the AI's treatment of Sleep-inducing moves is to always encourage them if its Pokémon also knows Dream Eater or Nightmare, and otherwise encourage them 50% of the time. Since there are two instances of dec [hl], it's considered to be "greatly encouraging" instead of just encouraging. Subtracting 2 points from the move's score instead of just one.

But... does anything look off here? We check for Dream Eater, and then we jump to .encourage... but then after .encourage, we run AI_50_50. Sooo... doesn't that mean there's only a 50% chance to encourage the move even if we do know Dream Eater or Nightmare? As it turns out, yes. Game Freak screwed up here. So, we can move .encourage to fix that.

AI_Smart_Sleep:
; Greatly encourage sleep inducing moves if the enemy has either Dream Eater or Nightmare.
; 50% chance to greatly encourage sleep inducing moves otherwise.

	ld b, EFFECT_DREAM_EATER
	call AIHasMoveEffect
	jr c, .encourage

	ld b, EFFECT_NIGHTMARE
	call AIHasMoveEffect
	ret nc

-.encourage
	call AI_50_50
	ret c
+.encourage
	dec [hl]
	dec [hl]
	ret

Anyway, now that we've cleaned up Game Freak's mess, we can consider whether this is the effect we actually want from the function. It makes sense to favor Sleep if you know Dream Eater or Nightmare, and even if you don't, no Pokémon is immune to Sleep in Gen II, so it's a powerful effect, and thus worth ranking rather high, or rather low as it were when it comes to the move's score. But remember that this routine will apply to all Sleep moves, from highly inaccurate ones like Sing to the holy grail of Spore.

While we're talking about Sleep stuff, here's the routine for Dream Eater:

AI_Smart_DreamEater:
; 90% chance to greatly encourage this move.
; The AI_Basic layer will make sure that
; Dream Eater is only used against sleeping targets.
	call Random
	cp 10 percent
	ret c
	dec [hl]
	dec [hl]
	dec [hl]
	ret

Most Smart routines don't need to account for certain levels of redundancy due to AI_Baisc running AI_Redundant. Hence the note here that Dream Eater doesn't need special treatment to make the AI only use it on Sleeping targets. Or, rather, it does get 3x encouraged here, but this will only apply when the player is asleep thanks to AI_Redundant. So if you have to write any brand-new Smart routines for new moves, keep in mind what's already covered by AI_Redundant, and that you could add other stuff to it instead of putting it in the Smart routine.

Alright, how about something else.

AI_Smart_Selfdestruct:
; Selfdestruct, Explosion

; Unless this is the enemy's last Pokemon...
	push hl
	farcall FindAliveEnemyMons
	pop hl
	jr nc, .notlastmon

; ...greatly discourage this move unless this is the player's last Pokemon too.
	push hl
	call AICheckLastPlayerMon
	pop hl
	jr nz, .discourage

.notlastmon
; Greatly discourage this move if enemy's HP is above 50%.
	call AICheckEnemyHalfHP
	jr c, .discourage

; Do nothing if enemy's HP is below 25%.
	call AICheckEnemyQuarterHP
	ret nc

; If enemy's HP is between 25% and 50%,
; over 90% chance to greatly discourage this move.
	call Random
	cp 8 percent
	ret c

.discourage
	inc [hl]
	inc [hl]
	inc [hl]
	ret

Here we see the inc [hl] instead of dec [hl]. This routine goes to lengths to find a good time to use Selfdestruct (or Explosion) and avoid using it otherwise, which is why it was weird that the effect was listed in "reckless" moves, effectively favoring it in AI_AGGRESSIVE.

There are a whole lot of Smart routines in here already, some with problems like we saw with Sleep, and some that are fine. By looking at different ones, you can learn how to encourage and discourage moves under different circumstances. It's all up to you how much you want to adjust any of this to your own preferences. Not only is it a lot to cover in a tutorial, but as with the move lists, there's a lot of subjective judgement involved.

If you do get into making new Smart routines, you will likely get a bank overflow error when you try to build the ROM. There's only so much space in one memory bank, so you're limited in what you can do. However, since Smart routines are all dealing with modifying move scores, there's a lot of code repeated in them that could be shared instead in order to save space. Here's one example:

AI_Smart_Bide:
; 90% chance to discourage this move unless enemy's HP is full.

	call AICheckEnemyMaxHP
	ret c
	call Random
	cp 10 percent
	ret c
	inc [hl]
	ret

...

AI_Smart_LightScreen:
AI_Smart_Reflect:
; Over 90% chance to discourage this move unless enemy's HP is full.

	call AICheckEnemyMaxHP
	ret c
	call Random
	cp 8 percent
	ret c
	inc [hl]
	ret

Reflect and Light Screen already share the same code. But Bide's code is also very similar. If you need the space, you could just make them all share one set of code, like this:

-AI_Smart_Bide:
-; 90% chance to discourage this move unless enemy's HP is full.
-
-	call AICheckEnemyMaxHP
-	ret c
-	call Random
-	cp 10 percent
-	ret c
-	inc [hl]
-	ret

...

+AI_Smart_Bide:
AI_Smart_LightScreen:
AI_Smart_Reflect:
; Over 90% chance to discourage this move unless enemy's HP is full.

	call AICheckEnemyMaxHP
	ret c
	call Random
	cp 8 percent
	ret c
	inc [hl]
	ret

Pick either 8% or 10%, whichever you like. (Or 9%!) This kind of code sharing is often called "optimization."

You can also make room by just deleting stuff that's unused:

...

AI_Smart_EffectHandlers:
	dbw EFFECT_SLEEP,            AI_Smart_Sleep
	...
	dbw EFFECT_LOCK_ON,          AI_Smart_LockOn
-	dbw EFFECT_DEFROST_OPPONENT, AI_Smart_DefrostOpponent
	dbw EFFECT_SLEEP_TALK,       AI_Smart_SleepTalk
	...
	dbw EFFECT_FLY,              AI_Smart_Fly
	db -1 ; end

...

-AI_Smart_DefrostOpponent:
-; Greatly encourage this move if enemy is frozen.
-; No move has EFFECT_DEFROST_OPPONENT, so this layer is unused.
-
-	ld a, [wEnemyMonStatus]
-	and 1 << FRZ
-	ret z
-	dec [hl]
-	dec [hl]
-	dec [hl]
-	ret

...

If you're going to be adding a lot of new moves into your hack, then you'll have to get into making new Smart AI routines for them, at least if they have new effects. And if you change the effects of existing moves, make sure you update the AI's treatment of them.

That's as far as this tutorial is going to carry you. Take some time to go through all of those Smart routines and see what clever tricks you can come up with. Or don't—obviously, the game mostly works fine with Game Freak's original AI preferences. And even if you do add new moves without giving them specific Smart AI routines, it just means that the AI will use them when all of its other moves receive a higher score, so they won't go completely unused. But putting your own unique touches onto AI behavior can give your hack its own flavor in a subtle yet important way.