clone

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

Reuse codec validators across search states

Dami committed 5946dd7
commit
5946dd7e1c5ea5b70acd0a09e9423e337611a87a
parent
4c34901d3a450781fe30b47c0c27c9645e7c2e44
author
Dami <dami.etoile@gmail.com>
2 files changed, +28 -11
Msrc/solver/state-codec.js31+++++++++++++++++++-----------
Mtests/solver-codec.test.js8++++++++
M src/solver/state-codec.js +20 -11
@@ -11,6 +11,9 @@ const MAGIC = [0x4d, 0x53, 0x43, 0x31]; // MSC1
1111 const MAX_U16 = 0xffff;1212 const MAX_U32 = 0xffffffff;1313 const TOKEN_RE = /^[A-Z][A-Z0-9_-]*$/;14+const SNAPSHOT_FIELDS = new Set(['width', 'height', 'levelId', 'title', 'hint', 'objects', 'moves', 'won', 'rules', 'sentences', 'automata', 'mechanics']);15+const OBJECT_FIELDS = new Set(['id', 'kind', 'noun', 'text', 'x', 'y']);16+const AUTOMATON_FIELDS = new Set(['id', 'subject', 'property', 'pattern', 'phase', 'active', 'carriers', 'instanceIds', 'next']);1417 1518 export class StateCodecError extends TypeError {1619 constructor(message) {
@@ -54,21 +57,28 @@ function normalizeMechanics(registry) {
5457 if (!/^[a-z][a-zA-Z0-9_]*$/.test(name) || !['boolean', 'u16', 'u32'].includes(type)) throw new StateCodecError(`mechanics.${mechanic.id}.${name} has an unsupported field type`);5558 return { name, type };5659 });57- return { id: mechanic.id, fields: fields.sort((a, b) => compareId(a.name, b.name)) };60+ const sortedFields = fields.sort((a, b) => compareId(a.name, b.name));61+ return { id: mechanic.id, fields: sortedFields, fieldNames: new Set(sortedFields.map(field => field.name)) };5862 }).sort((a, b) => compareId(a.id, b.id));5963 }6064 61-function writeField(writer, value, type, label) {65+function validateField(value, type, label) {6266 if (type === 'boolean') {6367 if (typeof value !== 'boolean') throw new StateCodecError(`${label} must be boolean`);64- writer.u8(value ? 1 : 0);6568 } else if (type === 'u16') {66- assertU16(value, label); writer.u16(value);69+ assertU16(value, label);6770 } else {68- assertU32(value, label); writer.u32(value);71+ assertU32(value, label);6972 }7073 }7174 75+function writeField(writer, value, type, label) {76+ validateField(value, type, label);77+ if (type === 'boolean') writer.u8(value ? 1 : 0);78+ else if (type === 'u16') writer.u16(value);79+ else writer.u32(value);80+}81+7282 function readField(reader, type) {7383 if (type === 'boolean') {7484 const value = reader.take();
@@ -103,14 +113,13 @@ export function createStateCodec(level, options = {}) {
103113 104114 function normalizeSnapshot(input) {105115 if (!isRecord(input)) throw new StateCodecError('snapshot must be an object');106- const allowedSnapshotFields = new Set(['width', 'height', 'levelId', 'title', 'hint', 'objects', 'moves', 'won', 'rules', 'sentences', 'automata', 'mechanics']);107- for (const name of Object.keys(input)) if (!allowedSnapshotFields.has(name)) throw new StateCodecError(`unregistered snapshot field: ${name}`);116+ for (const name of Object.keys(input)) if (!SNAPSHOT_FIELDS.has(name)) throw new StateCodecError(`unregistered snapshot field: ${name}`);108117 if (input.width !== level.width || input.height !== level.height) throw new StateCodecError('snapshot board shape does not match level');109118 if (!Array.isArray(input.objects)) throw new StateCodecError('snapshot.objects must be an array');110119 const seen = new Set();111120 const objects = input.objects.map((object, index) => {112121 if (!isRecord(object) || typeof object.id !== 'string' || !byId.has(object.id)) throw new StateCodecError(`snapshot.objects[${index}] is not authored`);113- for (const name of Object.keys(object)) if (!['id', 'kind', 'noun', 'text', 'x', 'y'].includes(name)) throw new StateCodecError(`unregistered object field: ${object.id}.${name}`);122+ for (const name of Object.keys(object)) if (!OBJECT_FIELDS.has(name)) throw new StateCodecError(`unregistered object field: ${object.id}.${name}`);114123 if (seen.has(object.id)) throw new StateCodecError(`snapshot repeats object ${object.id}`);115124 seen.add(object.id);116125 const source = byId.get(object.id);
@@ -129,7 +138,7 @@ export function createStateCodec(level, options = {}) {
129138 if (!Array.isArray(input.automata) || input.automata.length !== automata.length) throw new StateCodecError('snapshot.automata does not match level definitions');130139 for (const runtime of input.automata) {131140 if (!isRecord(runtime)) throw new StateCodecError('snapshot automaton runtime must be an object');132- for (const name of Object.keys(runtime)) if (!['id', 'subject', 'property', 'pattern', 'phase', 'active', 'carriers', 'instanceIds', 'next'].includes(name)) throw new StateCodecError(`unregistered automaton field: ${runtime.id || '?'}.${name}`);141+ for (const name of Object.keys(runtime)) if (!AUTOMATON_FIELDS.has(name)) throw new StateCodecError(`unregistered automaton field: ${runtime.id || '?'}.${name}`);133142 const system = automataById.get(runtime?.id);134143 if (!system || phases.has(runtime.id) || !Number.isInteger(runtime.phase) || runtime.phase < 0 || runtime.phase >= system.patternLength) throw new StateCodecError(`snapshot has an invalid automaton phase: ${runtime?.id || '?'}`);135144 phases.set(runtime.id, runtime.phase);
@@ -144,10 +153,10 @@ export function createStateCodec(level, options = {}) {
144153 const fields = {};145154 for (const field of mechanic.fields) {146155 if (!(field.name in value)) throw new StateCodecError(`snapshot.mechanics.${mechanic.id}.${field.name} is required`);147- writeField(new Writer(), value[field.name], field.type, `snapshot.mechanics.${mechanic.id}.${field.name}`);156+ validateField(value[field.name], field.type, `snapshot.mechanics.${mechanic.id}.${field.name}`);148157 fields[field.name] = value[field.name];149158 }150- for (const name of Object.keys(value)) if (!mechanic.fields.some(field => field.name === name)) throw new StateCodecError(`unregistered mechanic field: ${mechanic.id}.${name}`);159+ for (const name of Object.keys(value)) if (!mechanic.fieldNames.has(name)) throw new StateCodecError(`unregistered mechanic field: ${mechanic.id}.${name}`);151160 mechanicState[mechanic.id] = fields;152161 }153162 if (input.moves !== undefined) assertU32(input.moves, 'snapshot.moves');
M tests/solver-codec.test.js +8 -0