use std::cmp::Ordering;
use crate::models::{PokemonDetail, StatKind};
use crate::team;
use crate::typechart;
pub const STAT_ORDER: [StatKind; 6] = [
StatKind::Hp,
StatKind::Attack,
StatKind::Defense,
StatKind::SpecialAttack,
StatKind::SpecialDefense,
StatKind::Speed,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Left,
Right,
Tie,
}
pub fn side(left: u32, right: u32) -> Side {
match left.cmp(&right) {
Ordering::Greater => Side::Left,
Ordering::Less => Side::Right,
Ordering::Equal => Side::Tie,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatRow {
pub kind: StatKind,
pub left: u16,
pub right: u16,
}
pub fn stat_rows(left: &PokemonDetail, right: &PokemonDetail) -> Vec<StatRow> {
STAT_ORDER
.iter()
.map(|&kind| StatRow {
kind,
left: base(left, kind),
right: base(right, kind),
})
.collect()
}
pub fn peak(rows: &[StatRow]) -> u16 {
rows.iter()
.map(|row| row.left.max(row.right))
.max()
.unwrap_or(0)
}
fn base(species: &PokemonDetail, kind: StatKind) -> u16 {
species
.stats
.iter()
.find(|stat| stat.kind == kind)
.map_or(0, |stat| stat.base)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hit<'a> {
pub attack_type: &'a str,
pub multiplier: f32,
}
pub fn best_hit<'a>(attacker: &'a PokemonDetail, defender: &PokemonDetail) -> Option<Hit<'a>> {
let immune: Vec<&str> = team::ability_immunities(defender)
.iter()
.filter(|immunity| immunity.certain)
.map(|immunity| immunity.immune_to)
.collect();
attacker
.types
.iter()
.map(|attack_type| Hit {
attack_type,
multiplier: match immune.contains(&attack_type.as_str()) {
true => 0.0,
false => typechart::combined(attack_type, &defender.types),
},
})
.max_by(|a, b| {
a.multiplier
.partial_cmp(&b.multiplier)
.unwrap_or(Ordering::Equal)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Ability, FieldData, Stat};
use std::collections::HashMap;
fn species(name: &str, types: &[&str], stats: &[(StatKind, u16)]) -> PokemonDetail {
with_abilities(name, types, stats, &[])
}
fn with_abilities(
name: &str,
types: &[&str],
stats: &[(StatKind, u16)],
abilities: &[&str],
) -> PokemonDetail {
PokemonDetail {
name: name.to_string(),
species: name.to_string(),
forms: Vec::new(),
dex_number: 0,
is_legendary: false,
is_mythical: false,
is_baby: false,
types: types.iter().map(|t| t.to_string()).collect(),
abilities: abilities
.iter()
.map(|a| Ability {
name: a.to_string(),
is_hidden: false,
})
.collect(),
stats: stats
.iter()
.map(|&(kind, base)| Stat { kind, base })
.collect(),
height: 0,
weight: 0,
sprite_url: None,
shiny_sprite_url: None,
genera: HashMap::new(),
flavors: HashMap::new(),
moves: Vec::new(),
learnset_games: None,
field: FieldData::default(),
}
}
#[test]
fn each_row_names_its_winner_and_by_how_much() {
let fast = species("jolteon", &["electric"], &[(StatKind::Speed, 130)]);
let slow = species("snorlax", &["normal"], &[(StatKind::Speed, 30)]);
let rows = stat_rows(&fast, &slow);
let speed = rows.last().expect("six rows");
assert_eq!(speed.kind, StatKind::Speed);
assert_eq!(side(speed.left.into(), speed.right.into()), Side::Left);
assert_eq!(speed.left.abs_diff(speed.right), 100);
}
#[test]
fn a_stat_they_share_is_reported_as_a_tie_rather_than_a_near_miss() {
let a = species("a", &["normal"], &[(StatKind::Attack, 100)]);
let b = species("b", &["normal"], &[(StatKind::Attack, 100)]);
let attack = stat_rows(&a, &b)[1];
assert_eq!(side(attack.left.into(), attack.right.into()), Side::Tie);
assert_eq!(attack.left.abs_diff(attack.right), 0);
}
#[test]
fn the_rows_line_up_however_the_records_order_their_stats() {
let forwards = species(
"a",
&["normal"],
&[(StatKind::Hp, 10), (StatKind::Speed, 20)],
);
let backwards = species(
"b",
&["normal"],
&[(StatKind::Speed, 40), (StatKind::Hp, 30)],
);
let rows = stat_rows(&forwards, &backwards);
assert_eq!(rows[0].kind, StatKind::Hp);
assert_eq!((rows[0].left, rows[0].right), (10, 30));
assert_eq!(rows[5].kind, StatKind::Speed);
assert_eq!((rows[5].left, rows[5].right), (20, 40));
}
#[test]
fn a_stat_neither_record_carries_still_gets_a_row() {
let rows = stat_rows(&species("a", &[], &[]), &species("b", &[], &[]));
assert_eq!(rows.len(), STAT_ORDER.len());
assert!(rows
.iter()
.all(|row| side(row.left.into(), row.right.into()) == Side::Tie));
}
#[test]
fn the_bars_scale_to_the_biggest_number_on_the_card() {
let rows = stat_rows(
&species("a", &[], &[(StatKind::Hp, 55)]),
&species("b", &[], &[(StatKind::Hp, 120)]),
);
assert_eq!(peak(&rows), 120);
}
#[test]
fn the_best_hit_is_the_hardest_of_the_attackers_own_types() {
let gengar = species("gengar", &["ghost", "poison"], &[]);
let alakazam = species("alakazam", &["psychic"], &[]);
let hit = best_hit(&gengar, &alakazam).expect("a typing to hit with");
assert_eq!(hit.attack_type, "ghost");
assert_eq!(hit.multiplier, 2.0);
}
#[test]
fn a_dual_type_defender_multiplies_both_ways() {
let glaceon = species("glaceon", &["ice"], &[]);
let dragonite = species("dragonite", &["dragon", "flying"], &[]);
assert_eq!(best_hit(&glaceon, &dragonite).unwrap().multiplier, 4.0);
}
#[test]
fn a_certain_ability_immunity_answers_the_chart() {
let sandslash = species("sandslash", &["ground"], &[]);
let eelektross = with_abilities("eelektross", &["electric"], &[], &["levitate"]);
assert_eq!(best_hit(&sandslash, &eelektross).unwrap().multiplier, 0.0);
let maybe = with_abilities("maybe", &["electric"], &[], &["levitate", "static"]);
assert_eq!(best_hit(&sandslash, &maybe).unwrap().multiplier, 2.0);
}
#[test]
fn a_species_with_no_typing_has_no_hit_to_report() {
assert_eq!(
best_hit(&species("a", &[], &[]), &species("b", &[], &[])),
None
);
}
}