Tre scene con aree calpestabili e punti cliccabili ricavati dagli sfondi, tools/prepare_background.py per ridurli a 640x360, il castello del Trebbio che dà indizi in base ai progressi, container nginx sulla porta 8080. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
5.2 KiB
JavaScript
98 lines
5.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { story } from '../src/story/story.js';
|
|
import { solution } from '../src/story/solution.js';
|
|
import { Game, VERBS } from '../src/engine/game.js';
|
|
import { inside } from '../src/engine/geometry.js';
|
|
import { existsSync } from 'node:fs';
|
|
import { settle } from './helpers.mjs';
|
|
|
|
const EFFECTS = ['say', 'walk', 'face', 'give', 'drop', 'set', 'unset', 'if', 'dialogue', 'node', 'endDialogue', 'go', 'end'];
|
|
|
|
// Visita ogni elenco di effetti della storia, dentro scene, oggetti, dialoghi e rami `if`.
|
|
function* effectLists() {
|
|
for (const [id, scene] of Object.entries(story.scenes)) {
|
|
yield [`${id}.enter`, scene.enter ?? []];
|
|
for (const h of scene.hotspots ?? [])
|
|
for (const table of ['actions', 'with', 'give'])
|
|
for (const [k, list] of Object.entries(h[table] ?? {})) yield [`${id}.${h.id}.${table}.${k}`, list];
|
|
}
|
|
for (const [id, item] of Object.entries(story.items))
|
|
for (const table of ['actions', 'with'])
|
|
for (const [k, list] of Object.entries(item[table] ?? {})) yield [`${id}.${table}.${k}`, list];
|
|
for (const [id, d] of Object.entries(story.dialogues))
|
|
for (const [n, node] of Object.entries(d.nodes))
|
|
for (const o of node.options) yield [`${id}.${n}.${o.text}`, o.then ?? []];
|
|
}
|
|
function* flatten(list) {
|
|
for (const e of list) {
|
|
yield e;
|
|
if (e.if !== undefined) { yield* flatten(e.then ?? []); yield* flatten(e.else ?? []); }
|
|
}
|
|
}
|
|
|
|
test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esistono', () => {
|
|
assert.ok(story.scenes[story.start.scene] && inside(story.scenes[story.start.scene].walk, story.start.at));
|
|
for (const [id, scene] of Object.entries(story.scenes)) {
|
|
assert.ok(inside(scene.walk, scene.entry), `${id}: ingresso fuori dall'area`);
|
|
if (scene.background) assert.ok(existsSync(new URL('../' + scene.background, import.meta.url)), `${id}: manca ${scene.background}`);
|
|
const ids = new Set();
|
|
for (const h of scene.hotspots ?? []) {
|
|
assert.ok(!ids.has(h.id), `${id}.${h.id} doppio`);
|
|
ids.add(h.id);
|
|
assert.ok(!story.items[h.id], `${id}.${h.id}: stesso nome di un oggetto dell'inventario`);
|
|
assert.ok(h.name && h.box?.length === 4, `${id}.${h.id}: nome o riquadro mancante`);
|
|
if (h.at) assert.ok(inside(scene.walk, h.at), `${id}.${h.id}: punto d'arrivo fuori dall'area`);
|
|
if (h.default) assert.ok(VERBS.includes(h.default));
|
|
for (const k of Object.keys(h.actions ?? {})) assert.ok(VERBS.includes(k), `${id}.${h.id}: verbo ${k}`);
|
|
for (const k of [...Object.keys(h.with ?? {}), ...Object.keys(h.give ?? {})]) assert.ok(story.items[k], `${id}.${h.id}: oggetto ${k}`);
|
|
if (h.exit) {
|
|
const to = story.scenes[h.exit.scene];
|
|
assert.ok(to, `${id}.${h.id}: scena ${h.exit.scene}`);
|
|
assert.ok(inside(to.walk, h.exit.at ?? to.entry), `${id}.${h.id}: arrivo fuori dall'area`);
|
|
}
|
|
}
|
|
}
|
|
for (const [where, list] of effectLists())
|
|
for (const e of flatten(list)) {
|
|
const kind = EFFECTS.find(k => e[k] !== undefined);
|
|
assert.ok(kind, `${where}: effetto sconosciuto ${JSON.stringify(e)}`);
|
|
if (e.give || e.drop) assert.ok(story.items[e.give ?? e.drop], `${where}: oggetto ${e.give ?? e.drop}`);
|
|
if (e.dialogue) assert.ok(story.dialogues[e.dialogue], `${where}: dialogo ${e.dialogue}`);
|
|
if (e.go) assert.ok(story.scenes[e.go], `${where}: scena ${e.go}`);
|
|
if (e.who) assert.ok(story.characters[e.who], `${where}: personaggio ${e.who}`);
|
|
}
|
|
for (const [id, d] of Object.entries(story.dialogues)) {
|
|
assert.ok(d.nodes.start, `${id}: manca il nodo start`);
|
|
for (const node of Object.values(d.nodes)) {
|
|
assert.ok(node.options.some(o => o.next === 'end' && !o.when && !o.once), `${id}: ogni nodo deve avere un'uscita sempre disponibile`);
|
|
for (const o of node.options) {
|
|
if (o.once) assert.ok(o.id, `${id}: opzione una tantum senza id`);
|
|
if (o.next && o.next !== 'end') assert.ok(d.nodes[o.next], `${id}: nodo ${o.next}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('la soluzione si percorre tutta dall\'inizio alla fine', () => {
|
|
const saves = [];
|
|
const g = new Game(story, { onSave: s => saves.push(s) });
|
|
g.go(story.start.scene, story.start.at);
|
|
settle(g);
|
|
for (const [verb, target, item] of solution) {
|
|
if (verb === 'choose') {
|
|
const i = g.options().findIndex(o => o.text === target);
|
|
assert.ok(i >= 0, `opzione non disponibile: ${target}`);
|
|
g.choose(i);
|
|
} else {
|
|
if (!story.items[target]) assert.ok(g.hotspot(target), `non c'è ${target} in ${g.state.scene}`);
|
|
if (item) assert.ok(g.state.inventory.includes(item), `manca ${item}`);
|
|
assert.ok(g.act(verb, target, item), `comando rifiutato: ${verb} ${target}`);
|
|
}
|
|
settle(g);
|
|
}
|
|
assert.ok(g.state.ended, 'la partita non è finita');
|
|
assert.ok(g.state.flags.indizio_regolo);
|
|
assert.ok(saves.at(-1).ended, 'la fine non è salvata');
|
|
});
|