Carte che volano: dalla mano al tavolo e, finita la presa, sul mucchietto di chi ha preso (briscola, tressette, scopa; Andrea: «non si capisce quando vengono buttate le carte»). Il valzer della balera ai tavoli da carte (tools/valzer_carte.py: fisarmonica musette, basso, melodia di mandolino col tremolo), musica per primo piano; video «Come si gioca»: cache del Chromium della VM spenta, narratore e personaggi a turno, musica di sottofondo, pronunce del narratore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
Binary file not shown.
@@ -1,6 +1,12 @@
|
|||||||
import { canPlay, cardName, counted, countRows } from './briscola.js';
|
import { canPlay, cardName, counted, countRows, PLAY_TIME, TRICK_HOLD, TRICK_TIME } from './briscola.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
// Una carta in volo da un rettangolo [x, y, w, h] a un altro, con k da 0 a 1 (parte svelta, arriva piano).
|
||||||
|
// La usano anche tressette e scopa.
|
||||||
|
export function flight(from, to, k) {
|
||||||
|
const e = 1 - (1 - Math.min(1, Math.max(0, k))) ** 3;
|
||||||
|
return from.map((v, i) => v + (to[i] - v) * e);
|
||||||
|
}
|
||||||
// Coordinate condivise da disegno e hit test: bersagli larghi anche sul telefono.
|
// 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 const HAND = { x: 176, y: 230, gap: 102, w: 84, h: 120 };
|
||||||
export function cardAt(c, [x, y]) {
|
export function cardAt(c, [x, y]) {
|
||||||
@@ -285,9 +291,13 @@ export function drawBriscola(ctx, c, def) {
|
|||||||
text(ctx, 'Prese', 550, y + 72, 6, paper, 'center');
|
text(ctx, 'Prese', 550, y + 72, 6, paper, 'center');
|
||||||
}
|
}
|
||||||
c.table.forEach((p, i) => {
|
c.table.forEach((p, i) => {
|
||||||
// Il brevissimo scorrimento precede il fermo della presa.
|
// La carta vola dalla mano di chi la gioca; finita la presa, tutte e due volano sulle prese di chi ha preso.
|
||||||
const slide = i === c.table.length - 1 ? Math.max(0, 1 - c.t / 0.18) : 0;
|
const spot = [242 + i * 86, 130, 64, 92];
|
||||||
drawCard(ctx, p.card, 242 + i * 86, 130 + slide * (p.who === 'player' ? 60 : -40), 64, 92);
|
const hand = p.who === 'player' ? [HAND.x + (p.from ?? 1) * HAND.gap, HAND.y, HAND.w, HAND.h] : [229 + (p.from ?? 1) * 62, 49, 52, 74];
|
||||||
|
let r = spot;
|
||||||
|
if (i === c.table.length - 1 && c.step === 'play' || c.step === 'trick' && i === 1 && c.t < PLAY_TIME) r = flight(hand, spot, c.t / PLAY_TIME);
|
||||||
|
if (c.step === 'trick' && c.t > TRICK_HOLD) r = flight(spot, [526, c.last.winner === 'cpu' ? 74 : 257, 42, 60], (c.t - TRICK_HOLD) / (TRICK_TIME - TRICK_HOLD - 0.05));
|
||||||
|
drawCard(ctx, p.card, ...r);
|
||||||
});
|
});
|
||||||
c.hands.player.forEach((card, i) => {
|
c.hands.player.forEach((card, i) => {
|
||||||
const x = HAND.x + i * HAND.gap;
|
const x = HAND.x + i * HAND.gap;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export const DECKS = {
|
|||||||
napoletane: { suits: ['denari', 'coppe', 'spade', 'bastoni'], ranks: ['Asso', '2', '3', '4', '5', '6', '7', 'Fante', 'Cavallo', 'Re'] },
|
napoletane: { suits: ['denari', 'coppe', 'spade', 'bastoni'], ranks: ['Asso', '2', '3', '4', '5', '6', '7', 'Fante', 'Cavallo', 'Re'] },
|
||||||
};
|
};
|
||||||
export const LEVELS = ['bona', 'seria', 'male'];
|
export const LEVELS = ['bona', 'seria', 'male'];
|
||||||
|
// I tempi delle animazioni (briscola-render.js): la carta vola dalla mano al tavolo; la presa resta ferma un attimo e
|
||||||
|
// poi vola sul mucchietto di chi ha preso (Andrea: «non si capisce quando vengono buttate le carte»).
|
||||||
|
export const PLAY_TIME = 0.3, TRICK_HOLD = 0.85, TRICK_TIME = 1.4;
|
||||||
const VALUES = [11, 0, 10, 0, 0, 0, 0, 2, 3, 4];
|
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 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) =>
|
const build = ({ suits, ranks }) => Object.freeze(suits.flatMap((suit, s) => ranks.map((rank, r) =>
|
||||||
@@ -54,7 +57,7 @@ export function nudge(c, direction) {
|
|||||||
export function play(c, index = c.selected, who = 'player') {
|
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;
|
if (c.step !== 'play' || c.current !== who || !Number.isInteger(index) || !c.hands[who][index]) return false;
|
||||||
const [card] = c.hands[who].splice(index, 1);
|
const [card] = c.hands[who].splice(index, 1);
|
||||||
c.table.push({ who, card }); c.t = 0;
|
c.table.push({ who, card, from: index }); c.t = 0; // `from`: da dove parte la carta nel disegno
|
||||||
c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
||||||
c.events.push({ kind: 'card-slide' }); c.landing = true;
|
c.events.push({ kind: 'card-slide' }); c.landing = true;
|
||||||
if (c.table.length === 2) {
|
if (c.table.length === 2) {
|
||||||
@@ -171,8 +174,8 @@ export function chooseCard(v, level = 'seria', seed = 1) {
|
|||||||
export function update(c, dt) {
|
export function update(c, dt) {
|
||||||
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
||||||
c.t += dt;
|
c.t += dt;
|
||||||
if (c.landing && c.t >= 0.18) { c.events.push({ kind: 'card-tap' }); c.landing = false; }
|
if (c.landing && c.t >= PLAY_TIME) { c.events.push({ kind: 'card-tap' }); c.landing = false; }
|
||||||
if (c.step === 'trick' && c.t >= 1.25) collect(c);
|
if (c.step === 'trick' && c.t >= TRICK_TIME) collect(c);
|
||||||
else if (c.step === 'count') {
|
else if (c.step === 'count') {
|
||||||
// Una carta ogni 0,35 s; i totali crescono solo ora, alla fine della partita.
|
// 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));
|
c.count = Math.min(40, Math.floor(c.t / 0.35));
|
||||||
|
|||||||
+3
-1
@@ -8,11 +8,13 @@ import { versioned } from '../version.js';
|
|||||||
// d'apertura nessuno: lì suona la sua musichetta, una volta sola (main.js).
|
// d'apertura nessuno: lì suona la sua musichetta, una volta sola (main.js).
|
||||||
// Quale brano: quello della scena se ce l'ha; altrimenti, all'aperto, quello della fase del giorno (la sera ha il suo);
|
// Quale brano: quello della scena se ce l'ha; altrimenti, all'aperto, quello della fase del giorno (la sera ha il suo);
|
||||||
// altrimenti quello generale del paese.
|
// altrimenti quello generale del paese.
|
||||||
export function trackFor(story, { screen, scene, phase }) {
|
// Un primo piano con la sua musica (i tavoli da carte, col valzer) la porta con sé finché resta aperto.
|
||||||
|
export function trackFor(story, { screen, scene, phase, closeup }) {
|
||||||
const m = story.music ?? {};
|
const m = story.music ?? {};
|
||||||
if (screen === 'splash') return null;
|
if (screen === 'splash') return null;
|
||||||
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
||||||
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
||||||
|
if (closeup && story.closeups?.[closeup]?.music) return story.closeups[closeup].music;
|
||||||
const sc = story.scenes[scene];
|
const sc = story.scenes[scene];
|
||||||
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { canPlay, TARGET } from './scopa.js';
|
import { canPlay, TARGET, PLAY_TIME, TAKE_HOLD, TAKE_TIME } from './scopa.js';
|
||||||
import { drawCard, drawTable } from './briscola-render.js';
|
import { drawCard, drawTable, flight } from './briscola-render.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
// Coordinate condivise da disegno e tocco: la mano di Andrea (grande) e le carte in tavola.
|
// Coordinate condivise da disegno e tocco: la mano di Andrea (grande) e le carte in tavola.
|
||||||
@@ -35,17 +35,30 @@ function board(ctx, c, names) {
|
|||||||
if (c.scope[who]) text(ctx, `Scope: ${c.scope[who]}`, 581, y + 70, 6, gold, 'center');
|
if (c.scope[who]) text(ctx, `Scope: ${c.scope[who]}`, 581, y + 70, 6, gold, 'center');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const PILE = { cpu: 92, player: 196 };
|
||||||
function table(ctx, c) {
|
function table(ctx, c) {
|
||||||
const take = c.step === 'take' && c.last.captured.length, shown = take ? [...c.table, ...c.last.captured] : c.table;
|
// Durante una presa la tavola resta com'era (le carte prese al loro posto, incorniciate) finché non volano via.
|
||||||
|
const take = c.step === 'take' && c.last.captured.length, shown = take ? c.last.before : c.table;
|
||||||
const marked = new Set((take ? c.last.captured : c.step === 'choose' ? c.choice.options[c.choice.selected] : []).map(a => a.id));
|
const marked = new Set((take ? c.last.captured : c.step === 'choose' ? c.choice.options[c.choice.selected] : []).map(a => a.id));
|
||||||
|
const fly = take && c.t > TAKE_HOLD ? (c.t - TAKE_HOLD) / (TAKE_TIME - TAKE_HOLD - 0.05) : 0;
|
||||||
|
const pile = take ? [560, PILE[c.last.who], 42, 60] : null;
|
||||||
|
// Da dove parte la carta giocata: la mano di Andrea o quella coperta del giocatore.
|
||||||
|
const hand = c.step === 'take' && (c.last.who === 'player'
|
||||||
|
? [handX(c.hands.player.length + 1, c.last.from), HAND.y, HAND.w, HAND.h]
|
||||||
|
: [320 - ((c.hands.cpu.length + 1) * 48 - 6) / 2 + c.last.from * 48, 38, 42, 60]);
|
||||||
|
const arrive = Math.min(1, c.t / PLAY_TIME);
|
||||||
shown.forEach((card, i) => {
|
shown.forEach((card, i) => {
|
||||||
const [x, y] = tableXY(shown.length, i);
|
const [x, y] = tableXY(shown.length, i);
|
||||||
if (marked.has(card.id)) frame(ctx, x, y, TABLE.w, TABLE.h);
|
let r = [x, y, TABLE.w, TABLE.h];
|
||||||
else if (c.step === 'take' && !take && card.id === c.last.card.id) frame(ctx, x, y, TABLE.w, TABLE.h, '#9fe3ff');
|
if (c.step === 'take' && !take && card.id === c.last.card.id) r = flight(hand, r, arrive);
|
||||||
drawCard(ctx, card, x, y, TABLE.w, TABLE.h);
|
if (fly && marked.has(card.id)) r = flight(r, pile, fly);
|
||||||
|
else if (marked.has(card.id)) frame(ctx, x, y, TABLE.w, TABLE.h);
|
||||||
|
drawCard(ctx, card, ...r);
|
||||||
});
|
});
|
||||||
if (take) {
|
if (take) {
|
||||||
drawCard(ctx, c.last.card, 299, 200, TABLE.w, TABLE.h);
|
let r = flight(hand, [299, 200, TABLE.w, TABLE.h], arrive);
|
||||||
|
if (fly) r = flight(r, pile, fly);
|
||||||
|
drawCard(ctx, c.last.card, ...r);
|
||||||
if (c.last.scopa) { panel(ctx, 260, 98, 120, 22); text(ctx, 'SCOPA!', 320, 109, 9, gold, 'center'); }
|
if (c.last.scopa) { panel(ctx, 260, 98, 120, 22); text(ctx, 'SCOPA!', 320, 109, 9, gold, 'center'); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -5,6 +5,9 @@ import { DECKS, cardName } from './briscola.js';
|
|||||||
export { cardName };
|
export { cardName };
|
||||||
export const LEVELS = ['bona', 'seria', 'male'];
|
export const LEVELS = ['bona', 'seria', 'male'];
|
||||||
export const TARGET = 11;
|
export const TARGET = 11;
|
||||||
|
// I tempi delle animazioni (scopa-render.js): la carta vola dalla mano; le carte prese restano ferme un attimo e poi
|
||||||
|
// volano sul mucchietto di chi ha preso.
|
||||||
|
export const PLAY_TIME = 0.3, TAKE_HOLD = 0.75, TAKE_TIME = 1.35;
|
||||||
export const COIN = { toscane: 'mattoni', napoletane: 'denari' };
|
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,
|
// 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.
|
// 3 13, 2 12, le figure 10.
|
||||||
@@ -105,7 +108,7 @@ export function take(c, k = c.choice?.selected) {
|
|||||||
export function cancel(c) { if (c.step !== 'choose') return false; c.step = 'play'; c.choice = null; return true; }
|
export function cancel(c) { if (c.step !== 'choose') return false; c.step = 'play'; c.choice = null; return true; }
|
||||||
function execute(c, who, index, captured) {
|
function execute(c, who, index, captured) {
|
||||||
if (!captured && captures(c.table, c.hands[who][index]).length) return false;
|
if (!captured && captures(c.table, c.hands[who][index]).length) return false;
|
||||||
const [card] = c.hands[who].splice(index, 1);
|
const before = [...c.table], [card] = c.hands[who].splice(index, 1);
|
||||||
if (captured) {
|
if (captured) {
|
||||||
const ids = new Set(captured.map(a => a.id));
|
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;
|
c.table = c.table.filter(a => !ids.has(a.id)); c.taken[who].push(card, ...captured); c.lastTaker = who;
|
||||||
@@ -114,7 +117,8 @@ function execute(c, who, index, captured) {
|
|||||||
const lastPlay = !c.stock.length && !c.hands.player.length && !c.hands.cpu.length;
|
const lastPlay = !c.stock.length && !c.hands.player.length && !c.hands.cpu.length;
|
||||||
const scopa = !!captured && !c.table.length && !lastPlay;
|
const scopa = !!captured && !c.table.length && !lastPlay;
|
||||||
if (scopa) c.scope[who]++;
|
if (scopa) c.scope[who]++;
|
||||||
c.last = { who, card, captured: captured ?? [], scopa };
|
// `from` e `before` servono al disegno: da dove parte la carta e com'era la tavola prima della presa.
|
||||||
|
c.last = { who, card, captured: captured ?? [], scopa, from: index, before };
|
||||||
c.events.push({ kind: captured ? 'card-tap' : 'card-slide', who }, ...(scopa ? [{ kind: 'card-busso', who }] : []));
|
c.events.push({ kind: captured ? 'card-tap' : 'card-slide', who }, ...(scopa ? [{ kind: 'card-busso', who }] : []));
|
||||||
c.step = 'take'; c.t = 0;
|
c.step = 'take'; c.t = 0;
|
||||||
if (who === 'player') c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
if (who === 'player') c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1));
|
||||||
@@ -230,7 +234,7 @@ export function choose(v, level = 'seria', seed = 1) {
|
|||||||
export function update(c, dt) {
|
export function update(c, dt) {
|
||||||
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
||||||
c.t += dt;
|
c.t += dt;
|
||||||
if (c.step === 'take' && c.t >= (c.last.captured.length ? 1.3 : 0.6)) after(c);
|
if (c.step === 'take' && c.t >= (c.last.captured.length ? TAKE_TIME : PLAY_TIME * 2)) after(c);
|
||||||
else if (c.step === 'play' && c.current === 'cpu' && c.t >= 0.75) {
|
else if (c.step === 'play' && c.current === 'cpu' && c.t >= 0.75) {
|
||||||
c.aiSeed = lcg(c.aiSeed);
|
c.aiSeed = lcg(c.aiSeed);
|
||||||
const m = choose(observation(c, 'cpu'), c.level, c.aiSeed);
|
const m = choose(observation(c, 'cpu'), c.level, c.aiSeed);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { canPlay, legal, accuseName, NAMES } from './tressette.js';
|
import { canPlay, legal, accuseName, team, NAMES, PLAY_TIME, TRICK_HOLD, TRICK_TIME } from './tressette.js';
|
||||||
import { drawCard, drawTable } from './briscola-render.js';
|
import { drawCard, drawTable, flight } from './briscola-render.js';
|
||||||
import { text, panel } from './bocce-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.
|
// La mano di Andrea a ventaglio, carte a grandezza naturale (42×60): coordinate condivise da disegno e tocco.
|
||||||
@@ -19,7 +19,9 @@ export function cardAt(c, [x, y]) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
// Dove cade la carta di ognuno al centro, e da dove arriva.
|
// 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 SPOT = [[299, 190], [354, 146], [299, 100], [244, 146]];
|
||||||
|
// I mucchietti delle prese: noi in basso a destra, loro in basso a sinistra.
|
||||||
|
const PILE = [[560, 288], [34, 288]];
|
||||||
const LABEL = [[320, 280, 'center'], [612, 92, 'right'], [320, 39, 'center'], [28, 92, 'left']];
|
const LABEL = [[320, 280, 'center'], [612, 92, 'right'], [320, 39, 'center'], [28, 92, 'left']];
|
||||||
const gold = '#ffe09b', paper = '#fff0cd', ours = '#9fe3ff', theirs = '#ffb0a3';
|
const gold = '#ffe09b', paper = '#fff0cd', ours = '#9fe3ff', theirs = '#ffb0a3';
|
||||||
const SAY = { busso: 'BUSSO!', striscio: 'STRISCIO!', volo: 'VOLO!' };
|
const SAY = { busso: 'BUSSO!', striscio: 'STRISCIO!', volo: 'VOLO!' };
|
||||||
@@ -37,6 +39,14 @@ function others(ctx, c, names) {
|
|||||||
text(ctx, names[seat], x, y, 6, turn ? gold : seat % 2 ? theirs : ours, align);
|
text(ctx, names[seat], x, y, 6, turn ? gold : seat % 2 ? theirs : ours, align);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function piles(ctx, c) {
|
||||||
|
// Quante prese ha fatto ogni coppia, a mucchietto (una carta coperta ogni due prese).
|
||||||
|
for (const t of [0, 1]) {
|
||||||
|
const n = c.tricks.filter(x => team(x.winner) === t).length, [x, y] = PILE[t];
|
||||||
|
for (let k = 0; k < Math.ceil(n / 2); k++) drawCard(ctx, null, x + k, y - k, 42, 60, true);
|
||||||
|
text(ctx, t ? 'Prese loro' : 'Prese noi', x + 21, y - 8, 5, t ? theirs : ours, 'center');
|
||||||
|
}
|
||||||
|
}
|
||||||
function scoreboard(ctx, c) {
|
function scoreboard(ctx, c) {
|
||||||
panel(ctx, 26, 34, 118, 38);
|
panel(ctx, 26, 34, 118, 38);
|
||||||
text(ctx, `Noi ${c.score[0]}`, 34, 46, 7, ours);
|
text(ctx, `Noi ${c.score[0]}`, 34, 46, 7, ours);
|
||||||
@@ -51,10 +61,15 @@ function accuses(ctx, c, names) {
|
|||||||
c.accuse.forEach((a, i) => text(ctx, `${names[a.who]}: ${accuseName(a)} +${a.points}`, 403, 41 + i * 11, 5, a.who % 2 ? theirs : ours));
|
c.accuse.forEach((a, i) => text(ctx, `${names[a.who]}: ${accuseName(a)} +${a.points}`, 403, 41 + i * 11, 5, a.who % 2 ? theirs : ours));
|
||||||
}
|
}
|
||||||
function trick(ctx, c, names) {
|
function trick(ctx, c, names) {
|
||||||
|
// Da dove parte la carta: la mano di Andrea, o le carte coperte degli altri.
|
||||||
|
const from = p => p.who === 0 ? [handX(c.hands[0].length + 1, p.from ?? 0), CARD.y, CARD.w, CARD.h]
|
||||||
|
: p.who === 2 ? [300, 46, 26, 37] : [p.who === 1 ? 584 : 30, 102 + (p.from ?? 0) * 12, 26, 37];
|
||||||
c.table.forEach((p, i) => {
|
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];
|
||||||
const [x, y] = SPOT[p.who], [dx, dy] = FROM[p.who];
|
let r = [x, y, CARD.w, CARD.h];
|
||||||
drawCard(ctx, p.card, x + dx * slide, y + dy * slide, CARD.w, CARD.h);
|
if (i === c.table.length - 1 && c.t < PLAY_TIME && (c.step === 'play' || c.step === 'trick')) r = flight(from(p), r, c.t / PLAY_TIME);
|
||||||
|
if (c.step === 'trick' && c.t > TRICK_HOLD) r = flight(r, [...PILE[team(c.last.winner)], 42, 60], (c.t - TRICK_HOLD) / (TRICK_TIME - TRICK_HOLD - 0.05));
|
||||||
|
drawCard(ctx, p.card, ...r);
|
||||||
if (p.signal) {
|
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];
|
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');
|
panel(ctx, lx - 30, ly - 7, 60, 14); text(ctx, SAY[p.signal], lx, ly, 6, gold, 'center');
|
||||||
@@ -86,7 +101,7 @@ export function drawTressette(ctx, c, def) {
|
|||||||
const names = def?.seats ?? NAMES;
|
const names = def?.seats ?? NAMES;
|
||||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||||
drawTable(ctx, 'TRESSETTE AI 21');
|
drawTable(ctx, 'TRESSETTE AI 21');
|
||||||
scoreboard(ctx, c); others(ctx, c, names); accuses(ctx, c, names); trick(ctx, c, names); hand(ctx, c);
|
scoreboard(ctx, c); others(ctx, c, names); piles(ctx, c); accuses(ctx, c, names); hand(ctx, c); trick(ctx, c, names);
|
||||||
if (c.step === 'smazzata') summary(ctx, c, `Fine della smazzata ${c.round}`);
|
if (c.step === 'smazzata') summary(ctx, c, `Fine della smazzata ${c.round}`);
|
||||||
if (c.step === 'result') summary(ctx, c, c.result.winner === 'player' ? 'Fuori! Avete vinto' : 'Sono fuori loro');
|
if (c.step === 'result') summary(ctx, c, c.result.winner === 'player' ? 'Fuori! Avete vinto' : 'Sono fuori loro');
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export { cardName };
|
|||||||
export const LEVELS = ['bona', 'seria', 'male'];
|
export const LEVELS = ['bona', 'seria', 'male'];
|
||||||
export const SIGNALS = ['busso', 'striscio', 'volo'];
|
export const SIGNALS = ['busso', 'striscio', 'volo'];
|
||||||
export const TARGET = 21;
|
export const TARGET = 21;
|
||||||
|
// I tempi delle animazioni (tressette-render.js): la carta vola dalla mano al tavolo; la presa resta ferma un attimo e
|
||||||
|
// poi vola sul mucchietto della coppia che ha preso.
|
||||||
|
export const PLAY_TIME = 0.3, TRICK_HOLD = 0.8, TRICK_TIME = 1.35;
|
||||||
// Posti in senso antiorario, come si gira da noi: 0 Andrea (sotto), 1 a destra, 2 il socio (di fronte), 3 a sinistra.
|
// 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 SEATS = [0, 1, 2, 3];
|
||||||
export const team = seat => seat % 2;
|
export const team = seat => seat % 2;
|
||||||
@@ -115,7 +118,7 @@ export function play(c, index = c.selected, who = 0, signal = who === 0 ? c.sign
|
|||||||
if (out(c)) return true;
|
if (out(c)) return true;
|
||||||
}
|
}
|
||||||
const [card] = c.hands[who].splice(index, 1);
|
const [card] = c.hands[who].splice(index, 1);
|
||||||
c.table.push({ who, card, signal: signal || null }); c.t = 0; c.landing = true;
|
c.table.push({ who, card, signal: signal || null, from: index }); c.t = 0; c.landing = true; // `from`: per il disegno
|
||||||
// Il segno si sente: busso batte sul tavolo, striscio la fa strusciare, volo la lascia cadere dall'alto.
|
// 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 });
|
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 (who === 0) { c.signal = null; c.refused = ''; c.selected = Math.min(c.selected, Math.max(0, c.hands[0].length - 1)); }
|
||||||
@@ -254,8 +257,8 @@ export function choose(v, level = 'seria', seed = 1) {
|
|||||||
export function update(c, dt) {
|
export function update(c, dt) {
|
||||||
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return;
|
||||||
c.t += dt;
|
c.t += dt;
|
||||||
if (c.landing && c.t >= 0.18) { c.events.push({ kind: 'card-tap' }); c.landing = false; }
|
if (c.landing && c.t >= PLAY_TIME) { c.events.push({ kind: 'card-tap' }); c.landing = false; }
|
||||||
if (c.step === 'trick' && c.t >= 1.2) collect(c);
|
if (c.step === 'trick' && c.t >= TRICK_TIME) collect(c);
|
||||||
else if (c.step === 'play' && c.current !== 0 && c.t >= 0.75) {
|
else if (c.step === 'play' && c.current !== 0 && c.t >= 0.75) {
|
||||||
c.aiSeed = lcg(c.aiSeed);
|
c.aiSeed = lcg(c.aiSeed);
|
||||||
const seat = c.current, { index, signal } = choose(observation(c, seat), seat === 2 ? 'male' : c.level, c.aiSeed);
|
const seat = c.current, { index, signal } = choose(observation(c, seat), seat === 2 ? 'male' : c.level, c.aiSeed);
|
||||||
|
|||||||
+1
-1
@@ -906,7 +906,7 @@ function frame(now) {
|
|||||||
// Mentre si parla la musica si abbassa; quando compaiono gli occhi del Regolo quasi si spegne, per la sorpresa.
|
// Mentre si parla la musica si abbassa; quando compaiono gli occhi del Regolo quasi si spegne, per la sorpresa.
|
||||||
const searching = game?.closeup?.id === 'fessura';
|
const searching = game?.closeup?.id === 'fessura';
|
||||||
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
||||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase() }), dt);
|
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id }), dt);
|
||||||
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
||||||
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
||||||
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
||||||
|
|||||||
+4
-3
@@ -109,15 +109,16 @@ export const story = {
|
|||||||
closeups: {
|
closeups: {
|
||||||
// [DA ANDREA: chi è] Partita facoltativa; battute provvisorie.
|
// [DA ANDREA: chi è] Partita facoltativa; battute provvisorie.
|
||||||
briscola: {
|
briscola: {
|
||||||
type: 'briscola', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
type: 'briscola', music: 'assets/audio/carte.mp3', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
||||||
win: [{ set: 'briscola_vinta' }, { set: 'briscola_giocata' }, { say: 'Oh, bravo! Ora però rimescolo io.', who: 'giocatore_carte', pose: 'laugh' }],
|
win: [{ set: 'briscola_vinta' }, { set: 'briscola_giocata' }, { say: 'Oh, bravo! Ora però rimescolo io.', who: 'giocatore_carte', pose: 'laugh' }],
|
||||||
lose: [{ set: 'briscola_giocata' }, { say: 'I carichi portali qui, mica alla stazione! Rivincita quando vuoi.', who: 'giocatore_carte', pose: 'laugh' }],
|
lose: [{ set: 'briscola_giocata' }, { say: 'I carichi portali qui, mica alla stazione! Rivincita quando vuoi.', who: 'giocatore_carte', pose: 'laugh' }],
|
||||||
draw: [{ set: 'briscola_giocata' }, { say: 'Sessanta pari. Un si litiga nemmeno sul conto!', who: 'giocatore_carte', pose: 'talk' }],
|
draw: [{ set: 'briscola_giocata' }, { say: 'Sessanta pari. Un si litiga nemmeno sul conto!', who: 'giocatore_carte', pose: 'talk' }],
|
||||||
quit: [{ say: 'Vai, ti tengo il posto. Le carte mica scappano.', who: 'giocatore_carte' }],
|
quit: [{ say: 'Vai, ti tengo il posto. Le carte mica scappano.', who: 'giocatore_carte' }],
|
||||||
},
|
},
|
||||||
|
// I tre tavoli da carte suonano il valzer della balera (tools/valzer_carte.py, Andrea: «una sua melodia»).
|
||||||
// Il tressette a coppie ai 21 (idea di Andrea): busso, striscio, volo e accuse. [DA ANDREA: chi sono il socio e l'altro]
|
// Il tressette a coppie ai 21 (idea di Andrea): busso, striscio, volo e accuse. [DA ANDREA: chi sono il socio e l'altro]
|
||||||
tressette: {
|
tressette: {
|
||||||
type: 'tressette', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
type: 'tressette', music: 'assets/audio/carte.mp3', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
||||||
seats: ['Andrea', 'Il giocatore', 'Il socio', 'L\'altro'],
|
seats: ['Andrea', 'Il giocatore', 'Il socio', 'L\'altro'],
|
||||||
win: [{ set: 'tressette_vinto' }, { say: 'Ventuno per primi. Bella coppia, voi due!', who: 'giocatore_carte', pose: 'laugh' }],
|
win: [{ set: 'tressette_vinto' }, { say: 'Ventuno per primi. Bella coppia, voi due!', who: 'giocatore_carte', pose: 'laugh' }],
|
||||||
lose: [{ say: 'Ventuno! Il tressette è un\'arte: si studia da piccini.', who: 'giocatore_carte', pose: 'laugh' }],
|
lose: [{ say: 'Ventuno! Il tressette è un\'arte: si studia da piccini.', who: 'giocatore_carte', pose: 'laugh' }],
|
||||||
@@ -126,7 +127,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
// La scopa ai 11 (idea di Andrea): carte, mattoni (o denari), settebello, primiera e scope.
|
// La scopa ai 11 (idea di Andrea): carte, mattoni (o denari), settebello, primiera e scope.
|
||||||
scopa: {
|
scopa: {
|
||||||
type: 'scopa', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
type: 'scopa', music: 'assets/audio/carte.mp3', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' },
|
||||||
win: [{ set: 'scopa_vinta' }, { say: 'Settebello, primiera e pure le scope. Oggi l\'hai vista bene.', who: 'giocatore_carte', pose: 'talk' }],
|
win: [{ set: 'scopa_vinta' }, { say: 'Settebello, primiera e pure le scope. Oggi l\'hai vista bene.', who: 'giocatore_carte', pose: 'talk' }],
|
||||||
lose: [{ say: 'Undici! Il settebello lo sento arrivare da lontano.', who: 'giocatore_carte', pose: 'laugh' }],
|
lose: [{ say: 'Undici! Il settebello lo sento arrivare da lontano.', who: 'giocatore_carte', pose: 'laugh' }],
|
||||||
draw: [],
|
draw: [],
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ try {
|
|||||||
game.closeup = { id: 'briscola', ...newBriscola(91421, { level: 'male', deck }) };
|
game.closeup = { id: 'briscola', ...newBriscola(91421, { level: 'male', deck }) };
|
||||||
}, deck);
|
}, deck);
|
||||||
await page.waitForSelector('#briscola-controls:not([hidden])');
|
await page.waitForSelector('#briscola-controls:not([hidden])');
|
||||||
|
// Il tavolo ha la sua musica (il valzer delle carte), non quella dell'oratorio.
|
||||||
|
await page.waitForFunction(() => music.want === 'assets/audio/carte.mp3');
|
||||||
assert.ok(await page.isHidden('#bocce-controls'));
|
assert.ok(await page.isHidden('#bocce-controls'));
|
||||||
const scene = await page.locator('#scene').boundingBox(), exit = await page.locator('#closeup-close').boundingBox();
|
const scene = await page.locator('#scene').boundingBox(), exit = await page.locator('#closeup-close').boundingBox();
|
||||||
assert.ok(exit.y >= scene.y + scene.height, 'uscita fuori dal tavolo');
|
assert.ok(exit.y >= scene.y + scene.height, 'uscita fuori dal tavolo');
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ test('nessun obbligo di seme; la presa resta sul tavolo, chi prende pesca prima
|
|||||||
assert.ok(play(c, 0, 'cpu')); assert.ok(play(c, 1));
|
assert.ok(play(c, 0, 'cpu')); assert.ok(play(c, 1));
|
||||||
assert.equal(c.step, 'trick'); assert.equal(c.last.winner, 'player'); assert.equal(c.table.length, 2);
|
assert.equal(c.step, 'trick'); assert.equal(c.last.winner, 'player'); assert.equal(c.table.length, 2);
|
||||||
update(c, 1); assert.equal(c.table.length, 2); assert.equal(c.stock.length, stock.length);
|
update(c, 1); assert.equal(c.table.length, 2); assert.equal(c.stock.length, stock.length);
|
||||||
update(c, 0.26);
|
update(c, 0.41); // TRICK_TIME: 1,4 s, la presa vola sulle prese
|
||||||
assert.equal(c.hands.player.at(-1).id, stock[0].id);
|
assert.equal(c.hands.player.at(-1).id, stock[0].id);
|
||||||
assert.equal(c.hands.cpu.at(-1).id, stock[1].id);
|
assert.equal(c.hands.cpu.at(-1).id, stock[1].id);
|
||||||
assert.equal(c.current, 'player'); assert.equal(c.table.length, 0);
|
assert.equal(c.current, 'player'); assert.equal(c.table.length, 0);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ test('il brano giusto per titolo, fine e scena', () => {
|
|||||||
assert.equal(trackFor(s, { screen: 'game', scene: 'a' }), 'g.mp3');
|
assert.equal(trackFor(s, { screen: 'game', scene: 'a' }), 'g.mp3');
|
||||||
assert.equal(trackFor(s, { screen: 'game', scene: 'b' }), 'b.mp3');
|
assert.equal(trackFor(s, { screen: 'game', scene: 'b' }), 'b.mp3');
|
||||||
assert.equal(trackFor({ scenes: {} }, { screen: 'game', scene: 'x' }), null);
|
assert.equal(trackFor({ scenes: {} }, { screen: 'game', scene: 'x' }), null);
|
||||||
|
// Un tavolo da carte aperto ha la sua musica; un primo piano senza musica lascia quella della scena.
|
||||||
|
const tavoli = { ...s, closeups: { carte: { music: 'c.mp3' }, libro: {} } };
|
||||||
|
assert.equal(trackFor(tavoli, { screen: 'game', scene: 'b', closeup: 'carte' }), 'c.mp3');
|
||||||
|
assert.equal(trackFor(tavoli, { screen: 'game', scene: 'b', closeup: 'libro' }), 'b.mp3');
|
||||||
// La sera ha la sua musica all'aperto; la scena col suo brano e gli interni non cambiano.
|
// La sera ha la sua musica all'aperto; la scena col suo brano e gli interni non cambiano.
|
||||||
const sera = { id: 'sera', music: 's.mp3' };
|
const sera = { id: 'sera', music: 's.mp3' };
|
||||||
assert.equal(trackFor({ ...s, scenes: { ...s.scenes, i: { indoor: true } } }, { screen: 'game', scene: 'a', phase: sera }), 's.mp3');
|
assert.equal(trackFor({ ...s, scenes: { ...s.scenes, i: { indoor: true } } }, { screen: 'game', scene: 'a', phase: sera }), 's.mp3');
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""La musica dei tavoli da carte all'oratorio (assets/audio/carte.mp3), sintetizzata da zero come il trio del biliardo:
|
||||||
|
un valzer da balera in sol maggiore. Fisarmonica a due voci un po' scordate fra loro (il «musette») sul due e sul tre,
|
||||||
|
basso pizzicato sull'uno, e una melodia originale di mandolino, col tremolo sulle note lunghe. Trentadue battute che
|
||||||
|
chiudono sul re settima: il loop non si sente. (Andrea, 26 ottobre: «come per il biliardo, una sua melodia».)
|
||||||
|
python3 tools/valzer_carte.py # scrive assets/audio/carte.mp3 (serve ffmpeg)
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import wave
|
||||||
|
from array import array
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SR = 22050
|
||||||
|
BPM = 138
|
||||||
|
BEAT = 60 / BPM
|
||||||
|
OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "carte.mp3"
|
||||||
|
|
||||||
|
# Un accordo per battuta: fondamentale del basso e note della fisarmonica.
|
||||||
|
HARMONY = {"G": ["B3", "D4", "G4"], "D7": ["C4", "D4", "F#4"], "G7": ["B3", "F4", "G4"], "C": ["C4", "E4", "G4"],
|
||||||
|
"Em": ["B3", "E4", "G4"], "Am": ["C4", "E4", "A4"], "E7": ["B3", "D4", "G#4"]}
|
||||||
|
BASS = {"G": ("G2", "D2"), "D7": ("D2", "A1"), "G7": ("G2", "F2"), "C": ("C2", "G1"), "Em": ("E2", "B1"), "Am": ("A1", "E2"),
|
||||||
|
"E7": ("E2", "B1")}
|
||||||
|
CHORDS = ("G G D7 D7 D7 D7 G G G G7 C C G D7 G G "
|
||||||
|
"Em Em Am Am D7 D7 G G C C G E7 Am D7 G D7").split()
|
||||||
|
# La melodia del mandolino, battuta per battuta: (nota, durata in movimenti); None è una pausa.
|
||||||
|
MELODY = [
|
||||||
|
[("B4", 2), ("D5", 1)], [("G5", 3)], [("F#5", 2), ("E5", 1)], [("D5", 2), ("C5", 1)],
|
||||||
|
[("A4", 2), ("C5", 1)], [("F#5", 3)], [("E5", 2), ("D5", 1)], [("B4", 3)],
|
||||||
|
[("B4", 1), ("D5", 1), ("G5", 1)], [("F5", 3)], [("E5", 2), ("G5", 1)], [("C6", 3)],
|
||||||
|
[("B5", 2), ("G5", 1)], [("A5", 1), ("F#5", 1), ("D5", 1)], [("G5", 3)], [(None, 2), ("D5", 1)],
|
||||||
|
[("G5", 2), ("F#5", 1)], [("E5", 3)], [("C5", 1), ("E5", 1), ("A5", 1)], [("A5", 2), ("G5", 1)],
|
||||||
|
[("F#5", 2), ("E5", 1)], [("D5", 1), ("F#5", 1), ("A5", 1)], [("B5", 3)], [("G5", 2), ("B4", 1)],
|
||||||
|
[("C5", 1), ("E5", 1), ("G5", 1)], [("E5", 3)], [("D5", 2), ("G5", 1)], [("G#5", 2), ("B5", 1)],
|
||||||
|
[("A5", 2), ("C6", 1)], [("A5", 1), ("F#5", 1), ("C5", 1)], [("B4", 3)], [("D5", 1), ("E5", 1), ("F#5", 1)],
|
||||||
|
]
|
||||||
|
assert len(MELODY) == len(CHORDS) and all(sum(d for _, d in bar) == 3 for bar in MELODY)
|
||||||
|
DUR = len(CHORDS) * 3 * BEAT
|
||||||
|
buf = [0.0] * int(SR * DUR)
|
||||||
|
rng = random.Random(7)
|
||||||
|
|
||||||
|
|
||||||
|
def midi(note: str) -> int:
|
||||||
|
names = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
||||||
|
semis = names[note[0]] + (1 if "#" in note else -1 if len(note) > 2 and note[1] == "b" else 0)
|
||||||
|
return semis + 12 * (int(note[-1]) + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def hz(note: str) -> float:
|
||||||
|
return 440 * 2 ** ((midi(note) - 69) / 12)
|
||||||
|
|
||||||
|
|
||||||
|
def add(start: float, samples) -> None:
|
||||||
|
"""Somma un suono alla traccia; la coda che esce dalla fine rientra all'inizio (loop continuo)."""
|
||||||
|
n0 = int(start * SR)
|
||||||
|
for i, v in enumerate(samples):
|
||||||
|
buf[(n0 + i) % len(buf)] += v
|
||||||
|
|
||||||
|
|
||||||
|
def tone(freq: float, length: float, vol: float, partials, decay: float, attack: float = 0.006, release: float = 0.06):
|
||||||
|
n = int((length + release) * SR)
|
||||||
|
out = []
|
||||||
|
for i in range(n):
|
||||||
|
t = i / SR
|
||||||
|
env = min(1.0, t / attack) * math.exp(-t / decay)
|
||||||
|
if t > length:
|
||||||
|
env *= max(0.0, 1 - (t - length) / release)
|
||||||
|
out.append(vol * env * sum(a * math.exp(-t / d) * math.sin(2 * math.pi * freq * k * t) for k, a, d in partials))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
at = lambda beat: beat * BEAT # noqa: E731
|
||||||
|
LONG = 99
|
||||||
|
# Fisarmonica: ance ricche di armonici, che non si spengono finché il mantice spinge.
|
||||||
|
REED = ((1, 1.0, LONG), (2, 0.55, LONG), (3, 0.38, LONG), (4, 0.22, LONG), (5, 0.12, LONG), (6, 0.06, LONG))
|
||||||
|
# Mandolino: corda pizzicata, armonici alti che muoiono subito.
|
||||||
|
PLUCK = ((1, 1.0, LONG), (2, 0.5, 0.25), (3, 0.3, 0.12), (4, 0.15, 0.06))
|
||||||
|
|
||||||
|
for bar, name in enumerate(CHORDS):
|
||||||
|
b0 = bar * 3
|
||||||
|
# Basso sull'uno: la fondamentale, una battuta sì e una no la quinta.
|
||||||
|
add(at(b0), tone(hz(BASS[name][bar % 2]), BEAT * 0.9, 0.3, ((1, 1.0, LONG), (2, 0.4, 0.3), (3, 0.15, 0.15)), decay=0.5))
|
||||||
|
# Fisarmonica sul due e sul tre: due ance, una un filo crescente, che battono fra loro.
|
||||||
|
for beat in (1, 2):
|
||||||
|
for note in HARMONY[name]:
|
||||||
|
for detune, vol in ((1.0, 0.03), (1.0035, 0.022)):
|
||||||
|
add(at(b0 + beat), tone(hz(note) * detune, BEAT * 0.62, vol, REED, decay=LONG, attack=0.02, release=0.05))
|
||||||
|
|
||||||
|
beat = 0.0
|
||||||
|
for bar in MELODY:
|
||||||
|
for note, length in bar:
|
||||||
|
if note:
|
||||||
|
if length >= 2:
|
||||||
|
# Le note lunghe col tremolo del plettro: tanti colpi fitti, sempre più piano.
|
||||||
|
picks = int(length * BEAT / 0.068)
|
||||||
|
for k in range(picks):
|
||||||
|
add(at(beat) + k * 0.068, tone(hz(note), 0.068, 0.07 * (1 - 0.35 * k / picks) * (1.15 if k == 0 else 1),
|
||||||
|
PLUCK, decay=0.12, attack=0.002, release=0.02))
|
||||||
|
else:
|
||||||
|
add(at(beat), tone(hz(note), BEAT * length * 0.95, 0.1, PLUCK, decay=0.35, attack=0.002, release=0.05))
|
||||||
|
beat += length
|
||||||
|
|
||||||
|
peak = max(abs(x) for x in buf) or 1
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
w = Path(tmp) / "carte.wav"
|
||||||
|
with wave.open(str(w), "wb") as f:
|
||||||
|
f.setnchannels(1)
|
||||||
|
f.setsampwidth(2)
|
||||||
|
f.setframerate(SR)
|
||||||
|
f.writeframes(array("h", (int(x / peak * 0.85 * 32767) for x in buf)).tobytes())
|
||||||
|
# Il cortile dell'oratorio: un po' d'eco dai muri, niente alti taglienti.
|
||||||
|
af = "aecho=0.8:0.55:60|130:0.2|0.12,lowpass=f=8000,volume=1.25"
|
||||||
|
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(w), "-af", af, "-c:a", "libmp3lame", "-b:a", "96k", str(OUT)], check=True)
|
||||||
|
print(f"-> {OUT} ({DUR:.0f} s)")
|
||||||
+24
-1
@@ -22,10 +22,16 @@ OUT = VIDEO / "out"
|
|||||||
# Area del gioco nello schermo della VM (Chromium a tutto schermo, zoom 125 %): misurata con «convert -trim».
|
# Area del gioco nello schermo della VM (Chromium a tutto schermo, zoom 125 %): misurata con «convert -trim».
|
||||||
TAGLIO = "crop=1044:760:278:70"
|
TAGLIO = "crop=1044:760:278:70"
|
||||||
FASCIA = 110 # px sopra il gioco per le didascalie
|
FASCIA = 110 # px sopra il gioco per le didascalie
|
||||||
|
# I video «Come si gioca» (--tutorial): ai tavoli la pagina usa tutta l'altezza, comandi sotto compresi; didascalie
|
||||||
|
# lunghe anche su tre righe.
|
||||||
|
TAGLIO_TUTORIAL = "crop=1046:900:268:0"
|
||||||
|
FASCIA_TUTORIAL = 132
|
||||||
FONDO = "0x1B1420" # il colore di fondo della pagina del gioco
|
FONDO = "0x1B1420" # il colore di fondo della pagina del gioco
|
||||||
VOLUME = 0.5 # musica della demo-prototipo (senza voci)
|
VOLUME = 0.5 # musica della demo-prototipo (senza voci)
|
||||||
VOLUME_SOTTO = 0.22 # musica sotto le voci: bassa (Andrea, 25 ottobre)
|
VOLUME_SOTTO = 0.22 # musica sotto le voci: bassa (Andrea, 25 ottobre)
|
||||||
VOLUME_VOCI = 1.0
|
VOLUME_VOCI = 1.0
|
||||||
|
VOLUME_NARRATORE = 1.15
|
||||||
|
VOLUME_TUTORIAL = 0.07 # musica dei video «Come si gioca»: sottofondo # il narratore dei video «Come si gioca», sopra la musica bassa
|
||||||
FADE = 1.2
|
FADE = 1.2
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@ def ts(s: float) -> str:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
d = Path(sys.argv[1]).expanduser()
|
d = Path(sys.argv[1]).expanduser()
|
||||||
|
tutorial = "--tutorial" in sys.argv[2:]
|
||||||
|
taglio, fascia = (TAGLIO_TUTORIAL, FASCIA_TUTORIAL) if tutorial else (TAGLIO, FASCIA)
|
||||||
meta = json.loads((d / "cues.json").read_text())
|
meta = json.loads((d / "cues.json").read_text())
|
||||||
nome = meta["clip"]
|
nome = meta["clip"]
|
||||||
registro = OUT / f"{nome}-audio.json"
|
registro = OUT / f"{nome}-audio.json"
|
||||||
@@ -59,6 +67,10 @@ def main() -> None:
|
|||||||
aperta["fine"] = e["t"]
|
aperta["fine"] = e["t"]
|
||||||
aperta = None
|
aperta = None
|
||||||
volume = VOLUME_SOTTO
|
volume = VOLUME_SOTTO
|
||||||
|
# Nei video «Come si gioca» parla il narratore quasi sempre: la musica resta un sottofondo (Andrea, 26 ottobre:
|
||||||
|
# «troppo alta la musica del gioco»).
|
||||||
|
if (OUT / f"{nome}-narrazione.json").exists():
|
||||||
|
volume = VOLUME_TUTORIAL
|
||||||
else:
|
else:
|
||||||
musica = json.loads((OUT / "musica.json").read_text())
|
musica = json.loads((OUT / "musica.json").read_text())
|
||||||
volume = VOLUME
|
volume = VOLUME
|
||||||
@@ -105,9 +117,20 @@ def main() -> None:
|
|||||||
lungo = f"atrim=0:{v['fine'] - v['t'] + 0.1:.3f}," if "fine" in v else ""
|
lungo = f"atrim=0:{v['fine'] - v['t'] + 0.1:.3f}," if "fine" in v else ""
|
||||||
catene.append(f"[{n}:a]{lungo}asetpts=PTS-STARTPTS,volume={VOLUME_VOCI},adelay={int((v['t'] + scarto) * 1000)}:all=1[v{len(pezzi)}]")
|
catene.append(f"[{n}:a]{lungo}asetpts=PTS-STARTPTS,volume={VOLUME_VOCI},adelay={int((v['t'] + scarto) * 1000)}:all=1[v{len(pezzi)}]")
|
||||||
pezzi.append(f"[v{len(pezzi)}]")
|
pezzi.append(f"[v{len(pezzi)}]")
|
||||||
|
# Il narratore dei video «Come si gioca» (video/narra.py): ogni frase all'inizio della sua battuta.
|
||||||
|
narrazione = OUT / f"{nome}-narrazione.json"
|
||||||
|
if narrazione.exists():
|
||||||
|
frasi = {v["id"]: v["src"] for v in json.loads(narrazione.read_text())["voci"]}
|
||||||
|
for c in cues:
|
||||||
|
if c["id"] not in frasi:
|
||||||
|
continue
|
||||||
|
ingressi += ["-i", str(GIOCO / frasi[c["id"]])]
|
||||||
|
n = ingressi.count("-i")
|
||||||
|
catene.append(f"[{n}:a]asetpts=PTS-STARTPTS,volume={VOLUME_NARRATORE},adelay={int((c['start'] + scarto) * 1000)}:all=1[n{len(pezzi)}]")
|
||||||
|
pezzi.append(f"[n{len(pezzi)}]")
|
||||||
mix = "".join(pezzi) + f"amix=inputs={len(pezzi)}:normalize=0:duration=longest[a]"
|
mix = "".join(pezzi) + f"amix=inputs={len(pezzi)}:normalize=0:duration=longest[a]"
|
||||||
stile = f"FontName=DejaVu Sans,FontSize=11,Bold=1,MarginV=4,Outline=1,Shadow=0,PrimaryColour=&H007AD2FF"
|
stile = f"FontName=DejaVu Sans,FontSize=11,Bold=1,MarginV=4,Outline=1,Shadow=0,PrimaryColour=&H007AD2FF"
|
||||||
video = f"[0:v]{TAGLIO},pad=iw:ih+{FASCIA}:0:{FASCIA}:{FONDO},subtitles={srt}:force_style='{stile}'[v]"
|
video = f"[0:v]{taglio},pad=iw:ih+{fascia}:0:{fascia}:{FONDO},subtitles={srt}:force_style='{stile}'[v]"
|
||||||
finale = OUT / f"{nome}.mp4"
|
finale = OUT / f"{nome}.mp4"
|
||||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(raw), *ingressi,
|
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(raw), *ingressi,
|
||||||
"-filter_complex", ";".join([video, *catene, mix]), "-map", "[v]", "-map", "[a]",
|
"-filter_complex", ";".join([video, *catene, mix]), "-map", "[v]", "-map", "[a]",
|
||||||
|
|||||||
+4
-1
@@ -36,7 +36,10 @@ def main() -> None:
|
|||||||
# XTTS dice «punto» se la frase finisce col punto: si toglie, come fa narrate.py del tour.
|
# XTTS dice «punto» se la frase finisce col punto: si toglie, come fa narrate.py del tour.
|
||||||
lavori = [{"text": c["it"].rstrip(". "), "out": str(cartella / f"{c['id']}.wav"), "lang": "it", "speaker": NARRATORE} for c in cues]
|
lavori = [{"text": c["it"].rstrip(". "), "out": str(cartella / f"{c['id']}.wav"), "lang": "it", "speaker": NARRATORE} for c in cues]
|
||||||
(cartella / "lavori.json").write_text(json.dumps(lavori, ensure_ascii=False, indent=1))
|
(cartella / "lavori.json").write_text(json.dumps(lavori, ensure_ascii=False, indent=1))
|
||||||
pron = ["--pronounce", str(PRONUNCIA)] if PRONUNCIA.exists() else []
|
# Le pronunce del gioco più quelle del narratore, in un file solo per `mbot parla`.
|
||||||
|
regole = {k: v for f in (PRONUNCIA, VIDEO / "pronuncia-narratore.json") if f.exists() for k, v in json.loads(f.read_text()).items() if not k.startswith("_")}
|
||||||
|
(cartella / "pronuncia.json").write_text(json.dumps(regole, ensure_ascii=False, indent=1))
|
||||||
|
pron = ["--pronounce", str(cartella / "pronuncia.json")]
|
||||||
subprocess.run(["mbot", "parla", "--jobs", str(cartella / "lavori.json"), *pron], check=True)
|
subprocess.run(["mbot", "parla", "--jobs", str(cartella / "lavori.json"), *pron], check=True)
|
||||||
ascolto = [{"file": l["out"], "expect": l["text"]} for l in lavori]
|
ascolto = [{"file": l["out"], "expect": l["text"]} for l in lavori]
|
||||||
(cartella / "ascolto.json").write_text(json.dumps(ascolto, ensure_ascii=False, indent=1))
|
(cartella / "ascolto.json").write_text(json.dumps(ascolto, ensure_ascii=False, indent=1))
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"_nota": "Pronunce in più per il narratore dei video «Come si gioca» (video/narra.py), oltre a tools/pronuncia.json. L'accento scritto sposta quello di XTTS (Andrea, 26 ottobre: «piccola» e «volta» con l'accento sbagliato).",
|
||||||
|
"piccola": "pìccola",
|
||||||
|
"piccole": "pìccole",
|
||||||
|
"volta": "vòlta",
|
||||||
|
"volte": "vòlte"
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export const cues = [
|
|||||||
c("pesca", "Chi prende pesca per primo ed esce alla mano dopo. E non c'è l'obbligo di rispondere al seme"),
|
c("pesca", "Chi prende pesca per primo ed esce alla mano dopo. E non c'è l'obbligo di rispondere al seme"),
|
||||||
c("consiglio", "Un consiglio: su un liscio rispondi con un liscio, e i carichi dell'avversario prendili con una briscola piccola"),
|
c("consiglio", "Un consiglio: su un liscio rispondi con un liscio, e i carichi dell'avversario prendili con una briscola piccola"),
|
||||||
c("tastiera", "Si gioca toccando la carta, oppure coi pulsanti sotto il tavolo, o con le frecce e Invio"),
|
c("tastiera", "Si gioca toccando la carta, oppure coi pulsanti sotto il tavolo, o con le frecce e Invio"),
|
||||||
c("conto", "Finito il mazzo si contano le prese, una carta alla volta. Vince chi arriva a sessantuno punti, e sessanta a sessanta è patta"),
|
c("conto", "Finito il mazzo si contano le prese, una carta alla volta. Vince chi arriva a sessantuno punti; sessanta a sessanta è pari, e non vince nessuno"),
|
||||||
c("fine", "Poi si torna all'oratorio, e si può chiedere la rivincita. Buona briscola!"),
|
c("fine", "Poi si torna all'oratorio, e si può chiedere la rivincita. Buona briscola!"),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export const setup = base;
|
|||||||
|
|
||||||
export async function run(d) {
|
export async function run(d) {
|
||||||
const p = d.page, r = regista(d, "tutorial-briscola");
|
const p = d.page, r = regista(d, "tutorial-briscola");
|
||||||
|
// Il registro dell'audio parte con la ripresa: monta.py rimette le voci del gioco al loro istante.
|
||||||
await r.ascolta();
|
await r.ascolta();
|
||||||
await r.vaiA("oratorio", [470, 300]);
|
await r.vaiA("oratorio", [470, 300]);
|
||||||
await r.cue("dove");
|
await r.cue("dove");
|
||||||
@@ -30,12 +31,14 @@ export async function run(d) {
|
|||||||
await r.scegli("Con le toscane.");
|
await r.scegli("Con le toscane.");
|
||||||
await r.cue("livello");
|
await r.cue("livello");
|
||||||
await r.scegli("Seria.");
|
await r.scegli("Seria.");
|
||||||
// Una mano fissa, la stessa a ogni ripresa.
|
// Una mano fissa, la stessa a ogni ripresa: giocando attento, Andrea vince 68 a 52.
|
||||||
await p.evaluate(async () => {
|
await p.evaluate(async () => {
|
||||||
const { newBriscola } = await import("/src/engine/briscola.js");
|
const { newBriscola } = await import("/src/engine/briscola.js");
|
||||||
game.closeup = { id: "briscola", ...newBriscola(91421, { level: "seria", deck: "toscane" }) };
|
game.closeup = { id: "briscola", ...newBriscola(8, { level: "seria", deck: "toscane" }) };
|
||||||
});
|
});
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
|
// Il tavolo ha la sua musica: se non parte, il gioco servito nella VM è vecchio.
|
||||||
|
await p.waitForFunction(() => window.music?.want === "assets/audio/carte.mp3", null, { timeout: 10000 });
|
||||||
await r.cue("tavolo");
|
await r.cue("tavolo");
|
||||||
await r.muovi([150, 160], 1.0); await sleep(600); await r.muovi([320, 280], 1.0);
|
await r.muovi([150, 160], 1.0); await sleep(600); await r.muovi([320, 280], 1.0);
|
||||||
|
|
||||||
@@ -69,6 +72,7 @@ export async function run(d) {
|
|||||||
await p.waitForFunction(() => game.closeup?.step === "result", null, { timeout: 60000 });
|
await p.waitForFunction(() => game.closeup?.step === "result", null, { timeout: 60000 });
|
||||||
await sleep(2500);
|
await sleep(2500);
|
||||||
await r.cue("fine");
|
await r.cue("fine");
|
||||||
|
await r.aspetta(); // la battuta del giocatore, uscendo, dopo quella del narratore
|
||||||
await d.click(p.locator("#closeup-close"), 0.6);
|
await d.click(p.locator("#closeup-close"), 0.6);
|
||||||
await r.fermi(800);
|
await r.fermi(800);
|
||||||
await r.fine();
|
await r.fine();
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export function narrazione(clip) {
|
|||||||
|
|
||||||
export async function setup(d) {
|
export async function setup(d) {
|
||||||
const p = d.page;
|
const p = d.page;
|
||||||
|
// Niente cache: il Chromium della VM teneva i moduli JavaScript di una ripresa prima (il valzer non partiva).
|
||||||
|
const cdp = await p.context().newCDPSession(p);
|
||||||
|
await cdp.send("Network.enable");
|
||||||
|
await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
|
||||||
|
await cdp.send("Network.clearBrowserCache");
|
||||||
await p.goto(URL_GIOCO);
|
await p.goto(URL_GIOCO);
|
||||||
await p.evaluate(() => localStorage.clear());
|
await p.evaluate(() => localStorage.clear());
|
||||||
await p.reload();
|
await p.reload();
|
||||||
@@ -26,13 +31,16 @@ export function regista(d, clip) {
|
|||||||
const p = d.page, voce = narrazione(clip);
|
const p = d.page, voce = narrazione(clip);
|
||||||
let inizio = 0, dura = 0;
|
let inizio = 0, dura = 0;
|
||||||
const r = {
|
const r = {
|
||||||
// Una battuta del narratore: prima si lascia finire quella di prima.
|
// Una battuta del narratore: prima si lascia finire quella di prima, e chi sta parlando nel gioco.
|
||||||
async cue(id) {
|
async cue(id) {
|
||||||
const resto = inizio + dura + 350 - Date.now();
|
await r.aspetta();
|
||||||
if (resto > 0) await sleep(resto);
|
for (let i = 0; i < 300 && (await p.evaluate(() => !!game.speech || !!(window.voices?.audio && !window.voices.audio.paused && !window.voices.audio.ended))); i++) await sleep(100);
|
||||||
|
await sleep(250);
|
||||||
await d.cue(id);
|
await d.cue(id);
|
||||||
inizio = Date.now(); dura = voce[id] ?? 0;
|
inizio = Date.now(); dura = voce[id] ?? 0;
|
||||||
},
|
},
|
||||||
|
// Prima di far parlare un personaggio, il narratore deve aver finito: le due voci non si sovrappongono.
|
||||||
|
async aspetta() { const resto = inizio + dura + 350 - Date.now(); if (resto > 0) await sleep(resto); },
|
||||||
async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); },
|
async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); },
|
||||||
// Puntatore vero sulla scena (coordinate 640×360).
|
// Puntatore vero sulla scena (coordinate 640×360).
|
||||||
async punto(x, y) {
|
async punto(x, y) {
|
||||||
@@ -54,7 +62,7 @@ export function regista(d, clip) {
|
|||||||
}
|
}
|
||||||
await sleep(pausa);
|
await sleep(pausa);
|
||||||
},
|
},
|
||||||
async scegli(testo) { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); },
|
async scegli(testo) { await r.aspetta(); await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); },
|
||||||
// Partita nuova per Campomigliaio, introduzione saltata, dritti nella scena del gioco.
|
// Partita nuova per Campomigliaio, introduzione saltata, dritti nella scena del gioco.
|
||||||
async vaiA(scena, at) {
|
async vaiA(scena, at) {
|
||||||
await d.click(p.locator("#new-game"));
|
await d.click(p.locator("#new-game"));
|
||||||
@@ -62,10 +70,12 @@ export function regista(d, clip) {
|
|||||||
await d.click(p.locator('#rione [data-rione="campomigliaio"]'));
|
await d.click(p.locator('#rione [data-rione="campomigliaio"]'));
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
if (await p.isVisible("#skip-intro")) await d.click(p.locator("#skip-intro"), 0.5);
|
if (await p.isVisible("#skip-intro")) await d.click(p.locator("#skip-intro"), 0.5);
|
||||||
await p.evaluate(([s, a]) => { game.go(s, a); }, [scena, at]);
|
// Le battute d'apertura di Andrea (il palio sparito) qui non servono: si saltano tutte.
|
||||||
|
await p.evaluate(([s, a]) => { for (let i = 0; i < 50 && (game.speech || game.queue.length); i++) { game.skip(); game.update(0.05); } game.go(s, a); }, [scena, at]);
|
||||||
await r.fermi(1200);
|
await r.fermi(1200);
|
||||||
},
|
},
|
||||||
async parlaCon(id) {
|
async parlaCon(id) {
|
||||||
|
await r.aspetta();
|
||||||
const [x, y] = await p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id);
|
const [x, y] = await p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id);
|
||||||
await r.clicca([x, y]);
|
await r.clicca([x, y]);
|
||||||
await r.fermi();
|
await r.fermi();
|
||||||
|
|||||||
Reference in New Issue
Block a user