fuzzyworld

explodorama / files / test / turn.test.ts

turn.test.ts

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

00ad7d0a 2026-10-24 Pavel Safronov1import { LEVEL_WIDTH } from '../src/sim/levelgen'
5551eb37 2026-10-20 Pavel Safronov2import { describe, expect, it } from 'vitest'
3import { ACTIONS, isAvailable } from '../src/sim/actions'
4import { step } from '../src/sim/turn'
5import { board, v } from './helpers'
6
7describe('move', () => {
8 it('goes to a free neighbour only', () => {
9 const s = board(`
10 #####
11 #P.+#
12 #.#.#
13 #####`)
14 expect(ACTIONS.move.validTargets(s)).toEqual([v(1, 2), v(2, 1)])
15 expect(() => step(s, { id: 'move', target: v(1, 0) })).toThrow()
16 expect(step(s, { id: 'move', target: v(2, 1) }).state.player).toEqual(v(2, 1))
17 })
18
19 it('is blocked by a bomb', () => {
20 const s = board(`
21 #####
22 #P3.#
23 #####`)
24 expect(ACTIONS.move.validTargets(s)).toEqual([])
25 expect(isAvailable(s, 'move')).toBe(false)
26 })
27
28 it('does not change the state it was given', () => {
29 const s = board(`
30 ####
31 #P.#
32 ####`)
33 const before = structuredClone(s)
34 step(s, { id: 'move', target: v(2, 1) })
35 expect(s).toEqual(before)
36 })
37})
38
39describe('bomb timers', () => {
40 it('a placed bomb starts at the fuse and is not ticked on the turn it is placed', () => {
41 const s = board(`
42 #####
43 #P..#
44 #####`)
45 const { state, events } = step(s, { id: 'bomb', target: v(2, 1) })
46 expect(state.bombs).toEqual([{ pos: v(2, 1), timer: 3, fire: 1, placedTurn: 5 }])
47 expect(events.map((e) => e.type)).toEqual(['bombPlaced'])
48 })
49
50 it('the number is how many more actions until it explodes', () => {
51 let s = board(`
52 #######
53 #P....#
54 #######`, { fire: 1 })
55 s = step(s, { id: 'bomb', target: v(2, 1) }).state // shows 3
56 s = step(s, { id: 'wait' }).state
57 expect(s.bombs[0]!.timer).toBe(2)
58 s = step(s, { id: 'wait' }).state
59 expect(s.bombs[0]!.timer).toBe(1)
60 const last = step(s, { id: 'wait' })
61 expect(last.state.bombs).toEqual([])
62 expect(last.events.some((e) => e.type === 'exploded')).toBe(true)
63 })
64
65 it('respects bomb capacity', () => {
66 const s = board(`
67 #####
68 #P.3#
69 #####`)
70 expect(ACTIONS.bomb.validTargets(s)).toEqual([])
71 expect(ACTIONS.bomb.validTargets({ ...s, stats: { ...s.stats, capacity: 2 } })).toEqual([v(2, 1)])
72 })
73})
74
75describe('death', () => {
76 it('flame on the player costs a life and waits for continue', () => {
77 const s = board(`
78 #####
79 #P1.#
80 #####`)
81 const { state, events } = step(s, { id: 'wait' })
82 expect(state.status).toBe('dead')
83 expect(state.lives).toBe(2)
84 expect(events.at(-1)).toEqual({ type: 'playerDied', pos: v(1, 1), livesLeft: 2 })
85 expect(() => step(state, { id: 'wait' })).toThrow()
86 })
87
88 it('continue restarts the level from its seed, keeping lives and stats', () => {
89 const s = board(`
90 #####
91 #P1.#
92 #####`, { fire: 2 })
93 const dead = step(s, { id: 'wait' }).state
94 const again = step(dead, { id: 'continue' }).state
95 expect(again.status).toBe('playing')
96 expect(again.lives).toBe(2)
97 expect(again.stats.fire).toBe(2)
98 expect(again.turn).toBe(0)
00ad7d0a 2026-10-24 Pavel Safronov99 expect(again.width).toBe(LEVEL_WIDTH)
5551eb37 2026-10-20 Pavel Safronov100 })
101
102 it('the last life ends the run', () => {
103 const s = { ...board(`
104 #####
105 #P1.#
106 #####`), lives: 1 }
107 const { state } = step(s, { id: 'wait' })
108 expect(state.status).toBe('gameOver')
109 expect(() => step(state, { id: 'continue' })).toThrow()
110 })
111})
0580e85e 2026-10-24 Pavel Safronov112
113describe('how the player died (for the banner, user request 2026-10-24)', () => {
114 it('walked into an enemy, or an enemy walked in', () => {
115 const into = board(`
116 #####
117 #PB.#
118 #####`)
119 expect(step(into, { id: 'move', target: v(2, 1) }).state.death).toEqual({ cause: 'walkedIntoEnemy', enemy: 'blob', turn: into.turn + 1 })
120 const caught = board(`
121 #####
122 #P.S#
123 #####`)
124 caught.enemies[0]!.plan = [v(2, 1), v(1, 1)]
125 const after = step(step(caught, { id: 'wait' }).state, { id: 'wait' }).state
126 expect(after.death).toMatchObject({ cause: 'caughtByEnemy', enemy: 'seeker' })
127 })
128
129 it('caught in a bomb\'s blast, or only by one a chain set off', () => {
130 const blast = board(`
131 #####
132 #P1.#
133 #####`)
134 expect(step(blast, { id: 'wait' }).state.death?.cause).toBe('blast')
135 // The 1 goes off and sets off the 5, whose blast reaches the player; the 1's doesn't.
136 const chain = board(`
137 ########
138 #P.5.1.#
139 ########`, { fire: 2 })
140 chain.bombs.find((b) => b.timer === 5)!.fire = 2
141 chain.bombs.find((b) => b.timer === 1)!.fire = 2
142 const r = step(chain, { id: 'wait' }).state
143 expect(r.status).toBe('dead')
144 expect(r.death?.cause).toBe('chain')
145 })
146
147 it('nothing while alive, and nothing when Immortal', () => {
148 const s = board(`
149 #####
150 #P1.#
151 #####`)
152 expect(board(`
153 ###
154 #P#
155 ###`).death).toBeNull()
156 s.options = { ...s.options, immortal: true }
157 expect(step(s, { id: 'wait' }).state.death).toBeNull()
158 })
159})