use std::collections::HashMap;
use crate::encoder::ConstraintExtractor;
use crate::tile::Tile;
pub struct TelephoneChain {
tiles: Vec<Tile>,
fact_tracker: FactTracker,
original_facts: HashMap<String, String>,
}
impl TelephoneChain {
pub fn new(source: &str) -> Self {
let extractor = ConstraintExtractor;
let original_facts = extractor.extract(source);
let encoder = crate::encoder::TileEncoder::default();
let tile = encoder.encode(source, &crate::tile::SalienceMap::default());
let mut fact_tracker = FactTracker::new(&original_facts);
fact_tracker.record_round(&tile);
Self {
tiles: vec![tile],
fact_tracker,
original_facts,
}
}
pub fn add_round(&mut self, reconstruction: &str) -> &Tile {
let encoder = crate::encoder::TileEncoder::default();
let parent_id = self.tiles.last().unwrap().id.clone();
let mut tile = encoder.encode(reconstruction, &crate::tile::SalienceMap::default());
tile.generation = self.tiles.len() as u32;
tile.parent_id = Some(parent_id);
self.fact_tracker.record_round(&tile);
self.tiles.push(tile);
self.tiles.last().unwrap()
}
pub fn len(&self) -> usize {
self.tiles.len()
}
pub fn is_empty(&self) -> bool {
self.tiles.is_empty()
}
pub fn get(&self, round: usize) -> Option<&Tile> {
self.tiles.get(round)
}
pub fn crystallization_point(&self) -> Option<usize> {
if self.tiles.len() < 3 {
return None;
}
let mut prev_rate = 1.0;
for round in 0..self.tiles.len() {
let rate = self.fact_tracker.survival_rate(round);
let drop = prev_rate - rate;
if drop < 0.05 && round > 0 {
return Some(round);
}
prev_rate = rate;
}
None
}
pub fn fact_timeline(&self) -> HashMap<String, Vec<bool>> {
self.fact_tracker.timeline.clone()
}
pub fn original_facts(&self) -> &HashMap<String, String> {
&self.original_facts
}
}
pub struct FactTracker {
facts: HashMap<String, String>,
timeline: HashMap<String, Vec<bool>>,
}
impl FactTracker {
pub fn new(facts: &HashMap<String, String>) -> Self {
Self {
facts: facts.clone(),
timeline: facts.keys().map(|k| (k.clone(), vec![])).collect(),
}
}
pub fn record_round(&mut self, tile: &Tile) {
let tile_text = format!("{} {}", tile.summary, tile.constraints.values().cloned().collect::<Vec<_>>().join(" ")).to_lowercase();
for (key, original_value) in &self.facts {
let survived = tile_text.contains(&original_value.to_lowercase());
self.timeline.entry(key.clone()).or_default().push(survived);
}
}
pub fn survival_rate(&self, round: usize) -> f64 {
if self.timeline.is_empty() {
return 1.0;
}
let survived: usize = self
.timeline
.values()
.map(|v| v.get(round).copied().unwrap_or(false) as usize)
.sum();
survived as f64 / self.timeline.len() as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn telephone_chain_basic() {
let chain = TelephoneChain::new("Alice went to Seattle on March 15th to work on Project Alpha.");
assert_eq!(chain.len(), 1);
assert!(!chain.tiles[0].summary.is_empty());
}
#[test]
fn add_rounds() {
let mut chain = TelephoneChain::new("Alice discovered a critical bug in the production system.");
chain.add_round("Alice found a bug in production.");
chain.add_round("There was a bug found.");
assert_eq!(chain.len(), 3);
assert_eq!(chain.tiles[0].generation, 0);
assert_eq!(chain.tiles[1].generation, 1);
assert_eq!(chain.tiles[2].generation, 2);
assert!(chain.tiles[0].parent_id.is_none());
assert_eq!(chain.tiles[1].parent_id.as_ref(), Some(&chain.tiles[0].id));
}
#[test]
fn fact_timeline_tracks() {
let chain = TelephoneChain::new("Alice went to Seattle for the important meeting.");
let timeline = chain.fact_timeline();
assert!(!timeline.is_empty());
for (_, rounds) in &timeline {
assert!(!rounds.is_empty());
}
}
#[test]
fn survival_rate_declines() {
let mut chain = TelephoneChain::new("Alice reported a critical emergency in Seattle on March 15th.");
let rate_0 = chain.fact_tracker.survival_rate(0);
chain.add_round("Someone reported something somewhere.");
let rate_1 = chain.fact_tracker.survival_rate(1);
assert!(rate_0 >= 0.0 && rate_0 <= 1.0);
assert!(rate_1 >= 0.0 && rate_1 <= 1.0);
}
#[test]
fn crystallization_point_needs_multiple_rounds() {
let chain = TelephoneChain::new("Test content");
assert!(chain.crystallization_point().is_none());
}
}