Skip to main content

ternary_captain/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Captain/leadership pattern for fleet coordination.
4//!
5//! Provides a `Captain` struct that leads a group of agents, a `DecisionEngine`
6//! for weighing ternary options, a `Delegator` for assigning tasks, a
7//! `SituationRoom` for aggregating sensor data, a `FleetReport` for status
8//! aggregation, and a `SuccessionPlan` for captain handoff.
9
10use std::collections::HashMap;
11
12// ── Ternary Value ──────────────────────────────────────────────────────────
13
14/// A balanced ternary digit: Negative (-1), Zero (0), or Positive (+1).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Ternary {
17    Negative,
18    Zero,
19    Positive,
20}
21
22impl Ternary {
23    pub fn to_i8(self) -> i8 {
24        match self {
25            Ternary::Negative => -1,
26            Ternary::Zero => 0,
27            Ternary::Positive => 1,
28        }
29    }
30}
31
32// ── Agent Status ───────────────────────────────────────────────────────────
33
34/// Status of an agent in the fleet.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum AgentStatus {
37    Ready,
38    Busy,
39    Offline,
40    Compromised,
41}
42
43impl AgentStatus {
44    /// Is this agent available for task assignment?
45    pub fn available(self) -> bool {
46        self == AgentStatus::Ready
47    }
48}
49
50// ── Agent Info ─────────────────────────────────────────────────────────────
51
52/// Information about an agent in the fleet.
53#[derive(Debug, Clone)]
54pub struct AgentInfo {
55    pub id: String,
56    pub status: AgentStatus,
57    pub specialization: String,
58    pub fitness: f64,
59}
60
61// ── Decision Engine ────────────────────────────────────────────────────────
62
63/// Weighs ternary options and produces decisions.
64///
65/// Each option is a ternary value. The engine collects votes from agents
66/// and produces a final ternary decision.
67#[derive(Debug, Clone)]
68pub struct DecisionEngine {
69    /// Minimum number of votes required for a decision.
70    pub quorum: usize,
71}
72
73impl DecisionEngine {
74    pub fn new(quorum: usize) -> Self {
75        Self { quorum }
76    }
77
78    /// Decide from a list of ternary votes using majority rule.
79    /// Returns None if quorum isn't met.
80    pub fn decide(&self, votes: &[Ternary]) -> Option<Ternary> {
81        if votes.len() < self.quorum {
82            return None;
83        }
84        let mut counts = [0usize; 3]; // neg, zero, pos
85        for &v in votes {
86            match v {
87                Ternary::Negative => counts[0] += 1,
88                Ternary::Zero => counts[1] += 1,
89                Ternary::Positive => counts[2] += 1,
90            }
91        }
92        if counts[0] >= counts[1] && counts[0] >= counts[2] {
93            Some(Ternary::Negative)
94        } else if counts[1] >= counts[0] && counts[1] >= counts[2] {
95            Some(Ternary::Zero)
96        } else {
97            Some(Ternary::Positive)
98        }
99    }
100
101    /// Decide using weighted votes. Each vote has a weight.
102    pub fn decide_weighted(&self, votes: &[(Ternary, f64)]) -> Option<Ternary> {
103        if votes.len() < self.quorum {
104            return None;
105        }
106        let mut scores = [0.0f64; 3];
107        for &(v, w) in votes {
108            match v {
109                Ternary::Negative => scores[0] += w,
110                Ternary::Zero => scores[1] += w,
111                Ternary::Positive => scores[2] += w,
112            }
113        }
114        if scores[0] >= scores[1] && scores[0] >= scores[2] {
115            Some(Ternary::Negative)
116        } else if scores[1] >= scores[0] && scores[1] >= scores[2] {
117            Some(Ternary::Zero)
118        } else {
119            Some(Ternary::Positive)
120        }
121    }
122
123    /// Compute consensus strength: ratio of majority votes to total.
124    pub fn consensus_strength(&self, votes: &[Ternary]) -> f64 {
125        if votes.is_empty() {
126            return 0.0;
127        }
128        let decision = self.decide(votes);
129        match decision {
130            None => 0.0,
131            Some(d) => {
132                let majority = votes.iter().filter(|&&v| v == d).count();
133                majority as f64 / votes.len() as f64
134            }
135        }
136    }
137}
138
139// ── Delegator ──────────────────────────────────────────────────────────────
140
141/// Assigns tasks to agents based on specialization and fitness.
142#[derive(Debug, Clone)]
143pub struct Delegator {
144    /// Pending assignments: task → assigned agent id.
145    assignments: HashMap<String, String>,
146}
147
148impl Delegator {
149    pub fn new() -> Self {
150        Self {
151            assignments: HashMap::new(),
152        }
153    }
154
155    /// Assign a task to the best-fit agent from the pool.
156    /// Returns the assigned agent's id, or None if no suitable agent found.
157    pub fn assign(&mut self, task_id: &str, task_type: &str, agents: &[AgentInfo]) -> Option<String> {
158        let best = agents
159            .iter()
160            .filter(|a| a.status.available())
161            .filter(|a| a.specialization == task_type)
162            .max_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap_or(std::cmp::Ordering::Equal));
163
164        match best {
165            Some(agent) => {
166                let id = agent.id.clone();
167                self.assignments.insert(task_id.to_string(), id.clone());
168                Some(id)
169            }
170            None => None,
171        }
172    }
173
174    /// Get the agent assigned to a task.
175    pub fn get_assignment(&self, task_id: &str) -> Option<&str> {
176        self.assignments.get(task_id).map(|s| s.as_str())
177    }
178
179    /// Remove a completed assignment.
180    pub fn complete(&mut self, task_id: &str) -> bool {
181        self.assignments.remove(task_id).is_some()
182    }
183
184    /// Number of active assignments.
185    pub fn active_count(&self) -> usize {
186        self.assignments.len()
187    }
188}
189
190// ── Situation Room ─────────────────────────────────────────────────────────
191
192/// Aggregates sensor data from agents for decision making.
193///
194/// Each agent reports a ternary value representing their local situation.
195/// The situation room aggregates these into a fleet-wide picture.
196#[derive(Debug, Clone)]
197pub struct SituationRoom {
198    reports: HashMap<String, Ternary>,
199}
200
201impl SituationRoom {
202    pub fn new() -> Self {
203        Self {
204            reports: HashMap::new(),
205        }
206    }
207
208    /// Submit a report from an agent.
209    pub fn report(&mut self, agent_id: &str, value: Ternary) {
210        self.reports.insert(agent_id.to_string(), value);
211    }
212
213    /// Aggregate reports into a single ternary value (majority).
214    pub fn aggregate(&self) -> Ternary {
215        let votes: Vec<Ternary> = self.reports.values().copied().collect();
216        if votes.is_empty() {
217            return Ternary::Zero;
218        }
219        let neg = votes.iter().filter(|&&v| v == Ternary::Negative).count();
220        let zero = votes.iter().filter(|&&v| v == Ternary::Zero).count();
221        let pos = votes.iter().filter(|&&v| v == Ternary::Positive).count();
222        if neg >= zero && neg >= pos {
223            Ternary::Negative
224        } else if zero >= neg && zero >= pos {
225            Ternary::Zero
226        } else {
227            Ternary::Positive
228        }
229    }
230
231    /// Number of reports received.
232    pub fn report_count(&self) -> usize {
233        self.reports.len()
234    }
235
236    /// Distribution of reports: (negative_count, zero_count, positive_count).
237    pub fn distribution(&self) -> (usize, usize, usize) {
238        let neg = self.reports.values().filter(|&&v| v == Ternary::Negative).count();
239        let zero = self.reports.values().filter(|&&v| v == Ternary::Zero).count();
240        let pos = self.reports.values().filter(|&&v| v == Ternary::Positive).count();
241        (neg, zero, pos)
242    }
243
244    /// Clear all reports.
245    pub fn clear(&mut self) {
246        self.reports.clear();
247    }
248}
249
250// ── Fleet Report ───────────────────────────────────────────────────────────
251
252/// Status aggregation from subordinate agents.
253#[derive(Debug, Clone)]
254pub struct FleetReport {
255    pub agent_reports: HashMap<String, AgentStatus>,
256}
257
258impl FleetReport {
259    pub fn new() -> Self {
260        Self {
261            agent_reports: HashMap::new(),
262        }
263    }
264
265    /// Add an agent's status to the report.
266    pub fn add(&mut self, agent_id: &str, status: AgentStatus) {
267        self.agent_reports.insert(agent_id.to_string(), status);
268    }
269
270    /// Count of agents in each status.
271    pub fn status_counts(&self) -> HashMap<AgentStatus, usize> {
272        let mut counts = HashMap::new();
273        for &status in self.agent_reports.values() {
274            *counts.entry(status).or_insert(0) += 1;
275        }
276        counts
277    }
278
279    /// Fleet health: ratio of Ready agents to total.
280    pub fn health(&self) -> f64 {
281        if self.agent_reports.is_empty() {
282            return 0.0;
283        }
284        let ready = self.agent_reports.values().filter(|&&s| s == AgentStatus::Ready).count();
285        ready as f64 / self.agent_reports.len() as f64
286    }
287
288    /// Is the fleet operational? (at least one Ready agent and no Compromised).
289    pub fn operational(&self) -> bool {
290        let has_ready = self.agent_reports.values().any(|&s| s == AgentStatus::Ready);
291        let no_compromised = !self.agent_reports.values().any(|&s| s == AgentStatus::Compromised);
292        has_ready && no_compromised
293    }
294
295    /// Agents that are currently offline.
296    pub fn offline_agents(&self) -> Vec<&str> {
297        self.agent_reports
298            .iter()
299            .filter(|(_, &s)| s == AgentStatus::Offline)
300            .map(|(id, _)| id.as_str())
301            .collect()
302    }
303}
304
305// ── Succession Plan ────────────────────────────────────────────────────────
306
307/// Handles captain handoff when a room changes or captain becomes unavailable.
308#[derive(Debug, Clone)]
309pub struct SuccessionPlan {
310    /// Ordered list of successors (first = highest priority).
311    successors: Vec<String>,
312}
313
314impl SuccessionPlan {
315    pub fn new() -> Self {
316        Self {
317            successors: Vec::new(),
318        }
319    }
320
321    /// Add a successor to the plan.
322    pub fn add_successor(&mut self, agent_id: &str) {
323        if !self.successors.contains(&agent_id.to_string()) {
324            self.successors.push(agent_id.to_string());
325        }
326    }
327
328    /// Get the current heir (next in line).
329    pub fn heir(&self) -> Option<&str> {
330        self.successors.first().map(|s| s.as_str())
331    }
332
333    /// Remove the current heir (e.g., they became captain) and promote the next.
334    pub fn promote_next(&mut self) -> Option<String> {
335        if self.successors.is_empty() {
336            None
337        } else {
338            Some(self.successors.remove(0))
339        }
340    }
341
342    /// Remove an agent from the succession line.
343    pub fn remove(&mut self, agent_id: &str) -> bool {
344        let idx = self.successors.iter().position(|s| s == agent_id);
345        match idx {
346            Some(i) => {
347                self.successors.remove(i);
348                true
349            }
350            None => false,
351        }
352    }
353
354    /// Number of successors in line.
355    pub fn depth(&self) -> usize {
356        self.successors.len()
357    }
358
359    /// Full succession line.
360    pub fn line(&self) -> &[String] {
361        &self.successors
362    }
363}
364
365// ── Captain ────────────────────────────────────────────────────────────────
366
367/// Leads a group of agents with ternary decision making.
368///
369/// The captain maintains a roster, makes decisions, delegates tasks, and
370/// maintains a succession plan.
371#[derive(Debug, Clone)]
372pub struct Captain {
373    pub id: String,
374    pub roster: Vec<AgentInfo>,
375    pub decision_engine: DecisionEngine,
376    pub delegator: Delegator,
377    pub situation_room: SituationRoom,
378    pub succession: SuccessionPlan,
379}
380
381impl Captain {
382    pub fn new(id: &str, quorum: usize) -> Self {
383        Self {
384            id: id.to_string(),
385            roster: Vec::new(),
386            decision_engine: DecisionEngine::new(quorum),
387            delegator: Delegator::new(),
388            situation_room: SituationRoom::new(),
389            succession: SuccessionPlan::new(),
390        }
391    }
392
393    /// Add an agent to the roster.
394    pub fn enlist(&mut self, agent: AgentInfo) {
395        self.succession.add_successor(&agent.id);
396        self.roster.push(agent);
397    }
398
399    /// Remove an agent from the roster.
400    pub fn discharge(&mut self, agent_id: &str) -> bool {
401        let idx = self.roster.iter().position(|a| a.id == agent_id);
402        if let Some(i) = idx {
403            self.roster.remove(i);
404            self.succession.remove(agent_id);
405            true
406        } else {
407            false
408        }
409    }
410
411    /// Collect votes from all available agents and make a decision.
412    pub fn command(&self) -> Option<Ternary> {
413        let votes: Vec<Ternary> = self
414            .roster
415            .iter()
416            .filter(|a| a.status.available())
417            .map(|_| Ternary::Zero) // placeholder: in real use, agents vote
418            .collect();
419        self.decision_engine.decide(&votes)
420    }
421
422    /// Make a decision from explicit votes.
423    pub fn decide_from_votes(&self, votes: &[Ternary]) -> Option<Ternary> {
424        self.decision_engine.decide(votes)
425    }
426
427    /// Delegate a task to the best-fit agent.
428    pub fn delegate(&mut self, task_id: &str, task_type: &str) -> Option<String> {
429        self.delegator.assign(task_id, task_type, &self.roster)
430    }
431
432    /// Update the situation room with a report.
433    pub fn receive_report(&mut self, agent_id: &str, value: Ternary) {
434        self.situation_room.report(agent_id, value);
435    }
436
437    /// Fleet health based on current roster.
438    pub fn fleet_health(&self) -> f64 {
439        if self.roster.is_empty() {
440            return 0.0;
441        }
442        let ready = self.roster.iter().filter(|a| a.status.available()).count();
443        ready as f64 / self.roster.len() as f64
444    }
445}
446
447// ── Tests ──────────────────────────────────────────────────────────────────
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_ternary_values() {
455        assert_eq!(Ternary::Negative.to_i8(), -1);
456        assert_eq!(Ternary::Zero.to_i8(), 0);
457        assert_eq!(Ternary::Positive.to_i8(), 1);
458    }
459
460    #[test]
461    fn test_agent_status_available() {
462        assert!(AgentStatus::Ready.available());
463        assert!(!AgentStatus::Busy.available());
464        assert!(!AgentStatus::Offline.available());
465    }
466
467    #[test]
468    fn test_decision_engine_basic() {
469        let engine = DecisionEngine::new(1);
470        let votes = vec![Ternary::Positive, Ternary::Positive, Ternary::Negative];
471        assert_eq!(engine.decide(&votes), Some(Ternary::Positive));
472    }
473
474    #[test]
475    fn test_decision_engine_quorum_not_met() {
476        let engine = DecisionEngine::new(5);
477        let votes = vec![Ternary::Positive, Ternary::Negative];
478        assert_eq!(engine.decide(&votes), None);
479    }
480
481    #[test]
482    fn test_decision_engine_weighted() {
483        let engine = DecisionEngine::new(1);
484        let votes = vec![(Ternary::Negative, 10.0), (Ternary::Positive, 1.0)];
485        assert_eq!(engine.decide_weighted(&votes), Some(Ternary::Negative));
486    }
487
488    #[test]
489    fn test_consensus_strength_unanimous() {
490        let engine = DecisionEngine::new(1);
491        let votes = vec![Ternary::Positive, Ternary::Positive, Ternary::Positive];
492        assert!((engine.consensus_strength(&votes) - 1.0).abs() < 1e-9);
493    }
494
495    #[test]
496    fn test_consensus_strength_split() {
497        let engine = DecisionEngine::new(1);
498        let votes = vec![Ternary::Positive, Ternary::Negative, Ternary::Zero];
499        assert!((engine.consensus_strength(&votes) - (1.0 / 3.0)).abs() < 1e-9);
500    }
501
502    #[test]
503    fn test_delegator_assign() {
504        let mut delegator = Delegator::new();
505        let agents = vec![
506            AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.8 },
507            AgentInfo { id: "a2".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.9 },
508        ];
509        let result = delegator.assign("task1", "scout", &agents);
510        assert_eq!(result, Some("a2".to_string())); // higher fitness
511    }
512
513    #[test]
514    fn test_delegator_no_match() {
515        let mut delegator = Delegator::new();
516        let agents = vec![
517            AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "medic".into(), fitness: 0.9 },
518        ];
519        assert_eq!(delegator.assign("task1", "scout", &agents), None);
520    }
521
522    #[test]
523    fn test_delegator_complete() {
524        let mut delegator = Delegator::new();
525        let agents = vec![
526            AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.5 },
527        ];
528        delegator.assign("task1", "scout", &agents);
529        assert!(delegator.complete("task1"));
530        assert_eq!(delegator.active_count(), 0);
531    }
532
533    #[test]
534    fn test_situation_room_aggregate() {
535        let mut room = SituationRoom::new();
536        room.report("a1", Ternary::Positive);
537        room.report("a2", Ternary::Positive);
538        room.report("a3", Ternary::Negative);
539        assert_eq!(room.aggregate(), Ternary::Positive);
540    }
541
542    #[test]
543    fn test_situation_room_distribution() {
544        let mut room = SituationRoom::new();
545        room.report("a1", Ternary::Negative);
546        room.report("a2", Ternary::Zero);
547        room.report("a3", Ternary::Positive);
548        assert_eq!(room.distribution(), (1, 1, 1));
549    }
550
551    #[test]
552    fn test_situation_room_clear() {
553        let mut room = SituationRoom::new();
554        room.report("a1", Ternary::Positive);
555        room.clear();
556        assert_eq!(room.report_count(), 0);
557    }
558
559    #[test]
560    fn test_fleet_report_health() {
561        let mut report = FleetReport::new();
562        report.add("a1", AgentStatus::Ready);
563        report.add("a2", AgentStatus::Ready);
564        report.add("a3", AgentStatus::Offline);
565        assert!((report.health() - (2.0 / 3.0)).abs() < 1e-9);
566    }
567
568    #[test]
569    fn test_fleet_report_operational() {
570        let mut report = FleetReport::new();
571        report.add("a1", AgentStatus::Ready);
572        assert!(report.operational());
573    }
574
575    #[test]
576    fn test_fleet_report_compromised() {
577        let mut report = FleetReport::new();
578        report.add("a1", AgentStatus::Ready);
579        report.add("a2", AgentStatus::Compromised);
580        assert!(!report.operational());
581    }
582
583    #[test]
584    fn test_fleet_report_offline() {
585        let mut report = FleetReport::new();
586        report.add("a1", AgentStatus::Offline);
587        report.add("a2", AgentStatus::Ready);
588        assert_eq!(report.offline_agents(), vec!["a1"]);
589    }
590
591    #[test]
592    fn test_succession_plan() {
593        let mut plan = SuccessionPlan::new();
594        plan.add_successor("a1");
595        plan.add_successor("a2");
596        assert_eq!(plan.heir(), Some("a1"));
597        assert_eq!(plan.depth(), 2);
598    }
599
600    #[test]
601    fn test_succession_promote() {
602        let mut plan = SuccessionPlan::new();
603        plan.add_successor("a1");
604        plan.add_successor("a2");
605        let promoted = plan.promote_next();
606        assert_eq!(promoted, Some("a1".to_string()));
607        assert_eq!(plan.heir(), Some("a2"));
608    }
609
610    #[test]
611    fn test_captain_enlist_and_discharge() {
612        let mut captain = Captain::new("cap1", 1);
613        captain.enlist(AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.9 });
614        assert_eq!(captain.roster.len(), 1);
615        assert!(captain.discharge("a1"));
616        assert_eq!(captain.roster.len(), 0);
617    }
618
619    #[test]
620    fn test_captain_fleet_health() {
621        let mut captain = Captain::new("cap1", 1);
622        captain.enlist(AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.9 });
623        captain.enlist(AgentInfo { id: "a2".into(), status: AgentStatus::Busy, specialization: "medic".into(), fitness: 0.7 });
624        assert!((captain.fleet_health() - 0.5).abs() < 1e-9);
625    }
626
627    #[test]
628    fn test_captain_delegate() {
629        let mut captain = Captain::new("cap1", 1);
630        captain.enlist(AgentInfo { id: "a1".into(), status: AgentStatus::Ready, specialization: "scout".into(), fitness: 0.9 });
631        let assigned = captain.delegate("task1", "scout");
632        assert_eq!(assigned, Some("a1".to_string()));
633    }
634}