## Sprint 22 — Entanglement ratio configuration acceptance tests (#175, #178) ## ## Test-first stubs for the client-side surface of the world_seed feature. ## These tests will warn-and-skip until the implementation lands (Tyre, #175). ## ## Client-side acceptance criteria (#175): ## - GameState carries a world_seed field (stores the seed for this session) ## - SessionManager.new_game() generates and stores a world_seed ## - The IPC startup payload carries world_seed so the server can seed SimRng ## ## Server-side acceptance criteria (#178) are in: ## - server/src/content/entanglement.rs (Rust unit tests) ## ## Spec: D-029 (30/50/20 entanglement ratio, variable per seed), D-010 (deterministic sim) ## Tickets: #175, #178 class_name TestEntanglementSprint22 extends GdUnitTestSuite # -- Client-side: GameState.world_seed field (#175) --------------------------- func test_game_state_has_world_seed_field() -> void: # #175 client-side: GameState must store the world_seed for this session. # The seed is set by SessionManager.new_game() and read by SimBridge to # carry it in the session startup IPC message. if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed not found — test-first stub (awaiting #175)") return # Field exists — verify it is numeric (int or null are both acceptable initial states) var seed_val = GameState.get("world_seed") assert_bool(seed_val == null or seed_val is int).override_failure_message( "GameState.world_seed must be int or null" ).is_true() func test_game_state_world_seed_can_be_set_and_read() -> void: if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — skipped (#175 not yet implemented)") return var orig = GameState.get("world_seed") GameState.world_seed = 0xDEADBEEF assert_int(GameState.world_seed).is_equal(0xDEADBEEF) # Restore GameState.world_seed = orig func test_game_state_world_seed_default_is_null_or_zero() -> void: # Before a session starts, world_seed should be null (no session) or 0 (unset). if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — skipped") return var seed_val = GameState.get("world_seed") assert_bool(seed_val == null or seed_val == 0).override_failure_message( "GameState.world_seed should be null or 0 before any session starts" ).is_true() # -- Client-side: SessionManager seed generation (#175) ----------------------- func test_session_manager_exists() -> void: var sm = get_node_or_null("/root/SessionManager") if sm == null: push_warning("TestEntanglementSprint22: SessionManager autoload not found — skipped") return assert_that(sm).is_not_null() func test_session_manager_new_game_generates_world_seed() -> void: # #175: new_game() must generate and store world_seed in GameState. # The seed is a non-zero u64 that will be sent to the server on startup. var sm = get_node_or_null("/root/SessionManager") if sm == null: push_warning("TestEntanglementSprint22: SessionManager not found — skipped") return if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub") return # Call new_game() (will create a save dir — acceptable in test environment) var orig_seed = GameState.get("world_seed") var orig_game_id: String = GameState.current_game_id sm.new_game() var generated_seed = GameState.get("world_seed") # world_seed must have been set to a non-null, non-zero value assert_bool(generated_seed != null).override_failure_message( "SessionManager.new_game() must set GameState.world_seed (#175)" ).is_true() if generated_seed != null: assert_bool(generated_seed != 0).override_failure_message( "Generated world_seed must be non-zero" ).is_true() # Restore state GameState.current_game_id = orig_game_id GameState.world_seed = orig_seed func test_session_manager_same_game_id_has_same_seed() -> void: # Resuming a session must restore the original world_seed (not generate a new one). # This ensures deterministic replays work correctly (D-010). var sm = get_node_or_null("/root/SessionManager") if sm == null: push_warning("TestEntanglementSprint22: SessionManager not found — skipped") return if not sm.has_method("resume_game"): push_warning("TestEntanglementSprint22: resume_game() missing — skipped") return if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub") return # Set a known seed and game_id, then resume — seed must not be clobbered GameState.world_seed = 12345678 var orig_game_id: String = GameState.current_game_id sm.resume_game("20260228-120000-abc123") # resume_game() must NOT overwrite world_seed assert_int(GameState.world_seed).override_failure_message( "resume_game() must not overwrite world_seed — seed is loaded from the save, not regenerated" ).is_equal(12345678) GameState.current_game_id = orig_game_id # -- IPC startup message: world_seed field (#175) ---------------------------- func test_protocol_encode_startup_message_has_world_seed_field() -> void: # #175 acceptance: startup IPC message must carry "world_seed" key. # Verifies Protocol.encode_startup_message encodes the seed so the server # can deserialize it as StartupMessage { world_seed: u64 }. var seed: int = 0xDEADBEEF # 3735928559 — fits in u32, safely maps to Rust u64 var bytes: PackedByteArray = Protocol.encode_startup_message(seed) assert_bool(bytes.size() > 0).override_failure_message( "Protocol.encode_startup_message must return non-empty bytes" ).is_true() var decoded = Messagepack.decode(bytes) assert_that(decoded.status).override_failure_message( "encode_startup_message output must be valid msgpack: %s" % str(decoded.status) ).is_null() var msg = decoded.value assert_bool(msg is Dictionary and msg.has("world_seed")).override_failure_message( "StartupMessage wire payload must contain 'world_seed' key, got: %s" % str(msg) ).is_true() assert_int(msg["world_seed"]).override_failure_message( "world_seed must round-trip through msgpack unchanged" ).is_equal(seed) func test_protocol_encode_startup_message_zero_seed() -> void: # Edge case: seed=0 must still encode a valid payload (world_seed: 0). var bytes: PackedByteArray = Protocol.encode_startup_message(0) assert_bool(bytes.size() > 0).is_true() var decoded = Messagepack.decode(bytes) assert_that(decoded.status).is_null() assert_int(decoded.value["world_seed"]).is_equal(0) func test_sim_bridge_can_send_world_seed_in_startup() -> void: # #175 acceptance: "startup IPC message carries a world_seed field" # The client must be able to include world_seed in the session startup payload. # Test-first: verify the API exists (method or field), else warn-and-skip. var sim_bridge = get_node_or_null("/root/SimBridge") if sim_bridge == null: push_warning("TestEntanglementSprint22: SimBridge not found — skipped") return # Option A: SimBridge has a world_seed property that is sent during startup if "world_seed" in sim_bridge: sim_bridge.world_seed = 99999 assert_int(sim_bridge.world_seed).is_equal(99999) sim_bridge.world_seed = 0 return # Option B: SimBridge has a set_world_seed() method if sim_bridge.has_method("set_world_seed"): # Method exists — this is the expected API sim_bridge.set_world_seed(99999) return # Neither found — test-first stub push_warning( "TestEntanglementSprint22: SimBridge has no world_seed field or set_world_seed() — " + "test-first stub awaiting #175 implementation" ) # -- Protocol: world_seed flows from client to server (#175) ------------------ func test_apply_snapshot_does_not_clobber_world_seed() -> void: # world_seed is set at session start and must persist across all subsequent snapshots. # Snapshots must not overwrite or clear the world_seed that was set at startup. if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub") return GameState.world_seed = 42000 GameState.apply_snapshot({"tick": 5, "visible_tiles": []}) assert_int(GameState.world_seed).override_failure_message( "apply_snapshot() must not clear or overwrite world_seed — seed is set once at session start" ).is_equal(42000) GameState.world_seed = null # -- Seed variation property (#178, informational — full test is Rust-side) --- func test_different_seeds_produce_different_configs_informational() -> void: # D-029: "entanglement rate varies per seed to prevent metagaming calibration" # The definitive acceptance test for this is Rust-side (server/src/content/entanglement.rs): # - EntanglementConfig::from_rng(seed_A) == EntanglementConfig::from_rng(seed_A) [deterministic] # - EntanglementConfig::from_rng(seed_A) != EntanglementConfig::from_rng(seed_B) [variable, >=90%] # # This test only verifies the client side: world_seed is a u64 large enough to # have sufficient entropy. A 24-bit game_id hex component alone has 16M combinations; # the full u64 seed provides 2^64 possibilities. # # We verify that two calls to new_game() produce different seeds. var sm = get_node_or_null("/root/SessionManager") if sm == null: push_warning("TestEntanglementSprint22: SessionManager not found — skipped") return if not "world_seed" in GameState: push_warning("TestEntanglementSprint22: GameState.world_seed missing — test-first stub") return var orig_game_id: String = GameState.current_game_id sm.new_game() var seed_a = GameState.get("world_seed") sm.new_game() var seed_b = GameState.get("world_seed") if seed_a == null or seed_b == null: push_warning("TestEntanglementSprint22: new_game() did not set world_seed — test-first stub") GameState.current_game_id = orig_game_id return # Two different sessions should produce different seeds assert_bool(seed_a != seed_b).override_failure_message( "Two calls to new_game() must produce different world_seeds (D-029 anti-metagaming)" ).is_true() GameState.current_game_id = orig_game_id