use bucket_brigade_core::{SCENARIOS, Scenario};
pub const DEFAULT_NUM_AGENTS: usize = 4;
#[derive(Debug, Clone, Copy)]
struct ScenarioVersion {
base: &'static str,
default_num_agents: usize,
}
const SCENARIO_VERSIONS: &[(&str, ScenarioVersion)] = &[
("default-v1", ScenarioVersion { base: "default", default_num_agents: 4 }),
("hard-v1", ScenarioVersion { base: "hard", default_num_agents: 4 }),
(
"trivial_cooperation-v1",
ScenarioVersion { base: "trivial_cooperation", default_num_agents: 4 },
),
(
"early_containment-v1",
ScenarioVersion { base: "early_containment", default_num_agents: 4 },
),
(
"greedy_neighbor-v1",
ScenarioVersion { base: "greedy_neighbor", default_num_agents: 4 },
),
(
"sparse_heroics-v1",
ScenarioVersion { base: "sparse_heroics", default_num_agents: 4 },
),
("rest_trap-v1", ScenarioVersion { base: "rest_trap", default_num_agents: 4 }),
(
"chain_reaction-v1",
ScenarioVersion { base: "chain_reaction", default_num_agents: 4 },
),
(
"deceptive_calm-v1",
ScenarioVersion { base: "deceptive_calm", default_num_agents: 4 },
),
(
"overcrowding-v1",
ScenarioVersion { base: "overcrowding", default_num_agents: 4 },
),
(
"mixed_motivation-v1",
ScenarioVersion { base: "mixed_motivation", default_num_agents: 4 },
),
(
"minimal_specialization-v1",
ScenarioVersion { base: "minimal_specialization", default_num_agents: 4 },
),
("v2_minimal-v1", ScenarioVersion { base: "v2_minimal", default_num_agents: 4 }),
];
pub fn list_versioned_scenarios() -> Vec<&'static str> {
let mut ids: Vec<&'static str> = SCENARIO_VERSIONS.iter().map(|(id, _)| *id).collect();
ids.sort_unstable();
ids
}
pub fn parse_scenario_id(scenario_id: &str) -> Result<(&str, u32), String> {
let Some(idx) = scenario_id.rfind("-v") else {
return Err(format!(
"Invalid scenario ID {scenario_id:?}: expected '<name>-v<int>' shape \
(e.g. 'minimal_specialization-v1')."
));
};
let (base, rest) = scenario_id.split_at(idx);
let version_str = &rest[2..];
if base.is_empty() || version_str.is_empty() {
return Err(format!(
"Invalid scenario ID {scenario_id:?}: expected '<name>-v<int>' shape with a \
non-empty base name and integer version."
));
}
let version: u32 = version_str.parse().map_err(|_| {
format!(
"Invalid scenario ID {scenario_id:?}: version suffix must parse as a u32 \
(e.g. 'v1', 'v2')."
)
})?;
Ok((base, version))
}
pub fn default_num_agents_for(scenario_id: &str) -> Option<usize> {
SCENARIO_VERSIONS.iter().find_map(|(id, v)| {
if *id == scenario_id {
Some(v.default_num_agents)
} else {
None
}
})
}
pub fn get_scenario_by_id(scenario_id: &str) -> Result<Scenario, String> {
let entry = SCENARIO_VERSIONS
.iter()
.find_map(|(id, v)| if *id == scenario_id { Some(v) } else { None })
.ok_or_else(|| {
let available = list_versioned_scenarios().join(", ");
format!("Unknown scenario ID {scenario_id:?}. Available IDs: {available}")
})?;
SCENARIOS.get(entry.base).cloned().ok_or_else(|| {
format!(
"Scenario ID {scenario_id:?} maps to base name {:?}, which is not present in \
bucket_brigade_core::SCENARIOS. The submodule and the Thrust registry are out of \
sync.",
entry.base
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_id() {
let (base, version) = parse_scenario_id("minimal_specialization-v1").unwrap();
assert_eq!(base, "minimal_specialization");
assert_eq!(version, 1);
}
#[test]
fn parse_id_without_version_errors() {
assert!(parse_scenario_id("minimal_specialization").is_err());
assert!(parse_scenario_id("minimal_specialization-").is_err());
assert!(parse_scenario_id("-v1").is_err());
assert!(parse_scenario_id("foo-vbar").is_err());
}
#[test]
fn parse_handles_dashes_in_base_name() {
let (base, version) = parse_scenario_id("foo-bar-v2").unwrap();
assert_eq!(base, "foo-bar");
assert_eq!(version, 2);
}
#[test]
fn registry_includes_minimal_specialization() {
let ids = list_versioned_scenarios();
assert!(ids.contains(&"minimal_specialization-v1"));
}
#[test]
fn registry_is_sorted() {
let ids = list_versioned_scenarios();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted);
}
#[test]
fn get_scenario_returns_minimal_specialization() {
let scenario = get_scenario_by_id("minimal_specialization-v1").unwrap();
assert_eq!(scenario.team_reward_house_survives, 10.0);
assert_eq!(scenario.team_penalty_house_burns, 10.0);
assert!(scenario.reward_own_house_survives.iter().all(|&v| v == 50.0));
assert!(scenario.penalty_own_house_burns.iter().all(|&v| v == 100.0));
}
#[test]
fn unknown_id_errors() {
let err = get_scenario_by_id("nonexistent-v1").unwrap_err();
assert!(err.contains("nonexistent-v1"));
assert!(err.contains("minimal_specialization-v1"));
}
#[test]
fn default_num_agents_lookup() {
assert_eq!(default_num_agents_for("minimal_specialization-v1"), Some(4));
assert_eq!(default_num_agents_for("nonexistent-v1"), None);
}
#[test]
fn every_registered_id_resolves() {
for id in list_versioned_scenarios() {
get_scenario_by_id(id)
.unwrap_or_else(|e| panic!("registry entry {id} does not resolve: {e}"));
}
}
}