1use std::collections::HashMap;
20
21#[derive(Debug, Clone)]
27pub struct ScoreComponent {
28 pub name: String,
30 pub weight: f64,
32 pub value: f64,
34 pub last_updated_tick: u64,
36}
37
38#[derive(Debug, Clone)]
40pub struct SbPeerScore {
41 pub peer_id: String,
43 pub components: HashMap<String, ScoreComponent>,
45 pub composite_score: f64,
47}
48
49#[derive(Debug, Clone)]
51pub struct ScoreboardStats {
52 pub peer_count: usize,
54 pub component_count: usize,
56 pub avg_composite: f64,
58 pub max_composite: f64,
60}
61
62#[derive(Debug, Clone)]
67pub struct PeerScoreboard {
68 peers: HashMap<String, SbPeerScore>,
69 component_weights: HashMap<String, f64>,
70 current_tick: u64,
71 decay_rate: f64,
72}
73
74impl PeerScoreboard {
79 pub fn new(decay_rate: f64) -> Self {
83 Self {
84 peers: HashMap::new(),
85 component_weights: HashMap::new(),
86 current_tick: 0,
87 decay_rate: decay_rate.clamp(0.0, 1.0),
88 }
89 }
90
91 pub fn register_component(&mut self, name: &str, default_weight: f64) {
93 self.component_weights
94 .insert(name.to_string(), default_weight);
95 }
96
97 pub fn update_score(&mut self, peer_id: &str, component: &str, value: f64) {
102 let weight = match self.component_weights.get(component) {
103 Some(&w) => w,
104 None => return,
105 };
106
107 let tick = self.current_tick;
108 let entry = self
109 .peers
110 .entry(peer_id.to_string())
111 .or_insert_with(|| SbPeerScore {
112 peer_id: peer_id.to_string(),
113 components: HashMap::new(),
114 composite_score: 0.0,
115 });
116
117 let comp = entry
118 .components
119 .entry(component.to_string())
120 .or_insert_with(|| ScoreComponent {
121 name: component.to_string(),
122 weight,
123 value: 0.0,
124 last_updated_tick: tick,
125 });
126
127 comp.value = value.clamp(0.0, 1.0);
128 comp.weight = weight;
129 comp.last_updated_tick = tick;
130
131 Self::recompute_composite(entry);
132 }
133
134 pub fn get_score(&self, peer_id: &str) -> Option<f64> {
136 self.peers.get(peer_id).map(|p| p.composite_score)
137 }
138
139 pub fn get_component(&self, peer_id: &str, component: &str) -> Option<f64> {
141 self.peers
142 .get(peer_id)
143 .and_then(|p| p.components.get(component))
144 .map(|c| c.value)
145 }
146
147 pub fn rank_peers(&self) -> Vec<(String, f64)> {
149 let mut ranked: Vec<(String, f64)> = self
150 .peers
151 .values()
152 .map(|p| (p.peer_id.clone(), p.composite_score))
153 .collect();
154 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
155 ranked
156 }
157
158 pub fn top_peers(&self, n: usize) -> Vec<(String, f64)> {
160 let ranked = self.rank_peers();
161 ranked.into_iter().take(n).collect()
162 }
163
164 pub fn apply_decay(&mut self) {
166 let rate = self.decay_rate;
167 for entry in self.peers.values_mut() {
168 for comp in entry.components.values_mut() {
169 comp.value *= rate;
170 comp.value = comp.value.clamp(0.0, 1.0);
172 }
173 Self::recompute_composite(entry);
174 }
175 }
176
177 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
179 self.peers.remove(peer_id).is_some()
180 }
181
182 pub fn peer_count(&self) -> usize {
184 self.peers.len()
185 }
186
187 pub fn tick(&mut self) {
189 self.current_tick = self.current_tick.saturating_add(1);
190 self.apply_decay();
191 }
192
193 pub fn stats(&self) -> ScoreboardStats {
195 let peer_count = self.peers.len();
196 let component_count = self.component_weights.len();
197
198 let (sum, max) = self.peers.values().fold((0.0_f64, 0.0_f64), |(s, m), p| {
199 (s + p.composite_score, m.max(p.composite_score))
200 });
201
202 let avg_composite = if peer_count > 0 {
203 sum / peer_count as f64
204 } else {
205 0.0
206 };
207
208 ScoreboardStats {
209 peer_count,
210 component_count,
211 avg_composite,
212 max_composite: max,
213 }
214 }
215
216 pub fn current_tick(&self) -> u64 {
218 self.current_tick
219 }
220
221 fn recompute_composite(entry: &mut SbPeerScore) {
227 let total_weight: f64 = entry.components.values().map(|c| c.weight.abs()).sum();
228 if total_weight == 0.0 {
229 entry.composite_score = 0.0;
230 return;
231 }
232 let weighted_sum: f64 = entry.components.values().map(|c| c.value * c.weight).sum();
233 entry.composite_score = weighted_sum / total_weight;
234 }
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
248 fn test_new_scoreboard_empty() {
249 let sb = PeerScoreboard::new(0.999);
250 assert_eq!(sb.peer_count(), 0);
251 assert_eq!(sb.current_tick(), 0);
252 }
253
254 #[test]
255 fn test_register_component() {
256 let mut sb = PeerScoreboard::new(0.999);
257 sb.register_component("latency", 2.0);
258 sb.register_component("uptime", 1.0);
259 let stats = sb.stats();
260 assert_eq!(stats.component_count, 2);
261 }
262
263 #[test]
264 fn test_update_score_creates_peer() {
265 let mut sb = PeerScoreboard::new(0.999);
266 sb.register_component("latency", 1.0);
267 sb.update_score("p1", "latency", 0.5);
268 assert_eq!(sb.peer_count(), 1);
269 }
270
271 #[test]
272 fn test_get_score_known_peer() {
273 let mut sb = PeerScoreboard::new(0.999);
274 sb.register_component("x", 1.0);
275 sb.update_score("p1", "x", 0.7);
276 let score = sb.get_score("p1");
277 assert!(score.is_some());
278 assert!((score.expect("just checked") - 0.7).abs() < 1e-9);
279 }
280
281 #[test]
282 fn test_get_score_unknown_peer() {
283 let sb = PeerScoreboard::new(0.999);
284 assert!(sb.get_score("missing").is_none());
285 }
286
287 #[test]
288 fn test_get_component() {
289 let mut sb = PeerScoreboard::new(0.999);
290 sb.register_component("a", 1.0);
291 sb.update_score("p1", "a", 0.42);
292 let v = sb.get_component("p1", "a");
293 assert!(v.is_some());
294 assert!((v.expect("just checked") - 0.42).abs() < 1e-9);
295 }
296
297 #[test]
298 fn test_get_component_missing_component() {
299 let mut sb = PeerScoreboard::new(0.999);
300 sb.register_component("a", 1.0);
301 sb.update_score("p1", "a", 0.5);
302 assert!(sb.get_component("p1", "b").is_none());
303 }
304
305 #[test]
306 fn test_get_component_missing_peer() {
307 let sb = PeerScoreboard::new(0.999);
308 assert!(sb.get_component("nope", "x").is_none());
309 }
310
311 #[test]
314 fn test_value_clamped_above_one() {
315 let mut sb = PeerScoreboard::new(0.999);
316 sb.register_component("c", 1.0);
317 sb.update_score("p1", "c", 5.0);
318 assert!((sb.get_component("p1", "c").expect("exists") - 1.0).abs() < 1e-9);
319 }
320
321 #[test]
322 fn test_value_clamped_below_zero() {
323 let mut sb = PeerScoreboard::new(0.999);
324 sb.register_component("c", 1.0);
325 sb.update_score("p1", "c", -3.0);
326 assert!((sb.get_component("p1", "c").expect("exists")).abs() < 1e-9);
327 }
328
329 #[test]
330 fn test_decay_rate_clamped() {
331 let sb = PeerScoreboard::new(2.0);
332 assert!((sb.decay_rate - 1.0).abs() < 1e-9);
333 let sb2 = PeerScoreboard::new(-0.5);
334 assert!(sb2.decay_rate.abs() < 1e-9);
335 }
336
337 #[test]
340 fn test_composite_single_component() {
341 let mut sb = PeerScoreboard::new(0.999);
342 sb.register_component("a", 1.0);
343 sb.update_score("p1", "a", 0.8);
344 assert!((sb.get_score("p1").expect("exists") - 0.8).abs() < 1e-9);
345 }
346
347 #[test]
348 fn test_composite_equal_weights() {
349 let mut sb = PeerScoreboard::new(0.999);
350 sb.register_component("a", 1.0);
351 sb.register_component("b", 1.0);
352 sb.update_score("p1", "a", 0.6);
353 sb.update_score("p1", "b", 0.4);
354 assert!((sb.get_score("p1").expect("exists") - 0.5).abs() < 1e-9);
356 }
357
358 #[test]
359 fn test_composite_unequal_weights() {
360 let mut sb = PeerScoreboard::new(0.999);
361 sb.register_component("a", 3.0);
362 sb.register_component("b", 1.0);
363 sb.update_score("p1", "a", 1.0);
364 sb.update_score("p1", "b", 0.0);
365 assert!((sb.get_score("p1").expect("exists") - 0.75).abs() < 1e-9);
367 }
368
369 #[test]
370 fn test_composite_updates_on_change() {
371 let mut sb = PeerScoreboard::new(0.999);
372 sb.register_component("a", 1.0);
373 sb.update_score("p1", "a", 0.2);
374 assert!((sb.get_score("p1").expect("exists") - 0.2).abs() < 1e-9);
375 sb.update_score("p1", "a", 0.9);
376 assert!((sb.get_score("p1").expect("exists") - 0.9).abs() < 1e-9);
377 }
378
379 #[test]
382 fn test_rank_peers_descending() {
383 let mut sb = PeerScoreboard::new(0.999);
384 sb.register_component("a", 1.0);
385 sb.update_score("low", "a", 0.1);
386 sb.update_score("mid", "a", 0.5);
387 sb.update_score("high", "a", 0.9);
388 let ranked = sb.rank_peers();
389 assert_eq!(ranked.len(), 3);
390 assert_eq!(ranked[0].0, "high");
391 assert_eq!(ranked[1].0, "mid");
392 assert_eq!(ranked[2].0, "low");
393 }
394
395 #[test]
396 fn test_rank_peers_empty() {
397 let sb = PeerScoreboard::new(0.999);
398 assert!(sb.rank_peers().is_empty());
399 }
400
401 #[test]
402 fn test_top_peers() {
403 let mut sb = PeerScoreboard::new(0.999);
404 sb.register_component("a", 1.0);
405 sb.update_score("p1", "a", 0.1);
406 sb.update_score("p2", "a", 0.9);
407 sb.update_score("p3", "a", 0.5);
408 let top = sb.top_peers(2);
409 assert_eq!(top.len(), 2);
410 assert_eq!(top[0].0, "p2");
411 assert_eq!(top[1].0, "p3");
412 }
413
414 #[test]
415 fn test_top_peers_n_exceeds_count() {
416 let mut sb = PeerScoreboard::new(0.999);
417 sb.register_component("a", 1.0);
418 sb.update_score("p1", "a", 0.5);
419 let top = sb.top_peers(100);
420 assert_eq!(top.len(), 1);
421 }
422
423 #[test]
426 fn test_apply_decay_reduces_scores() {
427 let mut sb = PeerScoreboard::new(0.5);
428 sb.register_component("a", 1.0);
429 sb.update_score("p1", "a", 1.0);
430 sb.apply_decay();
431 let v = sb.get_component("p1", "a").expect("exists");
432 assert!((v - 0.5).abs() < 1e-9);
433 }
434
435 #[test]
436 fn test_apply_decay_multiple_times() {
437 let mut sb = PeerScoreboard::new(0.5);
438 sb.register_component("a", 1.0);
439 sb.update_score("p1", "a", 1.0);
440 sb.apply_decay();
441 sb.apply_decay();
442 let v = sb.get_component("p1", "a").expect("exists");
443 assert!((v - 0.25).abs() < 1e-9);
444 }
445
446 #[test]
447 fn test_tick_advances_and_decays() {
448 let mut sb = PeerScoreboard::new(0.5);
449 sb.register_component("a", 1.0);
450 sb.update_score("p1", "a", 1.0);
451 sb.tick();
452 assert_eq!(sb.current_tick(), 1);
453 let v = sb.get_component("p1", "a").expect("exists");
454 assert!((v - 0.5).abs() < 1e-9);
455 }
456
457 #[test]
458 fn test_tick_multiple() {
459 let mut sb = PeerScoreboard::new(0.9);
460 sb.register_component("a", 1.0);
461 sb.update_score("p1", "a", 1.0);
462 for _ in 0..10 {
463 sb.tick();
464 }
465 assert_eq!(sb.current_tick(), 10);
466 let v = sb.get_component("p1", "a").expect("exists");
467 let expected = 0.9_f64.powi(10);
468 assert!((v - expected).abs() < 1e-9);
469 }
470
471 #[test]
474 fn test_remove_peer_existing() {
475 let mut sb = PeerScoreboard::new(0.999);
476 sb.register_component("a", 1.0);
477 sb.update_score("p1", "a", 0.5);
478 assert!(sb.remove_peer("p1"));
479 assert_eq!(sb.peer_count(), 0);
480 assert!(sb.get_score("p1").is_none());
481 }
482
483 #[test]
484 fn test_remove_peer_nonexistent() {
485 let mut sb = PeerScoreboard::new(0.999);
486 assert!(!sb.remove_peer("ghost"));
487 }
488
489 #[test]
492 fn test_stats_empty_board() {
493 let sb = PeerScoreboard::new(0.999);
494 let s = sb.stats();
495 assert_eq!(s.peer_count, 0);
496 assert_eq!(s.component_count, 0);
497 assert!((s.avg_composite).abs() < 1e-9);
498 assert!((s.max_composite).abs() < 1e-9);
499 }
500
501 #[test]
502 fn test_stats_with_peers() {
503 let mut sb = PeerScoreboard::new(0.999);
504 sb.register_component("a", 1.0);
505 sb.update_score("p1", "a", 0.4);
506 sb.update_score("p2", "a", 0.8);
507 let s = sb.stats();
508 assert_eq!(s.peer_count, 2);
509 assert_eq!(s.component_count, 1);
510 assert!((s.avg_composite - 0.6).abs() < 1e-9);
511 assert!((s.max_composite - 0.8).abs() < 1e-9);
512 }
513
514 #[test]
517 fn test_update_unregistered_component_ignored() {
518 let mut sb = PeerScoreboard::new(0.999);
519 sb.update_score("p1", "nope", 1.0);
520 assert_eq!(sb.peer_count(), 0);
521 }
522
523 #[test]
526 fn test_multiple_components_weighted() {
527 let mut sb = PeerScoreboard::new(0.999);
528 sb.register_component("speed", 2.0);
529 sb.register_component("reliability", 3.0);
530 sb.register_component("age", 1.0);
531 sb.update_score("p1", "speed", 0.8);
532 sb.update_score("p1", "reliability", 0.6);
533 sb.update_score("p1", "age", 1.0);
534 let expected = 4.4 / 6.0;
536 assert!((sb.get_score("p1").expect("exists") - expected).abs() < 1e-9);
537 }
538
539 #[test]
542 fn test_register_component_after_peers_added() {
543 let mut sb = PeerScoreboard::new(0.999);
544 sb.register_component("a", 1.0);
545 sb.update_score("p1", "a", 0.5);
546 sb.register_component("b", 1.0);
548 sb.update_score("p1", "b", 1.0);
549 assert!((sb.get_score("p1").expect("exists") - 0.75).abs() < 1e-9);
551 }
552
553 #[test]
556 fn test_decay_with_zero_rate() {
557 let mut sb = PeerScoreboard::new(0.0);
558 sb.register_component("a", 1.0);
559 sb.update_score("p1", "a", 1.0);
560 sb.apply_decay();
561 assert!((sb.get_component("p1", "a").expect("exists")).abs() < 1e-9);
562 }
563
564 #[test]
567 fn test_decay_with_rate_one() {
568 let mut sb = PeerScoreboard::new(1.0);
569 sb.register_component("a", 1.0);
570 sb.update_score("p1", "a", 0.7);
571 sb.apply_decay();
572 assert!((sb.get_component("p1", "a").expect("exists") - 0.7).abs() < 1e-9);
573 }
574
575 #[test]
578 fn test_peer_count_multiple() {
579 let mut sb = PeerScoreboard::new(0.999);
580 sb.register_component("a", 1.0);
581 sb.update_score("p1", "a", 0.1);
582 sb.update_score("p2", "a", 0.2);
583 sb.update_score("p3", "a", 0.3);
584 assert_eq!(sb.peer_count(), 3);
585 }
586
587 #[test]
590 fn test_composite_partial_components() {
591 let mut sb = PeerScoreboard::new(0.999);
592 sb.register_component("a", 2.0);
593 sb.register_component("b", 2.0);
594 sb.update_score("p1", "a", 1.0);
596 assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
598 }
599
600 #[test]
603 fn test_last_updated_tick() {
604 let mut sb = PeerScoreboard::new(0.999);
605 sb.register_component("a", 1.0);
606 sb.tick(); sb.tick(); sb.update_score("p1", "a", 0.5);
609 let entry = sb.peers.get("p1").expect("exists");
610 let comp = entry.components.get("a").expect("exists");
611 assert_eq!(comp.last_updated_tick, 2);
612 }
613
614 #[test]
617 fn test_decay_recomputes_composite() {
618 let mut sb = PeerScoreboard::new(0.5);
619 sb.register_component("a", 1.0);
620 sb.register_component("b", 1.0);
621 sb.update_score("p1", "a", 1.0);
622 sb.update_score("p1", "b", 1.0);
623 assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
625 sb.apply_decay();
626 assert!((sb.get_score("p1").expect("exists") - 0.5).abs() < 1e-9);
628 }
629
630 #[test]
633 fn test_rank_peers_equal_scores() {
634 let mut sb = PeerScoreboard::new(0.999);
635 sb.register_component("a", 1.0);
636 sb.update_score("p1", "a", 0.5);
637 sb.update_score("p2", "a", 0.5);
638 let ranked = sb.rank_peers();
639 assert_eq!(ranked.len(), 2);
640 assert!((ranked[0].1 - 0.5).abs() < 1e-9);
642 assert!((ranked[1].1 - 0.5).abs() < 1e-9);
643 }
644
645 #[test]
648 fn test_reregister_component_updates_weight() {
649 let mut sb = PeerScoreboard::new(0.999);
650 sb.register_component("a", 1.0);
651 sb.update_score("p1", "a", 1.0);
652 assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
653 sb.register_component("a", 3.0);
655 sb.update_score("p1", "a", 1.0);
656 assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
658 }
659}