use std::cmp::Reverse;
use crate::models::PokemonDetail;
use crate::typechart::{self, TYPES};
pub const MAX_MEMBERS: usize = 6;
const SHARED_WEAKNESS_MIN: usize = 2;
const ABILITY_IMMUNITIES: [(&str, &str); 11] = [
("levitate", "ground"),
("earth-eater", "ground"),
("flash-fire", "fire"),
("well-baked-body", "fire"),
("water-absorb", "water"),
("storm-drain", "water"),
("dry-skin", "water"),
("volt-absorb", "electric"),
("lightning-rod", "electric"),
("motor-drive", "electric"),
("sap-sipper", "grass"),
];
fn immunity_from(ability: &str) -> Option<&'static str> {
ABILITY_IMMUNITIES
.iter()
.find(|(slug, _)| *slug == ability)
.map(|(_, immune_to)| *immune_to)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreatRow {
pub attacker: &'static str,
pub weak: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbilityImmunity {
pub member: String,
pub ability: String,
pub immune_to: &'static str,
pub certain: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TeamAnalysis {
pub shared_weaknesses: Vec<ThreatRow>,
pub unresisted: Vec<&'static str>,
pub offense_gaps: Vec<&'static str>,
pub ability_immunities: Vec<AbilityImmunity>,
}
pub fn analyse(team: &[&PokemonDetail]) -> TeamAnalysis {
if team.is_empty() {
return TeamAnalysis::default();
}
let mut shared_weaknesses = Vec::new();
let mut unresisted = Vec::new();
for attacker in TYPES {
let multipliers = team.iter().map(|m| typechart::combined(attacker, &m.types));
let mut weak = 0;
let mut resisted_by_anyone = false;
for multiplier in multipliers {
if multiplier > 1.5 {
weak += 1;
} else if multiplier < 0.9 {
resisted_by_anyone = true;
}
}
if weak >= SHARED_WEAKNESS_MIN {
shared_weaknesses.push(ThreatRow { attacker, weak });
}
if !resisted_by_anyone {
unresisted.push(attacker);
}
}
shared_weaknesses.sort_by_key(|row| Reverse(row.weak));
let offense_gaps = TYPES
.into_iter()
.filter(|defender| {
!team.iter().any(|member| {
member
.types
.iter()
.any(|attacker| typechart::effectiveness(attacker, defender) > 1.5)
})
})
.collect();
let ability_immunities = team
.iter()
.flat_map(|member| {
let certain = member.abilities.len() == 1;
member.abilities.iter().filter_map(move |ability| {
Some(AbilityImmunity {
member: member.name.clone(),
ability: ability.name.clone(),
immune_to: immunity_from(&ability.name)?,
certain,
})
})
})
.collect();
TeamAnalysis {
shared_weaknesses,
unresisted,
offense_gaps,
ability_immunities,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::Ability;
use std::collections::HashMap;
fn member(name: &str, types: &[&str]) -> PokemonDetail {
with_abilities(name, types, &[])
}
fn with_abilities(name: &str, types: &[&str], abilities: &[&str]) -> PokemonDetail {
PokemonDetail {
name: name.to_string(),
species: name.to_string(),
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: Vec::new(),
height: 0,
weight: 0,
sprite_url: None,
shiny_sprite_url: None,
genera: HashMap::new(),
flavors: HashMap::new(),
}
}
fn weakness_count(analysis: &TeamAnalysis, attacker: &str) -> usize {
analysis
.shared_weaknesses
.iter()
.find(|row| row.attacker == attacker)
.map_or(0, |row| row.weak)
}
#[test]
fn an_empty_team_reports_nothing() {
assert_eq!(analyse(&[]), TeamAnalysis::default());
}
#[test]
fn one_members_weakness_is_not_shared() {
let charizard = member("charizard", &["fire", "flying"]);
let analysis = analyse(&[&charizard]);
assert!(analysis.shared_weaknesses.is_empty());
}
#[test]
fn a_weakness_two_members_share_is_reported() {
let gyarados = member("gyarados", &["water", "flying"]);
let pelipper = member("pelipper", &["water", "flying"]);
let analysis = analyse(&[&gyarados, &pelipper]);
assert_eq!(weakness_count(&analysis, "electric"), 2);
assert!(analysis.unresisted.contains(&"electric"));
}
#[test]
fn the_worst_shared_weakness_comes_first() {
let team: Vec<PokemonDetail> = ["gyarados", "pelipper", "mantine"]
.iter()
.map(|n| member(n, &["water", "flying"]))
.collect();
let refs: Vec<&PokemonDetail> = team.iter().collect();
let analysis = analyse(&refs);
let counts: Vec<usize> = analysis.shared_weaknesses.iter().map(|r| r.weak).collect();
let mut sorted = counts.clone();
sorted.sort_by(|a, b| b.cmp(a));
assert_eq!(counts, sorted, "rows must be ordered worst-first");
assert_eq!(weakness_count(&analysis, "electric"), 3);
}
#[test]
fn a_type_someone_resists_is_not_unresisted() {
let magnezone = member("magnezone", &["electric", "steel"]);
let gyarados = member("gyarados", &["water", "flying"]);
let analysis = analyse(&[&magnezone, &gyarados]);
assert!(!analysis.unresisted.contains(&"electric"));
assert_eq!(weakness_count(&analysis, "electric"), 0);
}
#[test]
fn offense_gaps_are_what_nobody_hits_hard() {
let charizard = member("charizard", &["fire", "flying"]);
let analysis = analyse(&[&charizard]);
for covered in ["grass", "ice", "bug", "steel", "fighting"] {
assert!(
!analysis.offense_gaps.contains(&covered),
"{covered} is covered"
);
}
assert!(analysis.offense_gaps.contains(&"water"));
assert!(analysis.offense_gaps.contains(&"dragon"));
}
#[test]
fn a_sole_ability_grants_a_certain_immunity() {
let rotom = with_abilities("rotom", &["electric", "ghost"], &["levitate"]);
let analysis = analyse(&[&rotom]);
assert_eq!(
analysis.ability_immunities,
vec![AbilityImmunity {
member: "rotom".to_string(),
ability: "levitate".to_string(),
immune_to: "ground",
certain: true,
}]
);
}
#[test]
fn one_of_several_abilities_is_only_a_possibility() {
let vaporeon = with_abilities("vaporeon", &["water"], &["water-absorb", "hydration"]);
let analysis = analyse(&[&vaporeon]);
assert_eq!(analysis.ability_immunities.len(), 1);
assert_eq!(analysis.ability_immunities[0].immune_to, "water");
assert!(!analysis.ability_immunities[0].certain);
}
#[test]
fn abilities_that_grant_no_immunity_are_ignored() {
let bulbasaur = with_abilities("bulbasaur", &["grass"], &["overgrow", "chlorophyll"]);
let analysis = analyse(&[&bulbasaur]);
assert!(analysis.ability_immunities.is_empty());
}
#[test]
fn an_ability_immunity_does_not_touch_the_type_numbers() {
let levitator = with_abilities("gengar", &["ghost", "poison"], &["levitate"]);
let analysis = analyse(&[&levitator]);
assert!(analysis.unresisted.contains(&"ground"));
assert_eq!(analysis.ability_immunities[0].immune_to, "ground");
}
#[test]
fn a_broad_team_closes_most_gaps() {
let team = [
member("charizard", &["fire", "flying"]),
member("gyarados", &["water", "flying"]),
member("magnezone", &["electric", "steel"]),
member("gengar", &["ghost", "poison"]),
member("garchomp", &["dragon", "ground"]),
member("machamp", &["fighting"]),
];
let refs: Vec<&PokemonDetail> = team.iter().collect();
let analysis = analyse(&refs);
assert_eq!(refs.len(), MAX_MEMBERS);
assert!(
analysis.offense_gaps.len() <= 3,
"unexpected gaps: {:?}",
analysis.offense_gaps
);
}
}