fuzzyworld

explodorama / files / test / keyDemo.test.ts

keyDemo.test.ts

7.1 KB · history · blame

1import { describe, expect, it } from 'vitest'
2import { DYING_KINDS, generateScene, LIVING_KINDS, PAD_CONTROL_KINDS, PAD_LATER, replayScene, SCENES, SCENE_KINDS } from '../src/sim/demo'
3import { eq } from '../src/sim/state'
4import {
5 KEY_GLYPHS, KEY_STILL_CAPTIONS, KEY_STILLS, keyCaptionHtml, keyDemoOrder, keyPlan, pressParts, runKeys, type KeyPress,
6} from '../src/ui/keyDemo'
7import { 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). */
10const KINDS = [...PAD_CONTROL_KINDS, ...LIVING_KINDS, ...DYING_KINDS].filter((k) => !PAD_LATER.includes(k) && !SCENES[k].fixed)
11
12describe('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
43describe('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
83describe('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
107describe('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
121describe('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
139describe('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})