fuzzyworld

explodorama / files / test / oil-slide.test.ts

oil-slide.test.ts

Blame: the commit that last changed each line · file · history

1f0038ac 2026-11-01 Pavel Safronov1import { describe, expect, it } from 'vitest'
2import { planIntents } from '../src/sim/enemies'
3import { autoMoveAction, safeStep } from '../src/sim/path'
4import { predictDanger } from '../src/sim/danger'
5import { step } from '../src/sim/turn'
6import { board, v } from './helpers'
7
8describe('oil is slippery (user request, 2026-10-31)', () => {
9 it('a step onto oil slides the player to the last oiled tile of the line', () => {
10 const s = board(`
11 #########
12 #P.~~~.##
13 #########`)
14 const r = step(s, { id: 'move', target: v(2, 1) })
15 expect(r.state.player).toEqual(v(2, 1))
16 const t = step(r.state, { id: 'move', target: v(3, 1) })
17 expect(t.state.player).toEqual(v(5, 1))
18 expect(t.events).toContainEqual({ type: 'oilSlid', from: v(3, 1), to: v(5, 1) })
19 })
20
21 it('stops on the last oiled tile, not at the wall or the next plain tile', () => {
22 const s = board(`
23 #######
24 #P~~..#
25 #######`)
26 expect(step(s, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(3, 1))
27 })
28
29 it('goes straight on: it does not follow a bend in the spill', () => {
30 const s = board(`
31 #####
32 #P~.#
33 #.~.#
34 #####`)
35 expect(step(s, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(2, 1))
36 })
37
38 it('leaving oil, or stepping on a lone oil tile, does not slide', () => {
39 const s = board(`
40 #####
41 #~P.#
42 #####`)
43 expect(step(s, { id: 'move', target: v(3, 1) }).state.player).toEqual(v(3, 1))
44 expect(step(s, { id: 'move', target: v(1, 1) }).state.player).toEqual(v(1, 1))
45 })
46
47 it('stops before a bomb, and before an enemy', () => {
48 const bomb = board(`
49 #######
50 #P~~1.#
51 #######`)
52 bomb.bombs[0]!.timer = 5
53 expect(step(bomb, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(3, 1))
54 const foe = board(`
55 #######
56 #P~~B.#
57 #######`)
58 expect(step(foe, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(3, 1))
59 })
60
61 it('a run that lands on oil slides on', () => {
62 const s = board(`
63 ##########
64 #P.~~~~..#
65 ##########`)
66 s.powers.run = true
67 expect(step(s, { id: 'run', target: v(3, 1) }).state.player).toEqual(v(6, 1))
68 })
69
70 it('picks up what lies on the oil it slides over', () => {
71 const s = board(`
72 #########
73 #P~~~~..#
74 #########`)
75 s.drops.push({ pos: v(3, 1), kind: 'coins', amount: 1 })
76 const r = step(s, { id: 'move', target: v(2, 1) })
77 expect(r.state.player).toEqual(v(5, 1))
78 expect(r.state.coins).toBe(1)
79 })
80
81 it('a slide that ends in flame kills', () => {
82 const s = board(`
83 #########
84 #P~~~...#
85 #########`)
86 s.bombs.push({ pos: v(5, 1), timer: 1, fire: 2, placedTurn: 0 })
87 // The bomb's flame (and the oil it lights) is where the slide ends.
88 const r = step(s, { id: 'move', target: v(2, 1) })
89 expect(r.state.status).toBe('dead')
90 })
91
92 it('a basic enemy planning a step onto oil announces the whole slide', () => {
93 const s = board(`
94 ##########
95 #D~~~..P##
96 ##########`)
97 planIntents(s)
98 expect(s.enemies[0]!.plan[0]).toEqual(v(4, 1))
99 })
100
101 it('moves along the whole announced slide', () => {
102 const s = board(`
103 ##########
104 #D~~~..P##
105 ##########`)
106 s.enemies[0]!.plan = [v(4, 1)]
107 const r = step(s, { id: 'wait' })
108 expect(r.state.enemies[0]!.pos).toEqual(v(4, 1))
109 })
110
111 it('a Ghost or Wisp flies over oil without slipping', () => {
112 const s = board(`
113 ##########
114 #G~~~..P##
115 ##########`)
116 s.enemies[0]!.plan = [v(2, 1)]
117 const r = step(s, { id: 'wait' })
118 expect(r.state.enemies[0]!.pos).toEqual(v(2, 1))
119 })
120
121 it('an enemy that slides through the player catches them there', () => {
122 const s = board(`
123 ##########
124 #D~~~~...#
125 #####P####
126 ##########`)
127 s.player = v(3, 1)
128 s.enemies[0]!.plan = [v(5, 1)]
129 const r = step(s, { id: 'wait' })
130 expect(r.state.status).toBe('dead')
131 expect(r.state.enemies[0]!.pos).toEqual(v(3, 1))
132 })
133
134 it('a plan that no longer matches the oil is dropped and planned again', () => {
135 const s = board(`
136 ##########
137 #D~~~..P##
138 ##########`)
139 s.enemies[0]!.plan = [v(4, 1)]
140 s.oil = []
141 planIntents(s)
142 expect(s.enemies[0]!.plan[0]).not.toEqual(v(4, 1))
143 })
144
145 it('walking onto an enemy that stands on oil is a death, even though the slide carries on', () => {
146 const s = board(`
147 #######
148 #PB~~.#
149 #######`)
150 s.oil.push(2 + 7)
151 const r = step(s, { id: 'move', target: v(2, 1) })
152 expect(r.state.status).toBe('dead')
153 expect(r.state.death?.cause).toBe('walkedIntoEnemy')
154 })
155
156 it('a diagonal step onto oil does not slide', () => {
157 const s = board(`
158 #####
159 #P..#
160 #.~~#
161 #####`)
162 s.powers.diagonal = true
163 expect(step(s, { id: 'diagonal', target: v(2, 2) }).state.player).toEqual(v(2, 2))
164 })
165
166 it('with Bomb Pass the slide goes over a bomb', () => {
167 const s = board(`
168 #######
169 #P~~5.#
170 #######`)
171 s.oil.push(4 + 7)
172 s.powers.bombPass = true
173 expect(step(s, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(4, 1))
174 })
175
176 it('stops on the open exit, which clears the level', () => {
177 const s = board(`
178 #######
179 #P~~E.#
180 #######`)
181 s.oil.push(4 + 7)
182 const r = step(s, { id: 'move', target: v(2, 1) })
183 expect(r.state.player).toEqual(v(4, 1))
184 expect(r.state.status).toBe('cleared')
185 })
186
187 it('lingering flame on the way burns the player', () => {
188 const s = board(`
189 #######
190 #P~~~.#
191 #######`)
192 s.burning = [3 + 7]
193 expect(step(s, { id: 'move', target: v(2, 1) }).state.status).toBe('dead')
194 })
195
196 it('a Sprinter’s planned dash that lands on oil slides on, and the plan is kept while valid', () => {
197 const s = board(`
198 ###########
199 #R.~~~..P##
200 ###########`)
201 const e = s.enemies[0]!
202 e.plan = [v(1, 1), v(5, 1)]
203 planIntents(s)
204 expect(e.plan.slice(0, 2)).toEqual([v(1, 1), v(5, 1)])
205 })
206
207 it('auto-move keeps off a step that would slide, so it never bounces or slides onto the exit', () => {
208 const s = board(`
209 #########
210 #P.~~~~.#
211 #########`)
212 const danger = predictDanger(s)
213 expect(autoMoveAction(s, v(4, 1), (p) => safeStep(s, danger, p))).toBeNull()
214 const e = board(`
215 ##########
216 #P~~E...##
217 ####.#####
218 ##########`)
219 e.oil.push(4 + 10)
220 const act = autoMoveAction(e, v(4, 2), () => true)
221 expect(act).toBeNull()
222 })
223
224 it('a Ghost’s plan is not slid when planned', () => {
225 const s = board(`
226 ##########
227 #G~~~..P##
228 ##########`)
229 s.enemies[0]!.dir = 'right'
230 planIntents(s)
231 for (const p of s.enemies[0]!.plan) expect(p.x).toBeLessThanOrEqual(2)
232 })
233})
8edb5cb9 2026-11-01 Pavel Safronov234
235describe('two gems on one turn (user report, 2026-10-31)', () => {
236 it('a slide over two gems offers them one after the other', () => {
237 const s = board(`
238 ########
239 #P.....#
240 ########`)
241 s.powers.slide = true
242 s.gems = [{ pos: v(2, 1), color: 'red' }, { pos: v(3, 1), color: 'red' }]
243 const r = step(s, { id: 'slide', target: v(6, 1) })
244 expect(r.state.offer).not.toBeNull()
245 expect(r.state.waitingGems).toHaveLength(1)
246 expect(r.state.gems).toHaveLength(0)
247 const first = r.state.offer![0]!
248 const t = step(r.state, { id: 'choose', kind: first })
249 expect(t.state.offer).not.toBeNull()
250 expect(t.state.waitingGems).toHaveLength(0)
251 expect(t.events.some((e) => e.type === 'offerAgain')).toBe(true)
252 const u = step(t.state, { id: 'choose', kind: t.state.offer![0]! })
253 expect(u.state.offer).toBeNull()
254 })
255})
256
257describe('two gems, more cases (reviewer, 2026-10-31)', () => {
258 it('a slide over two gems onto the open exit still asks for both', () => {
259 const s = board(`
260 #########
261 #P.%.%.E#
262 #########`)
263 s.powers.slide = true
264 const r = step(s, { id: 'slide', target: v(7, 1) })
265 expect(r.state.status).toBe('cleared')
266 expect(r.state.offer).not.toBeNull()
267 const t = step(r.state, { id: 'choose', kind: r.state.offer![0]! })
268 expect(t.state.offer).not.toBeNull()
269 expect(t.events.filter((e) => e.type === 'offerAgain')).toHaveLength(1)
270 expect(r.events.filter((e) => e.type === 'gemQueued')).toHaveLength(1)
271 })
272
273 it('dying on the turn two gems are taken drops both', () => {
274 const s = board(`
275 #########
276 #P%%1...#
277 #########`)
278 s.powers.slide = true
279 s.gems = [{ pos: v(2, 1), color: 'red' }, { pos: v(3, 1), color: 'red' }]
280 const r = step(s, { id: 'slide', target: v(3, 1) })
281 expect(r.state.status).toBe('dead')
282 expect(r.state.offer).toBeNull()
283 expect(r.state.waitingGems).toEqual([])
284 })
285})