La macchina del tempo: dall'Arcu («Bei tempi quando qui si faceva i pokerini…») al Barrettino del 10 luglio 1997 col viaggio sul cruscotto, Vannino e Pierino Parigi, e il pokerino a cinque carte col cambio; l'Arcu in Via Liguri Magelli più grande (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
@@ -12,6 +12,7 @@ import * as scopa from './scopa.js';
|
||||
import * as gara from './gara.js';
|
||||
import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
import * as poker from './poker.js';
|
||||
|
||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||
export const MINIGAMES = {
|
||||
@@ -23,6 +24,7 @@ export const MINIGAMES = {
|
||||
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
||||
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
||||
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||
poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
|
||||
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
||||
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||
@@ -51,6 +53,9 @@ const WALK = ['walk_1', 'walk_2', 'walk_3', 'walk_4'], STEP = 15, REACH = 0.45;
|
||||
// Il primo piano di un libro (story.closeups, effetto `closeup`): copertina → frontespizio → una pagina girata →
|
||||
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
|
||||
export const PAGE_TURN = 0.7, CLOSEUP_PAUSE = 1.4;
|
||||
// I tempi del viaggio nel tempo, in secondi: quando comincia a comparire la data, quanto dura ogni tasto, quando si
|
||||
// carica (tremori e lampi), quando arriva il bianco, quando si chiude.
|
||||
export const TRIP = { type: 0.8, key: 0.16, charge: 3.6, flash: 5.4, end: 6.6 };
|
||||
|
||||
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
|
||||
export function check(cond, state) {
|
||||
@@ -306,6 +311,8 @@ export class Game {
|
||||
if (c.step === 'done') this.queue.unshift(...def.complete);
|
||||
return true;
|
||||
}
|
||||
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
||||
if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; }
|
||||
// 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]) {
|
||||
@@ -336,6 +343,18 @@ export class Game {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Il viaggio nel tempo (story.closeups, type 'viaggio'): la data si compone sul cruscotto una cifra alla volta, poi
|
||||
// tremori, lampi e il bianco; alla fine si chiude da solo e parte `complete` (di solito il `go` nella scena d'arrivo).
|
||||
updateTrip(dt) {
|
||||
const c = this.closeup, def = this.story.closeups[c.id], T = TRIP;
|
||||
c.t += dt;
|
||||
const typed = Math.max(0, Math.min(def.date.length, Math.floor((c.t - T.type) / T.key) + 1));
|
||||
for (; c.typed < typed; c.typed++) if (def.date[c.typed] !== ' ') c.events.push({ kind: 'card-tap', power: 0.7 });
|
||||
if (!c.charge && c.t >= T.charge) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
|
||||
if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'gara-buca', power: 1 }); }
|
||||
if (c.t >= T.end) this.closeupClose();
|
||||
}
|
||||
|
||||
updateTorch(dt) {
|
||||
const c = this.closeup, def = this.story.closeups[c.id];
|
||||
c.t += dt;
|
||||
@@ -453,6 +472,7 @@ export class Game {
|
||||
const c = this.closeup;
|
||||
if (c) {
|
||||
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
||||
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
|
||||
const m = this.minigame();
|
||||
if (m) {
|
||||
m.update(c, dt);
|
||||
@@ -494,6 +514,7 @@ export class Game {
|
||||
else if (e.closeup) {
|
||||
const type = this.story.closeups[e.closeup].type;
|
||||
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
|
||||
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { SEATS, NAMES, MANI, canDraw, myTurn, evaluate, CATEGORIES, red } from './poker.js';
|
||||
import { drawTable, drawCard } from './briscola-render.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
// Le carte di Andrea, in basso: coordinate condivise da disegno e tocco (bersagli larghi anche sul telefono).
|
||||
export const HAND = { x: 150, y: 222, gap: 70, w: 62, h: 90 };
|
||||
export function cardAt(c, [x, y]) {
|
||||
if (!canDraw(c)) return -1;
|
||||
return c.cards.andrea.findIndex((_, i) => x >= HAND.x + i * HAND.gap && x < HAND.x + i * HAND.gap + HAND.w && y >= HAND.y - 14 && y < HAND.y + HAND.h);
|
||||
}
|
||||
const ink = '#2a1f1a', paper = '#fff4dc';
|
||||
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), w, h); }
|
||||
// I semi francesi a pixel (il carattere del gioco non ha ♥♦♣♠): cuore, quadro, fiore, picca, larghi 2·s.
|
||||
function pip(ctx, suit, x, y, s) {
|
||||
const color = suit === 'cuori' || suit === 'quadri' ? '#c0392b' : ink;
|
||||
ctx.fillStyle = color; ctx.beginPath();
|
||||
if (suit === 'quadri') { ctx.moveTo(x, y - s); ctx.lineTo(x + s * 0.75, y); ctx.lineTo(x, y + s); ctx.lineTo(x - s * 0.75, y); }
|
||||
else if (suit === 'cuori') {
|
||||
ctx.moveTo(x, y + s); ctx.lineTo(x - s, y - s * 0.1); ctx.arc(x - s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 0.85, Math.PI * 1.95);
|
||||
ctx.arc(x + s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 1.05, Math.PI * 0.15); ctx.lineTo(x, y + s);
|
||||
} else if (suit === 'picche') {
|
||||
ctx.moveTo(x, y - s); ctx.lineTo(x + s, y + s * 0.15); ctx.arc(x + s * 0.5, y + s * 0.3, s * 0.5, 0, Math.PI * 0.9);
|
||||
ctx.lineTo(x, y + s * 0.2); ctx.arc(x - s * 0.5, y + s * 0.3, s * 0.5, Math.PI * 0.1, Math.PI); ctx.closePath();
|
||||
ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y + s * 0.3); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
|
||||
} else {
|
||||
for (const [dx, dy] of [[0, -0.45], [-0.48, 0.15], [0.48, 0.15]]) { ctx.moveTo(x + dx * s + s * 0.42, y + dy * s); ctx.arc(x + dx * s, y + dy * s, s * 0.42, 0, Math.PI * 2); }
|
||||
ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
|
||||
}
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
// Una carta francese: angolo col valore e il seme, il seme grande in mezzo (le figure con una cornice).
|
||||
export function drawFrench(ctx, card, x, y, w, h, back = false) {
|
||||
if (back) { drawCard(ctx, null, x, y, w, h, true); return; }
|
||||
rect(ctx, x + 2, y + 3, w, h, '#15231d'); rect(ctx, x, y, w, h, ink); rect(ctx, x + 1, y + 1, w - 2, h - 2, paper);
|
||||
const color = red(card) ? '#c0392b' : ink, k = w / 62;
|
||||
text(ctx, card.rank, x + 5 * k, y + 9 * k, Math.round(9 * k), color);
|
||||
pip(ctx, card.suit, x + 9 * k, y + 22 * k, 5 * k);
|
||||
if (['J', 'Q', 'K'].includes(card.rank)) {
|
||||
rect(ctx, x + 14 * k, y + 28 * k, 34 * k, 50 * k, red(card) ? '#f3d6c4' : '#d8dfe6');
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.strokeRect(Math.round(x + 14 * k) + 0.5, Math.round(y + 28 * k) + 0.5, Math.round(34 * k), Math.round(50 * k));
|
||||
text(ctx, card.rank, x + 31 * k, y + 46 * k, Math.round(16 * k), color, 'center');
|
||||
pip(ctx, card.suit, x + 31 * k, y + 66 * k, 7 * k);
|
||||
} else pip(ctx, card.suit, x + 31 * k, y + 52 * k, 14 * k);
|
||||
}
|
||||
// Le fiches del piatto: pile colorate, una ogni cinque.
|
||||
function chips(ctx, n, x, y) {
|
||||
const colors = ['#c0392b', '#2f6fb0', '#e8e0c8', '#3a8a4a'];
|
||||
for (let i = 0; i < Math.min(24, Math.ceil(n / 5)); i++) {
|
||||
const col = i % 6, row = Math.floor(i / 6), cx = x + col * 14 - 35, cy = y - row * 3;
|
||||
rect(ctx, cx, cy, 12, 4, '#1a1410'); rect(ctx, cx, cy - 1, 12, 3, colors[i % colors.length]);
|
||||
}
|
||||
}
|
||||
// Dove siedono: Vannino a sinistra, Pierino a destra, Andrea in basso.
|
||||
const SPOTS = { vannino: { x: 40, y: 70 }, pierino: { x: 420, y: 70 } };
|
||||
|
||||
export function drawPoker(ctx, c) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
drawTable(ctx, 'IL POKERINO DEL BARRETTINO · 10 LUGLIO 1997');
|
||||
const show = c.step === 'show' || c.step === 'result';
|
||||
for (const who of ['vannino', 'pierino']) {
|
||||
const { x, y } = SPOTS[who], out = !c.in?.[who];
|
||||
text(ctx, `${NAMES[who]} · ${c.chips[who]}`, x + 90, y - 22, 7, c.current === who ? '#ffe28a' : '#fff0c8', 'center');
|
||||
c.cards[who].forEach((card, i) => drawFrench(ctx, card, x + i * 36, y - 10, 42, 62, !(show && c.in[who])));
|
||||
if (out && c.step !== 'result') { ctx.fillStyle = 'rgba(20,16,12,0.55)'; ctx.fillRect(x - 2, y - 12, 190, 66); text(ctx, 'lascia', x + 90, y + 22, 7, '#d9ccba', 'center'); }
|
||||
if (show && c.in[who] && c.cards[who].length) text(ctx, CATEGORIES[evaluate(c.cards[who])[0]], x + 90, y + 62, 6, c.winners?.includes(who) ? '#b9e5b3' : '#d9ccba', 'center');
|
||||
if (c.changed?.[who] !== undefined && c.step !== 'show') text(ctx, c.changed[who] ? `cambia ${c.changed[who]}` : 'servito', x + 90, y + 62, 6, '#9fd0ff', 'center');
|
||||
}
|
||||
// Il piatto in mezzo.
|
||||
chips(ctx, c.pot ?? 0, 320, 178);
|
||||
text(ctx, `Piatto ${c.pot ?? 0}`, 320, 192, 7, '#ffe09b', 'center');
|
||||
// Le carte di Andrea: quelle scelte per il cambio si alzano.
|
||||
c.cards.andrea.forEach((card, i) => {
|
||||
const up = c.selected?.has(i) ? -14 : 0;
|
||||
drawFrench(ctx, card, HAND.x + i * HAND.gap, HAND.y + up, HAND.w, HAND.h);
|
||||
if (up) text(ctx, 'cambia', HAND.x + i * HAND.gap + HAND.w / 2, HAND.y + up - 7, 5, '#ffe28a', 'center');
|
||||
});
|
||||
text(ctx, `Andrea · ${c.chips.andrea}`, 320, 210, 7, myTurn(c) || canDraw(c) ? '#ffe28a' : '#fff0c8', 'center');
|
||||
if (c.cards.andrea.length) text(ctx, CATEGORIES[evaluate(c.cards.andrea)[0]], 590, 300, 6, '#d9ccba', 'center');
|
||||
text(ctx, `Mano ${Math.min(c.hand, MANI)}/${MANI}`, 40, 300, 6, '#d9ccba');
|
||||
if (c.line && c.step !== 'result') { ctx.fillStyle = '#17160fd0'; ctx.fillRect(120, 146, 400, 16); text(ctx, c.line, 320, 154, 7, c.step === 'show' ? '#b9e5b3' : '#9fd0ff', 'center'); }
|
||||
if (c.step === 'result') {
|
||||
const r = c.result, title = r.winner === 'player' ? 'HAI VINTO IL POKERINO!' : r.winner === 'draw' ? 'PARI' : `HA VINTO ${NAMES[r.top[0]].toUpperCase()}`;
|
||||
panel(ctx, 150, 110, 340, 84);
|
||||
text(ctx, title, 320, 132, 11, r.winner === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
||||
text(ctx, SEATS.map(s => `${NAMES[s]} ${r.chips[s]}`).join(' · '), 320, 156, 7, '#efd49e', 'center');
|
||||
text(ctx, 'Torna al Barrettino per uscire.', 320, 176, 7, '#d9ccba', 'center');
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Il pokerino del Barrettino, luglio 1997 (idea di Andrea, 29 ottobre): poker a cinque carte coperte col cambio, col
|
||||
// mazzo corto da 32 (dal 7 all'asso, come nei bar), Andrea contro Vannino e Pierino Parigi. Si gioca a fiches.
|
||||
// Col mazzo corto il colore è più raro del full e lo batte; la scala più bassa è A-7-8-9-10.
|
||||
// Una mano: il piatto (ognuno mette l'invito), cinque carte, un giro di puntate, il cambio, un altro giro, chi resta
|
||||
// mostra. Puntate fisse (PUNTO) e al massimo RILANCI rilanci a giro. Dopo MANI mani vince chi ha più fiches.
|
||||
|
||||
export const RANKS = ['7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
||||
export const SUITS = ['cuori', 'quadri', 'fiori', 'picche'];
|
||||
export const SYMBOL = { cuori: '♥', quadri: '♦', fiori: '♣', picche: '♠' };
|
||||
export const NAMES = { andrea: 'Andrea', vannino: 'Vannino', pierino: 'Pierino' };
|
||||
export const SEATS = ['andrea', 'vannino', 'pierino'];
|
||||
export const START = 100, INVITO = 2, PUNTO = 5, RILANCI = 2, MANI = 5, CPU_TIME = 0.9;
|
||||
export const CATEGORIES = ['carta alta', 'coppia', 'doppia coppia', 'tris', 'scala', 'full', 'colore', 'poker', 'scala colore'];
|
||||
// I livelli: quanto ci pensano gli altri (0 = a caso, 1 = col conto giusto) e quanto bluffano.
|
||||
export const LEVELS = {
|
||||
bona: { skill: 0.35, bluff: 0.04 },
|
||||
seria: { skill: 0.7, bluff: 0.08 },
|
||||
male: { skill: 1, bluff: 0.14 },
|
||||
};
|
||||
|
||||
const value = card => RANKS.indexOf(card.rank) + 7;
|
||||
export const cardName = card => `${card.rank}${SYMBOL[card.suit]}`;
|
||||
export const red = card => card.suit === 'cuori' || card.suit === 'quadri';
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
function deck(c) {
|
||||
const cards = SUITS.flatMap(suit => RANKS.map(rank => ({ rank, suit })));
|
||||
for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(random(c) * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; }
|
||||
return cards;
|
||||
}
|
||||
|
||||
// Il punto di una mano: [categoria, poi i valori per gli spareggi], da confrontare in ordine.
|
||||
export function evaluate(hand) {
|
||||
const values = hand.map(value).sort((a, b) => b - a);
|
||||
const counts = new Map();
|
||||
for (const v of values) counts.set(v, (counts.get(v) ?? 0) + 1);
|
||||
const groups = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
|
||||
const flush = hand.every(card => card.suit === hand[0].suit);
|
||||
const distinct = [...counts.keys()].sort((a, b) => b - a);
|
||||
let straight = null;
|
||||
if (distinct.length === 5) {
|
||||
if (distinct[0] - distinct[4] === 4) straight = distinct[0];
|
||||
else if (distinct.join() === '14,10,9,8,7') straight = 10; // A-7-8-9-10: l'asso fa da 6
|
||||
}
|
||||
const shape = groups.map(g => g[1]).join('');
|
||||
const tie = groups.map(g => g[0]);
|
||||
if (straight && flush) return [8, straight];
|
||||
if (shape === '41') return [7, ...tie];
|
||||
if (flush) return [6, ...values];
|
||||
if (shape === '32') return [5, ...tie];
|
||||
if (straight) return [4, straight];
|
||||
if (shape === '311') return [3, ...tie];
|
||||
if (shape === '221') return [2, ...tie];
|
||||
if (shape === '2111') return [1, ...tie];
|
||||
return [0, ...values];
|
||||
}
|
||||
export const compare = (a, b) => { for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) - (b[i] ?? 0); return 0; };
|
||||
export const handName = hand => CATEGORIES[evaluate(hand)[0]];
|
||||
|
||||
export function newPoker(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
const c = { step: 'deal', t: 0, seed, level, skill: LEVELS[level], chips: Object.fromEntries(SEATS.map(s => [s, START])),
|
||||
hand: 0, dealer: 2, events: [], line: null, selected: new Set(), history: [], result: null };
|
||||
newHand(c);
|
||||
return c;
|
||||
}
|
||||
const alive = c => SEATS.filter(s => c.chips[s] >= INVITO);
|
||||
const active = c => SEATS.filter(s => c.in[s]);
|
||||
|
||||
// Una mano nuova: l'invito, il mazzo mischiato, cinque carte a testa; parla per primo chi sta dopo il mazziere.
|
||||
function newHand(c) {
|
||||
c.hand++;
|
||||
c.dealer = (c.dealer + 1) % SEATS.length;
|
||||
c.deck = deck(c);
|
||||
c.in = Object.fromEntries(SEATS.map(s => [s, c.chips[s] >= INVITO]));
|
||||
c.pot = 0;
|
||||
for (const s of active(c)) { c.chips[s] -= INVITO; c.pot += INVITO; }
|
||||
c.cards = Object.fromEntries(SEATS.map(s => [s, c.in[s] ? c.deck.splice(0, 5) : []]));
|
||||
c.changed = {}; c.selected = new Set(); c.winners = null; c.line = null;
|
||||
c.events.push({ kind: 'card-shuffle', power: 1 });
|
||||
startBetting(c, 'bet1');
|
||||
}
|
||||
function startBetting(c, step) {
|
||||
c.step = step; c.t = 0;
|
||||
c.bets = Object.fromEntries(SEATS.map(s => [s, 0]));
|
||||
c.acted = Object.fromEntries(SEATS.map(s => [s, false]));
|
||||
c.raises = 0; c.toBet = 0;
|
||||
c.current = firstAfter(c, c.dealer);
|
||||
skipIfDone(c);
|
||||
}
|
||||
function firstAfter(c, i) {
|
||||
for (let k = 1; k <= SEATS.length; k++) { const s = SEATS[(i + k) % SEATS.length]; if (c.in[s]) return s; }
|
||||
return null;
|
||||
}
|
||||
// Un giro di puntate finisce quando tutti quelli rimasti hanno parlato e messo quanto il più alto.
|
||||
const roundOver = c => active(c).every(s => c.acted[s] && (c.bets[s] === c.toBet || c.chips[s] === 0));
|
||||
function skipIfDone(c) {
|
||||
if (active(c).length === 1) return showdown(c);
|
||||
if (roundOver(c)) return endBetting(c);
|
||||
while (c.current && (!c.in[c.current] || c.chips[c.current] === 0 && c.acted[c.current])) c.current = firstAfter(c, SEATS.indexOf(c.current));
|
||||
}
|
||||
function endBetting(c) {
|
||||
if (c.step === 'bet1') { c.step = 'draw'; c.t = 0; c.current = firstAfter(c, c.dealer); return; }
|
||||
showdown(c);
|
||||
}
|
||||
|
||||
export const toCall = (c, who = 'andrea') => Math.max(0, c.toBet - c.bets[who]);
|
||||
export const myTurn = c => (c.step === 'bet1' || c.step === 'bet2') && c.current === 'andrea' && c.in.andrea;
|
||||
export const canRaise = (c, who = 'andrea') => c.raises < RILANCI && c.chips[who] > toCall(c, who);
|
||||
export const canDraw = c => c.step === 'draw' && c.current === 'andrea';
|
||||
|
||||
// Le mosse: «lascio» (si esce dalla mano), «vedo» (o «parola» se non c'è niente da mettere), «punto»/«rilancio».
|
||||
function put(c, who, amount) {
|
||||
const n = Math.min(amount, c.chips[who]);
|
||||
c.chips[who] -= n; c.bets[who] += n; c.pot += n;
|
||||
if (n) c.events.push({ kind: 'card-tap', power: 0.6 });
|
||||
}
|
||||
export function act(c, who, move) {
|
||||
if (c.current !== who || !(c.step === 'bet1' || c.step === 'bet2')) return false;
|
||||
const due = toCall(c, who);
|
||||
if (move === 'lascio' && due === 0) move = 'vedo'; // lasciare gratis non ha senso: si dice parola
|
||||
if (move === 'rilancio' && !canRaise(c, who)) move = 'vedo';
|
||||
if (move === 'lascio') { c.in[who] = false; c.line = `${NAMES[who]}: lascio.`; }
|
||||
else if (move === 'vedo') { put(c, who, due); c.line = `${NAMES[who]}: ${due ? `vedo (${due})` : 'parola'}.`; }
|
||||
else { put(c, who, due + PUNTO); c.toBet = c.bets[who]; c.raises++; for (const s of SEATS) if (s !== who) c.acted[s] = false; c.line = `${NAMES[who]}: ${due ? 'rilancio' : 'apro'} di ${PUNTO}.`; }
|
||||
c.acted[who] = true;
|
||||
c.history.push({ hand: c.hand, step: c.step, who, move });
|
||||
c.current = firstAfter(c, SEATS.indexOf(who));
|
||||
c.t = 0;
|
||||
skipIfDone(c);
|
||||
return true;
|
||||
}
|
||||
// Il cambio: si scartano le carte scelte (al massimo 4) e se ne pescano altrettante.
|
||||
export function draw(c, who, indexes) {
|
||||
if (c.step !== 'draw' || c.current !== who) return false;
|
||||
const keep = c.cards[who].filter((_, i) => !indexes.includes(i));
|
||||
c.cards[who] = [...keep, ...c.deck.splice(0, 5 - keep.length)];
|
||||
c.changed[who] = 5 - keep.length;
|
||||
c.line = `${NAMES[who]}: ${c.changed[who] ? `cambio ${c.changed[who]}` : 'servito'}.`;
|
||||
if (c.changed[who]) c.events.push({ kind: 'card-slide', power: 0.6 });
|
||||
if (who === 'andrea') c.selected = new Set();
|
||||
c.current = firstAfter(c, SEATS.indexOf(who));
|
||||
c.t = 0;
|
||||
// Il cambio è finito quando tutti quelli rimasti hanno cambiato.
|
||||
if (active(c).every(s => c.changed[s] !== undefined)) startBetting(c, 'bet2');
|
||||
return true;
|
||||
}
|
||||
function showdown(c) {
|
||||
const left = active(c);
|
||||
const best = left.map(s => ({ s, score: evaluate(c.cards[s]) })).sort((a, b) => compare(b.score, a.score));
|
||||
const winners = left.length === 1 ? left : best.filter(b => compare(b.score, best[0].score) === 0).map(b => b.s);
|
||||
const share = Math.floor(c.pot / winners.length);
|
||||
for (const w of winners) c.chips[w] += share;
|
||||
c.chips[winners[0]] += c.pot - share * winners.length;
|
||||
c.winners = winners;
|
||||
c.line = left.length === 1 ? `Tutti lasciano: il piatto (${c.pot}) va a ${NAMES[winners[0]]}.`
|
||||
: `${winners.map(w => NAMES[w]).join(' e ')} ${winners.length > 1 ? 'dividono' : 'vince'} con ${handName(c.cards[winners[0]])}: ${c.pot} fiches.`;
|
||||
c.step = 'show'; c.t = 0; c.current = null;
|
||||
c.events.push({ kind: 'card-shuffle', power: 0.5 });
|
||||
}
|
||||
// Dopo la mostra un tocco (o «Prossima mano») va avanti; finite le mani, o senza più fiches, il risultato.
|
||||
export function advance(c) {
|
||||
if (c.step !== 'show') return false;
|
||||
if (c.hand >= MANI || c.chips.andrea < INVITO || alive(c).length < 2) return finish(c);
|
||||
newHand(c);
|
||||
return true;
|
||||
}
|
||||
function finish(c) {
|
||||
const max = Math.max(...SEATS.map(s => c.chips[s])), top = SEATS.filter(s => c.chips[s] === max);
|
||||
c.step = 'result';
|
||||
c.result = { winner: top.includes('andrea') ? (top.length > 1 ? 'draw' : 'player') : 'cpu', chips: { ...c.chips }, top };
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Gli altri due: quanto vale la mano (0..1), cosa cambiare, come puntare.
|
||||
export function strength(hand) {
|
||||
const [cat, ...rest] = evaluate(hand);
|
||||
return Math.min(1, cat / 8 + ((rest[0] ?? 7) - 7) / 7 / 9);
|
||||
}
|
||||
export function cpuDiscards(c, who) {
|
||||
const hand = c.cards[who], values = hand.map(value), [cat] = evaluate(hand);
|
||||
if (cat >= 4 && cat !== 7) return []; // scala, full, colore, scala colore: servito
|
||||
const counts = new Map(); values.forEach(v => counts.set(v, (counts.get(v) ?? 0) + 1));
|
||||
if (cat >= 1) return hand.map((_, i) => i).filter(i => counts.get(values[i]) === 1 && !(cat === 7)); // tiene le coppie, i tris, il poker
|
||||
const bySuit = SUITS.map(s => hand.map((card, i) => card.suit === s ? i : -1).filter(i => i >= 0)).find(l => l.length === 4);
|
||||
if (bySuit && random(c) < c.skill.skill) return hand.map((_, i) => i).filter(i => !bySuit.includes(i)); // quattro a colore
|
||||
// Altrimenti tiene le due carte più alte (con poca testa, una a caso).
|
||||
const order = hand.map((_, i) => i).sort((a, b) => values[b] - values[a]);
|
||||
return order.slice(random(c) < c.skill.skill ? 2 : 1);
|
||||
}
|
||||
export function cpuMove(c, who) {
|
||||
const s = strength(c.cards[who]), due = toCall(c, who), k = c.skill, noise = (random(c) - 0.5) * (1 - k.skill) * 0.5;
|
||||
const value = s + noise, bluff = random(c) < k.bluff;
|
||||
if (value > 0.42 || bluff) return canRaise(c, who) && (value > 0.55 || bluff) ? 'rilancio' : 'vedo';
|
||||
if (due === 0) return 'vedo';
|
||||
return value > 0.13 + due / 60 ? 'vedo' : 'lascio';
|
||||
}
|
||||
|
||||
export function update(c, dt) {
|
||||
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||
c.t += dt;
|
||||
if (!c.current || c.current === 'andrea' || c.t < CPU_TIME) return;
|
||||
if (c.step === 'bet1' || c.step === 'bet2') act(c, c.current, cpuMove(c, c.current));
|
||||
else if (c.step === 'draw') draw(c, c.current, cpuDiscards(c, c.current));
|
||||
}
|
||||
|
||||
// --- I comandi di Andrea: toccare una carta la segna per il cambio; i pulsanti fanno il resto.
|
||||
export function play(c, index) {
|
||||
if (canDraw(c)) {
|
||||
if (c.selected.has(index)) c.selected.delete(index);
|
||||
else if (c.selected.size < 4) c.selected.add(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export const change = c => canDraw(c) && draw(c, 'andrea', [...c.selected]);
|
||||
export function moves(c) {
|
||||
if (canDraw(c)) return [{ id: 'cambia', label: c.selected.size ? `Cambia ${c.selected.size}` : 'Servito' }];
|
||||
if (!myTurn(c)) return [];
|
||||
const due = toCall(c);
|
||||
return [
|
||||
{ id: 'lascio', label: 'Lascio', disabled: due === 0 },
|
||||
{ id: 'vedo', label: due ? `Vedo ${due}` : 'Parola' },
|
||||
{ id: 'rilancio', label: due ? `Rilancio ${PUNTO}` : `Apro ${PUNTO}`, disabled: !canRaise(c) },
|
||||
];
|
||||
}
|
||||
export function choose(c, id) {
|
||||
if (id === 'cambia') return change(c);
|
||||
return myTurn(c) && act(c, 'andrea', id);
|
||||
}
|
||||
|
||||
export function status(c) {
|
||||
const chips = SEATS.map(s => `${NAMES[s]} ${c.chips[s]}`).join(', ');
|
||||
if (c.step === 'result') return `Fine del pokerino. ${chips}. ${c.result.winner === 'player' ? 'Hai vinto tu!' : c.result.winner === 'draw' ? 'Pari.' : `Ha vinto ${NAMES[c.result.top[0]]}.`}`;
|
||||
const where = `Mano ${c.hand} di ${MANI}, piatto ${c.pot}. ${chips}.`;
|
||||
if (c.step === 'show') return `${where} ${c.line} Tocca per la prossima mano.`;
|
||||
if (canDraw(c)) return `${where} Cambio: tocca le carte da cambiare (al massimo 4), poi Cambia.`;
|
||||
if (myTurn(c)) return `${where} ${c.line ?? ''} Tocca a te: ${toCall(c) ? `per vedere servono ${toCall(c)}` : 'parola o apri'}.`.replace(' ', ' ');
|
||||
return `${where} ${c.line ?? ''} ${c.current ? `Parla ${NAMES[c.current]}…` : ''}`.trim();
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import { drawPesca } from './pesca-render.js';
|
||||
import { drawGara } from './gara-render.js';
|
||||
import { drawMicro } from './micro-render.js';
|
||||
import { drawCerbottana } from './cerbottana-render.js';
|
||||
import { drawPoker } from './poker-render.js';
|
||||
import { drawViaggio } from './viaggio.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { drawScopa } from './scopa-render.js';
|
||||
@@ -678,6 +680,11 @@ function drawCloseup(ctx, game, time) {
|
||||
if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; }
|
||||
if (def.type === 'tressette') { drawTressette(ctx, c, def); return; }
|
||||
if (def.type === 'scopa') { drawScopa(ctx, c, def); return; }
|
||||
if (def.type === 'viaggio') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawViaggio(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
if (def.type === 'torch') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||
@@ -688,6 +695,7 @@ function drawCloseup(ctx, game, time) {
|
||||
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
||||
return;
|
||||
}
|
||||
if (def.type === 'poker') { drawPoker(ctx, c); return; }
|
||||
if (def.type === 'cerbottana') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Il disegno del viaggio nel tempo (story.closeups, type 'viaggio'; i tempi stanno in game.js, TRIP): il cruscotto,
|
||||
// la data che si compone nel display con le cifre a segmenti che brillano, poi tremori, lampi e il bianco.
|
||||
import { TRIP } from './game.js';
|
||||
|
||||
// Le cifre a sette segmenti: quali segmenti sono accesi (a b c d e f g).
|
||||
const SEG = { 0: 'abcdef', 1: 'bc', 2: 'abged', 3: 'abgcd', 4: 'fgbc', 5: 'afgcd', 6: 'afgedc', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg', '/': '', ':': '', ' ': '' };
|
||||
function digit(ctx, ch, x, y, w, h, color) {
|
||||
const t = Math.max(2, Math.round(w / 6)), on = SEG[ch] ?? '';
|
||||
const bars = { a: [x + t, y, w - 2 * t, t], b: [x + w - t, y + t, t, h / 2 - t], c: [x + w - t, y + h / 2, t, h / 2 - t],
|
||||
d: [x + t, y + h - t, w - 2 * t, t], e: [x, y + h / 2, t, h / 2 - t], f: [x, y + t, t, h / 2 - t], g: [x + t, y + h / 2 - t / 2, w - 2 * t, t] };
|
||||
for (const [k, r] of Object.entries(bars)) { ctx.fillStyle = on.includes(k) ? color : 'rgba(255,90,40,0.08)'; ctx.fillRect(...r.map(Math.round)); }
|
||||
if (ch === ':') { ctx.fillStyle = color; ctx.fillRect(x + w / 2 - t / 2, y + h * 0.3, t, t); ctx.fillRect(x + w / 2 - t / 2, y + h * 0.65, t, t); }
|
||||
if (ch === '/') { ctx.fillStyle = color; for (let i = 0; i < 6; i++) ctx.fillRect(x + w * 0.7 - i * w * 0.08, y + h * 0.15 + i * h * 0.13, t, t); }
|
||||
}
|
||||
export function drawViaggio(ctx, c, def, bg) {
|
||||
const t = c.t, T = TRIP, [dx, dy, dw, dh] = def.display;
|
||||
// Tremori: crescono dalla carica al bianco.
|
||||
const shake = t > T.charge ? Math.min(1, (t - T.charge) / (T.flash - T.charge)) * 5 : 0;
|
||||
ctx.save();
|
||||
ctx.translate(Math.round((Math.sin(t * 61) + Math.sin(t * 37)) * shake), Math.round(Math.cos(t * 53) * shake));
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 640, 360); }
|
||||
// La data, un tasto alla volta, in due righe: giorno/mese/anno e ora.
|
||||
const lines = def.date.split(' '), shown = c.typed ?? 0;
|
||||
let n = 0;
|
||||
const glow = t > T.charge ? 0.75 + 0.25 * Math.sin(t * 30) : 1;
|
||||
ctx.shadowColor = '#ff5a28'; ctx.shadowBlur = 8;
|
||||
lines.forEach((line, row) => {
|
||||
const ch = 14, cw = 13, gap = 4, total = line.length * (cw + gap) - gap;
|
||||
const x0 = dx + (dw - total) / 2, y0 = dy + 8 + row * (ch * 2 + 2);
|
||||
[...line].forEach((d, i) => {
|
||||
const lit = n++ < shown;
|
||||
digit(ctx, lit ? d : ' ', x0 + i * (cw + gap), y0, cw, ch * 2 - 6, `rgba(255,${Math.round(110 * glow)},40,${lit ? 1 : 0})`);
|
||||
});
|
||||
n++; // lo spazio fra le due righe
|
||||
});
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.restore();
|
||||
// I lampi: archi azzurri che attraversano il parabrezza, a scatti.
|
||||
if (t > T.charge + 0.6) {
|
||||
const k = Math.floor(t * 14);
|
||||
if (k % 3 !== 0) {
|
||||
ctx.strokeStyle = 'rgba(160,220,255,0.9)'; ctx.lineWidth = 2; ctx.beginPath();
|
||||
let x = 40 + (k * 97) % 560, y = 0; ctx.moveTo(x, y);
|
||||
for (let i = 0; i < 7; i++) { x += ((k * (i + 3) * 37) % 50) - 25; y += 18; ctx.lineTo(x, y); }
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(180,230,255,0.12)'; ctx.fillRect(0, 0, 640, 360);
|
||||
}
|
||||
}
|
||||
// Il bianco, poi resta bianco fino alla fine.
|
||||
if (t > T.flash) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, (t - T.flash) / 0.5)})`; ctx.fillRect(0, 0, 640, 360); }
|
||||
}
|
||||
Reference in New Issue
Block a user