Files
settled-reach/server/tests/game_loop.rs
T
jpmschweitzerandClaude Opus 4.6 9943684f2c refactor(server): address PR #16 review feedback
Hoshe + Tyre review items:
- Use StableId consistently for wire entity_id (H4) across observer,
  observation, interpretation, and interaction systems
- Make NearbyInteractionBuffer.interactions private with take() (H1/H20)
- Add system ordering constraint for compute_nearby_interactions (H5)
- Panic on missing PlayerCharacter in input processing (H2)
- Remove redundant paused field from GameTime (Tyre8)
- Remove #[serde(default)] from nearby_interactions (H3)
- Change NearbyInteraction.distance from f32 to u32 (H8)
- Add sort stability for equal verb priorities (H6)
- Scope constants to pub(crate) (H7)
- Add debug_assert for last_observed_tick ordering (H10)
- Strengthen unregistered entity handling to debug_assert + error (H11)
- Document fractional tick accumulation (Tyre9)
- Extract collect_remembered_entities helper (Tyre2/H17)
- Add half_rate_no_drift_over_10000_frames test (H14)
- Add mid-range and deterministic sort tests (H15)
- Add fixture version assertion (H16)
- Regenerate msgpack fixtures for wire format changes

146 unit + 19 integration tests pass, zero clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:14:35 +01:00

87 lines
3.5 KiB
Rust

//! E2E integration test: full game loop with input processing and snapshot generation
//! Tests the complete pipeline: client sends input -> server processes -> server sends snapshot
use bevy_app::prelude::*;
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::{BridgePlugin, BridgeResource};
use settled_reach_server::knowledge::{KnowledgeGraph, KnowledgePlugin};
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition, WalkabilityMap};
use settled_reach_server::simulation::SimulationPlugin;
use std::io::{BufReader, BufWriter};
use std::net::{TcpListener, TcpStream};
use std::thread;
#[test]
fn player_moves_north_through_full_pipeline() {
// Bind listener first — port guaranteed ready, no sleep needed
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
let server_addr = listener.local_addr().expect("get local addr");
// Spawn server thread
let server_handle = thread::spawn(move || {
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
// Build app
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.insert_resource(BridgeResource::new(bridge));
app.insert_resource(WalkabilityMap::new(32, 32, 1));
app.world_mut().spawn((
PlayerCharacter,
TilePosition::new(16, 16, 0),
KnowledgeGraph::new(),
));
// Run one tick: receive input, process, validate movement, generate snapshot, send
app.update();
});
// Client: connect and send input (no sleep — listener was pre-bound)
let stream = TcpStream::connect(server_addr).expect("client connect");
let mut reader = BufReader::new(stream.try_clone().expect("clone for reader"));
let mut writer = BufWriter::new(stream);
// Send PlayerInput: MoveNorth at tick 0
let inputs = vec![PlayerInput {
tick: 0,
action: PlayerAction::MoveNorth,
}];
let payload = rmp_serde::to_vec_named(&inputs).expect("serialize inputs");
write_framed(&mut writer, &payload).expect("send inputs");
// Receive ObserverSnapshot
let response = read_framed(&mut reader)
.expect("read snapshot")
.expect("not EOF");
let snapshot: ObserverSnapshot =
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, 4);
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_eq!(snapshot.game_time.tick_rate, settled_reach_server::simulation::time::TickRate::Full);
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)
assert_eq!(player_entity.x, 16.5);
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);
drop(writer);
server_handle.join().expect("server thread panicked");
}