use std::collections::HashMap;
use std::sync::OnceLock;
const TABLE: &str = include_str!("base_stats.tsv");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatKey {
Hp,
Attack,
Defense,
SpecialAttack,
SpecialDefense,
Speed,
Total,
}
impl StatKey {
pub fn parse(key: &str) -> Option<Self> {
match key.to_lowercase().as_str() {
"hp" => Some(Self::Hp),
"atk" | "attack" => Some(Self::Attack),
"def" | "defense" => Some(Self::Defense),
"spa" | "spatk" => Some(Self::SpecialAttack),
"spd" | "spdef" => Some(Self::SpecialDefense),
"spe" | "speed" => Some(Self::Speed),
"bst" | "total" => Some(Self::Total),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaseStats([u8; 6]);
impl BaseStats {
pub fn get(self, key: StatKey) -> u16 {
let [hp, atk, def, spa, spd, spe] = self.0.map(u16::from);
match key {
StatKey::Hp => hp,
StatKey::Attack => atk,
StatKey::Defense => def,
StatKey::SpecialAttack => spa,
StatKey::SpecialDefense => spd,
StatKey::Speed => spe,
StatKey::Total => hp + atk + def + spa + spd + spe,
}
}
}
pub fn lookup(name: &str) -> Option<BaseStats> {
static PARSED: OnceLock<HashMap<&'static str, BaseStats>> = OnceLock::new();
PARSED.get_or_init(|| parse(TABLE)).get(name).copied()
}
fn parse(table: &'static str) -> HashMap<&'static str, BaseStats> {
table
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.filter_map(parse_row)
.collect()
}
fn parse_row(line: &'static str) -> Option<(&'static str, BaseStats)> {
let mut fields = line.split('\t');
let name = fields.next()?;
let mut stats = [0u8; 6];
for stat in &mut stats {
*stat = fields.next()?.parse().ok()?;
}
fields.next().is_none().then_some((name, BaseStats(stats)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::MAX_DEX_NUMBER;
#[test]
fn every_row_of_the_table_parses() {
let rows = TABLE
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty());
for row in rows {
assert!(parse_row(row).is_some(), "malformed row: {row:?}");
}
}
#[test]
fn the_table_is_no_older_than_the_dex() {
let rows = TABLE
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.count();
assert!(rows >= MAX_DEX_NUMBER as usize);
assert!(lookup("bulbasaur").is_some());
}
#[test]
fn known_species_read_back_their_stats() {
let shuckle = lookup("shuckle").unwrap();
assert_eq!(shuckle.get(StatKey::Defense), 230);
assert_eq!(shuckle.get(StatKey::Speed), 5);
assert_eq!(lookup("ninjask").unwrap().get(StatKey::Speed), 160);
assert_eq!(lookup("blissey").unwrap().get(StatKey::Hp), 255);
}
#[test]
fn the_total_is_the_sum_of_the_six() {
assert_eq!(lookup("pikachu").unwrap().get(StatKey::Total), 320);
assert_eq!(lookup("arceus").unwrap().get(StatKey::Total), 720);
}
#[test]
fn alternate_forms_have_rows_of_their_own() {
let alola = lookup("raichu-alola").unwrap();
let kanto = lookup("raichu").unwrap();
assert_eq!(alola.get(StatKey::SpecialAttack), 95);
assert_eq!(kanto.get(StatKey::SpecialAttack), 90);
}
#[test]
fn an_unknown_name_has_no_stats() {
assert_eq!(lookup("missingno"), None);
assert_eq!(lookup(""), None);
}
#[test]
fn stat_keys_answer_to_short_and_long_names_in_any_case() {
assert_eq!(StatKey::parse("spe"), Some(StatKey::Speed));
assert_eq!(StatKey::parse("Speed"), Some(StatKey::Speed));
assert_eq!(StatKey::parse("BST"), Some(StatKey::Total));
assert_eq!(StatKey::parse("spdef"), Some(StatKey::SpecialDefense));
assert_eq!(StatKey::parse("luck"), None);
}
}