fuzzyworld

explodorama / files / test / shop.test.ts

shop.test.ts

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

e78b7781 2026-10-25 Pavel Safronov1import { describe, expect, it } from 'vitest'
2import { LAST_LEVEL } from '../src/sim/levelgen'
3import { drawShop, PRICES, rerollPrice, SHOP_EVERY, SHOP_SIZE, shopAfter } from '../src/sim/shop'
4import { newRun } from '../src/sim/state'
5import { step } from '../src/sim/turn'
6import type { GameState } from '../src/sim/types'
7
8/** A run on `level`, just cleared, with `coins`. */
9function cleared(level: number, coins = 100): GameState {
10 const s = newRun(5, level)
11 s.status = 'cleared'
12 s.coins = coins
13 return s
14}
15
16describe('the shop', () => {
17 it(`opens after every ${SHOP_EVERY}rd cleared level, not after the last`, () => {
18 expect([1, 2, 3, 4, 5, 6].filter(shopAfter)).toEqual([3, 6])
19 expect(shopAfter(LAST_LEVEL)).toBe(false)
20 const { state, events } = step(cleared(SHOP_EVERY), { id: 'continue' })
21 expect(events).toContainEqual({ type: 'shopOpened' })
22 expect(state.level).toBe(SHOP_EVERY)
23 expect(state.shop!.items).toHaveLength(SHOP_SIZE)
24 // Going on from it starts the next level, the shop gone.
25 const next = step(state, { id: 'continue' }).state
26 expect(next.level).toBe(SHOP_EVERY + 1)
27 expect(next.shop).toBeNull()
28 })
29
30 it('goes straight on after the other levels', () => {
31 const { state } = step(cleared(1), { id: 'continue' })
32 expect(state.level).toBe(2)
33 expect(state.shop).toBeNull()
34 })
35
36 it('sells power-ups not held (no Coin bag) and charges, the same for the same seed', () => {
37 const s = cleared(3)
38 s.powers.run = true
39 for (let r = 0; r < 30; r++) {
40 const items = drawShop(s, r)
41 expect(new Set(items).size).toBe(SHOP_SIZE)
42 expect(items).not.toContain('run')
43 expect(items).not.toContain('coinBag')
44 expect(drawShop(s, r)).toEqual(items)
45 }
46 })
47
48 it('buying takes the coins, works at once, carries on to the next level, and sells once', () => {
49 let s = step(cleared(3, 40), { id: 'continue' }).state
50 s.shop!.items = ['run', 'shield', 'heart', 'bombPlus']
51 s = step(s, { id: 'buy', kind: 'run' }).state
52 expect(s.coins).toBe(40 - PRICES.run)
53 expect(s.powers.run).toBe(true)
54 s = step(s, { id: 'buy', kind: 'shield' }).state
d3e4ed26 2026-10-25 Pavel Safronov55 expect(s.charge).toEqual({ kind: 'shield', left: 1 })
e78b7781 2026-10-25 Pavel Safronov56 expect(() => step(s, { id: 'buy', kind: 'run' })).toThrow()
57 const next = step(s, { id: 'continue' }).state
58 expect(next.powers.run).toBe(true)
59 expect(next.coins).toBe(40 - PRICES.run - PRICES.shield)
60 })
61
d3e4ed26 2026-10-25 Pavel Safronov62 it('a charge bought while carrying another kind asks; the shop waits for the answer', () => {
63 let s = step(cleared(3, 40), { id: 'continue' }).state
64 s.charge = { kind: 'pierce', left: 3 }
65 s.shop!.items = ['nuke', 'run', 'heart', 'bombPlus']
66 s = step(s, { id: 'buy', kind: 'nuke' }).state
fcf44f32 2026-10-25 Pavel Safronov67 expect(s.chargeOffer?.kind).toBe('nuke')
d3e4ed26 2026-10-25 Pavel Safronov68 expect(() => step(s, { id: 'continue' })).toThrow()
69 s = step(s, { id: 'swapCharge' }).state
70 expect(s.charge).toEqual({ kind: 'nuke', left: 3 })
71 expect(step(s, { id: 'continue' }).state.level).toBe(4)
72 })
73
e78b7781 2026-10-25 Pavel Safronov74 it('refuses what the player can’t afford', () => {
75 const s = step(cleared(3, 3), { id: 'continue' }).state
76 expect(() => step(s, { id: 'buy', kind: s.shop!.items[0]! })).toThrow()
77 })
78
0b63d158 2026-10-25 Pavel Safronov79 it('restocks for 1 coin, then 2, 3…, with a new draw', () => {
e78b7781 2026-10-25 Pavel Safronov80 let s = step(cleared(3, 20), { id: 'continue' }).state
0b63d158 2026-10-25 Pavel Safronov81 expect(rerollPrice(s.shop!)).toBe(1)
e78b7781 2026-10-25 Pavel Safronov82 s = step(s, { id: 'reroll' }).state
0b63d158 2026-10-25 Pavel Safronov83 expect(s.coins).toBe(19)
84 expect(rerollPrice(s.shop!)).toBe(2)
e78b7781 2026-10-25 Pavel Safronov85 s = step(s, { id: 'reroll' }).state
0b63d158 2026-10-25 Pavel Safronov86 expect(s.coins).toBe(17)
e78b7781 2026-10-25 Pavel Safronov87 expect(s.shop!.items).toEqual(drawShop(s, 2))
88 })
89
90 it('takes no actions on the board while open, and none outside a shop', () => {
91 const s = step(cleared(3), { id: 'continue' }).state
92 expect(() => step(s, { id: 'wait' })).toThrow()
93 expect(() => step(newRun(5), { id: 'buy', kind: 'heart' })).toThrow()
94 })
95})