use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReckoningState {
pub room_credibility: i32,
pub crowd_nerve: i32,
pub witness_integrity: i32,
pub evidence_continuity: i32,
pub procedural_control: i32,
pub sequence: Vec<ReckoningAction>,
pub turn: u32,
pub eli_acted: bool,
pub phase: ReckoningPhase,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReckoningPhase {
Opening,
Presentation,
Counterstrike,
EliAct,
Verdict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReckoningAction {
pub actor: String,
pub action_type: ReckoningActionType,
pub description: String,
pub effects: ReckoningEffects,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReckoningActionType {
PresentEvidence,
MedicalTestimony,
TerritorialTestimony,
StabilizeRoom,
SystemRead,
ContaminationTestimony,
EliDefiningAct,
OppositionStrike,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReckoningEffects {
pub credibility: i32,
pub crowd_nerve: i32,
pub witness_integrity: i32,
pub evidence_continuity: i32,
pub procedural_control: i32,
}
impl ReckoningState {
pub fn new(ch9_result: &str) -> Self {
let (cred, crowd, proc) = match ch9_result {
"switchback" => (60, 45, 65), "caldwell" => (55, 35, 50), "pine_signal" => (55, 50, 55), _ => (50, 40, 50), };
Self {
room_credibility: cred,
crowd_nerve: crowd,
witness_integrity: 70,
evidence_continuity: 75,
procedural_control: proc,
sequence: Vec::new(),
turn: 0,
eli_acted: false,
phase: ReckoningPhase::Opening,
}
}
pub fn execute_action(&mut self, action: ReckoningAction) {
self.turn += 1;
self.room_credibility = (self.room_credibility + action.effects.credibility).clamp(0, 100);
self.crowd_nerve = (self.crowd_nerve + action.effects.crowd_nerve).clamp(0, 100);
self.witness_integrity = (self.witness_integrity + action.effects.witness_integrity).clamp(0, 100);
self.evidence_continuity = (self.evidence_continuity + action.effects.evidence_continuity).clamp(0, 100);
self.procedural_control = (self.procedural_control + action.effects.procedural_control).clamp(0, 100);
if action.action_type == ReckoningActionType::EliDefiningAct {
self.eli_acted = true;
self.phase = ReckoningPhase::EliAct;
info!("Eli's defining act — Loyalty line unlocks");
}
debug!(
turn = self.turn,
credibility = self.room_credibility,
crowd = self.crowd_nerve,
witness = self.witness_integrity,
evidence = self.evidence_continuity,
procedure = self.procedural_control,
"reckoning action executed"
);
self.sequence.push(action);
}
pub fn advance_phase(&mut self) {
self.phase = match self.phase {
ReckoningPhase::Opening => ReckoningPhase::Presentation,
ReckoningPhase::Presentation => ReckoningPhase::Counterstrike,
ReckoningPhase::Counterstrike => {
if !self.eli_acted { ReckoningPhase::EliAct } else { ReckoningPhase::Verdict }
}
ReckoningPhase::EliAct => ReckoningPhase::Verdict,
ReckoningPhase::Verdict => ReckoningPhase::Verdict,
};
}
pub fn any_bar_critical(&self) -> bool {
self.room_credibility <= 10 ||
self.crowd_nerve <= 10 ||
self.procedural_control <= 10
}
pub fn overall_score(&self) -> i32 {
debug_assert!((0..=100).contains(&self.room_credibility), "room_credibility out of 0..=100: {}", self.room_credibility);
debug_assert!((0..=100).contains(&self.crowd_nerve), "crowd_nerve out of 0..=100: {}", self.crowd_nerve);
debug_assert!((0..=100).contains(&self.witness_integrity), "witness_integrity out of 0..=100: {}", self.witness_integrity);
debug_assert!((0..=100).contains(&self.evidence_continuity), "evidence_continuity out of 0..=100: {}", self.evidence_continuity);
debug_assert!((0..=100).contains(&self.procedural_control), "procedural_control out of 0..=100: {}", self.procedural_control);
(self.room_credibility + self.crowd_nerve + self.witness_integrity +
self.evidence_continuity + self.procedural_control) / 5
}
pub fn partial_victory(&self) -> bool {
self.overall_score() >= 40 && self.eli_acted && !self.any_bar_critical()
}
}
pub fn eli_defining_act() -> ReckoningAction {
ReckoningAction {
actor: "eli".to_string(),
action_type: ReckoningActionType::EliDefiningAct,
description: "Eli steps into the room's center, identifies himself as the man \
who took the ledger at Saint's Mile, and tells the truth in plain \
language that damages him as much as anyone else.".to_string(),
effects: ReckoningEffects {
credibility: 20, crowd_nerve: -10, witness_integrity: 15, evidence_continuity: 10, procedural_control: -5, ..Default::default()
},
}
}
pub fn galen_present_evidence() -> ReckoningAction {
ReckoningAction {
actor: "galen".to_string(),
action_type: ReckoningActionType::PresentEvidence,
description: "Galen presents evidence — timing and sequence.".to_string(),
effects: ReckoningEffects {
credibility: 8,
evidence_continuity: 10,
..Default::default()
},
}
}
pub fn ada_medical_testimony() -> ReckoningAction {
ReckoningAction {
actor: "ada".to_string(),
action_type: ReckoningActionType::MedicalTestimony,
description: "Ada makes bodies impossible to reduce to paperwork.".to_string(),
effects: ReckoningEffects {
credibility: 10,
witness_integrity: 8,
crowd_nerve: 5,
..Default::default()
},
}
}
pub fn rosa_territorial_testimony() -> ReckoningAction {
ReckoningAction {
actor: "rosa".to_string(),
action_type: ReckoningActionType::TerritorialTestimony,
description: "Rosa puts territorial consequence into the room.".to_string(),
effects: ReckoningEffects {
credibility: 7,
crowd_nerve: -3, ..Default::default()
},
}
}
pub fn miriam_stabilize() -> ReckoningAction {
ReckoningAction {
actor: "miriam".to_string(),
action_type: ReckoningActionType::StabilizeRoom,
description: "Miriam holds panic below ignition.".to_string(),
effects: ReckoningEffects {
crowd_nerve: 12,
procedural_control: 8,
..Default::default()
},
}
}
pub fn eli_system_read() -> ReckoningAction {
ReckoningAction {
actor: "eli".to_string(),
action_type: ReckoningActionType::SystemRead,
description: "Eli reads the procedural weak points.".to_string(),
effects: ReckoningEffects {
procedural_control: 10,
evidence_continuity: 5,
..Default::default()
},
}
}
pub fn opposition_strike() -> ReckoningAction {
ReckoningAction {
actor: "opposition".to_string(),
action_type: ReckoningActionType::OppositionStrike,
description: "The opposition pushes back — discredit, dismiss, agitate.".to_string(),
effects: ReckoningEffects {
credibility: -12,
crowd_nerve: -8,
witness_integrity: -10,
procedural_control: -5,
..Default::default()
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reckoning_state_tracks_five_bars() {
let state = ReckoningState::new("mixed");
assert!(state.room_credibility > 0);
assert!(state.crowd_nerve > 0);
assert!(state.witness_integrity > 0);
assert!(state.evidence_continuity > 0);
assert!(state.procedural_control > 0);
}
#[test]
fn ch9_results_change_opening() {
let switchback = ReckoningState::new("switchback");
let caldwell = ReckoningState::new("caldwell");
assert!(switchback.procedural_control > caldwell.procedural_control,
"switchback should give more procedural control");
assert!(caldwell.crowd_nerve < switchback.crowd_nerve,
"caldwell should make the crowd hotter");
}
#[test]
fn eli_act_is_the_hinge() {
let mut state = ReckoningState::new("mixed");
let cred_before = state.room_credibility;
state.execute_action(eli_defining_act());
assert!(state.room_credibility > cred_before,
"Eli's act should boost credibility");
assert!(state.eli_acted);
assert_eq!(state.phase, ReckoningPhase::EliAct);
}
#[test]
fn opposition_degrades_all_bars() {
let mut state = ReckoningState::new("mixed");
let score_before = state.overall_score();
state.execute_action(opposition_strike());
assert!(state.overall_score() < score_before,
"opposition should lower overall score");
}
#[test]
fn sequence_matters() {
let mut state_a = ReckoningState::new("mixed");
state_a.execute_action(galen_present_evidence());
state_a.execute_action(miriam_stabilize());
let mut state_b = ReckoningState::new("mixed");
state_b.execute_action(miriam_stabilize());
state_b.execute_action(galen_present_evidence());
assert_eq!(state_a.sequence.len(), 2);
assert_eq!(state_b.sequence.len(), 2);
assert_ne!(state_a.sequence[0].actor, state_b.sequence[0].actor);
}
}