Merge remote-tracking branch 'origin/client'
This commit is contained in:
@@ -123,7 +123,7 @@ func test_game_state_warns_on_missing_player() -> void:
|
||||
# -- SimBridge: test data completeness --
|
||||
|
||||
func test_sim_bridge_test_snapshot_has_tiles() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.has("tiles")).is_true()
|
||||
assert_that(snap.tiles.size()).is_greater(0)
|
||||
@@ -133,7 +133,7 @@ func test_sim_bridge_test_snapshot_has_tiles() -> void:
|
||||
assert_that(tile.has("type")).is_true()
|
||||
|
||||
func test_sim_bridge_test_snapshot_has_visible_positions() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.has("visible_positions")).is_true()
|
||||
assert_that(snap.visible_positions.size()).is_greater(0)
|
||||
@@ -142,7 +142,7 @@ func test_sim_bridge_test_snapshot_has_visible_positions() -> void:
|
||||
assert_that(pos.has("y")).is_true()
|
||||
|
||||
func test_sim_bridge_test_snapshot_has_player_entity() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
var has_player := false
|
||||
for entity in snap.entities:
|
||||
@@ -152,7 +152,7 @@ func test_sim_bridge_test_snapshot_has_player_entity() -> void:
|
||||
assert_that(has_player).is_true()
|
||||
|
||||
func test_sim_bridge_test_snapshot_has_npc() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
var has_npc := false
|
||||
for entity in snap.entities:
|
||||
@@ -162,7 +162,7 @@ func test_sim_bridge_test_snapshot_has_npc() -> void:
|
||||
assert_that(has_npc).is_true()
|
||||
|
||||
func test_sim_bridge_test_tiles_contain_all_types() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
var types: Dictionary = {}
|
||||
for tile in snap.tiles:
|
||||
@@ -172,7 +172,7 @@ func test_sim_bridge_test_tiles_contain_all_types() -> void:
|
||||
assert_that(types.has("door")).is_true()
|
||||
|
||||
func test_sim_bridge_test_snapshot_has_v2_fields() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap = SimBridge._test_snapshot()
|
||||
assert_that(snap.has("version")).is_true()
|
||||
assert_that(snap.version).is_equal(2)
|
||||
|
||||
@@ -51,8 +51,25 @@ func test_missing_fields_partial_update() -> void:
|
||||
assert_that(GameState.player_position).is_equal(Vector2(10, 15))
|
||||
assert_that(GameState.current_tick).is_equal(2)
|
||||
|
||||
func test_sim_bridge_los_clear_path() -> void:
|
||||
# No wall between (10,10) and (10,8) — clear LOS
|
||||
assert_that(SimBridge._test_has_los(Vector2i(10, 10), Vector2i(10, 8))).is_true()
|
||||
|
||||
func test_sim_bridge_los_blocked_by_wall() -> void:
|
||||
# Wall at (12,10) blocks LOS from (10,10) to (12,9) via east path
|
||||
# Direct line from (10,10) → (12,9) passes through (11,10) then (12,10) — wall
|
||||
assert_that(SimBridge._test_has_los(Vector2i(10, 10), Vector2i(14, 10))).is_false()
|
||||
|
||||
func test_sim_bridge_los_diagonal() -> void:
|
||||
# Diagonal LOS from (10,10) to (11,9) — no wall in path
|
||||
assert_that(SimBridge._test_has_los(Vector2i(10, 10), Vector2i(11, 9))).is_true()
|
||||
|
||||
func test_sim_bridge_los_same_position() -> void:
|
||||
# LOS to self is always true
|
||||
assert_that(SimBridge._test_has_los(Vector2i(10, 10), Vector2i(10, 10))).is_true()
|
||||
|
||||
func test_sim_bridge_test_snapshot_deterministic() -> void:
|
||||
SimBridge._test_tick = 0
|
||||
SimBridge.reset_test_state()
|
||||
var snap1 = SimBridge._test_snapshot()
|
||||
var snap2 = SimBridge._test_snapshot()
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
## Sprint 2 Proof: Fog of Perception (#357)
|
||||
## Verifies all 7 acceptance criteria through the full server pipeline:
|
||||
## AC1: Player moves, AC2: Camera follows (via player_position),
|
||||
## AC3: Tiles render (visible_tiles non-empty), AC4: Entities via LOS,
|
||||
## AC5: Fog (not all tiles visible), AC6: Walls hide, AC7: Corner reveal.
|
||||
## Requires: server binary built (cargo build in server/)
|
||||
##
|
||||
## Server proof room layout:
|
||||
## (16,13) = NPC (16,14) = WALL (16,16) = Player start
|
||||
## Player facing North → NPC blocked by wall.
|
||||
## Move East+North around the wall → NPC becomes visible.
|
||||
class_name TestSprint2Proof
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const CONNECT_TIMEOUT: float = 3.0
|
||||
const RESPONSE_TIMEOUT: float = 5.0
|
||||
const MAX_PORT_ATTEMPTS: int = 5
|
||||
|
||||
var _server_pid: int = -1
|
||||
var _bridge: LocalBridge = null
|
||||
var _test_port: int = 0
|
||||
|
||||
|
||||
func _server_binary_path() -> String:
|
||||
var project_dir := ProjectSettings.globalize_path("res://")
|
||||
return project_dir.path_join("../server/target/debug/settled-reach-server")
|
||||
|
||||
|
||||
static func _random_test_port() -> int:
|
||||
return 49152 + (randi() % (65535 - 49152 + 1))
|
||||
|
||||
|
||||
func _spawn_server(server_path: String) -> bool:
|
||||
for attempt in range(MAX_PORT_ATTEMPTS):
|
||||
_test_port = _random_test_port()
|
||||
var addr := "127.0.0.1:%d" % _test_port
|
||||
_server_pid = OS.create_process(server_path, [addr])
|
||||
if _server_pid <= 0:
|
||||
continue
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
if OS.is_process_running(_server_pid):
|
||||
return true
|
||||
_server_pid = -1
|
||||
return false
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
if _bridge != null:
|
||||
_bridge.disconnect_from_server()
|
||||
_bridge = null
|
||||
if _server_pid > 0 and OS.is_process_running(_server_pid):
|
||||
OS.kill(_server_pid)
|
||||
_server_pid = -1
|
||||
|
||||
|
||||
## Send a batch input and receive the snapshot response.
|
||||
func _send_and_receive(action_name: String, tick: int = 0) -> Variant:
|
||||
var inputs: Array = [{"tick": tick, "action_name": action_name}]
|
||||
var encoded := Protocol.encode_player_inputs(inputs)
|
||||
assert_that(encoded.size()).is_greater(0)
|
||||
var send_err := _bridge.send_message(encoded)
|
||||
assert_that(send_err).is_equal(OK)
|
||||
|
||||
var snapshot_bytes := PackedByteArray()
|
||||
var elapsed := 0.0
|
||||
while elapsed < RESPONSE_TIMEOUT:
|
||||
_bridge.poll()
|
||||
snapshot_bytes = _bridge.poll_message()
|
||||
if snapshot_bytes.size() > 0:
|
||||
break
|
||||
await get_tree().create_timer(0.05).timeout
|
||||
elapsed += 0.05
|
||||
|
||||
assert_that(snapshot_bytes.size()).is_greater(0)
|
||||
var snapshot: Variant = Protocol.decode_snapshot(snapshot_bytes)
|
||||
assert_that(snapshot).is_not_null()
|
||||
return snapshot
|
||||
|
||||
|
||||
## Connect to server, returning true on success.
|
||||
func _connect_to_server() -> bool:
|
||||
var server_path := _server_binary_path()
|
||||
if not FileAccess.file_exists(server_path):
|
||||
push_warning("Sprint 2 proof skipped: server binary not found at %s" % server_path)
|
||||
return false
|
||||
|
||||
var spawned := await _spawn_server(server_path)
|
||||
if not spawned:
|
||||
return false
|
||||
|
||||
_bridge = LocalBridge.new()
|
||||
var elapsed := 0.0
|
||||
while elapsed < CONNECT_TIMEOUT:
|
||||
if _server_pid > 0 and not OS.is_process_running(_server_pid):
|
||||
push_warning("Server process died during connection")
|
||||
return false
|
||||
if _bridge.get_status() == StreamPeerTCP.STATUS_NONE:
|
||||
_bridge.connect_to_server("127.0.0.1", _test_port)
|
||||
_bridge.poll()
|
||||
if _bridge.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||
return true
|
||||
if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR:
|
||||
_bridge.disconnect_from_server()
|
||||
_bridge.reset()
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
elapsed += 0.1
|
||||
|
||||
return false
|
||||
|
||||
|
||||
# -- AC#1, AC#2, AC#3, AC#5: Movement, camera, tiles, fog -------------------------
|
||||
|
||||
func test_proof_player_moves_and_v2_snapshot() -> void:
|
||||
var ok := await _connect_to_server()
|
||||
if not ok:
|
||||
return
|
||||
|
||||
var snapshot: Dictionary = await _send_and_receive("MoveNorth")
|
||||
|
||||
# AC#1: Player moved from (16,16) to (16,15)
|
||||
var player: Dictionary = snapshot.entities[0]
|
||||
assert_float(player.x).is_equal_approx(16.5, 0.001)
|
||||
assert_float(player.y).is_equal_approx(15.5, 0.001)
|
||||
assert_that(player.kind.variant).is_equal("Player")
|
||||
|
||||
# v2 protocol fields present
|
||||
assert_that(snapshot.version).is_equal(2)
|
||||
assert_that(snapshot.player_facing).is_equal("North")
|
||||
assert_that(snapshot.game_time).is_not_null()
|
||||
|
||||
# AC#3: Tiles flowing through bridge (visible_tiles non-empty)
|
||||
assert_that(snapshot.visible_tiles.size()).is_greater(0)
|
||||
|
||||
# AC#5: Not all 1024 tiles visible — blind spot behind player
|
||||
assert_that(snapshot.visible_tiles.size()).is_less(1024)
|
||||
|
||||
|
||||
# -- AC#6: Wall hides entity -------------------------------------------------------
|
||||
|
||||
func test_proof_wall_hides_entity() -> void:
|
||||
var ok := await _connect_to_server()
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# After MoveNorth: player at (16,15) facing North.
|
||||
# Wall at (16,14) blocks LOS to NPC at (16,13).
|
||||
var snapshot: Dictionary = await _send_and_receive("MoveNorth")
|
||||
|
||||
# Only player should be visible — NPC is behind wall
|
||||
var npc_count := 0
|
||||
for entity in snapshot.entities:
|
||||
if entity.kind.variant == "Npc":
|
||||
npc_count += 1
|
||||
assert_that(npc_count).is_equal(0)
|
||||
|
||||
|
||||
# -- AC#4, AC#7: Entity appears via LOS / corner reveal ----------------------------
|
||||
|
||||
func test_proof_corner_reveal() -> void:
|
||||
var ok := await _connect_to_server()
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# Step 1: Move East twice to get beside the wall
|
||||
# (16,16) → MoveEast → (17,16) → MoveEast → (18,16)
|
||||
await _send_and_receive("MoveEast", 0)
|
||||
await _send_and_receive("MoveEast", 1)
|
||||
|
||||
# Step 2: Move North past the wall line (y=14)
|
||||
# (18,16) → MoveNorth → (18,15) → MoveNorth → (18,14) → MoveNorth → (18,13)
|
||||
await _send_and_receive("MoveNorth", 2)
|
||||
await _send_and_receive("MoveNorth", 3)
|
||||
var snapshot: Dictionary = await _send_and_receive("MoveNorth", 4)
|
||||
|
||||
# Player at (18,13) facing North. NPC at (16,13) is 2 tiles west —
|
||||
# within peripheral cone, no wall between. NPC should be visible.
|
||||
var npc_found := false
|
||||
for entity in snapshot.entities:
|
||||
if entity.kind.variant == "Npc":
|
||||
assert_float(entity.x).is_equal_approx(16.5, 0.001)
|
||||
assert_float(entity.y).is_equal_approx(13.5, 0.001)
|
||||
npc_found = true
|
||||
break
|
||||
assert_that(npc_found).is_true()
|
||||
Reference in New Issue
Block a user