sb-api: label helpers wiring + gym/stat_value + policy constants (WI-2, issue #2)

This commit is contained in:
Jan-Peter von Hunnius 2026-10-03 12:08:21 +02:00
parent 5d5a594a7a
commit f0659421e7
7 changed files with 217 additions and 47 deletions

View file

@ -392,6 +392,27 @@ pub fn find_class_info(class: &Class) -> &'static ClassInfo {
.expect("invalid class")
}
/// Gym stat display order — pinned to the get_gym handler's fixed array
/// (routes/mod.rs:588–594), deliberately differs from TRAINING_GROUNDS order.
pub const GYM_STAT_DISPLAY_ORDER: &[&str] = &[
"strength",
"intelligence",
"dexterity",
"constitution",
"luck",
];
pub fn stat_value(stats: &CharacterStats, stat: &str) -> Option<i64> {
match stat {
"strength" => Some(stats.strength),
"intelligence" => Some(stats.intelligence),
"dexterity" => Some(stats.dexterity),
"constitution" => Some(stats.constitution),
"luck" => Some(stats.luck),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -631,4 +652,70 @@ mod tests {
assert_eq!(first.energy, second.energy);
assert_eq!(first.nerve, second.nerve);
}
#[test]
fn gym_display_order_covers_all_training_grounds() {
let stats = CharacterStats {
strength: 10,
intelligence: 11,
dexterity: 12,
constitution: 13,
luck: 14,
};
assert_eq!(GYM_STAT_DISPLAY_ORDER.len(), TRAINING_GROUNDS.len());
for name in GYM_STAT_DISPLAY_ORDER {
assert!(
TRAINING_GROUNDS.iter().any(|g| g.stat == *name),
"no training ground for stat {}",
name
);
assert!(
stat_value(&stats, name).is_some(),
"stat_value cannot resolve {}",
name
);
}
for ground in TRAINING_GROUNDS {
assert!(
GYM_STAT_DISPLAY_ORDER.contains(&ground.stat),
"gym display order missing stat {}",
ground.stat
);
}
}
#[test]
fn race_table_covers_all_variants() {
let all = [
Race::Human,
Race::Elf,
Race::Dwarf,
Race::Hobbit,
Race::HalfElf,
];
for race in &all {
assert!(RACES.iter().any(|r| &r.race == race));
let _ = find_race_info(race);
}
}
#[test]
fn class_table_covers_all_variants() {
let all = [
Class::Warrior,
Class::Paladin,
Class::Rogue,
Class::Bard,
Class::Hunter,
Class::Monk,
Class::Mage,
Class::Wizard,
Class::Conjurer,
Class::Sorcerer,
];
for class in &all {
assert!(CLASSES.iter().any(|c| &c.class == class));
let _ = find_class_info(class);
}
}
}

View file

@ -424,4 +424,9 @@ mod tests {
assert!(matches!(parse_attack_type("unarmed"), AttackType::Crush));
assert!(matches!(parse_attack_type("unknown"), AttackType::Crush));
}
#[test]
fn crit_tables_parse() {
assert!(!CRIT_TABLES.is_empty());
}
}

View file

@ -59,3 +59,18 @@ pub fn load_enemies() -> Vec<EnemyDef> {
pub fn load_zones() -> Vec<ZoneDef> {
serde_json::from_str(ZONES_JSON).expect("failed to parse zones.json")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enemies_json_parses() {
assert!(!load_enemies().is_empty());
}
#[test]
fn zones_json_parses() {
assert!(!load_zones().is_empty());
}
}

View file

@ -29,3 +29,13 @@ pub fn item_price(item: &Item) -> i32 {
pub fn item_sell_price(item: &Item) -> i32 {
item.value / 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_items_json_parses() {
assert!(!load_all_items().is_empty());
}
}

View file

@ -110,4 +110,9 @@ mod tests {
);
}
}
#[test]
fn spells_json_parses() {
assert!(!load_spells().is_empty());
}
}