feat(map-tiles): rectangle paint, region query, and isolation detection APIs - #139
feat(map-tiles): rectangle paint, region query, and isolation detection APIs#139JemimahEkong wants to merge 1 commit into
Conversation
…on APIs - POST /admin/game-data/maps/:mapNum/tiles/paint-rectangle: atomic rectangle paint with 500-tile limit - GET /admin/game-data/maps/:mapNum/tiles/region: query overrides within a bounding box - POST /admin/game-data/maps/:mapNum/tiles/blocked-check: BFS-based isolated region detection - Enhanced PUT tiles and rectangle paint to return isolatedRegions when blocked flag changes - Added 12 integration tests covering paint, validation, rollback, region query, isolation detection, and size limits - Added CI env vars for admin auth in integration tests
| export async function checkIsolatedRegions( | ||
| mapNum: number, | ||
| ): Promise<{ isolated: boolean; unreachableCount: number }> { | ||
| const grid = loadBaseBlockedTiles(mapNum); | ||
|
|
||
| if (!grid) { | ||
| return { isolated: false, unreachableCount: 0 }; | ||
| } | ||
|
|
||
| const height = grid.length; | ||
| const width = grid[0]!.length; | ||
|
|
||
| const overrides = await queryRegion(mapNum, { | ||
| startX: 1, | ||
| startY: 1, |
There was a problem hiding this comment.
⚠️ Bug: Isolation check reports false positives on naturally-disconnected maps
checkIsolatedRegions seeds a BFS from the first walkable tile and reports isolated=true whenever total walkable tiles exceed those reachable from that single seed. But base maps already contain multiple disconnected walkable regions — running this on the clean, unedited mapa_1/terrain.json yields 15 walkable components, 6118 walkable tiles, and unreachableCount=368 with isolated=true. So every PUT/paint-rectangle edit that sets blocked will return isolatedRegions=true regardless of whether the edit actually fractured the map, defeating the feature's purpose and misleading editors. Test #9 only asserts the return type, not that a clean map yields isolated=false, so this slips through. Fix by comparing connectivity against a baseline computed on the pre-edit grid (e.g. count connected components before vs. after, or only report regions newly isolated by this edit) rather than comparing a single seed's reach to the global walkable total.
Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
Important
Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
| function loadBaseBlockedTiles(mapNum: number): boolean[][] | null { | ||
| const terrainPath = path.join( | ||
| MAPAS_SOURCE_DIR, | ||
| `mapa_${mapNum}`, | ||
| "terrain.json", | ||
| ); | ||
|
|
||
| if (!fs.existsSync(terrainPath)) { | ||
| return null; | ||
| } | ||
|
|
||
| const terrain = JSON.parse( | ||
| fs.readFileSync(terrainPath, "utf8"), | ||
| ) as TerrainJson; | ||
| const palette = terrain.palette ?? {}; |
There was a problem hiding this comment.
💡 Performance: BFS uses array.shift() and synchronous file I/O on request path
checkIsolatedRegions runs on every blocked-flag edit (PUT tiles and paint-rectangle). It uses queue.shift() which is O(n) per dequeue, making the BFS O(n^2) on up to 10,000 cells, and loadBaseBlockedTiles calls fs.existsSync/fs.readFileSync + JSON.parse synchronously, blocking the event loop for the whole file read on each such request. Both are functionally fine at 100x100 but avoidable: use an index-based queue head pointer (or a deque) instead of shift(), and use fs.promises.readFile to avoid blocking. Optionally cache parsed terrain per map since terrain.json is static.
Was this helpful? React with 👍 / 👎
feat(map-tiles): rectangle paint, region query & isolation detection APIs
Closes #7
What
Three new admin endpoints for map tile editing:
POST /admin/game-data/maps/:mapNum/tiles/paint-rectangle— Paint a filled rectangle of tiles in a single atomic operation (max 500 tiles, auto-swaps min/max coordinates)GET /admin/game-data/maps/:mapNum/tiles/region?startX=..&startY=..&endX=..&endY=..— Query all tile overrides within a bounding box (drafts take priority over published)POST /admin/game-data/maps/:mapNum/tiles/blocked-check— Run an in-memory BFS from the first walkable tile to detect isolated unreachable regionsThe existing
PUT /admin/game-data/maps/:mapNum/tilesand the new rectangle endpoint both return anisolatedRegionsboolean in the response when any tile in the batch setsblocked: true/false, giving immediate feedback on whether the edit fractured the map.Why
The current tile-painting workflow requires one HTTP request per tile. For large edits (walls, terrain zones, blocked corridors) this is slow and error-prone. The rectangle endpoint lets editors paint an entire area in one call. The isolation check prevents accidentally sealing off parts of the map from players — a bug that's only discoverable by walking every path manually.
Changes
api/src/repositories/worldBuilder.tspaintRectangle(),queryRegion(),checkIsolatedRegions()+ Zod schemasapi/src/server.tsPUT tilesresponseapi/src/tests/map-tiles.integration.test.ts.github/workflows/ci.ymlImplementation details
TilePaint[]array and delegates to the existingpaintTiles()— no new transaction logic, full atomicity viaBEGIN/COMMIT/ROLLBACK.DISTINCT ON (x, y, layer)withstatus ASCordering so drafts override published entries for the same coordinate.terrain.jsonfrom disk, applies all overrides (drafts preferred viaqueryRegion), then runs a 4-directional BFS in memory. If any walkable tile is unreachable from the first walkable tile found,isolated: trueis returned with the exact count of unreachable tiles.Tests
blocked: trueisolatedRegionsOut of scope
server/(game server) — server-side loading of published tile overrides for movement validation is a separate issue, since the game server currently reads base maps fromterrain.jsonat startup rather than syncing overrides from the API.frontend/— the client already consumesGET /maps/:mapNum/overrides.game_map_tile_overridestable is sufficient.Verification
Acceptance criteria