fuzzyworld

explodorama / files / test / padTiles.test.ts

padTiles.test.ts

7.9 KB · history · blame

1import { describe, expect, it } from 'vitest'
2import { actionableTiles, cursorDeadly, cursorLines, cursorStart, cursorWanted, ghostAction, keepCursor, l1Hint, moveCursor, restingCursor, tileActions } from '../src/ui/padTiles'
3import { board, v } from './helpers'
4
5const room = () =>
6 board(`
7 #######
8 #.....#
9 #.....#
10 #..P..#
11 #.....#
12 #.....#
13 #######`)
14
15const by = (actions: ReturnType<typeof tileActions>, b: string) => actions.find((a) => a.button === b)!
16
17describe('actionableTiles', () => {
18 it('the player first, then every tile a button can use, each once', () => {
19 const tiles = actionableTiles(room())
20 expect(tiles[0]).toEqual(v(3, 3))
21 expect(tiles.some((t) => t.x === 4 && t.y === 3)).toBe(true)
22 expect(new Set(tiles.map((t) => `${t.x},${t.y}`)).size).toBe(tiles.length)
23 })
24 it('walls and far tiles are not in it', () => {
25 const tiles = actionableTiles(room())
26 expect(tiles.some((t) => t.x === 0)).toBe(false)
27 expect(tiles.some((t) => t.x === 1 && t.y === 1)).toBe(false)
28 })
29 it('with Run, tiles two away are in it too', () => {
30 const s = room()
31 s.powers.run = true
32 expect(actionableTiles(s).some((t) => t.x === 5 && t.y === 3)).toBe(true)
33 })
34})
35
36describe('tileActions', () => {
37 it('Wait only on the player\'s own tile', () => {
38 const s = room()
39 expect(by(tileActions(s, v(3, 3)), 'wait').possible).toBe(true)
40 expect(by(tileActions(s, v(4, 3)), 'wait').possible).toBe(false)
41 })
42 it('a floor tile next to the player: Move and Bomb, not Charge or Wait', () => {
43 const a = tileActions(room(), v(4, 3))
44 expect(by(a, 'move')).toMatchObject({ possible: true, id: 'move', deadly: false, label: 'Move to this tile' })
45 expect(by(a, 'bomb')).toMatchObject({ possible: true, id: 'bomb', label: 'Place a bomb on this tile' })
46 expect(by(a, 'charged').possible).toBe(false)
47 expect(by(a, 'charged').label).toBe('')
48 })
49 it('the own tile with only Wait possible', () => {
50 const a = tileActions(room(), v(3, 3))
51 expect(a.filter((x) => x.possible).map((x) => x.button)).toEqual(['wait'])
52 expect(by(a, 'wait').label).toBe('Wait a turn')
53 })
54 it('marks a button deadly and says so', () => {
55 const s = board(`
56 #######
57 #P.1..#
58 #######`)
59 const move = by(tileActions(s, v(2, 1)), 'move')
60 expect(move.deadly).toBe(true)
61 expect(move.possible).toBe(true)
62 expect(move.label).toContain('(costs a life!)')
63 })
64 it('Wait is deadly when staying is', () => {
65 const s = board(`
66 #######
67 #P1...#
68 #######`)
69 const wait = by(tileActions(s, v(1, 1)), 'wait')
70 expect(wait.possible).toBe(true)
71 expect(wait.deadly).toBe(true)
72 expect(wait.label).toContain('(costs a life!)')
73 })
74 it('a Freeze charge is possible on the own tile', () => {
75 const s = room()
76 s.charge = { kind: 'freeze', left: 1 }
77 const a = tileActions(s, v(3, 3))
78 expect(by(a, 'charged')).toMatchObject({ possible: true, id: 'freeze' })
79 expect(by(a, 'wait').possible).toBe(true)
80 })
81})
82
83describe('the cursor', () => {
84 const tiles = [v(3, 3), v(4, 3), v(2, 3), v(3, 2), v(3, 4), v(5, 3)]
85 it('rests on the player\'s own tile, as a copy', () => {
86 const s = room()
87 const rest = restingCursor(s)
88 expect(rest).toEqual(v(3, 3))
89 expect(rest).not.toBe(s.player)
90 })
91 it('starts on the destination while auto-move walks, else on the player', () => {
92 const s = room()
93 expect(cursorStart(s, null)).toEqual(v(3, 3))
94 const start = cursorStart(s, v(5, 1))
95 expect(start).toEqual(v(5, 1))
96 expect(start).not.toBe(s.player)
97 // Arrived (destination is the player's own tile): back on the player.
98 expect(cursorStart(s, v(3, 3))).toEqual(v(3, 3))
99 })
100 it('Q80: a destination that is no longer a candidate (a flame made it unsafe) is not where the cursor starts', () => {
101 const s = room()
102 expect(cursorStart(s, v(5, 1), [v(3, 3), v(5, 1)])).toEqual(v(5, 1))
103 expect(cursorStart(s, v(5, 1), [v(3, 3)])).toEqual(v(3, 3))
104 })
105 it('the first d-pad press moves from the resting tile', () => {
106 expect(moveCursor(tiles, v(3, 3), 1, 0)).toEqual(v(4, 3))
107 expect(moveCursor(tiles, v(3, 3), 0, -1)).toEqual(v(3, 2))
108 expect(moveCursor(tiles, v(3, 3), 1, 1)).toEqual(v(3, 3))
109 expect(moveCursor([v(3, 3)], v(3, 3), 1, 0)).toEqual(v(3, 3))
110 })
111 it('is wanted only while a pad plays the board', () => {
112 const ok = { padActive: true, demo: false, dialogOpen: false, inspecting: false, offerPending: false, autopilot: false, status: 'playing' as const }
113 expect(cursorWanted(ok)).toBe(true)
114 expect(cursorWanted({ ...ok, padActive: false })).toBe(false)
115 expect(cursorWanted({ ...ok, demo: true })).toBe(false)
116 expect(cursorWanted({ ...ok, dialogOpen: true })).toBe(false)
117 expect(cursorWanted({ ...ok, inspecting: true })).toBe(false)
118 expect(cursorWanted({ ...ok, offerPending: true })).toBe(false)
119 expect(cursorWanted({ ...ok, autopilot: true })).toBe(false)
120 for (const status of ['dead', 'cleared', 'gameOver', 'won'] as const) expect(cursorWanted({ ...ok, status })).toBe(false)
121 })
122 it('moves between actionable tiles, staying put at the edge', () => {
123 expect(moveCursor(tiles, v(4, 3), 1, 0)).toEqual(v(5, 3))
124 expect(moveCursor(tiles, v(5, 3), 1, 0)).toEqual(v(5, 3))
125 expect(moveCursor(tiles, v(4, 3), -1, 0)).toEqual(v(3, 3))
126 })
127 it('keepCursor: stays, else the nearest still actionable, else nothing', () => {
128 expect(keepCursor(tiles, v(4, 3))).toEqual(v(4, 3))
129 expect(keepCursor(tiles, v(6, 3))).toEqual(v(5, 3))
130 expect(keepCursor([], v(1, 1))).toBeNull()
131 })
132})
133
134describe('the L1 hint', () => {
135 it('says what L1 will do, dimmed only when there is nothing to undo', () => {
136 expect(l1Hint({ inspecting: true, picked: false, onPlayer: true })).toEqual({ text: 'Move cursor back to you', dim: false })
137 expect(l1Hint({ inspecting: false, picked: true, onPlayer: true })).toEqual({ text: 'Cancel what you picked', dim: false })
138 expect(l1Hint({ inspecting: false, picked: false, onPlayer: false })).toEqual({ text: 'Move cursor back to you', dim: false })
139 expect(l1Hint({ inspecting: false, picked: false, onPlayer: true })).toEqual({ text: 'Move cursor back to you', dim: true })
140 })
141 it('"What is that?" wins over a pick, which wins over the cursor being away', () => {
142 expect(l1Hint({ inspecting: true, picked: true, onPlayer: false }).text).toBe('Move cursor back to you')
143 expect(l1Hint({ inspecting: false, picked: true, onPlayer: false }).text).toBe('Cancel what you picked')
144 })
145})
146
147describe('label and ghost', () => {
148 it('lists each possible button with what it does', () => {
149 expect(cursorLines(tileActions(room(), v(4, 3)))).toEqual([
150 { button: 'move', label: 'Move to this tile' },
151 { button: 'bomb', label: 'Place a bomb on this tile' },
152 ])
153 expect(cursorLines(tileActions(room(), v(3, 3)))).toEqual([{ button: 'wait', label: 'Wait a turn' }])
154 })
155 it('the tile is red only when everything possible there would cost a life', () => {
156 const s = board(`
157 #######
158 #P.1..#
159 #######`)
160 // Walking there is deadly; so is a bomb at fuse 1? Only what the sim says counts.
161 const a = tileActions(s, v(2, 1))
162 const all = a.filter((x) => x.possible)
163 expect(cursorDeadly(a)).toBe(all.every((x) => x.deadly))
164 expect(cursorDeadly(a.map((x) => ({ ...x, deadly: x.possible })))).toBe(true)
165 expect(cursorDeadly(a.map((x, i) => ({ ...x, deadly: x.possible && i === 0 })))).toBe(all.length === 1)
166 expect(cursorDeadly(a.map((x) => ({ ...x, deadly: false })))).toBe(false)
167 })
168 it('no possible action: not red', () => {
169 expect(cursorDeadly([])).toBe(false)
170 expect(cursorDeadly(tileActions(room(), v(1, 1)).map((x) => ({ ...x, deadly: true })))).toBe(false)
171 })
172 it('the ghost is the first of Move, Bomb, Charge; none on the own tile', () => {
173 const s = room()
174 expect(ghostAction(tileActions(s, v(4, 3)), v(4, 3), s.player)).toEqual({ id: 'move', target: v(4, 3) })
175 expect(ghostAction(tileActions(s, v(3, 3)), v(3, 3), s.player)).toBeNull()
176 })
177})