Skip to content

PyQuest — mini-giochi a griglia con Python (motore, renderer, mondo pilota, editor) - #15

Merged
marcofarina merged 12 commits into
mainfrom
feat/pyquest
Jul 29, 2026
Merged

PyQuest — mini-giochi a griglia con Python (motore, renderer, mondo pilota, editor)#15
marcofarina merged 12 commits into
mainfrom
feat/pyquest

Conversation

@marcofarina

Copy link
Copy Markdown
Owner

Chiude l'implementazione di PyQuest (spec esecutiva, fasi 1–5 / step 1–21). Milestone PM m8.

Cos'è

Componente riutilizzabile per mini-giochi a griglia: lo studente scrive Python vero, eseguito in-pagina da Brython, che muove Byte il droide su una griglia, combatte nemici e raccoglie risorse. L'esecuzione produce una trace di eventi che React anima passo per passo, con player a velocità regolabile.

  • Galleria su /pyquest (voce in navbar) con sblocco sequenziale, stelle e record per livello.
  • Mondo pilota mondo-01: cinque livelli a curva didattica — sequenza → for → sensori e whileattack() → combinato (melee + risorsa + vittoria multipla).
  • Editor livelli su /level-editor: pubblico, non linkato e noindex, con anteprima live e import/export JSON.
  • In MDX: <PyQuest world="…" level="…" />, registrato tra i componenti globali.

Architettura (decisioni della spec)

  • Motore trace-based (static/bry-libs/pyquest.py): il codice studente gira davvero, per intero e sincrono, contro un modello logico in puro Python; ogni azione emette subito un evento. I sensori funzionano perché leggono lo stato aggiornato; seek, step-back e replay sono gratis perché la scena è derivata dalla trace.
  • Dispatch incrementale, non trace serializzata a fine esecuzione: se il codice dello studente solleva un'eccezione, React ha già la trace parziale e la anima fino al punto dell'errore.
  • Livello iniettato come preCode (nessun ramo template in brython_runner.py), payload eventi come stringa JSON, onCustom aggiunto a bryBridge (~6 righe, retrocompatibile).
  • Rendering DOM+CSS dietro un confine renderer-agnostico: GameScene consuma lo SceneState derivato, con identità DOM stabile (nemici morti e risorse raccolte non vengono smontati).
  • Validazione build-time dei mondi nel plugin: un livello malformato fa fallire la build, mai lo studente.
  • Progressione in localStorage (pdb:pyquest:progress, versionata, parse difensivo); sblocco calcolato, mai salvato.

Verifica

npm run typecheck && npm run lint && npm run build verdi. Checklist manuale completa nel browser (light e dark, desktop e viewport stretto):

  • le cinque solution vincono in esattamente par azioni (6/7/7/5/6), verificate anche con un harness CPython; nessuno starterCode vince per sbaglio;
  • vittoria con stelle e record, morte dell'eroe (UI di sconfitta, non pannello d'errore), step limit, precedenza won > failed > error;
  • errore runtime: trace parziale animata + traceback con la riga giusta; print() interleaved allo step corretto;
  • seek, step-back, velocità; hint progressivi; sblocco e persistenza dopo reload;
  • due istanze dello stesso livello sulla stessa pagina restano indipendenti; id ignoto → pannello esplicito;
  • roundtrip dell'editor: import di un livello reale → export identico all'originale;
  • griglia che riscala sotto i 320px senza perdere colonne.

L'ultimo commit corregge sei difetti emersi proprio da questa checklist: 404 del bottone «Livello successivo» (baseUrl), contrasto muro/pavimento in dark a 1,3:1, traceback duplicato nel pannello d'errore, accenti mancanti nei commenti degli starterCode, target touch sotto i 44px e solution spedita nel bundle di ogni pagina.

Note

  • Rischio residuo accettato e documentato: while True: pass senza chiamate API blocca il main thread, come oggi in PyRunner. Lo spike su sys.settrace (step 6) ha mostrato che in Brython 3.12 il codice a livello modulo — cioè quello dello studente — non genera line events, quindi la guardia non vedrebbe il caso canonico. Motivazione completa in testa a pyquest.py. I loop che chiamano qualcosa restano coperti da step limit e contatore sensori.
  • Rimossi static/bry-libs/grid.py e game.py (superseded) e le tre regex orfane in brython_runner.py.
  • Fuori scope: l'embed in una lezione reale del Volume 1 — quando avverrà, seguire lo skill /nuovo-capitolo.
  • Non verificato: emulazione device reale con touch (in questa sessione il resize della finestra non si rifletteva nel viewport); la scala della griglia è stata comunque provata a viewport stretto.

🤖 Generated with Claude Code

https://claude.ai/code/session_015uYRFDbz7dZiMnZ9GeUXWj

marcofarina and others added 12 commits July 2, 2026 09:03
Fase 1 (M1), step 1-4 della spec PyQuest.

- config.py: espone Config.NODE_ID (node_id grezzo) in set_id(), così
  pyquest.py può fare notify() sullo stesso div che bryBridge ascolta.
- bryBridge.ts: RunOptions.onCustom + BryNotifyDetail.type ora string; nel
  switch un ramo default inoltra gli eventi fuori protocollo (D3).
  Retrocompatibile: PyRunner/SQLRunner invariati.
- pyquest.py: modello logico puro (World/Hero), _load_level che riassegna il
  global _world (no contaminazione tra run del modulo cacheato), move/turn,
  sensori is_blocked/on_goal, action_guard + StepLimitError, contatore sensori,
  win spec «reach». Emissione eventi come payload JSON (D4).
- types.ts: tipi normativi della trace (GameEvent/SceneState/LogLine) + schema
  dati LevelDef/WorldDef.
- runLevel.ts: compone il preCode (from pyquest import * + _load_level + shadow
  di input) con JSON compatto spogliato dei campi testuali (D2/D7); riparsa gli
  eventi via onCustom.

Logica del motore validata con CPython (stub di config/py_back_trace):
move/turn/sensori, vittoria reach, no-op post-vittoria, assenza di
contaminazione tra run, step_limit. Bridge Brython/DOM riusa l'infrastruttura
PyRunner esistente.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pv1PzGn4LtwKFku39y15V4
Fase 1 (M1), step 5 della spec PyQuest.

- index.tsx: prima versione debug del componente PyQuest — editor riusato da
  PyRunner + bottone Esegui + dump grezzo degli eventi e della console. Contatore
  di modulo per il suffisso del codeId (D12), così due istanze sulla stessa
  pagina restano indipendenti. Verrà riscritto allo step 12 (renderer + player).
- pyquest-test.mdx: pagina unlisted /docs/pyquest-test con due harness e la
  checklist di verifica manuale (movimento, indipendenza, errore, vittoria).
- MDXComponents.tsx: registra <PyQuest> tra i componenti MDX globali.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pv1PzGn4LtwKFku39y15V4
Test nel browser (Brython 3.12): settrace emette line events solo nei
frame di funzione; il codice a livello modulo/exec (dove gira il codice
studente) non genera line events, frame.f_trace è inefficace e il raise
dal tracer non è contenibile in modo affidabile. Decisione documentata
in testa a pyquest.py: rischio residuo `while True: pass` accettato,
come oggi in PyRunner. Pagina spike temporanea eliminata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
…ittoria

Review fase 1: a mondo congelato (dopo win) move/turn_* ritornavano prima di
_action_guard(), quindi 'while True: move()' dopo la vittoria ciclava su no-op
senza far avanzare nessun contatore, bloccando il main thread. Ora la guardia
scatta sempre: le azioni post-win restano no-op ma costano un passo e il loop
muore con StepLimitError (trace: win seguito da step_limit).

Minore: makeCodeId con hash base36 a larghezza fissa (padStart 7) per rendere
impossibile la collisione teorica della concatenazione hash+contatore.

Verificato con test CPython (19/19, stub config/py_back_trace) e live nel
browser: scenari a/b/c dello step 5, loop post-win che termina con messaggio
didattico, indipendenza tra istanze, smoke PyRunner senza regressioni.
typecheck/lint/build verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
Plugin Docusaurus `pyquest` che carica e valida a build-time i mondi in
static/pyquest/<world>/world.json (schema versionato, guardia quoting
sui campi iniettati nel motore Python). GameScene renderizza la griglia
e i personaggi, GamePlayer anima la trace step-by-step, applyEvent.ts
deriva lo stato del mondo da ogni evento. PyQuest/index.tsx orchestra
editor, esecuzione (runLevel) e animazione con la macchina a stati
idle → executing → animating → finished, esito won > failed > error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
Step 13 — motore completo (static/bry-libs/pyquest.py):
- classi Enemy/Resource; tick nemici deterministico (zero RNG): static
  attacca se adiacente, melee attacca o fa un passo greedy verso l'eroe
  (asse col delta maggiore, a parità x, poi l'altro asse, altrimenti
  fermo; niente sovrapposizioni tra nemici o con l'eroe);
- attack()/collect(); sensori enemy_ahead/on_resource/health/inventory;
  move() e is_blocked() ora trattano il nemico come blocco (bump);
- GameOverError (sollevata dopo l'evento death → la precedenza esito
  won>failed>error mostra la UI di sconfitta, non il pannello errore);
- win collect (inventory>=qty) e defeat (tutti i target, o count; guard
  len>0 anti-vittoria-fantasma);
- guardia _action_guard PRIMA del check won anche in attack/collect
  (emendamento D7).

Step 15 — HintPanel.tsx (reveal progressivo, stato in memoria) + slot
"Spiegamelo facile" cablato in index.tsx (buildExplainText +
copyToClipboard riusati da PyRunner).

Step 16 — useProgress.ts (chiave versionata pdb:pyquest:progress, parse
difensivo con reset, sync same-tab via CustomEvent + storage cross-tab
sul pattern di usePref; recordWin minimizza bestSteps, isUnlocked
calcolato; countActions = move/turn/bump/attack/collect fino al win;
starsFor D11) + pannello vittoria con stelle e "nuovo record" + striscia
"Completato · record N" persistente + stili in PyQuest.module.css.

Step 14 (scena combat) era già coperto dalla fase 2 (applyEvent +
GameScene gestivano già gli eventi combat/collect, HUD PV/inventario e
fx): nessuna modifica, solo verifica.

Dati: livello di collaudo "duello" (bug melee + gemma, win defeat+collect,
par 7) in mondo-01/world.json; tolto il fantasma decorativo dal livello 2.
Harness docs/pyquest-test.mdx aggiornato (sezione combat + note h/i/j).

Verifica: motore validato sotto CPython (trace del duello deterministica
su 3 run, win a 7 azioni; morte -> death+GameOverError; while True su
livello senza nemici -> step_limit; reach win). typecheck+lint+build
verdi. Verifica visiva nel browser (Chrome): combat, raccolta, stelle,
persistenza del record al reload, hint progressivi, dark mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
…render (review fase 3)

Review del codice della fase 3. Motore validato sotto CPython (19 check
verdi: determinismo del duello, morte, step limit post-win, guard
sensori, defeat count/kind, nessuna vittoria fantasma né sovrapposizioni
melee): nessun bug lato Python. Correzioni tutte lato UI:

- «nuovo record!» compariva a ogni pareggio: recordWin scrive il best
  PRIMA del render del pannello vittoria, quindi il confronto col best
  salvato era sempre vero eguagliando il proprio record. Ora recordWin
  decide (e restituisce) se il run è un record — prima vittoria o
  miglioramento stretto — e index.tsx lo tiene in uno stato catturato
  al momento della vittoria.
- La striscia «Completato · record N» compariva all'inizio
  dell'animazione della prima vittoria (recordWin scatta in finishRun),
  rivelando l'esito in anticipo: durante l'animazione ora è visibile
  solo se il livello era già completato all'avvio del run.
- GamePlayer: al re-run il pannello di esito restava visibile per tutto
  il replay — l'effect su playKey resettava il cursore DOPO che l'effect
  di onStepChange aveva notificato il cursore stantio del run precedente
  (già a fine trace) con la trace nuova, e il genitore chiudeva subito
  l'animazione. Reset spostato in render (pattern «adjust state during
  render»).
- Plurali al valore 1: «1 stella su 3» (aria-label) e «1 azione».
- HintPanel con key={level.id}: i suggerimenti rivelati ripartono da
  zero al cambio livello (anteprima live dell'editor, fase 4).

Verifica: typecheck+lint+build verdi. Verifica visiva nel browser sul
livello duello: prima vittoria con «nuovo record!» e striscia solo a
fine animazione; pareggio senza «nuovo record!» né pannello anticipato;
striscia e record persistenti al reload; console senza warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
…export/import (fase 4)

Nuova pagina interna /level-editor (noindex, non in navbar): disegna un livello
con palette+paint click&drag, ridimensiona la griglia, compila i metadati
(vittoria, nemici, risorse, hint, codice iniziale), lo vede girare
nell'anteprima live <PyQuest levelData={draft} /> e ne esporta/importa il JSON
da incollare in static/pyquest/<mondo>/world.json. Validazione client che
ricalca le regole del plugin build-time. Esporta FACING_DEG da GameScene e
ENEMY_KINDS/RESOURCE_KINDS da characters perché l'editor li riusa nei menu;
index.tsx ora resetta lo stato di gioco quando cambia l'identità del livello
o dello starterCode a runtime, per l'anteprima live dell'editor.

Verificato nel browser (build pulita, paint/resize/validazione, esecuzione
codice nell'anteprima con combat, round-trip export→import esatto, nessun
errore console).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9dpQX4sKY6tJkTetJz3ho
Aggiunge la pagina galleria (src/pages/pyquest/) con lucchetti sui livelli
non ancora sbloccati e stelle per livello, più /pyquest/gioca che dopo la
vittoria propone la navigazione al livello successivo tramite la nuova prop
onWin di PyQuest (callback a fine animazione, dopo il salvataggio del
progresso).

Rinomina e ribilancia i livelli del mondo pilota mondo-01 (nomi definitivi,
nuovo livello sul for/range, ordine gemma-poi-bug nel duello) e allinea la
pagina di collaudo pyquest-test.mdx alle nuove soluzioni/hint. Aggiunge la
voce di navbar «PyQuest». Rimuove game.py/grid.py (libreria canvas non più
usata) e i relativi riferimenti in brython_runner.py.
…list manuale

Trovate provando il mondo pilota end-to-end nel browser (light + dark, desktop
e viewport stretto) e ricontrollando le solution con un harness CPython.

- «Livello successivo» usava `history.push` su un path assoluto, che non
  antepone il baseUrl come fa <Link>: portava alla 404. Ora il path passa da
  useBaseUrl.
- In dark mode muro (#2b3245) e pavimento (#1e2230) stavano a 1,3:1 di
  contrasto e la mappa era di fatto illeggibile. Nuova coppia a ~3:1, muro
  chiaro su pavimento scuro (l'inverso del tema light).
- Il pannello d'errore ripeteva per intero il traceback già mostrato nella
  console del player: ora ne tiene solo l'ultima riga (tipo + messaggio) e
  rimanda alla console per il dettaglio con la riga esatta.
- Commenti degli starterCode senza accenti nel world.json («gia», «e» per «è»).
- Target del transport a 34px: su puntatore coarse salgono a 44px.
- I global data del plugin includevano `solution` di ogni livello, che finisce
  nel bundle main.js servito a ogni pagina del sito e non viene mai letta dal
  client. Ora viene spogliata. Il world.json statico resta accessibile: non è
  una protezione, è peso in meno e la risposta non arriva insieme al livello.

Aggiunte /docs/pyquest-test e /level-editor all'indice /test.

Verificato: le 5 solution vincono in esattamente `par` azioni (6/7/7/5/6) e
nessuno starterCode vince per sbaglio; vittoria con stelle e record, morte,
step limit, precedenza won > failed > error, errore runtime con trace parziale
animata, print interleaved, seek e step-back, hint progressivi, sblocco
sequenziale e persistenza dopo reload, due istanze indipendenti sulla stessa
pagina, roundtrip dell'editor identico all'originale. typecheck, lint e build
verdi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uYRFDbz7dZiMnZ9GeUXWj
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uYRFDbz7dZiMnZ9GeUXWj
@marcofarina
marcofarina merged commit 445f701 into main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant