fuzzyworld

explodorama / files / test / padDemo.test.ts

padDemo.test.ts

7.5 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 { PAD_STILL_CAPTIONS, cursorPath, padDemoOrder, padHops, padPlan, pressPart } from '../src/ui/padDemo'
5import { padCaptionHtml } from '../src/ui/padGlyph'
6import { actionableTiles, moveCursor, tileActions } from '../src/ui/padTiles'
7import { board, v } from './helpers'
8
9/** The scenes the pad's demo plays (PAD_LATER ones ask a dialog's question: later). */
10const KINDS = [...PAD_CONTROL_KINDS, ...LIVING_KINDS, ...DYING_KINDS].filter((k) => !PAD_LATER.includes(k) && !SCENES[k].fixed)
11
12describe('padPlan on the demo scenes', () => {
13 for (const kind of KINDS) {
14 it(`${kind}: every turn has a plan whose presses end on the action's tile with the face button that does it`, () => {
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 if (g.kind === 'choice' || g.kind === 'pick') {
21 expect(padPlan(t.prev, t.action, g)).toBeNull()
22 continue
23 }
24 const plan = padPlan(t.prev, t.action, g)
25 expect(plan, `seed ${seed} turn ${i} ${JSON.stringify(t.action)}`).not.toBeNull()
26 const target = 'target' in t.action && t.action.target ? t.action.target : t.prev.player
27 // Replay the d-pad presses on the cursor, as the pad does: it ends on the target.
28 const tiles = actionableTiles(t.prev)
29 let cursor = { ...t.prev.player }
30 for (const step of plan!) {
31 if (step.press.kind === 'dir') cursor = moveCursor(tiles, cursor, step.press.dx, step.press.dy)
32 expect(step.cursor).toEqual(step.press.kind === 'dir' ? cursor : target)
33 }
34 expect(eq(cursor, target)).toBe(true)
35 // The last press is the face button whose entry on that tile is this action.
36 const last = plan![plan!.length - 1]!.press
37 expect(last.kind).toBe('face')
38 if (last.kind === 'face') expect(tileActions(t.prev, target).find((a) => a.button === last.button)?.id).toBe(t.action.id)
39 }
40 }
41 })
42 }
43
44 it('every scene the pad plays, whose touch caption talks of a tap, finger or hold, has a pad caption', () => {
45 for (const kind of KINDS) if (/\b(tap|finger|hold)\b/i.test(SCENES[kind].caption)) expect(SCENES[kind].padCaption, kind).toBeTruthy()
46 })
47})
48
49describe('cursorPath', () => {
50 const room = () =>
51 board(`
52 #######
53 #.....#
54 #.....#
55 #..P..#
56 #.....#
57 #.....#
58 #######`)
59
60 it('is empty to the player\'s own tile', () => {
61 expect(cursorPath(room(), v(3, 3))).toEqual([])
62 })
63 it('one press to a tile next to the player', () => {
64 expect(cursorPath(room(), v(4, 3))).toEqual([{ dx: 1, dy: 0, cursor: v(4, 3) }])
65 })
66 it('null for a tile that is not actionable', () => {
67 expect(cursorPath(room(), v(1, 1))).toBeNull()
68 expect(cursorPath(room(), v(0, 0))).toBeNull()
69 })
70})
71
72describe('padPlan', () => {
73 const s = board(`
74 #######
75 #.....#
76 #..P..#
77 #.....#
78 #######`)
79
80 it('Wait is one press, with the cursor on the player', () => {
81 expect(padPlan(s, { id: 'wait' }, { kind: 'button', button: 'wait' })).toEqual([{ press: { kind: 'face', button: 'wait' }, cursor: v(3, 2) }])
82 })
83 it('a step is a direction, then Move', () => {
84 expect(padPlan(s, { id: 'move', target: v(3, 1) }, { kind: 'tap', tile: v(3, 1) })).toEqual([
85 { press: { kind: 'dir', dx: 0, dy: -1 }, cursor: v(3, 1) },
86 { press: { kind: 'face', button: 'move' }, cursor: v(3, 1) },
87 ])
88 })
89 it('choices and picks are left to the dialogs', () => {
90 expect(padPlan(s, { id: 'sellCharge' }, { kind: 'choice', swap: false })).toBeNull()
91 expect(padPlan(s, { id: 'continue' }, { kind: 'pick' })).toBeNull()
92 })
93 it('a tile where no face button does the action has no plan (the pad picks per button: a tile with two options is not shown)', () => {
94 const b = board(`
95 #######
96 #.....#
97 #.P3..#
98 #.....#
99 #######`)
100 b.powers = { ...b.powers, kick: true, detonate: true }
101 const target = v(3, 2)
102 const g = { kind: 'tap', tile: target } as const
103 expect(padPlan(b, { id: 'kick', target }, g)?.at(-1)?.press).toEqual({ kind: 'face', button: 'move' })
104 expect(padPlan(b, { id: 'detonate', target }, g)?.at(-1)?.press).toEqual({ kind: 'face', button: 'bomb' })
105 expect(padPlan(b, { id: 'throw', target }, g)).toBeNull()
106 })
107 it('an action with no tile, or a tile nothing can be done on, has no plan', () => {
108 expect(padPlan(s, { id: 'move' }, { kind: 'go' })).toBeNull()
109 expect(padPlan(s, { id: 'move', target: v(0, 0) }, { kind: 'tap', tile: v(0, 0) })).toBeNull()
110 })
111})
112
113describe('padCaptions', () => {
114 it('every scene caption and still-scene caption draws, with no unknown picture name', () => {
115 for (const k of SCENE_KINDS) {
116 const text = SCENES[k].padCaption
117 if (text) expect(() => padCaptionHtml(text, 'xbox', (n) => n), k).not.toThrow()
118 }
119 expect(PAD_STILL_CAPTIONS.padStick).toContain('{stick}')
120 expect(PAD_STILL_CAPTIONS.padLook).toContain('{stick}')
121 for (const [k, text] of Object.entries(PAD_STILL_CAPTIONS)) expect(() => padCaptionHtml(text, 'playstation', (n) => n), k).not.toThrow()
122 })
123 it('the charge scenes name the charge button', () => {
124 expect(SCENES.freeze.padCaption).toContain('{charge}')
125 expect(SCENES.decoy.padCaption).toContain('{charge}')
126 })
127})
128
129describe('pressPart', () => {
130 it('maps the four directions, the face buttons and the rest to the controller\'s parts', () => {
131 expect(pressPart({ kind: 'dir', dx: 0, dy: -1 }).part).toBe('up')
132 expect(pressPart({ kind: 'dir', dx: 0, dy: 1 }).part).toBe('down')
133 expect(pressPart({ kind: 'dir', dx: -1, dy: 0 }).part).toBe('left')
134 expect(pressPart({ kind: 'dir', dx: 1, dy: 0 }).part).toBe('right')
135 expect(pressPart({ kind: 'face', button: 'charged' }).part).toBe('charge')
136 expect(pressPart({ kind: 'face', button: 'bomb' }).part).toBe('bomb')
137 expect(pressPart({ kind: 'shoulder', side: 'r' }).part).toBe('r1')
138 expect(pressPart({ kind: 'stick', dir: 'left' })).toEqual({ part: 'stick', stickDir: 'left' })
139 expect(pressPart({ kind: 'start' }).part).toBe('start')
140 })
141})
142
143describe('padHops', () => {
144 const s = board(`
145 #######
146 #.....#
147 #..P..#
148 #.....#
149 #######`)
150 it('hops to tiles not been on, the first from the player, none twice', () => {
151 const hops = padHops(s, 5)
152 expect(hops.length).toBeGreaterThan(0)
153 expect(hops.length).toBeLessThanOrEqual(5)
154 const at = [s.player, ...hops.map((h) => h.to)]
155 for (let i = 0; i < at.length; i++) for (let j = i + 1; j < at.length; j++) expect(eq(at[i]!, at[j]!)).toBe(false)
156 expect(padHops(s, 0)).toEqual([])
157 })
158})
159
160describe('padDemoOrder', () => {
161 const touch = ['tapMove', 'holdBomb', 'bombButton', 'wait', 'autoMove', 'topBar', 'whatIsThat', 'run', 'chargeSwap', 'kick'] as const
162 const order = padDemoOrder(touch)
163 it('puts the pad scenes first', () => {
164 expect(order.slice(0, 9)).toEqual(['padCursor', ...PAD_CONTROL_KINDS, 'padL1', 'padR1', 'padStick', 'padLook', 'padStart'])
165 })
166 it('drops the touch-only scenes and the ones that need a dialog', () => {
167 for (const k of ['holdBomb', 'autoMove', 'topBar', 'whatIsThat', ...PAD_LATER]) expect(order).not.toContain(k)
168 expect(order.slice(9)).toEqual(['run', 'kick'])
169 })
170})