clone

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

Bound retained solver states independently of move attempts

Dami committed 428011b
commit
428011bbb1c134f29563174d12aac8924896ef80
parent
5946dd7e1c5ea5b70acd0a09e9423e337611a87a
author
Dami <dami.etoile@gmail.com>
5 files changed, +35 -2
Mopenspec/changes/scale-solver-state-search/design.md7+++++++
Msrc/solver/search.js12++++++++++--
Msrc/solver/solve-level.js3+++
Mtests/solver-level.test.js5+++++
Mtests/solver-search.test.js10++++++++++
M openspec/changes/scale-solver-state-search/design.md +7 -0
M src/solver/search.js +10 -2
@@ -59,6 +59,8 @@ function sameBytes(left, right) {
5959 * every frontier state.6060 * @param {(snapshot: object, action: unknown) => {accepted?: boolean, state?: object, meta?: object}} options.transition6161 * @param {number} [options.cap=100000] Maximum number of attempted actions.62+ * @param {number} [options.maxVisited=Infinity] Maximum number of compact63+ * frontier states retained, including the initial state.6264 * @param {(context: {snapshot: object, action: unknown, result: object}) => boolean} [options.filter]6365 * Optional post-transition filter. Rejected transitions and filtered6466 * transitions both count toward `attempts`.
@@ -76,6 +78,7 @@ export function searchStates({
7678 actions,7779 transition,7880 cap = 100_000,81+ maxVisited = Infinity,7982 filter,8083 isWin = snapshot => snapshot?.won === true,8184 hash = hashBytes,
@@ -87,10 +90,12 @@ export function searchStates({
8790 const moveSet = asActions(actions);8891 if (typeof transition !== 'function') throw new TypeError('search transition must be a function');8992 if (!Number.isSafeInteger(cap) || cap < 0) throw new RangeError('search cap must be a non-negative safe integer');93+ if (maxVisited !== Infinity && (!Number.isSafeInteger(maxVisited) || maxVisited < 0)) throw new RangeError('search maxVisited must be a non-negative safe integer');9094 const allowTransition = filter || (() => true);9195 if (typeof allowTransition !== 'function' || typeof isWin !== 'function' || typeof hash !== 'function') throw new TypeError('search filter, isWin, and hash must be functions');9296 9397 if (isWin(initial)) return resultFor('solved', { path: [], attempts: 0, expanded: 0, visited: 0, state: initial });98+ if (maxVisited === 0) return resultFor('limit', { reason: 'visited' });9499 95100 const initialBlob = codec.encode(initial);96101 const nodes = [{ blob: initialBlob, parent: -1, action: undefined }];
@@ -105,6 +110,7 @@ export function searchStates({
105110 const indexes = Array.isArray(bucket) ? bucket : [bucket];106111 if (indexes.some(index => sameBytes(nodes[index].blob, blob))) return false;107112 }113+ if (nodes.length >= maxVisited) return null;108114 const index = nodes.length;109115 if (bucket === undefined) seen.set(bucketHash, index);110116 else if (Array.isArray(bucket)) bucket.push(index);
@@ -122,7 +128,7 @@ export function searchStates({
122128 expanded += 1;123129 124130 for (const action of moveSet) {125- if (attempts >= cap) return resultFor('limit', { attempts, expanded, visited: nodes.length });131+ if (attempts >= cap) return resultFor('limit', { reason: 'attempts', attempts, expanded, visited: nodes.length });126132 attempts += 1;127133 const result = transition(snapshot, action);128134 if (!isRecord(result)) throw new TypeError('search transition must return an object');
@@ -142,7 +148,9 @@ export function searchStates({
142148 }143149 144150 const blob = codec.encode(result.state);145- if (!remember(blob)) continue;151+ const remembered = remember(blob);152+ if (remembered === null) return resultFor('limit', { reason: 'visited', attempts, expanded, visited: nodes.length });153+ if (!remembered) continue;146154 nodes.push({ blob, parent: nodeIndex, action });147155 }148156 }
M src/solver/solve-level.js +3 -0
M tests/solver-level.test.js +5 -0
M tests/solver-search.test.js +10 -0