feat(simulation): integrate observer visibility query (#112)

Replace unfiltered generate_snapshot with compute_observer_snapshot
that combines shadowcasting + vision cone to send only visible
entities and tiles. Enforces information asymmetry (D-011): NPCs
behind walls or in the blind spot are excluded from the snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 00:21:07 +01:00
co-authored by Claude Opus 4.6
parent f760624967
commit e205b38938
5 changed files with 381 additions and 3 deletions
+11
View File
@@ -4,6 +4,7 @@ use settled_reach_server::bridge::framing::{read_framed, write_framed};
use settled_reach_server::bridge::local::LocalBridge;
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::SimBridge;
use settled_reach_server::simulation::time::DayPhase;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::thread;
@@ -32,14 +33,24 @@ fn snapshot_roundtrip_over_unix_socket() {
let bridge = LocalBridge::accept(&server_path).expect("failed to accept");
let snapshot = ObserverSnapshot {
version: 2,
tick: 42,
game_time: GameTime {
day: 0,
time_of_day: 0,
day_phase: DayPhase::Morning,
paused: false,
},
player_facing: FacingDirection::North,
entities: vec![VisibleEntity {
entity_id: 100,
x: 10.5,
y: 20.3,
z: 0,
kind: EntityKind::Npc,
visibility: VisibilitySector::Forward,
}],
visible_tiles: vec![],
};
bridge
+11
View File
@@ -4,6 +4,7 @@ use settled_reach_server::bridge::framing::{read_framed, write_framed};
use settled_reach_server::bridge::tcp::TcpBridge;
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::SimBridge;
use settled_reach_server::simulation::time::DayPhase;
use std::net::{TcpListener, TcpStream};
use std::thread;
@@ -18,14 +19,24 @@ fn snapshot_roundtrip_over_tcp() {
let bridge = TcpBridge::accept_on(listener).expect("failed to accept");
let snapshot = ObserverSnapshot {
version: 2,
tick: 42,
game_time: GameTime {
day: 0,
time_of_day: 0,
day_phase: DayPhase::Morning,
paused: false,
},
player_facing: FacingDirection::North,
entities: vec![VisibleEntity {
entity_id: 100,
x: 10.5,
y: 20.3,
z: 0,
kind: EntityKind::Npc,
visibility: VisibilitySector::Forward,
}],
visible_tiles: vec![],
};
bridge
+7
View File
@@ -56,9 +56,15 @@ fn player_moves_north_through_full_pipeline() {
rmp_serde::from_slice(&response).expect("deserialize snapshot");
// Snapshot captures state at end of tick 0 (before advance_tick increments to 1)
assert_eq!(snapshot.version, 2);
assert_eq!(snapshot.tick, 0);
assert_eq!(snapshot.entities.len(), 1);
// v2 fields populated
assert_eq!(snapshot.game_time.day, 0);
assert_eq!(snapshot.game_time.day_phase, settled_reach_server::simulation::time::DayPhase::Morning);
assert!(!snapshot.game_time.paused);
let player_entity = &snapshot.entities[0];
// Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
// Render coords: (16.5, 15.5, 0)
@@ -66,6 +72,7 @@ fn player_moves_north_through_full_pipeline() {
assert_eq!(player_entity.y, 15.5);
assert_eq!(player_entity.z, 0);
assert!(matches!(player_entity.kind, EntityKind::Player));
assert!(matches!(player_entity.visibility, VisibilitySector::Forward));
// Clean up
drop(reader);