fuzzyworld

explodorama / files / test / farTiles.test.ts

farTiles.test.ts

6.6 KB · history · blame

1import { describe, expect, it } from 'vitest'
2import { isValidTarget } from '../src/sim/actions'
3import { isDeadly, predictDanger } from '../src/sim/danger'
4import { autoMoveAction, playerPath, safeStep } from '../src/sim/path'
5import { tapMeaning } from '../src/sim/picks'
6import { eq, newRun } from '../src/sim/state'
7import { step } from '../src/sim/turn'
8import type { GameState, Vec } from '../src/sim/types'
9import { defaultButton, keyButton } from '../src/ui/keyCursor'
10import { actionableTiles, farActions, farTiles, ghostAction, keepCursor, moveCursor, cursorLines } from '../src/ui/padTiles'
11import { board, v } from './helpers'
12
13const room = () =>
14 board(`
15 #######
16 #.....#
17 #.....#
18 #..P..#
19 #.....#
20 #.....#
21 #######`)
22
23const safeOf = (s: GameState) => {
24 const danger = predictDanger(s)
25 return (p: Vec) => safeStep(s, danger, p)
26}
27const far = (s: GameState, destination: Vec | null = null) => farTiles(s, safeOf(s), destination)
28const key = (p: Vec) => `${p.x},${p.y}`
29
30describe('farTiles', () => {
31 it('is every floor tile the bright blue does not cover, without the player\'s own', () => {
32 const s = room()
33 const tiles = far(s).map((f) => f.tile)
34 expect(tiles.some((t) => eq(t, v(1, 1)))).toBe(true)
35 expect(tiles.some((t) => eq(t, v(3, 3)))).toBe(false)
36 expect(tiles.some((t) => eq(t, v(4, 3)))).toBe(false)
37 expect(tiles.some((t) => t.x === 0)).toBe(false)
38 // 25 floor tiles: the player's, 4 neighbours (bright, also a bomb target), the rest faded.
39 const bright = new Set(actionableTiles(s).map(key))
40 expect(tiles.length + bright.size).toBe(25)
41 })
42
43 it('with Run, the tiles two away are bright, not faded', () => {
44 const s = room()
45 s.powers.run = true
46 const tiles = far(s).map((f) => f.tile)
47 expect(tiles.some((t) => eq(t, v(5, 3)))).toBe(false)
48 expect(tiles.some((t) => eq(t, v(1, 1)))).toBe(true)
49 })
50
51 it('counts the steps of the walk, nearest first', () => {
52 const s = room()
53 const list = far(s)
54 expect(list.find((f) => eq(f.tile, v(1, 1)))!.steps).toBe(4)
55 for (let i = 1; i < list.length; i++) expect(list[i]!.steps).toBeGreaterThanOrEqual(list[i - 1]!.steps)
56 })
57
58 it('leaves out tiles that cannot be walked to', () => {
59 const s = board(`
60 #######
61 #P..#.#
62 #...#.#
63 #######`)
64 expect(far(s).some((f) => eq(f.tile, v(5, 1)))).toBe(false)
65 })
66
67 it('leaves out tiles whose first move is not safe', () => {
68 // The bomb about to go off lights up tiles; auto-move never lands on one as its first move.
69 const s = board(`
70 #########
71 #P.1....#
72 #.......#
73 #########`)
74 const danger = predictDanger(s)
75 expect(danger.now.size).toBeGreaterThan(0)
76 const all = far(s).map((f) => f.tile)
77 expect(all.length).toBeGreaterThan(0)
78 for (const t of all) {
79 const move = autoMoveAction(s, t, safeOf(s))!
80 expect('target' in move && move.target && danger.now.has(move.target.y * s.width + move.target.x)).toBeFalsy()
81 }
82 })
83
84 it('with a destination, only it', () => {
85 const s = room()
86 expect(far(s, v(1, 1)).map((f) => f.tile)).toEqual([v(1, 1)])
87 expect(far(s, v(4, 3))).toEqual([])
88 })
89
90 for (const seed of [1, 2, 3, 4, 5, 6, 7, 8]) {
91 it(`seed ${seed}: every faded tile is tapped as a walk, with a legal and safe first move, and the label's steps are the walk`, () => {
92 const s = newRun(seed, 1 + (seed % 6))
93 const safe = safeOf(s)
94 const bright = new Set(actionableTiles(s).map(key))
95 const list = far(s)
96 for (const f of list) {
97 expect(bright.has(key(f.tile))).toBe(false)
98 expect(eq(f.tile, s.player)).toBe(false)
99 expect(tapMeaning(s, f.tile)).toBe('walk')
100 const move = autoMoveAction(s, f.tile, safe)!
101 expect(move).not.toBeNull()
102 expect(isDeadly(s, move)).toBe(false)
103 expect('target' in move && move.target && isValidTarget(s, move.id, move.target)).toBe(true)
104 expect(() => step(s, move)).not.toThrow()
105 expect(f.steps).toBe(playerPath(s, f.tile)!.length)
106 }
107 })
108
109 it(`seed ${seed}: the d-pad reaches every faded tile from the player's tile`, () => {
110 const s = newRun(seed, 1 + (seed % 6))
111 const list = far(s)
112 const candidates = [...actionableTiles(s), ...list.map((f) => f.tile)]
113 // Breadth first over the cursor's hops.
114 const seen = new Set([key(s.player)])
115 const queue: Vec[] = [s.player]
116 for (let i = 0; i < queue.length; i++) {
117 for (const [dx, dy] of [[0, -1], [0, 1], [-1, 0], [1, 0]] as const) {
118 const next = moveCursor(candidates, queue[i]!, dx, dy)
119 if (!seen.has(key(next))) {
120 seen.add(key(next))
121 queue.push(next)
122 }
123 }
124 }
125 for (const f of list) expect(seen.has(key(f.tile)), `${key(f.tile)}`).toBe(true)
126 })
127 }
128})
129
130describe('farActions', () => {
131 const s = room()
132 const f = far(s).find((x) => eq(x.tile, v(1, 1)))!
133
134 it('only Move is possible, saying how far, and nothing else', () => {
135 const actions = farActions(f, false)
136 expect(actions.filter((a) => a.possible).map((a) => a.button)).toEqual(['move'])
137 expect(actions.find((a) => a.button === 'move')).toMatchObject({ label: 'Walk here · 4 steps', deadly: false, far: true })
138 expect(cursorLines(actions)).toEqual([{ button: 'move', label: 'Walk here · 4 steps' }])
139 })
140
141 it('one step is singular, and a destination already set says Keep walking', () => {
142 expect(farActions({ tile: v(1, 1), steps: 1 }, false)[0]!.label).toBe('Walk here · 1 step')
143 expect(farActions(f, true)[0]!.label).toBe('Keep walking · 4 steps')
144 })
145
146 it('has no ghost, and the keyboard\'s M and Enter both walk', () => {
147 const actions = farActions(f, false)
148 expect(ghostAction(actions, f.tile, s.player)).toBeNull()
149 expect(keyButton(actions, 'move')).toBe('move')
150 expect(keyButton(actions, 'bomb')).toBeNull()
151 expect(defaultButton(actions, f.tile, s.player)).toBe('move')
152 })
153})
154
155describe('the cursor over bright and faded tiles', () => {
156 it('keeps a faded tile while it stays a candidate, else goes to the nearest', () => {
157 const s = room()
158 const candidates = [...actionableTiles(s), ...far(s).map((f) => f.tile)]
159 expect(keepCursor(candidates, v(1, 1))).toEqual(v(1, 1))
160 expect(keepCursor(candidates.filter((t) => !eq(t, v(1, 1))), v(1, 1))).not.toBeNull()
161 })
162
163 it('hops one tile at a time over the open floor, and onto a faded tile', () => {
164 const s = room()
165 const candidates = [...actionableTiles(s), ...far(s).map((f) => f.tile)]
166 let at: Vec = s.player
167 at = moveCursor(candidates, at, -1, 0)
168 expect(at).toEqual(v(2, 3))
169 at = moveCursor(candidates, at, -1, 0)
170 expect(at).toEqual(v(1, 3))
171 })
172})