Briscola all'oratorio: alle tavolate il giocatore di carte (segnaposto, sprite di Codex) sfida Andrea a briscola in due. Si sceglie il mazzo, toscane (cuori, mattoni, fiori, picche; gobbo, donna e regio) o napoletane, e il livello: «Alla bona», «Seria», «A fassi male» (conta le carte uscite, finale perfetto a mazzo finito, mai la mano altrui). Regole fedeli, conteggio delle prese a fine partita, rivincita, suoni di carte, comandi con mouse, tocco e tastiera. Carte e dorso col Regolo serpentello disegnati da Codex; motore e prove nel browser rivisti da Claude
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,295 @@
|
||||
import { canPlay, cardName, counted, countRows } from './briscola.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
// Coordinate condivise da disegno e hit test: bersagli larghi anche sul telefono.
|
||||
export const HAND = { x: 176, y: 230, gap: 102, w: 84, h: 120 };
|
||||
export function cardAt(c, [x, y]) {
|
||||
if (!canPlay(c)) return -1;
|
||||
return c.hands.player.findIndex((_, i) => x >= HAND.x + i * HAND.gap && x < HAND.x + i * HAND.gap + HAND.w && y >= HAND.y && y < HAND.y + HAND.h);
|
||||
}
|
||||
const ink = '#38271f', paper = '#fff0cd', gold = '#dba32b', red = '#aa4034', blue = '#386a89', green = '#487348';
|
||||
function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(x, y, w, h); }
|
||||
function poly(ctx, points, color) {
|
||||
ctx.fillStyle = color; ctx.beginPath(); points.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath(); ctx.fill();
|
||||
}
|
||||
// Simboli originali: moneta incisa, calice, lama ricurva e bastone coi nodi.
|
||||
function suit(ctx, name, x, y) {
|
||||
ctx.save(); ctx.translate(x, y);
|
||||
if (name === 'denari') {
|
||||
rect(ctx, -4, -3, 8, 6, ink); rect(ctx, -3, -4, 6, 8, ink);
|
||||
rect(ctx, -3, -2, 6, 4, gold); rect(ctx, -2, -3, 4, 6, gold);
|
||||
rect(ctx, -2, -2, 1, 4, '#ffe58c'); rect(ctx, 0, -1, 2, 2, '#9e6b25');
|
||||
} else if (name === 'coppe') {
|
||||
rect(ctx, -4, -4, 8, 2, ink); rect(ctx, -3, -2, 6, 3, gold);
|
||||
rect(ctx, -2, 0, 4, 2, red); rect(ctx, -1, 2, 2, 2, ink);
|
||||
rect(ctx, -3, 4, 6, 1, gold); rect(ctx, -3, -3, 6, 1, '#ffdd70');
|
||||
} else if (name === 'spade') {
|
||||
poly(ctx, [[-4,5],[-3,1],[0,-1],[2,-5],[4,-6],[3,-1],[-1,3],[-2,6]], ink);
|
||||
rect(ctx, 1, -3, 2, 3, '#9eb3ba'); rect(ctx, -1, 0, 2, 2, '#c9d8d6');
|
||||
rect(ctx, -5, 3, 5, 1, gold); rect(ctx, -4, 5, 2, 2, red);
|
||||
} else {
|
||||
poly(ctx, [[-3,6],[-4,4],[-1,-1],[0,-6],[3,-5],[3,-2],[0,3],[-1,6]], ink);
|
||||
rect(ctx, 0, -4, 2, 5, '#94632e'); rect(ctx, -2, 1, 2, 4, '#ae7a39');
|
||||
rect(ctx, 2, -3, 2, 2, gold); rect(ctx, -3, 2, 2, 2, gold); rect(ctx, 3, -5, 2, 1, green);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function regolo(ctx) {
|
||||
// Cornice simmetrica e un piccolo re sorridente, avvolto nelle proprie spire.
|
||||
rect(ctx, 3, 3, 36, 54, blue); rect(ctx, 5, 5, 32, 50, gold); rect(ctx, 6, 6, 30, 48, '#203e44');
|
||||
for (let y = 9; y < 54; y += 6) for (const x of [8, 32]) rect(ctx, x, y, 2, 2, '#63867c');
|
||||
poly(ctx, [[15,32],[28,32],[32,36],[32,43],[28,47],[14,47],[10,43],[10,37]], ink);
|
||||
poly(ctx, [[15,34],[27,34],[30,37],[30,42],[27,45],[15,45],[12,42],[12,38]], green);
|
||||
poly(ctx, [[17,37],[26,37],[28,39],[27,41],[16,41],[15,39]], '#203e44');
|
||||
rect(ctx, 14, 42, 13, 2, '#a3bc72'); rect(ctx, 16, 36, 11, 1, '#a3bc72');
|
||||
poly(ctx, [[19,25],[25,25],[25,33],[23,36],[18,36],[17,33],[20,32]], green);
|
||||
rect(ctx, 20, 28, 2, 6, '#d0cd83');
|
||||
poly(ctx, [[15,16],[27,16],[30,19],[30,25],[27,29],[15,29],[12,25],[12,20]], ink);
|
||||
poly(ctx, [[16,17],[26,17],[29,20],[29,24],[26,27],[16,27],[13,24],[13,21]], '#94b365');
|
||||
rect(ctx, 15, 19, 4, 4, paper); rect(ctx, 23, 19, 4, 4, paper);
|
||||
rect(ctx, 17, 20, 2, 3, ink); rect(ctx, 23, 20, 2, 3, ink);
|
||||
rect(ctx, 18, 25, 6, 1, ink); rect(ctx, 16, 24, 2, 1, ink); rect(ctx, 24, 24, 2, 1, ink);
|
||||
rect(ctx, 17, 12, 9, 4, gold); for (const x of [17, 21, 25]) rect(ctx, x, 9, 1, 4, '#ffe396');
|
||||
rect(ctx, 21, 13, 1, 2, red);
|
||||
for (const x of [15, 25]) { rect(ctx, x, 50, 2, 2, gold); rect(ctx, x + 1, 49, 1, 4, gold); }
|
||||
}
|
||||
function figure(ctx, rank, name) {
|
||||
// Figure intere: fante in tunica, cavaliere a cavallo, re con corona e manto.
|
||||
const king = rank === 'Re', horse = rank === 'Cavallo', y = horse ? 15 : 17;
|
||||
if (horse) {
|
||||
poly(ctx, [[8,36],[12,29],[27,30],[30,23],[35,23],[37,29],[32,33],[31,41],[12,41]], ink);
|
||||
rect(ctx, 11, 33, 20, 7, '#b78044'); rect(ctx, 30, 26, 4, 8, '#c79c60');
|
||||
rect(ctx, 12, 40, 3, 10, ink); rect(ctx, 27, 39, 3, 11, ink);
|
||||
rect(ctx, 15, 40, 2, 8, '#a36b3c'); rect(ctx, 32, 27, 1, 1, paper);
|
||||
}
|
||||
if (king) poly(ctx, [[15,24],[29,24],[33,48],[10,48]], red);
|
||||
poly(ctx, [[19,y],[25,y],[27,y+2],[28,y+4],[26,y+5],[26,y+8],[20,y+9],[17,y+6],[17,y+2]], ink);
|
||||
poly(ctx, [[20,y+1],[25,y+1],[25,y+3],[27,y+4],[25,y+5],[25,y+7],[21,y+8],[19,y+5]], '#eab578');
|
||||
rect(ctx, 18, y + 1, 2, 4, '#86502d'); rect(ctx, 20, y + 1, 4, 1, '#ffe0a2');
|
||||
rect(ctx, 24, y + 2, 1, 2, ink); rect(ctx, 24, y + 6, 3, 1, ink);
|
||||
rect(ctx, 17, y - 2, 10, 3, king ? gold : blue);
|
||||
if (king) for (const x of [17, 21, 25]) rect(ctx, x, y - 5, 2, 4, gold);
|
||||
else rect(ctx, 18, y - 5, 2, 4, red);
|
||||
poly(ctx, [[18,y+9],[26,y+9],[29,y+12],[27,y+19],[29,y+24],[15,y+24],[17,y+19],[15,y+12]], ink);
|
||||
poly(ctx, [[19,y+10],[25,y+10],[27,y+12],[25,y+19],[27,y+22],[17,y+22],[19,y+18],[17,y+12]], horse ? red : blue);
|
||||
rect(ctx, 18, y + 10, 3, 10, king ? gold : '#6291a5'); rect(ctx, 17, y + 19, 11, 2, gold);
|
||||
rect(ctx, 23, y + 11, 1, 6, paper); rect(ctx, 23, y + 19, 2, 2, ink);
|
||||
rect(ctx, 12, y + 11, 4, 3, '#eab578'); rect(ctx, 28, y + 10, 4, 4, '#eab578');
|
||||
if (!horse) {
|
||||
poly(ctx, [[17,y+24],[21,y+24],[20,y+32],[16,y+32]], ink);
|
||||
poly(ctx, [[23,y+24],[27,y+24],[29,y+32],[25,y+32]], ink);
|
||||
rect(ctx, 18, y + 24, 2, 5, '#b9884f'); rect(ctx, 24, y + 24, 2, 4, '#b9884f');
|
||||
rect(ctx, 15, y + 32, 6, 2, red); rect(ctx, 24, y + 32, 6, 2, red);
|
||||
}
|
||||
suit(ctx, name, 10, 24);
|
||||
if (king) { rect(ctx, 32, 22, 1, 24, gold); rect(ctx, 31, 20, 3, 3, gold); }
|
||||
}
|
||||
const PIPS = {
|
||||
Asso: [[21,32]], 2: [[21,22],[21,42]], 3: [[21,20],[21,32],[21,44]],
|
||||
4: [[13,23],[29,23],[13,41],[29,41]],
|
||||
5: [[13,21],[29,21],[21,32],[13,43],[29,43]],
|
||||
6: [[13,20],[29,20],[13,32],[29,32],[13,44],[29,44]],
|
||||
7: [[13,19],[29,19],[13,32],[29,32],[13,45],[29,45],[21,32]],
|
||||
};
|
||||
// Le toscane hanno semi francesi, nessun indice sulle numerali e figure intere.
|
||||
const TUSCAN_COLORS = {
|
||||
cuori: { cloth: '#326447', light: '#65936a', cloak: '#ab393b', ground: '#bc7760' },
|
||||
mattoni: { cloth: '#a93832', light: '#d56a46', cloak: '#315f7e', ground: '#b3a05b' },
|
||||
fiori: { cloth: '#315e85', light: '#6995ac', cloak: '#bc842e', ground: '#829968' },
|
||||
picche: { cloth: '#755079', light: '#ac7d9a', cloak: '#376b66', ground: '#9e8271' },
|
||||
};
|
||||
const TUSCAN_PIPS = {
|
||||
Asso: [[21,30]], 2: [[21,17],[21,43]], 3: [[21,15],[21,30],[21,45]],
|
||||
4: [[12,16],[30,16],[12,44],[30,44]],
|
||||
5: [[12,16],[30,16],[21,30],[12,44],[30,44]],
|
||||
6: [[12,15],[30,15],[12,30],[30,30],[12,45],[30,45]],
|
||||
7: [[10,14],[32,14],[21,22],[10,30],[32,30],[10,46],[32,46]],
|
||||
};
|
||||
function tuscanSuit(ctx, name, x, y, inverted = false) {
|
||||
ctx.save(); ctx.translate(x, y);
|
||||
if (inverted) ctx.scale(1, -1);
|
||||
const color = name === 'cuori' || name === 'mattoni' ? '#c12f35' : '#202326';
|
||||
// Sagome a gradini sulla griglia: nessuna sfumatura nei semi.
|
||||
if (name === 'cuori') {
|
||||
poly(ctx, [[-4,-4],[-1,-4],[0,-3],[1,-4],[4,-4],[5,-3],[5,0],[4,1],[4,2],[3,2],[3,3],[2,3],[2,4],[1,4],[1,5],[0,6],[-1,5],[-1,4],[-2,4],[-2,3],[-3,3],[-3,2],[-4,2],[-4,1],[-5,0],[-5,-3]], color);
|
||||
} else if (name === 'mattoni') {
|
||||
poly(ctx, [[0,-6],[1,-4],[2,-3],[3,-2],[5,0],[3,2],[2,3],[1,4],[0,6],[-1,4],[-2,3],[-3,2],[-5,0],[-3,-2],[-2,-3],[-1,-4]], color);
|
||||
} else if (name === 'fiori') {
|
||||
poly(ctx, [[-2,-5],[2,-5],[3,-4],[3,-2],[2,-1],[4,-2],[6,0],[6,2],[4,4],[2,4],[1,3],[1,4],[3,6],[-3,6],[-1,4],[-1,3],[-2,4],[-4,4],[-6,2],[-6,0],[-4,-2],[-2,-1],[-3,-2],[-3,-4]], color);
|
||||
} else {
|
||||
poly(ctx, [[0,-6],[1,-5],[1,-4],[3,-2],[5,0],[5,3],[3,4],[1,3],[1,4],[3,6],[-3,6],[-1,4],[-1,3],[-3,4],[-5,3],[-5,0],[-3,-2],[-1,-4],[-1,-5]], color);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function tuscanFigure(ctx, rank, name) {
|
||||
const p = TUSCAN_COLORS[name], skin = '#e9b785', shade = '#bf805d', trim = '#e0b957';
|
||||
const queen = rank === 'Donna', king = rank === 'Regio';
|
||||
// Cornice sottile, terreno e silhouette originali, disegnati su una griglia 42×60.
|
||||
rect(ctx, 3, 3, 36, 54, '#b6a48a'); rect(ctx, 3.5, 3.5, 35, 53, paper);
|
||||
poly(ctx, [[4,51],[14,49],[26,50],[38,48],[38,56],[4,56]], p.ground);
|
||||
if (queen) {
|
||||
// Dama di profilo: diadema, manica a sbuffo, gonna a ventaglio e fazzoletto.
|
||||
poly(ctx, [[21,11],[27,11],[29,14],[28,22],[31,26],[27,29],[19,24],[18,18]], ink);
|
||||
poly(ctx, [[22,12],[26,12],[27,15],[25,19],[22,21],[20,18],[19,17],[21,16]], skin);
|
||||
rect(ctx, 23, 12, 4, 2, '#70452c'); rect(ctx, 26, 14, 2, 7, '#70452c');
|
||||
rect(ctx, 21, 15, 1, 1, ink); rect(ctx, 20, 18, 2, 1, shade);
|
||||
poly(ctx, [[21,12],[21,10],[23,11],[24,9],[25,11],[27,10],[27,13]], trim);
|
||||
rect(ctx, 24, 10, 1, 1, '#c12f35');
|
||||
poly(ctx, [[21,21],[26,20],[30,24],[28,31],[31,39],[37,54],[12,54],[15,43],[18,33],[17,26]], ink);
|
||||
poly(ctx, [[22,22],[26,21],[28,24],[26,31],[29,39],[35,53],[14,53],[17,42],[20,32],[19,26]], p.cloth);
|
||||
poly(ctx, [[21,32],[25,32],[25,40],[31,52],[23,51],[19,46]], p.light);
|
||||
poly(ctx, [[26,34],[29,41],[33,51],[30,48],[27,43]], trim);
|
||||
poly(ctx, [[19,35],[18,45],[16,52],[20,52],[21,49],[19,47]], p.cloak);
|
||||
poly(ctx, [[18,23],[22,25],[20,29],[16,31],[12,29],[13,26]], '#d8dfd7');
|
||||
rect(ctx, 17, 25, 4, 2, '#94a7ad');
|
||||
poly(ctx, [[14,28],[16,30],[12,33],[9,32],[10,30]], skin);
|
||||
poly(ctx, [[9,31],[12,32],[10,35],[8,35],[6,40],[6,35]], '#fcf7df');
|
||||
rect(ctx, 8, 34, 1, 3, '#94a7ad');
|
||||
poly(ctx, [[20,29],[26,29],[27,31],[20,33],[18,32]], trim);
|
||||
rect(ctx, 22, 23, 1, 3, trim); rect(ctx, 16, 54, 7, 1, ink);
|
||||
} else if (king) {
|
||||
// Regio anziano: corona sempre visibile, barba bianca e bordi d'ermellino.
|
||||
poly(ctx, [[15,23],[27,22],[32,27],[36,54],[9,54],[11,31]], ink);
|
||||
poly(ctx, [[15,24],[27,23],[30,28],[34,53],[11,53],[13,31]], p.cloak);
|
||||
poly(ctx, [[18,27],[25,27],[27,52],[16,52]], p.cloth);
|
||||
rect(ctx, 20, 33, 3, 18, p.light); rect(ctx, 16, 48, 11, 2, trim);
|
||||
poly(ctx, [[15,24],[18,25],[16,34],[15,45],[12,52],[11,51],[13,33]], '#f6edcf');
|
||||
poly(ctx, [[26,24],[28,25],[29,35],[32,51],[29,52],[26,35],[24,27]], '#f6edcf');
|
||||
for (const [x,y] of [[14,29],[13,37],[12,46],[27,29],[28,38],[30,47]]) rect(ctx, x, y, 1, 2, ink);
|
||||
poly(ctx, [[17,13],[24,12],[27,16],[26,23],[23,28],[18,26],[15,21],[15,16]], '#b9bfbb');
|
||||
poly(ctx, [[18,14],[24,14],[25,17],[27,19],[24,20],[24,22],[19,22],[17,18]], skin);
|
||||
rect(ctx, 23, 16, 1, 2, ink); rect(ctx, 18, 16, 1, 2, shade);
|
||||
poly(ctx, [[16,18],[19,21],[22,20],[25,21],[24,25],[21,28],[18,25],[16,23]], '#f7f0d7');
|
||||
rect(ctx, 21, 22, 3, 1, '#9ba8a3'); rect(ctx, 18, 22, 1, 3, '#c6cdc0');
|
||||
poly(ctx, [[16,14],[15,8],[17,8],[18,11],[20,10],[20,7],[22,7],[22,10],[24,11],[25,8],[27,8],[26,14]], ink);
|
||||
poly(ctx, [[17,13],[16,9],[17,9],[18,12],[21,11],[21,8],[22,12],[25,12],[26,9],[25,13]], trim);
|
||||
rect(ctx, 17, 13, 9, 2, trim); rect(ctx, 21, 13, 2, 1, '#c12f35');
|
||||
rect(ctx, 33, 23, 1, 30, '#9b6a29'); rect(ctx, 33, 24, 1, 27, trim);
|
||||
poly(ctx, [[33,19],[35,21],[35,23],[33,25],[31,23],[31,21]], trim);
|
||||
rect(ctx, 33, 21, 1, 2, '#fff1ae'); rect(ctx, 31, 32, 4, 3, skin);
|
||||
rect(ctx, 10, 33, 4, 3, skin); rect(ctx, 15, 53, 6, 2, ink); rect(ctx, 24, 53, 6, 2, ink);
|
||||
} else {
|
||||
// Gobbo giovane, senza corona: berretto morbido, mantello corto e mazza.
|
||||
poly(ctx, [[15,23],[26,22],[31,27],[33,39],[27,43],[11,40],[10,28]], ink);
|
||||
poly(ctx, [[15,24],[25,23],[29,27],[31,38],[27,41],[12,39],[12,28]], p.cloak);
|
||||
poly(ctx, [[18,37],[23,38],[21,48],[20,53],[15,53],[17,46]], ink);
|
||||
poly(ctx, [[24,37],[28,37],[28,46],[31,53],[26,53],[23,46]], ink);
|
||||
rect(ctx, 18, 41, 2, 9, '#d3ad60'); rect(ctx, 25, 41, 2, 6, '#d3ad60');
|
||||
rect(ctx, 16, 50, 4, 4, '#744531'); rect(ctx, 27, 50, 3, 4, '#744531');
|
||||
rect(ctx, 13, 54, 7, 1, ink); rect(ctx, 26, 54, 7, 1, ink);
|
||||
poly(ctx, [[17,12],[24,12],[27,16],[26,23],[22,25],[17,22],[15,18]], '#70452c');
|
||||
poly(ctx, [[19,14],[24,14],[24,16],[26,18],[24,19],[24,21],[21,23],[18,20]], skin);
|
||||
rect(ctx, 23, 16, 1, 2, ink); rect(ctx, 23, 20, 2, 1, shade);
|
||||
rect(ctx, 17, 15, 2, 7, '#70452c'); rect(ctx, 16, 19, 2, 5, '#70452c');
|
||||
poly(ctx, [[16,14],[17,11],[21,10],[25,11],[26,14],[28,15],[16,15]], p.cloth);
|
||||
rect(ctx, 18, 13, 8, 1, p.light);
|
||||
poly(ctx, [[18,24],[24,23],[27,27],[25,33],[28,40],[15,40],[17,33],[15,27]], ink);
|
||||
poly(ctx, [[19,25],[23,24],[25,27],[23,33],[26,39],[17,39],[19,33],[17,27]], p.cloth);
|
||||
rect(ctx, 20, 26, 2, 7, p.light); rect(ctx, 16, 34, 10, 2, trim);
|
||||
poly(ctx, [[18,23],[21,25],[20,29],[17,25]], '#f6edcf');
|
||||
rect(ctx, 21, 34, 2, 2, ink);
|
||||
poly(ctx, [[10,27],[13,29],[12,33],[9,34],[8,32]], skin);
|
||||
poly(ctx, [[27,28],[30,30],[28,34],[25,35],[24,33],[27,32]], skin);
|
||||
poly(ctx, [[7,30],[9,30],[9,46],[11,50],[11,53],[6,53],[6,49],[7,46]], ink);
|
||||
rect(ctx, 8, 33, 1, 17, '#ae783a'); rect(ctx, 7, 49, 3, 3, trim);
|
||||
rect(ctx, 7, 31, 3, 2, skin);
|
||||
}
|
||||
ctx.save(); ctx.translate(9, 8); ctx.scale(0.65, 0.65); tuscanSuit(ctx, name, 0, 0); ctx.restore();
|
||||
text(ctx, queen ? 'D' : king ? 'R' : 'G', 7, 16, 4, ink);
|
||||
}
|
||||
function tuscanCard(ctx, card) {
|
||||
// Angoli arrotondati a gradini e un solo bordo sottile; nessuna scritta al piede.
|
||||
const outline = [[2,0],[40,0],[42,2],[42,58],[40,60],[2,60],[0,58],[0,2]];
|
||||
ctx.save(); ctx.translate(1, 2); poly(ctx, outline, '#15231d'); ctx.restore();
|
||||
poly(ctx, outline, '#89765f');
|
||||
poly(ctx, [[2,1],[40,1],[41,2],[41,58],[40,59],[2,59],[1,58],[1,2]], paper);
|
||||
if (TUSCAN_PIPS[card.rank]) {
|
||||
for (const [x, y] of TUSCAN_PIPS[card.rank]) tuscanSuit(ctx, card.suit, x, y, y > 30);
|
||||
} else tuscanFigure(ctx, card.rank, card.suit);
|
||||
}
|
||||
export function drawCard(ctx, card, x, y, w = 84, h = 120, back = false) {
|
||||
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.scale(w / 42, h / 60);
|
||||
if (!back && TUSCAN_COLORS[card.suit]) { tuscanCard(ctx, card); ctx.restore(); return; }
|
||||
rect(ctx, 1, 2, 42, 60, '#15231d'); rect(ctx, 0, 0, 42, 60, ink);
|
||||
rect(ctx, 1, 1, 40, 58, '#c5a576'); rect(ctx, 2, 2, 38, 56, paper);
|
||||
if (back) regolo(ctx);
|
||||
else {
|
||||
const rank = { Asso: 'A', Fante: 'F', Cavallo: 'C', Re: 'R', Gobbo: 'G', Donna: 'D', Regio: 'R' }[card.rank] ?? card.rank;
|
||||
text(ctx, rank, 4, 7, 6, ink); suit(ctx, card.suit, 34, 8);
|
||||
if (PIPS[card.rank]) {
|
||||
for (const [px, py] of PIPS[card.rank]) {
|
||||
// Una lama per simbolo: le coppie si allungano e s'incrociano al centro.
|
||||
if (card.suit === 'spade' && (px === 13 || px === 29)) {
|
||||
ctx.save(); ctx.translate(px === 13 ? 18 : 24, py); ctx.scale(px === 13 ? 1.8 : -1.8, 1);
|
||||
suit(ctx, card.suit, 0, 0); ctx.restore();
|
||||
} else suit(ctx, card.suit, px, py);
|
||||
}
|
||||
} else figure(ctx, card.rank, card.suit);
|
||||
text(ctx, card.suit.toUpperCase(), 21, 55, 3.7, ink, 'center');
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function countTable(ctx, c, names) {
|
||||
panel(ctx, 12, 32, 616, 318);
|
||||
const rows = countRows(c), shown = rows.slice(0, c.count), totals = counted(c);
|
||||
for (const [side, who] of ['player', 'cpu'].entries()) {
|
||||
const x = 25 + side * 310;
|
||||
text(ctx, names[who], x, 48, 7);
|
||||
const cards = shown.filter(p => p.who === who);
|
||||
cards.forEach(({ card }, i) => {
|
||||
const cx = x + (i % 8) * 36, cy = 64 + Math.floor(i / 8) * 49;
|
||||
drawCard(ctx, card, cx, cy, 30, 40);
|
||||
text(ctx, '+' + card.points, cx + 15, cy + 44, 5, '#ffe09b', 'center');
|
||||
});
|
||||
text(ctx, `${totals[who]} punti`, x, 321, 10, '#ffe09b');
|
||||
}
|
||||
const last = shown.at(-1);
|
||||
if (c.step === 'count' && last) {
|
||||
panel(ctx, 270, 115, 100, 140); drawCard(ctx, last.card, 278, 123);
|
||||
}
|
||||
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;
|
||||
rect(ctx, 0, 0, 640, 360, '#573720');
|
||||
for (let y = 0; y < 360; y += 24) {
|
||||
rect(ctx, 0, y, 640, 2, '#38271e');
|
||||
for (let x = 0; x < 640; x += 53) rect(ctx, x + y % 17, y + 6, 29, 1, '#72462a');
|
||||
}
|
||||
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');
|
||||
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));
|
||||
// La briscola sporge sotto il mazzo, girata di traverso: più di metà resta scoperta, anche l'asso che ha
|
||||
// un seme solo nel mezzo (le toscane non hanno numeri negli angoli).
|
||||
if (c.stock.length) {
|
||||
ctx.save(); ctx.translate(154, 150); ctx.rotate(Math.PI / 2);
|
||||
drawCard(ctx, c.trumpCard, 0, 0, 64, 92); ctx.restore();
|
||||
if (c.stock.length > 1) {
|
||||
for (let i = Math.min(5, Math.ceil(c.stock.length / 7)); i >= 0; i--)
|
||||
drawCard(ctx, null, 38 + i, 116 - i, 58, 83, true);
|
||||
}
|
||||
}
|
||||
text(ctx, `Mazzo: ${c.stock.length}`, 34, 243, 7);
|
||||
text(ctx, 'Briscola:', 34, 263, 7); // a 6 punti la B sembra una D
|
||||
text(ctx, c.trump.toUpperCase(), 34, 278, 7, '#ffe09b');
|
||||
for (const [i, who] of ['cpu', 'player'].entries()) {
|
||||
const y = 74 + i * 183;
|
||||
if (c.taken[who].length) for (let n = 0; n < Math.min(4, c.taken[who].length / 2); n++) drawCard(ctx, null, 526 + n * 2, y - n * 2, 42, 60, true);
|
||||
text(ctx, 'Prese', 550, y + 72, 6, paper, 'center');
|
||||
}
|
||||
c.table.forEach((p, i) => {
|
||||
// Il brevissimo scorrimento precede il fermo della presa.
|
||||
const slide = i === c.table.length - 1 ? Math.max(0, 1 - c.t / 0.18) : 0;
|
||||
drawCard(ctx, p.card, 242 + i * 86, 130 + slide * (p.who === 'player' ? 60 : -40), 64, 92);
|
||||
});
|
||||
c.hands.player.forEach((card, i) => {
|
||||
const x = HAND.x + i * HAND.gap;
|
||||
if (canPlay(c) && c.selected === i) rect(ctx, x - 3, HAND.y - 3, HAND.w + 6, HAND.h + 6, '#ffdb76');
|
||||
drawCard(ctx, card, x, HAND.y, HAND.w, HAND.h);
|
||||
text(ctx, String(i + 1), x + HAND.w / 2, 221, 7, '#fff0cd', 'center');
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Briscola in due: regole e avversari senza DOM. Le carte sono immutabili, lo stato è serializzabile.
|
||||
// Due mazzi, stesse regole, stessi id: cambiano solo nomi e disegni (scelti nel dialogo, idea di Andrea).
|
||||
// Toscane: semi francesi (i quadri in Toscana sono «mattoni»), figure gobbo, donna e regio.
|
||||
export const DECKS = {
|
||||
toscane: { suits: ['cuori', 'mattoni', 'fiori', 'picche'], ranks: ['Asso', '2', '3', '4', '5', '6', '7', 'Gobbo', 'Donna', 'Regio'] },
|
||||
napoletane: { suits: ['denari', 'coppe', 'spade', 'bastoni'], ranks: ['Asso', '2', '3', '4', '5', '6', '7', 'Fante', 'Cavallo', 'Re'] },
|
||||
};
|
||||
export const LEVELS = ['bona', 'seria', 'male'];
|
||||
const VALUES = [11, 0, 10, 0, 0, 0, 0, 2, 3, 4];
|
||||
const ORDER = [9, 0, 8, 1, 2, 3, 4, 5, 6, 7];
|
||||
const build = ({ suits, ranks }) => Object.freeze(suits.flatMap((suit, s) => ranks.map((rank, r) =>
|
||||
Object.freeze({ id: s * 10 + r, suit, rank, points: VALUES[r], strength: ORDER[r] }))));
|
||||
export const DECK_CARDS = Object.fromEntries(Object.entries(DECKS).map(([name, deck]) => [name, build(deck)]));
|
||||
export const CARDS = DECK_CARDS.toscane, SUITS = DECKS.toscane.suits;
|
||||
export const other = who => who === 'player' ? 'cpu' : 'player';
|
||||
export const cardName = card => `${card.rank} di ${card.suit}`;
|
||||
export const points = cards => cards.reduce((n, card) => n + card.points, 0);
|
||||
function rng(seed) { let n = seed >>> 0; return () => ((n = (Math.imul(n, 1664525) + 1013904223) >>> 0) / 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;
|
||||
}
|
||||
// Il secondo vince solo con una briscola o con una carta più alta dello stesso seme.
|
||||
export function beats(second, first, trump) {
|
||||
return second.suit === first.suit ? second.strength > first.strength : second.suit === trump;
|
||||
}
|
||||
export function trickWinner(table, trump) {
|
||||
return beats(table[1].card, table[0].card, trump) ? table[1].who : table[0].who;
|
||||
}
|
||||
export function result(taken) {
|
||||
const player = points(taken.player), cpu = points(taken.cpu);
|
||||
return { player, cpu, winner: player === cpu ? null : player > cpu ? 'player' : 'cpu' };
|
||||
}
|
||||
export function newBriscola(seed = Date.now() >>> 0, { level = 'seria', deck = 'toscane' } = {}) {
|
||||
if (!DECKS[deck]) deck = 'toscane';
|
||||
const random = rng(seed), dealer = random() < 0.5 ? 'player' : 'cpu';
|
||||
const stock = shuffle([...DECK_CARDS[deck]], random), hands = { player: [], cpu: [] };
|
||||
// Distribuzione alternata, a partire da chi è di mano.
|
||||
for (let i = 0; i < 3; i++) for (const who of [other(dealer), dealer]) hands[who].push(stock.shift());
|
||||
const trumpCard = stock.shift(); stock.push(trumpCard);
|
||||
return { step: 'play', t: 0, age: 0, level: LEVELS.includes(level) ? level : 'seria', deck, dealer,
|
||||
hands, stock, trumpCard, trump: trumpCard.suit, current: other(dealer), table: [],
|
||||
taken: { player: [], cpu: [] }, history: [], selected: 0, last: null, drawnTrump: null,
|
||||
aiSeed: (seed ^ 0x9e3779b9) >>> 0, events: [{ kind: 'card-shuffle' }], count: 0 };
|
||||
}
|
||||
export const canPlay = c => c.step === 'play' && c.current === 'player';
|
||||
export function select(c, index) {
|
||||
if (!canPlay(c) || !Number.isInteger(index) || !c.hands.player[index]) return false;
|
||||
c.selected = index; return true;
|
||||
}
|
||||
export function nudge(c, direction) {
|
||||
return select(c, (c.selected + direction + c.hands.player.length) % c.hands.player.length);
|
||||
}
|
||||
export function play(c, index = c.selected, who = 'player') {
|
||||
if (c.step !== 'play' || c.current !== who || !Number.isInteger(index) || !c.hands[who][index]) return false;
|
||||
const [card] = c.hands[who].splice(index, 1);
|
||||
c.table.push({ who, card }); c.t = 0;
|
||||
c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
||||
c.events.push({ kind: 'card-slide' }); c.landing = true;
|
||||
if (c.table.length === 2) {
|
||||
c.step = 'trick'; c.last = { winner: trickWinner(c.table, c.trump), cards: c.table.map(p => p.card) };
|
||||
} else c.current = other(who);
|
||||
return true;
|
||||
}
|
||||
export function collect(c) {
|
||||
if (c.step !== 'trick') return false;
|
||||
const winner = c.last.winner;
|
||||
c.taken[winner].push(...c.last.cards);
|
||||
c.history.push({ winner, cards: [...c.last.cards] }); c.table = [];
|
||||
// Chi prende pesca per primo. La briscola scoperta è davvero l'ultima carta.
|
||||
for (const who of [winner, other(winner)]) if (c.stock.length) {
|
||||
const card = c.stock.shift(); c.hands[who].push(card);
|
||||
if (card.id === c.trumpCard.id) c.drawnTrump = who;
|
||||
}
|
||||
c.current = winner; c.t = 0; c.events.push({ kind: 'card-slide' });
|
||||
c.step = c.hands.player.length ? 'play' : 'count';
|
||||
return true;
|
||||
}
|
||||
// La sola porta d'ingresso dell'IA: copie di informazioni pubbliche e della PROPRIA mano.
|
||||
// Mai il mazzo, la mano avversaria o il seed della distribuzione.
|
||||
export function observation(c, who = 'cpu') {
|
||||
return { who, deck: c.deck, hand: [...c.hands[who]], opponentCount: c.hands[other(who)].length,
|
||||
table: c.table.map(p => ({ ...p })), played: c.history.flatMap(t => t.cards),
|
||||
ownPoints: points(c.taken[who]), otherPoints: points(c.taken[other(who)]),
|
||||
remaining: c.stock.length, trump: c.trump, trumpCard: c.trumpCard,
|
||||
knownOpponent: c.drawnTrump === other(who) && !c.history.some(t => t.cards.some(a => a.id === c.trumpCard.id))
|
||||
&& !c.table.some(p => p.card.id === c.trumpCard.id) ? [c.trumpCard] : [] };
|
||||
}
|
||||
export function unseen(v) {
|
||||
const seen = new Set([...v.hand, ...v.played, ...v.table.map(p => p.card)].map(a => a.id));
|
||||
if (v.remaining) seen.add(v.trumpCard.id);
|
||||
return DECK_CARDS[v.deck ?? 'toscane'].filter(a => !seen.has(a.id));
|
||||
}
|
||||
// Giocatore attento di riferimento: scarta i lisci, conserva le briscole e prende i carichi.
|
||||
export function attentive(v) {
|
||||
const lead = v.table[0]?.card;
|
||||
const cost = a => a.points * 2 + (a.suit === v.trump ? 10 + a.strength : a.strength * 0.15);
|
||||
const sorted = v.hand.map((a, i) => ({ a, i })).sort((a, b) => cost(a.a) - cost(b.a));
|
||||
if (lead) {
|
||||
const wins = sorted.filter(({ a }) => beats(a, lead, v.trump));
|
||||
const take = wins.find(({ a }) => a.suit !== v.trump || lead.points + a.points >= 8 || !v.remaining);
|
||||
if (take) return take.i;
|
||||
}
|
||||
return sorted[0].i;
|
||||
}
|
||||
// Finale a informazione completa: minimax esatto, nessuna euristica né campionamento.
|
||||
// Massimizza i punti finali (quindi anche vittoria/patta), con memoizzazione per i sottogiochi.
|
||||
export function perfect(hand, enemy, table, trump, who, memo = new Map()) {
|
||||
const solve = (own, opp, lead, turn) => {
|
||||
if (!own.length && !opp.length && !lead.length) return 0;
|
||||
const key = `${own.map(a => a.id)}|${opp.map(a => a.id)}|${lead.map(p => p.who + p.card.id)}|${turn}`;
|
||||
if (memo.has(key)) return memo.get(key);
|
||||
const mine = turn === who, choices = mine ? own : opp;
|
||||
let best = mine ? -Infinity : Infinity;
|
||||
choices.forEach((card, i) => {
|
||||
const next = choices.filter((_, j) => i !== j), a = mine ? next : own, b = mine ? opp : next;
|
||||
const pair = [...lead, { who: turn, card }];
|
||||
let value;
|
||||
if (pair.length === 2) {
|
||||
const winner = trickWinner(pair, trump);
|
||||
value = (winner === who ? 1 : -1) * points(pair.map(p => p.card)) + solve(a, b, [], winner);
|
||||
} else value = solve(a, b, pair, other(turn));
|
||||
best = mine ? Math.max(best, value) : Math.min(best, value);
|
||||
});
|
||||
memo.set(key, best); return best;
|
||||
};
|
||||
return hand.map((card, i) => {
|
||||
const own = hand.filter((_, j) => i !== j), pair = [...table, { who, card }];
|
||||
if (pair.length === 1) return solve(own, enemy, pair, other(who));
|
||||
const winner = trickWinner(pair, trump);
|
||||
return (winner === who ? 1 : -1) * points(pair.map(p => p.card)) + solve(own, enemy, [], winner);
|
||||
});
|
||||
}
|
||||
const bestIndex = scores => scores.indexOf(Math.max(...scores));
|
||||
|
||||
// «A fassi male»: mondi possibili compatibili con le carte uscite, mai la distribuzione vera.
|
||||
// Stesse distribuzioni campionate per tutte le mosse, entrambi giocano attenti nei seguiti.
|
||||
// Taratura: 1000 partite/livello, seed imul(1..1000, 2654435761), tools/calibrate_briscola.mjs.
|
||||
// Vittorie di Andrea attento: bona 84,7%, seria 48,1%, male 35,9%; patte 0,9%, 2,5%, 2,0%.
|
||||
// Mazziere Andrea in 496/1000 partite. È una misura contro questa euristica, non contro persone.
|
||||
export function chooseCard(v, level = 'seria', seed = 1) {
|
||||
const random = rng(seed);
|
||||
if (level === 'bona') return Math.floor(random() * v.hand.length);
|
||||
if (level !== 'male') return attentive(v);
|
||||
const unknown = unseen(v);
|
||||
if (!v.remaining) return bestIndex(perfect(v.hand, unknown, v.table, v.trump, v.who));
|
||||
const totals = v.hand.map(() => 0), known = new Set(v.knownOpponent.map(a => a.id));
|
||||
for (let sample = 0; sample < 40; sample++) {
|
||||
const pool = shuffle(unknown.filter(a => !known.has(a.id)), random);
|
||||
const enemy = [...v.knownOpponent, ...pool.splice(0, v.opponentCount - known.size)];
|
||||
const stock = [...pool, v.trumpCard];
|
||||
for (let move = 0; move < v.hand.length; move++) {
|
||||
const hands = { [v.who]: [...v.hand], [other(v.who)]: [...enemy] };
|
||||
let table = [...v.table], turn = v.who, deck = [...stock], delta = 0, first = true;
|
||||
while (hands.player.length || hands.cpu.length || table.length) {
|
||||
const idx = first ? move : attentive({ hand: hands[turn], table, trump: v.trump, remaining: deck.length });
|
||||
first = false;
|
||||
const [card] = hands[turn].splice(idx, 1); table.push({ who: turn, card });
|
||||
if (table.length === 1) { turn = other(turn); continue; }
|
||||
turn = trickWinner(table, v.trump);
|
||||
delta += (turn === v.who ? 1 : -1) * points(table.map(p => p.card)); table = [];
|
||||
for (const who of [turn, other(turn)]) if (deck.length) hands[who].push(deck.shift());
|
||||
}
|
||||
totals[move] += delta;
|
||||
}
|
||||
}
|
||||
return bestIndex(totals);
|
||||
}
|
||||
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.25) collect(c);
|
||||
else if (c.step === 'count') {
|
||||
// Una carta ogni 0,35 s; i totali crescono solo ora, alla fine della partita.
|
||||
c.count = Math.min(40, Math.floor(c.t / 0.35));
|
||||
if (c.count === 40) { c.result = result(c.taken); c.step = 'result'; }
|
||||
} else if (c.step === 'play' && c.current === 'cpu' && c.t >= 0.7) {
|
||||
c.aiSeed = (Math.imul(c.aiSeed, 1664525) + 1013904223) >>> 0;
|
||||
play(c, chooseCard(observation(c), c.level, c.aiSeed), 'cpu');
|
||||
}
|
||||
}
|
||||
export function countRows(c) {
|
||||
return ['player', 'cpu'].flatMap(who => c.taken[who].map(card => ({ who, card })));
|
||||
}
|
||||
export function counted(c) {
|
||||
const totals = { player: 0, cpu: 0 };
|
||||
for (const p of countRows(c).slice(0, c.count)) totals[p.who] += p.card.points;
|
||||
return totals;
|
||||
}
|
||||
export function status(c, names = { player: 'Andrea', cpu: 'Il giocatore' }) {
|
||||
if (c.step === 'result') return `${c.result.winner ? 'Vince ' + names[c.result.winner] : 'Patta'}! ${c.result.player} a ${c.result.cpu}.`;
|
||||
if (c.step === 'count') {
|
||||
const last = countRows(c)[c.count - 1];
|
||||
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…`;
|
||||
}
|
||||
+11
-6
@@ -3,9 +3,11 @@
|
||||
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';
|
||||
|
||||
// I minigiochi in primo piano: stessi comandi (mira, pressione, rilascio), motori diversi.
|
||||
// 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 },
|
||||
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 },
|
||||
};
|
||||
@@ -267,16 +269,17 @@ export class Game {
|
||||
|
||||
// Bocce e biliardo: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
|
||||
minigame() { const c = this.closeup; return c ? MINIGAMES[this.story.closeups[c.id].type] ?? null : null; }
|
||||
closeupPress() { const m = this.minigame(); return !!m && !this.speech && m.press(this.closeup); }
|
||||
closeupRelease() { const m = this.minigame(); return !!m && m.release(this.closeup); }
|
||||
closeupCancel() { const m = this.minigame(); return !!m && m.cancel(this.closeup); }
|
||||
closeupPress() { const m = this.minigame(); return !!m && !this.speech && !!m.press?.(this.closeup); }
|
||||
closeupRelease() { return !!this.minigame()?.release?.(this.closeup); }
|
||||
closeupCancel() { return !!this.minigame()?.cancel?.(this.closeup); }
|
||||
closeupPlay(index) { return !this.speech && !!this.minigame()?.play?.(this.closeup, index); }
|
||||
|
||||
// Solo luce, nessun clic sugli oggetti dietro al primo piano. Il progresso è transitorio:
|
||||
// uscire o ricaricare durante la ricerca permette di ripeterla, senza anticipare il finale.
|
||||
closeupAim([x, y]) {
|
||||
const c = this.closeup;
|
||||
const m = this.minigame();
|
||||
if (m) return m.aim(c, [x, y]);
|
||||
if (m) return !!m.aim?.(c, [x, y]);
|
||||
if (!c || this.story.closeups[c.id].type !== 'torch' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
c.beam = [Math.max(70, Math.min(570, x)), Math.max(65, Math.min(295, y))];
|
||||
return true;
|
||||
@@ -440,7 +443,9 @@ export class Game {
|
||||
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] }
|
||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level) }
|
||||
// `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,
|
||||
Object.keys(this.story.closeups[e.closeup].decks ?? {}).find(d => s.flags[this.story.closeups[e.closeup].decks[d]])) }
|
||||
: { id: e.closeup, step: 'cover', t: 0, age: 0 };
|
||||
}
|
||||
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ambientEffects, ambientHash } from './ambient.js';
|
||||
import { drawTorch } from './torch.js';
|
||||
import { drawBocce } from './bocce-render.js';
|
||||
import { drawBiliardo } from './biliardo-render.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { versioned } from '../version.js';
|
||||
export const WIDTH = 640, HEIGHT = 360;
|
||||
|
||||
@@ -666,6 +667,7 @@ const twoLines = title => { const i = title.lastIndexOf(' '); return i > 0 ? [ti
|
||||
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 === 'torch') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||
|
||||
@@ -101,6 +101,12 @@ export class Soundscape {
|
||||
play(kind, { pan = (this.random() - 0.5) * 1.3, strength = 0.5 } = {}) {
|
||||
if (!this.audible) return;
|
||||
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); }
|
||||
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);
|
||||
}
|
||||
else if (kind === 'bocce-roll') this.noiseSound(0.7, 650, 0.06 + strength * 0.08, { pan });
|
||||
else if (kind === 'bocce-hit') { this.tone(760, 320, 0.075, 0.04 + strength * 0.07, 0, pan); this.noiseSound(0.09, 1600, 0.04, { pan }); }
|
||||
else if (kind === 'bocce-rail') this.noiseSound(0.16, 380, 0.04 + strength * 0.08, { pan });
|
||||
|
||||
Reference in New Issue
Block a user