Giochi di carte dopo la revisione di Codex sui tre insieme; player dei video «Come si gioca» e primo copione

Esito che vale e si salva appena deciso, per tutti i minigiochi (anche tornando al titolo dalla schermata del risultato); briscola: uscire durante il conteggio non perde più la vittoria; tressette: si esce ai 21 col primo evento che ci porta, accusa compresa (prima vinceva sempre Andrea se ci arrivavano tutte e due), il carico che fa vincere si dà anche se è il 2, accuse che non escono dal tavolo, convenzioni scritte nell'aiuto; scopa: primiera valutata per quel che migliora, finale esatto a mazzo finito, presa sulla tavola al rilascio con scelta e conferma, frecce e Invio sempre sulla presa scelta. Tastiera: Invio e Spazio giocano sempre la carta scelta. Tavola descritta a parole. Classi CSS comuni ai tre giochi.
Video: player nel gioco (pulsante «Guarda il video» nel «Come si gioca», compare quando il video esiste), video/narra.py per la voce del narratore, pezzi comuni dei copioni e il copione della briscola; scaletta in video/SCALETTA-CARTE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
2026-10-26 16:39:22 +01:00
co-authored by Claude Opus 5.5
parent 30e324d7e2
commit 7e1e6f6144
24 changed files with 594 additions and 73 deletions
+101
View File
@@ -0,0 +1,101 @@
// Pezzi comuni dei copioni «Come si gioca» (video/SCALETTA-CARTE.md): briscola, tressette, scopa, bocce, biliardo.
// Si girano come demo-prototipo.mjs (video/SCALETTA.md, «Come si rigira»); prima `python3 video/narra.py <copione>`
// per la voce del narratore, dopo `python3 video/monta.py <cartella> --tutorial`.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
export const URL_GIOCO = "http://127.0.0.1:8000/";
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export const c = (id, it) => ({ id, it, en: it, top: true });
// La durata di ogni frase del narratore (video/narra.py): la battuta resta finché la voce non ha finito.
export function narrazione(clip) {
const f = new URL(`./out/${clip}-narrazione.json`, import.meta.url);
return existsSync(f) ? Object.fromEntries(JSON.parse(readFileSync(f, "utf8")).voci.map((v) => [v.id, v.sec * 1000])) : {};
}
export async function setup(d) {
const p = d.page;
await p.goto(URL_GIOCO);
await p.evaluate(() => localStorage.clear());
await p.reload();
await p.waitForSelector("#title:not([hidden])");
await sleep(1000);
}
export function regista(d, clip) {
const p = d.page, voce = narrazione(clip);
let inizio = 0, dura = 0;
const r = {
// Una battuta del narratore: prima si lascia finire quella di prima.
async cue(id) {
const resto = inizio + dura + 350 - Date.now();
if (resto > 0) await sleep(resto);
await d.cue(id);
inizio = Date.now(); dura = voce[id] ?? 0;
},
async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); },
// Puntatore vero sulla scena (coordinate 640×360).
async punto(x, y) {
const b = await p.locator("#scene").boundingBox(), k = await p.evaluate(() => devicePixelRatio);
return [(b.x + (x / 640) * b.width) * k, (b.y + (y / 360) * b.height) * k];
},
async clicca([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur} click`); await sleep(300); },
async muovi([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur}`); await sleep(200); },
// Aspetta che l'omino abbia finito di camminare e di parlare; un dialogo o un primo piano aperti fermano l'attesa.
async fermi(pausa = 500) {
await sleep(150);
for (let i = 0; i < 1500; i++) {
const ok = await p.evaluate(() => {
if (game.speech || (game.queue.length && !game.closeup) || game.walking || game.reach > 0) return false;
return !!game.dialogue || !!game.closeup || (!game.busy && !game.task);
});
if (ok) break;
await sleep(100);
}
await sleep(pausa);
},
async scegli(testo) { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); },
// Partita nuova per Campomigliaio, introduzione saltata, dritti nella scena del gioco.
async vaiA(scena, at) {
await d.click(p.locator("#new-game"));
await sleep(1200);
await d.click(p.locator('#rione [data-rione="campomigliaio"]'));
await sleep(800);
if (await p.isVisible("#skip-intro")) await d.click(p.locator("#skip-intro"), 0.5);
await p.evaluate(([s, a]) => { game.go(s, a); }, [scena, at]);
await r.fermi(1200);
},
async parlaCon(id) {
const [x, y] = await p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id);
await r.clicca([x, y]);
await r.fermi();
},
// L'audio annotato dentro la pagina (musica voluta e voci del gioco), per rimontarlo (monta.py).
async ascolta() {
await p.evaluate(() => {
window.__t0 = Date.now(); window.__audio = [];
let music = undefined, voice = null;
const t = () => (Date.now() - window.__t0) / 1000;
const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1));
const look = () => {
if (window.music && window.music.want !== music) { music = window.music.want; window.__audio.push({ t: t(), music: music ?? null }); }
const a = window.voices?.audio, now = a && !a.paused && !a.ended ? a : null;
if (now !== voice) {
if (voice) window.__audio.push({ t: t(), voiceEnd: true });
if (now) window.__audio.push({ t: t(), voice: path(now.src) });
voice = now;
}
requestAnimationFrame(look);
};
look();
});
},
async salvaAudio() {
const audio = await p.evaluate(() => window.__audio);
const out = new URL("./out/", import.meta.url);
mkdirSync(out, { recursive: true });
writeFileSync(new URL(`${clip}-audio.json`, out), JSON.stringify({ eventi: audio }, null, 2));
},
};
return r;
}