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:
@@ -0,0 +1,210 @@
|
||||
// Scopa in due, ai 11: regole e avversari senza DOM, come la briscola. Carte, mattoni (o denari), settebello,
|
||||
// primiera e scope. Con le toscane il seme «buono» è mattoni, con le napoletane denari.
|
||||
import { DECKS, cardName } from './briscola.js';
|
||||
|
||||
export { cardName };
|
||||
export const LEVELS = ['bona', 'seria', 'male'];
|
||||
export const TARGET = 11;
|
||||
export const COIN = { toscane: 'mattoni', napoletane: 'denari' };
|
||||
// Valore per le prese: Asso 1, poi i numeri, figure 8, 9 e 10. Primiera: 7 vale 21, 6 18, Asso 16, 5 15, 4 14,
|
||||
// 3 13, 2 12, le figure 10.
|
||||
const PRIME = [16, 12, 13, 14, 15, 18, 21, 10, 10, 10];
|
||||
const build = ({ suits, ranks }) => Object.freeze(suits.flatMap((suit, s) => ranks.map((rank, r) =>
|
||||
Object.freeze({ id: s * 10 + r, suit, rank, value: r + 1, prime: PRIME[r] }))));
|
||||
export const DECK_CARDS = Object.fromEntries(Object.entries(DECKS).map(([name, deck]) => [name, build(deck)]));
|
||||
export const other = who => who === 'player' ? 'cpu' : 'player';
|
||||
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;
|
||||
}
|
||||
|
||||
// Le prese possibili con `card`: se in tavola c'è una carta dello stesso valore si prende quella (una sola, a
|
||||
// scelta se sono più d'una); altrimenti ogni gruppo di carte che somma il suo valore. Se una presa c'è, è obbligatoria.
|
||||
export function captures(table, card) {
|
||||
const same = table.filter(a => a.value === card.value);
|
||||
if (same.length) return same.map(a => [a]);
|
||||
const out = [];
|
||||
for (let mask = 1; mask < 1 << table.length; mask++) {
|
||||
const pick = table.filter((_, i) => mask & 1 << i);
|
||||
if (pick.length > 1 && pick.reduce((n, a) => n + a.value, 0) === card.value) out.push(pick);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const isSettebello = (card, deck) => card.suit === COIN[deck] && card.value === 7;
|
||||
// Primiera: la carta migliore di ogni seme; chi non ha tutti e quattro i semi non la fa.
|
||||
export function primiera(cards, deck) {
|
||||
const suits = DECKS[deck].suits;
|
||||
if (!suits.every(s => cards.some(a => a.suit === s))) return 0;
|
||||
return suits.reduce((n, s) => n + Math.max(...cards.filter(a => a.suit === s).map(a => a.prime)), 0);
|
||||
}
|
||||
// I punti di una smazzata: carte (più di 20), mattoni o denari (più di 5), settebello, primiera, una per scopa.
|
||||
export function tally(taken, scope, deck) {
|
||||
const coin = COIN[deck], rows = [], who = (a, b) => a > b ? 'player' : b > a ? 'cpu' : null;
|
||||
const count = w => taken[w].length, coins = w => taken[w].filter(a => a.suit === coin).length, prime = w => primiera(taken[w], deck);
|
||||
rows.push({ key: 'carte', label: 'Carte', player: count('player'), cpu: count('cpu'), winner: who(count('player'), count('cpu')) });
|
||||
rows.push({ key: 'coin', label: coin === 'mattoni' ? 'Mattoni' : 'Denari', player: coins('player'), cpu: coins('cpu'), winner: who(coins('player'), coins('cpu')) });
|
||||
const seven = taken.player.some(a => isSettebello(a, deck)) ? 'player' : 'cpu';
|
||||
rows.push({ key: 'settebello', label: 'Settebello', player: +(seven === 'player'), cpu: +(seven === 'cpu'), winner: seven });
|
||||
rows.push({ key: 'primiera', label: 'Primiera', player: prime('player'), cpu: prime('cpu'), winner: who(prime('player'), prime('cpu')) });
|
||||
const points = { player: 0, cpu: 0 };
|
||||
for (const r of rows) if (r.winner) points[r.winner]++;
|
||||
rows.push({ key: 'scope', label: 'Scope', player: scope.player, cpu: scope.cpu, winner: null });
|
||||
points.player += scope.player; points.cpu += scope.cpu;
|
||||
return { rows, points };
|
||||
}
|
||||
|
||||
function deal(c) {
|
||||
// Con tre o quattro re in tavola si rimescola.
|
||||
let stock;
|
||||
do { c.seed = lcg(c.seed); stock = shuffle([...DECK_CARDS[c.deck]], rng(c.seed)); }
|
||||
while (stock.slice(0, 4).filter(a => a.value === 10).length >= 3);
|
||||
c.table = stock.splice(0, 4); c.stock = stock;
|
||||
c.hands = { player: [], cpu: [] }; c.taken = { player: [], cpu: [] }; c.scope = { player: 0, cpu: 0 };
|
||||
c.current = other(c.dealer); c.lastTaker = null; c.round++;
|
||||
refill(c); c.events.push({ kind: 'card-shuffle' });
|
||||
}
|
||||
// Tre carte a testa, prima a chi gioca per primo.
|
||||
function refill(c) {
|
||||
for (let i = 0; i < 3; i++) for (const who of [other(c.dealer), c.dealer]) c.hands[who].push(c.stock.shift());
|
||||
c.step = 'play'; c.t = 0; c.selected = 0; c.choice = null;
|
||||
}
|
||||
export function newScopa(seed = Date.now() >>> 0, { level = 'seria', deck = 'toscane' } = {}) {
|
||||
const c = { level: LEVELS.includes(level) ? level : 'seria', deck: DECKS[deck] ? deck : 'toscane', score: { player: 0, cpu: 0 },
|
||||
round: 0, seed: seed >>> 0, aiSeed: (seed ^ 0x27d4eb2f) >>> 0, age: 0, events: [], last: null, summary: null };
|
||||
c.dealer = rng(seed)() < 0.5 ? 'player' : 'cpu';
|
||||
deal(c); return c;
|
||||
}
|
||||
export const canPlay = c => c.step === 'play' && c.current === 'player';
|
||||
export function select(c, index) {
|
||||
if (c.step === 'choose' && Number.isInteger(index) && c.choice.options[index]) { c.choice.selected = index; return true; }
|
||||
if (!canPlay(c) || !Number.isInteger(index) || !c.hands.player[index]) return false;
|
||||
c.selected = index; return true;
|
||||
}
|
||||
export function nudge(c, direction) {
|
||||
if (c.step === 'choose') { const n = c.choice.options.length; return select(c, (c.choice.selected + direction + n) % n); }
|
||||
const n = c.hands.player.length;
|
||||
return select(c, (c.selected + direction + n) % n);
|
||||
}
|
||||
// Andrea gioca una carta: se le prese possibili sono più d'una, sceglie lui quale (passo «choose»).
|
||||
export function play(c, index = c.selected, who = 'player', option = null) {
|
||||
if (c.step !== 'play' || c.current !== who || !Number.isInteger(index) || !c.hands[who][index]) return false;
|
||||
const options = captures(c.table, c.hands[who][index]);
|
||||
if (options.length > 1 && option === null) {
|
||||
if (who !== 'player') return false;
|
||||
c.step = 'choose'; c.choice = { index, options, selected: 0 }; return true;
|
||||
}
|
||||
return execute(c, who, index, options.length ? options[option ?? 0] : null);
|
||||
}
|
||||
export function take(c, k = c.choice?.selected) {
|
||||
if (c.step !== 'choose' || !c.choice.options[k]) return false;
|
||||
const { index, options } = c.choice; c.step = 'play'; c.choice = null;
|
||||
return execute(c, 'player', index, options[k]);
|
||||
}
|
||||
export function cancel(c) { if (c.step !== 'choose') return false; c.step = 'play'; c.choice = null; return true; }
|
||||
function execute(c, who, index, captured) {
|
||||
if (!captured && captures(c.table, c.hands[who][index]).length) return false;
|
||||
const [card] = c.hands[who].splice(index, 1);
|
||||
if (captured) {
|
||||
const ids = new Set(captured.map(a => a.id));
|
||||
c.table = c.table.filter(a => !ids.has(a.id)); c.taken[who].push(card, ...captured); c.lastTaker = who;
|
||||
} else c.table.push(card);
|
||||
// L'ultima carta della smazzata non fa scopa.
|
||||
const lastPlay = !c.stock.length && !c.hands.player.length && !c.hands.cpu.length;
|
||||
const scopa = !!captured && !c.table.length && !lastPlay;
|
||||
if (scopa) c.scope[who]++;
|
||||
c.last = { who, card, captured: captured ?? [], scopa };
|
||||
c.events.push({ kind: captured ? 'card-tap' : 'card-slide', who }, ...(scopa ? [{ kind: 'card-busso', who }] : []));
|
||||
c.step = 'take'; c.t = 0;
|
||||
if (who === 'player') c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
||||
return true;
|
||||
}
|
||||
function after(c) {
|
||||
c.current = other(c.last.who); c.t = 0; c.step = 'play';
|
||||
if (c.hands.player.length || c.hands.cpu.length) return;
|
||||
if (c.stock.length) { refill(c); return; }
|
||||
// Fine smazzata: quel che resta in tavola va all'ultimo che ha preso.
|
||||
if (c.lastTaker) c.taken[c.lastTaker].push(...c.table);
|
||||
c.table = [];
|
||||
const t = tally(c.taken, c.scope, c.deck);
|
||||
c.score.player += t.points.player; c.score.cpu += t.points.cpu;
|
||||
c.summary = { ...t, score: { ...c.score } }; c.t = 0;
|
||||
// Il risultato è subito definitivo: uscire dal riepilogo non può cancellare una vittoria.
|
||||
if (finished(c)) { c.step = 'result'; c.result = { winner: c.score.player > c.score.cpu ? 'player' : 'cpu', ...c.score }; }
|
||||
else c.step = 'smazzata';
|
||||
}
|
||||
// Si va agli 11, contati a fine smazzata: vince chi ne ha di più; pari (o nessuno agli 11), se ne fa un'altra.
|
||||
export const finished = c => Math.max(c.score.player, c.score.cpu) >= TARGET && c.score.player !== c.score.cpu;
|
||||
// Dopo il riepilogo (resta finché Andrea non va avanti) si fa la smazzata dopo.
|
||||
export function advance(c) {
|
||||
if (c.step !== 'smazzata') return false;
|
||||
c.dealer = other(c.dealer); deal(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 } };
|
||||
}
|
||||
// Le carte che non si sono viste: in mano all'altro o nel mazzo.
|
||||
export function unseen(v) {
|
||||
const seen = new Set([...v.hand, ...v.table, ...v.taken.player, ...v.taken.cpu].map(a => a.id));
|
||||
return DECK_CARDS[v.deck].filter(a => !seen.has(a.id));
|
||||
}
|
||||
// Quanto vale una mossa: le carte prese (settebello, sette, mattoni, primiera, scopa) meno il rischio di lasciare
|
||||
// una scopa all'altro. «Seria» il rischio lo stima a occhio; «A fassi male» conta quante carte di quel valore
|
||||
// possono essere ancora in mano all'altro.
|
||||
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);
|
||||
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) {
|
||||
let p = 0.5;
|
||||
if (counting) {
|
||||
const pool = unseen(v), same = pool.filter(a => a.value === sum).length;
|
||||
p = pool.length ? 1 - (1 - same / pool.length) ** v.opponentCount : 0;
|
||||
}
|
||||
value -= p * 20;
|
||||
}
|
||||
// Lasciare in tavola un 7 o il settebello vuol dire regalarli.
|
||||
value -= left.filter(a => a.value === 7).length * (counting ? 1.5 : 0) + (left.some(a => isSettebello(a, v.deck)) ? 6 : 0);
|
||||
return value;
|
||||
}
|
||||
export function moves(v) {
|
||||
return v.hand.flatMap((card, index) => {
|
||||
const opts = captures(v.table, card);
|
||||
return opts.length ? opts.map((captured, option) => ({ index, option, captured })) : [{ index, option: null, captured: null }];
|
||||
});
|
||||
}
|
||||
// «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».
|
||||
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';
|
||||
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) {
|
||||
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
||||
c.t += dt;
|
||||
if (c.step === 'take' && c.t >= (c.last.captured.length ? 1.3 : 0.6)) after(c);
|
||||
else if (c.step === 'play' && c.current === 'cpu' && c.t >= 0.75) {
|
||||
c.aiSeed = lcg(c.aiSeed);
|
||||
const m = choose(observation(c, 'cpu'), c.level, c.aiSeed);
|
||||
play(c, m.index, 'cpu', m.option);
|
||||
}
|
||||
}
|
||||
export function status(c, names = { player: 'Andrea', cpu: 'Il giocatore' }) {
|
||||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Hai vinto' : `Vince ${names.cpu}`}, ${c.score.player} a ${c.score.cpu}!`;
|
||||
if (c.step === 'smazzata') return `Smazzata finita: siamo ${c.score.player} a ${c.score.cpu}. Invio per continuare.`;
|
||||
if (c.step === 'choose') return 'Più prese possibili: scegli quale (frecce o tocca una carta in tavola), Invio prende.';
|
||||
if (c.step === 'take') {
|
||||
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…`;
|
||||
}
|
||||
Reference in New Issue
Block a user