Le bocce al pallaio contro Mariano: si tiene premuto per la forza, tre bocce a testa, vince il più vicino al pallino; se vinci, «Hai vinto, ti ci porto!» a Brento Sanico (sfondo e Mariano di Codex dalle foto di Andrea)

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:59:09 +01:00
co-authored by Claude Opus 5.5
parent 7f0584f0fc
commit c4bac08c02
29 changed files with 528 additions and 13 deletions
+172
View File
@@ -0,0 +1,172 @@
// Le bocce al pallaio (primo piano `type: 'bocce'`): una mano da tre bocce a testa contro il campione.
// Tieni premuto e la forza va su e giù; lasci e la boccia parte. Vince la mano chi ha la boccia più vicina al pallino.
// La pista è un piano: `u` lungo la pista (0 dove si tira, 100 la sponda di fondo), `w` di traverso (-10…10).
// Niente fisica vera: attrito costante, urti semplici (chi colpisce cede quasi tutta la spinta), sponde che respingono.
export const LANE = 100, HALF = 10, FRICTION = 40, BALL = 1.25, JACK = 0.6, BALLS = 3;
export const SWING = 1.6; // secondi per un giro della forza (0 → 1 → 0)
const REST = 0.6; // pausa a bocce ferme prima del tiro seguente
// Numeri «a caso» ripetibili: lo stato del generatore sta nel primo piano, così le prove sono sempre uguali.
function random(c) {
c.seed = (c.seed * 1664525 + 1013904223) % 4294967296;
return c.seed / 4294967296;
}
const gauss = c => (random(c) + random(c) + random(c) - 1.5) * 1.15;
export function newBocce(seed = Date.now() % 4294967296) {
const c = { step: 'aim', t: 0, age: 0, seed, balls: [], turn: 0, power: 0, holding: false, holdTime: 0 };
c.jack = { u: 38 + random(c) * 22, w: (random(c) - 0.5) * 5, vu: 0, vw: 0, r: JACK, jack: true };
return c;
}
// Di chi è il tiro: Andrea ai tiri pari, il campione ai dispari.
export const thrower = c => (c.turn % 2 ? 'cpu' : 'player');
export const left = (c, who) => BALLS - c.balls.filter(b => b.who === who).length;
const moving = c => [c.jack, ...c.balls].some(b => Math.hypot(b.vu, b.vw) > 0.01);
const dist = (a, b) => Math.hypot(a.u - b.u, a.w - b.w);
// Una boccia lanciata verso un punto: parte da dove si tira con la velocità che basta ad arrivarci.
function launch(c, who, u, w) {
const d = Math.hypot(u, w), v = Math.sqrt(2 * FRICTION * d);
c.balls.push({ who, u: 0, w: 0, vu: v * u / d, vw: v * w / d, r: BALL });
c.step = 'rolling';
c.t = 0;
}
// Andrea: la forza decide quanto lontano; la direzione punta al pallino, con un po' di mano che trema.
export function press(c) {
if (c.step !== 'aim' || thrower(c) !== 'player' || c.holding) return false;
c.holding = true;
c.holdTime = 0;
c.power = 0;
return true;
}
export function release(c) {
if (!c.holding) return false;
c.holding = false;
launch(c, 'player', 8 + c.power * (LANE - 4), c.jack.w + gauss(c) * 0.9);
return true;
}
export const powerAt = holdTime => (1 - Math.cos((2 * Math.PI * holdTime) / SWING)) / 2;
function step(c, dt) {
const all = [c.jack, ...c.balls];
for (const b of all) {
const v = Math.hypot(b.vu, b.vw);
if (v <= 0.01) { b.vu = b.vw = 0; continue; }
const slow = Math.max(0, v - FRICTION * dt) / v;
b.vu *= slow; b.vw *= slow;
b.u += b.vu * dt; b.w += b.vw * dt;
// Le sponde: di lato e in fondo respingono, piano.
if (Math.abs(b.w) > HALF - b.r) { b.w = Math.sign(b.w) * (HALF - b.r); b.vw *= -0.4; }
if (b.u > LANE - b.r) { b.u = LANE - b.r; b.vu *= -0.3; }
}
// Gli urti: chi va più forte spinge l'altro lungo la congiungente e si tiene poco.
for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) {
const a = all[i], b = all[j], d = dist(a, b), min = a.r + b.r;
if (d >= min || d === 0) continue;
const [hit, other] = Math.hypot(a.vu, a.vw) >= Math.hypot(b.vu, b.vw) ? [a, b] : [b, a];
const nu = (other.u - hit.u) / d, nw = (other.w - hit.w) / d, push = hit.vu * nu + hit.vw * nw;
if (push <= 0) continue;
const k = other.jack ? 1.1 : 0.85;
other.vu += nu * push * k; other.vw += nw * push * k;
hit.vu -= nu * push * 0.85; hit.vw -= nw * push * 0.85;
// Si staccano subito, così non restano incollate.
const back = (min - d) / 2;
hit.u -= nu * back; hit.w -= nw * back; other.u += nu * back; other.w += nw * back;
}
}
// Il punto: chi ha la boccia più vicina al pallino vince, e segna una boccia per ognuna più vicina della migliore
// dell'altro.
export function score(c) {
const by = who => c.balls.filter(b => b.who === who).map(b => dist(b, c.jack)).sort((a, b) => a - b);
const p = by('player'), q = by('cpu');
if (!p.length || !q.length) return { winner: null, points: 0 };
const winner = p[0] <= q[0] ? 'player' : 'cpu', [mine, theirs] = winner === 'player' ? [p, q] : [q, p];
return { winner, points: mine.filter(d => d < theirs[0]).length };
}
// L'età del primo piano (`age`) la conta il motore; qui solo il tempo del passo in corso.
export function update(c, dt) {
c.t += dt;
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
if (c.step === 'rolling') {
for (let s = 0; s < 4; s++) step(c, dt / 4);
if (!moving(c) && c.t > 0.3) { c.step = 'settle'; c.t = 0; }
} else if (c.step === 'settle' && c.t >= REST) {
c.turn++;
c.t = 0;
c.step = c.turn >= BALLS * 2 ? 'result' : 'aim';
if (c.step === 'result') c.result = score(c);
} else if (c.step === 'aim' && thrower(c) === 'cpu' && c.t >= 1.1) {
// Il campione mira al pallino e sbaglia di poco, a volte di più.
launch(c, 'cpu', Math.min(LANE - 2, Math.max(10, c.jack.u + gauss(c) * 7)), c.jack.w + gauss(c) * 1.4);
}
}
// --- Il disegno: lo sfondo della pista e, sopra, bocce e pallino in prospettiva (misurata sullo sfondo di Codex).
const NEAR = { y: 360, x: 355, half: 339 }, FAR = { y: 187, x: 298, half: 37.5 };
const S_FAR = FAR.half / NEAR.half, Q = (1 / S_FAR - 1) / LANE, HORIZON = (FAR.y - NEAR.y * S_FAR) / (1 - S_FAR);
export function project(u, w) {
const s = 1 / (1 + Q * u), k = (s - S_FAR) / (1 - S_FAR);
const cx = FAR.x + (NEAR.x - FAR.x) * k;
return { x: cx + (w / HALF) * NEAR.half * s, y: HORIZON + (NEAR.y - HORIZON) * s, s };
}
const COLORS = { player: ['#1d3f8f', '#3f6fd1', '#a9c4ff'], cpu: ['#7a1f1a', '#c2392d', '#ffb0a3'], jack: ['#a8862a', '#f2d75c', '#fff6c4'] };
// Il disegno ha la sua misura (le bocce vere su una pista di 4 metri), più piccola di quella degli urti; lontano si
// ingrandisce un po' (fino a 2,6 volte in fondo), sennò in prospettiva esatta resterebbero puntini.
const LOOK = { player: 0.42, cpu: 0.42, jack: 0.2 };
function ball(ctx, b, kind) {
const { x, y, s } = project(b.u, b.w), r = Math.max(2, LOOK[kind] * (NEAR.half / HALF) * s * (1 + 1.6 * (1 - s)));
const [dark, mid, light] = COLORS[kind];
ctx.fillStyle = 'rgba(0,0,0,0.28)';
ctx.beginPath(); ctx.ellipse(x + r * 0.25, y + r * 0.15, r, r * 0.45, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = dark; ctx.beginPath(); ctx.arc(x, y - r, r, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = mid; ctx.beginPath(); ctx.arc(x - r * 0.15, y - r * 1.12, r * 0.72, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = light; ctx.fillRect(Math.round(x - r * 0.45), Math.round(y - r * 1.55), Math.max(1, Math.round(r * 0.35)), Math.max(1, Math.round(r * 0.3)));
}
const FONT = '"Press Start 2P", monospace';
function label(ctx, text, x, y, size = 7, color = '#ffd27a', align = 'center') {
ctx.font = `${size}px ${FONT}`;
ctx.textAlign = align;
ctx.textBaseline = 'middle';
const w = ctx.measureText(text).width, x0 = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
ctx.fillStyle = 'rgba(0,0,0,0.62)';
ctx.fillRect(Math.round(x0 - 6), Math.round(y - size), Math.round(w + 12), size * 2);
ctx.fillStyle = color;
ctx.fillText(text, Math.round(x), Math.round(y));
}
export function drawBocce(ctx, c, def, bg, time) {
ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#c9b48a'; ctx.fillRect(0, 0, 640, 360); }
const pieces = [{ b: c.jack, kind: 'jack' }, ...c.balls.map(b => ({ b, kind: b.who }))].sort((a, b) => b.b.u - a.b.u);
for (const p of pieces) ball(ctx, p.b, p.kind);
// Il punteggio in basso a sinistra (in alto ci sono Pausa e il volume): le bocce che restano a ciascuno.
const dots = (who, y) => label(ctx, `${def.names[who]} ${'o'.repeat(left(c, who))}`, 16, y, 7, who === 'player' ? '#a9c4ff' : '#ffb0a3', 'left');
dots('player', 326);
dots('cpu', 344);
if (c.step === 'aim' && thrower(c) === 'player') {
// La forza: una barra che sale e scende finché si tiene premuto.
const x = 586, y = 320, h = 120;
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(x - 3, y - h - 3, 20, h + 6);
ctx.fillStyle = '#3a2d42'; ctx.fillRect(x, y - h, 14, h);
const p = c.holding ? c.power : 0, grad = ['#7fe08a', '#ffd27a', '#ff7a5a'];
ctx.fillStyle = grad[Math.min(2, Math.floor(p * 3))]; ctx.fillRect(x, Math.round(y - h * p), 14, Math.round(h * p));
label(ctx, 'FORZA', x + 7, y + 14, 6);
if (!c.holding && c.age > 0.4) {
ctx.globalAlpha = 0.65 + 0.35 * Math.sin(time / 300);
label(ctx, 'Tieni premuto per caricare, lascia per tirare', 320, 62);
ctx.globalAlpha = 1;
}
} else if (c.step === 'aim') label(ctx, `Tira ${def.names.cpu}...`, 320, 62);
if (c.step === 'result' && c.result) {
const win = c.result.winner === 'player';
label(ctx, win ? `Punto per ${def.names.player}!` : `Punto per ${def.names.cpu}!`, 320, 150, 12, win ? '#7fe08a' : '#ffb0a3');
}
ctx.restore();
}
+23 -4
View File
@@ -1,6 +1,7 @@
// Il cuore dell'avventura, senza DOM: stato, comandi, coda delle azioni, dialoghi.
// La storia (scene, oggetti, dialoghi) arriva tutta dai dati: qui non c'è nessun enigma scritto a mano.
import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js';
import { newBocce, update as bocceUpdate, press as boccePress, release as bocceRelease } from './bocce.js';
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
export const VERB_LABELS = { walk: 'Vai a', look: 'Guarda', take: 'Prendi', use: 'Usa', talk: 'Parla con', open: 'Apri', give: 'Dai', drop: 'Butta' };
@@ -229,7 +230,7 @@ export class Game {
closeupTap() {
const c = this.closeup;
if (!c || this.speech) return false;
if (this.story.closeups[c.id].type === 'torch') return false;
if (this.story.closeups[c.id].type) return false; // torcia e bocce non si sfogliano
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
else return false;
@@ -247,11 +248,20 @@ export class Game {
if (c.step === 'done') this.queue.unshift(...def.complete);
return true;
}
// Le bocce: a mano finita, la battuta di chi ha vinto; uscendo prima, quella di chi lascia.
if (def?.type === 'bocce') {
this.queue.unshift(...(c.step === 'result' ? def[c.result?.winner === 'player' ? 'win' : 'lose'] : def.quit ?? []));
return true;
}
// Chi è arrivato all'ultima pagina, chiudendo, la commenta (`closing`).
if (c.step === 'end') this.queue.unshift(...(this.story.closeups?.[c.id]?.closing ?? []));
return true;
}
// Le bocce: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
closeupPress() { const c = this.closeup; return !!c && this.story.closeups[c.id].type === 'bocce' && !this.speech && boccePress(c); }
closeupRelease() { const c = this.closeup; return !!c && this.story.closeups[c.id].type === 'bocce' && bocceRelease(c); }
// Solo luce, nessun clic sugli oggetti dietro al primo piano. Il progresso è transitorio:
// uscire o ricaricare durante la ricerca permette di ripeterla, senza anticipare il finale.
closeupAim([x, y]) {
@@ -378,6 +388,12 @@ export class Game {
const c = this.closeup;
if (c) {
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
if (this.story.closeups[c.id].type === 'bocce') {
bocceUpdate(c, dt);
// Il risultato resta a schermo un momento, poi si torna al pallaio per i commenti.
if (c.step === 'result' && c.t >= 2.4) this.closeupClose();
return;
}
c.t += dt;
if (c.step === 'turn' && c.t >= PAGE_TURN) Object.assign(c, { step: 'end', t: 0 });
if (c.step === 'end' && !c.lines && c.t >= CLOSEUP_PAUSE) c.lines = [...(this.story.closeups?.[c.id]?.comment ?? [])];
@@ -409,9 +425,12 @@ export class Game {
else if (e.node) this.dialogue.node = e.node;
else if (e.endDialogue) this.dialogue = null;
else if (e.end) s.ended = true;
else if (e.closeup) this.closeup = this.story.closeups[e.closeup].type === 'torch'
? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
: { id: e.closeup, step: 'cover', t: 0, age: 0 };
else if (e.closeup) {
const type = this.story.closeups[e.closeup].type;
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
: type === 'bocce' ? { id: e.closeup, ...newBocce(Math.floor(Math.random() * 4294967296)) }
: { id: e.closeup, step: 'cover', t: 0, age: 0 };
}
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
}
+6
View File
@@ -3,6 +3,7 @@
import { check, PAGE_TURN } from './game.js';
import { ambientEffects, ambientHash } from './ambient.js';
import { drawTorch } from './torch.js';
import { drawBocce } from './bocce.js';
import { versioned } from '../version.js';
export const WIDTH = 640, HEIGHT = 360;
@@ -669,6 +670,11 @@ function drawCloseup(ctx, game, time) {
drawTorch(ctx, c, def, backgrounds.get(def.picture));
return;
}
if (def.type === 'bocce') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return;
}
ctx.save();
ctx.fillStyle = `rgba(8, 5, 12, ${Math.min(0.78, c.age / 0.35 * 0.78)})`;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
+8 -2
View File
@@ -397,6 +397,8 @@ canvas.addEventListener('pointerdown', e => {
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
if (game.closeup) {
press = { closeup: true };
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
if (!game.skip()) game.closeupPress();
game.closeupAim(scenePoint(e));
canvas.setPointerCapture(e.pointerId);
return;
@@ -406,7 +408,7 @@ canvas.addEventListener('pointerdown', e => {
});
canvas.addEventListener('pointerup', e => {
if (!press) return;
if (press.closeup) { press = null; if (running && !game.skip()) game.closeupTap(); return; }
if (press.closeup) { press = null; if (running && !game.closeupRelease() && !game.skip()) game.closeupTap(); return; }
clearTimeout(press.timer);
// Il secondo clic vicino al primo, entro poco, è un doppio clic: stesso comando, al doppio della velocità.
if (!press.long) {
@@ -529,6 +531,7 @@ window.addEventListener('keydown', e => {
return;
}
if (e.key === 'Escape') game.closeupClose();
else if ([' ', 'Enter'].includes(e.key) && story.closeups[game.closeup.id].type === 'bocce') { e.preventDefault(); if (!e.repeat && !game.skip()) game.closeupPress(); }
else if ([' ', 'Enter', 'ArrowRight'].includes(e.key)) { e.preventDefault(); if (!game.skip()) game.closeupTap(); }
else if (e.key === 'm' || e.key === 'M') toggleMute();
return;
@@ -546,6 +549,9 @@ window.addEventListener('keydown', e => {
if (game.dialogue && n >= 1 && n <= 9) game.choose(n - 1);
});
// Alle bocce, lasciando spazio o Invio parte il tiro.
window.addEventListener('keyup', e => { if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease(); });
// --- Il primo piano: si chiude col pulsante; la pagina che gira fruscia (rumore filtrato, niente file audio).
$('closeup-close').addEventListener('click', () => game?.closeupClose());
let turning = null, reveal = null;
@@ -590,7 +596,7 @@ function updateUi() {
$('spots-button').hidden = !running || !!game.closeup;
$('notebook-button').hidden = !running || !story.notebook || !!game.closeup;
$('closeup-close').hidden = !running || !game.closeup;
$('closeup-close').textContent = torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : 'Chiudi';
$('closeup-close').textContent = torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : c && story.closeups[c.id].type === 'bocce' ? 'Lascia stare' : 'Chiudi';
$('panel').inert = !!c;
canvas.classList.toggle('torch', !!torch);
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : "La scena dell'avventura");
+48 -2
View File
@@ -107,6 +107,22 @@ export const story = {
// Primi piani interattivi: libro dell'oratorio e ricerca con la torcia nella fessura.
// Le immagini (Codex, assets/closeups/) hanno l'etichetta e le pagine vuote: titolo e «FINE» li scrive il codice.
closeups: {
// Le bocce al pallaio contro Mariano, il campione (idea di Andrea, 25 ottobre): solo per gloria.
bocce: {
type: 'bocce',
picture: 'assets/closeups/pallaio.png',
names: { player: 'Andrea', cpu: 'Mariano' },
win: [
{ say: 'Aspetta, ci vuole il metro...', who: 'mariano', pose: 'measure' },
{ say: 'Hai vinto, ti ci porto!', who: 'mariano', pose: 'laugh' },
{ set: 'bocce_vinte' },
],
lose: [
{ say: 'Bocciato! Al pallaio comando io.', who: 'mariano', pose: 'cheer' },
{ say: 'Quando vuoi la rivincita, sono qui.', who: 'mariano' },
],
quit: [{ say: 'Già stanco? Il pallaio ti aspetta.', who: 'mariano', pose: 'laugh' }],
},
fessura: {
type: 'torch',
picture: 'assets/closeups/palio-2007.jpg',
@@ -155,6 +171,7 @@ export const story = {
arcu: { name: 'L\'Arcu', color: '#7fb2ff', sprite: 'assets/characters/arcu.json' },
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
},
items: {
squama: {
@@ -1424,7 +1441,9 @@ export const story = {
name: 'Il tennis della chiesa',
map: [66, 51],
background: 'assets/scenes/tennis-chiesa.png',
speakers: { tosca: [292, 196] },
speakers: { tosca: [292, 196], mariano: [500, 234] },
// Mariano, il campione del pallaio (sprite di Codex dalla foto di Andrea, 25 ottobre).
people: [{ who: 'mariano', at: [500, 290], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.4 }],
walk: [[0, 360], [0, 300], [200, 290], [420, 268], [640, 258], [640, 360]],
depth: [[258, 0.7], [360, 1.15]],
entry: [320, 350],
@@ -1433,7 +1452,7 @@ export const story = {
id: 'pallaio', name: 'il pallaio', box: [300, 248, 340, 40], at: [400, 300], face: 'up',
actions: {
look: [{ say: 'Il pallaio, dove si gioca a bocce. Qui un punto si discute più di un rigore.' }],
use: [{ say: 'Senza bocce, e senza un vecchietto che misura col metro, non è una partita.' }],
use: [{ say: 'Una partita? Chiedo a Mariano, il campione.' }, { dialogue: 'mariano' }],
},
},
{
@@ -1464,6 +1483,13 @@ export const story = {
{
id: 'al_cancello', name: 'dal cancello verde, verso il posticino della Carza', box: [470, 140, 120, 130], at: [515, 280], exit: { scene: 'carza', at: [440, 290], face: 'left' },
},
{
id: 'mariano', name: 'Mariano', box: [482, 238, 36, 52], at: [455, 300], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Mariano, il campione del pallaio. Gli occhiali in testa, il sorriso di chi ha già vinto.' }],
talk: [{ dialogue: 'mariano' }],
},
},
],
},
// Fra la Fortezza e i cipressi: la stradina fra le vigne, la casa del maestro Bacci, e lassù il Trebbio.
@@ -2095,6 +2121,26 @@ export const story = {
},
},
},
// Mariano al pallaio: una mano a bocce, solo per gloria; se Andrea vince, la promessa di Brento Sanico.
mariano: {
nodes: {
start: {
options: [
{
text: 'Mariano! Facciamo una partita a bocce?', when: '!bocce_vinte', next: 'end',
then: [
{ say: 'Con me? Al pallaio sono il campione.', who: 'mariano', pose: 'explain' },
{ say: 'Facciamo così: se mi batti, ti porto a Brento Sanico.', who: 'mariano' },
{ closeup: 'bocce' },
],
},
{ text: 'La rivincita?', when: 'bocce_vinte', next: 'end', then: [{ say: 'Il campione non scappa. Tira!', who: 'mariano', pose: 'throw' }, { closeup: 'bocce' }] },
{ text: 'Allora, Brento Sanico?', when: 'bocce_vinte', then: [{ say: 'Promesso: ti ci porto. Ma prima ritrova il palio, che senza festa non si parte.', who: 'mariano', pose: 'explain' }] },
{ text: 'Ciao Mariano.', next: 'end' },
],
},
},
},
// Lo "psicologo": il castello ascolta e risponde con un indizio su quello che manca.
castello: {
nodes: {