fuzzyworld

explodorama / files / test / picks.test.ts

picks.test.ts

6.9 KB · history · blame

1import { describe, expect, it } from 'vitest'
2import { isDeadly } from '../src/sim/danger'
3import { approachTile, canPick, pickChoices, pickTargets, resolvePick, tapChoices, tapMeaning } from '../src/sim/picks'
4import { board, v } from './helpers'
5
6describe('Move stands for every way of moving', () => {
7 it('with nothing gained, only the 4 tiles next to you', () => {
8 const s = board(`
9 #####
10 #...#
11 #.P.#
12 #...#
13 #####`)
14 expect(pickTargets(s, 'move')).toHaveLength(4)
15 expect(resolvePick(s, 'move', v(3, 2))).toBe('move')
16 expect(resolvePick(s, 'move', v(3, 1))).toBeNull()
17 })
18
19 it('with Run, 2 tiles straight is a run', () => {
20 const s = board(`
21 ######
22 #P...#
23 ######`)
24 s.powers.run = true
25 expect(resolvePick(s, 'move', v(2, 1))).toBe('move')
26 expect(resolvePick(s, 'move', v(3, 1))).toBe('run')
27 })
28
29 it('with Kick, a bomb next to you is kicked', () => {
30 const s = board(`
31 ######
32 #P3..#
33 ######`)
34 expect(resolvePick(s, 'move', v(2, 1))).toBeNull()
35 s.powers.kick = true
36 expect(resolvePick(s, 'move', v(2, 1))).toBe('kick')
37 // With Bomb Pass too, walking into it still kicks it.
38 s.powers.bombPass = true
39 expect(resolvePick(s, 'move', v(2, 1))).toBe('kick')
40 })
41
42 it('with Bomb Pass, a bomb that cannot slide is walked over', () => {
43 const s = board(`
44 #####
45 #P3+#
46 #####`)
47 s.powers.kick = true
48 s.powers.bombPass = true
49 expect(resolvePick(s, 'move', v(2, 1))).toBe('move')
50 })
51
52 it('Teleport is the charge button’s: up to 3 steps away over anything (a charge since 2026-10-25)', () => {
53 const s = board(`
54 #######
55 #P#+..#
56 #######`)
57 s.charge = { kind: 'teleport', left: 3 }
58 expect(resolvePick(s, 'move', v(4, 1))).toBeNull()
59 expect(resolvePick(s, 'charged', v(4, 1))).toBe('teleport')
60 expect(resolvePick(s, 'charged', v(5, 1))).toBeNull()
61 })
62
63 it('a diagonal neighbour is a diagonal step, even with Teleport (a tile next to you is a step)', () => {
64 const s = board(`
65 #####
66 #P#.#
67 #...#
68 #####`)
69 s.powers.diagonal = true
70 expect(resolvePick(s, 'move', v(2, 2))).toBe('diagonal')
71 s.charge = { kind: 'teleport', left: 3 }
72 expect(resolvePick(s, 'move', v(2, 2))).toBe('diagonal')
73 })
74})
75
76describe('Bomb stands for placing and throwing', () => {
77 it('next to you is a plain bomb; with Throw, 2 tiles away over a crate is a throw', () => {
78 const s = board(`
79 ######
80 #P+..#
81 ######`)
82 expect(pickTargets(s, 'bomb')).toEqual([])
83 expect(canPick(s, 'bomb')).toBe(false)
84 s.powers.throw = true
85 expect(canPick(s, 'bomb')).toBe(true)
86 expect(resolvePick(s, 'bomb', v(3, 1))).toBe('throw')
87 })
88
89 it('other actions stand for just themselves', () => {
90 const s = board(`
91 #####
92 #P..#
93 #####`)
94 s.charge = { kind: 'teleport', left: 3 }
95 expect(resolvePick(s, 'teleport', v(3, 1))).toBe('teleport')
96 expect(resolvePick(s, 'teleport', v(1, 1))).toBeNull()
97 })
98})
99
100describe('a tap never costs a life (user request, 2026-10-26)', () => {
101 it('a tap on yourself does nothing (it was Wait)', () => {
102 const s = board(`
103 #####
104 #.P.#
105 #####`)
106 expect(tapMeaning(s, v(2, 1))).toBe('blocked')
107 })
108
109 it('a step in front of an enemy is deadly: a tap refuses it, Move then the tile still takes it', () => {
110 const s = board(`
111 ######
112 #P.B.#
113 ######`)
114 s.enemies[0]!.plan = [v(2, 1)]
115 expect(tapMeaning(s, v(2, 1))).toBe('deadly')
116 expect(resolvePick(s, 'move', v(2, 1))).toBe('move')
117 expect(isDeadly(s, { id: 'move', target: v(2, 1) })).toBe(true)
118 expect(isDeadly(s, { id: 'wait' })).toBe(false)
119 })
120
121 it('Wait is deadly when an enemy steps onto you, unless a Shield or Immortal takes the hit', () => {
122 const s = board(`
123 #####
124 #PB.#
125 #####`)
126 s.enemies[0]!.plan = [v(1, 1)]
127 expect(isDeadly(s, { id: 'wait' })).toBe(true)
128 expect(isDeadly({ ...s, charge: { kind: 'shield', left: 1 } }, { id: 'wait' })).toBe(false)
129 expect(isDeadly({ ...s, options: { ...s.options, immortal: true } }, { id: 'wait' })).toBe(false)
130 })
131})
132
133describe('a tap two actions could answer asks which (user request, 2026-10-31)', () => {
134 const bombNext = () =>
135 board(`
136 ######
137 #P3..#
138 ######`)
139
140 it('a bomb next to you, with Kick: kicked at once', () => {
141 const s = bombNext()
142 s.powers.kick = true
143 expect(tapMeaning(s, v(2, 1))).toBe('kick')
144 })
145
146 it('a bomb next to you, with Bomb Pass alone: walked onto', () => {
147 const s = bombNext()
148 s.powers.bombPass = true
149 expect(tapMeaning(s, v(2, 1))).toBe('move')
150 })
151
152 it('with Kick and Bomb Pass: a choice of both', () => {
153 const s = bombNext()
154 s.powers.kick = true
155 s.powers.bombPass = true
156 expect(tapMeaning(s, v(2, 1))).toBe('choose')
157 expect(tapChoices(s, v(2, 1))).toEqual(['kick', 'move'])
158 })
159
160 it('Move picked, then the bomb: the same choice, dangerous or not', () => {
161 const s = bombNext()
162 s.powers.kick = true
163 s.powers.bombPass = true
164 expect(pickChoices(s, 'move', v(2, 1))).toEqual(['kick', 'move'])
165 s.powers.bombPass = false
166 expect(pickChoices(s, 'move', v(2, 1))).toEqual([])
167 })
168
169 it('a bomb about to go off: walking onto it is deadly, so a tap just kicks; Move then the tile still offers both', () => {
170 const s = board(`
171 ######
172 #P1..#
173 ######`)
174 s.powers.kick = true
175 s.powers.bombPass = true
176 expect(isDeadly(s, { id: 'move', target: v(2, 1) })).toBe(true)
177 expect(isDeadly(s, { id: 'kick', target: v(2, 1) })).toBe(false)
178 expect(tapChoices(s, v(2, 1))).toEqual(['kick'])
179 expect(tapMeaning(s, v(2, 1))).toBe('kick')
180 expect(pickChoices(s, 'move', v(2, 1))).toEqual(['kick', 'move'])
181 })
182
183 it('a bomb with neither is not moved into', () => {
184 expect(tapMeaning(bombNext(), v(2, 1))).toBe('blocked')
185 })
186})
187
188describe('a tap on a crate walks to the tile beside it (user request, 2026-10-31)', () => {
189 const lane = () =>
190 board(`
191 #######
192 #P.+..#
193 #######`)
194
195 it('with Run, a crate 2 tiles away is approached, not ignored', () => {
196 const s = lane()
197 s.powers.run = true
198 expect(tapMeaning(s, v(3, 1))).toBe('approach')
199 expect(approachTile(s, v(3, 1))).toEqual(v(2, 1))
200 })
201
202 it('takes the side with the shorter way', () => {
203 const s = board(`
204 #######
205 #P....#
206 #.#+#.#
207 #.....#
208 #######`)
209 expect(approachTile(s, v(3, 2))).toEqual(v(3, 1))
210 // A crate diagonal from the player: the two tiles beside it that touch it.
211 const diag = board(`
212 #####
213 #P..#
214 #.+.#
215 #####`)
216 expect([v(2, 1), v(1, 2)]).toContainEqual(approachTile(diag, v(2, 2)))
217 })
218
219 it('beside it already, or with no way there: nothing', () => {
220 const s = lane()
221 s.player = v(2, 1)
222 expect(tapMeaning(s, v(3, 1))).toBeNull()
223 const sealed = board(`
224 #######
225 #P#+..#
226 #######`)
227 expect(tapMeaning(sealed, v(3, 1))).toBeNull()
228 })
229})