Giochi di carte dopo la revisione di Codex sui tre insieme; player dei video «Come si gioca» e primo copione
Esito che vale e si salva appena deciso, per tutti i minigiochi (anche tornando al titolo dalla schermata del risultato); briscola: uscire durante il conteggio non perde più la vittoria; tressette: si esce ai 21 col primo evento che ci porta, accusa compresa (prima vinceva sempre Andrea se ci arrivavano tutte e due), il carico che fa vincere si dà anche se è il 2, accuse che non escono dal tavolo, convenzioni scritte nell'aiuto; scopa: primiera valutata per quel che migliora, finale esatto a mazzo finito, presa sulla tavola al rilascio con scelta e conferma, frecce e Invio sempre sulla presa scelta. Tastiera: Invio e Spazio giocano sempre la carta scelta. Tavola descritta a parole. Classi CSS comuni ai tre giochi. Video: player nel gioco (pulsante «Guarda il video» nel «Come si gioca», compare quando il video esiste), video/narra.py per la voce del narratore, pezzi comuni dei copioni e il copione della briscola; scaletta in video/SCALETTA-CARTE.md 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,58 @@
|
||||
# Video «Come si gioca» — briscola, tressette ai 21, scopa, bocce, biliardo (scaletta)
|
||||
|
||||
Cinque clip brevi, una per gioco, con voce italiana (XTTS) e sottotitoli impressi. Si guardano dentro il gioco:
|
||||
un pulsante «Guarda il video» nel «Come si gioca» di ogni gioco apre un player con l'audio.
|
||||
Le mani sono preparate dal copione (semi fissi), così ogni regola si vede nel momento giusto.
|
||||
|
||||
## 1. Briscola (circa 2 minuti)
|
||||
|
||||
| # | Cosa si vede | Voce |
|
||||
|---|---|---|
|
||||
| 1 | Oratorio, Andrea parla al giocatore di carte, sceglie toscane e «Seria» | Dove si gioca, mazzo e livello |
|
||||
| 2 | Il tavolo: la mano, la briscola sotto il mazzo, il mazzo | Tre carte a testa, la briscola comanda |
|
||||
| 3 | Una presa col seme, una presa con la briscola | Chi prende: la più alta del seme, o la briscola |
|
||||
| 4 | Liscio su un carico, poi un carico preso con una briscolina | Asso 11, 3 dieci, regio 4, donna 3, gobbo 2 |
|
||||
| 5 | Fine mazzo e conteggio carta per carta | Si vince con 61, 60 a 60 è patta |
|
||||
|
||||
## 2. Tressette ai 21 (circa 3 minuti)
|
||||
|
||||
| # | Cosa si vede | Voce |
|
||||
|---|---|---|
|
||||
| 1 | «Un tressette ai 21?», i quattro al tavolo | In quattro a coppie, il socio di fronte |
|
||||
| 2 | Una presa in cui si risponde al seme, una carta rifiutata | Obbligo di seme; ordine 3, 2, Asso, figure |
|
||||
| 3 | Un'accusa scritta in alto | Napoletana, tre Assi/2/3, quattro |
|
||||
| 4 | Andrea esce con «Striscio», poi «Busso» e il socio che ritorna il seme, poi un «Volo» | I segni, coi pulsanti o B, S, V |
|
||||
| 5 | Riepilogo di smazzata e il «fuori» ai 21 | Terzi, ultima presa, si va fuori ai 21 |
|
||||
|
||||
## 3. Scopa (circa 2 minuti e mezzo)
|
||||
|
||||
| # | Cosa si vede | Voce |
|
||||
|---|---|---|
|
||||
| 1 | «Una scopa?», il tavolo con quattro carte | Valori: Asso 1 … regio 10 |
|
||||
| 2 | Una presa della carta uguale, una presa per somma | La carta uguale viene prima delle somme |
|
||||
| 3 | Due prese possibili: Andrea sceglie | Se le prese sono più d'una, scegli tu |
|
||||
| 4 | Una scopa | Pulire la tavola fa un punto |
|
||||
| 5 | Riepilogo: carte, mattoni, settebello, primiera, scope | Agli 11 |
|
||||
|
||||
## 4. Bocce al pallaio (circa 2 minuti)
|
||||
|
||||
| # | Cosa si vede | Voce |
|
||||
|---|---|---|
|
||||
| 1 | Brento Sanico, Andrea parla con Mariano, sceglie il livello | Dove si gioca e i livelli |
|
||||
| 2 | Mira toccando il campo e il dettaglio del pallino, il cerchio azzurro | Come si mira |
|
||||
| 3 | La forza col cursore e «Lancia»; poi tenere premuto e lasciare | Due modi di tirare, anche frecce e Spazio |
|
||||
| 4 | Una boccia che si accosta, una bocciata di Mariano | Tira chi non ha il punto |
|
||||
| 5 | Fine mano e punti | Tre bocce a testa, ogni boccia più vicina della migliore avversaria fa un punto |
|
||||
|
||||
## 5. Biliardo, cinque birilli (circa 2 minuti e mezzo)
|
||||
|
||||
| # | Cosa si vede | Voce |
|
||||
|---|---|---|
|
||||
| 1 | Circolo 4 Maggio, l'Ing. Tozzi, il livello | Dove si gioca |
|
||||
| 2 | Il tavolo: le due biglie, il pallino, il castello dei birilli | Si colpisce la propria biglia e la si manda sull'altra |
|
||||
| 3 | La mira: la linea tratteggiata, le frecce dopo l'urto; clic che fissa la mira | Come si mira, Maiusc per la mira fine |
|
||||
| 4 | Un colpo che abbatte un birillo bianco, poi il rosso | Bianco 2, rosso 4, rosso da solo 10, pallino 3 |
|
||||
| 5 | Un fallo (mancare la biglia) | I falli danno i punti all'avversario |
|
||||
| 6 | Il segnapunti e la vittoria a 20 | Si va a 20 |
|
||||
|
||||
Prima di girare: le correzioni della revisione di Codex sui tre giochi, così il video mostra il gioco definitivo.
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""La voce del narratore dei video «Come si gioca» (video/SCALETTA-CARTE.md), PRIMA di girare.
|
||||
|
||||
python3 video/narra.py video/tutorial-briscola.mjs
|
||||
|
||||
Legge le battute del copione (cues, «it»), le fa dire a XTTS con `mbot parla` (voce del narratore, diversa da
|
||||
quelle dei personaggi), le controlla con `mbot ascolta` e scrive video/out/<clip>-narrazione.json con file e
|
||||
durata di ognuna. Il copione aspetta che la frase sia finita prima di passare alla successiva; monta.py la mette
|
||||
nel video all'inizio della sua battuta. Le frasi già fatte si saltano (per rifarne una: cancellare il file).
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VIDEO = Path(__file__).resolve().parent
|
||||
GIOCO = VIDEO.parent
|
||||
OUT = VIDEO / "out"
|
||||
NARRATORE = "Claribel Dervla"
|
||||
PRONUNCIA = GIOCO / "tools" / "pronuncia.json"
|
||||
|
||||
|
||||
def durata(f: Path) -> float:
|
||||
return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(f)],
|
||||
capture_output=True, text=True, check=True).stdout)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
copione = Path(sys.argv[1]).resolve()
|
||||
clip = copione.stem
|
||||
cues = json.loads(subprocess.run(["node", "--input-type=module", "-e",
|
||||
f"const m = await import({json.dumps(copione.as_uri())}); console.log(JSON.stringify(m.cues))"],
|
||||
capture_output=True, text=True, check=True, cwd=GIOCO).stdout)
|
||||
cartella = OUT / f"{clip}-narrazione"
|
||||
cartella.mkdir(parents=True, exist_ok=True)
|
||||
# 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]
|
||||
(cartella / "lavori.json").write_text(json.dumps(lavori, ensure_ascii=False, indent=1))
|
||||
pron = ["--pronounce", str(PRONUNCIA)] if PRONUNCIA.exists() else []
|
||||
subprocess.run(["mbot", "parla", "--jobs", str(cartella / "lavori.json"), *pron], check=True)
|
||||
ascolto = [{"file": l["out"], "expect": l["text"]} for l in lavori]
|
||||
(cartella / "ascolto.json").write_text(json.dumps(ascolto, ensure_ascii=False, indent=1))
|
||||
subprocess.run(["mbot", "ascolta", "--jobs", str(cartella / "ascolto.json")])
|
||||
voci = [{"id": c["id"], "src": str(Path(l["out"]).relative_to(GIOCO)), "sec": round(durata(Path(l["out"])), 2)} for c, l in zip(cues, lavori)]
|
||||
(OUT / f"{clip}-narrazione.json").write_text(json.dumps({"voci": voci}, ensure_ascii=False, indent=1))
|
||||
print(f"-> {OUT / f'{clip}-narrazione.json'}: {len(voci)} frasi, {sum(v['sec'] for v in voci):.0f} s di voce")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copione del video «Come si gioca: la briscola» (video/SCALETTA-CARTE.md, clip 1).
|
||||
// Giro: video/narra.py, poi rec.mjs come in video/SCALETTA.md, poi video/monta.py <cartella> --tutorial.
|
||||
import { c, sleep, setup as base, regista } from "./tutorial-comune.mjs";
|
||||
|
||||
export const title = { it: "Come si gioca · la briscola", en: "Come si gioca · la briscola" };
|
||||
export const cues = [
|
||||
c("dove", "All'oratorio della Pieve, alle tavolate, c'è il giocatore di carte: per giocare basta parlarci"),
|
||||
c("mazzo", "Prima si sceglie il mazzo: le carte toscane, con gobbo, donna e regio, oppure le napoletane"),
|
||||
c("livello", "Poi quanto deve giocare forte: alla bona, seria, o a fassi male"),
|
||||
c("tavolo", "In basso le tue tre carte, in alto quelle coperte dell'avversario. A sinistra il mazzo, con la briscola girata sotto: quel seme comanda"),
|
||||
c("presa", "Si gioca una carta per uno. Prende la briscola più alta; se non ce ne sono, la carta più alta del seme di chi è uscito"),
|
||||
c("punti", "Contano l'Asso, undici punti, il tre, dieci, il regio quattro, la donna tre e il gobbo due. Le altre sono lisci e non valgono niente"),
|
||||
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("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("fine", "Poi si torna all'oratorio, e si può chiedere la rivincita. Buona briscola!"),
|
||||
];
|
||||
|
||||
export const setup = base;
|
||||
|
||||
export async function run(d) {
|
||||
const p = d.page, r = regista(d, "tutorial-briscola");
|
||||
await r.ascolta();
|
||||
await r.vaiA("oratorio", [470, 300]);
|
||||
await r.cue("dove");
|
||||
await r.parlaCon("giocatore_carte");
|
||||
await r.scegli("Una briscola?");
|
||||
await r.cue("mazzo");
|
||||
await r.scegli("Con le toscane.");
|
||||
await r.cue("livello");
|
||||
await r.scegli("Seria.");
|
||||
// Una mano fissa, la stessa a ogni ripresa.
|
||||
await p.evaluate(async () => {
|
||||
const { newBriscola } = await import("/src/engine/briscola.js");
|
||||
game.closeup = { id: "briscola", ...newBriscola(91421, { level: "seria", deck: "toscane" }) };
|
||||
});
|
||||
await sleep(800);
|
||||
await r.cue("tavolo");
|
||||
await r.muovi([150, 160], 1.0); await sleep(600); await r.muovi([320, 280], 1.0);
|
||||
|
||||
// Una carta di Andrea: quella che sceglierebbe un giocatore attento, cliccata davvero sul tavolo.
|
||||
const gioca = async () => {
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const s = await p.evaluate(async () => {
|
||||
const { canPlay } = await import("/src/engine/briscola.js");
|
||||
return game.closeup?.step === "result" || game.closeup?.step === "count" ? "fine" : canPlay(game.closeup) && !game.speech ? "tocca" : "aspetta";
|
||||
});
|
||||
if (s === "fine") return false;
|
||||
if (s === "tocca") break;
|
||||
await sleep(150);
|
||||
}
|
||||
const i = await p.evaluate(async () => {
|
||||
const { observation, attentive } = await import("/src/engine/briscola.js");
|
||||
return attentive(observation(game.closeup, "player"));
|
||||
});
|
||||
await r.clicca([176 + i * 102 + 42, 290], 0.7);
|
||||
return true;
|
||||
};
|
||||
const giocane = async (n) => { for (let k = 0; k < n; k++) if (!(await gioca())) return; };
|
||||
|
||||
await r.cue("presa"); await giocane(2);
|
||||
await r.cue("punti"); await giocane(2);
|
||||
await r.cue("pesca"); await giocane(2);
|
||||
await r.cue("consiglio"); await giocane(2);
|
||||
await r.cue("tastiera"); await giocane(20);
|
||||
await p.waitForFunction(() => game.closeup?.step === "count", null, { timeout: 60000 });
|
||||
await r.cue("conto");
|
||||
await p.waitForFunction(() => game.closeup?.step === "result", null, { timeout: 60000 });
|
||||
await sleep(2500);
|
||||
await r.cue("fine");
|
||||
await d.click(p.locator("#closeup-close"), 0.6);
|
||||
await r.fermi(800);
|
||||
await r.fine();
|
||||
await r.salvaAudio();
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Pezzi comuni dei copioni «Come si gioca» (video/SCALETTA-CARTE.md): briscola, tressette, scopa, bocce, biliardo.
|
||||
// Si girano come demo-prototipo.mjs (video/SCALETTA.md, «Come si rigira»); prima `python3 video/narra.py <copione>`
|
||||
// per la voce del narratore, dopo `python3 video/monta.py <cartella> --tutorial`.
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
export const URL_GIOCO = "http://127.0.0.1:8000/";
|
||||
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
export const c = (id, it) => ({ id, it, en: it, top: true });
|
||||
|
||||
// La durata di ogni frase del narratore (video/narra.py): la battuta resta finché la voce non ha finito.
|
||||
export function narrazione(clip) {
|
||||
const f = new URL(`./out/${clip}-narrazione.json`, import.meta.url);
|
||||
return existsSync(f) ? Object.fromEntries(JSON.parse(readFileSync(f, "utf8")).voci.map((v) => [v.id, v.sec * 1000])) : {};
|
||||
}
|
||||
|
||||
export async function setup(d) {
|
||||
const p = d.page;
|
||||
await p.goto(URL_GIOCO);
|
||||
await p.evaluate(() => localStorage.clear());
|
||||
await p.reload();
|
||||
await p.waitForSelector("#title:not([hidden])");
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
export function regista(d, clip) {
|
||||
const p = d.page, voce = narrazione(clip);
|
||||
let inizio = 0, dura = 0;
|
||||
const r = {
|
||||
// Una battuta del narratore: prima si lascia finire quella di prima.
|
||||
async cue(id) {
|
||||
const resto = inizio + dura + 350 - Date.now();
|
||||
if (resto > 0) await sleep(resto);
|
||||
await d.cue(id);
|
||||
inizio = Date.now(); dura = voce[id] ?? 0;
|
||||
},
|
||||
async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); },
|
||||
// Puntatore vero sulla scena (coordinate 640×360).
|
||||
async punto(x, y) {
|
||||
const b = await p.locator("#scene").boundingBox(), k = await p.evaluate(() => devicePixelRatio);
|
||||
return [(b.x + (x / 640) * b.width) * k, (b.y + (y / 360) * b.height) * k];
|
||||
},
|
||||
async clicca([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur} click`); await sleep(300); },
|
||||
async muovi([x, y], dur = 0.8) { const [a, b] = await r.punto(x, y); d.vm(`DISPLAY=:0 ~/lab/video/mv.py ${a} ${b} ${dur}`); await sleep(200); },
|
||||
// Aspetta che l'omino abbia finito di camminare e di parlare; un dialogo o un primo piano aperti fermano l'attesa.
|
||||
async fermi(pausa = 500) {
|
||||
await sleep(150);
|
||||
for (let i = 0; i < 1500; i++) {
|
||||
const ok = await p.evaluate(() => {
|
||||
if (game.speech || (game.queue.length && !game.closeup) || game.walking || game.reach > 0) return false;
|
||||
return !!game.dialogue || !!game.closeup || (!game.busy && !game.task);
|
||||
});
|
||||
if (ok) break;
|
||||
await sleep(100);
|
||||
}
|
||||
await sleep(pausa);
|
||||
},
|
||||
async scegli(testo) { 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.
|
||||
async vaiA(scena, at) {
|
||||
await d.click(p.locator("#new-game"));
|
||||
await sleep(1200);
|
||||
await d.click(p.locator('#rione [data-rione="campomigliaio"]'));
|
||||
await sleep(800);
|
||||
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]);
|
||||
await r.fermi(1200);
|
||||
},
|
||||
async parlaCon(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.fermi();
|
||||
},
|
||||
// L'audio annotato dentro la pagina (musica voluta e voci del gioco), per rimontarlo (monta.py).
|
||||
async ascolta() {
|
||||
await p.evaluate(() => {
|
||||
window.__t0 = Date.now(); window.__audio = [];
|
||||
let music = undefined, voice = null;
|
||||
const t = () => (Date.now() - window.__t0) / 1000;
|
||||
const path = (src) => decodeURI(new URL(src, location.href).pathname.slice(1));
|
||||
const look = () => {
|
||||
if (window.music && window.music.want !== music) { music = window.music.want; window.__audio.push({ t: t(), music: music ?? null }); }
|
||||
const a = window.voices?.audio, now = a && !a.paused && !a.ended ? a : null;
|
||||
if (now !== voice) {
|
||||
if (voice) window.__audio.push({ t: t(), voiceEnd: true });
|
||||
if (now) window.__audio.push({ t: t(), voice: path(now.src) });
|
||||
voice = now;
|
||||
}
|
||||
requestAnimationFrame(look);
|
||||
};
|
||||
look();
|
||||
});
|
||||
},
|
||||
async salvaAudio() {
|
||||
const audio = await p.evaluate(() => window.__audio);
|
||||
const out = new URL("./out/", import.meta.url);
|
||||
mkdirSync(out, { recursive: true });
|
||||
writeFileSync(new URL(`${clip}-audio.json`, out), JSON.stringify({ eventi: audio }, null, 2));
|
||||
},
|
||||
};
|
||||
return r;
|
||||
}
|
||||
Reference in New Issue
Block a user