explodorama / files / test / demo.test.ts
demo.test.ts
52d68b59 2026-10-24 Pavel Safronov | 1 | import { describe, expect, it } from 'vitest' |
36c5d6a2 2026-10-24 Pavel Safronov | 2 | import { isValidTarget } from '../src/sim/actions' |
e3d8dc49 2026-10-27 Pavel Safronov | 3 | import { isDeadly, predictDanger } from '../src/sim/danger' |
33a0256b 2026-10-24 Pavel Safronov | 4 | import { generateScene, MAX_STEPS, MIN_STEPS, replayScene, SCENE_KINDS, SCENES, showsAtEnd, type Gesture } from '../src/sim/demo' |
36c5d6a2 2026-10-24 Pavel Safronov | 5 | import { autoMoveAction, safeStep } from '../src/sim/path' |
| 6 | import { resolvePick, tapMeaning } from '../src/sim/picks' | |
52d68b59 2026-10-24 Pavel Safronov | 7 | import { step } from '../src/sim/turn' |
36c5d6a2 2026-10-24 Pavel Safronov | 8 | import type { Action, GameState, Vec } from '../src/sim/types' |
| 9 | ||
| 10 | /** | |
| 11 | * The action the real controls take for a gesture (main.ts: `tapTile`, the long press, `pick`), | |
| 12 | * given auto-move's destination; null if they would take none. | |
| 13 | */ | |
| 14 | function controlsTake(s: GameState, g: Gesture, destination: Vec | null): Action | null { | |
| 15 | const safe = (p: Vec) => safeStep(s, predictDanger(s), p) | |
| 16 | if (g.kind === 'tap') { | |
| 17 | const means = tapMeaning(s, g.tile) | |
7f242d36 2026-10-25 Pavel Safronov | 18 | return means === 'move' || means === 'diagonal' || means === 'kick' || means === 'detonate' || means === 'teleport' || means === 'slide' |
36c5d6a2 2026-10-24 Pavel Safronov | 19 | ? { id: means, target: g.tile } |
| 20 | : null | |
| 21 | } | |
e3d8dc49 2026-10-27 Pavel Safronov | 22 | if (g.kind === 'hold') return isValidTarget(s, 'bomb', g.tile) && !isDeadly(s, { id: 'bomb', target: g.tile }) ? { id: 'bomb', target: g.tile } : null |
| 23 | // Auto-move never takes a move that costs a life. | |
| 24 | const auto = (to: Vec) => { | |
| 25 | const a = autoMoveAction(s, to, safe) | |
| 26 | return a && !isDeadly(s, a) ? a : null | |
| 27 | } | |
| 28 | if (g.kind === 'walk') return tapMeaning(s, g.tile) === 'walk' ? auto(g.tile) : null | |
| 29 | if (g.kind === 'go') return destination && auto(destination) | |
8edb5cb9 2026-11-01 Pavel Safronov | 30 | if (g.kind === 'pick') return null |
0bfdc8e6 2026-10-25 Pavel Safronov | 31 | if (g.kind === 'choice') return s.chargeOffer ? { id: g.swap ? 'swapCharge' : 'sellCharge' } : null |
36c5d6a2 2026-10-24 Pavel Safronov | 32 | if (g.button === 'wait') return { id: 'wait' } |
| 33 | const id = g.tile && resolvePick(s, g.button, g.tile) | |
| 34 | return id ? { id, target: g.tile } : null | |
| 35 | } | |
52d68b59 2026-10-24 Pavel Safronov | 36 | |
| 37 | describe('demo scenes', () => { | |
8edb5cb9 2026-11-01 Pavel Safronov | 38 | for (const kind of SCENE_KINDS.filter((k) => !SCENES[k].fixed)) { |
36c5d6a2 2026-10-24 Pavel Safronov | 39 | it(`${kind}: found, its moment ${MIN_STEPS}-${MAX_STEPS} turns in from a calm start, each turn played by its gesture, the player alive (or dying at the end of a death scene)`, () => { |
fa3e81a7 2026-10-24 Pavel Safronov | 40 | for (let seed = 1; seed <= 6; seed++) { |
52d68b59 2026-10-24 Pavel Safronov | 41 | const scene = generateScene(kind, seed)! |
| 42 | expect(scene).not.toBeNull() | |
b8849d36 2026-11-01 Pavel Safronov | 43 | // The moment is MIN-MAX turns in (a scene may ask for more); a few turns of aftermath may follow it. |
| 44 | const moment = scene.moment! | |
| 45 | expect(moment + 1).toBeGreaterThanOrEqual(MIN_STEPS) | |
| 46 | expect(moment + 1).toBeLessThanOrEqual(SCENES[kind].maxSteps ?? MAX_STEPS) | |
| 47 | expect(scene.actions.length - (moment + 1)).toBeLessThanOrEqual(SCENES[kind].tail ?? 0) | |
36c5d6a2 2026-10-24 Pavel Safronov | 48 | expect(scene.gestures.length).toBe(scene.actions.length) |
| 49 | // Calm: the scene's own bombs are placed in it, so the countdown is seen from the start. | |
| 50 | expect(scene.start.bombs).toEqual([]) | |
| 51 | expect(scene.start.burning).toEqual([]) | |
e3384c6b 2026-10-24 Pavel Safronov | 52 | // The top bar shows in the demo: each scene starts as if fresh. |
| 53 | expect(scene.start.turn).toBe(0) | |
| 54 | expect(scene.start.coins).toBe(0) | |
d5e3ad4d 2026-10-24 Pavel Safronov | 55 | // Alive all along; at the end, dead in a scene about a way to die, alive in the rest. |
fa3e81a7 2026-10-24 Pavel Safronov | 56 | let s = scene.start |
36c5d6a2 2026-10-24 Pavel Safronov | 57 | let destination: Vec | null = null |
d5e3ad4d 2026-10-24 Pavel Safronov | 58 | for (const [i, a] of scene.actions.entries()) { |
36c5d6a2 2026-10-24 Pavel Safronov | 59 | const g = scene.gestures[i]! |
| 60 | if (g.kind === 'walk') destination = g.tile | |
| 61 | expect(controlsTake(s, g, destination)).toEqual(a) | |
fa3e81a7 2026-10-24 Pavel Safronov | 62 | s = step(s, a).state |
36c5d6a2 2026-10-24 Pavel Safronov | 63 | if (destination && s.player.x === destination.x && s.player.y === destination.y) destination = null |
d5e3ad4d 2026-10-24 Pavel Safronov | 64 | const last = i === scene.actions.length - 1 |
| 65 | expect(last && SCENES[kind].dies ? ['dead', 'gameOver'] : ['playing', 'cleared']).toContain(s.status) | |
52d68b59 2026-10-24 Pavel Safronov | 66 | } |
fa3e81a7 2026-10-24 Pavel Safronov | 67 | expect(showsAtEnd(scene)).toBe(true) |
33a0256b 2026-10-24 Pavel Safronov | 68 | // Sensible play, unless the scene is about a mistake: nothing lets enemies out of the exit |
| 69 | // or the gem (but in the scene about that), and no bomb goes on the exit. | |
| 70 | if (!SCENES[kind].dies) { | |
| 71 | for (const t of replayScene(scene).turns) { | |
| 72 | if (kind !== 'doorBlast') expect(t.events.some((e) => e.type === 'enemiesSpawned')).toBe(false) | |
| 73 | if (kind !== 'doorBlast' && t.action.id === 'bomb' && 'target' in t.action) expect(t.action.target).not.toEqual(t.prev.exit) | |
| 74 | } | |
| 75 | } | |
52d68b59 2026-10-24 Pavel Safronov | 76 | } |
| 77 | }) | |
| 78 | } | |
| 79 | ||
b8849d36 2026-11-01 Pavel Safronov | 80 | it('rubble: the ceiling falls at the moment, and the aftermath shows it', () => { |
| 81 | const scene = generateScene('rubble', 3)! | |
| 82 | expect(scene.start.cracked.length).toBe(1) | |
| 83 | const { turns } = replayScene(scene) | |
| 84 | expect(turns[scene.moment!]!.events.some((e) => e.type === 'ceilingFell')).toBe(true) | |
| 85 | expect(turns[turns.length - 1]!.next.cells.includes('rubble')).toBe(true) | |
| 86 | }) | |
| 87 | ||
52d68b59 2026-10-24 Pavel Safronov | 88 | it('the same seed gives the same scene', () => { |
| 89 | expect(generateScene('kill', 7)).toEqual(generateScene('kill', 7)) | |
| 90 | }) | |
| 91 | }) |