1use crate::models::{
7 CapabilityExt, CapabilityType, NodeConfig, NodeConfigExt, NodeState, NodeStateExt, Operator,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum CellRole {
15 Leader,
17 Sensor,
19 Compute,
21 Relay,
23 Strike,
25 Support,
27 Follower,
29}
30
31impl CellRole {
32 pub fn assignable_roles() -> Vec<Self> {
34 vec![
35 Self::Sensor,
36 Self::Compute,
37 Self::Relay,
38 Self::Strike,
39 Self::Support,
40 Self::Follower,
41 ]
42 }
43
44 pub fn description(&self) -> &'static str {
46 match self {
47 Self::Leader => "Cell leader - coordinates operations and makes tactical decisions",
48 Self::Sensor => "Sensor/scout - provides long-range detection and reconnaissance",
49 Self::Compute => "Compute node - processes sensor data and runs analysis algorithms",
50 Self::Relay => "Communications relay - extends network range and connectivity",
51 Self::Strike => "Strike platform - engages targets with weapons systems",
52 Self::Support => {
53 "Support platform - provides logistics, medical, or maintenance support"
54 }
55 Self::Follower => "General squad member - performs assigned tasks",
56 }
57 }
58
59 pub fn required_capabilities(&self) -> Vec<CapabilityType> {
61 match self {
62 Self::Leader => vec![CapabilityType::Communication],
63 Self::Sensor => vec![CapabilityType::Sensor],
64 Self::Compute => vec![CapabilityType::Compute],
65 Self::Relay => vec![CapabilityType::Communication],
66 Self::Strike => vec![CapabilityType::Payload],
67 Self::Support => vec![],
68 Self::Follower => vec![],
69 }
70 }
71
72 pub fn preferred_capabilities(&self) -> Vec<CapabilityType> {
74 match self {
75 Self::Leader => vec![CapabilityType::Compute, CapabilityType::Sensor],
76 Self::Sensor => vec![CapabilityType::Communication],
77 Self::Compute => vec![CapabilityType::Communication],
78 Self::Relay => vec![CapabilityType::Sensor],
79 Self::Strike => vec![CapabilityType::Sensor, CapabilityType::Compute],
80 Self::Support => vec![CapabilityType::Mobility],
81 Self::Follower => vec![],
82 }
83 }
84
85 pub fn relevant_mos(&self) -> Vec<&'static str> {
87 match self {
88 Self::Leader => vec!["11B", "11C", "19D"], Self::Sensor => vec!["19D", "35M", "35N"], Self::Compute => vec!["35F", "35N", "17C"], Self::Relay => vec!["25U", "25B", "25Q"], Self::Strike => vec!["11B", "11C", "19K"], Self::Support => vec!["68W", "88M", "91B"], Self::Follower => vec![],
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RoleAssignment {
102 pub platform_id: String,
104 pub role: CellRole,
106 pub score: f64,
108 pub is_primary_choice: bool,
110}
111
112impl RoleAssignment {
113 pub fn new(platform_id: String, role: CellRole, score: f64, is_primary_choice: bool) -> Self {
115 Self {
116 platform_id,
117 role,
118 score,
119 is_primary_choice,
120 }
121 }
122}
123
124pub struct RoleScorer;
126
127impl RoleScorer {
128 pub fn score_platform_for_role(
137 config: &NodeConfig,
138 state: &NodeState,
139 role: CellRole,
140 ) -> Option<f64> {
141 let operator = config.get_primary_operator();
142 let mut score = 0.0;
143 let mut weight_sum = 0.0;
144
145 for required_cap_type in role.required_capabilities() {
147 let has_required = config
148 .capabilities
149 .iter()
150 .any(|c| c.get_capability_type() == required_cap_type);
151
152 if !has_required {
153 return None; }
155 }
156
157 let required_score = Self::score_required_capabilities(config, &role);
159 score += required_score * 0.3;
160 weight_sum += 0.3;
161
162 let preferred_score = Self::score_preferred_capabilities(config, &role);
164 score += preferred_score * 0.2;
165 weight_sum += 0.2;
166
167 if let Some(op) = operator {
169 let mos_score = Self::score_operator_mos(op, &role);
170 score += mos_score * 0.3;
171 weight_sum += 0.3;
172 }
173
174 let health_score = Self::score_platform_health(state);
176 score += health_score * 0.2;
177 weight_sum += 0.2;
178
179 if weight_sum < 1.0 {
181 score /= weight_sum;
182 }
183
184 Some(score.clamp(0.0, 1.0))
185 }
186
187 fn score_required_capabilities(config: &NodeConfig, role: &CellRole) -> f64 {
189 let required = role.required_capabilities();
190 if required.is_empty() {
191 return 1.0;
192 }
193
194 let mut total_score = 0.0;
195 for req_type in &required {
196 let best_capability = config
197 .capabilities
198 .iter()
199 .filter(|c| c.get_capability_type() == *req_type)
200 .max_by(|a, b| {
201 a.confidence
202 .partial_cmp(&b.confidence)
203 .unwrap_or(std::cmp::Ordering::Equal)
204 });
205
206 if let Some(cap) = best_capability {
207 total_score += cap.confidence as f64;
208 }
209 }
210
211 total_score / required.len() as f64
212 }
213
214 fn score_preferred_capabilities(config: &NodeConfig, role: &CellRole) -> f64 {
216 let preferred = role.preferred_capabilities();
217 if preferred.is_empty() {
218 return 1.0;
219 }
220
221 let mut total_score = 0.0;
222 let mut count = 0;
223
224 for pref_type in preferred {
225 if let Some(best_cap) = config
226 .capabilities
227 .iter()
228 .filter(|c| c.get_capability_type() == pref_type)
229 .max_by(|a, b| {
230 a.confidence
231 .partial_cmp(&b.confidence)
232 .unwrap_or(std::cmp::Ordering::Equal)
233 })
234 {
235 total_score += best_cap.confidence as f64;
236 count += 1;
237 }
238 }
239
240 if count > 0 {
241 total_score / count as f64
242 } else {
243 0.5 }
245 }
246
247 fn score_operator_mos(operator: &Operator, role: &CellRole) -> f64 {
249 let relevant_mos = role.relevant_mos();
250 if relevant_mos.is_empty() {
251 return 0.5; }
253
254 if relevant_mos.contains(&operator.mos.as_str()) {
255 0.9 } else {
257 0.3 }
259 }
260
261 fn score_platform_health(state: &NodeState) -> f64 {
263 match state.get_health() {
264 crate::models::HealthStatus::Nominal => 1.0,
265 crate::models::HealthStatus::Degraded => 0.6,
266 crate::models::HealthStatus::Critical => 0.3,
267 crate::models::HealthStatus::Failed => 0.0,
268 crate::models::HealthStatus::Unspecified => 0.5,
269 }
270 }
271
272 pub fn score_all_roles(config: &NodeConfig, state: &NodeState) -> HashMap<CellRole, f64> {
274 let mut scores = HashMap::new();
275
276 for role in CellRole::assignable_roles() {
277 if let Some(score) = Self::score_platform_for_role(config, state, role) {
278 scores.insert(role, score);
279 }
280 }
281
282 scores
283 }
284
285 pub fn best_role_for_platform(
287 config: &NodeConfig,
288 state: &NodeState,
289 ) -> Option<(CellRole, f64)> {
290 Self::score_all_roles(config, state)
291 .into_iter()
292 .max_by(|(_, score_a), (_, score_b)| {
293 score_a
294 .partial_cmp(score_b)
295 .unwrap_or(std::cmp::Ordering::Equal)
296 })
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::models::{
304 AuthorityLevel, BindingType, Capability, HumanMachinePair, HumanMachinePairExt, NodeConfig,
305 NodeConfigExt, NodeStateExt, OperatorExt, OperatorRank,
306 };
307
308 fn create_test_platform_with_capabilities(caps: Vec<Capability>) -> (NodeConfig, NodeState) {
309 let mut config = NodeConfig::new("test_platform".to_string());
310 for cap in caps {
311 config.add_capability(cap);
312 }
313 let state = NodeState::new((0.0, 0.0, 0.0));
314 (config, state)
315 }
316
317 fn create_test_operator(mos: &str, rank: OperatorRank) -> Operator {
318 Operator::new(
319 "op_1".to_string(),
320 "Test Operator".to_string(),
321 rank,
322 AuthorityLevel::Commander,
323 mos.to_string(),
324 )
325 }
326
327 #[test]
328 fn test_role_required_capabilities() {
329 assert_eq!(
330 CellRole::Sensor.required_capabilities(),
331 vec![CapabilityType::Sensor]
332 );
333 assert_eq!(
334 CellRole::Strike.required_capabilities(),
335 vec![CapabilityType::Payload]
336 );
337 assert!(CellRole::Follower.required_capabilities().is_empty());
338 }
339
340 #[test]
341 fn test_role_relevant_mos() {
342 let sensor_mos = CellRole::Sensor.relevant_mos();
343 assert!(sensor_mos.contains(&"19D")); let relay_mos = CellRole::Relay.relevant_mos();
346 assert!(relay_mos.contains(&"25U")); }
348
349 #[test]
350 fn test_score_platform_without_required_capability() {
351 let (config, state) = create_test_platform_with_capabilities(vec![Capability::new(
353 "cpu_1".to_string(),
354 "CPU".to_string(),
355 CapabilityType::Compute,
356 0.8,
357 )]);
358
359 let score = RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor);
360 assert!(score.is_none());
361 }
362
363 #[test]
364 fn test_score_platform_with_required_capability() {
365 let (config, state) = create_test_platform_with_capabilities(vec![Capability::new(
367 "radar_1".to_string(),
368 "Radar".to_string(),
369 CapabilityType::Sensor,
370 0.9,
371 )]);
372
373 let score = RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor);
374 assert!(score.is_some());
375 assert!(score.unwrap() > 0.5);
376 }
377
378 #[test]
379 fn test_score_with_operator_mos_match() {
380 let (mut config, state) = create_test_platform_with_capabilities(vec![Capability::new(
381 "camera_1".to_string(),
382 "Camera".to_string(),
383 CapabilityType::Sensor,
384 0.8,
385 )]);
386
387 let operator = create_test_operator("19D", OperatorRank::E5); let binding = crate::models::HumanMachinePair::new(
389 vec![operator],
390 vec![config.id.clone()],
391 crate::models::BindingType::OneToOne,
392 );
393 config.set_operator_binding(Some(binding));
394
395 let score_with_match =
396 RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor).unwrap();
397
398 let (config_no_op, state_no_op) =
400 create_test_platform_with_capabilities(vec![Capability::new(
401 "camera_2".to_string(),
402 "Camera".to_string(),
403 CapabilityType::Sensor,
404 0.8,
405 )]);
406
407 let score_without_operator =
408 RoleScorer::score_platform_for_role(&config_no_op, &state_no_op, CellRole::Sensor)
409 .unwrap();
410
411 assert!(score_with_match > score_without_operator);
413 }
414
415 #[test]
416 fn test_score_with_operator_mos_mismatch() {
417 let (mut config, state) = create_test_platform_with_capabilities(vec![Capability::new(
418 "camera_1".to_string(),
419 "Camera".to_string(),
420 CapabilityType::Sensor,
421 0.8,
422 )]);
423
424 let operator = create_test_operator("68W", OperatorRank::E4); let binding = crate::models::HumanMachinePair::new(
426 vec![operator],
427 vec![config.id.clone()],
428 crate::models::BindingType::OneToOne,
429 );
430 config.set_operator_binding(Some(binding));
431
432 let score_with_mismatch =
433 RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor).unwrap();
434
435 assert!(score_with_mismatch > 0.0);
437 assert!(score_with_mismatch < 1.0);
438 }
439
440 #[test]
441 fn test_score_all_roles() {
442 let (config, state) = create_test_platform_with_capabilities(vec![
443 Capability::new(
444 "camera_1".to_string(),
445 "Camera".to_string(),
446 CapabilityType::Sensor,
447 0.9,
448 ),
449 Capability::new(
450 "radio_1".to_string(),
451 "Radio".to_string(),
452 CapabilityType::Communication,
453 0.7,
454 ),
455 ]);
456
457 let scores = RoleScorer::score_all_roles(&config, &state);
458
459 assert!(scores.contains_key(&CellRole::Sensor));
461 assert!(scores.contains_key(&CellRole::Relay));
462 assert!(scores.contains_key(&CellRole::Follower));
463
464 assert!(!scores.contains_key(&CellRole::Strike));
466 assert!(!scores.contains_key(&CellRole::Compute));
467 }
468
469 #[test]
470 fn test_best_role_for_platform() {
471 let mut config = NodeConfig::new("test_platform".to_string());
472 config.add_capability(Capability::new(
473 "radar_1".to_string(),
474 "Radar".to_string(),
475 CapabilityType::Sensor,
476 0.95,
477 ));
478 config.add_capability(Capability::new(
479 "radio_1".to_string(),
480 "Radio".to_string(),
481 CapabilityType::Communication,
482 0.5,
483 ));
484
485 let operator = create_test_operator("19D", OperatorRank::E4); let platform_id = config.id.clone();
488 config.operator_binding = Some(HumanMachinePair::new(
489 vec![operator],
490 vec![platform_id],
491 BindingType::OneToOne,
492 ));
493
494 let state = NodeState::new((0.0, 0.0, 0.0));
495
496 let (best_role, score) = RoleScorer::best_role_for_platform(&config, &state).unwrap();
497
498 assert_eq!(best_role, CellRole::Sensor);
500 assert!(score > 0.5);
501 }
502
503 #[test]
504 fn test_role_assignment_creation() {
505 let assignment = RoleAssignment::new("node_1".to_string(), CellRole::Sensor, 0.85, true);
506
507 assert_eq!(assignment.platform_id, "node_1");
508 assert_eq!(assignment.role, CellRole::Sensor);
509 assert_eq!(assignment.score, 0.85);
510 assert!(assignment.is_primary_choice);
511 }
512
513 #[test]
514 fn test_assignable_roles() {
515 let roles = CellRole::assignable_roles();
516
517 assert!(!roles.contains(&CellRole::Leader));
519
520 assert!(roles.contains(&CellRole::Sensor));
522 assert!(roles.contains(&CellRole::Compute));
523 assert!(roles.contains(&CellRole::Relay));
524 assert!(roles.contains(&CellRole::Strike));
525 assert!(roles.contains(&CellRole::Support));
526 assert!(roles.contains(&CellRole::Follower));
527 }
528
529 #[test]
530 fn test_degraded_platform_role_scoring() {
531 let (config_nominal, state_nominal) =
533 create_test_platform_with_capabilities(vec![Capability::new(
534 "sensor_1".to_string(),
535 "Sensor".to_string(),
536 CapabilityType::Sensor,
537 0.9,
538 )]);
539
540 let (config_degraded, mut state_degraded) =
541 create_test_platform_with_capabilities(vec![Capability::new(
542 "sensor_2".to_string(),
543 "Sensor".to_string(),
544 CapabilityType::Sensor,
545 0.9,
546 )]);
547 state_degraded.update_health(crate::models::HealthStatus::Degraded);
548
549 let score_nominal =
550 RoleScorer::score_platform_for_role(&config_nominal, &state_nominal, CellRole::Sensor)
551 .unwrap();
552 let score_degraded = RoleScorer::score_platform_for_role(
553 &config_degraded,
554 &state_degraded,
555 CellRole::Sensor,
556 )
557 .unwrap();
558
559 assert!(score_degraded < score_nominal);
561 assert!(score_degraded > 0.4);
563 }
564
565 #[test]
566 fn test_critical_platform_role_scoring() {
567 let (config, mut state) = create_test_platform_with_capabilities(vec![Capability::new(
569 "sensor_1".to_string(),
570 "Sensor".to_string(),
571 CapabilityType::Sensor,
572 0.9,
573 )]);
574 state.update_health(crate::models::HealthStatus::Critical);
575
576 let score = RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor).unwrap();
577
578 assert!(score > 0.0);
582 assert!(score < 0.7); }
584
585 #[test]
586 fn test_failed_platform_role_scoring() {
587 let (config, mut state) = create_test_platform_with_capabilities(vec![Capability::new(
589 "sensor_1".to_string(),
590 "Sensor".to_string(),
591 CapabilityType::Sensor,
592 0.9,
593 )]);
594 state.update_health(crate::models::HealthStatus::Failed);
595
596 let score = RoleScorer::score_platform_for_role(&config, &state, CellRole::Sensor).unwrap();
597
598 assert!(score > 0.2);
602 assert!(score < 0.6); }
604}