diff --git a/src/simulator/crates/mc-magic/src/ascension.rs b/src/simulator/crates/mc-magic/src/ascension.rs new file mode 100644 index 00000000..c891cfa4 --- /dev/null +++ b/src/simulator/crates/mc-magic/src/ascension.rs @@ -0,0 +1,184 @@ +//! Arcane Ascension victory path — second victory condition alongside domination. +//! +//! Scaffold only: defines the requirement config (parsed from `ascension.json`), +//! the runtime state machine, and a pure detection function. Turn-processor +//! integration and UI wiring come in follow-up tasks. + +use serde::{Deserialize, Serialize}; + +/// Ritual lifecycle. Transitions: +/// `Inactive -> RequirementsMet -> Channeling -> (Complete | Failed)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AscensionPhase { + Inactive, + RequirementsMet, + Channeling, + Complete, + Failed, +} + +/// Data-driven thresholds parsed from `ascension.json`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AscensionRequirements { + pub school_wonders_built: u32, + pub schools_maxed: u32, + pub mana_accumulated: u32, + pub high_archon_alive: bool, + #[serde(default)] + pub required_tech: String, +} + +impl Default for AscensionRequirements { + fn default() -> Self { + Self { + school_wonders_built: 5, + schools_maxed: 5, + mana_accumulated: 500, + high_archon_alive: true, + required_tech: "arcane_ascension".to_string(), + } + } +} + +/// Per-player ascension tracking. Mutated each turn once the ritual is active. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AscensionState { + pub phase: AscensionPhase, + pub wonder_count: u32, + pub schools_maxed: u32, + pub mana_accumulated: u32, + pub high_archon_alive: bool, + pub has_required_tech: bool, + pub channel_turns_remaining: u32, +} + +impl Default for AscensionState { + fn default() -> Self { + Self { + phase: AscensionPhase::Inactive, + wonder_count: 0, + schools_maxed: 0, + mana_accumulated: 0, + high_archon_alive: false, + has_required_tech: false, + channel_turns_remaining: 0, + } + } +} + +/// Pure check: do the current inputs satisfy the ritual requirements? +pub fn requirements_met(state: &AscensionState, req: &AscensionRequirements) -> bool { + state.wonder_count >= req.school_wonders_built + && state.schools_maxed >= req.schools_maxed + && state.mana_accumulated >= req.mana_accumulated + && (!req.high_archon_alive || state.high_archon_alive) + && (req.required_tech.is_empty() || state.has_required_tech) +} + +/// Advance the phase based on current inputs. Returns the new phase. +/// Call once per turn during Phase 7 victory evaluation. +pub fn evaluate(state: &mut AscensionState, req: &AscensionRequirements) -> AscensionPhase { + match state.phase { + AscensionPhase::Inactive => { + if requirements_met(state, req) { + state.phase = AscensionPhase::RequirementsMet; + } + } + AscensionPhase::RequirementsMet => { + if !state.high_archon_alive && req.high_archon_alive { + state.phase = AscensionPhase::Failed; + } + } + AscensionPhase::Channeling => { + if !state.high_archon_alive && req.high_archon_alive { + state.phase = AscensionPhase::Failed; + } else if state.channel_turns_remaining == 0 { + state.phase = AscensionPhase::Complete; + } else { + state.channel_turns_remaining -= 1; + } + } + AscensionPhase::Complete | AscensionPhase::Failed => {} + } + state.phase +} + +/// Short stable tag for the victory-event payload. +pub fn victory_type_str() -> &'static str { + "arcane_ascension" +} + +#[cfg(test)] +mod tests { + use super::*; + + fn maxed_state() -> AscensionState { + AscensionState { + phase: AscensionPhase::Inactive, + wonder_count: 5, + schools_maxed: 5, + mana_accumulated: 500, + high_archon_alive: true, + has_required_tech: true, + channel_turns_remaining: 0, + } + } + + #[test] + fn default_requirements_parse_from_canonical_json() { + let raw = include_str!( + "../../../public/games/age-of-dwarves/data/ascension.json" + ); + let v: serde_json::Value = serde_json::from_str(raw).expect("ascension.json parses"); + let req: AscensionRequirements = + serde_json::from_value(v["requirements"].clone()).expect("requirements parse"); + assert_eq!(req.school_wonders_built, 5); + assert_eq!(req.mana_accumulated, 500); + assert_eq!(req.required_tech, "arcane_ascension"); + } + + #[test] + fn requirements_not_met_when_any_input_short() { + let req = AscensionRequirements::default(); + let mut state = maxed_state(); + assert!(requirements_met(&state, &req)); + + state.mana_accumulated = 499; + assert!(!requirements_met(&state, &req)); + state.mana_accumulated = 500; + + state.high_archon_alive = false; + assert!(!requirements_met(&state, &req)); + } + + #[test] + fn evaluate_transitions_inactive_to_requirements_met() { + let req = AscensionRequirements::default(); + let mut state = maxed_state(); + assert_eq!(evaluate(&mut state, &req), AscensionPhase::RequirementsMet); + } + + #[test] + fn channeling_counts_down_to_complete() { + let req = AscensionRequirements::default(); + let mut state = maxed_state(); + state.phase = AscensionPhase::Channeling; + state.channel_turns_remaining = 2; + assert_eq!(evaluate(&mut state, &req), AscensionPhase::Channeling); + assert_eq!(state.channel_turns_remaining, 1); + assert_eq!(evaluate(&mut state, &req), AscensionPhase::Channeling); + assert_eq!(state.channel_turns_remaining, 0); + assert_eq!(evaluate(&mut state, &req), AscensionPhase::Complete); + } + + #[test] + fn high_archon_death_during_channel_fails_ritual() { + let req = AscensionRequirements::default(); + let mut state = maxed_state(); + state.phase = AscensionPhase::Channeling; + state.channel_turns_remaining = 5; + state.high_archon_alive = false; + assert_eq!(evaluate(&mut state, &req), AscensionPhase::Failed); + } +} diff --git a/src/simulator/crates/mc-magic/src/lib.rs b/src/simulator/crates/mc-magic/src/lib.rs index 9a35fbc8..211bff19 100644 --- a/src/simulator/crates/mc-magic/src/lib.rs +++ b/src/simulator/crates/mc-magic/src/lib.rs @@ -1 +1,13 @@ -// TODO: mana economy, spells, Archons, enchantments, Ascension +//! Magic subsystem: mana economy, spells, Archons, enchantments, Ascension. +//! +//! Scaffold stage — only the Ascension victory path is materialised here. +//! Other modules (mana, spells, archons, enchantments) follow in subsequent +//! tasks per CLAUDE.md's atomic-porting protocol. + +pub mod ascension; + +pub use ascension::{ + evaluate as evaluate_ascension, requirements_met as ascension_requirements_met, + victory_type_str as ascension_victory_type_str, AscensionPhase, AscensionRequirements, + AscensionState, +};