feat(simulation): add world_seed IPC and EntanglementConfig (#175, #178)

Implements the StartupMessage protocol: client generates world_seed in
SessionManager.new_game(), sends it after handshake, server uses it to
seed SimRng and sample EntanglementConfig.

EntanglementConfig samples flat ∈ [25,35]%, intrigue ∈ [15,25]%, mundane
as remainder (D-029). Same seed produces identical config (D-010
determinism). Different seeds produce distinct configs in ≥90% of pairs.

Protocol flow: HandshakeMessage (server→client) → StartupMessage with
world_seed (client→server) → SimRng initialization → tick loop.

10 Rust tests (determinism, variation, bounds, sum invariant).
9 GDScript test stubs + 2 encode tests for client-side pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 14:26:31 +01:00
co-authored by Claude Opus 4.6
parent ebc87d8e74
commit d591f44b35
13 changed files with 640 additions and 7 deletions
+22
View File
@@ -129,6 +129,28 @@ impl TcpBridge {
}
impl SimBridge for TcpBridge {
fn receive_startup(&self) -> Result<super::StartupMessage, BridgeError> {
let mut reader = self
.reader
.lock()
.map_err(|e| BridgeError::MutexPoisoned(format!("reader: {}", e)))?;
// Toggle to blocking for reliable startup message read.
// The client sends StartupMessage immediately after handshake validation,
// so this read should complete quickly.
reader.get_mut().set_nonblocking(false).map_err(BridgeError::Io)?;
let result = read_framed(reader.get_mut());
// Restore non-blocking for the tick loop
reader.get_mut().set_nonblocking(true).map_err(BridgeError::Io)?;
match result? {
Some(payload) => {
let msg: super::StartupMessage = rmp_serde::from_slice(&payload)?;
tracing::info!("received startup message: world_seed={}", msg.world_seed);
Ok(msg)
}
None => Err(BridgeError::Disconnected),
}
}
fn send_handshake(&self) -> Result<(), BridgeError> {
use super::types::{HandshakeMessage, PROTOCOL_VERSION};
let msg = HandshakeMessage {