poke_engine/
evaluate.rs

1use crate::abilities::Abilities;
2use crate::choices::MoveCategory;
3use crate::items::Items;
4use crate::state::{Pokemon, PokemonStatus, PokemonVolatileStatus, Side, State};
5
6const POKEMON_ALIVE: f32 = 30.0;
7const POKEMON_HP: f32 = 100.0;
8const USED_TERA: f32 = -75.0;
9
10const POKEMON_ATTACK_BOOST: f32 = 30.0;
11const POKEMON_DEFENSE_BOOST: f32 = 15.0;
12const POKEMON_SPECIAL_ATTACK_BOOST: f32 = 30.0;
13const POKEMON_SPECIAL_DEFENSE_BOOST: f32 = 15.0;
14const POKEMON_SPEED_BOOST: f32 = 30.0;
15
16const POKEMON_BOOST_MULTIPLIER_6: f32 = 3.3;
17const POKEMON_BOOST_MULTIPLIER_5: f32 = 3.15;
18const POKEMON_BOOST_MULTIPLIER_4: f32 = 3.0;
19const POKEMON_BOOST_MULTIPLIER_3: f32 = 2.5;
20const POKEMON_BOOST_MULTIPLIER_2: f32 = 2.0;
21const POKEMON_BOOST_MULTIPLIER_1: f32 = 1.0;
22const POKEMON_BOOST_MULTIPLIER_0: f32 = 0.0;
23const POKEMON_BOOST_MULTIPLIER_NEG_1: f32 = -1.0;
24const POKEMON_BOOST_MULTIPLIER_NEG_2: f32 = -2.0;
25const POKEMON_BOOST_MULTIPLIER_NEG_3: f32 = -2.5;
26const POKEMON_BOOST_MULTIPLIER_NEG_4: f32 = -3.0;
27const POKEMON_BOOST_MULTIPLIER_NEG_5: f32 = -3.15;
28const POKEMON_BOOST_MULTIPLIER_NEG_6: f32 = -3.3;
29
30const POKEMON_FROZEN: f32 = -40.0;
31const POKEMON_ASLEEP: f32 = -25.0;
32const POKEMON_PARALYZED: f32 = -25.0;
33const POKEMON_TOXIC: f32 = -30.0;
34const POKEMON_POISONED: f32 = -10.0;
35const POKEMON_BURNED: f32 = -25.0;
36
37const LEECH_SEED: f32 = -30.0;
38const SUBSTITUTE: f32 = 40.0;
39const CONFUSION: f32 = -20.0;
40
41const REFLECT: f32 = 20.0;
42const LIGHT_SCREEN: f32 = 20.0;
43const AURORA_VEIL: f32 = 40.0;
44const SAFE_GUARD: f32 = 5.0;
45const TAILWIND: f32 = 7.0;
46
47const STEALTH_ROCK: f32 = -10.0;
48const SPIKES: f32 = -7.0;
49const TOXIC_SPIKES: f32 = -7.0;
50const STICKY_WEB: f32 = -25.0;
51
52fn evaluate_poison(pokemon: &Pokemon, base_score: f32) -> f32 {
53    match pokemon.ability {
54        Abilities::POISONHEAL => 15.0,
55        Abilities::GUTS
56        | Abilities::MARVELSCALE
57        | Abilities::QUICKFEET
58        | Abilities::TOXICBOOST
59        | Abilities::MAGICGUARD => 10.0,
60        _ => base_score,
61    }
62}
63
64fn evaluate_burned(pokemon: &Pokemon) -> f32 {
65    // burn is not as punishing in certain situations
66
67    // guts, marvel scale, quick feet will result in a positive evaluation
68    match pokemon.ability {
69        Abilities::GUTS | Abilities::MARVELSCALE | Abilities::QUICKFEET => {
70            return -2.0 * POKEMON_BURNED
71        }
72        _ => {}
73    }
74
75    let mut multiplier = 0.0;
76    for mv in pokemon.moves.into_iter() {
77        if mv.choice.category == MoveCategory::Physical {
78            multiplier += 1.0;
79        }
80    }
81
82    // don't make burn as punishing for special attackers
83    if pokemon.special_attack > pokemon.attack {
84        multiplier /= 2.0;
85    }
86
87    multiplier * POKEMON_BURNED
88}
89
90fn get_boost_multiplier(boost: i8) -> f32 {
91    match boost {
92        6 => POKEMON_BOOST_MULTIPLIER_6,
93        5 => POKEMON_BOOST_MULTIPLIER_5,
94        4 => POKEMON_BOOST_MULTIPLIER_4,
95        3 => POKEMON_BOOST_MULTIPLIER_3,
96        2 => POKEMON_BOOST_MULTIPLIER_2,
97        1 => POKEMON_BOOST_MULTIPLIER_1,
98        0 => POKEMON_BOOST_MULTIPLIER_0,
99        -1 => POKEMON_BOOST_MULTIPLIER_NEG_1,
100        -2 => POKEMON_BOOST_MULTIPLIER_NEG_2,
101        -3 => POKEMON_BOOST_MULTIPLIER_NEG_3,
102        -4 => POKEMON_BOOST_MULTIPLIER_NEG_4,
103        -5 => POKEMON_BOOST_MULTIPLIER_NEG_5,
104        -6 => POKEMON_BOOST_MULTIPLIER_NEG_6,
105        _ => panic!("Invalid boost value: {}", boost),
106    }
107}
108
109fn evaluate_hazards(pokemon: &Pokemon, side: &Side) -> f32 {
110    let mut score = 0.0;
111    let pkmn_is_grounded = pokemon.is_grounded();
112    if pokemon.item != Items::HEAVYDUTYBOOTS {
113        if pokemon.ability != Abilities::MAGICGUARD {
114            score += side.side_conditions.stealth_rock as f32 * STEALTH_ROCK;
115            if pkmn_is_grounded {
116                score += side.side_conditions.spikes as f32 * SPIKES;
117                score += side.side_conditions.toxic_spikes as f32 * TOXIC_SPIKES;
118            }
119        }
120        if pkmn_is_grounded {
121            score += side.side_conditions.sticky_web as f32 * STICKY_WEB;
122        }
123    }
124
125    score
126}
127
128fn evaluate_pokemon(pokemon: &Pokemon) -> f32 {
129    let mut score = 0.0;
130    score += POKEMON_HP * pokemon.hp as f32 / pokemon.maxhp as f32;
131
132    match pokemon.status {
133        PokemonStatus::BURN => score += evaluate_burned(pokemon),
134        PokemonStatus::FREEZE => score += POKEMON_FROZEN,
135        PokemonStatus::SLEEP => score += POKEMON_ASLEEP,
136        PokemonStatus::PARALYZE => score += POKEMON_PARALYZED,
137        PokemonStatus::TOXIC => score += evaluate_poison(pokemon, POKEMON_TOXIC),
138        PokemonStatus::POISON => score += evaluate_poison(pokemon, POKEMON_POISONED),
139        PokemonStatus::NONE => {}
140    }
141
142    if pokemon.item != Items::NONE {
143        score += 10.0;
144    }
145
146    // without this a low hp pokemon could get a negative score and incentivize the other side
147    // to keep it alive
148    if score < 0.0 {
149        score = 0.0;
150    }
151
152    score += POKEMON_ALIVE;
153
154    score
155}
156
157pub fn evaluate(state: &State) -> f32 {
158    let mut score = 0.0;
159
160    let mut iter = state.side_one.pokemon.into_iter();
161    let mut s1_used_tera = false;
162    while let Some(pkmn) = iter.next() {
163        if pkmn.hp > 0 {
164            score += evaluate_pokemon(pkmn);
165            score += evaluate_hazards(pkmn, &state.side_one);
166            if iter.pokemon_index == state.side_one.active_index {
167                for vs in state.side_one.volatile_statuses.iter() {
168                    match vs {
169                        PokemonVolatileStatus::LEECHSEED => score += LEECH_SEED,
170                        PokemonVolatileStatus::SUBSTITUTE => score += SUBSTITUTE,
171                        PokemonVolatileStatus::CONFUSION => score += CONFUSION,
172                        _ => {}
173                    }
174                }
175
176                score += get_boost_multiplier(state.side_one.attack_boost) * POKEMON_ATTACK_BOOST;
177                score += get_boost_multiplier(state.side_one.defense_boost) * POKEMON_DEFENSE_BOOST;
178                score += get_boost_multiplier(state.side_one.special_attack_boost)
179                    * POKEMON_SPECIAL_ATTACK_BOOST;
180                score += get_boost_multiplier(state.side_one.special_defense_boost)
181                    * POKEMON_SPECIAL_DEFENSE_BOOST;
182                score += get_boost_multiplier(state.side_one.speed_boost) * POKEMON_SPEED_BOOST;
183            }
184        }
185        if pkmn.terastallized {
186            s1_used_tera = true;
187        }
188    }
189    if s1_used_tera {
190        score += USED_TERA;
191    }
192    let mut iter = state.side_two.pokemon.into_iter();
193    let mut s2_used_tera = false;
194    while let Some(pkmn) = iter.next() {
195        if pkmn.hp > 0 {
196            score -= evaluate_pokemon(pkmn);
197            score -= evaluate_hazards(pkmn, &state.side_two);
198
199            if iter.pokemon_index == state.side_two.active_index {
200                for vs in state.side_two.volatile_statuses.iter() {
201                    match vs {
202                        PokemonVolatileStatus::LEECHSEED => score -= LEECH_SEED,
203                        PokemonVolatileStatus::SUBSTITUTE => score -= SUBSTITUTE,
204                        PokemonVolatileStatus::CONFUSION => score -= CONFUSION,
205                        _ => {}
206                    }
207                }
208
209                score -= get_boost_multiplier(state.side_two.attack_boost) * POKEMON_ATTACK_BOOST;
210                score -= get_boost_multiplier(state.side_two.defense_boost) * POKEMON_DEFENSE_BOOST;
211                score -= get_boost_multiplier(state.side_two.special_attack_boost)
212                    * POKEMON_SPECIAL_ATTACK_BOOST;
213                score -= get_boost_multiplier(state.side_two.special_defense_boost)
214                    * POKEMON_SPECIAL_DEFENSE_BOOST;
215                score -= get_boost_multiplier(state.side_two.speed_boost) * POKEMON_SPEED_BOOST;
216            }
217        }
218        if pkmn.terastallized {
219            s2_used_tera = true;
220        }
221    }
222    if s2_used_tera {
223        score -= USED_TERA;
224    }
225
226    score += state.side_one.side_conditions.reflect as f32 * REFLECT;
227    score += state.side_one.side_conditions.light_screen as f32 * LIGHT_SCREEN;
228    score += state.side_one.side_conditions.aurora_veil as f32 * AURORA_VEIL;
229    score += state.side_one.side_conditions.safeguard as f32 * SAFE_GUARD;
230    score += state.side_one.side_conditions.tailwind as f32 * TAILWIND;
231
232    score -= state.side_two.side_conditions.reflect as f32 * REFLECT;
233    score -= state.side_two.side_conditions.light_screen as f32 * LIGHT_SCREEN;
234    score -= state.side_two.side_conditions.aurora_veil as f32 * AURORA_VEIL;
235    score -= state.side_two.side_conditions.safeguard as f32 * SAFE_GUARD;
236    score -= state.side_two.side_conditions.tailwind as f32 * TAILWIND;
237
238    score
239}