1use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct TileBounds {
10 pub max_deviation: f64,
12 pub max_age: u32,
14 pub min_agreement: usize,
16}
17
18impl TileBounds {
19 pub fn new(max_deviation: f64, max_age: u32, min_agreement: usize) -> Self {
20 Self { max_deviation, max_age, min_agreement }
21 }
22
23 pub fn check_bounds(&self, deviation: f64) -> bool {
25 deviation <= self.max_deviation
26 }
27
28 pub fn default_fleet() -> Self {
30 Self {
31 max_deviation: 1.0,
32 max_age: 100,
33 min_agreement: 3, }
35 }
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
40pub struct FleetTile {
41 pub room: String,
42 pub question: String,
43 pub answer: String,
44 pub domain: Option<String>,
45 pub tags: Vec<String>,
46 pub author: String,
47 pub timestamp: f64,
48}
49
50impl FleetTile {
51 pub fn new(room: String, question: String, answer: String) -> Self {
52 Self {
53 room,
54 question,
55 answer,
56 domain: None,
57 tags: vec![],
58 author: "fleet".to_string(),
59 timestamp: std::time::SystemTime::now()
60 .duration_since(std::time::UNIX_EPOCH)
61 .map(|d| d.as_secs() as f64)
62 .unwrap_or(0.0),
63 }
64 }
65
66 pub fn with_domain(mut self, domain: String) -> Self {
67 self.domain = Some(domain);
68 self
69 }
70
71 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
72 self.tags = tags;
73 self
74 }
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct TileCoordination {
80 pub room: String,
81 pub tiles: Vec<FleetTile>,
82 pub consensus_tiles: Vec<String>,
83 pub pending_tiles: Vec<String>,
84}
85
86impl TileCoordination {
87 pub fn new(room: String) -> Self {
88 Self {
89 room,
90 tiles: Vec::new(),
91 consensus_tiles: Vec::new(),
92 pending_tiles: Vec::new(),
93 }
94 }
95
96 pub fn add_tile(&mut self, tile: FleetTile) {
97 if !self.tiles.iter().any(|t| t.question == tile.question) {
98 self.tiles.push(tile.clone());
99 self.pending_tiles.push(tile.question.clone());
100 }
101 }
102
103 pub fn mark_consensus(&mut self, question: &str) {
104 self.pending_tiles.retain(|q| q != question);
105 if !self.consensus_tiles.contains(&question.to_string()) {
106 self.consensus_tiles.push(question.to_string());
107 }
108 }
109
110 pub fn tile_count(&self) -> usize {
111 self.tiles.len()
112 }
113}
114
115pub fn tile_to_glue(tile: &FleetTile) -> Vec<u8> {
117 let msg = serde_json::json!({
118 "t": 0x05,
119 "room": tile.room,
120 "question": tile.question,
121 "answer": tile.answer,
122 "author": tile.author,
123 });
124 serde_json::to_vec(&msg).unwrap_or_default()
125}
126
127pub fn glue_to_tile(data: &[u8]) -> Option<FleetTile> {
129 let msg: serde_json::Value = serde_json::from_slice(data).ok()?;
130 Some(FleetTile {
131 room: msg["room"].as_str()?.to_string(),
132 question: msg["question"].as_str()?.to_string(),
133 answer: msg["answer"].as_str()?.to_string(),
134 domain: msg["domain"].as_str().map(String::from),
135 tags: msg["tags"].as_array()
136 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
137 .unwrap_or_default(),
138 author: msg["author"].as_str().unwrap_or("fleet").to_string(),
139 timestamp: msg["timestamp"].as_f64().unwrap_or(0.0),
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_tile_roundtrip() {
149 let tile = FleetTile::new(
150 "fleet_math".to_string(),
151 "What is the Betti number beta1?".to_string(),
152 "beta1 = E - V + C for a graph with C connected components.".to_string(),
153 );
154 let data = tile_to_glue(&tile);
155 assert!(!data.is_empty());
156 }
157
158 #[test]
159 fn test_coordination_mark_consensus() {
160 let mut coord = TileCoordination::new("test".to_string());
161 coord.add_tile(FleetTile::new("test".to_string(), "Q1?".to_string(), "A1".to_string()));
162 coord.add_tile(FleetTile::new("test".to_string(), "Q2?".to_string(), "A2".to_string()));
163 assert_eq!(coord.tile_count(), 2);
164 assert_eq!(coord.pending_tiles.len(), 2);
165 coord.mark_consensus("Q1?");
166 assert_eq!(coord.consensus_tiles.len(), 1);
167 assert_eq!(coord.pending_tiles.len(), 1);
168 }
169
170 #[test]
171 fn test_tilebounds_check_bounds() {
172 let bounds = TileBounds::new(1.0, 100, 3);
173 assert!(bounds.check_bounds(0.5));
174 assert!(bounds.check_bounds(1.0));
175 assert!(!bounds.check_bounds(1.1));
176 }
177
178
179 #[test]
180 fn test_tilebounds_default_fleet() {
181 let bounds = TileBounds::default_fleet();
182 assert!(bounds.check_bounds(0.5));
183 assert_eq!(bounds.max_age, 100);
184 assert_eq!(bounds.min_agreement, 3);
185 }
186
187 #[test]
188 fn test_tilebounds_zero_deviation() {
189 let bounds = TileBounds::new(0.0, 50, 5);
190 assert!(bounds.check_bounds(0.0));
191 assert!(!bounds.check_bounds(0.001));
192 }
193
194 #[test]
195 fn test_tilebounds_exactly_at_bound() {
196 let bounds = TileBounds::new(2.5, 200, 4);
197 assert!(bounds.check_bounds(2.5));
198 assert!(!bounds.check_bounds(2.5001));
199 }
200}