When battling an enemy Trainer, the first decision the enemy needs to make is whether or not to switch an active Pokémon with another Pokémon in their party. This decision is primarily made by comparing matchup scores between each Pokémon in the enemy's party.
The core function for matchup score calculation can be found in src/field/pokemon.ts, within the Pokemon class:
getMatchupScore(pokemon: Pokemon): number;
This computes the source Pokémon's matchup score against the Pokémon passed by argument using the formula
$$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$
where
The maximum possible matchup score a Pokémon could have against a single opponent is $(16+16)\times 2=64$, which occurs when
In most situations, though, a Pokémon's matchup score against an opponent will be at most 16, which is equivalent to having two super effective types and resisting both of the opponent's types with the same HP ratios as before.
The minimum possible matchup score a Pokémon could have against a single opponent is near zero, which occurs when the Pokémon's HP ratio is near zero while the opponent is at max HP. However, a Pokémon's matchup score can also be very low when its type(s) are 4x weak to and/or resisted by its opponent's types.
The EnemyCommandPhase follows this process to determine whether or not an enemy Pokémon should switch on each turn during a Trainer battle.
FIGHT command (see next section).turnCommand and end this EnemyCommandPhase; otherwise, move on to resolving a FIGHT enemy command.At this point, the enemy (a wild or trainer Pokémon) has decided against switching and instead will use a move from its moveset. However, it still needs to figure out which move to use and, if applicable, which target to use the move against. The logic for determining an enemy's next move and target is contained within two methods: EnemyPokemon.getNextMove() and EnemyPokemon.getNextTargets() in src/field/pokemon.ts.
getNextMove()In getNextMove(), the enemy Pokémon chooses a move to use in the following steps:
NaN for each target. In this case, the game assumes the target score calculation for that move is unimplemented.aiType. If the enemy is a Boss Pokémon or has a Trainer, it uses the SMART AI type; otherwise, it uses the SMART_RANDOM AI type.aiType === SMART_RANDOM, the enemy has a 5/8 chance of selecting $m_0$ and a 3/8 chance of advancing to the next best move $m_1$, where it then repeats this roll. This process stops when a move is selected or the last move in the move pool is reached.
- If aiType === SMART, a similar loop is used to decide between selecting the move $m_i$ and advancing to the next iteration with the move $m_{i+1}$. However, instead of using a flat probability, the following conditions need to be met to advance from selecting $m_i$ to $m_{i+1}$:
As part of the move selection process, the enemy Pokémon must compute a target score (TS) for each legal target for each move in its move pool. The base target score is a combination of the move's user benefit score (UBS) and target benefit score (TBS), representing how much the move helps or hinders the user and/or its target(s).
$$ \text{TS} = \text{UBS} + \left( \text{TBS} \times \begin{cases} -1 & \text{if target is an opponent} \ 1 & \text{otherwise} \end{cases} \right) $$
A move's UBS and TBS are computed with the respective functions in the Move class:
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number;
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number;
Logically, these functions are very similar – they add up their respective benefit scores from each of the move's attributes (as determined by attr.getUserBenefitScore, and attr.getTargetBenefitScore, respectively) and return the total benefit score. However, there are two key functional differences in how the UBS and TBS of a move are handled:
In addition to the base score from Move.getTargetBenefitScore(), attack moves calculate an attackScore which influences the move's TBS based on the following properties:
VariablePowerAttrs are applied)More specifically, the following steps are taken to compute the move's attackScore:
$$ \text{typeMult} = \begin{cases} 2 & \text{if move is super effective (or better)} \ -2 & \text{otherwise} \end{cases} $$ 2. Compute a multiplier based on the move's category and the user's offensive stats:
$$
\text{statRatio} =
\begin{cases}
\frac{\text{userSpAtk}}{\text{userAtk}} & \text{if move is physical} \\
\frac{\text{userAtk}}{\text{userSpAtk}} & \text{otherwise}
\end{cases}
$$
Compute the stat-based multiplier:
$$ \text{statMult} = \begin{cases} 2 & \text{if statRatio} \leq 0.75 \ 1.5 & \text{if } 0.75 \leq \text{statRatio} \leq 0.875 \ 1 & \text{otherwise} \end{cases} $$
Calculate the move's attackScore:
$\text{attackScore} = (\text{typeMult}\times \text{statMult})+\lfloor \frac{\text{power}}{5} \rfloor$
The maximum total multiplier in attackScore ($\text{typeMult}\times \text{statMult}$) is 4, which occurs for attacks that are super effective against the target and are categorically aligned with the user's offensive stats (e.g. the move is physical, and the user has much higher Attack than Sp. Atk). The minimum total multiplier of -4 occurs (somewhat confusingly) for attacks that are not super effective but are categorically aligned with the user's offensive stats.
The attack move's total TBS, then, is $\text{TBS}=\text{baseScore}-\text{attackScore}$, where $\text{baseScore}$ is the result of Move.getTargetBenefitScore().
The final step to calculate an attack move's target score (TS) is to multiply the base target score by the move's type effectiveness and STAB (if it applies):
getNextTargets()The enemy's target selection for single-target moves works in a very similar way to its move selection. Each potential target is given a target selection score (TSS) which is based on the move's target benefit score for that target:
$$ \text{TSS} = \text{TBS} \times \begin{cases} -1 & \text{if target is an opponent} \ 1 & \text{otherwise} \end{cases} $$
Once the TSS is calculated for each target, the target is selected as follows:
$$ W_i = \begin{cases} w_i + |w_n| & \text{if } w_n \text{ is negative} \ w_i & \text{otherwise} \end{cases} $$
Once the target is selected, the enemy has successfully determined its next action for the turn, and its corresponding EnemyCommandPhase ends. From here, the TurnStartPhase processes the enemy's commands alongside the player's commands and begins to resolve the turn.
Suppose you enter a single battle against an enemy trainer with the following Pokémon in their party:
The enemy trainer leads with their Heatmor, and you lead with a Dachsbun with the Ability Well-Baked Body. We'll cover the enemy's behavior over the next two turns.
To determine whether the enemy should switch Pokémon, it first calculates each party member's matchup scores against the player's Dachsbun:
$$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$
defScore is 2. However, because of Dachsbun's Fire immunity granted by Well-Baked Body, Heatmor's atkScore against Dachsbun is 0. With both Pokémon at maximum HP, Heatmor's total matchup score is 2.defScore is also 2. In this case, though, Steel is also super effective against Fairy, so Excadrill's base atkScore is 2. If Excadrill outspeeds Dachsbun (possibly due to it having a +Spd nature or holding a Carbos), its atkScore is further increased to 2.5. Since both Pokémon are at maximum HP, Excadrill's total matchup score is 4 (or 4.5 if it outspeeds).Based on the enemy party's matchup scores, whether or not the trainer switches out Heatmor for Excadrill depends on the trainer's type. The difference in matchup scores is enough to cause a switch to Excadrill for boss trainers (e.g. gym leaders) but not for regular trainers. For this example, we'll assume the trainer is a boss and, therefore, decides to switch to Excadrill on this turn.
Now that the enemy Pokémon with the best matchup score is on the field (assuming it survives Dachsbun's attack on the last turn), the enemy will now decide to have Excadrill use one of its moves. Assuming all of its moves are usable, we'll go through the target score calculations for each move:
Earthquake: In a single battle, this move is just a 100-power Ground-type physical attack with no additional effects. With no additional benefit score from attributes, the move's base target score against the player's Dachsbun is just the attackScore from AttackMove.getTargetBenefitScore(). In this case, Earthquake's attackScore is given by
$\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = -2\times 2 + 20 = 16$
Here, typeMult is -2 because the move is not super effective, and statMult is 2 because Excadrill's Attack is significantly higher than its Sp. Atk. Accounting for STAB thanks to Excadrill's typing, the final target score for this move is 24
Iron Head: This move is an 80-power Steel-type physical attack with an additional chance to cause the target to flinch. With these properties, Iron Head has a user benefit score of 0 and a target benefit score given by
$\text{TBS}=\text{getTargetBenefitScore(FlinchAttr)}-\text{attackScore}$
Under its current implementation, the target benefit score of FlinchAttr is -5. Calculating the move's attackScore, we get:
$\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = 2\times 2 + 16 = 20$
Note that typeMult in this case is 2 because Iron Head is super effective (or better) against Dachsbun. With the move's UBS at 0, the base target score calculation against Dachsbun simplifies to
$\text{TS}=-\text{TBS}=-(-5-20)=25$
We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is 75.
Swords Dance: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's StatStageChangeAttr has a user benefit score of 0 and a target benefit score that, in this case, simplifies to
$\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$
where levels is the number of stat stages added by the attribute (in this case, +2). The final score for this move is 6 (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score).
Crush Claw: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same StatStageChangeAttr as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score.
$\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$
$\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$
$\text{TS}=-\text{TBS}=13$
This move is neutral against Dachsbun and isn't boosted by STAB from Excadrill, so we don't need to apply any extra multipliers. The final score for this move is 13.
We now have a sorted move pool in decreasing order of move scores:
Since no other score is at least half that of Iron Head's score, the enemy AI automatically chooses to use Iron Head against Dachsbun at this point.
When implementing a new move attribute, it's important to override MoveAttr's getUserBenefitScore and getTargetBenefitScore functions to ensure that the enemy AI can accurately determine when and how to use moves with that attribute. Here are a few basic specifications you should adhere to when implementing benefit scores for a new attribute: