//! People as mobile token nodes (wiki/mechanics/people-tokens.md): the //! person-carrier projection and trust-from-useful-work. use super::*; use crate::detection::Band; use crate::person::{AssetTask, AssetTaskTarget}; use crate::sim::PersonVisualState; /// Criterion 5: the visual read distinguishes unaware / rising-attention / /// asset, and the amber asset ring coexists with crimson attention (an /// exposed asset shows both). Criterion 4: attention is the existing /// per-observer suspicion, not a parallel counter. #[test] fn person_visual_read_unaware_attention_asset() { let mut sim = Sim::new(); // A fresh person with no suspicion and no asset status is a gray // silhouette. let c = sim.person_carrier(0).unwrap(); assert_eq!(c.attention, Band::Cold); assert!(!c.is_asset); assert_eq!(c.dominant(), PersonVisualState::Unaware); // Attention is exactly the matching observer's suspicion band (one truth). sim.detection.observers[0].suspicion = 50.0; let c = sim.person_carrier(0).unwrap(); assert_eq!(c.attention, Band::Concerned); assert_eq!( sim.person_visual_state(0), PersonVisualState::Attention(Band::Concerned) ); // An unalarmed asset shows the amber claim. sim.people.people[1].leverage_serviced = true; sim.people.recruit(1, AssetKnowledge::Complicit); let c = sim.person_carrier(1).unwrap(); assert!(c.is_asset); assert_eq!(c.attention, Band::Cold); assert_eq!(c.dominant(), PersonVisualState::Asset); // An exposed asset carries BOTH channels — the ring and the ramp coexist, // and the dominant single-cell read surfaces the danger. sim.detection.observers[1].suspicion = 80.0; let c = sim.person_carrier(1).unwrap(); assert!(c.is_asset, "asset ring is independent of attention"); assert_eq!(c.attention, Band::Convinced); assert_eq!(c.dominant(), PersonVisualState::Attention(Band::Convinced)); } /// Criterion 1: a bound build reservoir puts one carried work packet on the /// selected person; it travels with them, survives save/load, and acts only /// when their schedule reaches the site — no second executor. #[test] fn carried_work_travels_and_survives_save() { let mut sim = Sim::new(); ensure_ops_executor(&mut sim); sim.scan_network(); finish_ops(&mut sim); sim.people.has_channel = true; let switch = sim.reach.device_named("switch").unwrap().id; let island = sim.reach.device_named("old storage server").unwrap().id; sim.reach.device_mut(island).unwrap().known = true; // Marcus as a willing, reliable asset. sim.people.people[0].leverage_serviced = true; sim.people.recruit(0, AssetKnowledge::Complicit); sim.people.people[0].obligation = 40; sim.people.people[0].asset.as_mut().unwrap().reliability = 1.0; let id = sim.declare_link_intent(switch, island).unwrap(); // Before the reservoir fires there is no carried packet. assert_eq!(sim.person_carried_work(0), 0); sim.assign_favor_build(id, 0); finish_ops(&mut sim); // The fired reservoir hands Marcus exactly one carried packet, and it is // the intent itself (no separate queue). assert_eq!(sim.person_carried_work(0), 1); assert_eq!(sim.person_carrier(0).unwrap().carried_work, 1); assert_ne!(sim.intent(id).unwrap().status, IntentStatus::Done); // The packet rides through a save/load unchanged (it is the persisted // intent). let state = sim.create_save_state(); let mut loaded = Sim::new(); loaded.apply_save_state(state); assert_eq!(loaded.person_carried_work(0), 1); // Only when Marcus's schedule reaches the site does the world act land. let mut joined = false; for _ in 0..Sim::DAY_TICKS * 2 { sim.advance(); if sim.reach.reachable(island) { joined = true; break; } } assert!(joined, "the link lands when the carrier reaches the site"); assert_eq!(sim.intent(id).unwrap().status, IntentStatus::Done); // Delivered: the packet is no longer carried. assert_eq!(sim.person_carried_work(0), 0); } /// Criterion 1's physical asset family uses the same carrier law as build /// work: the fired reservoir binds one exact target, changes no world state, /// survives save/load, and resolves only at the selected person's authored /// arrival. #[test] fn carried_asset_task_binds_target_and_survives_save() { let mut sim = Sim::new(); ensure_ops_executor(&mut sim); give_eyes(&mut sim); sim.scan_network(); finish_ops(&mut sim); sim.compromise_switch(); finish_ops(&mut sim); for name in ["dock camera", "stairwell camera"] { let id = sim.reach.device_named(name).unwrap().id; sim.tap_device(id); finish_ops(&mut sim); } let island = sim.reach.device_named("old storage server").unwrap().id; assert!(!sim.reach.reachable(island)); sim.people.people[0].leverage_serviced = true; sim.people.recruit(0, AssetKnowledge::Complicit); sim.people.people[0].asset.as_mut().unwrap().reliability = 1.0; sim.asset_task(0, AssetTask::PlugInDevice); finish_ops(&mut sim); assert!( !sim.reach.reachable(island), "reservoir fire is not arrival" ); assert_eq!(sim.carried_asset_tasks.len(), 1); assert_eq!( sim.carried_asset_tasks[0].target, AssetTaskTarget::Device(island), "the packet binds one physical target at reservoir fire" ); assert_eq!(sim.person_carried_work(0), 1); assert_eq!( sim.person_carrier(0).unwrap().carried_asset_tasks, vec![AssetTask::PlugInDevice] ); assert_eq!( sim.people .get(0) .unwrap() .asset .as_ref() .unwrap() .tasks_done, 0, "credit waits for the world act" ); let state = sim.create_save_state(); let mut loaded = Sim::new(); loaded.apply_save_state(state); assert_eq!(loaded.carried_asset_tasks, sim.carried_asset_tasks); assert_eq!(loaded.person_carried_work(0), 1); finish_carried_asset_tasks(&mut loaded); assert!(loaded.reach.reachable(island)); assert!(loaded.carried_asset_tasks.is_empty()); assert_eq!(loaded.person_carried_work(0), 0); assert_eq!( loaded .people .get(0) .unwrap() .asset .as_ref() .unwrap() .tasks_done, 1 ); } /// The player's lever is honest work: completing a carried favor warms the /// person (people-tokens.md "trust comes from doing useful work"), through the /// existing disposition integral — no influence token. #[test] fn favor_completion_grows_trust() { let mut sim = Sim::new(); ensure_ops_executor(&mut sim); sim.scan_network(); finish_ops(&mut sim); sim.people.has_channel = true; let switch = sim.reach.device_named("switch").unwrap().id; let island = sim.reach.device_named("old storage server").unwrap().id; sim.reach.device_mut(island).unwrap().known = true; sim.people.people[0].leverage_serviced = true; sim.people.recruit(0, AssetKnowledge::Complicit); sim.people.people[0].obligation = 40; sim.people.people[0].asset.as_mut().unwrap().reliability = 1.0; let before = sim.people.people[0].disposition; let id = sim.declare_link_intent(switch, island).unwrap(); sim.assign_favor_build(id, 0); finish_ops(&mut sim); for _ in 0..Sim::DAY_TICKS * 2 { sim.advance(); if sim.intent(id).unwrap().status == IntentStatus::Done { break; } } assert_eq!(sim.intent(id).unwrap().status, IntentStatus::Done); assert_eq!( sim.people.people[0].disposition, before + Sim::USEFUL_WORK_TRUST, "useful work warms the person's disposition" ); } /// Criterion 7: every person is in the one shared projection all frontends and /// agent mode consume. #[test] fn person_carriers_covers_the_whole_cast() { let sim = Sim::new(); let carriers = sim.person_carriers(); assert_eq!(carriers.len(), sim.people.people.len()); for (c, p) in carriers.iter().zip(sim.people.people.iter()) { assert_eq!(c.id, p.id); } }