Scopa agli 11 all'oratorio, e il tressette rivisto dopo la revisione critica di Codex

Scopa: toscane o napoletane (mattoni o denari), presa obbligatoria, la carta uguale prima delle somme, scelta fra più prese toccando la tavola, coi pulsanti o con frecce e Invio; scope (non all'ultima carta), carte, mattoni, settebello, primiera; tre livelli tarati (92, 64, 52%); test e prova nel browser.
Tressette: fuori ai 21 presa per presa, vittoria subito definitiva (Esc non la cancella più), accuse dette al proprio turno e col seme che manca del tris, striscio e busso meno rigidi, il socio risponde al busso, tiene il 3 e il 2, non esce d'Asso scoperto, legge il volo; tastiera col fuoco sulla carta scelta, tavola descritta a parole, riepilogo che aspetta Andrea. Stesso Esc corretto anche nella scopa.

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 15:59:10 +01:00
co-authored by Claude Opus 5.5
parent e8152563d8
commit 30e324d7e2
25 changed files with 924 additions and 94 deletions
+72 -5
View File
@@ -14,6 +14,8 @@ import * as briscola from './engine/briscola.js';
import { cardAt } from './engine/briscola-render.js';
import * as tressette from './engine/tressette.js';
import { cardAt as tressetteAt } from './engine/tressette-render.js';
import * as scopa from './engine/scopa.js';
import { cardAt as scopaAt, tableAt as scopaTableAt } from './engine/scopa-render.js';
import { VERSION, versioned } from './version.js';
const $ = id => document.getElementById(id);
@@ -403,11 +405,18 @@ canvas.addEventListener('pointerdown', e => {
if (!running || e.button > 0) return;
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
if (game.closeup) {
const deck = CARD_AT[story.closeups[game.closeup.id].type];
const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type];
if (deck) {
if (game.skip()) return;
// Tressette: fra una smazzata e l'altra un tocco va avanti.
if (tressette.advance(game.closeup)) 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.
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]);
return;
}
press = { card: deck(game.closeup, scenePoint(e)), state: game.closeup, at: deck };
canvas.setPointerCapture(e.pointerId);
return;
@@ -463,6 +472,7 @@ canvas.addEventListener('pointerleave', () => { mouse = null; });
// Comandi dei minigiochi separati dai verbi: carte oppure mira, forza e lancio.
const MINI = {
scopa: { ...scopa, back: 'Torna all’oratorio', label: 'Scopa: tocca una carta per giocarla. Se le prese sono più d’una, tocca una carta in tavola o usa le frecce, Invio prende. Esc esce.' },
tressette: { ...tressette, back: 'Torna all’oratorio', label: 'Tressette: tocca una carta per giocarla. Frecce scelgono, Invio gioca; B, S, V fanno busso, striscio e volo. Esc esce.' },
briscola: { ...briscola, back: 'Torna all’oratorio', label: 'Briscola: tocca una carta per giocarla. Frecce o 1, 2, 3 scelgono; Invio gioca. Esc esce.' },
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
@@ -473,7 +483,14 @@ const mini = () => MINI[story.closeups[game.closeup.id].type];
$('bocce-power').addEventListener('input', e => { const c = miniState(); if (c) mini().setPower?.(c, Number(e.target.value) / 100); });
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); });
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
const CARD_AT = { briscola: cardAt, tressette: tressetteAt };
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
for (let i = 0; i < 3; i++) {
const button = $('scopa-card-' + 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('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++) {
const button = $('tressette-card-' + i);
button.addEventListener('click', () => game?.closeupPlay(i));
@@ -575,6 +592,27 @@ window.addEventListener('keydown', e => {
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
// Nel primo piano: Invio, spazio o freccia sfogliano (o saltano la battuta), Esc chiude il libro.
if (running && game.closeup) {
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;
if (['arrowleft', 'arrowright', 'arrowup', 'arrowdown', 'enter', ' '].includes(key) || /^[1-9]$/.test(key)) {
e.preventDefault();
if (e.repeat || game.skip()) return;
if (/^[1-9]$/.test(key)) scopa.select(c, Number(key) - 1);
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();
return;
}
// Esc: nella scelta della presa si torna alla mano, altrimenti si lascia il tavolo.
if (e.key === 'Escape') { if (!scopa.cancel(c)) game.closeupClose(); }
else if (key === 'm') toggleMute();
return;
}
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; }
@@ -588,6 +626,8 @@ window.addEventListener('keydown', e => {
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();
// 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;
}
if (e.key === 'Escape') game.closeupClose();
@@ -700,11 +740,38 @@ function updateUi() {
updateChoices();
updateSpeech();
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
const cards = def?.type === 'briscola' || def?.type === 'tressette';
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
$('bocce-controls').hidden = !mg || cards;
$('bocce-controls').inert = !running;
$('briscola-controls').hidden = def?.type !== 'briscola';
$('briscola-controls').inert = !running;
$('scopa-controls').hidden = def?.type !== 'scopa';
$('scopa-controls').inert = !running;
if (def?.type === 'scopa') {
$('panel').hidden = $('choices').hidden = true;
const line = scopa.status(c, def.names);
if ($('scopa-status').textContent !== line) $('scopa-status').textContent = line;
for (let i = 0; i < 3; i++) {
const button = $('scopa-card-' + i), card = c.hands.player[i];
const label = card ? `${i + 1}. ${scopa.cardName(card)}` : `${i + 1}. —`;
if (button.textContent !== label) button.textContent = label;
button.disabled = !scopa.canPlay(c) || !card || !!game.speech;
button.setAttribute('aria-pressed', String(c.selected === i && !!card && c.step === 'play'));
}
// Le prese fra cui scegliere, una per pulsante.
const options = c.step === 'choose' ? c.choice.options.map(o => o.map(scopa.cardName).join(' + ')) : [];
const box = $('scopa-options');
if (box.dataset.list !== options.join('|')) {
box.dataset.list = options.join('|');
box.replaceChildren(...options.map((o, k) => {
const b = Object.assign(document.createElement('button'), { type: 'button', textContent: `Prendi ${o}` });
b.dataset.option = k; return b;
}));
}
box.hidden = !options.length;
[...box.children].forEach((b, k) => b.setAttribute('aria-pressed', String(c.choice?.selected === k)));
$('scopa-next').hidden = c.step !== 'smazzata';
}
$('tressette-controls').hidden = def?.type !== 'tressette';
$('tressette-controls').inert = !running;
if (def?.type === 'tressette') {