Revisione di Codex: salvataggi a punti fermi, salva e carica da file, volume delle voci
- si salva solo a sequenza finita: uscire a metà di una risposta o della casetta riporta a prima; la squama presa resta salvata anche senza cambiare scena; test di interruzione su soluzione e catena del ghiacciolo - «Salva su file» nella pausa e «Carica» nel titolo, con controllo di versione e contenuto prima di sostituire il salvataggio del browser - cursore del volume delle voci («bla»), 50% di partenza; il muto zittisce subito la voce, la pausa la riprende - col dito un oggetto mancato di poco si prende, e i piccoli vincono sui grandi che li contengono (la squama); verbi su due colonne nel telefono in orizzontale - sfondi caricati per vicinanza alla scena di partenza, con tentativi limitati se falliscono; a gioco fermo non si ridisegna - modalità ?debug per scrivere le scene: area calpestabile, hotspot, punti, coordinate copiabili - corretto: i crediti dal titolo restavano dietro il titolo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
+100
-1
@@ -1,8 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { backgroundOrder } from '../src/engine/render.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { solution } from '../src/story/solution.js';
|
||||
import { Game, VERBS } from '../src/engine/game.js';
|
||||
import { Game, VERBS, checkSave } from '../src/engine/game.js';
|
||||
import { inside } from '../src/engine/geometry.js';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { settle } from './helpers.mjs';
|
||||
@@ -264,3 +265,101 @@ test('la catena del ghiacciolo: Tosca, cedrata, stecco, gomma, griglia, chiavett
|
||||
g.act('open', 'casetta'); say();
|
||||
assert.ok(g.state.flags.casetta_aperta && g.state.inventory.includes('torcia') && !g.state.inventory.includes('chiavetta'));
|
||||
});
|
||||
|
||||
// Uscire dal gioco in un istante qualsiasi di un passo deve riportare a prima del passo o a dopo, mai a metà:
|
||||
// si fa il passo fotogramma per fotogramma e a ogni fotogramma si guarda cosa verrebbe salvato.
|
||||
const essence = s => s && JSON.stringify({ ...s, x: 0, y: 0, facing: 0 });
|
||||
function stepChecked(g, label, step) {
|
||||
const before = essence(g.checkpoint), seen = [];
|
||||
step();
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
seen.push(essence(g.savable()));
|
||||
if (g.speech) g.skip();
|
||||
if (!g.busy && !g.walking && !g.queue.length && !g.task) break;
|
||||
g.update(0.05);
|
||||
}
|
||||
g.update(0);
|
||||
const after = essence(g.savable());
|
||||
for (const s of seen) assert.ok(s === before || s === after, `${label}: interrompendo a metà si salverebbe uno stato a metà`);
|
||||
assert.equal(essence(g.checkpoint), after, `${label}: a passo finito il punto fermo è aggiornato`);
|
||||
}
|
||||
|
||||
test('interrompere la soluzione in qualsiasi istante non lascia enigmi risolti a metà', () => {
|
||||
const saves = [];
|
||||
const g = new Game(story, { onSave: s => saves.push(s) });
|
||||
stepChecked(g, 'inizio', () => g.go(story.start.scene, story.start.at));
|
||||
for (const [verb, target, item] of solution)
|
||||
stepChecked(g, `${verb} ${target}`, () => verb === 'choose' ? g.choose(g.options().findIndex(o => o.text === target)) : g.act(verb, target, item));
|
||||
assert.ok(saves.at(-1).ended);
|
||||
});
|
||||
|
||||
test('la squama presa resta salvata anche senza cambiare scena', () => {
|
||||
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.slice(0, 5)) {
|
||||
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
|
||||
settle(g);
|
||||
}
|
||||
assert.ok(g.state.inventory.includes('squama'));
|
||||
assert.ok(saves.at(-1).inventory.includes('squama'), 'il salvataggio ha la squama');
|
||||
const again = new Game(story, { state: JSON.parse(JSON.stringify(saves.at(-1))) });
|
||||
assert.ok(again.state.inventory.includes('squama'));
|
||||
});
|
||||
|
||||
test('interrompere la catena del ghiacciolo non perde la torcia né la chiavetta', () => {
|
||||
const g = new Game(story);
|
||||
const steps = [
|
||||
['go tennis', () => g.go('tennis_chiesa')], ['parla con Tosca', () => g.act('talk', 'chiosco')],
|
||||
...['Una Coca.', 'Un ghiacciolo al limone.', 'Una cedrata, da portare via.', 'Niente, grazie Tosca.']
|
||||
.map(t => [t, () => g.choose(g.options().findIndex(o => o.text === t))]),
|
||||
['bevi la Coca', () => g.act('use', 'coca')], ['cedrata e ghiacciolo', () => g.act('use', 'cedrata', 'ghiacciolo')],
|
||||
['go oratorio', () => g.go('oratorio')], ['prendi la gomma', () => g.act('take', 'gomma_terra')],
|
||||
['gomma e stecco', () => g.act('use', 'gomma', 'stecco')], ['go ponticino', () => g.go('ponticino')],
|
||||
['griglia', () => g.act('use', 'griglia', 'stecco_gomma')], ['go area feste', () => g.go('area_feste')],
|
||||
['apri la casetta', () => g.act('open', 'casetta')],
|
||||
];
|
||||
for (const [label, step] of steps) stepChecked(g, label, step);
|
||||
assert.ok(g.checkpoint.inventory.includes('torcia') && g.checkpoint.flags.casetta_aperta);
|
||||
});
|
||||
|
||||
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
||||
const order = backgroundOrder(story, story.start.scene);
|
||||
const all = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
|
||||
assert.equal(order.length, all.size);
|
||||
assert.deepEqual(new Set(order), all);
|
||||
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
||||
const near = story.scenes[story.start.scene].hotspots.filter(h => h.exit).map(h => story.scenes[h.exit.scene].background);
|
||||
for (const bg of near) assert.ok(order.indexOf(bg) <= near.length, `${bg} è vicina e deve venire presto`);
|
||||
});
|
||||
|
||||
test('col dito la squama si prende anche toccando la panchina tutt\'intorno; col mouse vale il punto preciso', () => {
|
||||
const g = new Game(story);
|
||||
g.go('piazza'); settle(g);
|
||||
g.state.flags.luccichio = true;
|
||||
const sq = g.hotspot('cosa_luccicante');
|
||||
assert.ok(sq, 'la squama è in scena');
|
||||
const [x, y, w, h] = sq.box, cx = x + w / 2, cy = y + h / 2, reach = 14;
|
||||
for (const p of [[x - 8, cy], [x + w + 8, cy], [cx, y - 8], [cx, y + h + 3]])
|
||||
assert.equal(g.hotspotAt(p, reach)?.id, 'cosa_luccicante', `col dito in ${p}`);
|
||||
assert.notEqual(g.hotspotAt([x - 8, cy])?.id, 'cosa_luccicante', 'col mouse fuori dalla squama non la prende');
|
||||
assert.equal(g.hotspotAt([cx, cy])?.id, 'cosa_luccicante');
|
||||
});
|
||||
|
||||
test('un salvataggio da file si accetta solo se torna in tutto con la storia', () => {
|
||||
const g = new Game(story);
|
||||
g.go(story.start.scene, story.start.at); settle(g);
|
||||
const good = JSON.parse(JSON.stringify(g.snapshot()));
|
||||
assert.equal(checkSave(story, good), null);
|
||||
const bad = change => checkSave(story, { ...good, ...change });
|
||||
assert.match(bad({ scene: 'luna' }), /scena sconosciuta/);
|
||||
assert.match(bad({ scene: 'toString' }), /scena sconosciuta/);
|
||||
assert.ok(bad({ x: 'qui' }));
|
||||
assert.ok(bad({ facing: 0 }));
|
||||
assert.ok(bad({ inventory: ['astronave'] }), 'oggetto che non esiste');
|
||||
assert.ok(bad({ inventory: 'squama' }));
|
||||
assert.ok(bad({ flags: { a: 1 } }));
|
||||
assert.ok(bad({ used: null }));
|
||||
assert.ok(bad({ ended: 'no' }));
|
||||
assert.ok(checkSave(story, null));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user