Tressette ai 21 all'oratorio: in quattro a coppie, Andrea e il socio contro il giocatore di carte e l'altro. Obbligo di seme, punti in terzi e ultima presa, accuse (napoletana, tre o quattro Assi, 2 o 3), segni onesti di chi esce (busso, striscio, volo) coi loro suoni, si va ai 21 smazzata dopo smazzata. Avversari «Alla bona», «Seria» (gioca a regola ma non conta) e «A fassi male» (conta, legge semi finiti, accuse e busso del socio). Tavolo a quattro, pulsanti dei segni e della mano, tastiera B/S/V; test, taratura e prova nel browser
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -249,8 +249,8 @@ function countTable(ctx, c, names) {
|
||||
}
|
||||
text(ctx, c.step === 'result' ? c.result.winner ? `Vince ${names[c.result.winner]}!` : '60 a 60. Patta!' : last ? `${cardName(last.card)}: ${last.card.points} punti` : 'Si contano tutte le carte…', 320, 339, 6, paper, 'center');
|
||||
}
|
||||
export function drawBriscola(ctx, c, def) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
// Il tavolino dell'oratorio: legno, panno verde e il titolo. Lo usa anche il tressette.
|
||||
export function drawTable(ctx, title) {
|
||||
rect(ctx, 0, 0, 640, 360, '#573720');
|
||||
for (let y = 0; y < 360; y += 24) {
|
||||
rect(ctx, 0, y, 640, 2, '#38271e');
|
||||
@@ -258,7 +258,11 @@ export function drawBriscola(ctx, c, def) {
|
||||
}
|
||||
rect(ctx, 17, 27, 606, 329, '#b28949'); rect(ctx, 20, 30, 600, 323, '#234c40');
|
||||
for (let i = 0; i < 600; i++) rect(ctx, 23 + (i * 73) % 590, 33 + (i * 41) % 315, 1, 1, '#2a5143');
|
||||
text(ctx, 'BRISCOLA ALL’ORATORIO', 320, 15, 9, '#ffe0a0', 'center');
|
||||
text(ctx, title, 320, 15, 9, '#ffe0a0', 'center');
|
||||
}
|
||||
export function drawBriscola(ctx, c, def) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
drawTable(ctx, 'BRISCOLA ALL’ORATORIO');
|
||||
if (c.step === 'count' || c.step === 'result') { countTable(ctx, c, def.names); ctx.restore(); return; }
|
||||
text(ctx, def.names.cpu, 320, 38, 6, paper, 'center');
|
||||
c.hands.cpu.forEach((_, i) => drawCard(ctx, null, 229 + i * 62, 49, 52, 74, true));
|
||||
|
||||
@@ -4,10 +4,12 @@ import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js
|
||||
import * as bocce from './bocce.js';
|
||||
import * as biliardo from './biliardo.js';
|
||||
import * as briscola from './briscola.js';
|
||||
import * as tressette from './tressette.js';
|
||||
|
||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||
export const MINIGAMES = {
|
||||
briscola: { create: (seed, level, deck) => briscola.newBriscola(seed, { level, deck }), update: briscola.update, play: briscola.play },
|
||||
tressette: { create: (seed, level, deck) => tressette.newTressette(seed, { level, deck }), update: tressette.update, play: tressette.play },
|
||||
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
|
||||
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { drawTorch } from './torch.js';
|
||||
import { drawBocce } from './bocce-render.js';
|
||||
import { drawBiliardo } from './biliardo-render.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { versioned } from '../version.js';
|
||||
export const WIDTH = 640, HEIGHT = 360;
|
||||
|
||||
@@ -668,6 +669,7 @@ function drawCloseup(ctx, game, time) {
|
||||
const c = game.closeup, def = game.story.closeups?.[c.id];
|
||||
if (!def) return;
|
||||
if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; }
|
||||
if (def.type === 'tressette') { drawTressette(ctx, c, def); return; }
|
||||
if (def.type === 'torch') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||
|
||||
@@ -103,6 +103,10 @@ export class Soundscape {
|
||||
if (kind === 'page') this.noiseSound(0.42, 2800, 0.24, { paper: true });
|
||||
else if (kind === 'card-slide') this.noiseSound(0.18, 2100, 0.13, { paper: true, pan });
|
||||
else if (kind === 'card-tap') { this.noiseSound(0.065, 1400, 0.17, { pan }); this.tone(220, 100, 0.06, 0.06, 0, pan); }
|
||||
// I segni del tressette si sentono: due nocche sul tavolo, la carta strusciata, la carta lasciata cadere.
|
||||
else if (kind === 'card-busso') for (const d of [0, 0.16]) { this.tone(150, 90, 0.07, 0.16, d, pan); this.tone(320, 180, 0.03, 0.06, d, pan); }
|
||||
else if (kind === 'card-striscio') this.noiseSound(0.6, 1500, 0.14, { paper: true, pan });
|
||||
else if (kind === 'card-volo') { this.noiseSound(0.12, 4200, 0.08, { paper: true, pan }); this.tone(260, 140, 0.05, 0.09, 0.12, pan); }
|
||||
else if (kind === 'card-shuffle') {
|
||||
this.noiseSound(0.55, 3100, 0.16, { paper: true, pan });
|
||||
for (let i = 0; i < 6; i++) this.tone(900, 300, 0.035, 0.035, i * 0.07, pan);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { canPlay, legal, accuseName, NAMES } from './tressette.js';
|
||||
import { drawCard, drawTable } from './briscola-render.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
// La mano di Andrea a ventaglio, carte a grandezza naturale (42×60): coordinate condivise da disegno e tocco.
|
||||
export const CARD = { w: 42, h: 60, y: 288, lift: 8 };
|
||||
export function handX(n, i) {
|
||||
const gap = n > 1 ? Math.min(44, (440 - CARD.w) / (n - 1)) : 0;
|
||||
return Math.round(320 - (gap * (n - 1) + CARD.w) / 2 + i * gap);
|
||||
}
|
||||
// Le carte si coprono: vince quella sopra, cioè la più a destra.
|
||||
export function cardAt(c, [x, y]) {
|
||||
if (!canPlay(c)) return -1;
|
||||
const n = c.hands[0].length;
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const top = CARD.y - (c.selected === i ? CARD.lift : 0), left = handX(n, i);
|
||||
if (x >= left && x < left + CARD.w && y >= top && y < top + CARD.h + CARD.lift) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// Dove cade la carta di ognuno al centro, e da dove arriva.
|
||||
const SPOT = [[299, 190], [354, 146], [299, 100], [244, 146]], FROM = [[0, 70], [80, 0], [0, -60], [-80, 0]];
|
||||
const LABEL = [[320, 280, 'center'], [612, 92, 'right'], [320, 39, 'center'], [28, 92, 'left']];
|
||||
const gold = '#ffe09b', paper = '#fff0cd', ours = '#9fe3ff', theirs = '#ffb0a3';
|
||||
const SAY = { busso: 'BUSSO!', striscio: 'STRISCIO!', volo: 'VOLO!' };
|
||||
|
||||
function others(ctx, c, names) {
|
||||
// Il socio di fronte, gli avversari ai lati: carte coperte, quante ne hanno.
|
||||
const n2 = c.hands[2].length, w2 = n2 ? (n2 - 1) * 12 + 26 : 0;
|
||||
for (let i = 0; i < n2; i++) drawCard(ctx, null, 320 - w2 / 2 + i * 12, 46, 26, 37, true);
|
||||
for (const [seat, x] of [[3, 30], [1, 584]]) {
|
||||
for (let i = 0; i < c.hands[seat].length; i++) drawCard(ctx, null, x, 102 + i * 12, 26, 37, true);
|
||||
}
|
||||
for (const seat of [1, 2, 3]) {
|
||||
// Chi deve giocare ha il nome in giallo.
|
||||
const [x, y, align] = LABEL[seat], turn = c.step === 'play' && c.current === seat;
|
||||
text(ctx, names[seat], x, y, 6, turn ? gold : seat % 2 ? theirs : ours, align);
|
||||
}
|
||||
}
|
||||
function scoreboard(ctx, c) {
|
||||
panel(ctx, 26, 34, 118, 38);
|
||||
text(ctx, `Noi ${c.score[0]}`, 34, 46, 7, ours);
|
||||
text(ctx, `Loro ${c.score[1]}`, 136, 46, 7, theirs, 'right');
|
||||
text(ctx, `Smazzata ${c.round} · ai 21`, 85, 62, 5, paper, 'center');
|
||||
}
|
||||
function accuses(ctx, c, names) {
|
||||
// Le accuse si vedono finché non è finita la prima presa.
|
||||
if (c.tricks.length || !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));
|
||||
}
|
||||
function trick(ctx, c, names) {
|
||||
c.table.forEach((p, i) => {
|
||||
const slide = i === c.table.length - 1 && c.step === 'play' ? Math.max(0, 1 - c.t / 0.18) : 0;
|
||||
const [x, y] = SPOT[p.who], [dx, dy] = FROM[p.who];
|
||||
drawCard(ctx, p.card, x + dx * slide, y + dy * slide, CARD.w, CARD.h);
|
||||
if (p.signal) {
|
||||
const [lx, ly] = p.who === 0 ? [x + 21, y + 70] : p.who === 2 ? [x + 21, y - 8] : p.who === 1 ? [x + 66, y + 30] : [x - 24, y + 30];
|
||||
panel(ctx, lx - 30, ly - 7, 60, 14); text(ctx, SAY[p.signal], lx, ly, 6, gold, 'center');
|
||||
}
|
||||
});
|
||||
if (c.step === 'trick') text(ctx, `Prende ${names[c.last.winner]}`, 320, 262, 6, gold, 'center');
|
||||
}
|
||||
function hand(ctx, c) {
|
||||
const cards = c.hands[0], n = cards.length, mine = canPlay(c), ok = mine ? legal(cards, c.table) : [];
|
||||
cards.forEach((card, i) => {
|
||||
const x = handX(n, i), up = mine && c.selected === i ? CARD.lift : 0;
|
||||
if (up) { ctx.fillStyle = '#ffdb76'; ctx.fillRect(x - 2, CARD.y - up - 2, CARD.w + 4, CARD.h + 4); }
|
||||
drawCard(ctx, card, x, CARD.y - up, CARD.w, CARD.h);
|
||||
// Le carte che non rispondono al seme si spengono.
|
||||
if (mine && !ok.includes(i)) { ctx.fillStyle = 'rgba(20,28,24,0.55)'; ctx.fillRect(x, CARD.y, CARD.w, CARD.h); }
|
||||
});
|
||||
if (mine && !c.table.length && c.signal) text(ctx, `Segno: ${SAY[c.signal]}`, 320, 278, 6, gold, 'center');
|
||||
}
|
||||
function summary(ctx, c, title) {
|
||||
const s = c.summary, row = (y, who, t, color) =>
|
||||
text(ctx, `${who}: ${s.thirds[t]} terzi = ${s.points[t]} punti${s.accused[t] ? `, accuse +${s.accused[t]}` : ''}${s.lastTeam === t ? ' (con l’ultima)' : ''}`, 172, y, 6, color);
|
||||
panel(ctx, 160, 96, 320, 132);
|
||||
text(ctx, title, 320, 112, 8, gold, 'center');
|
||||
row(140, 'Noi', 0, ours); row(158, 'Loro', 1, theirs);
|
||||
text(ctx, `Siamo ${c.score[0]} a ${c.score[1]}`, 320, 186, 8, paper, 'center');
|
||||
text(ctx, c.step === 'result' ? 'Esc o Invio per tornare all’oratorio' : 'Invio o un tocco per la prossima smazzata', 320, 210, 5, paper, 'center');
|
||||
}
|
||||
export function drawTressette(ctx, c, def) {
|
||||
const names = def?.seats ?? NAMES;
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
drawTable(ctx, 'TRESSETTE AI 21');
|
||||
scoreboard(ctx, c); others(ctx, c, names); accuses(ctx, c, names); trick(ctx, c, names); hand(ctx, c);
|
||||
if (c.step === 'smazzata') summary(ctx, c, `Fine della smazzata ${c.round}`);
|
||||
if (c.step === 'result') summary(ctx, c, c.result.winner === 'player' ? 'Avete vinto!' : 'Hanno vinto loro');
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Tressette a coppie «ai 21», come si gioca in Mugello: quattro al tavolo, Andrea e il socio contro gli altri due.
|
||||
// Regole e avversari senza DOM, come la briscola. Segni di chi esce (busso, striscio, volo) e accuse a inizio smazzata.
|
||||
import { DECKS, cardName } from './briscola.js';
|
||||
|
||||
export { cardName };
|
||||
export const LEVELS = ['bona', 'seria', 'male'];
|
||||
export const SIGNALS = ['busso', 'striscio', 'volo'];
|
||||
export const TARGET = 21;
|
||||
// Posti in senso antiorario, come si gira da noi: 0 Andrea (sotto), 1 a destra, 2 il socio (di fronte), 3 a sinistra.
|
||||
export const SEATS = [0, 1, 2, 3];
|
||||
export const team = seat => seat % 2;
|
||||
export const next = seat => (seat + 1) % 4;
|
||||
export const partner = seat => (seat + 2) % 4;
|
||||
// Ordine di presa 3 > 2 > Asso > Re > Cavallo > Fante > 7 > 6 > 5 > 4 (indici delle carte: Asso, 2, …, 7, figure).
|
||||
const POWER = [7, 8, 9, 0, 1, 2, 3, 4, 5, 6];
|
||||
// Punti in terzi: l'Asso vale un punto, il 2, il 3 e le figure un terzo. 32 terzi più 3 dell'ultima presa: 11 punti.
|
||||
const THIRDS = [3, 1, 1, 0, 0, 0, 0, 1, 1, 1];
|
||||
const build = ({ suits, ranks }) => Object.freeze(suits.flatMap((suit, s) => ranks.map((rank, r) =>
|
||||
Object.freeze({ id: s * 10 + r, suit, rank, thirds: THIRDS[r], power: POWER[r] }))));
|
||||
export const DECK_CARDS = Object.fromEntries(Object.entries(DECKS).map(([name, deck]) => [name, build(deck)]));
|
||||
const lcg = n => (Math.imul(n, 1664525) + 1013904223) >>> 0;
|
||||
function rng(seed) { let n = seed >>> 0; return () => (n = lcg(n)) / 4294967296; }
|
||||
function shuffle(cards, random) {
|
||||
for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; }
|
||||
return cards;
|
||||
}
|
||||
const byMin = (list, key) => list.reduce((a, b) => key(b) < key(a) ? b : a);
|
||||
|
||||
// Le accuse: napoletana (Asso, 2 e 3 dello stesso seme) 3 punti; buongioco, tre o quattro Assi, 2 o 3, 3 o 4 punti.
|
||||
export function accuse(hand) {
|
||||
const out = [], tops = ['Asso', '2', '3'];
|
||||
for (const suit of new Set(hand.map(a => a.suit))) {
|
||||
const cards = hand.filter(a => a.suit === suit && tops.includes(a.rank));
|
||||
if (cards.length === 3) out.push({ kind: 'napoletana', suit, points: 3, cards });
|
||||
}
|
||||
for (const rank of tops) {
|
||||
const cards = hand.filter(a => a.rank === rank);
|
||||
if (cards.length >= 3) out.push({ kind: 'buongioco', rank, points: cards.length, cards });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
export const accuseName = a => a.kind === 'napoletana' ? `napoletana di ${a.suit}` : `${a.cards.length === 4 ? 'quattro' : 'tre'} ${a.rank === 'Asso' ? 'Assi' : a.rank}`;
|
||||
|
||||
// Si risponde al seme quando si può; chi esce gioca quel che vuole.
|
||||
export function legal(hand, table) {
|
||||
const led = table[0]?.card.suit, same = hand.flatMap((a, i) => a.suit === led ? [i] : []);
|
||||
return same.length ? same : hand.map((_, i) => i);
|
||||
}
|
||||
export const best = table => table.reduce((a, p) => p.card.suit === table[0].card.suit && p.card.power > a.card.power ? p : a);
|
||||
// I segni sono onesti: volo con l'ultima carta del seme, striscio se ne restano almeno due, busso se ne resta una.
|
||||
export function signalOk(hand, index, table, signal) {
|
||||
if (!signal) return true;
|
||||
if (table.length || !SIGNALS.includes(signal) || !hand[index]) return false;
|
||||
const rest = hand.filter((a, i) => i !== index && a.suit === hand[index].suit).length;
|
||||
return signal === 'volo' ? rest === 0 : signal === 'striscio' ? rest >= 2 : rest >= 1;
|
||||
}
|
||||
// Perché una carta non si può giocare: la frase da mostrare ad Andrea, o '' se va bene.
|
||||
export function reason(c, index, signal = c.signal) {
|
||||
const hand = c.hands[0], card = hand[index];
|
||||
if (!card) return 'Scegli una carta.';
|
||||
if (!legal(hand, c.table).includes(index)) return `Devi rispondere a ${c.table[0].card.suit}.`;
|
||||
if (!signal || signalOk(hand, index, c.table, signal)) return '';
|
||||
if (c.table.length) return 'I segni li fa solo chi esce.';
|
||||
return { volo: `Volo solo con l'ultima carta di ${card.suit}.`, striscio: `Striscio solo se di ${card.suit} te ne restano almeno due.`, busso: `Busso solo se di ${card.suit} te ne resta almeno un'altra.` }[signal];
|
||||
}
|
||||
|
||||
const order = (a, b) => a.suit === b.suit ? b.power - a.power : a.id - b.id;
|
||||
function deal(c) {
|
||||
c.seed = lcg(c.seed);
|
||||
const stock = shuffle([...DECK_CARDS[c.deck]], rng(c.seed));
|
||||
c.hands = SEATS.map(seat => stock.slice(seat * 10, seat * 10 + 10).sort(order));
|
||||
// Fa le carte chi sta a sinistra di chi ha fatto prima; esce chi sta alla destra del mazziere.
|
||||
c.dealer = c.round ? next(c.dealer) : c.dealer; c.round++;
|
||||
c.current = c.mano = next(c.dealer); c.table = []; c.tricks = []; c.thirds = [0, 0];
|
||||
c.accuse = SEATS.map(i => (c.mano + i) % 4).flatMap(who => accuse(c.hands[who]).map(a => ({ who, ...a })));
|
||||
for (const a of c.accuse) c.score[team(a.who)] += a.points;
|
||||
c.step = 'play'; c.t = 0; c.selected = 0; c.signal = null; c.refused = '';
|
||||
c.events.push({ kind: 'card-shuffle' });
|
||||
}
|
||||
export function newTressette(seed = Date.now() >>> 0, { level = 'seria', deck = 'toscane' } = {}) {
|
||||
const c = { level: LEVELS.includes(level) ? level : 'seria', deck: DECKS[deck] ? deck : 'toscane', score: [0, 0], round: 0,
|
||||
seed: seed >>> 0, aiSeed: (seed ^ 0x85ebca6b) >>> 0, age: 0, events: [], last: null, summary: null };
|
||||
c.dealer = Math.floor(rng(seed)() * 4);
|
||||
deal(c); return c;
|
||||
}
|
||||
export const canPlay = c => c.step === 'play' && c.current === 0;
|
||||
export function select(c, index) {
|
||||
if (!canPlay(c) || !Number.isInteger(index) || !c.hands[0][index]) return false;
|
||||
c.selected = index; c.refused = ''; return true;
|
||||
}
|
||||
export function nudge(c, direction) {
|
||||
const n = c.hands[0].length;
|
||||
return select(c, (c.selected + direction + n) % n);
|
||||
}
|
||||
// Il segno di Andrea vale per la prossima carta con cui esce; ripremuto, si toglie.
|
||||
export function setSignal(c, signal) {
|
||||
if (!canPlay(c) || c.table.length || (signal && !SIGNALS.includes(signal))) return false;
|
||||
c.signal = c.signal === signal ? null : signal; c.refused = ''; return true;
|
||||
}
|
||||
export function play(c, index = c.selected, who = 0, signal = who === 0 ? c.signal : null) {
|
||||
if (c.step !== 'play' || c.current !== who || !Number.isInteger(index) || !c.hands[who][index]) return false;
|
||||
if (!legal(c.hands[who], c.table).includes(index) || !signalOk(c.hands[who], index, c.table, signal)) {
|
||||
if (who === 0) c.refused = reason(c, index, signal);
|
||||
return false;
|
||||
}
|
||||
const [card] = c.hands[who].splice(index, 1);
|
||||
c.table.push({ who, card, signal: signal || null }); c.t = 0; c.landing = true;
|
||||
// Il segno si sente: busso batte sul tavolo, striscio la fa strusciare, volo la lascia cadere dall'alto.
|
||||
c.events.push({ kind: signal ? 'card-' + signal : 'card-slide', who });
|
||||
if (who === 0) { c.signal = null; c.refused = ''; c.selected = Math.min(c.selected, Math.max(0, c.hands[0].length - 1)); }
|
||||
if (c.table.length === 4) { c.step = 'trick'; c.last = { winner: best(c.table).who, cards: c.table.map(p => p.card) }; }
|
||||
else c.current = next(who);
|
||||
return true;
|
||||
}
|
||||
export function collect(c) {
|
||||
if (c.step !== 'trick') return false;
|
||||
const winner = c.last.winner;
|
||||
c.thirds[team(winner)] += c.table.reduce((n, p) => n + p.card.thirds, 0);
|
||||
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' });
|
||||
if (c.hands[0].length) { c.step = 'play'; return true; }
|
||||
// L'ultima presa vale un punto; i terzi avanzati non contano.
|
||||
c.thirds[team(winner)] += 3;
|
||||
const points = c.thirds.map(t => Math.floor(t / 3)), accused = [0, 1].map(t => c.accuse.filter(a => team(a.who) === t).reduce((n, a) => n + a.points, 0));
|
||||
c.score = c.score.map((s, t) => s + points[t]);
|
||||
c.summary = { thirds: [...c.thirds], points, accused, lastTeam: team(winner), score: [...c.score] };
|
||||
c.step = 'smazzata';
|
||||
return true;
|
||||
}
|
||||
// Si va ai 21: chi arriva per primo a fine smazzata; pari sopra i 21, si gioca un'altra smazzata.
|
||||
export const finished = c => Math.max(...c.score) >= TARGET && c.score[0] !== c.score[1];
|
||||
export function advance(c) {
|
||||
if (c.step !== 'smazzata') return false;
|
||||
if (finished(c)) { c.step = 'result'; c.result = { winner: c.score[0] > c.score[1] ? 'player' : 'cpu', score: [...c.score] }; }
|
||||
else deal(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 })),
|
||||
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) };
|
||||
}
|
||||
// Quel che un giocatore sa. Chi conta davvero («A fassi male») ricorda le carte uscite, i semi finiti di ognuno e le
|
||||
// carte accusate dagli altri.
|
||||
export function knowledge(v, full) {
|
||||
// Chi non conta («Seria») vede solo le carte in tavola e le sue: un 3 già uscito per lui può essere ancora in giro.
|
||||
const played = [...(full ? v.tricks.flatMap(t => t.cards) : []), ...v.table];
|
||||
const gone = new Set([...v.hand, ...played.map(p => p.card)].map(a => a.id));
|
||||
const out = DECK_CARDS[v.deck].filter(a => !gone.has(a.id));
|
||||
const voids = SEATS.map(() => new Set()), holder = new Map();
|
||||
if (full) {
|
||||
for (const t of [...v.tricks, { cards: v.table }]) t.cards.forEach((p, i) => { if (i && p.card.suit !== t.cards[0].card.suit) voids[p.who].add(t.cards[0].card.suit); });
|
||||
for (const a of v.accuse) if (a.who !== v.seat) for (const card of a.cards) if (!gone.has(card.id)) holder.set(card.id, a.who);
|
||||
}
|
||||
const could = (seat, card) => seat !== v.seat && !voids[seat].has(card.suit) && (!holder.has(card.id) || holder.get(card.id) === seat);
|
||||
// Qualcuno fra `seats` può ancora battere `card`?
|
||||
const beatable = (card, seats) => out.some(a => a.suit === card.suit && a.power > card.power && seats.some(s => could(s, a)));
|
||||
return { out, voids, could, beatable, full };
|
||||
}
|
||||
const rich = cards => byMin(cards, a => -a.thirds * 20 + a.power); // il carico più grosso, senza buttare i 3
|
||||
const cheap = cards => byMin(cards, a => a.thirds * 20 + a.power); // la carta che costa meno
|
||||
function lead(v, k) {
|
||||
const hand = v.hand, foes = [next(v.seat), (v.seat + 3) % 4];
|
||||
// Il socio ha bussato e la presa l'ho fatta io: gli ritorno il seme con la più bassa.
|
||||
const prev = v.tricks.at(-1), knock = prev?.cards[0];
|
||||
if (k.full && prev?.winner === v.seat && knock?.who === partner(v.seat) && knock.signal === 'busso') {
|
||||
const same = hand.filter(a => a.suit === knock.card.suit);
|
||||
if (same.length) return { index: hand.indexOf(cheap(same)), signal: null };
|
||||
}
|
||||
// Una carta che gli avversari non possono più battere: si fa la presa, e coi punti dentro.
|
||||
const sure = hand.filter(a => !k.beatable(a, foes));
|
||||
if (sure.length) { const card = rich(sure); return { index: hand.indexOf(card), signal: signalFor(hand, card, k) }; }
|
||||
// Altrimenti dal seme più lungo, la più bassa; il segno dice al socio com'è fatto il seme.
|
||||
const count = suit => hand.filter(a => a.suit === suit).length;
|
||||
const longest = byMin(hand, a => -count(a.suit) * 10 - a.thirds).suit;
|
||||
const card = cheap(hand.filter(a => a.suit === longest));
|
||||
return { index: hand.indexOf(card), signal: signalFor(hand, card, k) };
|
||||
}
|
||||
function signalFor(hand, card, k) {
|
||||
const rest = hand.filter(a => a !== card && a.suit === card.suit);
|
||||
if (!rest.length) return 'volo';
|
||||
// Busso: la mia più alta è la seconda buona del seme; il socio prenda con la prima e me lo ritorni.
|
||||
const top = rest.reduce((a, b) => b.power > a.power ? b : a);
|
||||
if (top.power > card.power && k.out.filter(a => a.suit === card.suit && a.power > top.power).length === 1) return 'busso';
|
||||
return rest.length >= 2 ? 'striscio' : null;
|
||||
}
|
||||
function follow(v, k, moves) {
|
||||
const cards = moves.map(i => v.hand[i]), top = best(v.table), mate = team(top.who) === team(v.seat);
|
||||
const after = [1, 2, 3].map(n => (v.seat + n) % 4).slice(0, 3 - v.table.length).filter(s => team(s) !== team(v.seat));
|
||||
const pick = card => v.hand.indexOf(card);
|
||||
// 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));
|
||||
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));
|
||||
}
|
||||
return pick(cheap(cards));
|
||||
}
|
||||
// «Alla bona» butta una carta a caso fra quelle buone; «Seria» gioca a regola ma non conta; «A fassi male» conta
|
||||
// le carte uscite, i semi finiti, le accuse e risponde al busso del socio. Il socio di Andrea gioca sempre «A fassi male».
|
||||
// Taratura (tools/calibrate_tressette.mjs, 300 partite ai 21 per livello, Andrea e il socio che giocano attenti):
|
||||
// Andrea e il socio vincono 86% contro «Alla bona», 59% contro «Seria», 52% contro «A fassi male» (docs/TRESSETTE.md).
|
||||
export function choose(v, level = 'seria', seed = 1) {
|
||||
const moves = legal(v.hand, v.table);
|
||||
if (level === 'bona') return { index: moves[Math.floor(rng(seed)() * moves.length)], signal: null };
|
||||
const k = knowledge(v, level === 'male');
|
||||
return v.table.length ? { index: follow(v, k, moves), signal: null } : lead(v, k);
|
||||
}
|
||||
|
||||
export function update(c, dt) {
|
||||
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
||||
c.t += dt;
|
||||
if (c.landing && c.t >= 0.18) { c.events.push({ kind: 'card-tap' }); c.landing = false; }
|
||||
if (c.step === 'trick' && c.t >= 1.2) collect(c);
|
||||
else if (c.step === 'smazzata' && c.t >= 7) advance(c);
|
||||
else if (c.step === 'play' && c.current !== 0 && c.t >= 0.75) {
|
||||
c.aiSeed = lcg(c.aiSeed);
|
||||
const seat = c.current, { index, signal } = choose(observation(c, seat), seat === 2 ? 'male' : c.level, c.aiSeed);
|
||||
play(c, index, seat, signal);
|
||||
}
|
||||
}
|
||||
export const NAMES = ['Andrea', 'Il giocatore', 'Il socio', 'L\'altro'];
|
||||
export function status(c, names = NAMES) {
|
||||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Avete vinto voi' : 'Hanno vinto loro'}, ${c.score[0]} a ${c.score[1]}!`;
|
||||
if (c.step === 'smazzata') return `Smazzata finita: noi ${c.summary.points[0]}, loro ${c.summary.points[1]}. Siamo ${c.score[0]} a ${c.score[1]}. Invio per continuare.`;
|
||||
if (c.step === 'trick') return `Prende ${names[c.last.winner]}.`;
|
||||
if (c.current !== 0) {
|
||||
const p = c.table.at(-1);
|
||||
return p?.signal ? `${names[p.who]}: «${p.signal[0].toUpperCase() + p.signal.slice(1)}!»` : `${names[c.current]} pensa…`;
|
||||
}
|
||||
if (c.refused) return c.refused;
|
||||
if (c.table.length) return `Rispondi a ${c.table[0].card.suit}, se ce l'hai. Frecce scelgono, Invio gioca.`;
|
||||
return `Esci tu${c.signal ? `, col segno «${c.signal}»` : ''}. B, S, V per bussare, strisciare o volare; Invio gioca.`;
|
||||
}
|
||||
Reference in New Issue
Block a user