Maze Runner: Level Service
HardAI-assisted coding · AI-Assisted Feature Work
Maze Runner: Level Service
You've joined the tools team for Labyrinth, a grid puzzle game. Level designers draw levels as ASCII maps in the level editor, and the level service behind it validates them, finds the fewest-steps solution, and draws that solution as an overlay. This sprint you'll fix two bug reports and then build the new cell types design has asked for.
Codebase
maze/glyphs.py: what each map character means, plusentry_costfor terrain.maze/grid.py:Grid(positions are(row, col)),DIRECTIONSorder,parse_levelvalidation.maze/rules.py: movement rules the solver consults for every move (the extension point for new cells).maze/search.py: breadth-first fewest-steps solver.maze/terrain.py: placeholder for the cost-aware planner (not implemented).maze/render.py: draws a path over a level for the editor overlay.maze/generator.py: deterministic procedural levels (seeded LCG). Known-good.maze/service.py:LevelService, which stores levels by id and caches solutions per id.maze/api.py: therun(operations)entrypoint and the operation dispatcher.tests/: the existing unit tests.tests/test_solver.py::test_open_roomandtests/test_render.py::test_render_marks_path_and_keeps_markersfail today.
How your code is run
run(operations: list[list]) -> list creates a fresh LevelService, applies each operation in order, and returns exactly one output per operation. Errors are returned as strings, never raised.
| Operation | Arguments | Returns | Errors |
|---|---|---|---|
load | level_id, rows (list of equal-length strings) | "ok" | validation error (below) |
generate | level_id, rows, cols, seed, style ("open" or "terrain") | "ok" | "error: invalid size" (rows or cols outside 2..300), "error: unknown style '<style>'" |
solve | level_id | fewest steps from S to E (int), or null if E is unreachable | "error: unknown level" |
path | level_id | that path as a list of [row, col], S first and E last, or null | "error: unknown level" |
render | level_id | the level's rows with the path drawn on it (unchanged rows if there is no path) | "error: unknown level" |
cheapest | level_id | added in Part 4 | "error: unknown operation 'cheapest'" today |
Any other operation name returns "error: unknown operation '<name>'".
Glyphs today: # wall, . floor, S start, E exit, 1-9 terrain (walkable, see Part 4). A move goes one cell up, right, down or left; walls can't be entered; leaving the map is impossible.
Validation runs in this order and the first failure is returned as "error: <reason>":
empty level(no rows, or the first row is empty)rows have different lengthsunknown glyph '<ch>' at row <r>, col <c>(the first one in row-major order)expected exactly one Sexpected exactly one E
A failed load or generate leaves any existing level with that id untouched. A successful one replaces it.
Tie-break. When several paths have the fewest steps, path (and so render) uses the one whose move sequence comes first when moves are compared one at a time with up < right < down < left. The existing BFS already gets this by exploring neighbours in DIRECTIONS order; keep it.
Worked example
[["load", "intro", ["S.#", "...", "#.E"]], ["solve", "intro"], ["path", "intro"], ["render", "intro"]]returns
["ok", 4, [[0,0],[0,1],[1,1],[1,2],[2,2]], ["S*#", ".**", "#.E"]]Four routes take 4 steps; right, down, right, down sorts first. Today render returns ["**#", ".**", "#.*"].
Part 1: Bug reports from level design
Goal: fix these reported problems without changing anything else.
- "When the solution is drawn in the editor, the S and E markers disappear."
rendermust draw*on every path cell except S and E, which always keep their glyph, even if the path crosses them more than once. Whatever else is under the path (floor, terrain, and the new cells from later parts) becomes*. - "The solver says there is no path through big open levels that are obviously solvable." An empty 8x8 room with S and E in opposite corners is reported as
nullinstead of14.solve/pathmust be correct for any level up to 300x300. - "After I re-upload a level under the same id,
solvesometimes gives the old level's answer." After a successfulloadorgenerate, every query on that id must reflect the new level.
Part 2: One-way gates
Goal: support the gate cells > < ^ v.
- The loader accepts the four gate glyphs.
- A gate can only be entered by a move in the direction its arrow points:
>by a move right,<by a move left,^by a move up,vby a move down. - Leaving a gate is unrestricted (any direction, subject to the normal rules for the cell you enter).
- A gate counts as one step like any other cell. The tie-break is unchanged.
Example
[["load", "oneway", ["S<..E", ".###.", "....."]], ["solve", "oneway"], ["load", "short", ["S>.E"]], ["solve", "short"]]returns ["ok", 8, "ok", 3]: in oneway the < can't be entered by moving right, so the route goes around the bottom (down, down, right x4, up, up).
Part 3: Keys and doors
Goal: support keys a b c d and their doors A B C D.
- The loader accepts the eight glyphs. A level may hold at most one key of each letter; a second one is the validation error
duplicate key '<k>', naming the key whose second copy comes first in row-major order. This check runs after the S and E checks. A door whose key isn't in the level is allowed (it can just never be opened). - You start with no keys. Stepping onto a key cell picks that key up for the rest of the walk. The key cell stays walkable.
- A door can be entered only while holding the key of the same letter (
aopensA). Doors never lock again. - The fewest-steps path may now revisit cells (including S) after collecting a key.
pathlists every position in walk order, and the walk ends the first time it reaches E. - Gates (Part 2) still apply, and the tie-break rule is unchanged.
Example
[["load", "k", ["S.a", "#A#", "#E#"]], ["solve", "k"], ["path", "k"], ["render", "k"]]returns ["ok", 5, [[0,0],[0,1],[0,2],[0,1],[1,1],[2,1]], ["S**", "#*#", "#E#"]]: walk right twice to get a, step back, then go down through A to E.
Part 4: Cheapest route over terrain (stretch)
Goal: add the cheapest operation.
["cheapest", level_id]returns the minimum total cost of a walk from S to E as an int,nullif E is unreachable, or"error: unknown level".- Each move costs the entry cost of the cell moved onto: a terrain digit costs its value, and every other walkable cell (
.,E,S, gates, keys, doors) costs 1. Standing on S at the start is free. This isglyphs.entry_cost. - Walls, gates, keys and doors work exactly as in Parts 2-3.
solveis unchanged: it still counts steps and ignores terrain.- Scale: tests generate levels up to 80 cells on a side and run several
solveandcheapestqueries in one call, all under the runner's 5-second limit for the whole suite. Scanning every state to find the next cheapest one, or enumerating whole paths, is too slow.
Example
[["load", "t", ["S9E", "1#1", "111"]], ["solve", "t"], ["cheapest", "t"]]returns ["ok", 2, 6]: the 2-step route costs 9 + 1 = 10, and the 6-step detour around the wall costs 6.
Notes
glyphs.pyconstants,generator.pyand theGridhelpers are known-good. The bugs are elsewhere.- Use the AI assistant freely, but check what it changes against the rules above.
- Later parts must not break earlier ones. Hidden tests re-check earlier behaviour.
- Parts 1-3 is a strong result. Part 4 is a stretch.
Example 1
- Input:
- [["load", "intro", ["S.#", "...", "#.E"]], ["solve", "intro"], ["path", "intro"], ["render", "intro"]]
- Output:
- ["ok", 4, [[0,0],[0,1],[1,1],[1,2],[2,2]], ["S*#", ".**", "#.E"]]
- Explanation
- Four routes take 4 steps; right, down, right, down sorts first under up < right < down < left. The overlay draws * on the path but keeps S and E.
Example 2
- Input:
- [["load", "k", ["S.a", "#A#", "#E#"]], ["solve", "k"], ["path", "k"]]
- Output:
- ["ok", 5, [[0,0],[0,1],[0,2],[0,1],[1,1],[2,1]]]
- Explanation
- Part 3: walk right twice to pick up key a, step back over (0,1), then go down through door A to the exit. The walk revisits a cell.
Example 3
- Input:
- [["load", "t", ["S9E", "1#1", "111"]], ["solve", "t"], ["cheapest", "t"]]
- Output:
- ["ok", 2, 6]
- Explanation
- Part 4: the 2-step route costs 9 + 1 = 10; the 6-step detour around the wall costs 1 per cell, 6 in total.
Guidelines
- Python 3 standard library only
- Positions are (row, col) and are returned as [row, col] lists
- A move is one cell up, right, down or left; ties between equally short paths go to up < right < down < left
- Levels are at most 300x300; generated levels are deterministic for a given seed
- Every operation returns exactly one value; errors are "error: ..." strings