clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

fix(index): move along visible serpentine path

Dami committed c6022a1
commit
c6022a1d7b85d238095f013e94176abc4758a7d6
parent
958049342a0d155c9aff8ba57121d985ba968737
author
Dami <dami.etoile@gmail.com>
2 files changed, +79 -3
Msrc/scenes/map.js31++++++++++++++++--
Mtests/map.test.js51++++++++++++++++++++++++++++++
M src/scenes/map.js +28 -3
@@ -251,9 +251,34 @@ function findNode(layout, nodeId) {
251251 }252252 253253 function candidateFor(node, direction, layout) {254- if (direction === 'next' || direction === 'right') return findNode(layout, node.nextId);255- if (direction === 'previous' || direction === 'left') return findNode(layout, node.previousId);256- return findNode(layout, node.neighbors?.[direction]);254+ // Arrow and gamepad directions are spatial. `next` and `previous` remain255+ // explicit path traversal for continue/campaign callers, including sheet256+ // boundaries where no visible neighbor exists.257+ if (direction === 'next') return findNode(layout, node.nextId);258+ if (direction === 'previous') return findNode(layout, node.previousId);259+ const deltas = { left: [-1, 0], right: [1, 0], up: [0, -1], down: [0, 1] };260+ const delta = deltas[direction];261+ if (!delta) return null;262+ // Only path edges are navigable. This keeps the arrows on the inked263+ // serpentine route instead of jumping between visually adjacent nodes that264+ // are separated by an empty grid cell in the drawn path.265+ for (const adjacentId of [node.previousId, node.nextId]) {266+ const candidate = findNode(layout, adjacentId);267+ if (!candidate) continue;268+ if (candidate.sheetId !== node.sheetId) {269+ // A sheet boundary is the one intentional spatial jump: the first270+ // node can page back with left, and the final node can page forward in271+ // the direction its serpentine row is travelling.272+ const isFirst = node.pathIndex === 0 && adjacentId === node.previousId;273+ const isLast = adjacentId === node.nextId && node.nextId && !layout.nodes.some(other => other.sheetId === node.sheetId && other.pathIndex > node.pathIndex);274+ const forward = node.row % 2 === 0 ? 'right' : 'left';275+ if ((isFirst && direction === 'left') || (isLast && direction === forward)) return candidate;276+ continue;277+ }278+ if (candidate.column - node.column === delta[0]279+ && candidate.row - node.row === delta[1]) return candidate;280+ }281+ return null;257282 }258283 259284 /**
M tests/map.test.js +51 -0
@@ -118,3 +118,54 @@ test('navigator indicates locked proofs, can skip them, and enters available pro
118118 assert.equal(result.node.id, 'core:second');119119 assert.equal(skipping.enter().levelId, 'second');120120 });121+122+test('arrow directions follow visible serpentine neighbors', () => {123+ const levels = Array.from({ length: 10 }, (_, index) => level('core', `proof-${index + 1}`));124+ const layout = decorateIndex(createIndexLayout(levels), { isSolved: () => true });125+ const navigator = createIndexNavigator(layout, { startNodeId: 'core:proof-7' });126+127+ // Row two runs right-to-left: 07 is visibly beside 08 on its left.128+ let result = navigator.move('left');129+ assert.equal(result.moved, true);130+ assert.equal(result.node.id, 'core:proof-8');131+ result = navigator.move('right');132+ assert.equal(result.moved, true);133+ assert.equal(result.node.id, 'core:proof-7');134+135+ // 07 has no vertical path edge: 04 is merely grid-adjacent above it.136+ result = navigator.move('up');137+ assert.equal(result.moved, false);138+ assert.equal(result.node.id, 'core:proof-7');139+140+ // The path turn is the actual vertical edge between 05 and 06.141+ navigator.setNode('core:proof-6');142+ result = navigator.move('up');143+ assert.equal(result.moved, true);144+ assert.equal(result.node.id, 'core:proof-5');145+ result = navigator.move('down');146+ assert.equal(result.moved, true);147+ assert.equal(result.node.id, 'core:proof-6');148+ // Explicit path traversal still follows numbering, independent of display.149+ navigator.setNode('core:proof-7');150+ result = navigator.move('next');151+ assert.equal(result.node.id, 'core:proof-8');152+});153+154+test('arrow directions page across sheet and imported-pack boundaries', () => {155+ const levels = [156+ ...Array.from({ length: 16 }, (_, index) => level('core', `proof-${index + 1}`)),157+ level('imported-pack', 'intro'),158+ ];159+ const layout = decorateIndex(createIndexLayout(levels), { isSolved: () => true });160+ const navigator = createIndexNavigator(layout, { startNodeId: 'core:proof-15' });161+162+ let result = navigator.move('right');163+ assert.equal(result.moved, true);164+ assert.equal(result.node.id, 'core:proof-16');165+ result = navigator.move('right');166+ assert.equal(result.moved, true);167+ assert.equal(result.node.id, 'imported-pack:intro');168+ result = navigator.move('left');169+ assert.equal(result.moved, true);170+ assert.equal(result.node.id, 'core:proof-16');171+});