Catégorie « Énigmes » dans le catalogue Blockly canonique : zacus_scene (écran maître title/subtitle/symbol/effect), zacus_puzzleQR et zacus_puzzleSound. L'export blocks_to_runtime3 les attache au step courant comme objets IR scene/puzzle (pas des actions), avec clamping/troncature vers les limites strictes du gateway pour que l'IR émis valide toujours. Copie hub régénérée via sync_blockly_catalog.sh. 14 tests d'export ajoutés (60 verts).
8.1 KiB
Zacus Runtime 3 Specification
Goal
Define a portable runtime artifact that can be compiled from YAML or Blockly-authored content and executed by firmware or simulated locally.
Design Principles
- Narrative canon remains in YAML during migration.
- Runtime IR is deterministic, versioned, and JSON serializable.
- Firmware consumes Runtime 3 but does not own narrative semantics.
- Import from legacy canonical YAML is allowed in a
linear_importmigration mode.
IR Shape
{
"schema_version": "zacus.runtime3.v1",
"scenario": {
"id": "ZACUS_V2",
"version": 3,
"title": "Le mystère du Professeur Zacus",
"entry_step_id": "STEP_U_SON_PROTO",
"source_kind": "yaml"
},
"steps": [
{
"id": "STEP_U_SON_PROTO",
"scene_id": "SCENE_U_SON_PROTO",
"audio_pack_id": "",
"actions": [],
"apps": [],
"transitions": []
}
],
"metadata": {
"migration_mode": "native",
"generated_by": "zacus_runtime3"
}
}
Transition Model
event_type:button,serial,timer,audio_done,unlock,espnow,actionevent_name: opaque token preserved as runtime contracttarget_step_id: requiredpriority: integer, lower firstafter_ms: only meaningful for timer-style transitions
Migration Modes
native: authored directly for Runtime 3 with explicit transitionslinear_import: derived from the legacy canonical YAML using ordered steps when no explicit graph exists
Execution Model
- Runtime loads one IR document.
- Entry step is resolved from
scenario.entry_step_id. - A simulator can replay deterministic transition events without hardware.
- Firmware adapters may enrich steps with board-specific actions, but not mutate story flow semantics.
Action Shape
Actions are emitted by authoring tools (blocks studio, firmware import, hand-written YAML) and consumed by runtime adapters. Two equivalent shapes are accepted:
- String form (legacy firmware import):
"led_pattern:rainbow"— opaque, runtime parses at its discretion. - Dict form (preferred for new authoring):
{ "kind": "<name>", ...payload }.
Standard action kinds
kind |
Payload | Semantics |
|---|---|---|
tts_say |
text: string |
Speak text via the active TTS backend |
wait_user_voice |
timeout_ms: int |
Block until utterance end or timeout |
hw_servo |
channel: int, angle: int |
Set servo on channel to angle degrees |
qr_expect |
value: string |
Arm the QR matcher with expected payload |
led_pattern |
pattern: string |
Play a named LED pattern |
sound_play |
asset: string |
Play an asset from the media manager |
score_add |
delta: int |
Mutate the run-scoped score variable |
set_var |
name: string, value: string |
Set a scenario variable |
condition |
expr: string, then: [action,…], else: [action,…] |
Standalone branching. Evaluate expr; execute the then action list if truthy, else otherwise. Nested condition actions are allowed. |
condition action
The condition action keeps branching local to the current step — no extra transitions or step duplication needed for simple if/else logic. The runtime evaluates expr against the current variable scope (left intentionally loose; runtimes that don't implement evaluation MAY treat unknown expr as falsy and log it).
Authoring tools that produce condition actions (e.g. BlockKind.logicIf in the Blocks Studio) MUST place all branch-local actions inside then / else. Use a transition (event_type action, event_name goto:<target>) when you need to jump to another step, not a condition.
Example:
{
"kind": "condition",
"expr": "score >= 3",
"then": [
{ "kind": "tts_say", "text": "Bravo, niveau suivant." },
{ "kind": "set_var", "name": "unlocked_act2", "value": "true" }
],
"else": [
{ "kind": "tts_say", "text": "Encore un effort." }
]
}
Step extras: puzzle and scene (master local puzzles + display)
Steps MAY carry two optional objects, passed through verbatim by the
compiler and consumed by the ESP32 master firmware
(ESP32_ZACUS/idf_zacus, contract mirrored in
components/game_endpoint/include/puzzle_binding.h). Validation is
STRICT at the gateway/compiler; the firmware re-validates leniently.
puzzle — local puzzle arming (P1 sound / P3 QR)
Armed when the master enters the step (POST /game/step). On solve, the
fragment digits are reported into the master's assembled code.
{ "id": 3, "type": "qr",
"codes": ["zacus-qr-1", "zacus-qr-2"],
"fragment": [5] }
{ "id": 1, "type": "sound",
"melody": [60, 62, 64, 65], "tolerance": 1,
"fragment": [1, 2] }
id: int 1..8 (puzzle slot in the master'spuzzle_state).type:qr(camera + ordered scan) orsound(mic + melody).codes(qr): 1..16 strings, each < 32 chars, scanned in order.melody(sound): 1..32 MIDI notes 0..127;tolerance: int >= 0 semitones (default 1).fragment: 1..4 decimal digits (ints 0..9) contributed to the code.
scene — master display metadata
Drives the master's on-device scene view (title/subtitle/symbol + effect). All fields optional.
{ "title": "MISSION QR", "subtitle": "scannez dans l'ordre",
"symbol": "RUN", "effect": "gyro" }
title<= 47 chars,subtitle<= 63,symbol<= 15.effect:pulse(default) |glitch|gyro|none.
État de l'authoring et chaînon manquant
Au stade actuel, les champs puzzle/scene sont portés de bout en bout
côté exécution : le compilateur (runtime3_common.py) les transmet
verbatim de l'IR au firmware, qui arme les énigmes locales (P1 son / P3 QR)
et pilote l'écran. Un IR de référence exerçant ces champs est fourni :
hardware/firmware/data/story/runtime3/EXAMPLE_LOCAL_PUZZLES.json (validé
par validate_runtime3_document, testé en bout-à-bout via POST /game/scenario
puis POST /game/step).
Le pont authoring → firmware est désormais construit. Le compilateur
traduit le vocabulaire d'authoring des puzzles: (blocs solution:) vers les
objets puzzle de l'IR et les attache au step correspondant (par
screen_scene_id == puzzle.scene). Fonctions : translate_solution_to_puzzle
et _attach_puzzles_from_authoring dans runtime3_common.py.
Table de correspondance solution: → puzzle: :
Authoring (type + solution) |
IR puzzle |
|---|---|
qr + qr_payload: "X" |
type:qr, codes:["X"] |
qr + qr_sequence: [a,b] |
type:qr, codes:[a,b] (ordre conservé) |
audio_frequency + frequency_hz (+ tolerance_hz) |
type:sound, melody:[hz→MIDI], tolerance (≈ semitons, ≥1) |
sound/sequence + solution.notes: [MIDI…] |
type:sound, melody:[…] |
Règles : le slot firmware id vient de puzzle_id sinon d'un défaut par type
(sound→1, sequence→2, qr→3) ; le fragment (1–4 chiffres) vient de
code_fragment sinon vaut le chiffre de l'id. Une énigme non traduisible
(ex. sequence de lettres sans notes MIDI — une suite de lettres n'est pas
une séquence de hauteurs détectable) est ignorée avec un avertissement, sans
casser la compilation du scénario. Un step portant déjà un puzzle explicite
n'est pas écrasé (le passthrough direct prime).
Côté atelier Blockly, la catégorie « Énigmes » expose trois blocs step-level
(catalogue frontend-v3/packages/shared/blockly/zacus_blocks.mjs, export
tools/zacus-gateway/blocks_to_runtime3.py) : zacus_scene
(title/subtitle/symbol/effect → objet scene), zacus_puzzleQR (slot, codes
ordonnés séparés par virgules, fragment) et zacus_puzzleSound (slot, mélodie
MIDI séparée par virgules, tolérance, fragment) → objet puzzle. Ces blocs
n'émettent pas d'actions : ils s'attachent au step courant (un de chaque par
step, le dernier gagne avec avertissement) ; l'export tronque/borne les valeurs
hors limites (avec avertissement) pour que l'IR émis passe toujours la
validation stricte. Tests : tests/runtime3/test_blocks_export_puzzle_scene.py.
EXAMPLE_LOCAL_PUZZLES.json reste l'IR de référence le plus complet
(puzzle + scene par step).