diff --git a/assets/characters/pierino_parigi.json b/assets/characters/pierino_parigi.json
new file mode 100644
index 0000000..371357f
--- /dev/null
+++ b/assets/characters/pierino_parigi.json
@@ -0,0 +1,70 @@
+{
+ "image": "pierino_parigi.png",
+ "height": 120,
+ "frames": {
+ "explain": {
+ "x": 0,
+ "y": 0,
+ "w": 61,
+ "h": 120,
+ "ax": 29.3,
+ "ay": 120
+ },
+ "talk": {
+ "x": 63,
+ "y": 0,
+ "w": 69,
+ "h": 120,
+ "ax": 26.6,
+ "ay": 120
+ },
+ "blink": {
+ "x": 134,
+ "y": 0,
+ "w": 44,
+ "h": 120,
+ "ax": 25.0,
+ "ay": 120
+ },
+ "stand": {
+ "x": 180,
+ "y": 0,
+ "w": 44,
+ "h": 120,
+ "ax": 24.9,
+ "ay": 120
+ },
+ "cheer": {
+ "x": 226,
+ "y": 0,
+ "w": 66,
+ "h": 128,
+ "ax": 34.6,
+ "ay": 128
+ },
+ "shrug": {
+ "x": 294,
+ "y": 0,
+ "w": 89,
+ "h": 120,
+ "ax": 43.8,
+ "ay": 120
+ },
+ "cards": {
+ "x": 385,
+ "y": 0,
+ "w": 50,
+ "h": 120,
+ "ax": 26.2,
+ "ay": 120
+ },
+ "laugh": {
+ "x": 437,
+ "y": 0,
+ "w": 54,
+ "h": 123,
+ "ax": 24.4,
+ "ay": 123
+ }
+ }
+}
diff --git a/assets/characters/pierino_parigi.png b/assets/characters/pierino_parigi.png
new file mode 100644
index 0000000..b3d84ee
Binary files /dev/null and b/assets/characters/pierino_parigi.png differ
diff --git a/assets/characters/vannino.json b/assets/characters/vannino.json
new file mode 100644
index 0000000..f1a017d
--- /dev/null
+++ b/assets/characters/vannino.json
@@ -0,0 +1,70 @@
+{
+ "image": "vannino.png",
+ "height": 120,
+ "frames": {
+ "explain": {
+ "x": 0,
+ "y": 0,
+ "w": 56,
+ "h": 120,
+ "ax": 28.6,
+ "ay": 120
+ },
+ "talk": {
+ "x": 58,
+ "y": 0,
+ "w": 64,
+ "h": 120,
+ "ax": 25.6,
+ "ay": 120
+ },
+ "blink": {
+ "x": 124,
+ "y": 0,
+ "w": 45,
+ "h": 120,
+ "ax": 25.0,
+ "ay": 120
+ },
+ "stand": {
+ "x": 171,
+ "y": 0,
+ "w": 46,
+ "h": 120,
+ "ax": 25.3,
+ "ay": 120
+ },
+ "cheer": {
+ "x": 219,
+ "y": 0,
+ "w": 52,
+ "h": 120,
+ "ax": 26.8,
+ "ay": 120
+ },
+ "shrug": {
+ "x": 273,
+ "y": 0,
+ "w": 80,
+ "h": 120,
+ "ax": 40.7,
+ "ay": 120
+ },
+ "cards": {
+ "x": 355,
+ "y": 0,
+ "w": 45,
+ "h": 121,
+ "ax": 22.7,
+ "ay": 121
+ },
+ "laugh": {
+ "x": 402,
+ "y": 0,
+ "w": 46,
+ "h": 121,
+ "ax": 22.7,
+ "ay": 121
+ }
+ }
+}
diff --git a/assets/characters/vannino.png b/assets/characters/vannino.png
new file mode 100644
index 0000000..a51a333
Binary files /dev/null and b/assets/characters/vannino.png differ
diff --git a/assets/closeups/cruscotto-tempo.png b/assets/closeups/cruscotto-tempo.png
new file mode 100644
index 0000000..b70a6b3
Binary files /dev/null and b/assets/closeups/cruscotto-tempo.png differ
diff --git a/assets/scenes/barrettino-1997.png b/assets/scenes/barrettino-1997.png
new file mode 100644
index 0000000..f0638e1
Binary files /dev/null and b/assets/scenes/barrettino-1997.png differ
diff --git a/assets/voices/andrea-1efd47285d.mp3 b/assets/voices/andrea-1efd47285d.mp3
new file mode 100644
index 0000000..03bbd77
Binary files /dev/null and b/assets/voices/andrea-1efd47285d.mp3 differ
diff --git a/assets/voices/andrea-23e4ec2f09.mp3 b/assets/voices/andrea-23e4ec2f09.mp3
new file mode 100644
index 0000000..f61eb75
Binary files /dev/null and b/assets/voices/andrea-23e4ec2f09.mp3 differ
diff --git a/assets/voices/andrea-2e7cda51c4.mp3 b/assets/voices/andrea-2e7cda51c4.mp3
new file mode 100644
index 0000000..d6c7c09
Binary files /dev/null and b/assets/voices/andrea-2e7cda51c4.mp3 differ
diff --git a/assets/voices/andrea-34d248b998.mp3 b/assets/voices/andrea-34d248b998.mp3
new file mode 100644
index 0000000..4a8c3ca
Binary files /dev/null and b/assets/voices/andrea-34d248b998.mp3 differ
diff --git a/assets/voices/andrea-5e16aca755.mp3 b/assets/voices/andrea-5e16aca755.mp3
new file mode 100644
index 0000000..ad8ed0b
Binary files /dev/null and b/assets/voices/andrea-5e16aca755.mp3 differ
diff --git a/assets/voices/andrea-64b8a38c86.mp3 b/assets/voices/andrea-64b8a38c86.mp3
new file mode 100644
index 0000000..cf376ad
Binary files /dev/null and b/assets/voices/andrea-64b8a38c86.mp3 differ
diff --git a/assets/voices/andrea-8c283ea588.mp3 b/assets/voices/andrea-8c283ea588.mp3
new file mode 100644
index 0000000..3e30153
Binary files /dev/null and b/assets/voices/andrea-8c283ea588.mp3 differ
diff --git a/assets/voices/andrea-a1771b44e8.mp3 b/assets/voices/andrea-a1771b44e8.mp3
new file mode 100644
index 0000000..4ec68e3
Binary files /dev/null and b/assets/voices/andrea-a1771b44e8.mp3 differ
diff --git a/assets/voices/andrea-a748d9ad25.mp3 b/assets/voices/andrea-a748d9ad25.mp3
new file mode 100644
index 0000000..a0dfde2
Binary files /dev/null and b/assets/voices/andrea-a748d9ad25.mp3 differ
diff --git a/assets/voices/andrea-b410f40be6.mp3 b/assets/voices/andrea-b410f40be6.mp3
new file mode 100644
index 0000000..43e25d7
Binary files /dev/null and b/assets/voices/andrea-b410f40be6.mp3 differ
diff --git a/assets/voices/andrea-baa95acb36.mp3 b/assets/voices/andrea-baa95acb36.mp3
new file mode 100644
index 0000000..5b7be6e
Binary files /dev/null and b/assets/voices/andrea-baa95acb36.mp3 differ
diff --git a/assets/voices/andrea-c342d764ea.mp3 b/assets/voices/andrea-c342d764ea.mp3
new file mode 100644
index 0000000..eefa353
Binary files /dev/null and b/assets/voices/andrea-c342d764ea.mp3 differ
diff --git a/assets/voices/andrea-c77b0c4b4c.mp3 b/assets/voices/andrea-c77b0c4b4c.mp3
new file mode 100644
index 0000000..2bf138e
Binary files /dev/null and b/assets/voices/andrea-c77b0c4b4c.mp3 differ
diff --git a/assets/voices/andrea-e9ea220852.mp3 b/assets/voices/andrea-e9ea220852.mp3
new file mode 100644
index 0000000..db671bd
Binary files /dev/null and b/assets/voices/andrea-e9ea220852.mp3 differ
diff --git a/assets/voices/andrea-f7481e5519.mp3 b/assets/voices/andrea-f7481e5519.mp3
new file mode 100644
index 0000000..41dea7f
Binary files /dev/null and b/assets/voices/andrea-f7481e5519.mp3 differ
diff --git a/assets/voices/arcu-00f5e0a28f.mp3 b/assets/voices/arcu-00f5e0a28f.mp3
new file mode 100644
index 0000000..7412fa6
Binary files /dev/null and b/assets/voices/arcu-00f5e0a28f.mp3 differ
diff --git a/assets/voices/arcu-1611cdb5fb.mp3 b/assets/voices/arcu-1611cdb5fb.mp3
new file mode 100644
index 0000000..7132273
Binary files /dev/null and b/assets/voices/arcu-1611cdb5fb.mp3 differ
diff --git a/assets/voices/arcu-2ddbc3341d.mp3 b/assets/voices/arcu-2ddbc3341d.mp3
new file mode 100644
index 0000000..728e054
Binary files /dev/null and b/assets/voices/arcu-2ddbc3341d.mp3 differ
diff --git a/assets/voices/arcu-6f0e1c4f8a.mp3 b/assets/voices/arcu-6f0e1c4f8a.mp3
new file mode 100644
index 0000000..55b87a6
Binary files /dev/null and b/assets/voices/arcu-6f0e1c4f8a.mp3 differ
diff --git a/assets/voices/index.json b/assets/voices/index.json
index a8ccfbb..f592f28 100644
--- a/assets/voices/index.json
+++ b/assets/voices/index.json
@@ -1631,6 +1631,14 @@
"file": "andrea-a193a43f3e.mp3",
"sec": 3.78
},
+ "andrea|La macchina del tempo, parcheggiata davanti al Trappola. Sul cruscotto la data è già pronta.": {
+ "file": "andrea-5e16aca755.mp3",
+ "sec": 6.33
+ },
+ "andrea|Salgo. Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.": {
+ "file": "andrea-c77b0c4b4c.mp3",
+ "sec": 9.24
+ },
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
"file": "andrea-3e85200509.mp3",
"sec": 2.93
@@ -1879,6 +1887,34 @@
"file": "andrea-d32e1a2a87.mp3",
"sec": 3.45
},
+ "andrea|Il Barrettino. Nel 1997. Si sente ancora l'odore del caffè e delle sigarette.": {
+ "file": "andrea-e9ea220852.mp3",
+ "sec": 7.94
+ },
+ "andrea|Il panno verde, le fiches in pila, il mazzo e il posacenere pieno. Una sedia è vuota: è per me.": {
+ "file": "andrea-8c283ea588.mp3",
+ "sec": 8.13
+ },
+ "andrea|Vannino, alto e secco, col canovaccio sulla spalla. E quella fossetta nel mento.": {
+ "file": "andrea-c342d764ea.mp3",
+ "sec": 6.77
+ },
+ "andrea|Pierino Parigi, con gli occhiali sul naso, che guarda le carte come se lo tradissero.": {
+ "file": "andrea-a1771b44e8.mp3",
+ "sec": 5.22
+ },
+ "andrea|Il televisore a tubo. È il 1997: niente smartphone, solo il televideo.": {
+ "file": "andrea-64b8a38c86.mp3",
+ "sec": 7.87
+ },
+ "andrea|Fuori c'è la sera del 1997. E parcheggiata lì, la macchina del tempo.": {
+ "file": "andrea-1efd47285d.mp3",
+ "sec": 5.94
+ },
+ "andrea|Si torna nel 2007. Ciao Vannino, ciao Pierino.": {
+ "file": "andrea-baa95acb36.mp3",
+ "sec": 4.41
+ },
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
"file": "andrea-0a073729fd.mp3",
"sec": 7.3
@@ -2511,6 +2547,10 @@
"file": "andrea-a960f89e7a.mp3",
"sec": 3.45
},
+ "andrea|Bei tempi quando qui si faceva i pokerini...": {
+ "file": "andrea-2e7cda51c4.mp3",
+ "sec": 2.55
+ },
"andrea|Ci si vede dopo, Arcu.": {
"file": "andrea-191021ad7c.mp3",
"sec": 1.68
@@ -2539,6 +2579,26 @@
"file": "arcu-fcaee733df.mp3",
"sec": 0.92
},
+ "arcu|La macchina è sempre lì. La data è già impostata.": {
+ "file": "arcu-2ddbc3341d.mp3",
+ "sec": 3.69
+ },
+ "arcu|I pokerini da Vannino, col Pierino che bluffava anche a briscola.": {
+ "file": "arcu-6f0e1c4f8a.mp3",
+ "sec": 3.85
+ },
+ "arcu|Ci vuoi tornare? Aspetta.": {
+ "file": "arcu-00f5e0a28f.mp3",
+ "sec": 3.73
+ },
+ "andrea|Ma quella è... la macchina del tempo!": {
+ "file": "andrea-a748d9ad25.mp3",
+ "sec": 3.13
+ },
+ "arcu|Il plutonio l'ha trovato Alvarino. Basta salire e mettere la data.": {
+ "file": "arcu-1611cdb5fb.mp3",
+ "sec": 4.88
+ },
"andrea|Alla bona, un giro di paese.": {
"file": "andrea-b35a4d8cc0.mp3",
"sec": 2.15
@@ -2659,6 +2719,50 @@
"file": "andrea-78ad61a67a.mp3",
"sec": 1.22
},
+ "andrea|C'è posto per uno?": {
+ "file": "andrea-f7481e5519.mp3",
+ "sec": 1.81
+ },
+ "andrea|Vannino, un caffè?": {
+ "file": "andrea-23e4ec2f09.mp3",
+ "sec": 1.86
+ },
+ "andrea|Pierino, come va la mano?": {
+ "file": "andrea-34d248b998.mp3",
+ "sec": 1.59
+ },
+ "andrea|Dopo.": {
+ "file": "andrea-b410f40be6.mp3",
+ "sec": 1.73
+ },
+ "vannino|La sedia è lì. Invito due, punto cinque. Come la vuoi?": {
+ "file": "vannino-225c8700a1.mp3",
+ "sec": 5.37
+ },
+ "vannino|Subito. Ma prima una mano, sennò che ci fai qui?": {
+ "file": "vannino-cd3ce69939.mp3",
+ "sec": 3.21
+ },
+ "pierino_parigi|Bene. O male. Con me un si sa mai, ed è questo il bello.": {
+ "file": "pierino_parigi-46d59a6a61.mp3",
+ "sec": 5.7
+ },
+ "andrea|No, aspetta.": {
+ "file": "andrea-04890c1d8e.mp3",
+ "sec": 1.39
+ },
+ "pierino_parigi|Alla bona: si gioca per il caffè.": {
+ "file": "pierino_parigi-9b57763042.mp3",
+ "sec": 2.33
+ },
+ "pierino_parigi|Seria. Carte, Vannino.": {
+ "file": "pierino_parigi-259f48a09a.mp3",
+ "sec": 2.55
+ },
+ "pierino_parigi|A fassi male? Allora io bluffo.": {
+ "file": "pierino_parigi-d98fcb71d3.mp3",
+ "sec": 2.76
+ },
"andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-17216997e2.mp3",
"sec": 2.74
@@ -2707,10 +2811,6 @@
"file": "andrea-fc5a08bbd6.mp3",
"sec": 0.73
},
- "andrea|No, aspetta.": {
- "file": "andrea-04890c1d8e.mp3",
- "sec": 1.39
- },
"chiaru|Alla bona: ti faccio vedere dove si ferma. E io vado piano.": {
"file": "chiaru-d31cc223f6.mp3",
"sec": 5.08
@@ -3075,6 +3175,22 @@
"file": "bertovo-ca941bee91.mp3",
"sec": 5.41
},
+ "vannino|M'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.": {
+ "file": "vannino-e68fb40eba.mp3",
+ "sec": 7.85
+ },
+ "pierino_parigi|Le fiches restano al banco. E tu torna quando hai imparato a bluffare.": {
+ "file": "pierino_parigi-2c2a083725.mp3",
+ "sec": 5.21
+ },
+ "vannino|Già finito? E il caffè chi lo paga?": {
+ "file": "vannino-3307746bb8.mp3",
+ "sec": 3.98
+ },
+ "pierino_parigi|Pari. Si rifà domani sera, alla stess'ora.": {
+ "file": "pierino_parigi-75a2b84953.mp3",
+ "sec": 3.8
+ },
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
"file": "andrea-585cd65f93.mp3",
"sec": 5.2
diff --git a/assets/voices/pierino_parigi-259f48a09a.mp3 b/assets/voices/pierino_parigi-259f48a09a.mp3
new file mode 100644
index 0000000..3a71b55
Binary files /dev/null and b/assets/voices/pierino_parigi-259f48a09a.mp3 differ
diff --git a/assets/voices/pierino_parigi-2c2a083725.mp3 b/assets/voices/pierino_parigi-2c2a083725.mp3
new file mode 100644
index 0000000..d8bb41c
Binary files /dev/null and b/assets/voices/pierino_parigi-2c2a083725.mp3 differ
diff --git a/assets/voices/pierino_parigi-46d59a6a61.mp3 b/assets/voices/pierino_parigi-46d59a6a61.mp3
new file mode 100644
index 0000000..40d7954
Binary files /dev/null and b/assets/voices/pierino_parigi-46d59a6a61.mp3 differ
diff --git a/assets/voices/pierino_parigi-75a2b84953.mp3 b/assets/voices/pierino_parigi-75a2b84953.mp3
new file mode 100644
index 0000000..53aebab
Binary files /dev/null and b/assets/voices/pierino_parigi-75a2b84953.mp3 differ
diff --git a/assets/voices/pierino_parigi-9b57763042.mp3 b/assets/voices/pierino_parigi-9b57763042.mp3
new file mode 100644
index 0000000..297726d
Binary files /dev/null and b/assets/voices/pierino_parigi-9b57763042.mp3 differ
diff --git a/assets/voices/pierino_parigi-d98fcb71d3.mp3 b/assets/voices/pierino_parigi-d98fcb71d3.mp3
new file mode 100644
index 0000000..59469ae
Binary files /dev/null and b/assets/voices/pierino_parigi-d98fcb71d3.mp3 differ
diff --git a/assets/voices/vannino-225c8700a1.mp3 b/assets/voices/vannino-225c8700a1.mp3
new file mode 100644
index 0000000..b68b056
Binary files /dev/null and b/assets/voices/vannino-225c8700a1.mp3 differ
diff --git a/assets/voices/vannino-3307746bb8.mp3 b/assets/voices/vannino-3307746bb8.mp3
new file mode 100644
index 0000000..27bee87
Binary files /dev/null and b/assets/voices/vannino-3307746bb8.mp3 differ
diff --git a/assets/voices/vannino-cd3ce69939.mp3 b/assets/voices/vannino-cd3ce69939.mp3
new file mode 100644
index 0000000..bea0077
Binary files /dev/null and b/assets/voices/vannino-cd3ce69939.mp3 differ
diff --git a/assets/voices/vannino-e68fb40eba.mp3 b/assets/voices/vannino-e68fb40eba.mp3
new file mode 100644
index 0000000..b9a49e6
Binary files /dev/null and b/assets/voices/vannino-e68fb40eba.mp3 differ
diff --git a/docs/IMMAGINI.md b/docs/IMMAGINI.md
index 160fa2c..d677927 100644
--- a/docs/IMMAGINI.md
+++ b/docs/IMMAGINI.md
@@ -35,6 +35,8 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
+| `barrettino-1997.png`, `closeups/cruscotto-tempo.png` | Codex, solo testo | nessuna | — | sì |
+| `characters/vannino.png`, `characters/pierino_parigi.png` | Codex, dalla descrizione di Andrea (niente foto) | nessuna | — | sì (Pierino: consenso del figlio Marco) |
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
diff --git a/docs/STATO.md b/docs/STATO.md
index fe86aa6..b15b85c 100644
--- a/docs/STATO.md
+++ b/docs/STATO.md
@@ -18,6 +18,14 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati
+- **La macchina del tempo e il pokerino del 1997 (29 ottobre)**: dall'Arcu sotto la tenda del Trappola, «Bei tempi
+ quando qui si faceva i pokerini…»: compare la macchina del tempo in Via Liguri Magelli. Dentro, il cruscotto (Codex)
+ compone 10/07/1997 18:30, poi tremori, lampi e il bianco (primo piano `viaggio`, `src/engine/viaggio.js`, tempi in
+ `TRIP` di `game.js`): si arriva al Barrettino del 1997 (`barrettino_1997`, scena `viaggio: true`: niente mappa né
+ strade, la porta riporta nel 2007), con Vannino (dalla descrizione di Andrea) e Pierino Parigi (consenso del figlio
+ Marco, 29 ottobre), sprite di Codex. Il pokerino (`poker.js`, `poker-render.js`): cinque carte col cambio, mazzo
+ corto dal 7 all'asso (colore sopra il full, A-7-8-9-10 scala), invito 2, punto 5, cinque mani. [DA ANDREA] l'interno
+ del Barrettino (è una proposta di Codex) e le battute.
- **Nuovi posti dalle foto di Andrea (29 ottobre, sfondi di Codex)**: Via della Compagnia (fra Via de' Medici, la
Pieve e Villa Adami), Via Cafaggio in tre tratti fino alla rotonda, la Provinciale sotto i platani, la passeggiata
sulla Carza (cascatella, orti, dietro le poste, sotto il ponte delle poste, gli orti dello zio Mario), la strada fra
diff --git a/index.html b/index.html
index dce91d2..4c7b596 100644
--- a/index.html
+++ b/index.html
@@ -171,6 +171,22 @@
Tastiera: B, S, V; frecce e 1–0 scelgono, Invio gioca, Esc lascia il tavolo.
+
diff --git a/src/engine/game.js b/src/engine/game.js
index f1466b5..19cc5d9 100644
--- a/src/engine/game.js
+++ b/src/engine/game.js
@@ -12,6 +12,7 @@ import * as scopa from './scopa.js';
import * as gara from './gara.js';
import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js';
+import * as poker from './poker.js';
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
export const MINIGAMES = {
@@ -23,6 +24,7 @@ export const MINIGAMES = {
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
+ poker: { create: (seed, level) => poker.newPoker(seed, { level }), update: poker.update, play: poker.play },
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
@@ -51,6 +53,9 @@ const WALK = ['walk_1', 'walk_2', 'walk_3', 'walk_4'], STEP = 15, REACH = 0.45;
// Il primo piano di un libro (story.closeups, effetto `closeup`): copertina → frontespizio → una pagina girata →
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
export const PAGE_TURN = 0.7, CLOSEUP_PAUSE = 1.4;
+// I tempi del viaggio nel tempo, in secondi: quando comincia a comparire la data, quanto dura ogni tasto, quando si
+// carica (tremori e lampi), quando arriva il bianco, quando si chiude.
+export const TRIP = { type: 0.8, key: 0.16, charge: 3.6, flash: 5.4, end: 6.6 };
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
export function check(cond, state) {
@@ -306,6 +311,8 @@ export class Game {
if (c.step === 'done') this.queue.unshift(...def.complete);
return true;
}
+ // Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
+ if (def?.type === 'viaggio') { this.queue.unshift(...def.complete); return true; }
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
// di chi ha vinto; uscendo prima, quella di chi lascia.
if (MINIGAMES[def?.type]) {
@@ -336,6 +343,18 @@ export class Game {
return true;
}
+ // Il viaggio nel tempo (story.closeups, type 'viaggio'): la data si compone sul cruscotto una cifra alla volta, poi
+ // tremori, lampi e il bianco; alla fine si chiude da solo e parte `complete` (di solito il `go` nella scena d'arrivo).
+ updateTrip(dt) {
+ const c = this.closeup, def = this.story.closeups[c.id], T = TRIP;
+ c.t += dt;
+ const typed = Math.max(0, Math.min(def.date.length, Math.floor((c.t - T.type) / T.key) + 1));
+ for (; c.typed < typed; c.typed++) if (def.date[c.typed] !== ' ') c.events.push({ kind: 'card-tap', power: 0.7 });
+ if (!c.charge && c.t >= T.charge) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); }
+ if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'gara-buca', power: 1 }); }
+ if (c.t >= T.end) this.closeupClose();
+ }
+
updateTorch(dt) {
const c = this.closeup, def = this.story.closeups[c.id];
c.t += dt;
@@ -453,6 +472,7 @@ export class Game {
const c = this.closeup;
if (c) {
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
+ if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
const m = this.minigame();
if (m) {
m.update(c, dt);
@@ -494,6 +514,7 @@ export class Game {
else if (e.closeup) {
const type = this.story.closeups[e.closeup].type;
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
+ : type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
diff --git a/src/engine/poker-render.js b/src/engine/poker-render.js
new file mode 100644
index 0000000..e0d5cb4
--- /dev/null
+++ b/src/engine/poker-render.js
@@ -0,0 +1,89 @@
+import { SEATS, NAMES, MANI, canDraw, myTurn, evaluate, CATEGORIES, red } from './poker.js';
+import { drawTable, drawCard } from './briscola-render.js';
+import { text, panel } from './bocce-render.js';
+
+// Le carte di Andrea, in basso: coordinate condivise da disegno e tocco (bersagli larghi anche sul telefono).
+export const HAND = { x: 150, y: 222, gap: 70, w: 62, h: 90 };
+export function cardAt(c, [x, y]) {
+ if (!canDraw(c)) return -1;
+ return c.cards.andrea.findIndex((_, i) => x >= HAND.x + i * HAND.gap && x < HAND.x + i * HAND.gap + HAND.w && y >= HAND.y - 14 && y < HAND.y + HAND.h);
+}
+const ink = '#2a1f1a', paper = '#fff4dc';
+function rect(ctx, x, y, w, h, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x), Math.round(y), w, h); }
+// I semi francesi a pixel (il carattere del gioco non ha ♥♦♣♠): cuore, quadro, fiore, picca, larghi 2·s.
+function pip(ctx, suit, x, y, s) {
+ const color = suit === 'cuori' || suit === 'quadri' ? '#c0392b' : ink;
+ ctx.fillStyle = color; ctx.beginPath();
+ if (suit === 'quadri') { ctx.moveTo(x, y - s); ctx.lineTo(x + s * 0.75, y); ctx.lineTo(x, y + s); ctx.lineTo(x - s * 0.75, y); }
+ else if (suit === 'cuori') {
+ ctx.moveTo(x, y + s); ctx.lineTo(x - s, y - s * 0.1); ctx.arc(x - s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 0.85, Math.PI * 1.95);
+ ctx.arc(x + s * 0.5, y - s * 0.35, s * 0.52, Math.PI * 1.05, Math.PI * 0.15); ctx.lineTo(x, y + s);
+ } else if (suit === 'picche') {
+ ctx.moveTo(x, y - s); ctx.lineTo(x + s, y + s * 0.15); ctx.arc(x + s * 0.5, y + s * 0.3, s * 0.5, 0, Math.PI * 0.9);
+ ctx.lineTo(x, y + s * 0.2); ctx.arc(x - s * 0.5, y + s * 0.3, s * 0.5, Math.PI * 0.1, Math.PI); ctx.closePath();
+ ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y + s * 0.3); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
+ } else {
+ for (const [dx, dy] of [[0, -0.45], [-0.48, 0.15], [0.48, 0.15]]) { ctx.moveTo(x + dx * s + s * 0.42, y + dy * s); ctx.arc(x + dx * s, y + dy * s, s * 0.42, 0, Math.PI * 2); }
+ ctx.fill(); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + s * 0.35, y + s); ctx.lineTo(x - s * 0.35, y + s);
+ }
+ ctx.closePath(); ctx.fill();
+}
+// Una carta francese: angolo col valore e il seme, il seme grande in mezzo (le figure con una cornice).
+export function drawFrench(ctx, card, x, y, w, h, back = false) {
+ if (back) { drawCard(ctx, null, x, y, w, h, true); return; }
+ rect(ctx, x + 2, y + 3, w, h, '#15231d'); rect(ctx, x, y, w, h, ink); rect(ctx, x + 1, y + 1, w - 2, h - 2, paper);
+ const color = red(card) ? '#c0392b' : ink, k = w / 62;
+ text(ctx, card.rank, x + 5 * k, y + 9 * k, Math.round(9 * k), color);
+ pip(ctx, card.suit, x + 9 * k, y + 22 * k, 5 * k);
+ if (['J', 'Q', 'K'].includes(card.rank)) {
+ rect(ctx, x + 14 * k, y + 28 * k, 34 * k, 50 * k, red(card) ? '#f3d6c4' : '#d8dfe6');
+ ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.strokeRect(Math.round(x + 14 * k) + 0.5, Math.round(y + 28 * k) + 0.5, Math.round(34 * k), Math.round(50 * k));
+ text(ctx, card.rank, x + 31 * k, y + 46 * k, Math.round(16 * k), color, 'center');
+ pip(ctx, card.suit, x + 31 * k, y + 66 * k, 7 * k);
+ } else pip(ctx, card.suit, x + 31 * k, y + 52 * k, 14 * k);
+}
+// Le fiches del piatto: pile colorate, una ogni cinque.
+function chips(ctx, n, x, y) {
+ const colors = ['#c0392b', '#2f6fb0', '#e8e0c8', '#3a8a4a'];
+ for (let i = 0; i < Math.min(24, Math.ceil(n / 5)); i++) {
+ const col = i % 6, row = Math.floor(i / 6), cx = x + col * 14 - 35, cy = y - row * 3;
+ rect(ctx, cx, cy, 12, 4, '#1a1410'); rect(ctx, cx, cy - 1, 12, 3, colors[i % colors.length]);
+ }
+}
+// Dove siedono: Vannino a sinistra, Pierino a destra, Andrea in basso.
+const SPOTS = { vannino: { x: 40, y: 70 }, pierino: { x: 420, y: 70 } };
+
+export function drawPoker(ctx, c) {
+ ctx.save(); ctx.imageSmoothingEnabled = false;
+ drawTable(ctx, 'IL POKERINO DEL BARRETTINO · 10 LUGLIO 1997');
+ const show = c.step === 'show' || c.step === 'result';
+ for (const who of ['vannino', 'pierino']) {
+ const { x, y } = SPOTS[who], out = !c.in?.[who];
+ text(ctx, `${NAMES[who]} · ${c.chips[who]}`, x + 90, y - 22, 7, c.current === who ? '#ffe28a' : '#fff0c8', 'center');
+ c.cards[who].forEach((card, i) => drawFrench(ctx, card, x + i * 36, y - 10, 42, 62, !(show && c.in[who])));
+ if (out && c.step !== 'result') { ctx.fillStyle = 'rgba(20,16,12,0.55)'; ctx.fillRect(x - 2, y - 12, 190, 66); text(ctx, 'lascia', x + 90, y + 22, 7, '#d9ccba', 'center'); }
+ if (show && c.in[who] && c.cards[who].length) text(ctx, CATEGORIES[evaluate(c.cards[who])[0]], x + 90, y + 62, 6, c.winners?.includes(who) ? '#b9e5b3' : '#d9ccba', 'center');
+ if (c.changed?.[who] !== undefined && c.step !== 'show') text(ctx, c.changed[who] ? `cambia ${c.changed[who]}` : 'servito', x + 90, y + 62, 6, '#9fd0ff', 'center');
+ }
+ // Il piatto in mezzo.
+ chips(ctx, c.pot ?? 0, 320, 178);
+ text(ctx, `Piatto ${c.pot ?? 0}`, 320, 192, 7, '#ffe09b', 'center');
+ // Le carte di Andrea: quelle scelte per il cambio si alzano.
+ c.cards.andrea.forEach((card, i) => {
+ const up = c.selected?.has(i) ? -14 : 0;
+ drawFrench(ctx, card, HAND.x + i * HAND.gap, HAND.y + up, HAND.w, HAND.h);
+ if (up) text(ctx, 'cambia', HAND.x + i * HAND.gap + HAND.w / 2, HAND.y + up - 7, 5, '#ffe28a', 'center');
+ });
+ text(ctx, `Andrea · ${c.chips.andrea}`, 320, 210, 7, myTurn(c) || canDraw(c) ? '#ffe28a' : '#fff0c8', 'center');
+ if (c.cards.andrea.length) text(ctx, CATEGORIES[evaluate(c.cards.andrea)[0]], 590, 300, 6, '#d9ccba', 'center');
+ text(ctx, `Mano ${Math.min(c.hand, MANI)}/${MANI}`, 40, 300, 6, '#d9ccba');
+ if (c.line && c.step !== 'result') { ctx.fillStyle = '#17160fd0'; ctx.fillRect(120, 146, 400, 16); text(ctx, c.line, 320, 154, 7, c.step === 'show' ? '#b9e5b3' : '#9fd0ff', 'center'); }
+ if (c.step === 'result') {
+ const r = c.result, title = r.winner === 'player' ? 'HAI VINTO IL POKERINO!' : r.winner === 'draw' ? 'PARI' : `HA VINTO ${NAMES[r.top[0]].toUpperCase()}`;
+ panel(ctx, 150, 110, 340, 84);
+ text(ctx, title, 320, 132, 11, r.winner === 'player' ? '#b9e5b3' : '#efd49e', 'center');
+ text(ctx, SEATS.map(s => `${NAMES[s]} ${r.chips[s]}`).join(' · '), 320, 156, 7, '#efd49e', 'center');
+ text(ctx, 'Torna al Barrettino per uscire.', 320, 176, 7, '#d9ccba', 'center');
+ }
+ ctx.restore();
+}
diff --git a/src/engine/poker.js b/src/engine/poker.js
new file mode 100644
index 0000000..41216bc
--- /dev/null
+++ b/src/engine/poker.js
@@ -0,0 +1,239 @@
+// Il pokerino del Barrettino, luglio 1997 (idea di Andrea, 29 ottobre): poker a cinque carte coperte col cambio, col
+// mazzo corto da 32 (dal 7 all'asso, come nei bar), Andrea contro Vannino e Pierino Parigi. Si gioca a fiches.
+// Col mazzo corto il colore è più raro del full e lo batte; la scala più bassa è A-7-8-9-10.
+// Una mano: il piatto (ognuno mette l'invito), cinque carte, un giro di puntate, il cambio, un altro giro, chi resta
+// mostra. Puntate fisse (PUNTO) e al massimo RILANCI rilanci a giro. Dopo MANI mani vince chi ha più fiches.
+
+export const RANKS = ['7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
+export const SUITS = ['cuori', 'quadri', 'fiori', 'picche'];
+export const SYMBOL = { cuori: '♥', quadri: '♦', fiori: '♣', picche: '♠' };
+export const NAMES = { andrea: 'Andrea', vannino: 'Vannino', pierino: 'Pierino' };
+export const SEATS = ['andrea', 'vannino', 'pierino'];
+export const START = 100, INVITO = 2, PUNTO = 5, RILANCI = 2, MANI = 5, CPU_TIME = 0.9;
+export const CATEGORIES = ['carta alta', 'coppia', 'doppia coppia', 'tris', 'scala', 'full', 'colore', 'poker', 'scala colore'];
+// I livelli: quanto ci pensano gli altri (0 = a caso, 1 = col conto giusto) e quanto bluffano.
+export const LEVELS = {
+ bona: { skill: 0.35, bluff: 0.04 },
+ seria: { skill: 0.7, bluff: 0.08 },
+ male: { skill: 1, bluff: 0.14 },
+};
+
+const value = card => RANKS.indexOf(card.rank) + 7;
+export const cardName = card => `${card.rank}${SYMBOL[card.suit]}`;
+export const red = card => card.suit === 'cuori' || card.suit === 'quadri';
+function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
+function deck(c) {
+ const cards = SUITS.flatMap(suit => RANKS.map(rank => ({ rank, suit })));
+ for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(random(c) * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; }
+ return cards;
+}
+
+// Il punto di una mano: [categoria, poi i valori per gli spareggi], da confrontare in ordine.
+export function evaluate(hand) {
+ const values = hand.map(value).sort((a, b) => b - a);
+ const counts = new Map();
+ for (const v of values) counts.set(v, (counts.get(v) ?? 0) + 1);
+ const groups = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
+ const flush = hand.every(card => card.suit === hand[0].suit);
+ const distinct = [...counts.keys()].sort((a, b) => b - a);
+ let straight = null;
+ if (distinct.length === 5) {
+ if (distinct[0] - distinct[4] === 4) straight = distinct[0];
+ else if (distinct.join() === '14,10,9,8,7') straight = 10; // A-7-8-9-10: l'asso fa da 6
+ }
+ const shape = groups.map(g => g[1]).join('');
+ const tie = groups.map(g => g[0]);
+ if (straight && flush) return [8, straight];
+ if (shape === '41') return [7, ...tie];
+ if (flush) return [6, ...values];
+ if (shape === '32') return [5, ...tie];
+ if (straight) return [4, straight];
+ if (shape === '311') return [3, ...tie];
+ if (shape === '221') return [2, ...tie];
+ if (shape === '2111') return [1, ...tie];
+ return [0, ...values];
+}
+export const compare = (a, b) => { for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) - (b[i] ?? 0); return 0; };
+export const handName = hand => CATEGORIES[evaluate(hand)[0]];
+
+export function newPoker(seed = Date.now() >>> 0, def = {}) {
+ const level = LEVELS[def.level] ? def.level : 'seria';
+ const c = { step: 'deal', t: 0, seed, level, skill: LEVELS[level], chips: Object.fromEntries(SEATS.map(s => [s, START])),
+ hand: 0, dealer: 2, events: [], line: null, selected: new Set(), history: [], result: null };
+ newHand(c);
+ return c;
+}
+const alive = c => SEATS.filter(s => c.chips[s] >= INVITO);
+const active = c => SEATS.filter(s => c.in[s]);
+
+// Una mano nuova: l'invito, il mazzo mischiato, cinque carte a testa; parla per primo chi sta dopo il mazziere.
+function newHand(c) {
+ c.hand++;
+ c.dealer = (c.dealer + 1) % SEATS.length;
+ c.deck = deck(c);
+ c.in = Object.fromEntries(SEATS.map(s => [s, c.chips[s] >= INVITO]));
+ c.pot = 0;
+ for (const s of active(c)) { c.chips[s] -= INVITO; c.pot += INVITO; }
+ c.cards = Object.fromEntries(SEATS.map(s => [s, c.in[s] ? c.deck.splice(0, 5) : []]));
+ c.changed = {}; c.selected = new Set(); c.winners = null; c.line = null;
+ c.events.push({ kind: 'card-shuffle', power: 1 });
+ startBetting(c, 'bet1');
+}
+function startBetting(c, step) {
+ c.step = step; c.t = 0;
+ c.bets = Object.fromEntries(SEATS.map(s => [s, 0]));
+ c.acted = Object.fromEntries(SEATS.map(s => [s, false]));
+ c.raises = 0; c.toBet = 0;
+ c.current = firstAfter(c, c.dealer);
+ skipIfDone(c);
+}
+function firstAfter(c, i) {
+ for (let k = 1; k <= SEATS.length; k++) { const s = SEATS[(i + k) % SEATS.length]; if (c.in[s]) return s; }
+ return null;
+}
+// Un giro di puntate finisce quando tutti quelli rimasti hanno parlato e messo quanto il più alto.
+const roundOver = c => active(c).every(s => c.acted[s] && (c.bets[s] === c.toBet || c.chips[s] === 0));
+function skipIfDone(c) {
+ if (active(c).length === 1) return showdown(c);
+ if (roundOver(c)) return endBetting(c);
+ while (c.current && (!c.in[c.current] || c.chips[c.current] === 0 && c.acted[c.current])) c.current = firstAfter(c, SEATS.indexOf(c.current));
+}
+function endBetting(c) {
+ if (c.step === 'bet1') { c.step = 'draw'; c.t = 0; c.current = firstAfter(c, c.dealer); return; }
+ showdown(c);
+}
+
+export const toCall = (c, who = 'andrea') => Math.max(0, c.toBet - c.bets[who]);
+export const myTurn = c => (c.step === 'bet1' || c.step === 'bet2') && c.current === 'andrea' && c.in.andrea;
+export const canRaise = (c, who = 'andrea') => c.raises < RILANCI && c.chips[who] > toCall(c, who);
+export const canDraw = c => c.step === 'draw' && c.current === 'andrea';
+
+// Le mosse: «lascio» (si esce dalla mano), «vedo» (o «parola» se non c'è niente da mettere), «punto»/«rilancio».
+function put(c, who, amount) {
+ const n = Math.min(amount, c.chips[who]);
+ c.chips[who] -= n; c.bets[who] += n; c.pot += n;
+ if (n) c.events.push({ kind: 'card-tap', power: 0.6 });
+}
+export function act(c, who, move) {
+ if (c.current !== who || !(c.step === 'bet1' || c.step === 'bet2')) return false;
+ const due = toCall(c, who);
+ if (move === 'lascio' && due === 0) move = 'vedo'; // lasciare gratis non ha senso: si dice parola
+ if (move === 'rilancio' && !canRaise(c, who)) move = 'vedo';
+ if (move === 'lascio') { c.in[who] = false; c.line = `${NAMES[who]}: lascio.`; }
+ else if (move === 'vedo') { put(c, who, due); c.line = `${NAMES[who]}: ${due ? `vedo (${due})` : 'parola'}.`; }
+ else { put(c, who, due + PUNTO); c.toBet = c.bets[who]; c.raises++; for (const s of SEATS) if (s !== who) c.acted[s] = false; c.line = `${NAMES[who]}: ${due ? 'rilancio' : 'apro'} di ${PUNTO}.`; }
+ c.acted[who] = true;
+ c.history.push({ hand: c.hand, step: c.step, who, move });
+ c.current = firstAfter(c, SEATS.indexOf(who));
+ c.t = 0;
+ skipIfDone(c);
+ return true;
+}
+// Il cambio: si scartano le carte scelte (al massimo 4) e se ne pescano altrettante.
+export function draw(c, who, indexes) {
+ if (c.step !== 'draw' || c.current !== who) return false;
+ const keep = c.cards[who].filter((_, i) => !indexes.includes(i));
+ c.cards[who] = [...keep, ...c.deck.splice(0, 5 - keep.length)];
+ c.changed[who] = 5 - keep.length;
+ c.line = `${NAMES[who]}: ${c.changed[who] ? `cambio ${c.changed[who]}` : 'servito'}.`;
+ if (c.changed[who]) c.events.push({ kind: 'card-slide', power: 0.6 });
+ if (who === 'andrea') c.selected = new Set();
+ c.current = firstAfter(c, SEATS.indexOf(who));
+ c.t = 0;
+ // Il cambio è finito quando tutti quelli rimasti hanno cambiato.
+ if (active(c).every(s => c.changed[s] !== undefined)) startBetting(c, 'bet2');
+ return true;
+}
+function showdown(c) {
+ const left = active(c);
+ const best = left.map(s => ({ s, score: evaluate(c.cards[s]) })).sort((a, b) => compare(b.score, a.score));
+ const winners = left.length === 1 ? left : best.filter(b => compare(b.score, best[0].score) === 0).map(b => b.s);
+ const share = Math.floor(c.pot / winners.length);
+ for (const w of winners) c.chips[w] += share;
+ c.chips[winners[0]] += c.pot - share * winners.length;
+ c.winners = winners;
+ c.line = left.length === 1 ? `Tutti lasciano: il piatto (${c.pot}) va a ${NAMES[winners[0]]}.`
+ : `${winners.map(w => NAMES[w]).join(' e ')} ${winners.length > 1 ? 'dividono' : 'vince'} con ${handName(c.cards[winners[0]])}: ${c.pot} fiches.`;
+ c.step = 'show'; c.t = 0; c.current = null;
+ c.events.push({ kind: 'card-shuffle', power: 0.5 });
+}
+// Dopo la mostra un tocco (o «Prossima mano») va avanti; finite le mani, o senza più fiches, il risultato.
+export function advance(c) {
+ if (c.step !== 'show') return false;
+ if (c.hand >= MANI || c.chips.andrea < INVITO || alive(c).length < 2) return finish(c);
+ newHand(c);
+ return true;
+}
+function finish(c) {
+ const max = Math.max(...SEATS.map(s => c.chips[s])), top = SEATS.filter(s => c.chips[s] === max);
+ c.step = 'result';
+ c.result = { winner: top.includes('andrea') ? (top.length > 1 ? 'draw' : 'player') : 'cpu', chips: { ...c.chips }, top };
+ return true;
+}
+
+// --- Gli altri due: quanto vale la mano (0..1), cosa cambiare, come puntare.
+export function strength(hand) {
+ const [cat, ...rest] = evaluate(hand);
+ return Math.min(1, cat / 8 + ((rest[0] ?? 7) - 7) / 7 / 9);
+}
+export function cpuDiscards(c, who) {
+ const hand = c.cards[who], values = hand.map(value), [cat] = evaluate(hand);
+ if (cat >= 4 && cat !== 7) return []; // scala, full, colore, scala colore: servito
+ const counts = new Map(); values.forEach(v => counts.set(v, (counts.get(v) ?? 0) + 1));
+ if (cat >= 1) return hand.map((_, i) => i).filter(i => counts.get(values[i]) === 1 && !(cat === 7)); // tiene le coppie, i tris, il poker
+ const bySuit = SUITS.map(s => hand.map((card, i) => card.suit === s ? i : -1).filter(i => i >= 0)).find(l => l.length === 4);
+ if (bySuit && random(c) < c.skill.skill) return hand.map((_, i) => i).filter(i => !bySuit.includes(i)); // quattro a colore
+ // Altrimenti tiene le due carte più alte (con poca testa, una a caso).
+ const order = hand.map((_, i) => i).sort((a, b) => values[b] - values[a]);
+ return order.slice(random(c) < c.skill.skill ? 2 : 1);
+}
+export function cpuMove(c, who) {
+ const s = strength(c.cards[who]), due = toCall(c, who), k = c.skill, noise = (random(c) - 0.5) * (1 - k.skill) * 0.5;
+ const value = s + noise, bluff = random(c) < k.bluff;
+ if (value > 0.42 || bluff) return canRaise(c, who) && (value > 0.55 || bluff) ? 'rilancio' : 'vedo';
+ if (due === 0) return 'vedo';
+ return value > 0.13 + due / 60 ? 'vedo' : 'lascio';
+}
+
+export function update(c, dt) {
+ if (!Number.isFinite(dt) || dt <= 0) return;
+ c.t += dt;
+ if (!c.current || c.current === 'andrea' || c.t < CPU_TIME) return;
+ if (c.step === 'bet1' || c.step === 'bet2') act(c, c.current, cpuMove(c, c.current));
+ else if (c.step === 'draw') draw(c, c.current, cpuDiscards(c, c.current));
+}
+
+// --- I comandi di Andrea: toccare una carta la segna per il cambio; i pulsanti fanno il resto.
+export function play(c, index) {
+ if (canDraw(c)) {
+ if (c.selected.has(index)) c.selected.delete(index);
+ else if (c.selected.size < 4) c.selected.add(index);
+ return true;
+ }
+ return false;
+}
+export const change = c => canDraw(c) && draw(c, 'andrea', [...c.selected]);
+export function moves(c) {
+ if (canDraw(c)) return [{ id: 'cambia', label: c.selected.size ? `Cambia ${c.selected.size}` : 'Servito' }];
+ if (!myTurn(c)) return [];
+ const due = toCall(c);
+ return [
+ { id: 'lascio', label: 'Lascio', disabled: due === 0 },
+ { id: 'vedo', label: due ? `Vedo ${due}` : 'Parola' },
+ { id: 'rilancio', label: due ? `Rilancio ${PUNTO}` : `Apro ${PUNTO}`, disabled: !canRaise(c) },
+ ];
+}
+export function choose(c, id) {
+ if (id === 'cambia') return change(c);
+ return myTurn(c) && act(c, 'andrea', id);
+}
+
+export function status(c) {
+ const chips = SEATS.map(s => `${NAMES[s]} ${c.chips[s]}`).join(', ');
+ if (c.step === 'result') return `Fine del pokerino. ${chips}. ${c.result.winner === 'player' ? 'Hai vinto tu!' : c.result.winner === 'draw' ? 'Pari.' : `Ha vinto ${NAMES[c.result.top[0]]}.`}`;
+ const where = `Mano ${c.hand} di ${MANI}, piatto ${c.pot}. ${chips}.`;
+ if (c.step === 'show') return `${where} ${c.line} Tocca per la prossima mano.`;
+ if (canDraw(c)) return `${where} Cambio: tocca le carte da cambiare (al massimo 4), poi Cambia.`;
+ if (myTurn(c)) return `${where} ${c.line ?? ''} Tocca a te: ${toCall(c) ? `per vedere servono ${toCall(c)}` : 'parola o apri'}.`.replace(' ', ' ');
+ return `${where} ${c.line ?? ''} ${c.current ? `Parla ${NAMES[c.current]}…` : ''}`.trim();
+}
diff --git a/src/engine/render.js b/src/engine/render.js
index d355aaa..2b248fa 100644
--- a/src/engine/render.js
+++ b/src/engine/render.js
@@ -11,6 +11,8 @@ import { drawPesca } from './pesca-render.js';
import { drawGara } from './gara-render.js';
import { drawMicro } from './micro-render.js';
import { drawCerbottana } from './cerbottana-render.js';
+import { drawPoker } from './poker-render.js';
+import { drawViaggio } from './viaggio.js';
import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js';
@@ -678,6 +680,11 @@ function drawCloseup(ctx, game, time) {
if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; }
if (def.type === 'tressette') { drawTressette(ctx, c, def); return; }
if (def.type === 'scopa') { drawScopa(ctx, c, def); return; }
+ if (def.type === 'viaggio') {
+ if (!backgrounds.has(def.picture)) loadBackground(def.picture);
+ drawViaggio(ctx, c, def, backgrounds.get(def.picture));
+ return;
+ }
if (def.type === 'torch') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawTorch(ctx, c, def, backgrounds.get(def.picture));
@@ -688,6 +695,7 @@ function drawCloseup(ctx, game, time) {
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return;
}
+ if (def.type === 'poker') { drawPoker(ctx, c); return; }
if (def.type === 'cerbottana') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
diff --git a/src/engine/viaggio.js b/src/engine/viaggio.js
new file mode 100644
index 0000000..bb63444
--- /dev/null
+++ b/src/engine/viaggio.js
@@ -0,0 +1,51 @@
+// Il disegno del viaggio nel tempo (story.closeups, type 'viaggio'; i tempi stanno in game.js, TRIP): il cruscotto,
+// la data che si compone nel display con le cifre a segmenti che brillano, poi tremori, lampi e il bianco.
+import { TRIP } from './game.js';
+
+// Le cifre a sette segmenti: quali segmenti sono accesi (a b c d e f g).
+const SEG = { 0: 'abcdef', 1: 'bc', 2: 'abged', 3: 'abgcd', 4: 'fgbc', 5: 'afgcd', 6: 'afgedc', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg', '/': '', ':': '', ' ': '' };
+function digit(ctx, ch, x, y, w, h, color) {
+ const t = Math.max(2, Math.round(w / 6)), on = SEG[ch] ?? '';
+ const bars = { a: [x + t, y, w - 2 * t, t], b: [x + w - t, y + t, t, h / 2 - t], c: [x + w - t, y + h / 2, t, h / 2 - t],
+ d: [x + t, y + h - t, w - 2 * t, t], e: [x, y + h / 2, t, h / 2 - t], f: [x, y + t, t, h / 2 - t], g: [x + t, y + h / 2 - t / 2, w - 2 * t, t] };
+ for (const [k, r] of Object.entries(bars)) { ctx.fillStyle = on.includes(k) ? color : 'rgba(255,90,40,0.08)'; ctx.fillRect(...r.map(Math.round)); }
+ if (ch === ':') { ctx.fillStyle = color; ctx.fillRect(x + w / 2 - t / 2, y + h * 0.3, t, t); ctx.fillRect(x + w / 2 - t / 2, y + h * 0.65, t, t); }
+ if (ch === '/') { ctx.fillStyle = color; for (let i = 0; i < 6; i++) ctx.fillRect(x + w * 0.7 - i * w * 0.08, y + h * 0.15 + i * h * 0.13, t, t); }
+}
+export function drawViaggio(ctx, c, def, bg) {
+ const t = c.t, T = TRIP, [dx, dy, dw, dh] = def.display;
+ // Tremori: crescono dalla carica al bianco.
+ const shake = t > T.charge ? Math.min(1, (t - T.charge) / (T.flash - T.charge)) * 5 : 0;
+ ctx.save();
+ ctx.translate(Math.round((Math.sin(t * 61) + Math.sin(t * 37)) * shake), Math.round(Math.cos(t * 53) * shake));
+ if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 640, 360); }
+ // La data, un tasto alla volta, in due righe: giorno/mese/anno e ora.
+ const lines = def.date.split(' '), shown = c.typed ?? 0;
+ let n = 0;
+ const glow = t > T.charge ? 0.75 + 0.25 * Math.sin(t * 30) : 1;
+ ctx.shadowColor = '#ff5a28'; ctx.shadowBlur = 8;
+ lines.forEach((line, row) => {
+ const ch = 14, cw = 13, gap = 4, total = line.length * (cw + gap) - gap;
+ const x0 = dx + (dw - total) / 2, y0 = dy + 8 + row * (ch * 2 + 2);
+ [...line].forEach((d, i) => {
+ const lit = n++ < shown;
+ digit(ctx, lit ? d : ' ', x0 + i * (cw + gap), y0, cw, ch * 2 - 6, `rgba(255,${Math.round(110 * glow)},40,${lit ? 1 : 0})`);
+ });
+ n++; // lo spazio fra le due righe
+ });
+ ctx.shadowBlur = 0;
+ ctx.restore();
+ // I lampi: archi azzurri che attraversano il parabrezza, a scatti.
+ if (t > T.charge + 0.6) {
+ const k = Math.floor(t * 14);
+ if (k % 3 !== 0) {
+ ctx.strokeStyle = 'rgba(160,220,255,0.9)'; ctx.lineWidth = 2; ctx.beginPath();
+ let x = 40 + (k * 97) % 560, y = 0; ctx.moveTo(x, y);
+ for (let i = 0; i < 7; i++) { x += ((k * (i + 3) * 37) % 50) - 25; y += 18; ctx.lineTo(x, y); }
+ ctx.stroke();
+ ctx.fillStyle = 'rgba(180,230,255,0.12)'; ctx.fillRect(0, 0, 640, 360);
+ }
+ }
+ // Il bianco, poi resta bianco fino alla fine.
+ if (t > T.flash) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, (t - T.flash) / 0.5)})`; ctx.fillRect(0, 0, 640, 360); }
+}
diff --git a/src/main.js b/src/main.js
index 6ccbad4..067c79a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -13,6 +13,8 @@ import * as biliardo from './engine/biliardo.js';
import * as rinquarto from './engine/rinquarto.js';
import * as micro from './engine/micro.js';
import * as cerbottana from './engine/cerbottana.js';
+import * as poker from './engine/poker.js';
+import { cardAt as pokerAt } from './engine/poker-render.js';
import * as basket from './engine/basket.js';
import * as pesca from './engine/pesca.js';
import * as gara from './engine/gara.js';
@@ -604,6 +606,7 @@ const MINI = {
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' },
+ poker: { ...poker, back: 'Torna al Barrettino', label: 'Il pokerino: tocca le carte da cambiare, poi Cambia; nei giri di puntate Lascio, Vedo o Parola, Apro o Rilancio. Dopo la mostra un tocco va avanti.' },
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' },
@@ -648,7 +651,12 @@ $('bocce-throw').addEventListener('pointerdown', e => {
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
-const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
+const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt, poker: pokerAt };
+// Il pokerino: tre pulsanti fissi (su iPhone un pulsante rifatto a ogni fotogramma perde il tocco), la scritta cambia.
+for (let i = 0; i < 3; i++) $('poker-move-' + i).addEventListener('click', e => {
+ const c = game?.closeup; if (!c || story.closeups[c.id].type !== 'poker' || game.speech) return;
+ if (c.step === 'show') poker.advance(c); else poker.choose(c, e.currentTarget.dataset.move);
+});
for (let i = 0; i < 3; i++) {
const button = $('scopa-card-' + i);
button.addEventListener('click', () => game?.closeupPlay(i));
@@ -946,7 +954,7 @@ function updateUi() {
updateChoices();
updateSpeech();
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
- const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
+ const cards = ['briscola', 'tressette', 'scopa', 'poker'].includes(def?.type);
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
$('app').classList.toggle('presenta', presenta);
@@ -956,6 +964,20 @@ function updateUi() {
setProp($('briscola-controls'), 'inert', !running);
setProp($('scopa-controls'), 'hidden', def?.type !== 'scopa');
setProp($('scopa-controls'), 'inert', !running);
+ setProp($('poker-controls'), 'hidden', def?.type !== 'poker');
+ setProp($('poker-controls'), 'inert', !running);
+ if (def?.type === 'poker') {
+ setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
+ setText($('poker-status'), poker.status(c));
+ const moves = c.step === 'show' ? [{ id: 'avanti', label: 'Prossima mano' }] : poker.moves(c);
+ for (let i = 0; i < 3; i++) {
+ const b = $('poker-move-' + i), m = moves[i];
+ setProp(b, 'hidden', !m);
+ if (!m) continue;
+ setText(b, m.label); b.dataset.move = m.id;
+ setProp(b, 'disabled', !!m.disabled || !!game.speech);
+ }
+ }
if (def?.type === 'scopa') {
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
const line = scopa.status(c, def.names);
@@ -1055,7 +1077,7 @@ function updateUi() {
setProp($('gara-help'), 'hidden', def.type !== 'gara');
if (mg.aimFree?.(c) ?? !mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
}
- if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
+ if (mg || c?.events) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
setProp($('map-button'), 'hidden', !running || !!game.dialogue || !!game.closeup || game.mapPlaces().length < 2);
setProp($('pause-button'), 'hidden', !running);
diff --git a/src/story/story.js b/src/story/story.js
index bbcfc3b..62ff29b 100644
--- a/src/story/story.js
+++ b/src/story/story.js
@@ -233,6 +233,24 @@ export const story = {
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
},
+ // Il viaggio nel tempo dalla macchina del tempo in Via Liguri Magelli: la data sul cruscotto (Codex), poi il Barrettino
+ // del 10 luglio 1997 (Andrea, 29 ottobre).
+ viaggio: {
+ type: 'viaggio',
+ picture: 'assets/closeups/cruscotto-tempo.png',
+ display: [220, 158, 212, 62],
+ date: '10/07/1997 18:30',
+ complete: [{ go: 'barrettino_1997', at: [320, 330], face: 'up' }],
+ },
+ // Il pokerino del Barrettino con Vannino e Pierino (Andrea, 29 ottobre). [DA ANDREA: le battute]
+ poker: {
+ type: 'poker',
+ names: { player: 'Andrea', cpu: 'Vannino e Pierino' },
+ win: [{ say: 'M\'hai pelato, ragazzo. Nel 1997 un ci si vedeva da queste parti, con quella faccia.', who: 'vannino', pose: 'shrug' }, { set: 'poker_vinto' }],
+ lose: [{ say: 'Le fiches restano al banco. E tu torna quando hai imparato a bluffare.', who: 'pierino_parigi', pose: 'laugh' }],
+ quit: [{ say: 'Già finito? E il caffè chi lo paga?', who: 'vannino', pose: 'talk' }],
+ draw: [{ say: 'Pari. Si rifà domani sera, alla stess\'ora.', who: 'pierino_parigi', pose: 'explain' }],
+ },
// La cerbottana con lo stucco verde di Pierin di Frilli, contro il muro della piazza (Andrea, 29 ottobre).
// [DA ANDREA: le battute sono provvisorie]
cerbottana: {
@@ -323,6 +341,11 @@ export const story = {
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
// Il Chiaru (Matteo Chiarusi, consenso dato ad Andrea il 29 ottobre), alla pista col gesso di Cardetole: sprite di
// Codex dalla foto di Andrea.
+ // Al Barrettino del 1997 (Andrea, 29 ottobre; senza foto, dalla descrizione di Andrea): Vannino, alto, magro, coi
+ // capelli corti e la fossetta nel mento; Pierino Parigi (consenso del figlio Marco, 29 ottobre), brizzolato, con gli
+ // occhiali e la barba rada. Sprite di Codex.
+ vannino: { name: 'Vannino', color: '#ffe6a8', sprite: 'assets/characters/vannino.json' },
+ pierino_parigi: { name: 'Pierino', color: '#c8e0ff', sprite: 'assets/characters/pierino_parigi.json' },
// Lo zio Mario di Andrea (sprite di Codex dalla foto di Andrea, 29 ottobre), alla Ghiacciomobile nel muro.
zio_mario: { name: 'Lo zio Mario', color: '#cfe3ff', sprite: 'assets/characters/zio_mario.json' },
chiaru: { name: 'Il Chiaru', color: '#ffd6a0', sprite: 'assets/characters/chiaru.json' },
@@ -2749,7 +2772,9 @@ export const story = {
speakers: { arcu: [374, 176], zio_mario: [440, 196] },
people: [
// L'Arcu sotto la tenda del Trappola: due minuti fa era a Campomigliaio (Andrea, 28 ottobre).
- { who: 'arcu', at: [374, 248], facing: -1, size: 0.75, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
+ // La macchina del tempo, dopo i pokerini dell'Arcu.
+ { who: 'macchina', when: 'macchina_trappola', at: [200, 262], size: 1.1, idle: 'stand', talk: [] },
+ { who: 'arcu', at: [374, 248], facing: -1, size: 1.05, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
// Lo zio Mario davanti alla Ghiacciomobile nel muro, che si gratta la testa (sprite di Codex dalla sua foto,
// 29 ottobre). Solo finché la macchina è lì. [DA ANDREA: le sue battute]
{ who: 'zio_mario', when: '!indizio_regolo', at: [440, 330], facing: 1, idle: 'think', talk: ['talk', 'explain', 'point'] },
@@ -2795,6 +2820,14 @@ export const story = {
use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }],
},
},
+ {
+ id: 'macchina_tempo', name: 'macchina del tempo', box: [150, 214, 104, 48], at: [212, 280], face: 'up', default: 'use', when: 'macchina_trappola',
+ actions: {
+ look: [{ say: 'La macchina del tempo, parcheggiata davanti al Trappola. Sul cruscotto la data è già pronta.' }],
+ use: [{ say: 'Salgo. Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.' }, { closeup: 'viaggio' }],
+ open: [{ say: 'Salgo. Imposto: 10 luglio 1997, ore 18 e 30. Il pokerino da Vannino.' }, { closeup: 'viaggio' }],
+ },
+ },
{
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
@@ -3322,6 +3355,54 @@ export const story = {
},
],
},
+ // Il Barrettino il 10 luglio 1997 alle 18:30 (Andrea, 29 ottobre; l'interno è una proposta di Codex, [DA ANDREA]):
+ // Vannino e Pierino Parigi con la tavola apparecchiata da poker. Si torna nel 2007 dalla porta.
+ barrettino_1997: {
+ name: 'Il Barrettino, 10 luglio 1997',
+ // Ci si arriva solo con la macchina del tempo: niente mappa, niente strade (la porta riporta nel 2007).
+ viaggio: true,
+ background: 'assets/scenes/barrettino-1997.png',
+ indoor: true,
+ walk: [[0, 360], [0, 300], [110, 282], [540, 282], [640, 300], [640, 360]],
+ depth: [[282, 2.1], [360, 2.6]],
+ entry: [320, 330],
+ enter: [{ if: '!visto_1997', then: [{ set: 'visto_1997' }, { say: 'Il Barrettino. Nel 1997. Si sente ancora l\'odore del caffè e delle sigarette.' }] }],
+ speakers: { vannino: [160, 120], pierino_parigi: [470, 120] },
+ people: [
+ { who: 'vannino', at: [160, 300], facing: 1, size: 1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.1 },
+ { who: 'pierino_parigi', at: [470, 300], facing: -1, size: 1, idle: 'cards', talk: ['talk', 'explain'], blink: 'blink', every: 3.6 },
+ ],
+ hotspots: [
+ {
+ id: 'tavola', name: 'la tavola da poker', box: [236, 186, 150, 70], at: [320, 300], face: 'up', default: 'talk',
+ actions: {
+ look: [{ say: 'Il panno verde, le fiches in pila, il mazzo e il posacenere pieno. Una sedia è vuota: è per me.' }],
+ talk: [{ dialogue: 'pokerino' }],
+ use: [{ dialogue: 'pokerino' }],
+ },
+ },
+ {
+ id: 'vannino', name: 'Vannino', box: [130, 160, 60, 140], at: [220, 320], face: 'left', default: 'talk',
+ actions: { look: [{ say: 'Vannino, alto e secco, col canovaccio sulla spalla. E quella fossetta nel mento.' }], talk: [{ dialogue: 'pokerino' }] },
+ },
+ {
+ id: 'pierino', name: 'Pierino', box: [440, 160, 60, 140], at: [410, 320], face: 'right', default: 'talk',
+ actions: { look: [{ say: 'Pierino Parigi, con gli occhiali sul naso, che guarda le carte come se lo tradissero.' }], talk: [{ dialogue: 'pokerino' }] },
+ },
+ {
+ id: 'televisore', name: 'televisore', box: [440, 20, 60, 50], at: [480, 300], face: 'up',
+ actions: { look: [{ say: 'Il televisore a tubo. È il 1997: niente smartphone, solo il televideo.' }] },
+ },
+ {
+ id: 'porta', name: 'la porta: si torna nel 2007', box: [70, 30, 70, 230], at: [120, 300], face: 'left', default: 'walk',
+ actions: {
+ look: [{ say: 'Fuori c\'è la sera del 1997. E parcheggiata lì, la macchina del tempo.' }],
+ walk: [{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' }, { go: 'via_liguri_magelli', at: [212, 280], face: 'down' }],
+ open: [{ say: 'Si torna nel 2007. Ciao Vannino, ciao Pierino.' }, { go: 'via_liguri_magelli', at: [212, 280], face: 'down' }],
+ },
+ },
+ ],
+ },
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
scuola: {
name: 'La scuola',
@@ -3685,6 +3766,16 @@ export const story = {
{ say: 'Col Betino? Io un perdo mai: un vo mai forte abbastanza da cadere.', who: 'arcu', pose: 'explain' },
{ say: 'Come la si fa?', who: 'arcu', pose: 'talk' },
] },
+ // I pokerini da Vannino: l'Arcu fa comparire la macchina del tempo (Andrea, 29 ottobre). [DA ANDREA: le battute dell'Arcu]
+ { text: 'Bei tempi quando qui si faceva i pokerini...', then: [
+ { if: 'macchina_trappola', then: [{ say: 'La macchina è sempre lì. La data è già impostata.', who: 'arcu', pose: 'explain' }], else: [
+ { say: 'I pokerini da Vannino, col Pierino che bluffava anche a briscola.', who: 'arcu', pose: 'laugh' },
+ { say: 'Ci vuoi tornare? Aspetta.', who: 'arcu', pose: 'explain' },
+ { set: 'macchina_trappola' },
+ { say: 'Ma quella è... la macchina del tempo!' },
+ { say: 'Il plutonio l\'ha trovato Alvarino. Basta salire e mettere la data.', who: 'arcu', pose: 'talk' },
+ ] },
+ ] },
{ text: 'Ci si vede dopo, Arcu.', next: 'end' },
],
},
@@ -3760,6 +3851,29 @@ export const story = {
},
},
},
+ // Al Barrettino del 1997: il pokerino con Vannino e Pierino. [DA ANDREA: le battute]
+ pokerino: {
+ nodes: {
+ start: {
+ options: [
+ { text: 'C\'è posto per uno?', next: 'livello', then: [
+ { say: 'La sedia è lì. Invito due, punto cinque. Come la vuoi?', who: 'vannino', pose: 'explain' },
+ ] },
+ { text: 'Vannino, un caffè?', then: [{ say: 'Subito. Ma prima una mano, sennò che ci fai qui?', who: 'vannino', pose: 'talk' }] },
+ { text: 'Pierino, come va la mano?', then: [{ say: 'Bene. O male. Con me un si sa mai, ed è questo il bello.', who: 'pierino_parigi', pose: 'laugh' }] },
+ { text: 'Dopo.', next: 'end' },
+ ],
+ },
+ livello: {
+ options: [
+ { text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona: si gioca per il caffè.', who: 'pierino_parigi', pose: 'talk' }, { closeup: 'poker', level: 'bona' }] },
+ { text: 'Seria.', next: 'end', then: [{ say: 'Seria. Carte, Vannino.', who: 'pierino_parigi', pose: 'cards' }, { closeup: 'poker', level: 'seria' }] },
+ { text: 'A fassi male.', next: 'end', then: [{ say: 'A fassi male? Allora io bluffo.', who: 'pierino_parigi', pose: 'laugh' }, { closeup: 'poker', level: 'male' }] },
+ { text: 'No, aspetta.', next: 'start' },
+ ],
+ },
+ },
+ },
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
chiaru: {
nodes: {
diff --git a/style.css b/style.css
index 94ef500..1c16401 100644
--- a/style.css
+++ b/style.css
@@ -37,14 +37,14 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
#scopa-controls p, #tressette-controls p { max-width: 76em; }
#scopa-next { width: 100%; min-height: 40px; margin-bottom: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; cursor: pointer; }
.card-hand + .card-buttons { margin-top: 8px; }
-#briscola-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; }
+#poker-controls, #briscola-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; }
#briscola-status { color: #f3dcac; min-height: 2em; margin: 0 0 10px; }
.card-hand { display: flex; gap: 8px; }
.card-hand button { flex: 1; min-width: 0; min-height: 48px; padding: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 10px; line-height: 1.7; cursor: pointer; }
.card-hand button[aria-pressed="true"] { border-color: #ffdb76; }
.card-hand button:disabled { opacity: .5; cursor: default; }
-#briscola-controls summary { cursor: pointer; padding: 10px 0; }
-#briscola-controls p { max-width: 76em; }
+#poker-controls summary, #briscola-controls summary { cursor: pointer; padding: 10px 0; }
+#poker-controls p, #briscola-controls p { max-width: 76em; }
#bocce-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; }
#bocce-status { color: #f3dcac; margin: 0 0 10px; line-height: 1.5; min-height: 1.5em; }
.bocce-adjust { display: flex; align-items: center; justify-content: center; gap: 12px; }
@@ -325,7 +325,7 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#app.presenta { justify-content: center; }
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
- #briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
+ #briscola-controls, #tressette-controls, #scopa-controls, #poker-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
.card-hand-many button, .card-buttons button { flex-basis: 40%; overflow-wrap: anywhere; }
.card-hand { flex-direction: column; gap: 4px; }
diff --git a/tests/poker.test.mjs b/tests/poker.test.mjs
new file mode 100644
index 0000000..d2d6960
--- /dev/null
+++ b/tests/poker.test.mjs
@@ -0,0 +1,86 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import * as P from '../src/engine/poker.js';
+import { drawPoker, cardAt, HAND } from '../src/engine/poker-render.js';
+import { Game, MINIGAMES, TRIP } from '../src/engine/game.js';
+import { story } from '../src/story/story.js';
+import { settle } from './helpers.mjs';
+
+const card = s => ({ rank: s.slice(0, -1), suit: { c: 'cuori', q: 'quadri', f: 'fiori', p: 'picche' }[s.at(-1)] });
+const hand = s => s.split(' ').map(card);
+
+test('i punti del mazzo corto: il colore batte il full, A-7-8-9-10 è la scala più bassa', () => {
+ const order = ['Ac Kf 9p 8q 7c', '7c 7f Kp 9q 8c', '7c 7f 9p 9q Ac', 'Jc Jf Jp 9q 8c', 'Ac 7f 8p 9q 10c', '7c 8f 9p 10q Jc',
+ 'Kc Kf Kp 9q 9c', 'Ac Kc 9c 8c 7c', 'Qc Qf Qp Qq 7c', '7c 8c 9c 10c Jc'];
+ const scores = order.map(h => P.evaluate(hand(h)));
+ for (let i = 1; i < scores.length; i++) assert.ok(P.compare(scores[i], scores[i - 1]) > 0, `${order[i]} batte ${order[i - 1]}`);
+ assert.equal(P.handName(hand('Ac Kc 9c 8c 7c')), 'colore');
+ assert.ok(P.compare(P.evaluate(hand('Ac Af 9p 8q 7c')), P.evaluate(hand('Ac Af 9p 8q Kc'))) < 0, 'a parità di coppia contano le carte spaiate');
+});
+
+test('una partita intera: le fiches non si perdono, finisce sempre, e chi sa giocare vince più di chi vede sempre', () => {
+ const play = (seed, level, brain) => {
+ const c = P.newPoker(seed, { level });
+ for (let i = 0; i < 20000 && c.step !== 'result'; i++) {
+ if (P.myTurn(c)) P.choose(c, brain(c)); else if (P.canDraw(c)) { for (const k of P.cpuDiscards({ ...c, skill: P.LEVELS.male }, 'andrea')) P.play(c, k); P.choose(c, 'cambia'); }
+ else if (c.step === 'show') P.advance(c); else P.update(c, 1);
+ const sum = P.SEATS.reduce((n, s) => n + c.chips[s], 0) + (c.step === 'show' || c.step === 'result' ? 0 : c.pot);
+ assert.equal(sum, P.START * 3);
+ }
+ assert.equal(c.step, 'result');
+ return c.result.chips.andrea;
+ };
+ const smart = c => { const s = P.strength(c.cards.andrea); return s > 0.5 && P.canRaise(c) ? 'rilancio' : s > 0.15 || !P.toCall(c) ? 'vedo' : 'lascio'; };
+ let a = 0, b = 0;
+ for (let seed = 1; seed <= 60; seed++) { a += play(seed, 'bona', smart); b += play(seed, 'bona', () => 'vedo'); }
+ assert.ok(a > b, `con giudizio ${a}, vedendo sempre ${b}`);
+});
+
+test('i comandi di Andrea: il cambio si sceglie toccando (al massimo 4), le puntate coi pulsanti', () => {
+ const c = P.newPoker(3);
+ while (!P.myTurn(c)) P.update(c, 1);
+ const moves = P.moves(c).map(m => m.id);
+ assert.deepEqual(moves, ['lascio', 'vedo', 'rilancio']);
+ assert.ok(P.choose(c, 'vedo'));
+ while (!P.canDraw(c) && c.step !== 'show') P.update(c, 1);
+ if (P.canDraw(c)) {
+ assert.equal(cardAt(c, [HAND.x + 5, HAND.y + 5]), 0);
+ for (const i of [0, 1, 2, 3, 4]) P.play(c, i);
+ assert.equal(c.selected.size, 4, 'al massimo quattro');
+ assert.ok(P.choose(c, 'cambia'));
+ assert.equal(c.changed.andrea, 4);
+ }
+});
+
+test('il disegno del pokerino non cambia lo stato', () => {
+ const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
+ const c = P.newPoker(9);
+ const before = JSON.stringify({ ...c, selected: [...c.selected] });
+ drawPoker(ctx, c);
+ assert.equal(JSON.stringify({ ...c, selected: [...c.selected] }), before);
+ assert.ok(calls.some(([op, t]) => op === 'fillText' && /POKERINO/.test(t)));
+});
+
+test('la macchina del tempo: l\'Arcu la fa comparire, il viaggio porta al Barrettino del 1997 e la porta riporta nel 2007', () => {
+ assert.ok(MINIGAMES.poker);
+ const g = new Game(story); g.go('via_liguri_magelli'); settle(g);
+ assert.ok(!g.hotspot('macchina_tempo'));
+ g.act('talk', 'arcu'); settle(g);
+ g.choose(g.options().findIndex(o => o.text.startsWith('Bei tempi'))); settle(g);
+ assert.ok(g.state.flags.macchina_trappola && g.hotspot('macchina_tempo'));
+ g.choose(g.options().findIndex(o => o.text.startsWith('Ci si vede'))); settle(g);
+ g.act('use', 'macchina_tempo'); settle(g);
+ assert.equal(g.closeup?.id, 'viaggio');
+ for (let t = 0; t < TRIP.end + 0.5 && g.closeup; t += 0.05) g.update(0.05);
+ settle(g);
+ assert.equal(g.state.scene, 'barrettino_1997');
+ g.act('talk', 'tavola'); settle(g);
+ g.choose(g.options().findIndex(o => o.text.startsWith('C\'è posto'))); settle(g);
+ g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
+ assert.equal(g.closeup?.id, 'poker'); assert.equal(g.closeup.level, 'male');
+ g.closeup.step = 'result'; g.closeup.result = { winner: 'player', chips: { andrea: 200, vannino: 60, pierino: 40 }, top: ['andrea'] };
+ g.closeupClose(); settle(g);
+ assert.ok(g.state.flags.poker_vinto);
+ g.act('walk', 'porta'); settle(g);
+ assert.equal(g.state.scene, 'via_liguri_magelli');
+});
diff --git a/tests/story.test.mjs b/tests/story.test.mjs
index b939b91..cd99a92 100644
--- a/tests/story.test.mjs
+++ b/tests/story.test.mjs
@@ -260,6 +260,8 @@ test('il libro dell\'oratorio: il primo piano si sfoglia, l\'ultima pagina è «
test('la mappa: ogni scena ci sta, i posti compaiono solo visti, il viaggio rispetta i dialoghi', () => {
for (const [id, sc] of Object.entries(story.scenes)) {
+ // Le scene del viaggio nel tempo (`viaggio`) non sono sulla mappa: ci si va solo con la macchina.
+ if (sc.viaggio) { assert.ok(!sc.map, `${id}: nel passato la mappa non porta`); continue; }
assert.ok(Array.isArray(sc.map), `${id} non è sulla mappa`);
assert.ok(sc.map.every(v => v >= 0 && v <= 100), `${id} fuori dalla pianta`);
}
@@ -341,7 +343,14 @@ test('le strade vanno e tornano: niente sensi unici, e ogni posto si raggiunge d
for (const b of to) assert.ok(exits[b]?.has(a), `da ${a} si va a ${b}, ma da ${b} non si torna a ${a}`);
const seen = new Set([story.start.scene]), queue = [story.start.scene];
while (queue.length) for (const b of exits[queue.shift()] ?? []) if (!seen.has(b)) { seen.add(b); queue.push(b); }
- for (const id of Object.keys(story.scenes)) assert.ok(seen.has(id), `${id} non si raggiunge`);
+ for (const [id, sc] of Object.entries(story.scenes)) {
+ if (!sc.viaggio) { assert.ok(seen.has(id), `${id} non si raggiunge`); continue; }
+ // Una scena del viaggio nel tempo: ci porta un `go` (la fine del viaggio), e un `go` riporta in una scena vera.
+ const text = JSON.stringify(story);
+ assert.ok(text.includes(`"go":"${id}"`), `${id}: nessun viaggio ci porta`);
+ const back = JSON.stringify(sc.hotspots).match(/"go":"([a-z_0-9]+)"/g)?.map(m => m.slice(6, -1)) ?? [];
+ assert.ok(back.some(b => seen.has(b)), `${id}: non si torna indietro`);
+ }
});
test('la catena del ghiacciolo: Tosca, cedrata, stecco, gomma, griglia, chiavetta, casetta, torcia', () => {
diff --git a/tools/voci.json b/tools/voci.json
index e26a2ce..04120fb 100644
--- a/tools/voci.json
+++ b/tools/voci.json
@@ -35,6 +35,13 @@
"speaker": "Xavier Hayasaka",
"filtro": "asetrate=24000*1.12,aresample=24000,atempo=0.92"
},
+ "vannino": {
+ "speaker": "Kumar Dahl"
+ },
+ "pierino_parigi": {
+ "speaker": "Eugenio Mataracı",
+ "filtro": "asetrate=24000*0.97,aresample=24000"
+ },
"zio_mario": {
"speaker": "Viktor Menelaos",
"filtro": "asetrate=24000*0.95,aresample=24000,atempo=1.04"