Compare commits
2 Commits
278255d409
...
c3e9eda702
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3e9eda702 | ||
|
|
705cae199e |
8
assets/tiles/walls_tilemap.tilemap.json
Normal file
8
assets/tiles/walls_tilemap.tilemap.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "walls_tilemap",
|
||||
"tile_size": 32,
|
||||
"tiles": [
|
||||
{"id": "wall_brick", "walkable": false, "color": [180, 100, 80]},
|
||||
{"id": "wall_wood", "walkable": false, "color": [140, 90, 50]}
|
||||
]
|
||||
}
|
||||
217
init.lua
217
init.lua
@@ -8,8 +8,217 @@ local maps = require("lib-core.maps")
|
||||
local M = {}
|
||||
|
||||
function M.run_tests(ctx)
|
||||
-- Setup
|
||||
local demo_map = maps.load("maps/demo.map.json")
|
||||
-- ---------- Task 1: GID encode/decode ----------
|
||||
engine.test.equals(maps.encode_gid(0, 0, 0), 0, "encode_gid(0,0,0) = 0 (empty sentinel)")
|
||||
engine.test.equals(maps.encode_gid(0, 12, 0), 192, "encode_gid(0,12,0) = 192 (= 12<<4)")
|
||||
engine.test.equals(maps.encode_gid(0, 12, 1), 196, "encode_gid(0,12,1) = 196 (= 12<<4 | 1<<2)")
|
||||
engine.test.equals(maps.encode_gid(1, 5, 0), 16777296, "encode_gid(1,5,0) = 16777296 (= 1<<24 | 5<<4)")
|
||||
engine.test.equals(maps.encode_gid(255, 1048575, 3), 0xFFFFFFFC, "encode_gid max-values fills 32 bits except reserved")
|
||||
|
||||
local a, t, r = maps.decode_gid(196)
|
||||
engine.test.equals(a, 0, "decode_gid(196) atlas = 0")
|
||||
engine.test.equals(t, 12, "decode_gid(196) tile = 12")
|
||||
engine.test.equals(r, 1, "decode_gid(196) rot = 1")
|
||||
|
||||
-- Round-trip
|
||||
for atlas = 0, 3 do
|
||||
for tile = 0, 100, 17 do
|
||||
for rot = 0, 3 do
|
||||
local gid = maps.encode_gid(atlas, tile, rot)
|
||||
local a2, t2, r2 = maps.decode_gid(gid)
|
||||
engine.test.equals(a2, atlas, "round-trip atlas")
|
||||
engine.test.equals(t2, tile, "round-trip tile")
|
||||
engine.test.equals(r2, rot, "round-trip rot")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------- Task 2: Schema-v2 + v1→v2 migration ----------
|
||||
-- Build a v1-style map table in-memory
|
||||
local v1_table = {
|
||||
id = "test_v1",
|
||||
tilemap = "demo_tilemap",
|
||||
size = { w = 2, h = 2 },
|
||||
tiles = { 1, 2, 2, 1 },
|
||||
tile_rotations = { 0, 90, 180, 270 },
|
||||
}
|
||||
local v2 = maps.upgrade_v1_to_v2(v1_table)
|
||||
engine.test.equals(v2.schema_version, 2, "upgraded schema_version = 2")
|
||||
engine.test.equals(v2.id, "test_v1", "upgraded id preserved")
|
||||
engine.test.equals(v2.size.w, 2, "upgraded size.w preserved")
|
||||
engine.test.equals(#v2.atlases, 1, "upgraded atlases[] has 1 entry")
|
||||
engine.test.equals(v2.atlases[1], "demo_tilemap", "upgraded atlases[0] = v1 tilemap")
|
||||
engine.test.assert(v2.layers.surface ~= nil, "upgraded has surface layer")
|
||||
engine.test.equals(#v2.layers.surface.tiles, 4, "upgraded surface tiles count")
|
||||
-- Check first cell: tile_id=1, rotation=0 → encode_gid(0, 1, 0) = 16
|
||||
engine.test.equals(v2.layers.surface.tiles[1], 16, "upgraded cell 0 = (atlas=0, tile=1, rot=0)")
|
||||
-- Check second cell: tile_id=2, rotation=90 → quadrant 1 → encode_gid(0, 2, 1) = (2<<4)|(1<<2) = 36
|
||||
engine.test.equals(v2.layers.surface.tiles[2], 36, "upgraded cell 1 with rot 90° → quadrant 1")
|
||||
-- Fourth cell: rotation 270 → quadrant 3 → encode_gid(0, 1, 3) = (1<<4)|(3<<2) = 28
|
||||
engine.test.equals(v2.layers.surface.tiles[4], 28, "upgraded cell 3 with rot 270° → quadrant 3")
|
||||
|
||||
-- ---------- Task 3: Schema-v2 validation ----------
|
||||
-- Valid minimal v2 map should NOT raise
|
||||
local v2_minimal = {
|
||||
schema_version = 2,
|
||||
id = "v2_min",
|
||||
size = { w = 2, h = 2 },
|
||||
atlases = { "demo_tilemap" },
|
||||
layers = { surface = { tiles = { 16, 16, 16, 16 } } }
|
||||
}
|
||||
local ok = pcall(function() maps.validate_map_table_v2(v2_minimal, "in-memory") end)
|
||||
engine.test.assert(ok, "valid v2 map validates without error")
|
||||
|
||||
-- Bad schema_version
|
||||
local bad_ver = { schema_version = 99, id = "x", size = {w=1,h=1}, atlases = {"a"}, layers = {} }
|
||||
local ok2, err2 = pcall(function() maps.validate_map_table_v2(bad_ver, "in-memory") end)
|
||||
engine.test.assert(not ok2, "rejected schema_version 99")
|
||||
|
||||
-- Missing atlases
|
||||
local no_atlas = { schema_version = 2, id = "x", size = {w=1,h=1}, layers = {} }
|
||||
local ok3 = pcall(function() maps.validate_map_table_v2(no_atlas, "in-memory") end)
|
||||
engine.test.assert(not ok3, "rejected missing atlases[]")
|
||||
|
||||
-- Wrong tile-array length
|
||||
local wrong_len = {
|
||||
schema_version = 2, id = "x", size = {w=2,h=2},
|
||||
atlases = {"demo_tilemap"},
|
||||
layers = { surface = { tiles = {16, 16, 16} } } -- only 3, expected 4
|
||||
}
|
||||
local ok4 = pcall(function() maps.validate_map_table_v2(wrong_len, "in-memory") end)
|
||||
engine.test.assert(not ok4, "rejected wrong tile array length")
|
||||
|
||||
-- Unknown layer name → warning logged but accepted (graceful)
|
||||
local unknown_layer = {
|
||||
schema_version = 2, id = "x", size = {w=1,h=1},
|
||||
atlases = {"demo_tilemap"},
|
||||
layers = { surface = { tiles = {0} }, made_up = { tiles = {0} } }
|
||||
}
|
||||
local ok5 = pcall(function() maps.validate_map_table_v2(unknown_layer, "in-memory") end)
|
||||
engine.test.assert(ok5, "unknown layer name accepted (logged warn)")
|
||||
|
||||
-- ---------- Task 4: Load v2 map with single atlas, multi-layer ----------
|
||||
local v2_id = maps.load("maps/demo_v2.map.json")
|
||||
engine.test.equals(v2_id, "demo_v2", "v2 map loaded with id 'demo_v2'")
|
||||
maps.set_current(v2_id)
|
||||
local v2_size = maps.size()
|
||||
engine.test.equals(v2_size.w, 4, "v2 map width = 4")
|
||||
engine.test.equals(v2_size.h, 4, "v2 map height = 4")
|
||||
|
||||
-- Atlas count (demo_v2 has 2 atlases as of Task 5 fixture)
|
||||
engine.test.equals(maps.atlas_count(), 2, "v2 map has 2 atlases")
|
||||
engine.test.equals(maps.atlas_id_at(0), "demo_tilemap", "v2 atlas[0] = demo_tilemap")
|
||||
|
||||
-- Layer existence
|
||||
engine.test.assert(maps.has_layer("surface"), "v2 has surface layer")
|
||||
engine.test.assert(maps.has_layer("topsurface"), "v2 has topsurface layer")
|
||||
engine.test.assert(maps.has_layer("wall"), "v2 has wall layer")
|
||||
|
||||
-- Roof
|
||||
engine.test.assert(maps.is_indoor(1, 1), "v2 cell (1,1) is indoor")
|
||||
engine.test.assert(not maps.is_indoor(0, 0), "v2 cell (0,0) is outdoor")
|
||||
|
||||
-- ---------- Task 5: Multi-atlas-per-map ----------
|
||||
-- Re-load (demo_v2 already loaded; reset state by closing & re-opening)
|
||||
-- For the test, we trust the existing loaded state if the fixture file was updated
|
||||
engine.test.equals(maps.atlas_count(), 2, "demo_v2 now has 2 atlases")
|
||||
engine.test.equals(maps.atlas_id_at(0), "demo_tilemap", "atlas[0] alias")
|
||||
engine.test.equals(maps.atlas_id_at(1), "walls_tilemap", "atlas[1] alias")
|
||||
|
||||
-- Per-layer atlas resolution: wall layer cell (0,0) should be atlas-1, tile-1
|
||||
local wall_gid = maps.cell_gid("wall", 0, 0)
|
||||
local a, t, r = maps.decode_gid(wall_gid)
|
||||
engine.test.equals(a, 1, "wall cell (0,0) atlas = 1 (walls_tilemap)")
|
||||
engine.test.equals(t, 1, "wall cell (0,0) tile = 1 (wall_brick)")
|
||||
|
||||
-- Empty cell in wall layer
|
||||
local center_gid = maps.cell_gid("wall", 1, 1)
|
||||
engine.test.equals(center_gid, 0, "wall cell (1,1) empty (inside roof)")
|
||||
|
||||
-- ---------- Task 6: Per-layer tile_at ----------
|
||||
-- tile_at_layer returns tile-record from the correct atlas
|
||||
local surface_tile = maps.tile_at_layer("surface", 1, 1)
|
||||
engine.test.assert(surface_tile ~= nil, "surface (1,1) returns tile")
|
||||
engine.test.equals(surface_tile.id, "grass", "surface (1,1) = grass")
|
||||
|
||||
local wall_tile = maps.tile_at_layer("wall", 0, 0)
|
||||
engine.test.assert(wall_tile ~= nil, "wall (0,0) returns tile")
|
||||
engine.test.equals(wall_tile.id, "wall_brick", "wall (0,0) = wall_brick (from walls_tilemap)")
|
||||
|
||||
-- Empty cell returns nil
|
||||
local empty_top = maps.tile_at_layer("topsurface", 0, 0)
|
||||
engine.test.equals(empty_top, nil, "empty topsurface cell returns nil")
|
||||
|
||||
-- OOB returns nil
|
||||
engine.test.equals(maps.tile_at_layer("surface", -1, 0), nil, "OOB tile_at_layer returns nil")
|
||||
|
||||
-- Legacy tile_at backwards-compat: queries surface layer
|
||||
local legacy = maps.tile_at(1, 1)
|
||||
engine.test.equals(legacy.id, "grass", "legacy tile_at(x,y) queries surface layer")
|
||||
|
||||
-- ---------- Task 7: Gameplay queries ----------
|
||||
-- Surface-only cells (inside, no wall) → walkable
|
||||
engine.test.assert(maps.is_walkable(1, 1), "inner (1,1) walkable (surface=grass, no wall)")
|
||||
engine.test.assert(maps.is_walkable(2, 2), "inner (2,2) walkable")
|
||||
|
||||
-- Wall cells block walk
|
||||
engine.test.assert(not maps.is_walkable(0, 0), "(0,0) not walkable (wall present)")
|
||||
engine.test.assert(not maps.is_walkable(3, 3), "(3,3) not walkable (wall present)")
|
||||
|
||||
-- blocks_walk direct query
|
||||
engine.test.assert(maps.blocks_walk(0, 0), "(0,0) blocks_walk")
|
||||
engine.test.assert(not maps.blocks_walk(1, 1), "(1,1) does not block_walk")
|
||||
|
||||
-- blocks_sight: wall layer cell blocks sight; surface-only cell does not
|
||||
engine.test.assert(maps.blocks_sight(0, 0), "(0,0) wall blocks sight")
|
||||
engine.test.assert(not maps.blocks_sight(1, 1), "(1,1) no wall does not block sight")
|
||||
|
||||
-- OOB → not walkable, blocks_walk false (out-of-map)
|
||||
engine.test.assert(not maps.is_walkable(-1, 0), "OOB not walkable")
|
||||
engine.test.assert(not maps.blocks_walk(-1, 0), "OOB does not block_walk (outside)")
|
||||
|
||||
-- ---------- Task 8: Layer iteration + draw ----------
|
||||
-- iterate_layers_pre_entities should visit foundation/subsurface/surface/topsurface in order
|
||||
local pre_visited = {}
|
||||
maps.iterate_layers_pre_entities(function(layer_name)
|
||||
pre_visited[#pre_visited + 1] = layer_name
|
||||
end)
|
||||
-- Only layers that exist in the map are visited; surface + topsurface here
|
||||
engine.test.equals(#pre_visited, 2, "pre_entities visits 2 layers (surface, topsurface)")
|
||||
engine.test.equals(pre_visited[1], "surface", "pre_entities[0] = surface")
|
||||
engine.test.equals(pre_visited[2], "topsurface", "pre_entities[1] = topsurface")
|
||||
|
||||
-- iterate_layers_post_entities should visit wall layers if present
|
||||
local post_visited = {}
|
||||
maps.iterate_layers_post_entities(function(layer_name)
|
||||
post_visited[#post_visited + 1] = layer_name
|
||||
end)
|
||||
engine.test.equals(#post_visited, 1, "post_entities visits 1 layer (wall)")
|
||||
engine.test.equals(post_visited[1], "wall", "post_entities[0] = wall")
|
||||
|
||||
-- draw_map should not raise (no actual render here; just call-through)
|
||||
-- We're not in a render-phase, so this will be a no-op or error-safe.
|
||||
-- Skip actual draw_map test; integration test happens in spine-prototype manual run.
|
||||
|
||||
-- ---------- Task 9: v1 map auto-migration on load ----------
|
||||
-- The existing demo.map.json is v1-format. Loading should auto-upgrade.
|
||||
local v1_id = maps.load("maps/demo.map.json")
|
||||
engine.test.equals(v1_id, "demo", "v1 demo map loaded")
|
||||
maps.set_current(v1_id)
|
||||
engine.test.equals(maps.atlas_count(), 1, "v1 auto-upgrade has 1 atlas")
|
||||
engine.test.equals(maps.atlas_id_at(0), "demo_tilemap", "v1 atlas[0] = demo_tilemap")
|
||||
engine.test.assert(maps.has_layer("surface"), "v1 auto-upgrade has surface layer")
|
||||
engine.test.assert(not maps.has_layer("wall"), "v1 has no wall layer")
|
||||
|
||||
-- Walkability via v2-API should match v1-API result
|
||||
engine.test.assert(maps.is_walkable(5, 5), "v1 (5,5) walkable (was grass in v1 test)")
|
||||
engine.test.assert(not maps.is_walkable(0, 0), "v1 (0,0) not walkable (was stone in v1 test)")
|
||||
|
||||
-- Restore current to v2 map for subsequent tests if any
|
||||
maps.set_current("demo_v2")
|
||||
|
||||
-- Setup (legacy demo assertions, now using already-loaded demo from Task 9 above)
|
||||
local demo_map = "demo"
|
||||
maps.set_current(demo_map)
|
||||
|
||||
-- Geometry assertions
|
||||
@@ -31,9 +240,9 @@ function M.run_tests(ctx)
|
||||
engine.test.equals(maps.tile_at(-1, 0), nil, "tile_at OOB negative tx returns nil")
|
||||
engine.test.equals(maps.tile_at(0, 16), nil, "tile_at OOB ty=size.h returns nil")
|
||||
|
||||
-- list() returns registered map-id
|
||||
-- list() returns registered map-ids (demo + demo_v2 loaded earlier)
|
||||
local list = maps.list()
|
||||
engine.test.equals(#list, 1, "maps.list returns 1 registered map")
|
||||
engine.test.equals(#list, 2, "maps.list returns 2 registered maps")
|
||||
|
||||
engine.exit(engine.test.failures())
|
||||
end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "lib-core.maps-test",
|
||||
"role": "test",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"api_min": "0.1",
|
||||
"deps": [
|
||||
{"id": "lib-core.maps", "version": "0.1.2"}
|
||||
{"id": "lib-core.maps", "version": "0.2.0"}
|
||||
]
|
||||
}
|
||||
|
||||
38
maps/demo_v2.map.json
Normal file
38
maps/demo_v2.map.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"id": "demo_v2",
|
||||
"size": { "w": 4, "h": 4 },
|
||||
"atlases": ["demo_tilemap", "walls_tilemap"],
|
||||
"layers": {
|
||||
"surface": {
|
||||
"tiles": [
|
||||
32, 32, 32, 32,
|
||||
32, 16, 16, 32,
|
||||
32, 16, 16, 32,
|
||||
32, 32, 32, 32
|
||||
]
|
||||
},
|
||||
"wall": {
|
||||
"tiles": [
|
||||
16777232, 16777232, 16777232, 16777232,
|
||||
16777232, 0, 0, 16777232,
|
||||
16777232, 0, 0, 16777232,
|
||||
16777232, 16777232, 16777232, 16777232
|
||||
]
|
||||
},
|
||||
"topsurface": {
|
||||
"tiles": [
|
||||
0, 0, 0, 0,
|
||||
0, 0, 16, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
},
|
||||
"roof": [
|
||||
0, 0, 0, 0,
|
||||
0, 1, 1, 0,
|
||||
0, 1, 1, 0,
|
||||
0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user