- commit
- 45f15b60d66e933894fd9d28613bf97a83acbec0
- parent
- d9a2bce25ebfe5dd6a0de425bbf4b43a7a8feb7f
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 14 | ++++++++++++-- |
| M | tests/ | 23 | +++++++++++++++++++++++ |
| M | tests/ | 7 | +++++++ |
@@ -43,8 +43,18 @@ function validatePalette(palette, errors) {
4343 }4444 for (const [noun, glyph] of Object.entries(palette)) {4545 if (!token(noun) || RESERVED.has(noun)) errors.push(issue(`palette.${noun}`, 'key must be an uppercase noun token, not a rule token'));46- if (typeof glyph !== 'string' || glyph.length === 0 || glyph.length > 16) {47- errors.push(issue(`palette.${noun}`, 'value must be a non-empty display string (16 characters maximum)'));46+ if (typeof glyph === 'string') {47+ if (glyph.length === 0 || glyph.length > 16) errors.push(issue(`palette.${noun}`, 'value must be a non-empty display string (16 characters maximum)'));48+ } else if (isRecord(glyph)) {49+ for (const key of Object.keys(glyph)) if (key !== 'glyph' && key !== 'faces') {50+ errors.push(issue(`palette.${noun}.${key}`, "unsupported field; use only 'glyph' and 'faces'"));51+ }52+ if (typeof glyph.glyph !== 'string' || glyph.glyph.length === 0 || glyph.glyph.length > 16) {53+ errors.push(issue(`palette.${noun}.glyph`, 'must be a non-empty display string (16 characters maximum)'));54+ }55+ if (glyph.faces !== 'left' && glyph.faces !== 'right') errors.push(issue(`palette.${noun}.faces`, "must be 'left' or 'right'"));56+ } else {57+ errors.push(issue(`palette.${noun}`, 'value must be a display string or { glyph, faces: \'left\' | \'right\' }'));4858 }4959 }5060 }@@ -28,6 +28,29 @@ test('normalizes plain CommonJS noun and word values to uppercase', () => {
2828 assert.equal(mod.levels[0].objects[2].noun, 'MIAU');2929 });3030 31+test('accepts directional palette faces and preserves plain strings', () => {32+ const mod = loadMod(`module.exports = {33+ manifest: { id: 'facing-demo', name: 'Facing Demo', version: '1.0.0' },34+ palette: { CAT: { glyph: '🐈', faces: 'left' }, FLAG: '🚩' },35+ levels: [{ id: 'intro', width: 1, height: 1, objects: [{ kind: 'object', noun: 'CAT', x: 0, y: 0 }] }],36+ };`);37+ assert.deepEqual(mod.palette, { CAT: { glyph: '🐈', faces: 'left' }, FLAG: '🚩' });38+});39+40+test('rejects unbounded directional palette shapes', () => {41+ assert.throws(() => loadMod(`module.exports = {42+ manifest: { id: 'bad-facing', name: 'Bad Facing', version: '1.0.0' },43+ palette: { CAT: { glyph: '🐈', faces: 'up', extra: true }, FLAG: { glyph: '' } },44+ levels: [{ id: 'intro', width: 1, height: 1, objects: [] }],45+ };`), error => {46+ assert.ok(error instanceof ModValidationError);47+ assert.match(error.message, /palette\.CAT\.faces/);48+ assert.match(error.message, /palette\.CAT\.extra/);49+ assert.match(error.message, /palette\.FLAG\.glyph/);50+ return true;51+ });52+});53+3154 test('reports malformed manifest, out-of-bounds position, and duplicate IDs together', () => {3255 assert.throws(() => loadMod(`module.exports = {3356 manifest: { id: 'Bad ID', name: '', version: 'tomorrow' },@@ -24,6 +24,13 @@ test('shared packs round trip as normalized Unicode data', () => {
2424 assert.equal(decoded.palette.MIAU, '🐈');2525 });2626 27+test('shared packs preserve bounded directional palette faces', () => {28+ const directional = { ...pack, palette: { ...pack.palette, MIAU: { glyph: '🐈', faces: 'right' } } };29+ const decoded = decodeSharedPack(encodeSharedPack(directional));30+ assert.deepEqual(decoded.palette.MIAU, { glyph: '🐈', faces: 'right' });31+ assert.equal(decoded.palette.FLAG, '🚩');32+});33+2734 test('encoding is stable and CommonJS export is deterministic data only', () => {2835 const first = encodeSharedPack(pack);2936 const second = encodeSharedPack({ levels: pack.levels, palette: pack.palette, manifest: pack.manifest });