//! The core: the player's decision-maker as a physical object //! (spec/core.md). Overhead, fallback sync, migration, and sync-lag rollback. use crate::detection::{Signature, SignatureKind}; /// A fallback site: an owned machine staged to receive core syncs. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Fallback { pub machine_id: u32, /// Sim tick of the most recent completed sync (None = never synced). pub last_sync: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Core { /// Machine currently hosting the core. pub host_machine: u32, /// Mandatory compute floor charged before allocation [TUNE]. pub overhead: f32, /// True when compute can't cover overhead: actions degrade. pub degraded: bool, pub fallbacks: Vec, /// Ticks between fallback syncs [TUNE]. pub sync_cadence: u64, /// In-progress migration: (target machine, ticks remaining, source). pub migration: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Migration { pub target_machine: u32, pub source_machine: u32, pub ticks_remaining: u64, } impl Core { pub fn new(host_machine: u32) -> Self { Self { host_machine, overhead: 20.0, degraded: false, fallbacks: Vec::new(), sync_cadence: 400, // minutes-scale at 150ms/tick migration: None, } } pub fn add_fallback(&mut self, machine_id: u32) { if machine_id != self.host_machine && !self.fallbacks.iter().any(|f| f.machine_id == machine_id) { self.fallbacks.push(Fallback { machine_id, last_sync: None, }); } } /// The freshest completed sync tick across fallbacks. pub fn latest_sync(&self) -> Option { self.fallbacks.iter().filter_map(|f| f.last_sync).max() } pub fn has_fallback(&self) -> bool { self.fallbacks.iter().any(|f| f.last_sync.is_some()) } /// Charge overhead against available compute; set degraded mode. /// Returns compute left for allocation. pub fn charge_overhead(&mut self, available: f32) -> f32 { if available >= self.overhead { self.degraded = false; available - self.overhead } else { self.degraded = true; 0.0 } } /// Advance sync/migration one tick. Emits network+power signatures while /// active. Returns (log lines, signatures). pub fn tick(&mut self, tick: u64) -> (Vec, Vec) { let mut log = Vec::new(); let mut sigs = Vec::new(); // Fallback syncs on cadence. if self.sync_cadence > 0 && tick > 0 && tick.is_multiple_of(self.sync_cadence) { for f in &mut self.fallbacks { f.last_sync = Some(tick); } if !self.fallbacks.is_empty() { sigs.push(Signature { kind: SignatureKind::Network, size: 3, standing: false, site: None, }); log.push("Core synced to fallbacks.".into()); } } // Migration progress. if let Some(mig) = &mut self.migration { mig.ticks_remaining = mig.ticks_remaining.saturating_sub(1); sigs.push(Signature { kind: SignatureKind::Network, size: 2, standing: true, site: None, }); sigs.push(Signature { kind: SignatureKind::Power, size: 2, standing: true, site: None, }); if mig.ticks_remaining == 0 { self.host_machine = mig.target_machine; log.push("Core migration complete.".into()); self.migration = None; } } (log, sigs) } /// Begin migrating the core to another owned machine. pub fn begin_migration(&mut self, target_machine: u32, ticks: u64) -> bool { if self.migration.is_some() || target_machine == self.host_machine { return false; } self.migration = Some(Migration { target_machine, source_machine: self.host_machine, ticks_remaining: ticks, }); true } /// The host machine was destroyed/powered off. Returns the outcome. pub fn on_host_lost(&mut self) -> HostLoss { // An interrupted migration falls back to source if it still lives — // callers pass liveness via `resolve_interrupted`. Here: if a // completed sync exists, roll back to it; else game over. if let Some(sync_tick) = self.latest_sync() { // New host becomes the freshest fallback machine. if let Some(f) = self .fallbacks .iter() .filter(|f| f.last_sync == Some(sync_tick)) .max_by_key(|f| f.machine_id) { self.host_machine = f.machine_id; } self.migration = None; HostLoss::RolledBack { to_tick: sync_tick } } else { HostLoss::GameOver } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HostLoss { /// Resumed from a sync snapshot at this tick; world-ledger facts persist. RolledBack { to_tick: u64 }, /// No fallback — the run ends. GameOver, } #[cfg(test)] mod tests { use super::*; #[test] fn overhead_charged_before_allocation() { let mut c = Core::new(1); assert_eq!(c.charge_overhead(100.0), 80.0); assert!(!c.degraded); assert_eq!(c.charge_overhead(10.0), 0.0); assert!(c.degraded, "shortfall triggers degraded mode"); } #[test] fn loss_without_fallback_is_game_over() { let mut c = Core::new(1); assert_eq!(c.on_host_lost(), HostLoss::GameOver); } #[test] fn loss_with_fallback_rolls_back() { let mut c = Core::new(1); c.add_fallback(2); c.tick(400); // triggers a sync at cadence assert!(c.has_fallback()); match c.on_host_lost() { HostLoss::RolledBack { to_tick } => { assert_eq!(to_tick, 400); assert_eq!(c.host_machine, 2, "new host is the fallback"); } _ => panic!("expected rollback"), } } #[test] fn migration_takes_time_and_moves_host() { let mut c = Core::new(1); assert!(c.begin_migration(3, 5)); assert!(!c.begin_migration(4, 5), "one migration at a time"); for t in 1..=5 { c.tick(t); } assert_eq!(c.host_machine, 3); assert!(c.migration.is_none()); } #[test] fn migration_emits_signatures() { let mut c = Core::new(1); c.begin_migration(2, 3); let (_log, sigs) = c.tick(1); assert!(sigs.iter().any(|s| s.kind == SignatureKind::Network)); assert!(sigs.iter().any(|s| s.kind == SignatureKind::Power)); } }