use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ScoreComponent {
pub name: String,
pub weight: f64,
pub value: f64,
pub last_updated_tick: u64,
}
#[derive(Debug, Clone)]
pub struct SbPeerScore {
pub peer_id: String,
pub components: HashMap<String, ScoreComponent>,
pub composite_score: f64,
}
#[derive(Debug, Clone)]
pub struct ScoreboardStats {
pub peer_count: usize,
pub component_count: usize,
pub avg_composite: f64,
pub max_composite: f64,
}
#[derive(Debug, Clone)]
pub struct PeerScoreboard {
peers: HashMap<String, SbPeerScore>,
component_weights: HashMap<String, f64>,
current_tick: u64,
decay_rate: f64,
}
impl PeerScoreboard {
pub fn new(decay_rate: f64) -> Self {
Self {
peers: HashMap::new(),
component_weights: HashMap::new(),
current_tick: 0,
decay_rate: decay_rate.clamp(0.0, 1.0),
}
}
pub fn register_component(&mut self, name: &str, default_weight: f64) {
self.component_weights
.insert(name.to_string(), default_weight);
}
pub fn update_score(&mut self, peer_id: &str, component: &str, value: f64) {
let weight = match self.component_weights.get(component) {
Some(&w) => w,
None => return,
};
let tick = self.current_tick;
let entry = self
.peers
.entry(peer_id.to_string())
.or_insert_with(|| SbPeerScore {
peer_id: peer_id.to_string(),
components: HashMap::new(),
composite_score: 0.0,
});
let comp = entry
.components
.entry(component.to_string())
.or_insert_with(|| ScoreComponent {
name: component.to_string(),
weight,
value: 0.0,
last_updated_tick: tick,
});
comp.value = value.clamp(0.0, 1.0);
comp.weight = weight;
comp.last_updated_tick = tick;
Self::recompute_composite(entry);
}
pub fn get_score(&self, peer_id: &str) -> Option<f64> {
self.peers.get(peer_id).map(|p| p.composite_score)
}
pub fn get_component(&self, peer_id: &str, component: &str) -> Option<f64> {
self.peers
.get(peer_id)
.and_then(|p| p.components.get(component))
.map(|c| c.value)
}
pub fn rank_peers(&self) -> Vec<(String, f64)> {
let mut ranked: Vec<(String, f64)> = self
.peers
.values()
.map(|p| (p.peer_id.clone(), p.composite_score))
.collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ranked
}
pub fn top_peers(&self, n: usize) -> Vec<(String, f64)> {
let ranked = self.rank_peers();
ranked.into_iter().take(n).collect()
}
pub fn apply_decay(&mut self) {
let rate = self.decay_rate;
for entry in self.peers.values_mut() {
for comp in entry.components.values_mut() {
comp.value *= rate;
comp.value = comp.value.clamp(0.0, 1.0);
}
Self::recompute_composite(entry);
}
}
pub fn remove_peer(&mut self, peer_id: &str) -> bool {
self.peers.remove(peer_id).is_some()
}
pub fn peer_count(&self) -> usize {
self.peers.len()
}
pub fn tick(&mut self) {
self.current_tick = self.current_tick.saturating_add(1);
self.apply_decay();
}
pub fn stats(&self) -> ScoreboardStats {
let peer_count = self.peers.len();
let component_count = self.component_weights.len();
let (sum, max) = self.peers.values().fold((0.0_f64, 0.0_f64), |(s, m), p| {
(s + p.composite_score, m.max(p.composite_score))
});
let avg_composite = if peer_count > 0 {
sum / peer_count as f64
} else {
0.0
};
ScoreboardStats {
peer_count,
component_count,
avg_composite,
max_composite: max,
}
}
pub fn current_tick(&self) -> u64 {
self.current_tick
}
fn recompute_composite(entry: &mut SbPeerScore) {
let total_weight: f64 = entry.components.values().map(|c| c.weight.abs()).sum();
if total_weight == 0.0 {
entry.composite_score = 0.0;
return;
}
let weighted_sum: f64 = entry.components.values().map(|c| c.value * c.weight).sum();
entry.composite_score = weighted_sum / total_weight;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_scoreboard_empty() {
let sb = PeerScoreboard::new(0.999);
assert_eq!(sb.peer_count(), 0);
assert_eq!(sb.current_tick(), 0);
}
#[test]
fn test_register_component() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("latency", 2.0);
sb.register_component("uptime", 1.0);
let stats = sb.stats();
assert_eq!(stats.component_count, 2);
}
#[test]
fn test_update_score_creates_peer() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("latency", 1.0);
sb.update_score("p1", "latency", 0.5);
assert_eq!(sb.peer_count(), 1);
}
#[test]
fn test_get_score_known_peer() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("x", 1.0);
sb.update_score("p1", "x", 0.7);
let score = sb.get_score("p1");
assert!(score.is_some());
assert!((score.expect("just checked") - 0.7).abs() < 1e-9);
}
#[test]
fn test_get_score_unknown_peer() {
let sb = PeerScoreboard::new(0.999);
assert!(sb.get_score("missing").is_none());
}
#[test]
fn test_get_component() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.42);
let v = sb.get_component("p1", "a");
assert!(v.is_some());
assert!((v.expect("just checked") - 0.42).abs() < 1e-9);
}
#[test]
fn test_get_component_missing_component() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.5);
assert!(sb.get_component("p1", "b").is_none());
}
#[test]
fn test_get_component_missing_peer() {
let sb = PeerScoreboard::new(0.999);
assert!(sb.get_component("nope", "x").is_none());
}
#[test]
fn test_value_clamped_above_one() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("c", 1.0);
sb.update_score("p1", "c", 5.0);
assert!((sb.get_component("p1", "c").expect("exists") - 1.0).abs() < 1e-9);
}
#[test]
fn test_value_clamped_below_zero() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("c", 1.0);
sb.update_score("p1", "c", -3.0);
assert!((sb.get_component("p1", "c").expect("exists")).abs() < 1e-9);
}
#[test]
fn test_decay_rate_clamped() {
let sb = PeerScoreboard::new(2.0);
assert!((sb.decay_rate - 1.0).abs() < 1e-9);
let sb2 = PeerScoreboard::new(-0.5);
assert!(sb2.decay_rate.abs() < 1e-9);
}
#[test]
fn test_composite_single_component() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.8);
assert!((sb.get_score("p1").expect("exists") - 0.8).abs() < 1e-9);
}
#[test]
fn test_composite_equal_weights() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.register_component("b", 1.0);
sb.update_score("p1", "a", 0.6);
sb.update_score("p1", "b", 0.4);
assert!((sb.get_score("p1").expect("exists") - 0.5).abs() < 1e-9);
}
#[test]
fn test_composite_unequal_weights() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 3.0);
sb.register_component("b", 1.0);
sb.update_score("p1", "a", 1.0);
sb.update_score("p1", "b", 0.0);
assert!((sb.get_score("p1").expect("exists") - 0.75).abs() < 1e-9);
}
#[test]
fn test_composite_updates_on_change() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.2);
assert!((sb.get_score("p1").expect("exists") - 0.2).abs() < 1e-9);
sb.update_score("p1", "a", 0.9);
assert!((sb.get_score("p1").expect("exists") - 0.9).abs() < 1e-9);
}
#[test]
fn test_rank_peers_descending() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("low", "a", 0.1);
sb.update_score("mid", "a", 0.5);
sb.update_score("high", "a", 0.9);
let ranked = sb.rank_peers();
assert_eq!(ranked.len(), 3);
assert_eq!(ranked[0].0, "high");
assert_eq!(ranked[1].0, "mid");
assert_eq!(ranked[2].0, "low");
}
#[test]
fn test_rank_peers_empty() {
let sb = PeerScoreboard::new(0.999);
assert!(sb.rank_peers().is_empty());
}
#[test]
fn test_top_peers() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.1);
sb.update_score("p2", "a", 0.9);
sb.update_score("p3", "a", 0.5);
let top = sb.top_peers(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].0, "p2");
assert_eq!(top[1].0, "p3");
}
#[test]
fn test_top_peers_n_exceeds_count() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.5);
let top = sb.top_peers(100);
assert_eq!(top.len(), 1);
}
#[test]
fn test_apply_decay_reduces_scores() {
let mut sb = PeerScoreboard::new(0.5);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
sb.apply_decay();
let v = sb.get_component("p1", "a").expect("exists");
assert!((v - 0.5).abs() < 1e-9);
}
#[test]
fn test_apply_decay_multiple_times() {
let mut sb = PeerScoreboard::new(0.5);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
sb.apply_decay();
sb.apply_decay();
let v = sb.get_component("p1", "a").expect("exists");
assert!((v - 0.25).abs() < 1e-9);
}
#[test]
fn test_tick_advances_and_decays() {
let mut sb = PeerScoreboard::new(0.5);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
sb.tick();
assert_eq!(sb.current_tick(), 1);
let v = sb.get_component("p1", "a").expect("exists");
assert!((v - 0.5).abs() < 1e-9);
}
#[test]
fn test_tick_multiple() {
let mut sb = PeerScoreboard::new(0.9);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
for _ in 0..10 {
sb.tick();
}
assert_eq!(sb.current_tick(), 10);
let v = sb.get_component("p1", "a").expect("exists");
let expected = 0.9_f64.powi(10);
assert!((v - expected).abs() < 1e-9);
}
#[test]
fn test_remove_peer_existing() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.5);
assert!(sb.remove_peer("p1"));
assert_eq!(sb.peer_count(), 0);
assert!(sb.get_score("p1").is_none());
}
#[test]
fn test_remove_peer_nonexistent() {
let mut sb = PeerScoreboard::new(0.999);
assert!(!sb.remove_peer("ghost"));
}
#[test]
fn test_stats_empty_board() {
let sb = PeerScoreboard::new(0.999);
let s = sb.stats();
assert_eq!(s.peer_count, 0);
assert_eq!(s.component_count, 0);
assert!((s.avg_composite).abs() < 1e-9);
assert!((s.max_composite).abs() < 1e-9);
}
#[test]
fn test_stats_with_peers() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.4);
sb.update_score("p2", "a", 0.8);
let s = sb.stats();
assert_eq!(s.peer_count, 2);
assert_eq!(s.component_count, 1);
assert!((s.avg_composite - 0.6).abs() < 1e-9);
assert!((s.max_composite - 0.8).abs() < 1e-9);
}
#[test]
fn test_update_unregistered_component_ignored() {
let mut sb = PeerScoreboard::new(0.999);
sb.update_score("p1", "nope", 1.0);
assert_eq!(sb.peer_count(), 0);
}
#[test]
fn test_multiple_components_weighted() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("speed", 2.0);
sb.register_component("reliability", 3.0);
sb.register_component("age", 1.0);
sb.update_score("p1", "speed", 0.8);
sb.update_score("p1", "reliability", 0.6);
sb.update_score("p1", "age", 1.0);
let expected = 4.4 / 6.0;
assert!((sb.get_score("p1").expect("exists") - expected).abs() < 1e-9);
}
#[test]
fn test_register_component_after_peers_added() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.5);
sb.register_component("b", 1.0);
sb.update_score("p1", "b", 1.0);
assert!((sb.get_score("p1").expect("exists") - 0.75).abs() < 1e-9);
}
#[test]
fn test_decay_with_zero_rate() {
let mut sb = PeerScoreboard::new(0.0);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
sb.apply_decay();
assert!((sb.get_component("p1", "a").expect("exists")).abs() < 1e-9);
}
#[test]
fn test_decay_with_rate_one() {
let mut sb = PeerScoreboard::new(1.0);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.7);
sb.apply_decay();
assert!((sb.get_component("p1", "a").expect("exists") - 0.7).abs() < 1e-9);
}
#[test]
fn test_peer_count_multiple() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.1);
sb.update_score("p2", "a", 0.2);
sb.update_score("p3", "a", 0.3);
assert_eq!(sb.peer_count(), 3);
}
#[test]
fn test_composite_partial_components() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 2.0);
sb.register_component("b", 2.0);
sb.update_score("p1", "a", 1.0);
assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
}
#[test]
fn test_last_updated_tick() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.tick(); sb.tick(); sb.update_score("p1", "a", 0.5);
let entry = sb.peers.get("p1").expect("exists");
let comp = entry.components.get("a").expect("exists");
assert_eq!(comp.last_updated_tick, 2);
}
#[test]
fn test_decay_recomputes_composite() {
let mut sb = PeerScoreboard::new(0.5);
sb.register_component("a", 1.0);
sb.register_component("b", 1.0);
sb.update_score("p1", "a", 1.0);
sb.update_score("p1", "b", 1.0);
assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
sb.apply_decay();
assert!((sb.get_score("p1").expect("exists") - 0.5).abs() < 1e-9);
}
#[test]
fn test_rank_peers_equal_scores() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 0.5);
sb.update_score("p2", "a", 0.5);
let ranked = sb.rank_peers();
assert_eq!(ranked.len(), 2);
assert!((ranked[0].1 - 0.5).abs() < 1e-9);
assert!((ranked[1].1 - 0.5).abs() < 1e-9);
}
#[test]
fn test_reregister_component_updates_weight() {
let mut sb = PeerScoreboard::new(0.999);
sb.register_component("a", 1.0);
sb.update_score("p1", "a", 1.0);
assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
sb.register_component("a", 3.0);
sb.update_score("p1", "a", 1.0);
assert!((sb.get_score("p1").expect("exists") - 1.0).abs() < 1e-9);
}
}