## Sprint 16 #543: GameState.current_zone_id population tests. ## Verifies that apply_snapshot() correctly extracts zone_id from the player's ## current tile after the O(N) → O(1) refactor. Behavior must be identical ## before and after the change (net-zero behavioral change per ticket spec). ## Spec refs: D-073, #543, #529. class_name TestSnapshotZoneId extends GdUnitTestSuite func before_test() -> void: GameState.current_zone_id = "" GameState.player_position = Vector2.ZERO GameState.visible_tiles = [] GameState.visible_positions = {} GameState.visibility_sectors = {} # -- Happy path (S16-Z01) ------------------------------------------------------ func test_zone_id_populated_when_player_on_zone_tile() -> void: # S16-Z01: Player at (5,5), visible_tiles has zone_id "zone_alpha" at (5,5). # D-073: current_zone_id must be set to the server-authoritative zone_id. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 3, "y": 3, "z": 0, "type": "floor", "zone_id": "zone_foyer"}, {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, {"x": 7, "y": 7, "z": 0, "type": "floor", "zone_id": "zone_beta"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "current_zone_id must match zone_id at player tile (5,5)" ).is_equal("zone_alpha") assert_that(GameState.current_zone_id.is_empty()).override_failure_message( "current_zone_id must be non-empty when player is on a zone-tagged tile" ).is_false() # -- Missing zone_id field (S16-Z02) ------------------------------------------- func test_zone_id_empty_when_tile_lacks_zone_field() -> void: # S16-Z02: Player's tile exists but has no zone_id key → empty string. # Server may send tiles without zone_id when tile is unzoned. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "Tile without zone_id field → current_zone_id must default to empty string" ).is_equal("") # -- Player off-tile (S16-Z03) ------------------------------------------------- func test_zone_id_empty_when_player_not_on_any_tile() -> void: # S16-Z03: Player at (99,99) but no tile at that position → empty string. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 99.0, "y": 99.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "Player at (99,99) with no matching tile → current_zone_id must be empty" ).is_equal("") # -- Empty tiles (S16-Z04) ----------------------------------------------------- func test_zone_id_empty_on_empty_visible_tiles() -> void: # S16-Z04: No visible tiles at all → empty string, no crash. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [], }) assert_that(GameState.current_zone_id).override_failure_message( "Empty visible_tiles → current_zone_id must be empty string (no crash)" ).is_equal("") # -- Correct tile selected among many (S16-Z05) -------------------------------- func test_zone_id_selects_correct_tile_among_many() -> void: # S16-Z05: 10x10 tile grid, player at (6,4). Only zone_6_4 must be selected. # Tests that the lookup doesn't accidentally match a neighboring tile. var tiles: Array = [] for tx in range(10): for ty in range(10): tiles.append({"x": tx, "y": ty, "z": 0, "type": "floor", "zone_id": "zone_%d_%d" % [tx, ty]}) GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 6.0, "y": 4.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": tiles, }) assert_that(GameState.current_zone_id).override_failure_message( "Player at (6,4) must get zone_6_4, not a neighboring tile" ).is_equal("zone_6_4") # -- Zone transition (S16-Z06) ------------------------------------------------- func test_zone_id_updates_on_zone_transition() -> void: # S16-Z06: Player moves from zone_alpha (5,5) to zone_beta (6,5). # current_zone_id must update on each snapshot. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, {"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta"}, ], }) assert_that(GameState.current_zone_id).is_equal("zone_alpha") GameState.apply_snapshot({ "tick": 2, "entities": [ {"entity_id": 1, "x": 6.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, {"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "After moving to (6,5), current_zone_id must update to zone_beta" ).is_equal("zone_beta") # -- Regression: visible_positions unaffected (S16-Z07) ----------------------- func test_visible_positions_unaffected_by_zone_id_refactor() -> void: # S16-Z07: The O(1) refactor must not break visible_positions population. # Both zone_id and visible_positions derive from the same visible_tiles loop — # verify both are correctly populated after a single apply_snapshot(). GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha", "visibility": "Forward"}, {"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_beta", "visibility": "Peripheral"}, ], }) assert_that(GameState.visible_positions.has(Vector2i(5, 5))).override_failure_message( "visible_positions must still contain (5,5) after zone_id refactor" ).is_true() assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_true() assert_that(GameState.current_zone_id).override_failure_message( "current_zone_id must be zone_alpha after same apply_snapshot call" ).is_equal("zone_alpha") # -- Fractional player position (S16-Z08) -------------------------------------- func test_zone_id_uses_int_truncation_of_player_position() -> void: # S16-Z08: Player at (5.7, 5.9) → int(5.7)=5, int(5.9)=5 → matches tile (5,5). # Server sends player coords as floats; zone lookup must truncate to tile index. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.7, "y": 5.9, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "Player at (5.7,5.9) must match tile (5,5) — int() truncation applies" ).is_equal("zone_alpha") # -- Test-mode tiles key (S16-Z09) -------------------------------------------- func test_zone_id_works_with_test_mode_tiles_key() -> void: # S16-Z09: Test mode sends "tiles" key, not "visible_tiles". # The O(1) refactor uses member visible_tiles (covers both paths). # Regression guard: if _tile_by_coord is moved into the snapshot.visible_tiles # block only, this test fails — catching the exact regression Tyre flagged. GameState.apply_snapshot({ "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_alpha"}, ], }) assert_that(GameState.current_zone_id).override_failure_message( "Test-mode 'tiles' key must populate zone_id via member visible_tiles" ).is_equal("zone_alpha")