refactor(client): optimize zone_id extraction from O(N) to O(1) lookup (#543)

Build _tile_by_coord dictionary from member visible_tiles (covers both
test-mode "tiles" key and live-server "visible_tiles" key), then replace
the linear scan with a single dict lookup. Net-zero complexity: adds one
dict-set per tile in an existing iteration, removes the separate scan loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 21:01:22 +01:00
co-authored by Claude Opus 4.6
parent 41e3b4a9b3
commit ebc973b558
2 changed files with 221 additions and 10 deletions
+10 -10
View File
@@ -240,16 +240,16 @@ func apply_snapshot(snapshot: Dictionary) -> void:
medium_sound_events = []
close_sound_events = []
# D-073 (#529): Extract zone_id from the player's current tile (server-authoritative).
# O(1) via visible_positions dict would be ideal, but tiles are arrays without
# positional indexing — use the same tile iteration below instead.
current_zone_id = ""
var _px := int(player_position.x)
var _py := int(player_position.y)
for _ztile in visible_tiles:
if _ztile is Dictionary and _ztile.get("x") == _px and _ztile.get("y") == _py:
current_zone_id = _ztile.get("zone_id", "")
break
# D-073 (#529): O(1) zone_id lookup. Build coord→tile dict from member visible_tiles
# (populated above from either "tiles" test-mode key or "visible_tiles" live key).
# Must use the member var, not snapshot.visible_tiles, so test mode is covered.
var _tile_by_coord: Dictionary = {}
for vtile in visible_tiles:
if vtile is Dictionary and vtile.has("x") and vtile.has("y"):
_tile_by_coord[Vector2i(vtile.x, vtile.y)] = vtile
var player_pos_key := Vector2i(int(player_position.x), int(player_position.y))
var player_tile = _tile_by_coord.get(player_pos_key, null)
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
+211
View File
@@ -0,0 +1,211 @@
## 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")