Compare commits

...
6 Commits
Author SHA1 Message Date
manzolo 6fdbef1dd7 Versione 0.1.70
test / test (push) Successful in 30s
test / browser (bruciate) (push) Successful in 47s
test / browser (cerbottana) (push) Successful in 46s
test / browser (ambient) (push) Successful in 48s
test / browser (biliardo) (push) Successful in 53s
test / browser (campana) (push) Successful in 59s
test / browser (briscola) (push) Successful in 1m7s
test / browser (gara) (push) Successful in 41s
test / browser (condor) (push) Successful in 42s
test / browser (flipper) (push) Successful in 48s
test / browser (ingorgo) (push) Successful in 1m2s
test / browser (micro) (push) Successful in 57s
test / browser (risveglio) (push) Successful in 34s
test / browser (scopa) (push) Successful in 28s
test / browser (tressette) (push) Successful in 25s
test / browser (telefono) (push) Successful in 31s
test / browser (smarronata) (push) Successful in 45s
test / browser (veicoli) (push) Successful in 12s
test / browser (minigiochi) (push) Successful in 2m44s
test / browser (telefoni) (push) Successful in 2m9s
test / browser (principale) (push) Successful in 4m24s
test / release (push) Successful in 11s
2026-11-01 17:47:32 +01:00
manzoloandClaude Opus 5.5 a926ad6423 Prove nel browser sulla scheda video: tests/chromium.mjs usa la GPU se c'è (schermata 41 ms invece di 633; tutte e venti in ~4 min invece di ~16, sei alla volta), altrimenti swiftshader; la pipeline dà la GPU ai job, runner a 6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-11-01 17:47:10 +01:00
manzoloandClaude Opus 5.5 8319f58f1e Flipper da tastiera: la freccia giù carica la molla come Spazio, i due Shift comandano le palette (main.js passa ai minigiochi ShiftLeft e ShiftRight); test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-11-01 17:47:10 +01:00
manzoloandClaude Opus 5.5 21cfe8bb89 STATO: durata vera del primo giro della pipeline
test / test (push) Successful in 19s
test / browser (ambient) (push) Skipped
test / browser (biliardo) (push) Skipped
test / browser (briscola) (push) Skipped
test / browser (bruciate) (push) Skipped
test / browser (campana) (push) Skipped
test / browser (cerbottana) (push) Skipped
test / browser (condor) (push) Skipped
test / browser (flipper) (push) Skipped
test / browser (gara) (push) Skipped
test / browser (ingorgo) (push) Skipped
test / browser (micro) (push) Skipped
test / browser (minigiochi) (push) Skipped
test / browser (principale) (push) Skipped
test / browser (risveglio) (push) Skipped
test / browser (scopa) (push) Skipped
test / browser (smarronata) (push) Skipped
test / browser (telefoni) (push) Skipped
test / browser (telefono) (push) Skipped
test / browser (tressette) (push) Skipped
test / browser (veicoli) (push) Skipped
test / release (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-11-01 15:48:35 +01:00
manzoloandClaude Opus 5.5 b5a38531c4 STATO: le prove nel browser nella pipeline, primo giro verde
test / test (push) Successful in 20s
test / browser (ambient) (push) Skipped
test / browser (biliardo) (push) Skipped
test / browser (briscola) (push) Skipped
test / browser (bruciate) (push) Skipped
test / browser (campana) (push) Skipped
test / browser (cerbottana) (push) Skipped
test / browser (condor) (push) Skipped
test / browser (flipper) (push) Skipped
test / browser (gara) (push) Skipped
test / browser (ingorgo) (push) Skipped
test / browser (micro) (push) Skipped
test / browser (minigiochi) (push) Skipped
test / browser (principale) (push) Skipped
test / browser (risveglio) (push) Skipped
test / browser (scopa) (push) Skipped
test / browser (smarronata) (push) Skipped
test / browser (telefoni) (push) Skipped
test / browser (telefono) (push) Skipped
test / browser (tressette) (push) Skipped
test / browser (veicoli) (push) Skipped
test / release (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-11-01 15:46:47 +01:00
manzoloandClaude Opus 5.5 666e274140 Le prove nel browser nella pipeline di Gitea: sui tag (o a mano) le venti prove, una per job nell'immagine di Playwright, prima della release; un test pretende che l'elenco coincida con le prove di verify.mjs e che l'immagine abbia il Playwright di package-lock.json
test / test (push) Successful in 20s
test / browser (ambient) (push) Skipped
test / browser (biliardo) (push) Skipped
test / browser (briscola) (push) Skipped
test / browser (bruciate) (push) Skipped
test / browser (campana) (push) Skipped
test / browser (cerbottana) (push) Skipped
test / browser (condor) (push) Skipped
test / browser (flipper) (push) Skipped
test / browser (gara) (push) Skipped
test / browser (ingorgo) (push) Skipped
test / browser (micro) (push) Skipped
test / browser (minigiochi) (push) Skipped
test / browser (principale) (push) Skipped
test / browser (risveglio) (push) Skipped
test / browser (scopa) (push) Skipped
test / browser (smarronata) (push) Skipped
test / browser (telefoni) (push) Skipped
test / browser (telefono) (push) Skipped
test / browser (tressette) (push) Skipped
test / browser (veicoli) (push) Skipped
test / release (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-11-01 15:26:37 +01:00
32 changed files with 139 additions and 44 deletions
+25 -2
View File
@@ -1,11 +1,13 @@
# Le Gitea Actions del gioco: girano sul runner del PC di Andrea (make runner; label ubuntu-latest = node:22-bookworm). # Le Gitea Actions del gioco: girano sul runner del PC di Andrea (make runner; label ubuntu-latest = node:22-bookworm).
# A ogni push su main e a ogni tag i test unitari; su un tag vX.Y.Z, se passano, la release con i commit dal tag prima. # A ogni push su main e a ogni tag i test unitari; su un tag vX.Y.Z (o a mano, «Run workflow») anche le venti prove nel
# browser, una per job nell'immagine di Playwright, sulla scheda video del PC; se passa tutto, la release.
# Il controllo dei file pubblicabili non sta qui (a pipeline partita il push è già pubblico): è l'hook pre-push. # Il controllo dei file pubblicabili non sta qui (a pipeline partita il push è già pubblico): è l'hook pre-push.
name: test name: test
on: on:
push: push:
branches: [main] branches: [main]
tags: ['v*'] tags: ['v*']
workflow_dispatch:
jobs: jobs:
test: test:
@@ -15,8 +17,29 @@ jobs:
- name: Test unitari - name: Test unitari
run: node --version && node --test tests/*.test.mjs run: node --version && node --test tests/*.test.mjs
release: # Le prove nel browser, una per job: l'elenco è SUITES di scripts/verify.mjs (verify.test.mjs pretende che coincidano),
# l'immagine ha la stessa versione di Playwright di package-lock.json (idem).
browser:
needs: test needs: test
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.64.1-noble
# La scheda video del PC (runtime NVIDIA di Docker): Chromium disegna lì, tests/chromium.mjs la trova da solo.
options: --gpus all -e NVIDIA_DRIVER_CAPABILITIES=all
strategy:
fail-fast: false
matrix:
prova: [principale, minigiochi, telefoni, ambient, veicoli, ingorgo, smarronata, briscola, tressette, scopa, biliardo, flipper, micro, cerbottana, condor, campana, bruciate, gara, telefono, risveglio]
steps:
- uses: actions/checkout@v4
- name: Playwright
run: npm ci --no-audit --no-fund
- name: Prova ${{ matrix.prova }}
run: node scripts/verify.mjs ${{ matrix.prova }}
release:
needs: [test, browser]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
+4 -3
View File
@@ -18,8 +18,9 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p
- Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura. - Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura.
- Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi - Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi
sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile
o risorse). Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende). o risorse). Un `chromium.launch` nuovo nelle prove usa `args: ARGS` di `tests/chromium.mjs`. Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende).
- Verifica visiva nel browser con Playwright e swiftshader prima di dichiarare finito qualcosa di grafico. - Verifica visiva nel browser con Playwright prima di dichiarare finito qualcosa di grafico. Chromium disegna sulla scheda
video del PC se c'è, altrimenti con swiftshader (`tests/chromium.mjs`, dal 2026-11-01: giro intero ~4 min invece di ~16).
- Pubblicare (tag/bump/push) solo quando Andrea lo chiede. - Pubblicare (tag/bump/push) solo quando Andrea lo chiede.
- Persone vere nel gioco solo con consenso; niente nomi, personaggi o musiche di giochi esistenti. - Persone vere nel gioco solo con consenso; niente nomi, personaggi o musiche di giochi esistenti.
- Prompt delle immagini in `prompts/` e fogli grezzi in `assets/raw/`: esclusi da git. - Prompt delle immagini in `prompts/` e fogli grezzi in `assets/raw/`: esclusi da git.
@@ -28,4 +29,4 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p
- **Pubblicare (dal 2026-11-01)**: origin è https://git.manzolo.it/manzolo/leggenda-del-regolo, pubblico: prima `make check-public`, poi `git push origin main --tags`, solo se Andrea lo chiede. - **Pubblicare (dal 2026-11-01)**: origin è https://git.manzolo.it/manzolo/leggenda-del-regolo, pubblico: prima `make check-public`, poi `git push origin main --tags`, solo se Andrea lo chiede.
`make deploy` costruisce l'immagine dal tag della versione, la spinge su `git.manzolo.it/manzolo/leggenda-regolo:vX.Y.Z` (e `:latest`); sul VPS il compose fissa quel tag e fa `docker compose pull && up -d`. `make deploy` costruisce l'immagine dal tag della versione, la spinge su `git.manzolo.it/manzolo/leggenda-regolo:vX.Y.Z` (e `:latest`); sul VPS il compose fissa quel tag e fa `docker compose pull && up -d`.
Per tornare indietro: sul VPS (`Workspaces/docker/leggenda-regolo/docker-compose.yml`) si rimette il tag precedente e si rifà pull e up. Per tornare indietro: sul VPS (`Workspaces/docker/leggenda-regolo/docker-compose.yml`) si rimette il tag precedente e si rifà pull e up.
Hook pre-push (`make hooks`, una volta per clone) lancia check-public; le Gitea Actions (`.gitea/workflows/test.yml`: test, e release sui tag) girano sul runner del PC, che si crea o riavvia con `make runner`. Hook pre-push (`make hooks`, una volta per clone) lancia check-public; le Gitea Actions (`.gitea/workflows/test.yml`: test unitari a ogni push; sui tag, o a mano, anche le venti prove nel browser, una per job, poi la release) girano sul runner del PC, che si crea o riavvia con `make runner`.
+12
View File
@@ -316,6 +316,18 @@ versione nuova.
`make bump` dopo una modifica alla briscola riprova la briscola e basta; dopo una modifica alla storia la partita intera `make bump` dopo una modifica alla briscola riprova la briscola e basta; dopo una modifica alla storia la partita intera
e le scene speciali (ambient, veicoli, ingorgo, smarronata), non la briscola; dopo `style.css` o `game.js` tutto. e le scene speciali (ambient, veicoli, ingorgo, smarronata), non la briscola; dopo `style.css` o `game.js` tutto.
Le prove dei minigiochi, che prima non entravano in `make verify`, ora ci stanno. `make verify-plan` spiega. Le prove dei minigiochi, che prima non entravano in `make verify`, ora ci stanno. `make verify-plan` spiega.
Nella pipeline di Gitea (runner sul PC) sui tag, o a mano, girano tutte e venti, una per job nell'immagine di
Playwright, prima della release: il primo giro completo (1 novembre, a mano su main) verde in 17 minuti.
- **Prove sulla scheda video (1 novembre, Andrea: «prova con la scheda video»):** Chromium disegnava con swiftshader,
sul processore: una schermata 633 ms. Con la RTX del PC (`--use-angle=gl-egl`) 41 ms; micro da 283 a 27 s, briscola da
259 a 29, flipper da 186 a 10; tutte e venti da ~16 minuti (due alla volta) a ~4 (sei alla volta), tutte verdi. Le
opzioni stanno in `tests/chromium.mjs`: scheda video se c'è (`/dev/dri`, `/dev/nvidia0`), altrimenti swiftshader;
`REGOLO_GPU=0/1` forza. Con 6 o 8 prove insieme il giro dura uguale: il limite è la partita intera (243 s, il tempo
del gioco, non della grafica). La pipeline dà la GPU ai job (`--gpus all`, runtime NVIDIA di Docker); runner a 6 job.
- **Flipper da tastiera (1 novembre, Andrea):** la freccia giù carica la molla come Spazio, i due Shift comandano le
palette (sinistro e destro, `e.code` passato da main.js ai minigiochi). Test unitario e nel browser.
## Materiale ricevuto da Andrea (in `assets/raw/`, escluso da git) ## Materiale ricevuto da Andrea (in `assets/raw/`, escluso da git)
+3 -3
View File
@@ -305,9 +305,9 @@
sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p> sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p>
</details> </details>
<details id="flipper-help" hidden><summary>Come si gioca</summary> <details id="flipper-help" hidden><summary>Come si gioca</summary>
<p>Il Regolo, il flipper del River. Tieni premuto per caricare la molla e lascia per lanciare, oppure usa <p>Il Regolo, il flipper del River. Tieni premuto per caricare la molla e lascia per lanciare, oppure tieni
Spazio o il pulsante Lancia. Una valvola impedisce alla pallina entrata in campo di tornare sulla molla. premuto Spazio o la freccia giù, o usa il pulsante Lancia. Una valvola impedisce alla pallina entrata in campo di tornare sulla molla.
Le frecce sinistra e destra, i pulsanti Flipper o le due metà della scena comandano le palette; Le frecce sinistra e destra, i due Shift, i pulsanti Flipper o le due metà della scena comandano le palette;
puoi usare due dita insieme e il tasto destro del mouse comanda sempre la paletta destra.</p> puoi usare due dita insieme e il tasto destro del mouse comanda sempre la paletta destra.</p>
<p>Le rampe FORTEZZA (ciano) e TREBBIO (ambra) valgono 15.000 punti. Tre passaggi completi sulla stessa <p>Le rampe FORTEZZA (ciano) e TREBBIO (ambra) valgono 15.000 punti. Tre passaggi completi sulla stessa
rampa entro 20 secondi dal primo fanno LOCK: la pallina viene trattenuta e ne arriva una nuova gratis. rampa entro 20 secondi dal primo fanno LOCK: la pallina viene trattenuta e ne arriva una nuova gratis.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.69", "version": "0.1.70",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+2
View File
@@ -26,4 +26,6 @@ if [[ ! -s $RUNNER_DATA/.runner ]]; then
fi fi
# Senza token nell'ambiente del container: da qui in poi si autentica con .runner. # Senza token nell'ambiente del container: da qui in poi si autentica con .runner.
docker compose -f "$COMPOSE" up -d docker compose -f "$COMPOSE" up -d
# config.yaml si legge solo all'avvio: se è cambiato (capacità, opzioni) il runner riparte.
[[ $(docker inspect -f '{{.State.StartedAt}}' gitea-runner | date -f - +%s) -lt $(stat -c %Y "$(dirname "$COMPOSE")/config.yaml") ]] && docker restart gitea-runner >/dev/null
sleep 2; docker logs --tail 1 gitea-runner sleep 2; docker logs --tail 1 gitea-runner
+11 -5
View File
@@ -3,7 +3,8 @@
// si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo // si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo
// verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il // verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il
// motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola. // motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola.
// node scripts/verify.mjs le prove da rifare, in parallelo (JOBS=2: con quattro Chromium swiftshader insieme le schermate vanno in timeout), e aggiorna i verdi // node scripts/verify.mjs le prove da rifare, in parallelo, e aggiorna i verdi (JOBS: quante insieme; con la
// scheda video JOBS_GPU, senza 2: con quattro Chromium swiftshader le schermate vanno in timeout)
// node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché // node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché
// node scripts/verify.mjs --all tutte, senza guardare i verdi // node scripts/verify.mjs --all tutte, senza guardare i verdi
// node scripts/verify.mjs briscola solo quelle nominate (anche se verdi) // node scripts/verify.mjs briscola solo quelle nominate (anche se verdi)
@@ -13,10 +14,11 @@ import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { cpus } from 'node:os'; import { cpus } from 'node:os';
import { GPU } from '../tests/chromium.mjs';
// Il motore comune a tutto: cambia questo e si riprova ogni minigioco. // Il motore comune a tutto: cambia questo e si riprova ogni minigioco.
export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js', export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js',
'tests/helpers.mjs', 'package-lock.json']; 'tests/helpers.mjs', 'tests/chromium.mjs', 'package-lock.json'];
// Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare), // Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare),
// test unitari (girano sempre, a parte), e questo stesso script (lo prova verify.test.mjs). // test unitari (girano sempre, a parte), e questo stesso script (lo prova verify.test.mjs).
export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\//, export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\//,
@@ -49,6 +51,9 @@ export const SUITES = {
risveglio: { run: ['tests/risveglio.browser.mjs'] }, risveglio: { run: ['tests/risveglio.browser.mjs'] },
}; };
// Prove insieme con la scheda video (misurato il 1° novembre 2026 sul PC di Andrea, 16 thread e RTX 3080 Ti).
const JOBS_GPU = 6; // con 6 e con 8 il giro intero dura uguale (~4 min): da solo «principale» ne prende 243 s
const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i'); const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i');
const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path; const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path;
export const ignored = path => IGNORED.some(r => r.test(path)); export const ignored = path => IGNORED.some(r => r.test(path));
@@ -121,9 +126,10 @@ async function main() {
const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : !t.changed.length ? "cambiato l'elenco dei file da cui dipende" : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`; const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : !t.changed.length ? "cambiato l'elenco dei file da cui dipende" : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`;
console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`); console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`);
} }
if (green.length) console.log(`già verdi: ${green.join(', ')}`); if (green.length && !named.length) console.log(`già verdi: ${green.join(', ')}`);
if (onlyPlan) return; if (onlyPlan) return;
const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(2, Math.floor(cpus().length / 4))); const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(GPU ? JOBS_GPU : 2, Math.floor(cpus().length / (GPU ? 2 : 4))));
console.log(`Chromium ${GPU ? 'sulla scheda video' : 'con swiftshader'}, ${Math.min(jobs, todo.length)} prove alla volta.`);
const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = []; const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = [];
const worker = async () => { for (let t; (t = queue.shift());) { const worker = async () => { for (let t; (t = queue.shift());) {
const r = await run(t.name); results.push(r); const r = await run(t.name); results.push(r);
@@ -137,7 +143,7 @@ async function main() {
for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`); for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`);
const failed = results.filter(r => r.code !== 0).map(r => r.name); const failed = results.filter(r => r.code !== 0).map(r => r.name);
if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); } if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); }
console.log(`Prove nel browser OK: ${results.length} rifatte, ${green.length} già verdi.`); console.log(`Prove nel browser OK: ${results.length} rifatte${named.length ? '' : `, ${green.length} già verdi`}.`);
} }
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
+4 -3
View File
@@ -498,7 +498,7 @@ export function cancel(c, pointerId) {
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId); for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
return true; return true;
} }
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), e le frecce. // I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), le frecce e i due Shift.
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true); export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
export function stopTurn(c, who = 'button') { export function stopTurn(c, who = 'button') {
if (who === 'click') return false; // il colpo breve finisce da sé if (who === 'click') return false; // il colpo breve finisce da sé
@@ -509,8 +509,9 @@ export const nudge = () => false;
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; } export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
export function key(c, k, down) { export function key(c, k, down) {
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js) if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; } if (['ArrowLeft', 'ArrowRight', 'ShiftLeft', 'ShiftRight'].includes(k)) { hold(c, k.endsWith('Left') ? 'left' : 'right', 'key-' + k, down); return true; }
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; } // La molla: Spazio, o la freccia giù (si tira indietro come la molla vera).
if (k === ' ' || k === 'ArrowDown') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
if (k === 'Enter') { if (down) shoot(c); return true; } if (k === 'Enter') { if (down) shoot(c); return true; }
return false; return false;
} }
+5 -3
View File
@@ -661,7 +661,7 @@ const MINI = {
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' }, condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' }, campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' }, bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper o le frecce sinistra e destra.' }, flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia; o tieni premuto Spazio o la freccia giù). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper, le frecce sinistra e destra o i due Shift.' },
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.' }, 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.' }, 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.' },
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto). // La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
@@ -825,6 +825,8 @@ function updateChoices() {
})); }));
} }
// Ai minigiochi lo Shift arriva coi due lati distinti (ShiftLeft, ShiftRight): nel flipper comandano le due palette.
const keyName = e => e.key === 'Shift' ? e.code : e.key;
window.addEventListener('keydown', e => { window.addEventListener('keydown', e => {
if (e.target instanceof HTMLInputElement && e.target.type === 'range' && e.key !== 'Escape') return; if (e.target instanceof HTMLInputElement && e.target.type === 'range' && e.key !== 'Escape') return;
// Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude. // Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude.
@@ -917,7 +919,7 @@ window.addEventListener('keydown', e => {
if (e.key === 'Escape') { game.closeupClose(); return; } if (e.key === 'Escape') { game.closeupClose(); return; }
const onButton = ['BUTTON', 'SUMMARY'].includes(e.target.tagName), onControl = onButton && e.target.closest?.('#bocce-controls'); const onButton = ['BUTTON', 'SUMMARY'].includes(e.target.tagName), onControl = onButton && e.target.closest?.('#bocce-controls');
// Il GP di Fortezza: frecce, Spazio e Invio finché si tengono giù (su un pulsante Spazio e Invio lo premono). // Il GP di Fortezza: frecce, Spazio e Invio finché si tengono giù (su un pulsante Spazio e Invio lo premono).
if (!(onButton && [' ', 'Enter'].includes(e.key)) && mini().key?.(game.closeup, e.key, true, e.shiftKey)) { e.preventDefault(); return; } if (!(onButton && [' ', 'Enter'].includes(e.key)) && mini().key?.(game.closeup, keyName(e), true, e.shiftKey)) { e.preventDefault(); return; }
if (onControl && [' ', 'Enter'].includes(e.key)) return; if (onControl && [' ', 'Enter'].includes(e.key)) return;
const c = game.closeup, m = mini(); const c = game.closeup, m = mini();
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) { if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
@@ -964,7 +966,7 @@ window.addEventListener('keydown', e => {
// Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro. // Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro.
window.addEventListener('keyup', e => { window.addEventListener('keyup', e => {
// Il GP di Fortezza tiene conto di ogni tasto per conto suo: lasciarne uno non toglie il gas del dito o degli altri. // Il GP di Fortezza tiene conto di ogni tasto per conto suo: lasciarne uno non toglie il gas del dito o degli altri.
if (miniState() && mini().key?.(game.closeup, e.key, false)) return; if (miniState() && mini().key?.(game.closeup, keyName(e), false)) return;
if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease(); if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease();
}); });
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json. // La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.69'; export const VERSION = '0.1.70';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica, // Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf). // mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+2 -1
View File
@@ -3,6 +3,7 @@
// npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT). // npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT).
import { chromium } from 'playwright'; import { chromium } from 'playwright';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
import { mkdirSync, writeFileSync } from 'node:fs'; import { mkdirSync, writeFileSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
@@ -11,7 +12,7 @@ mkdirSync(out, { recursive: true });
const server = await staticServer(), port = server.port; const server = await staticServer(), port = server.port;
let browser; let browser;
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const page = await browser.newPage({ viewport: { width: 640, height: 360 } }); const page = await browser.newPage({ viewport: { width: 640, height: 360 } });
const errors = []; const errors = [];
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync, writeFileSync } from 'node:fs'; import { mkdirSync, writeFileSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-biliardo'; const out = process.env.OUT ?? '/tmp/regolo-biliardo';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -64,7 +65,7 @@ async function checkArtwork(page, name) {
} }
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [ for (const [name, mobile, viewport] of [
['desktop', false, { width: 1280, height: 860 }], ['desktop', false, { width: 1280, height: 860 }],
['touch', true, { width: 844, height: 390 }], ['touch', true, { width: 844, height: 390 }],
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync, writeFileSync } from 'node:fs'; import { mkdirSync, writeFileSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true }); const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
const server = await staticServer(), errors = []; const server = await staticServer(), errors = [];
@@ -51,7 +52,7 @@ async function artwork(page) {
for (const [name, png] of Object.entries(pictures)) writeFileSync(`${out}/briscola-${name}.png`, Buffer.from(png, 'base64')); for (const [name, png] of Object.entries(pictures)) writeFileSync(`${out}/briscola-${name}.png`, Buffer.from(png, 'base64'));
} }
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const scenarios = ['mouse', 'touch', 'keyboard'].flatMap(mode => [ const scenarios = ['mouse', 'touch', 'keyboard'].flatMap(mode => [
{ name: `desktop-${mode}`, mode, viewport: { width: 1280, height: 860 } }, { name: `desktop-${mode}`, mode, viewport: { width: 1280, height: 860 } },
{ name: `telefono-${mode}`, mode, viewport: { width: 390, height: 844 } }, { name: `telefono-${mode}`, mode, viewport: { width: 390, height: 844 } },
+2 -1
View File
@@ -5,6 +5,7 @@
// attese si sovrappongono. Una parte sola: node tests/browser.mjs --parte=minigiochi (--media-only: solo multimedia). // attese si sovrappongono. Una parte sola: node tests/browser.mjs --parte=minigiochi (--media-only: solo multimedia).
import { chromium } from 'playwright'; import { chromium } from 'playwright';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
import { mkdirSync, readFileSync } from 'node:fs'; import { mkdirSync, readFileSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { solution, route } from '../src/story/solution.js'; import { solution, route } from '../src/story/solution.js';
@@ -31,7 +32,7 @@ if (!PARTE && !process.argv.includes('--media-only')) {
process.exit(esiti.every(e => e.code === 0) ? 0 : 1); process.exit(esiti.every(e => e.code === 0) ? 0 : 1);
} }
const server = await staticServer(), PORT = server.port; const server = await staticServer(), PORT = server.port;
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const browser = await chromium.launch({ args: ARGS });
const errors = []; const errors = [];
async function open(viewport, query = '') { async function open(viewport, query = '') {
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-bruciate'; const out = process.env.OUT ?? '/tmp/regolo-bruciate';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) { for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile }); const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-campana'; const out = process.env.OUT ?? '/tmp/regolo-campana';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) { for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile }); const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-cerbottana'; const out = process.env.OUT ?? '/tmp/regolo-cerbottana';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) { for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile }); const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+10
View File
@@ -0,0 +1,10 @@
// Come si apre Chromium nelle prove nel browser (solo da node: helpers.mjs lo carica anche la pagina).
// Con una scheda video (/dev/dri o /dev/nvidia0, anche nei contenitori lanciati con --gpus) si disegna lì: una
// schermata costa ~45 ms invece di ~600 con swiftshader, e le prove girano 5-10 volte più in fretta. Senza, swiftshader.
// REGOLO_GPU=0 forza swiftshader, REGOLO_GPU=1 la scheda video.
import { existsSync } from 'node:fs';
export const GPU = process.env.REGOLO_GPU ? process.env.REGOLO_GPU === '1' : ['/dev/dri/renderD128', '/dev/nvidia0'].some(existsSync);
const GL = GPU ? ['--use-gl=angle', '--use-angle=gl-egl', '--ignore-gpu-blocklist', '--enable-gpu-rasterization']
: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'];
export const ARGS = ['--autoplay-policy=no-user-gesture-required', ...GL];
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-condor'; const out = process.env.OUT ?? '/tmp/regolo-condor';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) { for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile }); const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+12 -3
View File
@@ -5,6 +5,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-flipper'; const out = process.env.OUT ?? '/tmp/regolo-flipper';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -12,7 +13,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) { for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile }); const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
@@ -45,9 +46,10 @@ try {
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' }); await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
await touch('touchEnd', []); await touch('touchEnd', []);
} else { } else {
await page.mouse.move(...P(440, 320)); await page.mouse.down(); // Da tastiera con la freccia giù (il mouse tiene la molla come il dito, provato sul telefono).
await page.keyboard.down('ArrowDown');
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' }); await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
await page.mouse.up(); await page.keyboard.up('ArrowDown');
} }
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 }); await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su. // Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
@@ -83,6 +85,13 @@ try {
await page.keyboard.up('ArrowLeft'); await page.keyboard.up('ArrowLeft');
await page.click('#resume'); await page.click('#resume');
await page.waitForFunction(() => document.querySelector('#pause').hidden); await page.waitForFunction(() => document.querySelector('#pause').hidden);
// I due Shift: il sinistro alza la paletta sinistra, il destro la destra.
await page.keyboard.down('ShiftRight');
await page.waitForFunction(() => game.closeup.flippers.right.held.includes('key-ShiftRight') && !game.closeup.flippers.left.held.length);
await page.keyboard.down('ShiftLeft');
await page.waitForFunction(() => game.closeup.flippers.left.held.includes('key-ShiftLeft'));
await page.keyboard.up('ShiftRight'); await page.keyboard.up('ShiftLeft');
await page.waitForFunction(() => !game.closeup.flippers.left.held.length && !game.closeup.flippers.right.held.length);
} }
// Il traguardo aggiunge una pallina e lascia continuare la partita. // Il traguardo aggiunge una pallina e lascia continuare la partita.
const reward = await page.evaluate(async () => { const reward = await page.evaluate(async () => {
+4
View File
@@ -146,10 +146,14 @@ test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; d
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click')); m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length); const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length);
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length); assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
assert.ok(m.key(c, 'ShiftLeft', true)); assert.ok(c.flippers.left.held.includes('key-ShiftLeft') && !c.flippers.right.held.length);
m.key(c, 'ShiftRight', true); assert.ok(c.flippers.right.held.includes('key-ShiftRight'));
m.key(c, 'ShiftLeft', false); m.key(c, 'ShiftRight', false); assert.ok(!c.flippers.left.held.length && !c.flippers.right.held.length);
assert.ok(!m.key(c, 'x', true)); assert.ok(!m.key(c, 'x', true));
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale'); assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi. // Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d)); const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
const g = m.newFlipper(11); assert.ok(m.key(g, 'ArrowDown', true)); run(g, 0.3); assert.ok(g.holding, 'anche la freccia giù carica la molla'); m.key(g, 'ArrowDown', false); assert.ok(!m.waiting(g));
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…'); const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 120000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true)); e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 120000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
}); });
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-gara'; const out = process.env.OUT ?? '/tmp/regolo-gara';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -12,7 +13,7 @@ let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport, level] of [ for (const [name, mobile, viewport, level] of [
['touch', true, { width: 844, height: 390 }, 'seria'], ['touch', true, { width: 844, height: 390 }, 'seria'],
['portrait', true, { width: 390, height: 844 }, 'bona'], ['portrait', true, { width: 390, height: 844 }, 'bona'],
+2 -1
View File
@@ -5,6 +5,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-ingorgo'; const out = process.env.OUT ?? '/tmp/regolo-ingorgo';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -12,7 +13,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } }); const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); }); page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
await page.goto(`http://127.0.0.1:${server.port}/`); await page.goto(`http://127.0.0.1:${server.port}/`);
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-micro'; const out = process.env.OUT ?? '/tmp/regolo-micro';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -12,7 +13,7 @@ let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
for (const [name, mobile, viewport] of [ for (const [name, mobile, viewport] of [
['desktop', false, { width: 1280, height: 860 }], ['desktop', false, { width: 1280, height: 860 }],
['touch', true, { width: 844, height: 390 }], ['touch', true, { width: 844, height: 390 }],
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-risveglio'; const out = process.env.OUT ?? '/tmp/regolo-risveglio';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
// Sul computer la versione con la sfocatura vera, sul telefono (touch) quella leggera senza filtri. // Sul computer la versione con la sfocatura vera, sul telefono (touch) quella leggera senza filtri.
for (const mobile of [false, true]) { for (const mobile of [false, true]) {
const page = await browser.newPage(mobile ? { viewport: { width: 844, height: 390 }, hasTouch: true, isMobile: true } : { viewport: { width: 1280, height: 860 } }); const page = await browser.newPage(mobile ? { viewport: { width: 844, height: 390 }, hasTouch: true, isMobile: true } : { viewport: { width: 1280, height: 860 } });
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true }); const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
const server = await staticServer(), errors = []; const server = await staticServer(), errors = [];
@@ -44,7 +45,7 @@ async function wait(page) {
}); });
} }
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const scenarios = [ const scenarios = [
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true }, { name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } }, { name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-smarronata'; const out = process.env.OUT ?? '/tmp/regolo-smarronata';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } }); const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); }); page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
await page.goto(`http://127.0.0.1:${server.port}/`); await page.goto(`http://127.0.0.1:${server.port}/`);
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-telefono'; const out = process.env.OUT ?? '/tmp/regolo-telefono';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
@@ -11,7 +12,7 @@ const server = await staticServer();
let browser; let browser;
const errors = []; const errors = [];
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } }); const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); }); page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
await page.goto(`http://127.0.0.1:${server.port}/`); await page.goto(`http://127.0.0.1:${server.port}/`);
+2 -1
View File
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true }); const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
const server = await staticServer(), errors = []; const server = await staticServer(), errors = [];
@@ -46,7 +47,7 @@ async function wait(page) {
}); });
} }
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const scenarios = [ const scenarios = [
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true }, { name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } }, { name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
+2 -1
View File
@@ -4,13 +4,14 @@ import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs'; import { staticServer } from './helpers.mjs';
import { ARGS } from './chromium.mjs';
const out = process.env.OUT ?? '/tmp/regolo-shots'; const out = process.env.OUT ?? '/tmp/regolo-shots';
mkdirSync(out, { recursive: true }); mkdirSync(out, { recursive: true });
const server = await staticServer(); const server = await staticServer();
let browser; let browser;
try { try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); browser = await chromium.launch({ args: ARGS });
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = []; const errors = [];
page.on('pageerror', e => errors.push(e.message)); page.on('pageerror', e => errors.push(e.message));
+10 -2
View File
@@ -2,7 +2,7 @@
// e l'impronta cambia solo se cambia un ingresso. // e l'impronta cambia solo se cambia un ingresso.
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readdirSync } from 'node:fs'; import { readdirSync, readFileSync } from 'node:fs';
import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts/verify.mjs'; import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts/verify.mjs';
const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md', const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md',
@@ -10,7 +10,7 @@ const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'pack
'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/corsa.js', 'src/story/piloti.js', 'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/corsa.js', 'src/story/piloti.js',
'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png', 'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png',
'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md', 'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md',
'tests/helpers.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs', 'tests/helpers.mjs', 'tests/chromium.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs',
'scripts/verify.mjs', 'scripts/bump.sh', 'tools/slice_sprites.py', '.verify-ok.json']; 'scripts/verify.mjs', 'scripts/bump.sh', 'tools/slice_sprites.py', '.verify-ok.json'];
const suitesWith = path => Object.keys(SUITES).filter(n => inputsOf(n, FILES).includes(path)); const suitesWith = path => Object.keys(SUITES).filter(n => inputsOf(n, FILES).includes(path));
const islands = Object.keys(SUITES).filter(n => !SUITES[n].core), cores = Object.keys(SUITES).filter(n => SUITES[n].core); const islands = Object.keys(SUITES).filter(n => !SUITES[n].core), cores = Object.keys(SUITES).filter(n => SUITES[n].core);
@@ -71,3 +71,11 @@ test("l'impronta cambia solo se cambia un ingresso, e il piano dice quali", () =
assert.deepEqual(after.find(p => p.name === 'briscola').changed, ['src/engine/briscola.js']); assert.deepEqual(after.find(p => p.name === 'briscola').changed, ['src/engine/briscola.js']);
assert.notEqual(fingerprint(['a'], () => '1'), fingerprint(['a'], () => '2')); assert.notEqual(fingerprint(['a'], () => '1'), fingerprint(['a'], () => '2'));
}); });
test('la pipeline di Gitea prova le stesse prove, con lo stesso Playwright del progetto', () => {
const wf = readFileSync('.gitea/workflows/test.yml', 'utf8');
const listed = wf.match(/prova: \[([^\]]*)\]/)[1].split(',').map(s => s.trim());
assert.deepEqual([...listed].sort(), Object.keys(SUITES).sort());
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8')).packages['node_modules/playwright'].version;
assert.match(wf, new RegExp(`mcr\\.microsoft\\.com/playwright:v${lock.replace(/\./g, '\\.')}-`), `immagine di Playwright diversa da ${lock}`);
});
+1 -1
View File
@@ -3,7 +3,7 @@ log:
level: info level: info
runner: runner:
file: .runner file: .runner
capacity: 2 capacity: 6
timeout: 30m timeout: 30m
container: container:
network: "" network: ""