pub const TYPES: [&str; 18] = [
"normal", "fire", "water", "electric", "grass", "ice", "fighting", "poison", "ground",
"flying", "psychic", "bug", "rock", "ghost", "dragon", "dark", "steel", "fairy",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chart {
Gen1,
Gen2To5,
Gen6On,
}
impl Chart {
pub fn types(self) -> &'static [&'static str] {
match self {
Chart::Gen1 => &TYPES[..15],
Chart::Gen2To5 => &TYPES[..17],
Chart::Gen6On => &TYPES,
}
}
pub fn has(self, slug: &str) -> bool {
self.types().contains(&slug)
}
pub fn effectiveness(self, attacker: &str, defender: &str) -> f32 {
if !self.has(attacker) || !self.has(defender) {
return 1.0;
}
match (self, attacker, defender) {
(Chart::Gen1, "bug", "poison") => 2.0,
(Chart::Gen1, "poison", "bug") => 2.0,
(Chart::Gen1, "ghost", "psychic") => 0.0,
(Chart::Gen1, "ice", "fire") => 1.0,
(Chart::Gen2To5, "ghost" | "dark", "steel") => 0.5,
_ => current(attacker, defender),
}
}
}
pub const LATEST_GENERATION: u8 = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Generation(u8);
impl Generation {
pub const LATEST: Generation = Generation(LATEST_GENERATION);
pub fn new(number: u8) -> Option<Self> {
(1..=LATEST_GENERATION)
.contains(&number)
.then_some(Generation(number))
}
pub fn number(self) -> u8 {
self.0
}
pub fn chart(self) -> Chart {
match self.0 {
1 => Chart::Gen1,
2..=5 => Chart::Gen2To5,
_ => Chart::Gen6On,
}
}
pub fn has_abilities(self) -> bool {
self.0 >= 3
}
pub fn next(self) -> Self {
Generation(self.0 % LATEST_GENERATION + 1)
}
pub fn previous(self) -> Self {
Generation((self.0 + LATEST_GENERATION - 2) % LATEST_GENERATION + 1)
}
pub fn roman(self) -> &'static str {
["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"][self.0 as usize - 1]
}
}
impl Default for Generation {
fn default() -> Self {
Generation::LATEST
}
}
fn current(attacker: &str, defender: &str) -> f32 {
match (attacker, defender) {
("normal", "rock" | "steel") => 0.5,
("normal", "ghost") => 0.0,
("fire", "fire" | "water" | "rock" | "dragon") => 0.5,
("fire", "grass" | "ice" | "bug" | "steel") => 2.0,
("water", "water" | "grass" | "dragon") => 0.5,
("water", "fire" | "ground" | "rock") => 2.0,
("electric", "electric" | "grass" | "dragon") => 0.5,
("electric", "ground") => 0.0,
("electric", "water" | "flying") => 2.0,
("grass", "fire" | "grass" | "poison" | "flying" | "bug" | "dragon" | "steel") => 0.5,
("grass", "water" | "ground" | "rock") => 2.0,
("ice", "fire" | "water" | "ice" | "steel") => 0.5,
("ice", "grass" | "ground" | "flying" | "dragon") => 2.0,
("fighting", "poison" | "flying" | "psychic" | "bug" | "fairy") => 0.5,
("fighting", "ghost") => 0.0,
("fighting", "normal" | "ice" | "rock" | "dark" | "steel") => 2.0,
("poison", "poison" | "ground" | "rock" | "ghost") => 0.5,
("poison", "steel") => 0.0,
("poison", "grass" | "fairy") => 2.0,
("ground", "grass" | "bug") => 0.5,
("ground", "flying") => 0.0,
("ground", "fire" | "electric" | "poison" | "rock" | "steel") => 2.0,
("flying", "electric" | "rock" | "steel") => 0.5,
("flying", "grass" | "fighting" | "bug") => 2.0,
("psychic", "psychic" | "steel") => 0.5,
("psychic", "dark") => 0.0,
("psychic", "fighting" | "poison") => 2.0,
("bug", "fire" | "fighting" | "poison" | "flying" | "ghost" | "steel" | "fairy") => 0.5,
("bug", "grass" | "psychic" | "dark") => 2.0,
("rock", "fighting" | "ground" | "steel") => 0.5,
("rock", "fire" | "ice" | "flying" | "bug") => 2.0,
("ghost", "dark") => 0.5,
("ghost", "normal") => 0.0,
("ghost", "psychic" | "ghost") => 2.0,
("dragon", "steel") => 0.5,
("dragon", "fairy") => 0.0,
("dragon", "dragon") => 2.0,
("dark", "fighting" | "dark" | "fairy") => 0.5,
("dark", "psychic" | "ghost") => 2.0,
("steel", "fire" | "water" | "electric" | "steel") => 0.5,
("steel", "ice" | "rock" | "fairy") => 2.0,
("fairy", "fire" | "poison" | "steel") => 0.5,
("fairy", "fighting" | "dragon" | "dark") => 2.0,
_ => 1.0,
}
}
pub fn combined(chart: Chart, attacker: &str, defender_types: &[String]) -> f32 {
defender_types
.iter()
.map(|d| chart.effectiveness(attacker, d))
.product()
}
#[derive(Debug, Clone)]
pub struct MatchupGroup {
pub label: &'static str,
pub types: Vec<&'static str>,
}
const BUCKETS: [(f32, &str); 5] = [
(4.0, "×4"),
(2.0, "×2"),
(0.5, "×½"),
(0.25, "×¼"),
(0.0, "×0"),
];
pub fn defensive_groups(
chart: Chart,
defender_types: &[String],
immune_to: &[&str],
) -> Vec<MatchupGroup> {
BUCKETS
.iter()
.filter_map(|&(multiplier, label)| {
let types: Vec<&'static str> = chart
.types()
.iter()
.copied()
.filter(|attacker| match immune_to.contains(attacker) {
true => same_multiplier(multiplier, 0.0),
false => same_multiplier(combined(chart, attacker, defender_types), multiplier),
})
.collect();
(!types.is_empty()).then_some(MatchupGroup { label, types })
})
.collect()
}
pub fn multiplier_label(multiplier: f32) -> &'static str {
BUCKETS
.iter()
.find(|&&(value, _)| same_multiplier(multiplier, value))
.map_or("×1", |&(_, label)| label)
}
pub fn offensive_coverage(chart: Chart, attacker_types: &[String]) -> Vec<&'static str> {
chart
.types()
.iter()
.copied()
.filter(|defender| {
attacker_types
.iter()
.any(|attacker| chart.effectiveness(attacker, defender) > 1.5)
})
.collect()
}
fn same_multiplier(a: f32, b: f32) -> bool {
(a - b).abs() < f32::EPSILON
}
#[cfg(test)]
mod tests {
use super::*;
fn types(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
fn differences(older: Chart, newer: Chart) -> Vec<(&'static str, &'static str, f32, f32)> {
let mut cells = Vec::new();
for &attacker in older.types() {
for &defender in older.types() {
let (was, is) = (
older.effectiveness(attacker, defender),
newer.effectiveness(attacker, defender),
);
if !same_multiplier(was, is) {
cells.push((attacker, defender, was, is));
}
}
}
cells
}
#[test]
fn generation_two_changed_exactly_four_cells_of_the_first_chart() {
assert_eq!(
differences(Chart::Gen1, Chart::Gen2To5),
[
("ice", "fire", 1.0, 0.5),
("poison", "bug", 2.0, 1.0),
("bug", "poison", 2.0, 0.5),
("ghost", "psychic", 0.0, 2.0),
]
);
}
#[test]
fn generation_six_changed_exactly_two_cells_of_the_second_chart() {
assert_eq!(
differences(Chart::Gen2To5, Chart::Gen6On),
[("ghost", "steel", 0.5, 1.0), ("dark", "steel", 0.5, 1.0)]
);
}
#[test]
fn each_chart_has_the_types_of_its_era() {
assert_eq!(Chart::Gen1.types().len(), 15);
assert!(!Chart::Gen1.has("dark") && !Chart::Gen1.has("steel"));
assert!(Chart::Gen2To5.has("steel") && !Chart::Gen2To5.has("fairy"));
assert!(Chart::Gen6On.has("fairy"));
}
#[test]
fn a_type_the_chart_does_not_have_is_neutral_on_either_side() {
assert_eq!(Chart::Gen2To5.effectiveness("dragon", "fairy"), 1.0);
assert_eq!(Chart::Gen2To5.effectiveness("fairy", "dragon"), 1.0);
assert_eq!(
combined(Chart::Gen1, "ground", &types(&["electric", "steel"])),
2.0
);
}
#[test]
fn an_old_chart_lists_none_of_the_types_it_does_not_have() {
let groups = defensive_groups(Chart::Gen1, &types(&["ghost", "poison"]), &[]);
let listed: Vec<&str> = groups.iter().flat_map(|g| g.types.clone()).collect();
assert!(listed.iter().all(|t| Chart::Gen1.has(t)), "{listed:?}");
let find = |label: &str| {
groups
.iter()
.find(|g| g.label == label)
.map(|g| g.types.clone())
.unwrap_or_default()
};
assert_eq!(find("×2"), vec!["ground", "psychic", "ghost"]);
assert!(find("×½").contains(&"grass"));
assert!(
!find("×½").contains(&"bug"),
"bug was ×1 on Gengar in Gen I"
);
let coverage = offensive_coverage(Chart::Gen1, &types(&["ghost"]));
assert_eq!(coverage, vec!["ghost"], "ghost did not hit psychic");
}
#[test]
fn generations_map_onto_their_charts() {
let chart = |n| Generation::new(n).unwrap().chart();
assert_eq!(chart(1), Chart::Gen1);
assert_eq!(chart(2), Chart::Gen2To5);
assert_eq!(chart(5), Chart::Gen2To5);
assert_eq!(chart(6), Chart::Gen6On);
assert_eq!(chart(9), Chart::Gen6On);
assert_eq!(Generation::new(0), None);
assert_eq!(Generation::new(LATEST_GENERATION + 1), None);
assert_eq!(Generation::default(), Generation::LATEST);
}
#[test]
fn stepping_through_the_generations_wraps_both_ways() {
let first = Generation::new(1).unwrap();
assert_eq!(Generation::LATEST.next(), first);
assert_eq!(first.previous(), Generation::LATEST);
assert_eq!(first.next().number(), 2);
assert_eq!(
Generation::LATEST.previous().number(),
LATEST_GENERATION - 1
);
let mut gen = first;
for _ in 0..LATEST_GENERATION {
gen = gen.next();
}
assert_eq!(gen, first);
}
#[test]
fn abilities_begin_in_the_third_generation() {
assert!(!Generation::new(2).unwrap().has_abilities());
assert!(Generation::new(3).unwrap().has_abilities());
assert_eq!(Generation::new(4).unwrap().roman(), "IV");
}
#[test]
fn chart_is_symmetric_where_the_games_are() {
assert_eq!(Chart::Gen6On.effectiveness("water", "fire"), 2.0);
assert_eq!(Chart::Gen6On.effectiveness("fire", "grass"), 2.0);
assert_eq!(Chart::Gen6On.effectiveness("grass", "water"), 2.0);
assert_eq!(Chart::Gen6On.effectiveness("grass", "fire"), 0.5);
}
#[test]
fn immunities_are_zero() {
assert_eq!(Chart::Gen6On.effectiveness("normal", "ghost"), 0.0);
assert_eq!(Chart::Gen6On.effectiveness("ghost", "normal"), 0.0);
assert_eq!(Chart::Gen6On.effectiveness("electric", "ground"), 0.0);
assert_eq!(Chart::Gen6On.effectiveness("dragon", "fairy"), 0.0);
assert_eq!(Chart::Gen6On.effectiveness("poison", "steel"), 0.0);
}
#[test]
fn unlisted_pairings_are_neutral() {
assert_eq!(Chart::Gen6On.effectiveness("normal", "normal"), 1.0);
assert_eq!(Chart::Gen6On.effectiveness("water", "steel"), 1.0);
}
#[test]
fn dual_types_stack_multiplicatively() {
assert_eq!(
combined(Chart::Gen6On, "rock", &types(&["fire", "flying"])),
4.0
);
assert_eq!(
combined(Chart::Gen6On, "grass", &types(&["fire", "flying"])),
0.25
);
assert_eq!(
combined(Chart::Gen6On, "ground", &types(&["fire", "flying"])),
0.0
);
}
#[test]
fn defensive_groups_cover_charizard() {
let groups = defensive_groups(Chart::Gen6On, &types(&["fire", "flying"]), &[]);
let find = |label: &str| {
groups
.iter()
.find(|g| g.label == label)
.map(|g| g.types.clone())
.unwrap_or_default()
};
assert_eq!(find("×4"), vec!["rock"]);
assert_eq!(find("×2"), vec!["water", "electric"]);
assert_eq!(find("×¼"), vec!["grass", "bug"]);
assert_eq!(find("×0"), vec!["ground"]);
}
#[test]
fn a_forced_immunity_moves_its_type_out_of_the_row_it_was_in() {
let rotom = types(&["electric", "ghost"]);
let groups = defensive_groups(Chart::Gen6On, &rotom, &["ground"]);
let find = |label: &str| {
groups
.iter()
.find(|g| g.label == label)
.map(|g| g.types.clone())
.unwrap_or_default()
};
assert!(find("×0").contains(&"ground"));
assert!(
!find("×2").contains(&"ground"),
"a type cannot be both ×2 and immune"
);
assert_eq!(find("×2"), vec!["ghost", "dark"]);
assert!(find("×0").contains(&"normal"), "ghost still ignores normal");
}
#[test]
fn a_forced_immunity_the_chart_already_knew_is_not_listed_twice() {
let groups = defensive_groups(Chart::Gen6On, &types(&["flying"]), &["ground"]);
let ground_rows: usize = groups
.iter()
.filter(|g| g.types.contains(&"ground"))
.count();
assert_eq!(ground_rows, 1);
}
#[test]
fn every_multiplier_the_chart_can_produce_has_a_label() {
assert_eq!(multiplier_label(4.0), "×4");
assert_eq!(multiplier_label(2.0), "×2");
assert_eq!(multiplier_label(1.0), "×1");
assert_eq!(multiplier_label(0.5), "×½");
assert_eq!(multiplier_label(0.25), "×¼");
assert_eq!(multiplier_label(0.0), "×0");
}
#[test]
fn neutral_matchups_are_omitted() {
let groups = defensive_groups(Chart::Gen6On, &types(&["normal"]), &[]);
let listed: usize = groups.iter().map(|g| g.types.len()).sum();
assert_eq!(listed, 2);
}
#[test]
fn offensive_coverage_is_the_union_of_both_types() {
let coverage = offensive_coverage(Chart::Gen6On, &types(&["fire", "flying"]));
assert!(coverage.contains(&"grass")); assert!(coverage.contains(&"steel")); assert!(coverage.contains(&"fighting")); assert!(!coverage.contains(&"water"));
}
}