## Greybox tile store contract (T-1088 design §3, §10.4): accumulation across ## LOS-filtered snapshots, never-evict / last-observation-wins, and the ## four-state visibility flips — on synthetic GameState.visible_tiles dicts, ## plus a fixture replay through the full decode pipeline ## (tests/fixtures/gauntlet/*.msgpack -> Protocol.decode_snapshot() -> ## GameState.apply_snapshot() -> Store), per design-input §1.6. ## ## Headless by design: only the Store inner class (RefCounted) is exercised — ## the GreyboxWorld painter node (MultiMesh writes, shaders) is §10.1/§10.2's ## live/visual job. class_name TestGreyboxStore extends GdUnitTestSuite const Store := GreyboxWorld.Store ## Repo-relative fixture dir (outside res:// — resolved via globalize, the ## visual_capture.gd:177-179 pattern). Regenerate: make fixtures-gauntlet. const FIXTURE_DIR := "tests/fixtures/gauntlet" ## Live-wire dict shape after the snapshot_handler.gd:52-64 merge: ## {x, y, z, visibility, type} with type already lowercased by protocol.gd. func _tile(x: int, y: int, type: String, visibility: String = "Forward") -> Dictionary: return {"x": x, "y": y, "z": 0, "visibility": visibility, "type": type} ## TestHarness "tiles" shape — no visibility key (design-input §1.5). func _harness_tile(x: int, y: int, type: String) -> Dictionary: return {"x": x, "y": y, "z": 0, "type": type} # -- accumulation + never-evict (§3.1) ------------------------------------------ func test_ingest_accumulates_across_snapshots() -> void: var store := Store.new() store.ingest([_tile(1, 1, "floor"), _tile(2, 1, "wall")]) store.ingest([_tile(3, 1, "floor"), _tile(4, 1, "floor")]) assert_int(store.size()).is_equal(4) assert_bool(store.has_tile(Vector3i(1, 1, 0))).is_true() assert_bool(store.has_tile(Vector3i(4, 1, 0))).is_true() func test_added_reported_once_then_diff_stays_empty() -> void: var store := Store.new() var first: Dictionary = store.ingest([_tile(1, 1, "floor")]) var second: Dictionary = store.ingest([_tile(1, 1, "floor")]) assert_array(first["added"]).contains_exactly([Vector3i(1, 1, 0)]) assert_array(second["added"]).is_empty() assert_array(second["rekinded"]).is_empty() assert_array(second["recolored"]).is_empty() func test_never_evicts() -> void: # Walked away — LOS goes empty for many snapshots; the tile stays known. var store := Store.new() store.ingest([_tile(5, 5, "wall")]) for i in 10: store.ingest([]) assert_int(store.size()).is_equal(1) assert_bool(store.has_tile(Vector3i(5, 5, 0))).is_true() assert_int(store.kind_of(Vector3i(5, 5, 0))).is_equal(Store.Kind.WALL) func test_kinds_parse_and_doors_collapse_to_floor() -> void: var store := Store.new() store.ingest( [_tile(1, 0, "floor"), _tile(2, 0, "wall"), _tile(3, 0, "door"), _tile(4, 0, "object")] ) assert_int(store.kind_of(Vector3i(1, 0, 0))).is_equal(Store.Kind.FLOOR) assert_int(store.kind_of(Vector3i(2, 0, 0))).is_equal(Store.Kind.WALL) # Wire tile_kind is walkability-derived (query.rs:78-82); doors are entities # on the wire — Door/Object collapse to FLOOR (§3.1). assert_int(store.kind_of(Vector3i(3, 0, 0))).is_equal(Store.Kind.FLOOR) assert_int(store.kind_of(Vector3i(4, 0, 0))).is_equal(Store.Kind.FLOOR) func test_last_observation_wins_kind_flip() -> void: var store := Store.new() store.ingest([_tile(7, 7, "wall")]) var diff: Dictionary = store.ingest([_tile(7, 7, "floor")]) assert_int(store.kind_of(Vector3i(7, 7, 0))).is_equal(Store.Kind.FLOOR) assert_array(diff["rekinded"]).contains_exactly([Vector3i(7, 7, 0)]) assert_array(diff["added"]).is_empty() assert_int(store.size()).is_equal(1) func test_z_level_distinguishes_tiles() -> void: var store := Store.new() store.ingest( [ {"x": 1, "y": 1, "z": 0, "visibility": "Forward", "type": "floor"}, {"x": 1, "y": 1, "z": 1, "visibility": "Forward", "type": "wall"}, ] ) assert_int(store.size()).is_equal(2) assert_int(store.kind_of(Vector3i(1, 1, 0))).is_equal(Store.Kind.FLOOR) assert_int(store.kind_of(Vector3i(1, 1, 1))).is_equal(Store.Kind.WALL) func test_malformed_entries_are_skipped() -> void: var store := Store.new() store.ingest([42, {"y": 3}, _tile(1, 1, "floor")]) assert_int(store.size()).is_equal(1) # -- four-state visibility (§3.2) ------------------------------------------------- func test_visibility_states_parse() -> void: var store := Store.new() store.ingest( [ _tile(1, 0, "floor", "Forward"), _tile(2, 0, "floor", "Peripheral"), _tile(3, 0, "wall", "BoundaryWall"), ] ) assert_int(store.state_of(Vector3i(1, 0, 0))).is_equal(Store.Vis.FORWARD) assert_int(store.state_of(Vector3i(2, 0, 0))).is_equal(Store.Vis.PERIPHERAL) assert_int(store.state_of(Vector3i(3, 0, 0))).is_equal(Store.Vis.BOUNDARY_WALL) func test_harness_tiles_without_visibility_read_forward() -> void: var store := Store.new() store.ingest([_harness_tile(1, 1, "floor")]) assert_int(store.state_of(Vector3i(1, 1, 0))).is_equal(Store.Vis.FORWARD) func test_out_of_sight_flips_to_remembered_exactly_once() -> void: var store := Store.new() store.ingest([_tile(1, 1, "floor")]) var leave: Dictionary = store.ingest([]) assert_int(store.state_of(Vector3i(1, 1, 0))).is_equal(Store.Vis.REMEMBERED) assert_array(leave["recolored"]).contains_exactly([Vector3i(1, 1, 0)]) # Still-remembered tiles must not re-emit the flip on later snapshots — # diff-only painting depends on it (§3.1, no full-buffer rebuilds). var later: Dictionary = store.ingest([]) assert_array(later["recolored"]).is_empty() func test_four_state_flip_sequence() -> void: # Forward -> Peripheral -> BoundaryWall -> (absent) Remembered -> Forward, # each flip emitted as exactly one recolor — never a re-add. var store := Store.new() var key := Vector3i(9, 9, 0) store.ingest([_tile(9, 9, "wall", "Forward")]) assert_int(store.state_of(key)).is_equal(Store.Vis.FORWARD) var to_peripheral: Dictionary = store.ingest([_tile(9, 9, "wall", "Peripheral")]) assert_int(store.state_of(key)).is_equal(Store.Vis.PERIPHERAL) assert_array(to_peripheral["recolored"]).contains_exactly([key]) var to_boundary: Dictionary = store.ingest([_tile(9, 9, "wall", "BoundaryWall")]) assert_int(store.state_of(key)).is_equal(Store.Vis.BOUNDARY_WALL) assert_array(to_boundary["recolored"]).contains_exactly([key]) var to_remembered: Dictionary = store.ingest([]) assert_int(store.state_of(key)).is_equal(Store.Vis.REMEMBERED) assert_array(to_remembered["recolored"]).contains_exactly([key]) var back: Dictionary = store.ingest([_tile(9, 9, "wall", "Forward")]) assert_int(store.state_of(key)).is_equal(Store.Vis.FORWARD) assert_array(back["recolored"]).contains_exactly([key]) assert_array(back["added"]).is_empty() assert_int(store.size()).is_equal(1) func test_never_seen_is_the_void() -> void: # Never-seen tiles have no state — nothing rendered; perception is upstream, # server-enforced (§3.2). var store := Store.new() store.ingest([_tile(1, 1, "floor")]) assert_bool(store.has_tile(Vector3i(99, 99, 0))).is_false() assert_int(store.state_of(Vector3i(99, 99, 0))).is_equal(-1) assert_int(store.kind_of(Vector3i(99, 99, 0))).is_equal(-1) # -- to_dict() persistence seam (design §0 scope fence) ---------------------------- func test_to_dict_exports_knowledge_copy() -> void: var store := Store.new() store.ingest([_tile(1, 1, "floor"), _tile(2, 1, "wall")]) var exported: Dictionary = store.to_dict() assert_int(exported.size()).is_equal(2) assert_int(exported[Vector3i(1, 1, 0)]).is_equal(Store.Kind.FLOOR) assert_int(exported[Vector3i(2, 1, 0)]).is_equal(Store.Kind.WALL) # A copy — the seam must not expose live store state. exported.erase(Vector3i(1, 1, 0)) assert_int(store.size()).is_equal(2) # -- fixture replay (design §10.4; full pipeline per design-input §1.6) ------------- func test_fixture_replay_hub_spawn_then_movement() -> void: # Fixture bytes -> Protocol.decode_snapshot() (real wire decode incl. the # tile_kind -> type mapping) -> GameState.apply_snapshot() (wire-spelling # merge) -> Store — the exact path the live sandbox runs. Expectations are # computed from the decoded arrays, not hardcoded, so fixture regeneration # cannot silently skew the assertions. var spawn: Dictionary = _decode_fixture("hub_spawn") var moved: Dictionary = _decode_fixture("hub_after_movement") var spawn_keys := _key_set(spawn["visible_tiles"]) var moved_keys := _key_set(moved["visible_tiles"]) assert_bool(spawn_keys.is_empty()).is_false() assert_bool(moved_keys.is_empty()).is_false() var store := Store.new() GameState.apply_snapshot(spawn) var first: Dictionary = store.ingest(GameState.visible_tiles) assert_int(store.size()).is_equal(spawn_keys.size()) assert_int((first["added"] as Array).size()).is_equal(spawn_keys.size()) # The Hub spawn frame discloses both kinds — walls fringe the floor cone. assert_bool(_contains_kind(store, spawn_keys, Store.Kind.FLOOR)).is_true() assert_bool(_contains_kind(store, spawn_keys, Store.Kind.WALL)).is_true() # The Hub spawn tile (50, 58) itself is a disclosed floor (design-input §1.6). assert_int(store.kind_of(Vector3i(50, 58, 0))).is_equal(Store.Kind.FLOOR) GameState.apply_snapshot(moved) store.ingest(GameState.visible_tiles) # Accumulation: the union of both frames, nothing evicted. var union := spawn_keys.duplicate() union.merge(moved_keys) assert_int(store.size()).is_equal(union.size()) # Every tile of the moved frame is visible now (never REMEMBERED)... var visible_marked_remembered := 0 for key: Vector3i in moved_keys: if store.state_of(key) == Store.Vis.REMEMBERED: visible_marked_remembered += 1 assert_int(visible_marked_remembered).is_equal(0) # ...and every spawn-only tile has dimmed to REMEMBERED. var stale_not_remembered := 0 for key: Vector3i in spawn_keys: if not moved_keys.has(key) and store.state_of(key) != Store.Vis.REMEMBERED: stale_not_remembered += 1 assert_int(stale_not_remembered).is_equal(0) func _decode_fixture(fixture_name: String) -> Dictionary: # Fixtures live outside res:// — resolve the repo root from the project dir # (pattern: visual_capture.gd:177-179). var repo_root := ProjectSettings.globalize_path("res://").rstrip("/").get_base_dir() var path := repo_root.path_join(FIXTURE_DIR).path_join(fixture_name + ".msgpack") var file := FileAccess.open(path, FileAccess.READ) assert_that(file).is_not_null().override_failure_message( "Gauntlet fixture missing: %s — run 'make fixtures-gauntlet'" % path ) var bytes := file.get_buffer(file.get_length()) file.close() var snapshot: Variant = Protocol.decode_snapshot(bytes) assert_bool(snapshot is Dictionary).is_true() return snapshot if snapshot is Dictionary else {} func _key_set(tiles: Array) -> Dictionary: var keys: Dictionary = {} for tile: Dictionary in tiles: keys[Vector3i(int(tile["x"]), int(tile["y"]), int(tile["z"]))] = true return keys func _contains_kind(store: GreyboxWorld.Store, keys: Dictionary, kind: int) -> bool: for key: Vector3i in keys: if store.kind_of(key) == kind: return true return false