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
+62 -13
View File
@@ -289,6 +289,38 @@ function closePause() {
running = true;
}
$('pause-button').addEventListener('click', openPause);
// I video «Come si gioca» (story.tutorials): il gioco si ferma come in pausa, la musica tace, il video ha il suo audio.
function openTutorial(kind) {
const t = story.tutorials?.[kind];
if (!t || !running) return;
running = false;
game.closeupCancel();
music.hold = true;
voices.pause();
ambience.configure({ active: false });
$('tutorial-title').textContent = t.title;
const video = $('tutorial-video');
video.src = versioned(t.src);
if (t.poster) video.poster = versioned(t.poster);
video.muted = music.muted;
$('tutorial').hidden = false;
video.play().catch(() => {});
$('tutorial-close').focus();
}
function closeTutorial() {
const video = $('tutorial-video');
video.pause(); video.removeAttribute('src'); video.load();
$('tutorial').hidden = true;
music.hold = false;
voices.resume();
idleSince = performance.now();
running = true;
}
for (const button of document.querySelectorAll('.tutorial-open')) {
button.hidden = !story.tutorials?.[button.dataset.tutorial];
button.addEventListener('click', () => openTutorial(button.dataset.tutorial));
}
$('tutorial-close').addEventListener('click', closeTutorial);
// Sul telefono, passando a un'altra app (o cambiando scheda), si ferma tutto: musica, voce, musichetta del cartello;
// la partita va in pausa e aspetta. Tornando riparte la musica; la voce solo se non c'è la pausa (riparte con Riprendi).
function leave(hidden) {
@@ -410,11 +442,10 @@ canvas.addEventListener('pointerdown', e => {
if (game.skip()) return;
// Tressette e scopa: fra una smazzata e l'altra un tocco va avanti.
if (MINI[type].advance?.(game.closeup)) return;
// Scopa, più prese possibili: si tocca una carta in tavola; se sta in una presa sola, si prende quella.
// Scopa, più prese possibili: si tocca una carta in tavola (si decide al rilascio, vedi pointerup).
if (type === 'scopa' && game.closeup.step === 'choose') {
const c = game.closeup, card = c.table[scopaTableAt(c, scenePoint(e))];
const options = c.choice.options.flatMap((o, k) => o.includes(card) ? [k] : []);
if (options.length === 1) scopa.take(c, options[0]); else if (options.length) scopa.select(c, options[0]);
press = { table: scopaTableAt(game.closeup, scenePoint(e)), state: game.closeup };
canvas.setPointerCapture(e.pointerId);
return;
}
press = { card: deck(game.closeup, scenePoint(e)), state: game.closeup, at: deck };
@@ -437,6 +468,16 @@ canvas.addEventListener('pointerdown', e => {
});
canvas.addEventListener('pointerup', e => {
if (!press) return;
// Scopa: la carta toccata e lasciata sullo stesso punto. Se sta nella presa già scelta, si prende quella; se no
// si sceglie la prima presa che la contiene (un altro tocco conferma). Trascinando via non succede niente.
if (press.table !== undefined) {
const p = press; press = null;
const c = game.closeup;
if (!running || c !== p.state || c.step !== 'choose' || p.table < 0 || scopaTableAt(c, scenePoint(e)) !== p.table) return;
const card = c.table[p.table], options = c.choice.options.flatMap((o, k) => o.includes(card) ? [k] : []);
if (options.includes(c.choice.selected)) scopa.take(c); else if (options.length) scopa.select(c, options[0]);
return;
}
if (press.card !== undefined) {
const p = press; press = null;
if (running && game.closeup === p.state && p.card >= 0 && p.at(game.closeup, scenePoint(e)) === p.card) game.closeupPlay(p.card);
@@ -454,7 +495,7 @@ canvas.addEventListener('pointerup', e => {
press = null;
});
canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); press = null; game?.closeupCancel(); });
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup || press?.card !== undefined) { press = null; game?.closeupCancel(); } });
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup || press?.card !== undefined || press?.table !== undefined) { press = null; game?.closeupCancel(); } });
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
@@ -489,6 +530,7 @@ for (let i = 0; i < 3; i++) {
button.addEventListener('click', () => game?.closeupPlay(i));
button.addEventListener('focus', () => { if (game?.closeup?.id === 'scopa' && game.closeup.step === 'play') scopa.select(game.closeup, i); });
}
$('scopa-options').addEventListener('focusin', e => { const k = e.target.dataset?.option; if (k !== undefined && game?.closeup?.step === 'choose') scopa.select(game.closeup, Number(k)); });
$('scopa-options').addEventListener('click', e => { const k = e.target.dataset?.option; if (k !== undefined && game?.closeup?.id === 'scopa') scopa.take(game.closeup, Number(k)); });
$('scopa-next').addEventListener('click', () => { if (game?.closeup?.id === 'scopa') scopa.advance(game.closeup); });
for (let i = 0; i < 10; i++) {
@@ -586,6 +628,7 @@ window.addEventListener('keydown', e => {
return;
}
if (debugKey(e)) return;
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); return; }
if (!$('credits').hidden) { if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; } return; }
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
if (!$('notebook').hidden) { if (['Escape', 'n', 'N'].includes(e.key)) $('notebook').hidden = true; return; }
@@ -595,7 +638,9 @@ window.addEventListener('keydown', e => {
if (story.closeups[game.closeup.id].type === 'scopa') {
const inControls = e.target.closest?.('#scopa-controls'), c = game.closeup, key = e.key.toLowerCase();
if (e.repeat && ['enter', ' '].includes(key)) { e.preventDefault(); return; }
if (inControls && ['BUTTON', 'SUMMARY'].includes(e.target.tagName) && ['enter', ' '].includes(key)) return;
// Sui pulsanti delle carte e delle prese Invio e Spazio valgono per quella scelta (una sola fonte: lo stato).
const choice = e.target.id?.startsWith('scopa-card-') || e.target.dataset?.option !== undefined;
if (inControls && ['BUTTON', 'SUMMARY'].includes(e.target.tagName) && ['enter', ' '].includes(key) && !choice) return;
if (['arrowleft', 'arrowright', 'arrowup', 'arrowdown', 'enter', ' '].includes(key) || /^[1-9]$/.test(key)) {
e.preventDefault();
if (e.repeat || game.skip()) return;
@@ -603,9 +648,10 @@ window.addEventListener('keydown', e => {
else if (key.startsWith('arrow')) scopa.nudge(c, ['arrowleft', 'arrowup'].includes(key) ? -1 : 1);
else if (c.step === 'result') game.closeupClose();
else if (c.step === 'choose') scopa.take(c);
else if (!scopa.advance(c) && key === 'enter') game.closeupPlay();
// Il fuoco segue la carta scelta: Invio su un pulsante gioca sempre quella che si vede scelta.
if (inControls && scopa.canPlay(c)) $('scopa-card-' + c.selected).focus();
else if (!scopa.advance(c)) game.closeupPlay();
// Il fuoco segue la scelta, così quel che si vede scelto è quel che parte.
if (inControls && c.step === 'choose') $('scopa-options').querySelector(`[data-option="${c.choice.selected}"]`)?.focus();
else if (inControls && scopa.canPlay(c)) $('scopa-card-' + c.selected).focus();
return;
}
// Esc: nella scelta della presa si torna alla mano, altrimenti si lascia il tavolo.
@@ -616,7 +662,10 @@ window.addEventListener('keydown', e => {
if (story.closeups[game.closeup.id].type === 'tressette') {
const inControls = e.target.closest?.('#tressette-controls'), c = game.closeup;
if (e.repeat && ['Enter', ' '].includes(e.key)) { e.preventDefault(); return; }
if (inControls && ['BUTTON', 'SUMMARY'].includes(e.target.tagName) && ['Enter', ' '].includes(e.key)) return;
// Sui pulsanti della mano Invio e Spazio giocano la carta scelta (anche se se ne è scelta una che non si
// può giocare: allora si legge perché), mai quella che aveva il fuoco prima.
const card = e.target.id?.startsWith('tressette-card-');
if (inControls && ['BUTTON', 'SUMMARY'].includes(e.target.tagName) && ['Enter', ' '].includes(e.key) && !card) return;
const key = e.key.toLowerCase(), signal = { b: 'busso', s: 'striscio', v: 'volo' }[key];
if (['arrowleft', 'arrowright', 'arrowup', 'arrowdown', 'enter', ' '].includes(key) || /^[0-9]$/.test(key) || signal) {
e.preventDefault();
@@ -625,7 +674,7 @@ window.addEventListener('keydown', e => {
else if (/^[0-9]$/.test(key)) tressette.select(c, (Number(key) + 9) % 10);
else if (key.startsWith('arrow')) tressette.nudge(c, ['arrowleft', 'arrowup'].includes(key) ? -1 : 1);
else if (c.step === 'result') game.closeupClose();
else if (!tressette.advance(c) && key === 'enter') game.closeupPlay();
else if (!tressette.advance(c)) game.closeupPlay(); // Invio o Spazio, come negli altri due giochi
// Il fuoco segue la carta scelta: Invio su un pulsante gioca sempre quella che si vede scelta.
if (inControls && tressette.canPlay(c) && !signal) $('tressette-card-' + c.selected).focus();
return;
@@ -638,7 +687,7 @@ window.addEventListener('keydown', e => {
const inControls = e.target.closest?.('#briscola-controls');
if (e.repeat && ['Enter', ' '].includes(e.key)) { e.preventDefault(); return; }
if (inControls && e.target.tagName === 'SUMMARY' && e.key === 'Enter') return;
if (inControls && e.target.tagName === 'BUTTON' && !e.target.disabled && e.key === 'Enter' && briscola.canPlay(game.closeup)) return;
if (inControls && e.target.tagName === 'BUTTON' && !e.target.disabled && ['Enter', ' '].includes(e.key) && briscola.canPlay(game.closeup)) return;
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', '1', '2', '3', 'Enter', ' '].includes(e.key)) {
e.preventDefault();
if (e.repeat || game.skip()) return;
@@ -646,7 +695,7 @@ window.addEventListener('keydown', e => {
if (/^[123]$/.test(e.key)) briscola.select(c, Number(e.key) - 1);
else if (e.key.startsWith('Arrow')) briscola.nudge(c, ['ArrowLeft', 'ArrowUp'].includes(e.key) ? -1 : 1);
else if (c.step === 'result') game.closeupClose();
else if (e.key === 'Enter') game.closeupPlay();
else game.closeupPlay(); // Invio o Spazio, come negli altri due giochi
if (inControls && briscola.canPlay(c)) $('briscola-card-' + c.selected).focus();
return;
}