fuzzyworld

explodorama / files / tools / balance / play.ts

play.ts

3.2 KB · history · blame

1import { predictDanger } from '../../src/sim/danger'
2import { LAST_LEVEL } from '../../src/sim/levelgen'
3import { idx, newRun } from '../../src/sim/state'
4import { step } from '../../src/sim/turn'
5import type { Death, GameState } from '../../src/sim/types'
6import type { Bot } from './bot'
7
8/** A level taking longer than this is given up: the bot is stuck, and the run counts as ended there. */
9const STALL_TURNS = 400
10
11export interface LevelStats {
12 level: number
13 /** Turns over every attempt at it. */
14 turns: number
15 deaths: number
16 cleared: boolean
17 underPar: boolean
18}
19
20export interface RunStats {
21 seed: number
22 levelsCleared: number
23 won: boolean
24 /** Ended because a level took over STALL_TURNS turns. */
25 stalled: boolean
26 turns: number
27 kills: number
28 coins: number
29 deaths: Partial<Record<Death['cause'], number>>
30 /** Turns ended standing on a tile that burns this turn or the next (the danger overlay). */
31 dangerTurns: number
32 /** Out of a blast's reach on the turn it went off. */
33 closeCalls: number
34 levels: LevelStats[]
35}
36
37/** What an experiment changes at the start of a run (a power-up held, a stat raised…). */
38export type Setup = (s: GameState) => void
39
40/** One run with `bot`, from level 1 until game over, the win, a stall, or `maxLevels` cleared. */
41export function playRun(seed: number, bot: Bot, setup: Setup, maxLevels = LAST_LEVEL): RunStats {
42 let s = newRun(seed)
43 setup(s)
44 const stats: RunStats = {
45 seed, levelsCleared: 0, won: false, stalled: false, turns: 0, kills: 0, coins: 0, deaths: {},
46 dangerTurns: 0, closeCalls: 0, levels: [],
47 }
48 let level: LevelStats = { level: s.level, turns: 0, deaths: 0, cleared: false, underPar: false }
49 stats.levels.push(level)
50 for (;;) {
51 if (s.status === 'gameOver' || s.status === 'won') break
52 if (s.status === 'cleared' && stats.levelsCleared >= maxLevels) break
53 if (level.turns > STALL_TURNS) {
54 stats.stalled = true
55 break
56 }
57 const prev = s
58 const action = bot.act(s)
59 const { state, events } = step(s, action)
60 s = state
61 // Whatever the bot did with the turn: what it was worth.
62 if (prev.status === 'playing' && !prev.offer && !prev.chargeOffer && action.id !== 'choose') {
63 level.turns++
64 stats.turns++
65 if (s.status === 'playing' && predictDanger(s).now.has(idx(s, s.player))) stats.dangerTurns++
66 const wasAt = prev.player
67 if (s.status === 'playing' && events.some((e) => e.type === 'exploded' && e.tiles.some((t) => t.x === wasAt.x && t.y === wasAt.y))) stats.closeCalls++
68 }
69 for (const e of events) {
70 if (e.type === 'enemyDied') stats.kills++
71 else if (e.type === 'playerDied') {
72 level.deaths++
73 const cause = s.death?.cause
74 if (cause) stats.deaths[cause] = (stats.deaths[cause] ?? 0) + 1
75 } else if (e.type === 'levelCleared') {
76 level.cleared = true
77 level.underPar = e.parBonus > 0
78 stats.levelsCleared++
79 } else if (e.type === 'levelStarted') {
80 level = { level: e.level, turns: 0, deaths: 0, cleared: false, underPar: false }
81 stats.levels.push(level)
82 }
83 }
84 if (s.status === 'won') stats.won = true
85 }
86 stats.coins = s.coins
87 return stats
88}