use crate::mesh::NodeId;
pub type NicheCategory = String;
pub const INBOX_CAP: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignalKind {
Direct,
Environmental { niche: NicheCategory },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Signal {
pub sender: NodeId,
pub value: i64,
pub kind: SignalKind,
pub sent_at_tick: u64,
}
impl Signal {
pub fn direct(sender: NodeId, value: i64, sent_at_tick: u64) -> Self {
Signal {
sender,
value,
kind: SignalKind::Direct,
sent_at_tick,
}
}
pub fn environmental(
sender: NodeId,
value: i64,
niche: NicheCategory,
sent_at_tick: u64,
) -> Self {
Signal {
sender,
value,
kind: SignalKind::Environmental { niche },
sent_at_tick,
}
}
pub fn is_direct(&self) -> bool {
matches!(self.kind, SignalKind::Direct)
}
}
#[derive(Clone, Debug)]
pub struct Inbox {
entries: Vec<Signal>,
cap: usize,
}
impl Default for Inbox {
fn default() -> Self {
Self::new()
}
}
impl Inbox {
pub fn new() -> Self {
Inbox {
entries: Vec::new(),
cap: INBOX_CAP,
}
}
pub fn with_capacity(cap: usize) -> Self {
Inbox {
entries: Vec::new(),
cap,
}
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn push(&mut self, signal: Signal) {
if self.entries.len() >= self.cap {
self.entries.remove(0);
}
self.entries.push(signal);
}
pub fn pop_oldest(&mut self) -> Option<Signal> {
if self.entries.is_empty() {
None
} else {
Some(self.entries.remove(0))
}
}
pub fn iter(&self) -> std::slice::Iter<'_, Signal> {
self.entries.iter()
}
pub fn evict_older_than(&mut self, min_tick: u64) {
self.entries.retain(|s| s.sent_at_tick >= min_tick);
}
}
pub const ENV_DECAY_RATE: f64 = 0.95;
pub const ENV_MIN_STRENGTH: f64 = 1.0;
#[derive(Clone, Debug, Default)]
pub struct EnvironmentalField {
slots: std::collections::HashMap<NicheCategory, f64>,
}
impl EnvironmentalField {
pub fn new() -> Self {
Self::default()
}
pub fn deposit(&mut self, niche: NicheCategory, value: f64) {
let entry = self.slots.entry(niche).or_insert(0.0);
*entry = (*entry + value).max(value);
}
pub fn sense(&self, niche: &str) -> i64 {
self.slots.get(niche).copied().unwrap_or(0.0) as i64
}
pub fn decay_tick(&mut self) {
self.slots.retain(|_, v| {
*v *= ENV_DECAY_RATE;
*v >= ENV_MIN_STRENGTH
});
}
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
pub fn iter(&self) -> std::collections::hash_map::Iter<'_, NicheCategory, f64> {
self.slots.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nid(b: u8) -> NodeId {
[b; 8]
}
#[test]
fn inbox_starts_empty() {
let inbox = Inbox::new();
assert!(inbox.is_empty());
assert_eq!(inbox.len(), 0);
assert_eq!(inbox.cap(), INBOX_CAP);
}
#[test]
fn inbox_default_matches_new() {
let a = Inbox::new();
let b = Inbox::default();
assert_eq!(a.cap(), b.cap());
assert_eq!(a.len(), b.len());
}
#[test]
fn push_then_len_one() {
let mut inbox = Inbox::new();
inbox.push(Signal::direct(nid(1), 42, 0));
assert_eq!(inbox.len(), 1);
assert!(!inbox.is_empty());
}
#[test]
fn pop_returns_fifo_order() {
let mut inbox = Inbox::new();
inbox.push(Signal::direct(nid(1), 10, 0));
inbox.push(Signal::direct(nid(2), 20, 1));
inbox.push(Signal::direct(nid(3), 30, 2));
assert_eq!(inbox.pop_oldest().unwrap().value, 10);
assert_eq!(inbox.pop_oldest().unwrap().value, 20);
assert_eq!(inbox.pop_oldest().unwrap().value, 30);
assert!(inbox.pop_oldest().is_none());
}
#[test]
fn overflow_drops_oldest() {
let mut inbox = Inbox::with_capacity(3);
inbox.push(Signal::direct(nid(1), 1, 0));
inbox.push(Signal::direct(nid(2), 2, 1));
inbox.push(Signal::direct(nid(3), 3, 2));
inbox.push(Signal::direct(nid(4), 4, 3));
assert_eq!(inbox.len(), 3);
let values: Vec<i64> = inbox.iter().map(|s| s.value).collect();
assert_eq!(values, vec![2, 3, 4]);
}
#[test]
fn cap_64_default() {
let mut inbox = Inbox::new();
for i in 0..70 {
inbox.push(Signal::direct(nid(0), i as i64, i as u64));
}
assert_eq!(inbox.len(), 64);
assert_eq!(inbox.pop_oldest().unwrap().value, 6);
}
#[test]
fn pop_empty_returns_none() {
let mut inbox = Inbox::new();
assert!(inbox.pop_oldest().is_none());
}
#[test]
fn signal_kind_direct_vs_environmental() {
let d = Signal::direct(nid(1), 7, 0);
let e = Signal::environmental(nid(2), 9, "fibonacci".to_string(), 1);
assert!(d.is_direct());
assert!(!e.is_direct());
assert_eq!(d.kind, SignalKind::Direct);
assert_eq!(
e.kind,
SignalKind::Environmental {
niche: "fibonacci".to_string()
}
);
}
#[test]
fn signal_round_trip_fields() {
let s = Signal::direct(nid(0xab), 12345, 99);
assert_eq!(s.sender, [0xab; 8]);
assert_eq!(s.value, 12345);
assert_eq!(s.sent_at_tick, 99);
}
#[test]
fn iter_does_not_consume() {
let mut inbox = Inbox::new();
inbox.push(Signal::direct(nid(1), 1, 0));
inbox.push(Signal::direct(nid(2), 2, 0));
let _ = inbox.iter().count();
assert_eq!(inbox.len(), 2);
}
#[test]
fn evict_older_than_drops_stale() {
let mut inbox = Inbox::new();
inbox.push(Signal::direct(nid(1), 1, 5));
inbox.push(Signal::direct(nid(2), 2, 10));
inbox.push(Signal::direct(nid(3), 3, 15));
inbox.evict_older_than(10);
assert_eq!(inbox.len(), 2);
assert_eq!(inbox.pop_oldest().unwrap().value, 2);
}
#[test]
fn env_field_starts_empty() {
let f = EnvironmentalField::new();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
assert_eq!(f.sense("anything"), 0);
}
#[test]
fn env_deposit_then_sense() {
let mut f = EnvironmentalField::new();
f.deposit("fibonacci".to_string(), 100.0);
assert_eq!(f.sense("fibonacci"), 100);
assert_eq!(f.sense("sorting"), 0);
}
#[test]
fn env_deposit_accumulates() {
let mut f = EnvironmentalField::new();
f.deposit("fib".to_string(), 30.0);
f.deposit("fib".to_string(), 20.0);
assert_eq!(f.sense("fib"), 50);
}
#[test]
fn env_large_deposit_displaces() {
let mut f = EnvironmentalField::new();
f.deposit("fib".to_string(), 5.0);
f.deposit("fib".to_string(), -2.0);
assert_eq!(f.sense("fib"), 3);
}
#[test]
fn env_decay_tick_multiplies_by_rate() {
let mut f = EnvironmentalField::new();
f.deposit("fib".to_string(), 100.0);
for _ in 0..5 {
f.decay_tick();
}
let sensed = f.sense("fib");
assert!(
(76..=78).contains(&sensed),
"expected ~77 after 5 ticks, got {}",
sensed
);
}
#[test]
fn env_decay_drops_below_floor() {
let mut f = EnvironmentalField::new();
f.deposit("fib".to_string(), 1.5);
f.decay_tick();
assert_eq!(f.len(), 1);
for _ in 0..40 {
f.decay_tick();
}
assert_eq!(f.len(), 0, "below ENV_MIN_STRENGTH entries should drop");
}
}