Files
settled-reach/client/tests/test_client_p1.gd
T
jpmschweitzerandClaude Opus 4.6 50aba3adf6 fix(client): address PR #78 review comments
- Widen world_seed entropy from u32 to full u64 by combining two randi()
  calls (Hoshe warning #1)
- Persist world_seed to save directory and restore on resume_game() so
  loaded sessions maintain D-010 deterministic replay (Tyre warning #2)
- Constrain EntanglementConfig intrigue range based on flat value so
  mundane_ratio stays within D-029 spec [45,55]% (both reviewers)
- Remove dead VIS_PERIPHERAL constant and _grow_bounds() method
- Update test_client_p1 peripheral test for forward-only simplification
- Fix misleading exp_fade shader comment (filter_nearest = hard step)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:11:17 +01:00

209 lines
8.1 KiB
GDScript

## P1 client tests: fog shader state (4), entity lifecycle (2), pending recognition blob (1).
## Information boundary + core rendering contract tests — the perception/fog system
## that makes asymmetric information work.
## Bug #5 class prevention: fog byte encoding errors. Bug #2 class: entity lifecycle.
## Spec ref: stig-round3.md Section 1 (tests #3-#9), workshop-outcomes.md.
class_name TestClientP1
extends GdUnitTestSuite
var EntityRendererScript = load("res://scripts/rendering/entity_renderer.gd")
var FogEntitiesScript = load("res://scripts/rendering/fog_entities.gd")
# -- Helpers -------------------------------------------------------------------
func _get_fog_state() -> Node:
var node = get_node_or_null("/root/FogState")
if node == null:
push_warning("TestClientP1: FogState autoload not found — test skipped")
return node
func _make_entity_renderer() -> Node2D:
var renderer = Node2D.new()
renderer.set_script(EntityRendererScript)
add_child(renderer)
return renderer
func _make_fog_entities() -> Node2D:
var node = Node2D.new()
node.set_script(FogEntitiesScript)
add_child(node)
return node
# -- Fog state: visibility byte values (#3-#4) --------------------------------
func test_fog_visibility_forward_tile() -> void:
# P1 #3: Forward-sector tile writes VIS_FORWARD (255) to _vis_bytes.
var fog = _get_fog_state()
if fog == null:
return
# Reset to deterministic state — 64x64 map at origin, all bytes zeroed
GameState.visible_tiles = []
fog._resize(Rect2i(0, 0, 64, 64))
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.visibility_sectors = {Vector2i(5, 5): "Forward"}
fog.update_from_state()
# Index: row 5 * width 64 + col 5
assert_that(fog._vis_bytes[5 * 64 + 5]).override_failure_message(
"Forward tile at (5,5) should be VIS_FORWARD=%d" % FogState.VIS_FORWARD
).is_equal(FogState.VIS_FORWARD)
GameState.visible_positions.clear()
GameState.visibility_sectors.clear()
func test_fog_visibility_peripheral_tile() -> void:
# P1 #4: Server simplified to forward-only (Sprint 22, #569). All visible
# tiles are now written as VIS_FORWARD regardless of visibility_sectors value.
# Peripheral sector is no longer a distinct visual state.
var fog = _get_fog_state()
if fog == null:
return
GameState.visible_tiles = []
fog._resize(Rect2i(0, 0, 64, 64))
GameState.visible_positions = {Vector2i(10, 8): true}
GameState.visibility_sectors = {Vector2i(10, 8): "Peripheral"}
fog.update_from_state()
assert_that(fog._vis_bytes[8 * 64 + 10]).override_failure_message(
"All visible tiles write VIS_FORWARD after forward-only simplification"
).is_equal(FogState.VIS_FORWARD)
GameState.visible_positions.clear()
GameState.visibility_sectors.clear()
# -- Fog state: exploration persistence (#5) -----------------------------------
func test_fog_exploration_persistence() -> void:
# P1 #5: Tile visible in frame 1 (EXP_VISIBLE), not visible in frame 2
# → _exp_bytes decays to EXP_EXPLORED (128), not EXP_UNEXPLORED (0).
var fog = _get_fog_state()
if fog == null:
return
GameState.visible_tiles = []
fog._resize(Rect2i(0, 0, 64, 64))
var pos := Vector2i(5, 5)
var idx: int = 5 * 64 + 5
# Frame 1: tile visible
GameState.visible_positions = {pos: true}
GameState.visibility_sectors = {pos: "Forward"}
fog.update_from_state()
assert_that(fog._exp_bytes[idx]).override_failure_message(
"Visible tile should have EXP_VISIBLE=%d" % FogState.EXP_VISIBLE
).is_equal(FogState.EXP_VISIBLE)
# Frame 2: tile no longer visible — should decay to EXP_EXPLORED, not EXP_UNEXPLORED
GameState.visible_positions = {}
GameState.visibility_sectors = {}
fog.update_from_state()
assert_that(fog._exp_bytes[idx]).override_failure_message(
"Previously visible tile should decay to EXP_EXPLORED=%d, not EXP_UNEXPLORED" % FogState.EXP_EXPLORED
).is_equal(FogState.EXP_EXPLORED)
GameState.visible_positions.clear()
GameState.visibility_sectors.clear()
# -- Fog state: hidden tile default (#6) --------------------------------------
func test_fog_hidden_tile_value() -> void:
# P1 #6: Tile never in visible_positions → _vis_bytes == VIS_HIDDEN (0).
# Verifies default state after _resize() and after update with other visible tiles.
var fog = _get_fog_state()
if fog == null:
return
GameState.visible_tiles = []
fog._resize(Rect2i(0, 0, 64, 64))
# Check a tile that was never made visible (30,30)
var idx: int = 30 * 64 + 30
assert_that(fog._vis_bytes[idx]).override_failure_message(
"Never-visible tile should be VIS_HIDDEN=%d after resize" % FogState.VIS_HIDDEN
).is_equal(FogState.VIS_HIDDEN)
# Also verify it stays VIS_HIDDEN after an update that makes OTHER tiles visible
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.visibility_sectors = {Vector2i(5, 5): "Forward"}
fog.update_from_state()
assert_that(fog._vis_bytes[idx]).override_failure_message(
"Non-visible tile should remain VIS_HIDDEN=%d after update" % FogState.VIS_HIDDEN
).is_equal(FogState.VIS_HIDDEN)
GameState.visible_positions.clear()
GameState.visibility_sectors.clear()
# -- Entity lifecycle: removal on leaving visibility (#7) ----------------------
func test_entity_removed_when_leaving_visibility() -> void:
# P1 #7: Entity present in one update, absent in next → entity_nodes empty.
# Verifies removal (queue_free), not just hiding.
var renderer := _make_entity_renderer()
var entity := [{"entity_id": 42, "x": 5.0, "y": 5.0, "z": 0,
"kind": {"variant": "Npc", "data": null}}]
renderer.update_entities(entity)
assert_that(renderer.entity_nodes.size()).override_failure_message(
"Entity should exist after first update"
).is_equal(1)
assert_that(renderer.entity_nodes.has(42)).is_true()
# Entity leaves visibility — removed from entity_nodes, node queue_free'd
renderer.update_entities([])
assert_that(renderer.entity_nodes.size()).override_failure_message(
"Entity should be removed when absent from update"
).is_equal(0)
assert_that(renderer.entity_nodes.has(42)).is_false()
renderer.queue_free()
# -- Entity lifecycle: creation on first appearance (#8) -----------------------
func test_entity_created_on_first_appearance() -> void:
# P1 #8: Empty renderer → update with 1 entity → entity_nodes.size() == 1,
# child exists in scene tree.
var renderer := _make_entity_renderer()
assert_that(renderer.entity_nodes.size()).override_failure_message(
"Fresh renderer should have no entities"
).is_equal(0)
var entity := [{"entity_id": 99, "x": 3.0, "y": 7.0, "z": 0,
"kind": {"variant": "Npc", "data": null}}]
renderer.update_entities(entity)
assert_that(renderer.entity_nodes.size()).override_failure_message(
"Renderer should have 1 entity after update"
).is_equal(1)
assert_that(renderer.entity_nodes.has(99)).is_true()
# Verify child actually exists in the scene tree
var node = renderer.entity_nodes[99]
assert_that(node.get_parent()).is_equal(renderer)
renderer.queue_free()
# -- Pending recognition blob rendering (#9) ----------------------------------
func test_pending_recognition_blob_rendering() -> void:
# P1 #9: pending_recognitions with 1 entry → FogEntities tracks blob entity
# (not a full entity sprite — drawn via _draw(), data in _entities dict).
var fog_entities := _make_fog_entities()
var saved_recognitions := GameState.pending_recognitions
GameState.pending_recognitions = [{
"entity_id": 77,
"x": 10.0,
"y": 15.0,
"z": 0,
"remaining_ticks": 5,
"total_delay_ticks": 10,
}]
fog_entities.update_from_state()
# FogEntities uses _draw() with no child nodes — verify internal tracking
assert_that(fog_entities._entities.size()).override_failure_message(
"FogEntities should track 1 blob entity"
).is_equal(1)
assert_that(fog_entities._entities.has(77)).is_true()
# Verify position stored correctly
var blob_data: Dictionary = fog_entities._entities[77]
assert_that(blob_data.pos).is_equal(Vector2(10.0, 15.0))
# Recognition progress should be calculated: 1.0 - 5/10 = 0.5
assert_that(blob_data.progress).is_equal_approx(0.5, 0.01)
# New entity triggers a sonar ping (D-059)
assert_that(fog_entities._pings.size()).override_failure_message(
"New fog entity should trigger a sonar ping"
).is_greater_equal(1)
# Cleanup
GameState.pending_recognitions = saved_recognitions
fog_entities.queue_free()