1use crate::result::ProbarResult;
31use crate::runtime::{EntityId, MemoryView, StateDelta};
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct GameStateSnapshot {
40 pub frame: u64,
42 pub state: GameStateData,
44 pub visual_phash: u64,
46 pub state_hash: u64,
48}
49
50impl GameStateSnapshot {
51 #[must_use]
53 pub fn new(frame: u64, state: GameStateData) -> Self {
54 let state_hash = state.compute_hash();
55 Self {
56 frame,
57 state,
58 visual_phash: 0,
59 state_hash,
60 }
61 }
62
63 #[must_use]
65 pub const fn with_phash(mut self, phash: u64) -> Self {
66 self.visual_phash = phash;
67 self
68 }
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct GameStateData {
74 pub positions: HashMap<u32, (f32, f32)>,
76 pub velocities: HashMap<u32, (f32, f32)>,
78 pub scores: HashMap<String, i32>,
80 pub flags: HashMap<String, bool>,
82 pub custom: HashMap<String, serde_json::Value>,
84}
85
86impl GameStateData {
87 #[must_use]
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn add_position(&mut self, entity_id: u32, x: f32, y: f32) {
95 self.positions.insert(entity_id, (x, y));
96 }
97
98 pub fn add_velocity(&mut self, entity_id: u32, vx: f32, vy: f32) {
100 self.velocities.insert(entity_id, (vx, vy));
101 }
102
103 pub fn set_score(&mut self, name: impl Into<String>, value: i32) {
105 self.scores.insert(name.into(), value);
106 }
107
108 pub fn set_flag(&mut self, name: impl Into<String>, value: bool) {
110 self.flags.insert(name.into(), value);
111 }
112
113 #[must_use]
115 pub fn get_position(&self, entity_id: u32) -> Option<(f32, f32)> {
116 self.positions.get(&entity_id).copied()
117 }
118
119 #[must_use]
121 pub fn get_velocity(&self, entity_id: u32) -> Option<(f32, f32)> {
122 self.velocities.get(&entity_id).copied()
123 }
124
125 #[must_use]
127 pub fn get_score(&self, name: &str) -> Option<i32> {
128 self.scores.get(name).copied()
129 }
130
131 #[must_use]
133 pub fn get_flag(&self, name: &str) -> Option<bool> {
134 self.flags.get(name).copied()
135 }
136
137 #[must_use]
139 pub fn compute_hash(&self) -> u64 {
140 use std::collections::hash_map::DefaultHasher;
141 use std::hash::{Hash, Hasher};
142
143 let mut hasher = DefaultHasher::new();
144
145 let mut positions: Vec<_> = self.positions.iter().collect();
147 positions.sort_by_key(|(k, _)| *k);
148 for (k, (x, y)) in positions {
149 k.hash(&mut hasher);
150 x.to_bits().hash(&mut hasher);
151 y.to_bits().hash(&mut hasher);
152 }
153
154 let mut velocities: Vec<_> = self.velocities.iter().collect();
156 velocities.sort_by_key(|(k, _)| *k);
157 for (k, (vx, vy)) in velocities {
158 k.hash(&mut hasher);
159 vx.to_bits().hash(&mut hasher);
160 vy.to_bits().hash(&mut hasher);
161 }
162
163 let mut scores: Vec<_> = self.scores.iter().collect();
165 scores.sort_by_key(|(k, _)| k.as_str());
166 for (k, v) in scores {
167 k.hash(&mut hasher);
168 v.hash(&mut hasher);
169 }
170
171 let mut flags: Vec<_> = self.flags.iter().collect();
173 flags.sort_by_key(|(k, _)| k.as_str());
174 for (k, v) in flags {
175 k.hash(&mut hasher);
176 v.hash(&mut hasher);
177 }
178
179 hasher.finish()
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EntitySnapshot {
186 pub id: EntityId,
188 pub name: String,
190 pub position: Option<(f32, f32)>,
192 pub velocity: Option<(f32, f32)>,
194 pub active: bool,
196 pub components: HashMap<String, serde_json::Value>,
198}
199
200impl EntitySnapshot {
201 #[must_use]
203 pub fn new(id: EntityId, name: impl Into<String>) -> Self {
204 Self {
205 id,
206 name: name.into(),
207 position: None,
208 velocity: None,
209 active: true,
210 components: HashMap::new(),
211 }
212 }
213
214 #[must_use]
216 pub const fn with_position(mut self, x: f32, y: f32) -> Self {
217 self.position = Some((x, y));
218 self
219 }
220
221 #[must_use]
223 pub const fn with_velocity(mut self, vx: f32, vy: f32) -> Self {
224 self.velocity = Some((vx, vy));
225 self
226 }
227
228 pub fn add_component(&mut self, name: impl Into<String>, value: serde_json::Value) {
230 self.components.insert(name.into(), value);
231 }
232}
233
234#[derive(Debug, Clone)]
238pub struct VisualDiff {
239 pub perceptual_similarity: f64,
241 pub pixel_diff_count: u64,
243 pub diff_regions: Vec<DiffRegion>,
245 pub expected: Option<Vec<u8>>,
247 pub actual: Vec<u8>,
249 pub highlighted: Vec<u8>,
251}
252
253impl VisualDiff {
254 #[must_use]
256 pub fn new(similarity: f64, actual: Vec<u8>) -> Self {
257 Self {
258 perceptual_similarity: similarity,
259 pixel_diff_count: 0,
260 diff_regions: Vec::new(),
261 expected: None,
262 actual,
263 highlighted: Vec::new(),
264 }
265 }
266
267 #[must_use]
269 pub fn matches(&self, threshold: f64) -> bool {
270 self.perceptual_similarity >= threshold
271 }
272
273 #[must_use]
275 pub fn is_identical(&self) -> bool {
276 self.perceptual_similarity >= 1.0 - f64::EPSILON
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct DiffRegion {
283 pub x: u32,
285 pub y: u32,
287 pub width: u32,
289 pub height: u32,
291 pub intensity: f64,
293}
294
295#[derive(Debug, Clone)]
297pub enum BridgeConnection {
298 Direct,
300 Rpc {
302 session_id: String,
304 },
305}
306
307#[derive(Debug)]
309pub struct SnapshotCache {
310 max_size: usize,
312 cache: HashMap<u64, GameStateSnapshot>,
314 access_order: Vec<u64>,
316}
317
318impl SnapshotCache {
319 #[must_use]
321 pub fn new(max_size: usize) -> Self {
322 Self {
323 max_size,
324 cache: HashMap::new(),
325 access_order: Vec::new(),
326 }
327 }
328
329 pub fn get(&mut self, frame: u64) -> Option<&GameStateSnapshot> {
331 if self.cache.contains_key(&frame) {
332 self.access_order.retain(|&f| f != frame);
334 self.access_order.push(frame);
335 self.cache.get(&frame)
336 } else {
337 None
338 }
339 }
340
341 pub fn insert(&mut self, frame: u64, snapshot: GameStateSnapshot) {
343 while self.cache.len() >= self.max_size {
345 if let Some(oldest) = self.access_order.first().copied() {
346 self.cache.remove(&oldest);
347 self.access_order.remove(0);
348 } else {
349 break;
350 }
351 }
352
353 self.cache.insert(frame, snapshot);
354 self.access_order.push(frame);
355 }
356
357 pub fn clear(&mut self) {
359 self.cache.clear();
360 self.access_order.clear();
361 }
362
363 #[must_use]
365 pub fn len(&self) -> usize {
366 self.cache.len()
367 }
368
369 #[must_use]
371 pub fn is_empty(&self) -> bool {
372 self.cache.is_empty()
373 }
374}
375
376#[derive(Debug)]
381pub struct StateBridge {
382 connection: BridgeConnection,
384 memory_view: Option<MemoryView>,
386 snapshot_cache: SnapshotCache,
388 delta_history: Vec<StateDelta>,
390}
391
392impl StateBridge {
393 #[must_use]
395 pub fn direct(memory_view: MemoryView) -> Self {
396 Self {
397 connection: BridgeConnection::Direct,
398 memory_view: Some(memory_view),
399 snapshot_cache: SnapshotCache::new(100),
400 delta_history: Vec::new(),
401 }
402 }
403
404 #[must_use]
406 pub fn rpc(session_id: impl Into<String>) -> Self {
407 Self {
408 connection: BridgeConnection::Rpc {
409 session_id: session_id.into(),
410 },
411 memory_view: None,
412 snapshot_cache: SnapshotCache::new(100),
413 delta_history: Vec::new(),
414 }
415 }
416
417 #[must_use]
419 pub const fn is_direct(&self) -> bool {
420 matches!(self.connection, BridgeConnection::Direct)
421 }
422
423 #[must_use]
425 pub const fn memory_view(&self) -> Option<&MemoryView> {
426 self.memory_view.as_ref()
427 }
428
429 pub fn query_entity(&self, entity_id: EntityId) -> ProbarResult<EntitySnapshot> {
435 match &self.connection {
436 BridgeConnection::Direct => {
437 Ok(EntitySnapshot::new(
440 entity_id,
441 format!("entity_{}", entity_id.raw()),
442 ))
443 }
444 BridgeConnection::Rpc { session_id } => {
445 let _ = session_id;
447 Ok(EntitySnapshot::new(
448 entity_id,
449 format!("entity_{}", entity_id.raw()),
450 ))
451 }
452 }
453 }
454
455 pub fn snapshot(&mut self, frame: u64) -> ProbarResult<GameStateSnapshot> {
461 if let Some(cached) = self.snapshot_cache.get(frame) {
463 return Ok(cached.clone());
464 }
465
466 let state = GameStateData::new();
468 let snapshot = GameStateSnapshot::new(frame, state);
469
470 self.snapshot_cache.insert(frame, snapshot.clone());
472
473 Ok(snapshot)
474 }
475
476 pub fn record_delta(&mut self, delta: StateDelta) {
478 self.delta_history.push(delta);
479 }
480
481 #[must_use]
483 pub fn deltas(&self) -> &[StateDelta] {
484 &self.delta_history
485 }
486
487 pub fn clear_deltas(&mut self) {
489 self.delta_history.clear();
490 }
491
492 #[must_use]
496 pub fn compute_phash(image_data: &[u8]) -> u64 {
497 use std::collections::hash_map::DefaultHasher;
501 use std::hash::{Hash, Hasher};
502
503 let mut hasher = DefaultHasher::new();
504
505 let sample_rate = (image_data.len() / 64).max(1);
507 for (i, &byte) in image_data.iter().enumerate() {
508 if i % sample_rate == 0 {
509 byte.hash(&mut hasher);
510 }
511 }
512
513 hasher.finish()
514 }
515
516 #[must_use]
518 pub fn visual_compare(expected: &[u8], actual: &[u8]) -> VisualDiff {
519 let phash_expected = Self::compute_phash(expected);
520 let phash_actual = Self::compute_phash(actual);
521
522 let hamming_distance = (phash_expected ^ phash_actual).count_ones();
524 let similarity = 1.0 - (f64::from(hamming_distance) / 64.0);
525
526 VisualDiff {
527 perceptual_similarity: similarity,
528 pixel_diff_count: 0, diff_regions: Vec::new(),
530 expected: Some(expected.to_vec()),
531 actual: actual.to_vec(),
532 highlighted: Vec::new(),
533 }
534 }
535}
536
537#[cfg(test)]
542#[allow(clippy::unwrap_used, clippy::expect_used)]
543mod tests {
544 use super::*;
545
546 mod game_state_data_tests {
547 use super::*;
548
549 #[test]
550 fn test_new_state_data() {
551 let state = GameStateData::new();
552 assert!(state.positions.is_empty());
553 assert!(state.velocities.is_empty());
554 assert!(state.scores.is_empty());
555 }
556
557 #[test]
558 fn test_add_position() {
559 let mut state = GameStateData::new();
560 state.add_position(1, 100.0, 200.0);
561 assert_eq!(state.get_position(1), Some((100.0, 200.0)));
562 assert_eq!(state.get_position(2), None);
563 }
564
565 #[test]
566 fn test_add_velocity() {
567 let mut state = GameStateData::new();
568 state.add_velocity(1, 5.0, -3.0);
569 assert_eq!(state.get_velocity(1), Some((5.0, -3.0)));
570 }
571
572 #[test]
573 fn test_set_score() {
574 let mut state = GameStateData::new();
575 state.set_score("player1", 100);
576 state.set_score("player2", 50);
577 assert_eq!(state.get_score("player1"), Some(100));
578 assert_eq!(state.get_score("player2"), Some(50));
579 assert_eq!(state.get_score("player3"), None);
580 }
581
582 #[test]
583 fn test_set_flag() {
584 let mut state = GameStateData::new();
585 state.set_flag("game_over", false);
586 state.set_flag("paused", true);
587 assert_eq!(state.get_flag("game_over"), Some(false));
588 assert_eq!(state.get_flag("paused"), Some(true));
589 }
590
591 #[test]
592 fn test_compute_hash_deterministic() {
593 let mut state1 = GameStateData::new();
594 state1.add_position(1, 100.0, 200.0);
595 state1.set_score("player1", 50);
596
597 let mut state2 = GameStateData::new();
598 state2.set_score("player1", 50);
599 state2.add_position(1, 100.0, 200.0);
600
601 assert_eq!(state1.compute_hash(), state2.compute_hash());
603 }
604
605 #[test]
606 fn test_compute_hash_different() {
607 let mut state1 = GameStateData::new();
608 state1.add_position(1, 100.0, 200.0);
609
610 let mut state2 = GameStateData::new();
611 state2.add_position(1, 100.0, 201.0);
612
613 assert_ne!(state1.compute_hash(), state2.compute_hash());
614 }
615 }
616
617 mod entity_snapshot_tests {
618 use super::*;
619
620 #[test]
621 fn test_new_entity_snapshot() {
622 let entity = EntitySnapshot::new(EntityId::new(42), "player");
623 assert_eq!(entity.id.raw(), 42);
624 assert_eq!(entity.name, "player");
625 assert!(entity.active);
626 }
627
628 #[test]
629 fn test_with_position() {
630 let entity = EntitySnapshot::new(EntityId::new(1), "ball").with_position(100.0, 200.0);
631 assert_eq!(entity.position, Some((100.0, 200.0)));
632 }
633
634 #[test]
635 fn test_with_velocity() {
636 let entity = EntitySnapshot::new(EntityId::new(1), "ball").with_velocity(5.0, -3.0);
637 assert_eq!(entity.velocity, Some((5.0, -3.0)));
638 }
639
640 #[test]
641 fn test_add_component() {
642 let mut entity = EntitySnapshot::new(EntityId::new(1), "player");
643 entity.add_component("health", serde_json::json!(100));
644 assert_eq!(
645 entity.components.get("health"),
646 Some(&serde_json::json!(100))
647 );
648 }
649 }
650
651 mod game_state_snapshot_tests {
652 use super::*;
653
654 #[test]
655 fn test_new_snapshot() {
656 let state = GameStateData::new();
657 let snapshot = GameStateSnapshot::new(100, state);
658 assert_eq!(snapshot.frame, 100);
659 assert_ne!(snapshot.state_hash, 0);
660 }
661
662 #[test]
663 fn test_with_phash() {
664 let state = GameStateData::new();
665 let snapshot = GameStateSnapshot::new(0, state).with_phash(12345);
666 assert_eq!(snapshot.visual_phash, 12345);
667 }
668 }
669
670 mod visual_diff_tests {
671 use super::*;
672
673 #[test]
674 fn test_new_visual_diff() {
675 let diff = VisualDiff::new(0.95, vec![1, 2, 3]);
676 assert!((diff.perceptual_similarity - 0.95).abs() < f64::EPSILON);
677 }
678
679 #[test]
680 fn test_matches_threshold() {
681 let diff = VisualDiff::new(0.95, vec![]);
682 assert!(diff.matches(0.90));
683 assert!(diff.matches(0.95));
684 assert!(!diff.matches(0.99));
685 }
686
687 #[test]
688 fn test_is_identical() {
689 let identical = VisualDiff::new(1.0, vec![]);
690 assert!(identical.is_identical());
691
692 let different = VisualDiff::new(0.99, vec![]);
693 assert!(!different.is_identical());
694 }
695 }
696
697 mod snapshot_cache_tests {
698 use super::*;
699
700 #[test]
701 fn test_new_cache() {
702 let cache = SnapshotCache::new(10);
703 assert!(cache.is_empty());
704 assert_eq!(cache.len(), 0);
705 }
706
707 #[test]
708 fn test_insert_and_get() {
709 let mut cache = SnapshotCache::new(10);
710 let snapshot = GameStateSnapshot::new(1, GameStateData::new());
711 cache.insert(1, snapshot);
712
713 assert!(!cache.is_empty());
714 assert_eq!(cache.len(), 1);
715 assert!(cache.get(1).is_some());
716 assert!(cache.get(2).is_none());
717 }
718
719 #[test]
720 fn test_lru_eviction() {
721 let mut cache = SnapshotCache::new(2);
722
723 cache.insert(1, GameStateSnapshot::new(1, GameStateData::new()));
724 cache.insert(2, GameStateSnapshot::new(2, GameStateData::new()));
725
726 let _ = cache.get(1);
728
729 cache.insert(3, GameStateSnapshot::new(3, GameStateData::new()));
731
732 assert!(cache.get(1).is_some()); assert!(cache.get(3).is_some()); }
736
737 #[test]
738 fn test_clear() {
739 let mut cache = SnapshotCache::new(10);
740 cache.insert(1, GameStateSnapshot::new(1, GameStateData::new()));
741 cache.insert(2, GameStateSnapshot::new(2, GameStateData::new()));
742 cache.clear();
743 assert!(cache.is_empty());
744 }
745 }
746
747 mod state_bridge_tests {
748 use super::*;
749
750 #[test]
751 fn test_direct_bridge() {
752 let view = MemoryView::new(1024);
753 let bridge = StateBridge::direct(view);
754 assert!(bridge.is_direct());
755 }
756
757 #[test]
758 fn test_rpc_bridge() {
759 let bridge = StateBridge::rpc("session-123");
760 assert!(!bridge.is_direct());
761 }
762
763 #[test]
764 fn test_query_entity() {
765 let view = MemoryView::new(1024);
766 let bridge = StateBridge::direct(view);
767 let entity = bridge.query_entity(EntityId::new(42)).unwrap();
768 assert_eq!(entity.id.raw(), 42);
769 }
770
771 #[test]
772 fn test_snapshot_caching() {
773 let view = MemoryView::new(1024);
774 let mut bridge = StateBridge::direct(view);
775
776 let snap1 = bridge.snapshot(100).unwrap();
777 let snap2 = bridge.snapshot(100).unwrap();
778
779 assert_eq!(snap1.state_hash, snap2.state_hash);
781 }
782
783 #[test]
784 fn test_record_delta() {
785 let view = MemoryView::new(1024);
786 let mut bridge = StateBridge::direct(view);
787
788 let delta = StateDelta::empty(0);
789 bridge.record_delta(delta);
790
791 assert_eq!(bridge.deltas().len(), 1);
792 }
793
794 #[test]
795 fn test_clear_deltas() {
796 let view = MemoryView::new(1024);
797 let mut bridge = StateBridge::direct(view);
798
799 bridge.record_delta(StateDelta::empty(0));
800 bridge.record_delta(StateDelta::empty(1));
801 bridge.clear_deltas();
802
803 assert!(bridge.deltas().is_empty());
804 }
805
806 #[test]
807 fn test_compute_phash() {
808 let data1 = vec![1, 2, 3, 4, 5];
809 let data2 = vec![1, 2, 3, 4, 5];
810 let data3 = vec![5, 4, 3, 2, 1];
811
812 let hash1 = StateBridge::compute_phash(&data1);
813 let hash2 = StateBridge::compute_phash(&data2);
814 let hash3 = StateBridge::compute_phash(&data3);
815
816 assert_eq!(hash1, hash2);
817 assert_ne!(hash1, hash3);
818 }
819
820 #[test]
821 fn test_visual_compare() {
822 let expected = vec![1, 2, 3, 4, 5];
823 let actual = vec![1, 2, 3, 4, 5];
824
825 let diff = StateBridge::visual_compare(&expected, &actual);
826 assert!(diff.perceptual_similarity > 0.99);
827 assert!(diff.is_identical());
828 }
829
830 #[test]
831 fn test_visual_compare_different() {
832 let expected = vec![1, 2, 3, 4, 5, 6, 7, 8];
833 let actual = vec![8, 7, 6, 5, 4, 3, 2, 1];
834
835 let diff = StateBridge::visual_compare(&expected, &actual);
836 assert!(diff.perceptual_similarity < 1.0);
838 }
839 }
840}