Suoni d'ambiente sintetizzati (vento, acqua, uccelli, grilli, campane) col loro volume in Pausa; la fessura in primo piano: si cerca col fascio della torcia il quadro e gli occhi (lavoro di Codex). La foto del palio leggibile da nginx, e un test che lo controlla per tutti i file serviti

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 18:17:01 +01:00
co-authored by Claude Opus 5.5
parent 1fabb6e543
commit de303c5825
22 changed files with 732 additions and 61 deletions
+24 -5
View File
@@ -6,12 +6,16 @@ import { solution } from '../src/story/solution.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
import { inside } from '../src/engine/geometry.js';
import { existsSync, readFileSync } from 'node:fs';
import { settle } from './helpers.mjs';
import { settle, exploreTorch } from './helpers.mjs';
const EFFECTS = ['say', 'walk', 'face', 'give', 'drop', 'set', 'unset', 'if', 'dialogue', 'node', 'endDialogue', 'go', 'end', 'closeup'];
// Visita ogni elenco di effetti della storia, dentro scene, oggetti, dialoghi e rami `if`.
function* effectLists() {
for (const [id, c] of Object.entries(story.closeups ?? {})) {
for (const k of ['comment', 'closing', 'complete']) yield [`${id}.${k}`, c[k] ?? []];
for (const t of c.targets ?? []) yield [`${id}.target`, t.lines ?? []];
}
for (const [id, scene] of Object.entries(story.scenes)) {
yield [`${id}.enter`, scene.enter ?? []];
for (const h of scene.hotspots ?? [])
@@ -66,7 +70,7 @@ test('la storia è coerente: scene, oggetti, dialoghi e punti calpestabili esist
if (e.closeup) {
const c = story.closeups?.[e.closeup];
assert.ok(c, `${where}: primo piano ${e.closeup}`);
for (const src of [c.closed, c.open]) assert.ok(existsSync(new URL('../' + src, import.meta.url)), `${where}: manca ${src}`);
for (const src of c.type === 'torch' ? [c.picture] : [c.closed, c.open]) assert.ok(existsSync(new URL('../' + src, import.meta.url)), `${where}: manca ${src}`);
}
if (e.who) assert.ok(story.characters[e.who], `${where}: personaggio ${e.who}`);
}
@@ -118,7 +122,7 @@ test('il taccuino: lungo la soluzione le tappe si spuntano in ordine, una alla v
let last = 0;
for (const [verb, target, item] of solution) {
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
settle(g);
settle(g, { explore: true });
const n = g.notebook().done.length;
assert.ok(n === last || n === last + 1, `tappe saltate dopo ${verb} ${target}`);
last = n;
@@ -134,7 +138,7 @@ test('le fasi del giorno seguono la storia: pomeriggio, poi tramonto, poi sera,
const ids = story.phases.map(p => p.id), seen = [g.phase().id];
for (const [verb, target, item] of solution) {
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
settle(g);
settle(g, { explore: true });
if (seen.at(-1) !== g.phase().id) seen.push(g.phase().id);
}
assert.deepEqual(seen, ids);
@@ -166,7 +170,7 @@ test('la soluzione si percorre tutta dall\'inizio alla fine', () => {
if (item) assert.ok(g.state.inventory.includes(item), `manca ${item}`);
assert.ok(g.act(verb, target, item), `comando rifiutato: ${verb} ${target}`);
}
settle(g);
settle(g, { explore: true });
}
assert.ok(g.state.ended, 'la partita non è finita');
assert.ok(g.state.flags.indizio_regolo && g.state.flags.palio_trovato, 'il palio visto nella fessura');
@@ -367,6 +371,7 @@ function stepChecked(g, label, step) {
for (let i = 0; i < 20000; i++) {
seen.push(essence(g.savable()));
if (g.speech) g.skip();
exploreTorch(g);
if (!g.busy && !g.walking && !g.queue.length && !g.task) break;
g.update(0.05);
}
@@ -584,3 +589,17 @@ test('il pettegolezzo cambia con le scoperte: vecchietti, Arcu e Tosca', () => {
assert.ok(texts().includes('Ho la torcia. Stasera entro nella fessura.'));
leave();
});
test('i file che il Docker copia sono leggibili da tutti (nginx gira con un altro utente)', async () => {
const { readdirSync, statSync } = await import('node:fs');
const root = new URL('../', import.meta.url);
const dirs = readFileSync(new URL('Dockerfile', root), 'utf8').match(/^COPY (assets\/\S+|src)\s/gm).map(l => l.split(' ')[1]);
const closed = [];
const walk = dir => { for (const e of readdirSync(new URL(dir + '/', root), { withFileTypes: true })) {
const path = `${dir}/${e.name}`;
if (e.isDirectory()) walk(path);
else if (!(statSync(new URL(path, root)).mode & 0o004)) closed.push(path);
} };
dirs.forEach(walk);
assert.deepEqual(closed, [], 'file non leggibili da nginx (chmod 644)');
});