1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
11
12#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum AgentStatus {
37 Ready,
38 Busy,
39 Offline,
40 Compromised,
41}
42
43impl AgentStatus {
44 pub fn available(self) -> bool {
46 self == AgentStatus::Ready
47 }
48}
49
50#[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#[derive(Debug, Clone)]
68pub struct DecisionEngine {
69 pub quorum: usize,
71}
72
73impl DecisionEngine {
74 pub fn new(quorum: usize) -> Self {
75 Self { quorum }
76 }
77
78 pub fn decide(&self, votes: &[Ternary]) -> Option<Ternary> {
81 if votes.len() < self.quorum {
82 return None;
83 }
84 let mut counts = [0usize; 3]; 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 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 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#[derive(Debug, Clone)]
143pub struct Delegator {
144 assignments: HashMap<String, String>,
146}
147
148impl Delegator {
149 pub fn new() -> Self {
150 Self {
151 assignments: HashMap::new(),
152 }
153 }
154
155 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 pub fn get_assignment(&self, task_id: &str) -> Option<&str> {
176 self.assignments.get(task_id).map(|s| s.as_str())
177 }
178
179 pub fn complete(&mut self, task_id: &str) -> bool {
181 self.assignments.remove(task_id).is_some()
182 }
183
184 pub fn active_count(&self) -> usize {
186 self.assignments.len()
187 }
188}
189
190#[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 pub fn report(&mut self, agent_id: &str, value: Ternary) {
210 self.reports.insert(agent_id.to_string(), value);
211 }
212
213 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 pub fn report_count(&self) -> usize {
233 self.reports.len()
234 }
235
236 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 pub fn clear(&mut self) {
246 self.reports.clear();
247 }
248}
249
250#[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 pub fn add(&mut self, agent_id: &str, status: AgentStatus) {
267 self.agent_reports.insert(agent_id.to_string(), status);
268 }
269
270 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 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 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 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#[derive(Debug, Clone)]
309pub struct SuccessionPlan {
310 successors: Vec<String>,
312}
313
314impl SuccessionPlan {
315 pub fn new() -> Self {
316 Self {
317 successors: Vec::new(),
318 }
319 }
320
321 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 pub fn heir(&self) -> Option<&str> {
330 self.successors.first().map(|s| s.as_str())
331 }
332
333 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 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 pub fn depth(&self) -> usize {
356 self.successors.len()
357 }
358
359 pub fn line(&self) -> &[String] {
361 &self.successors
362 }
363}
364
365#[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 pub fn enlist(&mut self, agent: AgentInfo) {
395 self.succession.add_successor(&agent.id);
396 self.roster.push(agent);
397 }
398
399 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 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) .collect();
419 self.decision_engine.decide(&votes)
420 }
421
422 pub fn decide_from_votes(&self, votes: &[Ternary]) -> Option<Ternary> {
424 self.decision_engine.decide(votes)
425 }
426
427 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 pub fn receive_report(&mut self, agent_id: &str, value: Ternary) {
434 self.situation_room.report(agent_id, value);
435 }
436
437 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#[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())); }
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}