diff --git a/src/engine/game.js b/src/engine/game.js index 7b361be..2a37505 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -38,6 +38,18 @@ export const miniature = src => src?.replace(/([^/]+)$/, 'mini/$1') ?? null; // Una riga dell'introduzione: un testo, oppure { rione: { id: testo } } con la riga del rione scelto. export const introText = (line, rione) => typeof line === 'string' ? line : line.rione[rione] ?? Object.values(line.rione)[0]; +// I crediti con i nomi di ogni voce in ordine casuale: nessuno viene sempre per primo. +export function shuffledCredits(credits, random = Math.random) { + return credits.map(({ role, names }) => { + const list = [...names]; + for (let i = list.length - 1; i > 0; i--) { + const j = Math.floor(random() * (i + 1)); + [list[i], list[j]] = [list[j], list[i]]; + } + return { role, names: list }; + }); +} + export const sayTime = text => Math.max(1.6, 0.9 + text.length * 0.055); export function newState(story) { diff --git a/src/main.js b/src/main.js index 155d33f..c0dfd7a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,6 @@ // Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti). import { story } from './story/story.js'; -import { Game, VERBS, VERB_LABELS, checkSave, introText } from './engine/game.js'; +import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits } from './engine/game.js'; import { Command } from './engine/command.js'; import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js'; import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js'; @@ -100,14 +100,18 @@ function nextIntro() { $('intro').addEventListener('click', e => { if (e.target !== $('skip-intro')) nextIntro(); }); $('skip-intro').addEventListener('click', () => { introLine = story.intro.length; nextIntro(); }); -// --- Crediti: costruiti da story.credits; si aprono dal titolo e dalla fine, Chiudi o Esc tornano indietro. -$('credits-list').replaceChildren(...(story.credits ?? []).flatMap(({ role, names }) => { - const dt = document.createElement('dt'), dd = document.createElement('dd'); - dt.textContent = role; - dd.replaceChildren(...names.map(n => Object.assign(document.createElement('span'), { textContent: n }))); - return [dt, dd]; -})); -for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', () => { $('credits').hidden = false; }); +// --- Crediti: costruiti da story.credits, con i nomi di ogni voce rimescolati a ogni apertura; si aprono dal titolo +// e dalla fine, Chiudi o Esc tornano indietro. +function openCredits() { + $('credits-list').replaceChildren(...shuffledCredits(story.credits ?? []).flatMap(({ role, names }) => { + const dt = document.createElement('dt'), dd = document.createElement('dd'); + dt.textContent = role; + dd.replaceChildren(...names.map(n => Object.assign(document.createElement('span'), { textContent: n }))); + return [dt, dd]; + })); + $('credits').hidden = false; +} +for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', openCredits); $('close-credits').addEventListener('click', () => { $('credits').hidden = true; }); // --- Mappa: i posti visti e le strade fra loro; un clic su un posto ci porta (game.travel). diff --git a/tests/story.test.mjs b/tests/story.test.mjs index b638f0a..a52dbb5 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -3,7 +3,7 @@ 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, checkSave, introText } from '../src/engine/game.js'; +import { Game, VERBS, checkSave, introText, shuffledCredits } from '../src/engine/game.js'; import { inside } from '../src/engine/geometry.js'; import { existsSync, readFileSync } from 'node:fs'; import { settle } from './helpers.mjs'; @@ -87,6 +87,20 @@ test('i crediti: ogni voce ha un ruolo e almeno un nome', () => { for (const name of ['Yuri Baldi', 'Francesco Bacci']) assert.ok(all.includes(name), `manca ${name}`); }); +test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', () => { + const idee = story.credits.find(c => c.names.length >= 3).role; + const orders = new Set(); + for (let seed = 1; seed <= 30; seed++) { + let x = seed; + const random = () => ((x = (x * 16807) % 2147483647) / 2147483647); + const out = shuffledCredits(story.credits, random); + assert.deepEqual(out.map(c => c.role), story.credits.map(c => c.role), 'le voci restano nel loro ordine'); + out.forEach((c, i) => assert.deepEqual([...c.names].sort(), [...story.credits[i].names].sort())); + orders.add(out.find(c => c.role === idee).names.join('|')); + } + assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`); +}); + test('la soluzione si percorre tutta dall\'inizio alla fine', () => { const saves = []; const g = new Game(story, { onSave: s => saves.push(s) });