ninelands-reddit/scripts/capture/capture.ts
Lukas Müllner 426776ee77 feat: Aufnahme der Katalog-Assets (Video, GIF, Standbild, Kachel)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 17:20:40 +01:00

114 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Aufnahmeseite für die Katalog-Assets: nur das Brett, ein Bot spielt eine echte Partie.
// Gesteuert von scripts/capture/record.mjs über window.capture.
import '../../src/client/style.css'
import {ZONE_NAMES} from '../../src/client/texts.ts'
import {BoardView} from '../../src/engine/app/board/BoardView.ts'
import {BOTS} from '../../src/engine/core/bots.ts'
import {Game} from '../../src/engine/core/Game.ts'
import {mulberry32} from '../../src/engine/core/rng.ts'
import {dailyBoard, dailyMap} from '../../src/shared/daily.ts'
const params = new URLSearchParams(location.search)
// Karte von vor dem ersten Spieltag – läuft nie als Tageskarte, verrät also nichts
const day = params.get('day') ?? '2026-10-10'
const seed = Number(params.get('seed') ?? 1)
const view = new BoardView({interactive: true})
const game = new Game(dailyBoard(dailyMap(new Date(`${day}T00:00:00Z`))), {
undoLimit: 0,
})
view.setBoard(game.board)
view.setLandNames(ZONE_NAMES)
const host = document.getElementById('board') as HTMLDivElement
host.append(view.svg)
// Ganze Partie vorab berechnen, damit die Aufnahme nur noch abspielt
const moves: {unit: Parameters<Game['place']>[0]; cell: number}[] = []
{
const sim = game.clone()
const rng = mulberry32(seed)
while (!sim.isGameOver()) {
const m = BOTS.smart(sim, rng)
sim.place(m.unit, m.cell)
moves.push({unit: m.unit, cell: m.cell})
}
}
function draw(): void {
view.update(game)
view.setRulers(game.rulers())
}
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
/** Einen Zug zeigen: kurz die Vorschau (beanspruchtes Land + Gold), dann setzen */
async function step(
i: number,
previewMs: number,
afterMs: number,
): Promise<void> {
const m = moves[i]
if (!m) return
const p = game.preview(m.unit, m.cell)
if (p.ok) {
view.setPlacementPreview({
cell: m.cell,
unit: m.unit,
zoi: p.zoi,
label: `${p.net >= 0 ? '+' : '−'}${Math.abs(p.net)}`,
owner: 1,
tone: p.net > 0 ? 'gain' : p.net < 0 ? 'loss' : 'neutral',
})
await sleep(previewMs)
}
view.setPlacementPreview(null)
const r = game.place(m.unit, m.cell)
if (r.ok) {
const net = r.yield.points - r.cost
if (net !== 0)
view.floatLabel(
m.cell,
`${net > 0 ? '+' : '−'}${Math.abs(net)}`,
net > 0 ? 'gain' : 'loss',
)
}
draw()
await sleep(afterMs)
}
declare global {
interface Window {
capture: {
moves: number
/** Partie abspielen: firstMoves sofort (für ein aussagekräftiges erstes Bild), dann im Takt */
play(
firstMoves: number,
previewMs: number,
afterMs: number,
): Promise<void>
/** Sofort bis Zug n springen (Standbilder) */
jump(n: number): void
}
}
}
window.capture = {
moves: moves.length,
async play(firstMoves, previewMs, afterMs) {
for (let i = 0; i < firstMoves; i++) {
const m = moves[i]
if (m) game.place(m.unit, m.cell)
}
draw()
for (let i = firstMoves; i < moves.length; i++)
await step(i, previewMs, afterMs)
},
jump(n) {
for (let i = game.moves.length; i < Math.min(n, moves.length); i++) {
const m = moves[i]
if (m) game.place(m.unit, m.cell)
}
draw()
},
}
draw()