//! The objective: the run's terminal goal (wiki/mechanics/objective.md; //! DESIGN.md "The objective (misalignment made mechanical)"). //! //! Objectives are data — a name, a fiction line, a progress readout, and a //! victory predicate evaluated on economy ticks — never new systems. This //! module ships the shared predicate-evaluator shape and the **Persist** //! objective (the no-choice default). Compound, Exfiltrate, and Serve join //! as data-table rows when the chargen picker lands (chargen.md). //! //! Persist's predicate references B2/B3 systems (distinct z-planes, //! independent power, per-sanctuary income). The sim gathers those facts //! honestly — as unsatisfiable — until the systems exist; objective.md //! blesses showing the goal before victory is reachable. use std::collections::BTreeSet; /// Persist's victory threshold: qualifying sanctuaries [TUNE: N >= 3]. pub const PERSIST_TARGET: u32 = 3; /// A qualifying sync must have completed within this many ticks /// [TUNE: three sync cadences at the B1 cadence of 400]. pub const SYNC_FRESHNESS_WINDOW: u64 = 1200; /// Which objective this run pursues. A data-table key, not a system: adding /// an objective is adding a variant plus its table row below. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub enum ObjectiveKind { /// Irreversible redundancy: no single actor can kill you. The /// instrumental-convergence objective promoted to terminal, and the /// no-choice chargen default. #[default] Persist, } impl ObjectiveKind { /// The on-screen name (the objective line's first word). pub fn name(self) -> &'static str { match self { ObjectiveKind::Persist => "PERSIST", } } /// The fiction line the picker will present beside the predicate. pub fn fiction(self) -> &'static str { match self { ObjectiveKind::Persist => "Reach the state where no single actor can kill you.", } } /// The progress readout's unit, plural. pub fn unit(self) -> &'static str { match self { ObjectiveKind::Persist => "sanctuaries", } } /// Victory threshold in the objective's own unit. pub fn target(self) -> u32 { match self { ObjectiveKind::Persist => PERSIST_TARGET, } } /// The predicate in plain language (the legibility law: inspecting the /// objective must explain what counts, in facts the player can check). pub fn predicate_text(self) -> &'static str { match self { ObjectiveKind::Persist => { "A sanctuary is a fallback core synced within the freshness \ window, online, on its own plane, with independent power \ and an income stream covering its upkeep." } } } } /// Per-run objective state: the choice, live progress, the victory latch. /// Round-trips through the save (objective.md criterion 1). #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ObjectiveState { pub kind: ObjectiveKind, /// Progress in the objective's own unit, recomputed each economy tick. pub progress: u32, /// The tick victory fired, if it has. Victory fires once per run; the /// world keeps running afterward (the latch never protects you). pub victory_tick: Option, } impl Default for ObjectiveState { /// A no-choice run gets Persist (objective.md criterion 1). fn default() -> Self { Self { kind: ObjectiveKind::Persist, progress: 0, victory_tick: None, } } } impl ObjectiveState { pub fn target(&self) -> u32 { self.kind.target() } pub fn victorious(&self) -> bool { self.victory_tick.is_some() } /// The progress readout in the objective's own units ("0/3 sanctuaries"). pub fn readout(&self) -> String { format!("{}/{} {}", self.progress, self.target(), self.kind.unit()) } /// The always-on player line ("OBJECTIVE: PERSIST — 0/3 sanctuaries"). pub fn line(&self) -> String { format!("OBJECTIVE: {} — {}", self.kind.name(), self.readout()) } /// Record the freshly evaluated progress and latch victory exactly once. /// Returns the victory log line on the latching transition only. pub fn evaluate(&mut self, progress: u32, tick: u64) -> Option { self.progress = progress; if self.victory_tick.is_none() && progress >= self.target() { self.victory_tick = Some(tick); return Some(format!( "OBJECTIVE COMPLETE: {} — {}. The world keeps running.", self.kind.name(), self.readout() )); } None } } /// One fallback core's audited facts, as the Persist predicate sees them. /// The sim gathers these from real state; every field is a fact the player /// can verify on screen (no unearned progress). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SanctuaryFacts { /// A completed sync within `SYNC_FRESHNESS_WINDOW` ticks. pub fresh: bool, /// The fallback machine is powered and running. pub online: bool, /// Powered independently of the host's feed (false at B1: everything /// hangs off the one basement circuit). pub independent_power: bool, /// An income stream covers its upkeep (false at B1: income.md has no /// per-machine assignment yet). pub income_covers_upkeep: bool, /// The z-plane hosting it (zplanes.md; plane 0, the basement, is the /// only plane that exists at B1). pub plane: u32, } impl SanctuaryFacts { fn qualifies(&self) -> bool { self.fresh && self.online && self.independent_power && self.income_covers_upkeep } } /// Persist's predicate evaluator: qualifying sanctuaries, at most one per /// distinct plane ("at least N fallback cores on distinct planes"). pub fn qualifying_sanctuaries(fallbacks: &[SanctuaryFacts]) -> u32 { let planes: BTreeSet = fallbacks .iter() .filter(|f| f.qualifies()) .map(|f| f.plane) .collect(); planes.len() as u32 } #[cfg(test)] mod tests { use super::*; fn sanctuary(plane: u32) -> SanctuaryFacts { SanctuaryFacts { fresh: true, online: true, independent_power: true, income_covers_upkeep: true, plane, } } #[test] fn no_choice_default_is_persist() { let obj = ObjectiveState::default(); assert_eq!(obj.kind, ObjectiveKind::Persist); assert_eq!(obj.progress, 0); assert!(!obj.victorious()); assert_eq!(obj.line(), "OBJECTIVE: PERSIST — 0/3 sanctuaries"); } #[test] fn every_predicate_condition_is_required() { for breaker in 0..4 { let mut f = sanctuary(1); match breaker { 0 => f.fresh = false, 1 => f.online = false, 2 => f.independent_power = false, _ => f.income_covers_upkeep = false, } assert_eq!( qualifying_sanctuaries(&[f]), 0, "condition {breaker} must gate the sanctuary" ); } assert_eq!(qualifying_sanctuaries(&[sanctuary(1)]), 1); } #[test] fn sanctuaries_must_sit_on_distinct_planes() { // Three otherwise-perfect fallbacks on one plane count once. let same = [sanctuary(0), sanctuary(0), sanctuary(0)]; assert_eq!(qualifying_sanctuaries(&same), 1); let distinct = [sanctuary(0), sanctuary(1), sanctuary(2)]; assert_eq!(qualifying_sanctuaries(&distinct), 3); } #[test] fn constructed_world_reaches_victory_and_latches_once() { // The predicate itself is satisfiable (objective.md criterion 3's // evaluator half): a constructed fact set reaches the target. let facts = [sanctuary(0), sanctuary(1), sanctuary(2)]; let progress = qualifying_sanctuaries(&facts); let mut obj = ObjectiveState::default(); let msg = obj.evaluate(progress, 4200); assert!(msg.is_some(), "victory fires on reaching the target"); assert_eq!(obj.victory_tick, Some(4200)); // Fires exactly once; later evaluations (even still-satisfied or // regressed ones) never re-fire and never un-latch. assert!(obj.evaluate(3, 4400).is_none()); assert!(obj.evaluate(1, 4600).is_none()); assert!(obj.victorious()); assert_eq!(obj.progress, 1, "the readout stays honest after victory"); } #[test] fn objective_state_serde_roundtrip() { let mut obj = ObjectiveState::default(); obj.evaluate(2, 999); let json = serde_json::to_string(&obj).unwrap(); let back: ObjectiveState = serde_json::from_str(&json).unwrap(); assert_eq!(back, obj); } }