clone

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

fix(facing): orient MIAU from its declared native pose

Dami committed a6a5f16
commit
a6a5f160bcd52560628edbf438256b4895ad5729
parent
ca0e3e67554a6deb15d987b4391d99fc28b96e6d
author
Dami <dami.etoile@gmail.com>
12 files changed, +127 -49
Mmods/automata-example.cjs2+-
Msrc/juice.js11++++-----
Msrc/render/board.js36++++++++-----------------------
Asrc/render/facing.js13+++++++++++
Msrc/render/index.js10++++++---
Msrc/scenes/title.js8++++---
Msrc/stage.js3++-
Mtests/board.test.js22++++++++++++++++---
Mtests/browser/automata.mjs35++++++++++++++++++++++++++++-
Atests/facing.test.js17++++++++++++++
Mtests/juice.test.js17++++++++++++---
Mtests/mods.test.js2+-
M mods/automata-example.cjs +1 -1
M src/juice.js +5 -6
M src/render/board.js +9 -27
@@ -1,3 +1,5 @@
1+import { nativeHorizontalFacing } from './facing.js';2+13 /**24 * Ink and signal board renderer.35 *
@@ -35,22 +37,6 @@ const PROPERTIES = new Set([
3537 "TICK",3638 ]);3739 const OPERATORS = new Set(["IS", "AND"]);38-// These glyphs depict an actor with a readable horizontal facing. Terrain,39-// flags, doors, and abstract symbols stay untouched when a frame has facing data.40-const DIRECTIONAL_NOUNS = new Set([41- "MIAU",42- "FENN",43- "CAT",44- "DOG",45- "FOX",46- "RABBIT",47- "MOUSE",48- "ROBOT",49- "GHOST",50- "BIRD",51- "FISH",52- "MONSTER",53-]);5440 const FONT = "'Recursive', ui-monospace, monospace";5541 5642 function clamp(value, min, max) {
@@ -612,15 +598,11 @@ export function drawBoard(ctx, state, options = {}) {
612598 ctx.textAlign = "center";613599 ctx.textBaseline = "middle";614600 ctx.fillStyle = colors.fg;615- const nativeFacing =616- paletteEntry && typeof paletteEntry === "object"617- ? paletteEntry.faces === "left" || paletteEntry.faces === "right"618- ? paletteEntry.faces619- : null620- : !paletteEntry &&621- DIRECTIONAL_NOUNS.has(String(object.noun || "").toUpperCase())622- ? "left"623- : null;601+ const nativeFacing = nativeHorizontalFacing(602+ object.noun,603+ glyph,604+ paletteEntry,605+ );624606 const actualFacing = horizontalFacing(visual) || nativeFacing;625607 const mirror =626608 nativeFacing &&
@@ -630,8 +612,8 @@ export function drawBoard(ctx, state, options = {}) {
630612 const glyphScaleX = Number.isFinite(visual.glyphScaleX)631613 ? visual.glyphScaleX632614 : 1;633- // The Apple Color Emoji cat faces left by default. Mirror right-facing634- // actors; left-facing and non-directional glyphs keep their native form.615+ // Mirror the emoji from its declared native direction; keep the shadow,616+ // cell geometry, and text unchanged.635617 if (mirror || glyphScaleX !== 1) {636618 ctx.save();637619 ctx.translate(cx, cy);
A src/render/facing.js +13 -0
M src/render/index.js +7 -3
M src/scenes/title.js +5 -3
M src/stage.js +2 -1
M tests/board.test.js +19 -3
@@ -88,17 +88,33 @@ test('drawBoard keeps native left-facing actors and mirrors right-facing actors'
8888 assert.ok(ctx.calls.some(call => call[0] === 'scale' && call[1] === -1 && call[2] === 1));8989 });9090 91-test('drawBoard only flips mod palette glyphs with explicit faces metadata', () => {91+test('drawBoard keeps custom plain glyphs neutral and flips explicit mod faces', () => {9292 const ctx = context();9393 const frame = { positions: new Map([['cat', { x: 1, y: 1, facing: 'right' }]]), sentences: [], effects: [], shake: { amplitude: 0 }, tint: null, wipe: null };94- drawBoard(ctx, state, { x: 20, y: 10, cell: 50, palette: { CAT: '🐈' }, frame });94+ const custom = { ...state, objects: state.objects.map(object => object.id === 'cat' ? { ...object, noun: 'UNICORN' } : object) };95+ drawBoard(ctx, custom, { x: 20, y: 10, cell: 50, palette: { UNICORN: '🦄' }, frame });9596 assert.equal(ctx.calls.some(call => call[0] === 'scale' && call[1] === -1 && call[2] === 1), false);9697 ctx.calls.length = 0;9798 frame.positions.set('cat', { x: 1, y: 1, facing: 'left' });98- drawBoard(ctx, state, { x: 20, y: 10, cell: 50, palette: { CAT: { glyph: '🐈', faces: 'right' } }, frame });99+ drawBoard(ctx, custom, { x: 20, y: 10, cell: 50, palette: { UNICORN: { glyph: '🦄', faces: 'right' } }, frame });99100 assert.ok(ctx.calls.some(call => call[0] === 'scale' && call[1] === -1 && call[2] === 1));100101 });101102 103+test('drawBoard faces MIAU forward when a bundled pack repeats the cat as a string', () => {104+ const ctx = context();105+ const miauState = {106+ ...state,107+ objects: state.objects.map(object => object.id === 'cat' ? { ...object, noun: 'MIAU' } : object),108+ };109+ const frame = { positions: new Map([['cat', { x: 1, y: 1, facing: 'right' }]]), sentences: [], effects: [] };110+ drawBoard(ctx, miauState, { x: 20, y: 10, cell: 50, palette: { MIAU: '🐈' }, frame });111+ assert.ok(ctx.calls.some(call => call[0] === 'scale' && call[1] === -1 && call[2] === 1));112+ ctx.calls.length = 0;113+ frame.positions.set('cat', { x: 1, y: 1, facing: 'left' });114+ drawBoard(ctx, miauState, { x: 20, y: 10, cell: 50, palette: { MIAU: '🐈' }, frame });115+ assert.equal(ctx.calls.some(call => call[0] === 'scale' && call[1] === -1 && call[2] === 1), false);116+});117+102118 test('drawBoard compares movement facing with native mod facing and isolates glyph squash', () => {103119 const ctx = context();104120 const stateWithModActor = {
M tests/browser/automata.mjs +34 -1
@@ -86,6 +86,26 @@ try {
8686 await screenshot('automata-desktop-initial.png');8787 check('desktop proof starts with FENN at (4,3), phase 0');8888 89+ // This pack deliberately repeats MIAU's 🐈 as a plain palette string.90+ // A successful right step must still use the cat's native-left face data.91+ await page.keyboard.press('ArrowRight');92+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });93+ current = await state();94+ assert.deepEqual(95+ { x: current.objects.find(object => object.kind === 'object' && object.noun === 'MIAU').x,96+ y: current.objects.find(object => object.kind === 'object' && object.noun === 'MIAU').y },97+ { x: 1, y: 3 },98+ );99+ await screenshot('automata-desktop-miau-right.png');100+ await page.keyboard.press('ArrowLeft');101+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout });102+ await screenshot('automata-desktop-miau-left.png');103+ await page.keyboard.press('z');104+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });105+ await page.keyboard.press('z');106+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });107+ check('desktop MIAU turns right then left with a plain-string cat palette, and Undo restores both turns');108+89109 await page.keyboard.press('Space');90110 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });91111 current = await state();
@@ -128,6 +148,19 @@ try {
128148 assert.deepEqual({ x: initialMobileFenn.x, y: initialMobileFenn.y }, { x: 4, y: 3 });129149 await mobilePage.waitForTimeout(750);130150 await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-initial.png` });151+ await mobilePage.keyboard.press('ArrowRight');152+ await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });153+ await mobilePage.waitForTimeout(750);154+ await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-miau-right.png` });155+ await mobilePage.keyboard.press('ArrowLeft');156+ await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout });157+ await mobilePage.waitForTimeout(750);158+ await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-miau-left.png` });159+ await mobilePage.keyboard.press('z');160+ await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });161+ await mobilePage.keyboard.press('z');162+ await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });163+ check('mobile MIAU right and left frames and Undo complete');131164 await mobilePage.mouse.click(195, 770);132165 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });133166 mobileState = await mobilePage.evaluate(() => JSON.parse(window.render_game_to_text()));
@@ -143,7 +176,7 @@ try {
143176 assert.deepEqual(consoleErrors, [], 'desktop browser should have no console/page errors');144177 check('desktop and mobile complete without console errors');145178 process.stdout.write(`shareUrl=${shareUrl}\n`);146- process.stdout.write(`screenshots=${screenshots}/automata-desktop-initial.png,${screenshots}/automata-desktop-wait.png,${screenshots}/automata-mobile-initial.png,${screenshots}/automata-mobile-wait.png\n`);179+ process.stdout.write(`screenshots=${screenshots}/automata-desktop-miau-right.png,${screenshots}/automata-desktop-miau-left.png,${screenshots}/automata-mobile-miau-right.png,${screenshots}/automata-mobile-miau-left.png\n`);147180 } finally {148181 await context.close();149182 await browser.close();
A tests/facing.test.js +17 -0
M tests/juice.test.js +14 -3
@@ -28,7 +28,7 @@ test('move and push tweens expose grid coordinates and push scale', () => {
2828 assert.equal(juice.frame(50).positions.get('miau').x, 1);2929 assert.equal(juice.frame(50).positions.get('rock').x, 2);3030 assert.equal(juice.frame(50).positions.get('miau').facing, 'right');31- assert.equal(juice.frame(50).positions.get('rock').facing, null);31+ assert.equal(juice.frame(50).positions.get('rock').facing, 'right');3232 });3333 3434 test('snapping before a level transition prevents reused ids from drawing at old cells', () => {
@@ -57,7 +57,7 @@ test('resetLevel clears old sentences, effects, and win beats before seeding the
5757 assert.ok(Math.abs(frame.positions.get('a').glyphScaleX - 1) < 1e-9);5858 });5959 60-test('facing follows horizontal self movement, preserves through pushes and vertical moves, and resets between levels', () => {60+test('facing follows horizontal movement and preserves through vertical moves and level changes', () => {6161 const juice = createJuice({ now: () => 0 });6262 juice.syncState(state([object('player', 1, 1), object('rock', 2, 1)]), 0);6363 juice.consume({
@@ -75,7 +75,7 @@ test('facing follows horizontal self movement, preserves through pushes and vert
7575 rulesAdded: [], rulesRemoved: [], destroyed: [],7676 }, 50);7777 assert.equal(juice.frame(95).positions.get('player').facing, 'right');78- assert.equal(juice.frame(95).positions.get('rock').facing, null);78+ assert.equal(juice.frame(95).positions.get('rock').facing, 'right');7979 juice.snap(100);8080 assert.equal(juice.frame(100).positions.get('player').facing, 'right');8181 juice.resetLevel(state([object('player', 3, 3)]), 120);
@@ -105,6 +105,17 @@ test('undo restores each entity previous facing', () => {
105105 assert.equal(juice.frame(200).positions.get('player').facing, 'right');106106 });107107 108+test('a pushed MIAU faces its actual horizontal travel and Undo restores its prior pose', () => {109+ const juice = createJuice({ now: () => 0 });110+ juice.syncState(state([object('miau', 1, 1)]), 0);111+ juice.consume({ kind: 'step', accepted: true, moved: [{ id: 'miau', from: { x: 1, y: 1 }, to: { x: 2, y: 1 }, pushedBy: 'rock' }], rulesAdded: [], rulesRemoved: [], destroyed: [] }, 0);112+ assert.equal(juice.frame(90).positions.get('miau').facing, 'right');113+ juice.consume({ kind: 'blocked', accepted: false, blocked: { id: 'miau', reason: 'bounds' }, moved: [] }, 100);114+ assert.equal(juice.frame(100).positions.get('miau').facing, 'right');115+ juice.consume({ kind: 'undo', accepted: true, moved: [{ id: 'miau', from: { x: 2, y: 1 }, to: { x: 1, y: 1 } }], rulesAdded: [], rulesRemoved: [], destroyed: [] }, 200);116+ assert.equal(juice.frame(200).positions.get('miau').facing, null);117+});118+108119 test('reset clears facing immediately and undo restores nonmoving entities', () => {109120 const juice = createJuice({ now: () => 0 });110121 juice.syncState(state([object('player', 0, 0), object('rock', 2, 0)]), 0);
M tests/mods.test.js +1 -1