Il viale alberato della Provinciale e la Carza verso La Luna; la mappa si cerca scrivendo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 18:47:01 +02:00
co-authored by Claude Opus 5.5
parent d89092303a
commit 3e99fb094c
22 changed files with 155 additions and 26 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+29 -9
View File
@@ -619,6 +619,22 @@
"file": "andrea-d39b7f7e61.mp3",
"sec": 8.25
},
"andrea|I negozi sotto i palazzi, al riparo degli alberi. Con la festa domani, stasera chiudono tardi.": {
"file": "andrea-77832a5082.mp3",
"sec": 6.76
},
"andrea|Gli alberi del viale: d'estate fanno un'ombra che in piazza se la sognano.": {
"file": "andrea-e82710e398.mp3",
"sec": 3.77
},
"andrea|Arrampicarmi? Ho ventisette anni, non sette.": {
"file": "andrea-1c43e80b87.mp3",
"sec": 4.92
},
"andrea|Case coi balconi pieni di fiori. Qui il palio si sente da lontano: il giusto.": {
"file": "andrea-5bd253332d.mp3",
"sec": 6.36
},
"andrea|Più avanti c'è La Luna, e poi Campomigliaio, il mio rione: i blu.": {
"file": "andrea-a96e07a9f3.mp3",
"sec": 5.92
@@ -639,9 +655,13 @@
"file": "andrea-7883437f7d.mp3",
"sec": 3.46
},
"andrea|La Sieve. Più sassi che acqua: a farci il bagno ci si sbuccia le ginocchia.": {
"file": "andrea-c0180ddbd4.mp3",
"sec": 6.46
"andrea|La Carza, la stessa che passa sotto il ponticino per Cardetole. Più sassi che acqua.": {
"file": "andrea-6248354f0f.mp3",
"sec": 6.38
},
"andrea|Da quando scavano le gallerie dell'alta velocità, d'estate sparisce. Torna viva d'inverno.": {
"file": "andrea-41e25f6dd0.mp3",
"sec": 7.07
},
"andrea|Scendere nel greto ora? Il palio non sta mica sotto un sasso.": {
"file": "andrea-cf81a4a625.mp3",
@@ -655,13 +675,13 @@
"file": "andrea-fd634c7a9b.mp3",
"sec": 3.81
},
"andrea|Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Sieve, e sui camion della Provinciale.": {
"file": "andrea-701e3d4e55.mp3",
"sec": 7.65
"andrea|Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Carza, e sui camion della Provinciale.": {
"file": "andrea-9618e5db93.mp3",
"sec": 6.65
},
"andrea|Il muretto lungo la Sieve, tutto licheni. È più vecchio di me, e sta in piedi meglio.": {
"file": "andrea-e3d25b718c.mp3",
"sec": 6.54
"andrea|Il muretto lungo la Carza, tutto licheni. È più vecchio di me, e sta in piedi meglio.": {
"file": "andrea-dc49e0c100.mp3",
"sec": 6.08
},
"andrea|Il pallaio, dove si gioca a bocce. Qui un punto si discute più di un rigore.": {
"file": "andrea-e94382adb6.mp3",
+6
View File
@@ -103,6 +103,12 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
**ancora da collegare** (camminata, hotspot, uscite, posto sulla mappa). Andrea, 24 ottobre: ci si arriva dal bivio
della Via Provinciale (verso stazione e forno), andando a sinistra; ma prima viene **La Luna**, che Andrea deve
ancora fotografare: si collega bivio → La Luna → Campomigliaio.
- **Verso La Luna** (24 ottobre, foto di Andrea): dal bivio, a sinistra, il viale alberato della Provinciale
(`provinciale_viale`), poi la Provinciale lungo la Carza in secca con la luna (`verso_luna`; la Carza d'estate si
secca da quando hanno scavato le gallerie dell'alta velocità, d'inverno torna viva). In basso si va verso La Luna e
Campomigliaio (per ora solo battute). Da mettere: le case vecchie con la curva, il rettilineo col cartello della
rotonda, l'incrocio coi cartelli (`assets/raw/foto/luna-*.jpg`): ordine da chiedere ad Andrea.
- **Ricerca nella mappa**: casella «Cerca un posto…», filtra mentre si scrive (`matchPlace`), Invio porta al primo.
- **Scelta del rione** (24 ottobre): dopo «Nuova partita», «Per chi corri quest'anno?» fra i quattro rioni
(`story.rioni`: Piazza rossi, Pieve bianchi, Cardetole verdi, Campomigliaio blu); resta nella partita come flag
`rione:<id>`. Cambiano la riga dell'introduzione e le battute su Pieve, Cardetole, Campomigliaio e la colonna.
+2 -1
View File
@@ -52,7 +52,8 @@
<div class="buttons"><button id="restart">Ricomincia</button><button class="open-credits secondary">Crediti</button></div>
</section>
<section id="map" class="overlay" hidden aria-label="Mappa del paese">
<h2>San Piero a Sieve</h2>
<div id="map-head"><h2>San Piero a Sieve</h2><input id="map-search" type="search" placeholder="Cerca un posto…" autocomplete="off" spellcheck="false" aria-label="Cerca un posto"></div>
<p id="map-none" hidden></p>
<div id="map-scroll"><div id="map-board"><svg id="map-roads" viewBox="0 0 100 100" preserveAspectRatio="none"></svg></div></div>
<div class="buttons"><button id="close-map">Chiudi</button></div>
</section>
+8
View File
@@ -50,6 +50,14 @@ export function shuffledCredits(credits, random = Math.random) {
});
}
// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e
// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici).
const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
export function matchPlace(name, query) {
const words = plain(query).split(' ').filter(Boolean), hay = plain(name);
return words.every(w => hay.includes(w));
}
export const sayTime = text => Math.max(1.6, 0.9 + text.length * 0.055);
export function newState(story) {
+32 -2
View File
@@ -1,6 +1,6 @@
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
import { story } from './story/story.js';
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits } from './engine/game.js';
import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace } from './engine/game.js';
import { Command } from './engine/command.js';
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
@@ -143,16 +143,41 @@ function openMap() {
}));
return row;
}));
$('map-search').value = '';
filterMap();
$('map').hidden = false;
drawRoads();
// Col mouse si può scrivere subito; sul telefono la tastiera si apre solo toccando la casella.
if (matchMedia('(pointer: fine)').matches) $('map-search').focus({ preventScroll: true });
// Sul telefono la mappa scorre: si parte col posto dove si è, al centro.
board.querySelector('.here')?.scrollIntoView({ block: 'center', inline: 'center' });
}
// La ricerca: mentre si scrive restano solo i posti che corrispondono; Invio porta al primo.
function filterMap() {
const query = $('map-search').value;
let shown = 0;
for (const row of $('map-board').querySelectorAll('.map-band')) {
let inRow = 0;
for (const b of row.children) inRow += (b.hidden = !matchPlace(b.title, query)) ? 0 : 1;
row.hidden = !inRow;
shown += inRow;
}
$('map-none').hidden = shown > 0;
$('map-none').textContent = `Nessun posto con «${query.trim()}»`;
drawRoads();
}
$('map-search').addEventListener('input', filterMap);
$('map-search').addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
const first = [...$('map-board').querySelectorAll('button')].find(b => !b.hidden && !b.classList.contains('here'));
if (first) first.click();
});
// Le strade fra le schede, misurate dopo l'impaginazione (e di nuovo se la finestra cambia misura).
function drawRoads() {
const board = $('map-board'), svg = $('map-roads'), origin = board.getBoundingClientRect();
const center = id => {
const r = board.querySelector(`[data-scene="${id}"] img`)?.getBoundingClientRect();
const card = board.querySelector(`[data-scene="${id}"]`);
const r = card && !card.hidden && !card.parentElement.hidden && card.querySelector('img').getBoundingClientRect();
return r && [r.left + r.width / 2 - origin.left, r.top + r.height / 2 - origin.top];
};
svg.setAttribute('viewBox', `0 0 ${board.scrollWidth} ${board.scrollHeight}`);
@@ -383,6 +408,11 @@ function updateChoices() {
window.addEventListener('keydown', e => {
if (e.target === $('volume') || e.target === $('voice-volume')) return;
// Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude.
if (e.target === $('map-search')) {
if (e.key === 'Escape') { if (e.target.value) { e.target.value = ''; filterMap(); } else $('map').hidden = true; }
return;
}
if (debugKey(e)) return;
if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; }
if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; }
+47 -11
View File
@@ -1032,7 +1032,7 @@ export const story = {
id: 'alla_scuola', name: 'più avanti, la stradina della scuola', box: [170, 196, 80, 40], at: [208, 226], exit: { scene: 'scuola', at: [600, 320], face: 'left' },
},
{
id: 'la_luna', name: 'a sinistra, verso La Luna e Campomigliaio', box: [0, 216, 44, 56], at: [12, 248], exit: { scene: 'verso_luna', at: [150, 350], face: 'up' },
id: 'la_luna', name: 'a sinistra, verso La Luna e Campomigliaio', box: [0, 216, 44, 56], at: [12, 248], exit: { scene: 'provinciale_viale', at: [300, 350], face: 'up' },
},
{
id: 'al_forno', name: 'laggiù, verso il forno', box: [280, 196, 80, 40], at: [318, 224], exit: { scene: 'forno', at: [12, 300], face: 'right' },
@@ -1042,17 +1042,53 @@ export const story = {
},
],
},
// La Provinciale lungo la Sieve, uscendo dal paese verso La Luna (foto di Andrea, 24 ottobre).
// La Provinciale dopo il bivio della stazione, fra gli alberi, prima della Carza (foto di Andrea, 24 ottobre).
provinciale_viale: {
name: 'Via Provinciale, il viale alberato',
map: [72, 68],
background: 'assets/scenes/provinciale-viale.png',
walk: [[0, 360], [0, 330], [400, 232], [462, 232], [620, 360]],
depth: [[232, 0.4], [360, 1.7]],
entry: [300, 350],
hotspots: [
{
id: 'verso_la_sieve', name: 'avanti, verso La Luna', box: [392, 220, 80, 22], at: [435, 236], exit: { scene: 'verso_luna', at: [150, 206], face: 'down' },
},
{
id: 'negozi', name: 'negozi', box: [20, 150, 200, 95], at: [150, 330], face: 'left',
actions: {
look: [{ say: 'I negozi sotto i palazzi, al riparo degli alberi. Con la festa domani, stasera chiudono tardi.' }],
},
},
{
id: 'alberi', name: 'alberi del viale', box: [230, 20, 160, 200], at: [330, 290], face: 'up',
actions: {
look: [{ say: 'Gli alberi del viale: d\'estate fanno un\'ombra che in piazza se la sognano.' }],
use: [{ say: 'Arrampicarmi? Ho ventisette anni, non sette.' }],
},
},
{
id: 'villette', name: 'case col balcone', box: [500, 0, 140, 230], at: [540, 330], face: 'right',
actions: {
look: [{ say: 'Case coi balconi pieni di fiori. Qui il palio si sente da lontano: il giusto.' }],
},
},
{
id: 'al_bivio', name: 'indietro, verso il bivio', box: [0, 334, 620, 26], at: [300, 354], exit: { scene: 'bivio_provinciale', at: [12, 248], face: 'right' },
},
],
},
// La Provinciale lungo la Carza, uscendo dal paese verso La Luna (foto di Andrea, 24 ottobre).
verso_luna: {
name: 'La Provinciale lungo la Sieve',
map: [70, 70],
name: 'La Provinciale lungo la Carza',
map: [66, 73],
background: 'assets/scenes/verso-luna.png',
walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]],
depth: [[192, 0.45], [360, 1.7]],
entry: [150, 350],
entry: [150, 206],
hotspots: [
{
id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 186, 172, 26], at: [110, 200], face: 'up', default: 'walk',
id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 334, 330, 26], at: [150, 352], face: 'down', default: 'walk',
actions: {
walk: [{
if: 'rione:campomigliaio',
@@ -1063,9 +1099,9 @@ export const story = {
},
},
{
id: 'sieve', name: 'la Sieve', box: [340, 205, 300, 155], at: [250, 300], face: 'right',
id: 'carza', name: 'la Carza', box: [340, 205, 300, 155], at: [250, 300], face: 'right',
actions: {
look: [{ say: 'La Sieve. Più sassi che acqua: a farci il bagno ci si sbuccia le ginocchia.' }],
look: [{ say: 'La Carza, la stessa che passa sotto il ponticino per Cardetole. Più sassi che acqua.' }, { say: 'Da quando scavano le gallerie dell\'alta velocità, d\'estate sparisce. Torna viva d\'inverno.' }],
use: [{ say: 'Scendere nel greto ora? Il palio non sta mica sotto un sasso.' }],
},
},
@@ -1079,17 +1115,17 @@ export const story = {
{
id: 'casa_bianca', name: 'casa bianca', box: [84, 112, 80, 64], at: [120, 220], face: 'up',
actions: {
look: [{ say: 'Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Sieve, e sui camion della Provinciale.' }],
look: [{ say: 'Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Carza, e sui camion della Provinciale.' }],
},
},
{
id: 'muretto', name: 'muretto', box: [178, 196, 120, 150], at: [190, 300], face: 'right',
actions: {
look: [{ say: 'Il muretto lungo la Sieve, tutto licheni. È più vecchio di me, e sta in piedi meglio.' }],
look: [{ say: 'Il muretto lungo la Carza, tutto licheni. È più vecchio di me, e sta in piedi meglio.' }],
},
},
{
id: 'al_bivio', name: 'indietro, verso il bivio', box: [0, 334, 330, 26], at: [150, 354], exit: { scene: 'bivio_provinciale', at: [12, 248], face: 'right' },
id: 'al_viale', name: 'indietro, verso il paese', box: [0, 182, 240, 26], at: [150, 200], exit: { scene: 'provinciale_viale', at: [435, 238], face: 'down' },
},
],
},
+7 -1
View File
@@ -145,7 +145,13 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#pause .hint { color: var(--dim); max-width: 34em; text-align: center; }
.load-error { margin: .6em 0 0; padding: .4em .8em; color: #fff; background: #7a1f1f; border-radius: 6px; font-size: max(9px, calc(var(--u) * 1.4)); }
#map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); }
#map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); }
#map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); margin: 0; }
#map-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4em 1.2em; width: 94%; margin: .3em 0; }
#map-search { font-size: clamp(8px, 1.1vw, 11px); padding: .5em .7em; width: 16em; max-width: 60%; border-radius: 6px;
border: 2px solid #5a4866; background: #231a29; color: var(--ink); }
#map-search:focus { outline: none; border-color: var(--accent); }
#map-none { margin: .6em 0 0; font-size: clamp(8px, 1.1vw, 11px); color: #b9a9c2; }
#map-board button[hidden], .map-band[hidden] { display: none; }
/* La mappa a gradini: una fascia per livello del paese, schede con miniatura e nome; si scorre se non ci sta. */
#map-scroll { width: 94%; flex: 1; min-height: 0; margin: .3em 0; overflow: auto; -webkit-overflow-scrolling: touch; border-radius: 8px;
background: radial-gradient(circle at 50% 60%, #3a2d42, #231a29); }
+12 -1
View File
@@ -237,7 +237,18 @@ try {
assert.equal(await page.locator('#map-board button[data-scene="pieve"]').count(), 0, 'la Pieve non è ancora vista');
assert.ok(await page.isVisible('#map-board button.here[data-scene="trebbio"]'));
await page.screenshot({ path: `${OUT}/07b-mappa.png` });
await page.click('#map-board button[data-scene="piazza"]');
// La ricerca: scrivendo restano solo i posti giusti; una «p» nella casella non chiude la mappa; Invio ci va.
await page.fill('#map-search', 'xyz');
assert.ok(await page.isVisible('#map-none'), 'nessun posto: lo dice');
await page.fill('#map-search', '');
await page.click('#map-search');
await page.keyboard.type('piaz');
assert.ok(await page.isVisible('#map'), 'scrivere «p» nella ricerca non chiude la mappa');
assert.ok(await page.isHidden('#map-none'));
const shown = await page.locator('#map-board button:visible').evaluateAll(bs => bs.map(b => b.dataset.scene));
assert.deepEqual(shown, ['piazza']);
await page.screenshot({ path: `${OUT}/07c-mappa-ricerca.png` });
await page.keyboard.press('Enter');
assert.ok(await page.isHidden('#map'));
assert.equal(await page.evaluate(() => game.state.scene), 'piazza');
+12 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { backgroundOrder } from '../src/engine/render.js';
import { story } from '../src/story/story.js';
import { solution } from '../src/story/solution.js';
import { Game, VERBS, checkSave, introText, shuffledCredits } from '../src/engine/game.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
import { inside } from '../src/engine/geometry.js';
import { existsSync, readFileSync } from 'node:fs';
import { settle } from './helpers.mjs';
@@ -101,6 +101,17 @@ test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', ()
assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`);
});
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
assert.ok(matchPlace('Campomigliaio', 'camp'));
assert.ok(matchPlace('Via de\' Medici', 'de medici'));
assert.ok(matchPlace('Via de\' Medici', 'MEDICI via'), 'le parole in qualsiasi ordine');
assert.ok(matchPlace('La Pieve di San Pietro', 'pieve '));
assert.ok(matchPlace('Piazza', ''), 'casella vuota: tutti');
assert.ok(!matchPlace('Piazza', 'pieve'));
const names = Object.values(story.scenes).filter(s => s.map).map(s => s.name);
assert.ok(names.filter(n => matchPlace(n, 'provinciale')).length >= 2, 'più posti sulla Provinciale');
});
test('la soluzione si percorre tutta dall\'inizio alla fine', () => {
const saves = [];
const g = new Game(story, { onSave: s => saves.push(s) });