explodorama / files / test / new-enemies.test.ts
new-enemies.test.ts
| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { predictDanger } from '../src/sim/danger' |
| 3 | import { enemyCanEnter, moveTiles, planIntents } from '../src/sim/enemies' |
| 4 | import { safeStep } from '../src/sim/path' |
| 5 | import { step } from '../src/sim/turn' |
| 6 | import type { GameState, Vec } from '../src/sim/types' |
| 7 | import { board, v } from './helpers' |
| 8 | |
| 9 | const steps = (a: Vec, b: Vec) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y) |
| 10 | |
| 11 | /** The size of each planned move: 0 a rest, 1 a step, 2 a Sprinter's dash. */ |
| 12 | function moveSizes(s: GameState, i = 0): number[] { |
| 13 | const e = s.enemies[i]! |
| 14 | return e.plan.map((to, k) => steps(k ? e.plan[k - 1]! : e.pos, to)) |
| 15 | } |
| 16 | |
| 17 | describe('Ghosts and Wisps go through crates', () => { |
| 18 | it('a Ghost or a Wisp can enter a crate; a Blob, Hunter or Sprinter cannot; a bomb stops all', () => { |
| 19 | const s = board(` |
| 20 | ##### |
| 21 | #P+1# |
| 22 | #####`) |
| 23 | for (const kind of ['ghost', 'wisp'] as const) expect(enemyCanEnter(s, v(2, 1), kind)).toBe(true) |
| 24 | for (const kind of ['blob', 'hunter', 'sprinter'] as const) expect(enemyCanEnter(s, v(2, 1), kind)).toBe(false) |
| 25 | expect(enemyCanEnter(s, v(3, 1), 'ghost')).toBe(false) |
| 26 | }) |
| 27 | |
| 28 | it('a Ghost chases straight through a wall of crates', () => { |
| 29 | const s = board(` |
| 30 | ####### |
| 31 | #P++.G# |
| 32 | #.##### |
| 33 | #######`) |
| 34 | // (A tile below the player, so they aren't boxed in.) |
| 35 | let s2 = s |
| 36 | planIntents(s2) |
| 37 | const been: Vec[] = [] |
| 38 | for (let turn = 0; turn < 5; turn++) { |
| 39 | s2 = step(s2, { id: 'wait' }).state |
| 40 | been.push(s2.enemies[0]!.pos) |
| 41 | } |
| 42 | // Into the first crate, and on to the next, a step every other turn. |
| 43 | expect(been).toContainEqual(v(3, 1)) |
| 44 | expect(been.filter((p) => p.x === 3)).toHaveLength(2) |
| 45 | }) |
| 46 | |
| 47 | it('flame that burns the crate a Ghost is in kills it', () => { |
| 48 | let s = board(` |
| 49 | ###### |
| 50 | #P.1+# |
| 51 | ######`) |
| 52 | s.enemies.push({ id: 0, kind: 'ghost', pos: v(4, 1), dir: 'left', plan: [v(4, 1)], spawned: true, sheltered: false }) |
| 53 | s.bombs[0]!.timer = 1 |
| 54 | s = step(s, { id: 'wait' }).state |
| 55 | expect(s.enemies).toHaveLength(0) |
| 56 | }) |
| 57 | }) |
| 58 | |
| 59 | describe('Ghosts and Sprinters rest every other turn', () => { |
| 60 | it('a Ghost alternates a rest and a step', () => { |
| 61 | const s = board(` |
| 62 | ######### |
| 63 | #P.....G# |
| 64 | #########`) |
| 65 | planIntents(s) |
| 66 | const sizes = moveSizes(s) |
| 67 | expect(sizes).toHaveLength(3) |
| 68 | expect(sizes.every((n, k) => n === (sizes[0]! === 0 ? [0, 1, 0] : [1, 0, 1])[k])).toBe(true) |
| 69 | }) |
| 70 | |
| 71 | it('keeps alternating as its plan is topped up turn after turn', () => { |
| 72 | let s = board(` |
| 73 | ########### |
| 74 | #P.......G# |
| 75 | ###########`) |
| 76 | planIntents(s) |
| 77 | const done: number[] = [] |
| 78 | for (let turn = 0; turn < 6; turn++) { |
| 79 | const before = s.enemies[0]!.pos |
| 80 | s = step(s, { id: 'wait' }).state |
| 81 | done.push(steps(before, s.enemies[0]!.pos)) |
| 82 | } |
| 83 | for (let k = 1; k < done.length; k++) expect(done[k]).not.toBe(done[k - 1]) |
| 84 | }) |
| 85 | |
| 86 | it('a Sprinter dashes 2 tiles along a corridor, resting between dashes', () => { |
| 87 | const s = board(` |
| 88 | ########### |
| 89 | #P.......R# |
| 90 | ###########`) |
| 91 | planIntents(s) |
| 92 | const sizes = moveSizes(s).filter((n) => n > 0) |
| 93 | expect(sizes.length).toBeGreaterThan(0) |
| 94 | expect(sizes.every((n) => n === 2)).toBe(true) |
| 95 | }) |
| 96 | |
| 97 | it('a dash goes only 1 tile when the second is blocked', () => { |
| 98 | const s = board(` |
| 99 | ##### |
| 100 | #.R.# |
| 101 | #P### |
| 102 | #####`) |
| 103 | s.enemies[0]!.plan = [v(2, 1)] |
| 104 | planIntents(s) |
| 105 | for (const [k, to] of s.enemies[0]!.plan.entries()) expect(steps(k ? s.enemies[0]!.plan[k - 1]! : v(2, 1), to)).toBeLessThanOrEqual(1) |
| 106 | }) |
| 107 | |
| 108 | it('a dash through the player catches them on the way', () => { |
| 109 | const s = board(` |
| 110 | ###### |
| 111 | #R.P.# |
| 112 | ######`) |
| 113 | s.enemies[0]!.plan = [v(3, 1)] |
| 114 | const r = step(s, { id: 'wait' }) |
| 115 | // The dash would have ended at (3, 1) anyway here; a longer one stops on the player. |
| 116 | expect(r.state.status).toBe('dead') |
| 117 | }) |
| 118 | |
| 119 | it('a dash whose middle tile is the player stops there', () => { |
| 120 | const s = board(` |
| 121 | ####### |
| 122 | #.R.P.# |
| 123 | #.##### |
| 124 | #######`) |
| 125 | s.player = v(3, 1) |
| 126 | s.enemies[0]!.plan = [v(4, 1)] |
| 127 | const r = step(s, { id: 'wait' }) |
| 128 | expect(r.events).toContainEqual({ type: 'enemyMoved', id: 0, from: v(2, 1), to: v(3, 1) }) |
| 129 | expect(r.state.status).toBe('dead') |
| 130 | }) |
| 131 | |
| 132 | it('a bomb on the middle tile stops a dash', () => { |
| 133 | const s = board(` |
| 134 | ####### |
| 135 | #R5..P# |
| 136 | #######`) |
| 137 | s.enemies[0]!.plan = [v(3, 1)] |
| 138 | const r = step(s, { id: 'wait' }) |
| 139 | expect(r.state.enemies[0]!.pos).toEqual(v(1, 1)) |
| 140 | }) |
| 141 | |
| 142 | it('moveTiles: none for a rest, the tile for a step, the middle and the end for a dash', () => { |
| 143 | expect(moveTiles(v(1, 1), v(1, 1))).toEqual([]) |
| 144 | expect(moveTiles(v(1, 1), v(2, 1))).toEqual([v(2, 1)]) |
| 145 | expect(moveTiles(v(1, 1), v(1, 3))).toEqual([v(1, 2), v(1, 3)]) |
| 146 | }) |
| 147 | }) |
| 148 | |
| 149 | describe('Hunters always chase', () => { |
| 150 | it('every planned step of a Hunter gets closer to the player', () => { |
| 151 | for (let seed = 1; seed <= 20; seed++) { |
| 152 | const s = board(` |
| 153 | ######### |
| 154 | #P......# |
| 155 | #.#.#.#.# |
| 156 | #......H# |
| 157 | #########`) |
| 158 | s.aiRng = seed |
| 159 | planIntents(s) |
| 160 | let from = s.enemies[0]!.pos |
| 161 | for (const to of s.enemies[0]!.plan) { |
| 162 | expect(steps(to, s.player)).toBe(steps(from, s.player) - 1) |
| 163 | from = to |
| 164 | } |
| 165 | } |
| 166 | }) |
| 167 | }) |
| 168 | |
| 169 | describe('auto-move and a dash', () => { |
| 170 | it('will not step onto the middle tile of a Sprinter’s next dash', () => { |
| 171 | const s = board(` |
| 172 | ####### |
| 173 | #R..P.# |
| 174 | #######`) |
| 175 | s.enemies[0]!.plan = [v(3, 1)] |
| 176 | expect(safeStep(s, predictDanger(s), v(2, 1))).toBe(false) |
| 177 | expect(safeStep(s, predictDanger(s), v(5, 1))).toBe(true) |
| 178 | }) |
| 179 | }) |