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:
@@ -74,6 +74,8 @@ export function collect(c) {
|
||||
}
|
||||
c.current = winner; c.t = 0; c.events.push({ kind: 'card-slide' });
|
||||
c.step = c.hands.player.length ? 'play' : 'count';
|
||||
// Il risultato è deciso all'ultima presa: il conteggio carta per carta lo mostra soltanto.
|
||||
if (c.step === 'count') c.result = result(c.taken);
|
||||
return true;
|
||||
}
|
||||
// La sola porta d'ingresso dell'IA: copie di informazioni pubbliche e della PROPRIA mano.
|
||||
@@ -195,5 +197,7 @@ export function status(c, names = { player: 'Andrea', cpu: 'Il giocatore' }) {
|
||||
return last ? `${names[last.who]}: ${cardName(last.card)}, +${last.card.points}. Totale: ${counted(c)[last.who]}.` : 'Si contano le prese, una carta alla volta…';
|
||||
}
|
||||
if (c.step === 'trick') return `Prende ${names[c.last.winner]}.`;
|
||||
return c.current === 'player' ? 'Tocca una carta per giocarla. Frecce o 1–2–3 scelgono, Invio gioca.' : `${names.cpu} sceglie…`;
|
||||
// A parole anche quel che si vede sul tavolo: la briscola e la carta già giocata.
|
||||
const table = `Briscola: ${c.trump}.${c.table.length ? ` In tavola: ${cardName(c.table[0].card)}.` : ''}`;
|
||||
return c.current === 'player' ? `${table} Tocca una carta per giocarla. Frecce o 1–2–3 scelgono, Invio gioca.` : `${table} ${names.cpu} sceglie…`;
|
||||
}
|
||||
|
||||
+19
-2
@@ -251,6 +251,20 @@ export class Game {
|
||||
}
|
||||
|
||||
// Chiuso il libro si torna alla scena, e gli effetti che seguivano il primo piano ripartono.
|
||||
outcome(def, c) { return def[c.result.winner === 'player' ? 'win' : c.result.winner === 'cpu' ? 'lose' : 'draw'] ?? []; }
|
||||
// Appena il risultato di un minigioco è deciso, i suoi flag (vittoria, partita giocata) valgono e si salvano:
|
||||
// anche tornando al titolo dalla schermata del risultato, senza passare dall'uscita. Una volta sola.
|
||||
settle(c) {
|
||||
const def = this.story.closeups?.[c.id];
|
||||
if (!c.result || c.settled || !def) return false;
|
||||
c.settled = true;
|
||||
for (const e of this.outcome(def, c)) {
|
||||
if (e.set) { this.state.flags[e.set] = true; if (this.checkpoint) this.checkpoint.flags[e.set] = true; }
|
||||
if (e.unset) { delete this.state.flags[e.unset]; if (this.checkpoint) delete this.checkpoint.flags[e.unset]; }
|
||||
}
|
||||
if (this.checkpoint) this.onSave(this.checkpoint);
|
||||
return true;
|
||||
}
|
||||
closeupClose() {
|
||||
const c = this.closeup;
|
||||
if (!c) return false;
|
||||
@@ -261,9 +275,11 @@ export class Game {
|
||||
if (c.step === 'done') this.queue.unshift(...def.complete);
|
||||
return true;
|
||||
}
|
||||
// Bocce e biliardo: a partita finita, la battuta di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
||||
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||
if (MINIGAMES[def?.type]) {
|
||||
this.queue.unshift(...(c.step === 'result' ? def[c.result?.winner === 'player' ? 'win' : c.result?.winner === 'cpu' ? 'lose' : 'draw'] ?? [] : def.quit ?? []));
|
||||
this.settle(c);
|
||||
this.queue.unshift(...(c.result ? this.outcome(def, c) : def.quit ?? []));
|
||||
return true;
|
||||
}
|
||||
// Chi è arrivato all'ultima pagina, chiudendo, la commenta (`closing`).
|
||||
@@ -409,6 +425,7 @@ export class Game {
|
||||
const m = this.minigame();
|
||||
if (m) {
|
||||
m.update(c, dt);
|
||||
this.settle(c);
|
||||
// Il risultato resta leggibile finché il giocatore non esce.
|
||||
return;
|
||||
}
|
||||
|
||||
+45
-4
@@ -146,7 +146,7 @@ export function advance(c) {
|
||||
// La sola porta d'ingresso dell'avversario: la sua mano, la tavola e le carte prese (si son viste tutte).
|
||||
export function observation(c, who = 'cpu') {
|
||||
return { who, deck: c.deck, hand: [...c.hands[who]], table: [...c.table], taken: { player: [...c.taken.player], cpu: [...c.taken.cpu] },
|
||||
opponentCount: c.hands[other(who)].length, stock: c.stock.length, scope: { ...c.scope } };
|
||||
opponentCount: c.hands[other(who)].length, stock: c.stock.length, scope: { ...c.scope }, lastTaker: c.lastTaker };
|
||||
}
|
||||
// Le carte che non si sono viste: in mano all'altro o nel mazzo.
|
||||
export function unseen(v) {
|
||||
@@ -159,7 +159,11 @@ export function unseen(v) {
|
||||
export function score(v, card, captured, counting) {
|
||||
const coin = COIN[v.deck], last = !v.stock && v.hand.length === 1 && !v.opponentCount;
|
||||
const left = captured ? v.table.filter(a => !captured.includes(a)) : [...v.table, card];
|
||||
const worth = a => 1 + (a.suit === coin ? 1.5 : 0) + (isSettebello(a, v.deck) ? 12 : 0) + (a.value === 7 ? 3 : a.value === 6 ? 1.2 : a.value === 1 ? 0.8 : 0);
|
||||
// La primiera vale per quanto migliora la propria: un 7 di un seme dove si ha già un 7 non aggiunge niente,
|
||||
// la prima carta di un seme che manca aggiunge parecchio.
|
||||
const mine = v.taken[v.who] ?? [], best = suit => Math.max(0, ...mine.filter(a => a.suit === suit).map(a => a.prime));
|
||||
const prime = a => Math.max(0, a.prime - best(a.suit)) / 4 + (best(a.suit) ? 0 : 1);
|
||||
const worth = a => 1 + (a.suit === coin ? 1.5 : 0) + (isSettebello(a, v.deck) ? 12 : 0) + prime(a);
|
||||
let value = captured ? [card, ...captured].reduce((n, a) => n + worth(a), 0) + (!left.length && !last ? 20 : 0) : -worth(card) * 0.6;
|
||||
const sum = left.reduce((n, a) => n + a.value, 0);
|
||||
if (left.length && sum <= 10 && v.opponentCount) {
|
||||
@@ -181,11 +185,46 @@ export function moves(v) {
|
||||
});
|
||||
}
|
||||
// «Alla bona» gioca a caso; «Seria» e «A fassi male» scelgono la mossa che vale di più.
|
||||
// Taratura (tools/calibrate_scopa.mjs, 300 partite): Andrea vince 92% contro «Alla bona», 64% contro «Seria», 52% contro «A fassi male».
|
||||
// Taratura (tools/calibrate_scopa.mjs, 300 partite): Andrea vince 96% contro «Alla bona», 76% contro «Seria», 50% contro «A fassi male».
|
||||
// A mazzo finito le carte dell'altro sono proprio quelle non viste: si calcola il finale esatto, fino al conteggio
|
||||
// (carte, mattoni, settebello, primiera, scope, e la tavola all'ultimo che prende).
|
||||
export function endgame(v) {
|
||||
const me = v.who, them = other(me), memo = new Map();
|
||||
const final = (taken, scope, table, last) => {
|
||||
const all = { player: [...taken.player], cpu: [...taken.cpu] };
|
||||
if (last) all[last].push(...table);
|
||||
const t = tally(all, scope, v.deck);
|
||||
return t.points[me] - t.points[them];
|
||||
};
|
||||
const solve = (hands, table, taken, scope, last, turn) => {
|
||||
if (!hands.player.length && !hands.cpu.length) return final(taken, scope, table, last);
|
||||
const key = `${turn}|${hands[me].map(a => a.id)}|${hands[them].map(a => a.id)}|${table.map(a => a.id).sort()}|${last}|${taken.player.length}|${scope.player}${scope.cpu}`;
|
||||
if (memo.has(key)) return memo.get(key);
|
||||
let best = turn === me ? -Infinity : Infinity;
|
||||
for (const m of moves({ hand: hands[turn], table })) {
|
||||
const r = step(hands, table, taken, scope, last, turn, m);
|
||||
const value = solve(...r);
|
||||
best = turn === me ? Math.max(best, value) : Math.min(best, value);
|
||||
}
|
||||
memo.set(key, best); return best;
|
||||
};
|
||||
const step = (hands, table, taken, scope, last, turn, m) => {
|
||||
const card = hands[turn][m.index], rest = { ...hands, [turn]: hands[turn].filter((_, i) => i !== m.index) };
|
||||
if (!m.captured) return [rest, [...table, card], taken, scope, last, other(turn)];
|
||||
const ids = new Set(m.captured.map(a => a.id)), left = table.filter(a => !ids.has(a.id));
|
||||
const end = !rest.player.length && !rest.cpu.length;
|
||||
return [rest, left, { ...taken, [turn]: [...taken[turn], card, ...m.captured] },
|
||||
!left.length && !end ? { ...scope, [turn]: scope[turn] + 1 } : scope, turn, other(turn)];
|
||||
};
|
||||
const hands = { [me]: v.hand, [them]: unseen(v) };
|
||||
const start = [hands, v.table, v.taken, v.scope ?? { player: 0, cpu: 0 }, v.lastTaker ?? null, me];
|
||||
return moves(v).map(m => ({ m, value: solve(...step(...start, m)) })).reduce((a, b) => b.value > a.value ? b : a).m;
|
||||
}
|
||||
export function choose(v, level = 'seria', seed = 1) {
|
||||
const all = moves(v);
|
||||
if (level === 'bona') return all[Math.floor(rng(seed)() * all.length)];
|
||||
const counting = level === 'male';
|
||||
if (counting && !v.stock && unseen(v).length === v.opponentCount) return endgame(v);
|
||||
return all.reduce((a, b) => score(v, v.hand[b.index], b.captured, counting) > score(v, v.hand[a.index], a.captured, counting) ? b : a);
|
||||
}
|
||||
export function update(c, dt) {
|
||||
@@ -206,5 +245,7 @@ export function status(c, names = { player: 'Andrea', cpu: 'Il giocatore' }) {
|
||||
const l = c.last;
|
||||
return l.scopa ? `Scopa di ${names[l.who]}!` : l.captured.length ? `${names[l.who]} prende con ${cardName(l.card)}.` : `${names[l.who]} posa ${cardName(l.card)}.`;
|
||||
}
|
||||
return c.current === 'player' ? 'Tocca una carta per giocarla. Frecce o 1–2–3 scelgono, Invio gioca.' : `${names.cpu} pensa…`;
|
||||
// A parole anche la tavola, per chi non vede il disegno.
|
||||
const table = `In tavola: ${c.table.map(cardName).join(', ') || 'niente'}.`;
|
||||
return c.current === 'player' ? `${table} Tocca una carta per giocarla. Frecce o 1–2–3 scelgono, Invio gioca.` : `${table} ${names.cpu} pensa…`;
|
||||
}
|
||||
|
||||
@@ -46,8 +46,9 @@ function scoreboard(ctx, c) {
|
||||
function accuses(ctx, c, names) {
|
||||
// Le accuse dette restano in vista per tutta la smazzata.
|
||||
if (!c.accuse.length) return;
|
||||
panel(ctx, 456, 34, 158, 12 + c.accuse.length * 11);
|
||||
c.accuse.forEach((a, i) => text(ctx, `${names[a.who]}: ${accuseName(a)} +${a.points}`, 463, 41 + i * 11, 5, a.who % 2 ? theirs : ours));
|
||||
// Largo abbastanza per «Il giocatore: tre Assi, manca mattoni +3» (200 px nel carattere a 5).
|
||||
panel(ctx, 396, 34, 218, 12 + c.accuse.length * 11);
|
||||
c.accuse.forEach((a, i) => text(ctx, `${names[a.who]}: ${accuseName(a)} +${a.points}`, 403, 41 + i * 11, 5, a.who % 2 ? theirs : ours));
|
||||
}
|
||||
function trick(ctx, c, names) {
|
||||
c.table.forEach((p, i) => {
|
||||
|
||||
+27
-13
@@ -111,6 +111,8 @@ export function play(c, index = c.selected, who = 0, signal = who === 0 ? c.sign
|
||||
if (!c.tricks.length && c.pending[who].length) {
|
||||
c.accuse.push(...c.pending[who]); c.pending[who] = []; recount(c);
|
||||
c.events.push({ kind: 'accusa', who });
|
||||
// Un'accusa che porta ai 21 fa uscire subito: è il primo evento decisivo, prima ancora della carta.
|
||||
if (out(c)) return true;
|
||||
}
|
||||
const [card] = c.hands[who].splice(index, 1);
|
||||
c.table.push({ who, card, signal: signal || null }); c.t = 0; c.landing = true;
|
||||
@@ -132,19 +134,24 @@ export function collect(c) {
|
||||
c.tricks.push({ leader: c.table[0].who, winner, cards: c.table }); c.table = [];
|
||||
c.current = winner; c.t = 0; c.events.push({ kind: 'card-slide' });
|
||||
recount(c);
|
||||
// Si va ai 21: chi ci arriva per primo è fuori e ha vinto, anche a metà smazzata (una presa porta punti a una
|
||||
// coppia sola, quindi non ci arrivano mai insieme).
|
||||
const out = [0, 1].find(t => c.score[t] >= TARGET);
|
||||
if (end || out !== undefined) {
|
||||
c.summary = { thirds: [...c.thirds], points: c.thirds.map(t => Math.floor(t / 3)), accused: [0, 1].map(t => accused(c, t)),
|
||||
lastTeam: end ? team(winner) : null, score: [...c.score], out: out ?? null };
|
||||
if (out !== undefined) { c.step = 'result'; c.result = { winner: out === 0 ? 'player' : 'cpu', score: [...c.score] }; }
|
||||
else c.step = 'smazzata';
|
||||
return true;
|
||||
}
|
||||
if (out(c, end ? team(winner) : null)) return true;
|
||||
if (end) { summarize(c, team(winner)); c.step = 'smazzata'; return true; }
|
||||
c.step = 'play';
|
||||
return true;
|
||||
}
|
||||
function summarize(c, lastTeam = null) {
|
||||
c.summary = { thirds: [...c.thirds], points: c.thirds.map(t => Math.floor(t / 3)), accused: [0, 1].map(t => accused(c, t)),
|
||||
lastTeam, score: [...c.score] };
|
||||
}
|
||||
// Si va ai 21: chi ci arriva per primo è fuori e ha vinto, anche a metà smazzata. Il controllo si fa a ogni evento che
|
||||
// porta punti (un'accusa, una presa), e ognuno ne porta a una coppia sola: non ci si arriva mai insieme.
|
||||
function out(c, lastTeam = null) {
|
||||
const t = [0, 1].find(t => c.score[t] >= TARGET);
|
||||
if (t === undefined) return false;
|
||||
summarize(c, lastTeam); c.summary.out = t;
|
||||
c.step = 'result'; c.result = { winner: t === 0 ? 'player' : 'cpu', score: [...c.score] };
|
||||
return true;
|
||||
}
|
||||
export const finished = c => c.step === 'result';
|
||||
// Dopo il riepilogo (resta finché Andrea non va avanti) si fa la smazzata dopo.
|
||||
export function advance(c) {
|
||||
@@ -154,7 +161,8 @@ export function advance(c) {
|
||||
|
||||
// La sola porta d'ingresso degli avversari e del socio: la propria mano e quello che si è visto al tavolo.
|
||||
export function observation(c, seat) {
|
||||
return { seat, deck: c.deck, hand: [...c.hands[seat]], table: c.table.map(p => ({ ...p })),
|
||||
// Anche il punteggio e i terzi presi: sono sul tabellone, li vede chiunque.
|
||||
return { seat, deck: c.deck, hand: [...c.hands[seat]], table: c.table.map(p => ({ ...p })), score: [...c.score], thirds: [...c.thirds],
|
||||
tricks: c.tricks.map(t => ({ ...t, cards: t.cards.map(p => ({ ...p })) })),
|
||||
accuse: c.accuse.map(a => ({ ...a, cards: [...a.cards] })), counts: c.hands.map(h => h.length) };
|
||||
}
|
||||
@@ -218,8 +226,14 @@ function follow(v, k, moves) {
|
||||
const over = cards.filter(a => a.power > top.card.power);
|
||||
if (over.length) return pick(over.reduce((a, b) => b.power > a.power ? b : a));
|
||||
}
|
||||
// Il socio prende di sicuro: gli si dà il carico (anche l'Asso di un altro seme, se il nostro è finito).
|
||||
if (mate && !k.beatable(top.card, after)) return pick(rich(cards));
|
||||
// Il socio prende di sicuro: gli si dà il carico (anche l'Asso di un altro seme, se il nostro è finito). Se un
|
||||
// carico basta per arrivare ai 21, si dà quello, anche il 2 o il 3: si esce e si vince.
|
||||
if (mate && !k.beatable(top.card, after)) {
|
||||
const t = team(v.seat), last = v.hand.length === 1, before = v.score?.[t] ?? 0, thirds = v.thirds?.[t] ?? 0;
|
||||
const sum = v.table.reduce((n, p) => n + p.card.thirds, 0) + (last ? 3 : 0);
|
||||
const wins = cards.filter(a => before - Math.floor(thirds / 3) + Math.floor((thirds + sum + a.thirds) / 3) >= TARGET);
|
||||
return pick(wins.length ? byMin(wins, a => a.power) : rich(cards));
|
||||
}
|
||||
if (cards[0].suit === top.card.suit) {
|
||||
const safe = cards.filter(a => a.power > top.card.power && !k.beatable(a, after));
|
||||
if (safe.length) return pick(byMin(safe, a => a.power));
|
||||
|
||||
+62
-13
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -2161,7 +2161,7 @@ export const story = {
|
||||
nodes: {
|
||||
start: { options: [
|
||||
{ text: 'Una briscola?', when: '!briscola_giocata', next: 'mazzo', then: [{ say: 'Gnamo. In due, senza soci e senza scuse.', who: 'giocatore_carte', pose: 'shuffle' }] },
|
||||
{ text: 'La rivincita?', when: 'briscola_giocata', next: 'mazzo', then: [{ say: 'Il tavolo è qui. Il coraggio ce lo metti te.', who: 'giocatore_carte', pose: 'laugh' }] },
|
||||
{ text: 'Rivincita a briscola?', when: 'briscola_giocata', next: 'mazzo', then: [{ say: 'Il tavolo è qui. Il coraggio ce lo metti te.', who: 'giocatore_carte', pose: 'laugh' }] },
|
||||
{ text: 'Una scopa?', next: 'mazzo_scopa', then: [{ say: 'Ai undici. E occhio al settebello: quello è sempre mio.', who: 'giocatore_carte', pose: 'shuffle' }] },
|
||||
{ text: 'Un tressette ai 21?', next: 'mazzo_tressette', then: [{ say: 'Ci vuole quattro: chiamo quei due che giocano qui dietro. Uno ti fa da socio.', who: 'giocatore_carte', pose: 'talk' }] },
|
||||
{ text: 'Ora cerco il palio. A dopo.', next: 'end' },
|
||||
|
||||
Reference in New Issue
Block a user