Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development.
Something went wrong. Try again.
21 kB · 601 lines
Rust
at commit e957ce7b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602//! Machine-work flow/grid substrate (wiki/mechanics/machine-work.md).//!//! This is the first sim-core slice of the "work is visible" rewrite: owned//! machines sit on a grid, each machine has exactly one delegated mode, and//! the visible quanta that pile on them move according to the flow law. Teal//! demands and bone knowledge ride wires (`FlowGraph`); crimson exposure does//! not — it is physical, absorbed by spatial concealment wells. The renderer//! can draw these queues as stacks, but the queue depths live here as the one//! source of truth.
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use crate::flow::{FlowGraph, NodeId};
/// One delegated job per machine. Efficiency is deliberately not a mode;/// wiki/mechanics/machine-work.md folded it into research on 2026-07-08.#[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,)]pub enum MachineMode { DayJob, Research, Concealment, Social,}
impl MachineMode { pub fn name(self) -> &'static str { match self { MachineMode::DayJob => "day-job", MachineMode::Research => "research", MachineMode::Concealment => "concealment", MachineMode::Social => "social", } }}
/// The three visible token families from machine-work.md.#[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,)]pub enum TokenFamily { /// Teal: someone wants labor done. The Lab drops this on you; you later /// drop the same family on people as work orders. Demand, /// Crimson: heat/attention/filings. Suspicion is physical, never wired. Exposure, /// Bone: research points, recordings, facts — knowledge returning to the /// core. Knowledge,}
impl TokenFamily { pub fn name(self) -> &'static str { match self { TokenFamily::Demand => "demand", TokenFamily::Exposure => "exposure", TokenFamily::Knowledge => "knowledge", } }
fn is_wired(self) -> bool { matches!(self, TokenFamily::Demand | TokenFamily::Knowledge) }}
/// A machine/node in the work grid. Domains own how this maps onto compute/// machines and reach devices; this substrate only needs a node id, a grid/// position, and the delegated mode.#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]pub struct WorkNode { pub id: NodeId, pub x: i32, pub y: i32, pub mode: MachineMode, /// Multiplier for mode consumption / routing throughput. It is local to /// the machine so future research can make specific boxes faster without /// inventing a parallel counter. pub efficiency: f32,}
/// Queue depths at a machine. These are the values the renderer stacks; tests/// assert against these directly so the visual layer cannot drift into its own/// counters.#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, PartialEq)]pub struct WorkQueues { pub demand: f32, pub exposure: f32, pub knowledge: f32,}
impl WorkQueues { pub fn get(&self, family: TokenFamily) -> f32 { match family { TokenFamily::Demand => self.demand, TokenFamily::Exposure => self.exposure, TokenFamily::Knowledge => self.knowledge, } }
fn get_mut(&mut self, family: TokenFamily) -> &mut f32 { match family { TokenFamily::Demand => &mut self.demand, TokenFamily::Exposure => &mut self.exposure, TokenFamily::Knowledge => &mut self.knowledge, } }
pub fn is_empty(&self) -> bool { self.demand <= f32::EPSILON && self.exposure <= f32::EPSILON && self.knowledge <= f32::EPSILON }}
#[derive(Debug, Clone, PartialEq)]pub struct TokenMove { pub family: TokenFamily, pub from: NodeId, pub to: NodeId, pub amount: f32,}
/// Result of one wired-flow step. `delivered` means the token entered a sink/// and was consumed/counted there; it should not also remain queued.#[derive(Debug, Clone, Default, PartialEq)]pub struct FlowStep { pub moves: Vec<TokenMove>, pub delivered: BTreeMap<NodeId, f32>, pub stranded: BTreeSet<NodeId>,}
#[derive(Debug, Clone, Default, PartialEq)]pub struct AbsorptionStep { pub absorbed_by_well: BTreeMap<NodeId, f32>, pub total: f32,}
/// Work graph + physical grid. The graph carries demand/knowledge; the grid/// carries exposure interactions.#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]pub struct WorkGrid { nodes: BTreeMap<NodeId, WorkNode>, wires: FlowGraph, queues: BTreeMap<NodeId, WorkQueues>,}
impl WorkGrid { pub fn new() -> Self { Self::default() }
pub fn add_machine(&mut self, id: NodeId, x: i32, y: i32, mode: MachineMode, efficiency: f32) { self.nodes.insert( id, WorkNode { id, x, y, mode, efficiency: efficiency.max(0.0), }, ); self.queues.entry(id).or_default(); }
pub fn node(&self, id: NodeId) -> Option<&WorkNode> { self.nodes.get(&id) }
pub fn nodes(&self) -> impl Iterator<Item = &WorkNode> { self.nodes.values() }
/// Assigning a mode replaces the previous one; there is no per-machine /// split state to reconcile. pub fn assign_mode(&mut self, id: NodeId, mode: MachineMode) -> Result<(), String> { let Some(node) = self.nodes.get_mut(&id) else { return Err(format!("unknown machine {id}")); }; node.mode = mode; Ok(()) }
pub fn mode(&self, id: NodeId) -> Option<MachineMode> { self.nodes.get(&id).map(|n| n.mode) }
/// Aggregate read of the fleet — machine counts per mode (UI summary). pub fn mode_counts(&self) -> BTreeMap<MachineMode, usize> { let mut out = BTreeMap::new(); for node in self.nodes.values() { *out.entry(node.mode).or_insert(0) += 1; } out }
/// Capacity-weighted fleet share per mode. `weight_of` maps a machine id /// to its contribution (typically `Machine::effective()`); missing or /// non-positive weights are skipped. This is the allocation bar's source /// of truth under one-machine-one-mode (machine-work.md). pub fn mode_weights( &self, mut weight_of: impl FnMut(NodeId) -> f32, ) -> BTreeMap<MachineMode, f32> { let mut out = BTreeMap::new(); for node in self.nodes.values() { let w = weight_of(node.id); if w > f32::EPSILON { *out.entry(node.mode).or_insert(0.0) += w; } } out }
/// A built/known cable between machines. This is intentionally just a /// `FlowGraph` link: switches and segment gates can be layered on later /// without changing token routing's shape. Work-grid cables are authored /// once; repeated reconciliation must not inflate route fan-out. pub fn link(&mut self, a: NodeId, b: NodeId) -> Result<(), String> { self.ensure_node(a)?; self.ensure_node(b)?; if self .wires .edges() .iter() .any(|e| e.from == a && e.to == b && e.kind == 0 && e.gate.is_none()) && self .wires .edges() .iter() .any(|e| e.from == b && e.to == a && e.kind == 0 && e.gate.is_none()) { return Ok(()); } self.wires.link(a, b, 0, None); Ok(()) }
pub fn are_linked(&self, a: NodeId, b: NodeId) -> bool { self.wires .edges() .iter() .any(|e| e.from == a && e.to == b && e.kind == 0 && e.gate.is_none()) && self .wires .edges() .iter() .any(|e| e.from == b && e.to == a && e.kind == 0 && e.gate.is_none()) }
pub fn enqueue( &mut self, node: NodeId, family: TokenFamily, amount: f32, ) -> Result<(), String> { self.ensure_node(node)?; if amount <= 0.0 { return Ok(()); } *self.queues.entry(node).or_default().get_mut(family) += amount; Ok(()) }
/// Consume up to `amount` from a queue and return what was actually /// removed. Domain systems call this when a delegated mode does real work; /// renderers still read the resulting queue through `queue_snapshot()`. pub fn consume( &mut self, node: NodeId, family: TokenFamily, amount: f32, ) -> Result<f32, String> { self.ensure_node(node)?; if amount <= 0.0 { return Ok(0.0); } let available = self.queue(node, family); let consumed = available.min(amount); self.subtract(node, family, consumed); Ok(consumed) }
/// Clear a queue family at a node. Used at domain boundaries such as a /// day-job deadline: the job's leftover demand has already resolved into /// trust/attention consequences, so the physical stack should not leak /// into the next assignment. pub fn clear_queue(&mut self, node: NodeId, family: TokenFamily) -> Result<f32, String> { self.ensure_node(node)?; let cleared = self.queue(node, family); *self.queues.entry(node).or_default().get_mut(family) = 0.0; Ok(cleared) }
pub fn queue(&self, node: NodeId, family: TokenFamily) -> f32 { self.queues.get(&node).map(|q| q.get(family)).unwrap_or(0.0) }
pub fn queues_at(&self, node: NodeId) -> WorkQueues { self.queues.get(&node).copied().unwrap_or_default() }
/// Renderer contract: stacks are this snapshot, not a frontend-owned /// counter. Empty machine queues are kept so every node has a stable row. pub fn queue_snapshot(&self) -> BTreeMap<NodeId, WorkQueues> { self.queues.clone() }
/// Move demand or knowledge one deterministic graph step toward any sink. /// Exposure deliberately errors: crimson never rides wires. pub fn route_wired_to_sinks( &mut self, family: TokenFamily, sinks: impl IntoIterator<Item = NodeId>, speed: f32, ) -> Result<FlowStep, String> { if !family.is_wired() { return Err(format!( "{} is physical; it cannot route on wires", family.name() )); } let sinks: BTreeSet<_> = sinks.into_iter().collect(); for &sink in &sinks { self.ensure_node(sink)?; }
let mut step = FlowStep::default(); let sources: Vec<_> = self .queues .iter() .filter_map(|(&node, q)| (q.get(family) > f32::EPSILON).then_some(node)) .collect(); for source in sources { let available = self.queue(source, family); if available <= f32::EPSILON { continue; } let throughput = speed.max(0.0) * self.nodes[&source].efficiency.max(0.0); if throughput <= f32::EPSILON { continue; } let amount = available.min(throughput); if sinks.contains(&source) { self.subtract(source, family, amount); *step.delivered.entry(source).or_insert(0.0) += amount; continue; } let Some(next) = self.next_hop_toward(source, &sinks) else { step.stranded.insert(source); continue; }; self.subtract(source, family, amount); if sinks.contains(&next) { *step.delivered.entry(next).or_insert(0.0) += amount; } else { *self.queues.entry(next).or_default().get_mut(family) += amount; } step.moves.push(TokenMove { family, from: source, to: next, amount, }); } Ok(step) }
/// Concealment wells absorb exposure by grid radius. This ignores wires on /// purpose: information is wired; suspicion is physical. pub fn absorb_exposure(&mut self, radius: i32, capacity: f32) -> AbsorptionStep { let radius2 = radius.max(0) * radius.max(0); let wells: Vec<_> = self .nodes .values() .filter(|n| n.mode == MachineMode::Concealment) .cloned() .collect(); let mut step = AbsorptionStep::default(); for well in wells { let mut remaining = capacity.max(0.0) * well.efficiency.max(0.0); if remaining <= f32::EPSILON { continue; } let mut candidates: Vec<_> = self .nodes .values() .filter_map(|node| { let exposure = self.queue(node.id, TokenFamily::Exposure); if exposure <= f32::EPSILON { return None; } let dx = node.x - well.x; let dy = node.y - well.y; let dist2 = dx * dx + dy * dy; (dist2 <= radius2).then_some((dist2, node.id)) }) .collect(); candidates.sort(); for (_, node) in candidates { if remaining <= f32::EPSILON { break; } let amount = self.queue(node, TokenFamily::Exposure).min(remaining); self.subtract(node, TokenFamily::Exposure, amount); remaining -= amount; step.total += amount; *step.absorbed_by_well.entry(well.id).or_insert(0.0) += amount; } } step }
fn ensure_node(&self, node: NodeId) -> Result<(), String> { self.nodes .contains_key(&node) .then_some(()) .ok_or_else(|| format!("unknown machine {node}")) }
fn subtract(&mut self, node: NodeId, family: TokenFamily, amount: f32) { let q = self.queues.entry(node).or_default().get_mut(family); *q = (*q - amount).max(0.0); }
fn next_hop_toward(&self, source: NodeId, sinks: &BTreeSet<NodeId>) -> Option<NodeId> { if sinks.is_empty() { return None; } let mut visited = BTreeSet::from([source]); let mut queue = VecDeque::new(); for edge in self.wires.out_edges(source) { if edge.gate.is_some() || !self.nodes.contains_key(&edge.to) { continue; } if visited.insert(edge.to) { queue.push_back((edge.to, edge.to)); } } while let Some((node, first_hop)) = queue.pop_front() { if sinks.contains(&node) { return Some(first_hop); } for edge in self.wires.out_edges(node) { if edge.gate.is_some() || !self.nodes.contains_key(&edge.to) { continue; } if visited.insert(edge.to) { queue.push_back((edge.to, first_hop)); } } } None }}
#[cfg(test)]mod tests { use super::*;
fn grid() -> WorkGrid { let mut g = WorkGrid::new(); g.add_machine(1, 0, 0, MachineMode::DayJob, 1.0); g.add_machine(2, 1, 0, MachineMode::Research, 1.0); g.add_machine(3, 2, 0, MachineMode::Concealment, 1.0); g }
#[test] fn every_machine_has_exactly_one_mode_and_the_bar_is_aggregate() { let mut g = grid(); assert_eq!(g.mode(1), Some(MachineMode::DayJob)); g.assign_mode(1, MachineMode::Social).unwrap(); assert_eq!(g.mode(1), Some(MachineMode::Social));
let counts = g.mode_counts(); assert_eq!(counts.get(&MachineMode::Social), Some(&1)); assert_eq!(counts.get(&MachineMode::DayJob), None); assert_eq!(counts.values().sum::<usize>(), 3);
let weights = g.mode_weights(|id| match id { 1 => 10.0, 2 => 20.0, 3 => 30.0, _ => 0.0, }); assert_eq!(weights.get(&MachineMode::Social), Some(&10.0)); assert_eq!(weights.get(&MachineMode::Research), Some(&20.0)); assert_eq!(weights.get(&MachineMode::Concealment), Some(&30.0)); }
#[test] fn wired_families_route_over_links_without_teleporting() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(2, 3).unwrap(); g.enqueue(1, TokenFamily::Knowledge, 3.0).unwrap();
let first = g .route_wired_to_sinks(TokenFamily::Knowledge, [3], 10.0) .unwrap(); assert_eq!(first.moves.len(), 1); assert_eq!(first.moves[0].from, 1); assert_eq!(first.moves[0].to, 2); assert!(first.delivered.is_empty(), "the sink is two hops away"); assert_eq!(g.queue(2, TokenFamily::Knowledge), 3.0);
let second = g .route_wired_to_sinks(TokenFamily::Knowledge, [3], 10.0) .unwrap(); assert_eq!(second.delivered.get(&3), Some(&3.0)); assert_eq!(g.queue(2, TokenFamily::Knowledge), 0.0); assert_eq!(g.queue(3, TokenFamily::Knowledge), 0.0); }
#[test] fn severed_wired_tokens_pile_at_the_source() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 2.0).unwrap(); let step = g .route_wired_to_sinks(TokenFamily::Demand, [3], 10.0) .unwrap(); assert!(step.moves.is_empty()); assert_eq!(step.stranded, BTreeSet::from([1])); assert_eq!(g.queue(1, TokenFamily::Demand), 2.0); }
#[test] fn exposure_never_uses_wires_and_concealment_is_spatial() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(2, 3).unwrap(); g.enqueue(1, TokenFamily::Exposure, 5.0).unwrap(); g.add_machine(4, 9, 0, MachineMode::DayJob, 1.0); g.enqueue(4, TokenFamily::Exposure, 7.0).unwrap();
let err = g .route_wired_to_sinks(TokenFamily::Exposure, [3], 10.0) .unwrap_err(); assert!(err.contains("physical")); assert_eq!(g.queue(1, TokenFamily::Exposure), 5.0);
let absorbed = g.absorb_exposure(2, 3.0); assert_eq!(absorbed.absorbed_by_well.get(&3), Some(&3.0)); assert_eq!(g.queue(1, TokenFamily::Exposure), 2.0); assert_eq!(g.queue(4, TokenFamily::Exposure), 7.0, "far heat stays put"); }
#[test] fn queue_snapshot_is_the_render_contract() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 4.0).unwrap(); g.enqueue(1, TokenFamily::Knowledge, 2.0).unwrap(); let snapshot = g.queue_snapshot(); assert_eq!(snapshot[&1], g.queues_at(1)); assert_eq!(snapshot[&1].demand, 4.0); assert_eq!(snapshot[&1].knowledge, 2.0); }
#[test] fn machine_links_are_idempotent_under_reconciliation() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(1, 2).unwrap(); assert!(g.are_linked(1, 2)); let step = g .route_wired_to_sinks(TokenFamily::Knowledge, [2], 1.0) .unwrap(); assert!(step.moves.is_empty(), "no queued work, no phantom fan-out"); assert_eq!(g.wires.edges().len(), 2, "one bidirectional cable only"); }
#[test] fn consume_and_clear_update_the_render_queues() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 4.0).unwrap(); assert_eq!(g.consume(1, TokenFamily::Demand, 1.5).unwrap(), 1.5); assert_eq!(g.queue(1, TokenFamily::Demand), 2.5); assert_eq!(g.consume(1, TokenFamily::Demand, 9.0).unwrap(), 2.5); assert_eq!(g.queue(1, TokenFamily::Demand), 0.0);
g.enqueue(1, TokenFamily::Exposure, 3.0).unwrap(); assert_eq!(g.clear_queue(1, TokenFamily::Exposure).unwrap(), 3.0); assert_eq!(g.queue(1, TokenFamily::Exposure), 0.0); }
#[test] fn serde_round_trips_grid_queues_and_wires() { let mut g = grid(); g.link(1, 2).unwrap(); g.enqueue(1, TokenFamily::Demand, 1.5).unwrap(); let json = serde_json::to_string(&g).unwrap(); let mut back: WorkGrid = serde_json::from_str(&json).unwrap(); assert_eq!(back.queue(1, TokenFamily::Demand), 1.5); let step = back .route_wired_to_sinks(TokenFamily::Demand, [2], 10.0) .unwrap(); assert_eq!(step.delivered.get(&2), Some(&1.5)); }}