fuzzyworld

explodorama / files / test / q75.test.ts

q75.test.ts

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

92cc4ee3 2026-11-02 Pavel Safronov1import { describe, expect, it } from 'vitest'
2import { isDeadly, predictDanger } from '../src/sim/danger'
3import { autoMoveAction, safeStep } from '../src/sim/path'
4import { tapMeaning } from '../src/sim/picks'
5import { eq } from '../src/sim/state'
6import { step } from '../src/sim/turn'
7import type { GameState, Vec } from '../src/sim/types'
8import { actionableTiles, cursorActions, cursorLines, farTiles, moveCursor, tileActions } from '../src/ui/padTiles'
9import { board, v } from './helpers'
10
11// Q75: a coin dropped by an enemy, in a pocket (block to the right, the edge to the left), reachable by walking round.
12const scene = (): GameState => {
13 const s = board(`
14 ..P..+B
15 $#.#+#.
16 ...+...
17 .#.#.#.
18 ..+...B
19 .#.#+#.
20 .......
21 .#.#.#.`)
22 s.powers.rpg = true
23 s.stats.capacity = 2
24 s.enemies[0]!.pos = v(6, 0)
25 s.enemies[0]!.plan = [v(6, 1)]
26 s.enemies[1]!.pos = v(6, 4)
27 s.enemies[1]!.plan = [v(6, 3)]
28 return s
29}
30const safeOf = (s: GameState) => {
31 const danger = predictDanger(s)
32 return (p: Vec) => safeStep(s, danger, p)
33}
34const coin = v(0, 1)
35const above = v(0, 0)
36
37describe('Q75: a coin in a pocket', () => {
38 it('the logic is right: a walk, a faded tile, no deadly first step', () => {
39 const s = scene()
40 expect(tapMeaning(s, coin)).toBe('walk')
41 expect(farTiles(s, safeOf(s)).some((f) => eq(f.tile, coin))).toBe(true)
42 expect(isDeadly(s, { id: 'move', target: v(2, 1) })).toBe(false)
43 })
44
45 it('the RPG tile above the coin is bright, Move greyed on it', () => {
46 const s = scene()
47 expect(actionableTiles(s).some((t) => eq(t, above))).toBe(true)
48 expect(tileActions(s, above).find((a) => a.button === 'move')!.possible).toBe(false)
49 expect(tileActions(s, above).find((a) => a.button === 'bomb')!.possible).toBe(true)
50 })
51
52 it('d-pad Down from the RPG tile reaches the coin, with nothing picked and with the coin as destination', () => {
53 const s = scene()
54 for (const dest of [null, coin]) {
55 const tiles = [...actionableTiles(s), ...farTiles(s, safeOf(s), dest).map((f) => f.tile)]
56 expect(eq(moveCursor(tiles, above, 0, 1), coin)).toBe(true)
57 }
58 })
59
60 it('with a destination elsewhere, the coin is no candidate (only the destination is far)', () => {
61 const s = scene()
62 const dest = v(0, 6)
63 const far = farTiles(s, safeOf(s), dest).map((f) => f.tile)
64 expect(far.some((t) => eq(t, coin))).toBe(false)
65 })
66})
67
68// Q75b: a tile that is both an RPG target and a walk away offers Move (the walk) and Bomb.
69describe('Q75b: Move on a tile that is also a bomb target', () => {
70 // The RPG flies to the end of the line: a wall at column 6 makes (5,1) the target.
71 const open = (): GameState => {
72 const s = board(`
73 .......
74 ...P..#
75 .......`)
76 s.powers.rpg = true
77 s.stats.capacity = 2
78 s.enemies = []
79 return s
80 }
81 const twoAway = v(5, 1)
82 const get = (s: GameState, tile: Vec, button: string) => cursorActions(s, safeOf(s), tile, null).find((a) => a.button === button)!
83
84 it('the old behaviour: the tile is bright, Move is greyed in tileActions', () => {
85 const s = open()
86 expect(actionableTiles(s).some((t) => eq(t, twoAway))).toBe(true)
87 expect(tileActions(s, twoAway).find((a) => a.button === 'move')!.possible).toBe(false)
88 expect(tileActions(s, twoAway).find((a) => a.button === 'bomb')!.possible).toBe(true)
89 expect(farTiles(s, safeOf(s)).some((f) => eq(f.tile, twoAway))).toBe(false)
90 })
91
92 it('the cursor offers Bomb and a walk of 2 steps', () => {
93 const s = open()
94 const move = get(s, twoAway, 'move')
95 expect(move.possible).toBe(true)
96 expect(move.far).toBe(true)
97 expect(move.label).toBe('Walk here · 2 steps')
98 expect(get(s, twoAway, 'bomb').possible).toBe(true)
99 const lines = cursorLines(cursorActions(s, safeOf(s), twoAway, null)).map((l) => l.button)
100 expect(lines).toEqual(['move', 'bomb'])
101 })
102
103 it('"Keep walking" when it is the destination', () => {
104 const s = open()
105 const move = cursorActions(s, safeOf(s), twoAway, twoAway).find((a) => a.button === 'move')!
106 expect(move.label).toBe('Keep walking · 2 steps')
107 })
108
109 it('Move walks (a step toward it), not an RPG', () => {
110 const s = open()
111 expect(tapMeaning(s, twoAway)).toBe('walk')
112 const action = autoMoveAction(s, twoAway, safeOf(s))!
113 const next = step(s, action).state
114 expect(next.player.x).toBeGreaterThan(s.player.x)
115 expect(next.bombs.length).toBe(0)
116 })
117
118 it('the same for a tile 3+ away', () => {
119 const s = open()
120 s.player = v(1, 1)
121 const far = v(5, 1)
122 expect(actionableTiles(s).some((t) => eq(t, far))).toBe(true)
123 expect(get(s, far, 'bomb').possible).toBe(true)
124 expect(get(s, far, 'move').far).toBe(true)
125 expect(get(s, far, 'move').label).toBe('Walk here · 4 steps')
126 })
127
128 it('a plain tile next to you keeps its ordinary Move, and your own tile has no walk', () => {
129 const s = open()
130 const near = get(s, v(4, 1), 'move')
131 expect(near.possible).toBe(true)
132 expect(near.far).toBeUndefined()
133 expect(get(s, s.player, 'move').possible).toBe(false)
134 })
135
136 it('the coin case: the faded tile still offers only the walk', () => {
137 const s = scene()
138 const acts = cursorActions(s, safeOf(s), coin, null)
139 expect(acts.find((a) => a.button === 'move')!.far).toBe(true)
140 expect(acts.filter((a) => a.possible).length).toBe(1)
141 })
142})