explodorama / files / test / keyDemo.test.ts
keyDemo.test.ts
| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { DYING_KINDS, generateScene, LIVING_KINDS, PAD_CONTROL_KINDS, PAD_LATER, replayScene, SCENES, SCENE_KINDS } from '../src/sim/demo' |
| 3 | import { eq } from '../src/sim/state' |
| 4 | import { |
| 5 | KEY_GLYPHS, KEY_STILL_CAPTIONS, KEY_STILLS, keyCaptionHtml, keyDemoOrder, keyPlan, pressParts, runKeys, type KeyPress, |
| 6 | } from '../src/ui/keyDemo' |
| 7 | import { board, v } from './helpers' |
| 8 | |
| 9 | /** The scenes the keyboard's demo plays: the controller demo's (the touch set but for the touch-only ones, and the dialog ones). */ |
| 10 | const KINDS = [...PAD_CONTROL_KINDS, ...LIVING_KINDS, ...DYING_KINDS].filter((k) => !PAD_LATER.includes(k) && !SCENES[k].fixed) |
| 11 | |
| 12 | describe('keyPlan on the demo scenes', () => { |
| 13 | for (const kind of KINDS) { |
| 14 | it(`${kind}: every turn has keys that, replayed through the game's rules, play that action on that tile`, () => { |
| 15 | for (let seed = 1; seed <= 6; seed++) { |
| 16 | const scene = generateScene(kind, seed)! |
| 17 | const { turns } = replayScene(scene) |
| 18 | for (const [i, t] of turns.entries()) { |
| 19 | const g = scene.gestures[i]! |
| 20 | const plan = keyPlan(t.prev, t.action, g) |
| 21 | if (g.kind === 'choice' || g.kind === 'pick') { |
| 22 | expect(plan).toBeNull() |
| 23 | continue |
| 24 | } |
| 25 | expect(plan, `seed ${seed} turn ${i} ${JSON.stringify(t.action)}`).not.toBeNull() |
| 26 | const { result, states } = runKeys(t.prev, plan!.map((p) => p.press)) |
| 27 | expect(result?.id).toBe(t.action.id) |
| 28 | const target = 'target' in t.action ? t.action.target : undefined |
| 29 | if (target) expect(eq(result!.target!, target)).toBe(true) |
| 30 | else expect(result?.target).toBeUndefined() |
| 31 | // What the board shows after each press is what the game's rules give. |
| 32 | expect(plan!.map((p) => ({ cursor: p.cursor, picked: p.picked }))).toEqual(states) |
| 33 | } |
| 34 | } |
| 35 | }) |
| 36 | } |
| 37 | |
| 38 | it('every scene the keyboard plays, whose touch caption talks of a tap, finger or hold, has a key caption', () => { |
| 39 | for (const kind of KINDS) if (/\b(tap|finger|hold)\b/i.test(SCENES[kind].caption)) expect(SCENES[kind].keyCaption, kind).toBeTruthy() |
| 40 | }) |
| 41 | }) |
| 42 | |
| 43 | describe('keyPlan', () => { |
| 44 | const s = board(` |
| 45 | ######## |
| 46 | #......# |
| 47 | #..P...# |
| 48 | #......# |
| 49 | #......# |
| 50 | ########`) |
| 51 | const keys = (plan: ReturnType<typeof keyPlan>) => plan!.map((p) => p.press) |
| 52 | |
| 53 | it('Wait is the Space key', () => { |
| 54 | expect(keys(keyPlan(s, { id: 'wait' }, { kind: 'button', button: 'wait' }))).toEqual([{ kind: 'key', button: 'wait' }]) |
| 55 | }) |
| 56 | it('a step next to you is one plain arrow', () => { |
| 57 | expect(keys(keyPlan(s, { id: 'move', target: v(3, 1) }, { kind: 'tap', tile: v(3, 1) }))).toEqual([{ kind: 'arrow', dx: 0, dy: -1 }]) |
| 58 | }) |
| 59 | it('a bomb next to you is B, then the arrow', () => { |
| 60 | expect(keys(keyPlan(s, { id: 'bomb', target: v(4, 2) }, { kind: 'button', button: 'bomb', tile: v(4, 2) }))).toEqual([ |
| 61 | { kind: 'key', button: 'bomb' }, { kind: 'arrow', dx: 1, dy: 0 }, |
| 62 | ]) |
| 63 | }) |
| 64 | it('a far tile the nearer ones hide from M and the arrow is Shift + arrows (the first wakes the cursor), then the key', () => { |
| 65 | const run = { ...s, powers: { ...s.powers, run: true } } |
| 66 | const plan = keyPlan(run, { id: 'run', target: v(5, 2) }, { kind: 'button', button: 'move', tile: v(5, 2) })! |
| 67 | const presses = keys(plan) |
| 68 | expect(presses[0]!.kind).toBe('hop') |
| 69 | expect(presses.slice(0, -1).every((p) => p.kind === 'hop')).toBe(true) |
| 70 | expect(presses.at(-1)).toEqual({ kind: 'key', button: 'move' }) |
| 71 | // The wake and the first hop are the same arrow; the cursor is on the player after the wake, and on the target at the end. |
| 72 | expect(presses[1]).toEqual(presses[0]) |
| 73 | expect(plan[0]!.cursor).toEqual(s.player) |
| 74 | expect(plan.at(-1)!.cursor).toEqual(v(5, 2)) |
| 75 | }) |
| 76 | it('choices and picks are left to the dialogs; an action with no tile has no plan', () => { |
| 77 | expect(keyPlan(s, { id: 'sellCharge' }, { kind: 'choice', swap: false })).toBeNull() |
| 78 | expect(keyPlan(s, { id: 'continue' }, { kind: 'pick' })).toBeNull() |
| 79 | expect(keyPlan(s, { id: 'move', target: v(0, 0) }, { kind: 'tap', tile: v(0, 0) })).toBeNull() |
| 80 | }) |
| 81 | }) |
| 82 | |
| 83 | describe('runKeys', () => { |
| 84 | const s = board(` |
| 85 | ####### |
| 86 | #.....# |
| 87 | #..P..# |
| 88 | #.....# |
| 89 | #######`) |
| 90 | it('plays nothing before the last press, or from keys that do nothing', () => { |
| 91 | const arrow: KeyPress = { kind: 'arrow', dx: 0, dy: -1 } |
| 92 | expect(runKeys(s, [arrow, arrow]).result).toBeNull() |
| 93 | expect(runKeys(s, [{ kind: 'hop', dx: 1, dy: 0 }]).result).toBeNull() |
| 94 | }) |
| 95 | it('the first Shift + arrow only shows the cursor on the player; the next hops it', () => { |
| 96 | const hop: KeyPress = { kind: 'hop', dx: 1, dy: 0 } |
| 97 | const { states } = runKeys(s, [hop, hop]) |
| 98 | expect(states[0]!.cursor).toEqual(v(3, 2)) |
| 99 | expect(states[1]!.cursor).not.toEqual(v(3, 2)) |
| 100 | }) |
| 101 | it('Space away from the cursor on the player does not wait', () => { |
| 102 | const hop: KeyPress = { kind: 'hop', dx: 1, dy: 0 } |
| 103 | expect(runKeys(s, [hop, hop, { kind: 'key', button: 'wait' }]).result).toBeNull() |
| 104 | }) |
| 105 | }) |
| 106 | |
| 107 | describe('pressParts', () => { |
| 108 | it('a hop holds Shift, a pan Alt; the charge key is V', () => { |
| 109 | expect(pressParts({ kind: 'hop', dx: 1, dy: 0 })).toEqual(['shift', 'right']) |
| 110 | expect(pressParts({ kind: 'pan', dx: 0, dy: -1 })).toEqual(['alt', 'up']) |
| 111 | expect(pressParts({ kind: 'arrow', dx: -1, dy: 0 })).toEqual(['left']) |
| 112 | expect(pressParts({ kind: 'key', button: 'charged' })).toEqual(['charge']) |
| 113 | expect(pressParts({ kind: 'key', button: 'wait' })).toEqual(['wait']) |
| 114 | expect(pressParts({ kind: 'esc' })).toEqual(['esc']) |
| 115 | }) |
| 116 | it('every part has a picture', () => { |
| 117 | for (const name of Object.values(KEY_GLYPHS)) expect(name).toMatch(/^keyboard_/) |
| 118 | }) |
| 119 | }) |
| 120 | |
| 121 | describe('keyCaptionHtml', () => { |
| 122 | it('turns every token into a key picture', () => { |
| 123 | expect(keyCaptionHtml('Press {B}, then {arrows}.', (n) => `/${n}.png`)).toBe( |
| 124 | 'Press <img class="pad-glyph" src="/keyboard_b.png" alt="">, then <img class="pad-glyph" src="/keyboard_arrows.png" alt="">.', |
| 125 | ) |
| 126 | }) |
| 127 | it('throws on an unknown token', () => { |
| 128 | expect(() => keyCaptionHtml('Press {F5}', (n) => n)).toThrow(/Unknown caption token/) |
| 129 | }) |
| 130 | it('every scene caption and still-scene caption draws', () => { |
| 131 | for (const k of SCENE_KINDS) { |
| 132 | const text = SCENES[k].keyCaption |
| 133 | if (text) expect(() => keyCaptionHtml(text, (n) => n), k).not.toThrow() |
| 134 | } |
| 135 | for (const [k, text] of Object.entries(KEY_STILL_CAPTIONS)) expect(() => keyCaptionHtml(text, (n) => n), k).not.toThrow() |
| 136 | }) |
| 137 | }) |
| 138 | |
| 139 | describe('keyDemoOrder', () => { |
| 140 | const touch = ['tapMove', 'holdBomb', 'bombButton', 'wait', 'autoMove', 'topBar', 'whatIsThat', 'run', 'chargeSwap', 'kick'] as const |
| 141 | const order = keyDemoOrder(touch) |
| 142 | it('puts the controls first, then the keyboard\'s own scenes', () => { |
| 143 | expect(order.slice(0, 7)).toEqual([...PAD_CONTROL_KINDS, ...KEY_STILLS]) |
| 144 | expect(KEY_STILLS).toEqual(['keyCursor', 'keyInspect', 'keyLook', 'keyMenu']) |
| 145 | }) |
| 146 | it('drops the touch-only scenes and the ones that need a dialog', () => { |
| 147 | for (const k of ['holdBomb', 'autoMove', 'topBar', 'whatIsThat', ...PAD_LATER]) expect(order).not.toContain(k) |
| 148 | expect(order.slice(7)).toEqual(['run', 'kick']) |
| 149 | }) |
| 150 | }) |