# Pixelbook · guía para IAs

Todo lo que necesitas para crear un nivel de Pixelbook con tu persona, en un solo fichero. Vale para la consola 1.21.0. Empieza por la «Guía rápida»; lo demás se lee por secciones. Más a fondo: [agentes.md](https://pixelbook.videolibros.app/crear/agentes.md) (la API entera), [api.md](https://pixelbook.videolibros.app/crear/api.md) (el formato) y [referencia.md](https://pixelbook.videolibros.app/crear/referencia.md) (el motor, función a función).

## Guía rápida

**Pixelbook** es un videojuego educativo en pixel art para adolescentes, en español de España, gratis y en el navegador. Cada nivel es un **cartucho**: una historia corta con escenas dibujadas, voces, música y retos. La historia se para en cada reto y sigue cuando quien juega lo resuelve. Tú escribes sus ficheros (JSON y JavaScript); Pixelbook pone las voces, el sonido, los puntos y la partida. Tu persona decide de qué va, lo juega y lo envía. Pixelbook no trae una IA: la IA eres tú.

**Dos caminos:**
- **A. Puedes hacer peticiones HTTP o tienes el conector de Pixelbook.** La API, sin clave: `https://admin-api.videolibros.app/api/external/pixelbook/creator`. El MCP: `https://mcp.videolibros.app/pixelbook/mcp`. Sigue los pasos de abajo.
- **B. Solo puedes leer páginas y escribir texto.** Escribe el nivel entero como paquete (ver «El paquete») en un bloque de código y pide a tu persona que lo pegue en https://pixelbook.videolibros.app/crear/pegar/. La página hace los pasos 2 a 5 y le da el enlace para jugar; si hay errores, te los pegará. Si puedes darle ficheros, también vale un ZIP con los del nivel: lo sube en esa página.

**Los pasos** (con la API; entre paréntesis, la herramienta del MCP):
1. Pregunta a tu persona el tema (cualquiera, no solo de clase) y la idea.
2. `POST /drafts` con `{"title": "…", "idea": "…"}` (`pixelbook_empezar_nivel`). Guarda `token` (`pbd_…`: secreto, va en `Authorization: Bearer`), `draft.slug` (el id del nivel, que puede no ser el que pediste) y `draft.idPrefix`. **El borrador empieza vacío**: `template` es la plantilla, un punto de partida que no se guarda solo.
3. Escribe los ficheros partiendo de `template` y guárdalos con `PUT /cartridges/{slug}/files` (`pixelbook_guardar_ficheros`): un paquete (`Content-Type: text/plain; charset=utf-8`) o JSON, `{"files": {"ruta": "texto"}}`. Guardar fusiona por ruta y `null` borra un fichero. La respuesta trae `validation`.
4. Corrige y vuelve a guardar hasta que `validation.ok` sea `true` (`POST /cartridges/{slug}/validate`, `pixelbook_validar`).
5. `POST /cartridges/{slug}/previews` (`pixelbook_vista_previa`) y dale `preview.url` a tu persona para que juegue ya. Aún sin voces: las frases se leen en los subtítulos. `preview.missing` dice qué de lo que usa tu nivel no suena en esta vista (la receta, EN DIRECTO o los ambientes) y `preview.missingHint` lo cuenta en una frase; cuéntale solo eso: para oírlo antes de publicar, pide una prueba completa. Cada cambio, otra vista.
6. Si hace falta, `POST /cartridges/{slug}/trials` (`pixelbook_prueba_completa`): capturas, partida automática y un enlace que ya suena con la receta, EN DIRECTO y los ambientes (las voces, aún estimadas). Puede tardar en empezar: mientras, sigue con vistas previas.
7. Solo cuando tu persona lo pida: `POST /cartridges/{slug}/versions` (`pixelbook_enviar`) y dale `confirm.url`. Lo abre, entra con su cuenta de Pixelbook y confirma. Nadie más puede confirmar, tampoco tú.

**Las seis reglas que más errores evitan:**
1. **Los ids.** El `id` de `cart.json` es el `slug`; los cromos y los logros de `nivel.json` son `vdc-…`, las preguntas `vdc1`, `vdc2`…, y los logros de `cart.json`, `vdcAlgo` (con tu prefijo). La plantilla ya los trae así.
2. **El texto.** Nunca la raya larga ni comillas tipográficas, tampoco en los comentarios del código: cita con «» y escribe el apóstrofo recto ('). Sin enlaces, correos, teléfonos ni @usuarios, y nunca un libro de texto.
3. **Cada reto, completo.** Cada `{ "gate": "id" }` del guion tiene su `defInteraction('id', …)` y su lista en `soluciones.json`, y cada escena del guion, su `defScene`.
4. **Todo lo que suena, declarado** en `audio` de `cart.json`. El kit de retos pide los efectos `correct` y `no` (`orderIX`, además `tap`, `pop` y `save`) y, en `clips`, `r-first`, `r-ok1` y `r-ok2`.
5. **La XP.** `xpBudget` cubre la XP que dan todos los retos a la primera. Lo que el código pida de más, la consola lo rechaza y la prueba completa falla.
6. **Solo lo que existe.** Nombres del catálogo (series, voces, músicas, iconos) y funciones del motor con su firma exacta. Nada de `setTimeout`, `fetch`, `window` ni `document`.

**El resto, por secciones**: cada fichero («Qué lleva un nivel»), dónde va cada cosa en pantalla («Maquetación»), los retos («El kit de retos»), el dibujo («Las funciones de dibujo»), los nombres («Qué se puede dibujar y oír»), la plantilla, la API, el MCP y los errores. Con el MCP, pide cada una con `pixelbook_guia` y `seccion`, y las firmas de todas las funciones, con `pixelbook_referencia`.

## Cómo trabajar con tu persona

1. **Pregunta el tema y la idea antes de empezar.** De qué va el nivel (de cualquier tema, no solo de clase) y qué le gustaría que pasara. Con eso basta: no le hagas un cuestionario. Si no lo dice, propón tú el tipo de retos.
2. **Dale siempre el enlace de la vista previa** para que juegue el nivel al momento.
3. **Pregúntale qué le parece y cambia lo que pida.** Cada cambio, otra vista previa.
4. **Envía solo cuando te lo pida.** El envío solo se completa cuando tu persona lo confirma con su cuenta de Pixelbook (si no la tiene, la crea al abrir el enlace), en el enlace que te da la API: dáselo. Nadie puede confirmar por ella, tampoco tú.
5. **No le pidas datos personales**: ni su nombre real, ni su correo, ni dónde vive, ni fotos. El nivel tampoco los lleva.
6. **El token del borrador (`pbd_…`) es secreto.** No lo publiques ni lo pongas en el nivel.

Después del envío, una persona del equipo revisa el nivel, le pone voces y sonido y, si está bien, sale en «Hechos por jugadores». Puede pedir cambios: te llegan como comentarios.

## Los dos caminos

**A. Con la API o el MCP**, como en la «Guía rápida»: empieza con `POST /drafts`, guarda los ficheros, corrige hasta que no haya errores, pide la vista previa y dale el enlace a tu persona. Los detalles, en «La API paso a paso» y «Las herramientas del MCP».

**B. Con «Pegar en Pixelbook»** (https://pixelbook.videolibros.app/crear/pegar/): la página crea el borrador, guarda el paquete que pega tu persona, le enseña los errores y le da los enlaces para jugar y enviar. Si hay errores, tu persona te pegará un texto con ellos: devuélvele solo los ficheros que cambien, en formato paquete. Si tu persona ha empezado el nivel en la página antes de hablar contigo, te dará un id y un prefijo: úsalos. Si no, elige tú un id (minúsculas, cifras y guiones, como `volcanes-de-canarias`) y un prefijo de tres letras minúsculas (como `vdc`); si están ocupados, la página le dirá qué pedirte.

**Si puedes crear ficheros**, dale en lugar del paquete un ZIP con los del nivel y sus rutas (`cart.json`, `src/00-nivel.js`…), en su raíz o dentro de una sola carpeta, y nada más. Tu persona lo sube o lo arrastra a la página, que lo abre en su navegador (quita esa carpeta y deja fuera `__MACOSX` y `.DS_Store`), se para si pasa de 60 ficheros o de 1 MB y guarda sus ficheros con `PUT …/files` en JSON: se fusionan por ruta, como los de un paquete. Las rutas las comprueba el servidor: un fichero que no es del nivel (un `LEEME.md`) es un error.

## El paquete

El nivel entero en un solo bloque de texto, sin escapar nada. El servidor lo lee siempre igual:

```text
PIXELBOOK-PAQUETE 1
=== cart.json ===
{ … }
=== script.json ===
{ … }
=== src/00-nivel.js ===
defScene(…);
=== FIN ===
```

- La primera línea, `PIXELBOOK-PAQUETE 1`, es opcional.
- Cada fichero empieza con una línea `=== <ruta> ===`. Solo cuenta si la ruta es `cart.json`, `script.json`, `nivel.json`, `soluciones.json`, `src/<nombre>.js` o `receta/<nombre>.js` (el nombre, en minúsculas, cifras, `-` y `_`, 40 como mucho). Cualquier otra línea es texto del fichero que está abierto.
- `=== FIN ===` cierra el paquete. Lo que haya antes del primer marcador y después de `FIN` se ignora: puedes escribirlo dentro de un bloque de código y con explicaciones alrededor.
- En cada fichero se quitan las líneas en blanco y las vallas de código (las líneas que empiezan por tres acentos graves) del principio y del final, los CRLF pasan a LF y se quita la marca BOM. Cada fichero acaba con un solo salto de línea.
- Si una ruta sale dos veces, vale la última y la respuesta lo avisa en `warnings`.
- Sin ningún marcador válido, el servidor responde `422 invalid_package`.
- Guardar fusiona por ruta: los ficheros que no mandes se quedan como estaban. Para corregir, manda solo los que cambian. Si cambias el nombre de un fichero, borra el viejo (`DELETE …/files/<ruta>`, o la ruta con `null` en JSON; en «Pegar en Pixelbook», con «Quitar»).

## Qué lleva un nivel

| Fichero | Qué es |
|---|---|
| `cart.json` | El manifiesto: id, versión, serie, reparto, puntos, estrellas, logros y sonidos |
| `script.json` | El guion: escenas, voces, pausas, música, efectos y retos |
| `nivel.json` | La ficha del nivel en el mapa, sus cromos, sus logros y sus preguntas del reto del día |
| `soluciones.json` | Cómo se supera cada reto: la partida automática lo juega así |
| `src/*.js` | El código: cómo se dibuja cada escena y cómo funciona cada reto |
| `receta/*.js` | Opcional: la música y los sonidos propios, hechos con el chip de sonido |

Límites: 60 ficheros y 1 MB entre todos, 256 KB por fichero y 32 KB `cart.json`, texto UTF-8. Las voces de un nivel suman como mucho 10.000 caracteres (unos diez minutos). Un buen nivel dura de 4 a 8 minutos y tiene de 4 a 8 retos.

### Los ids

Tu borrador tiene un **id** (`slug`) y un **prefijo** de tres letras (`idPrefix`). Con el id `volcanes-de-canarias` y el prefijo `vdc`:

| Qué | Regla | Ejemplo |
|---|---|---|
| `id` de `cart.json` y del nivel | El id del borrador | `volcanes-de-canarias` |
| Cromos y logros (`nivel.json`) | El prefijo, un guion y minúsculas, cifras o guiones | `vdc-magma` |
| Preguntas (`nivel.json`) | El prefijo y de 1 a 9 minúsculas o cifras | `vdc4` |
| Logros de `cart.json` (`flags`) | El prefijo, una mayúscula y solo letras | `vdcPerfecto` |

### cart.json

| Campo | Qué es |
|---|---|
| `api` | Siempre `1` |
| `id` | El id del borrador |
| `version` | `x.y.z`. Un borrador empieza en `0.1.0` (la plantilla) y cada envío lleva una versión mayor que la del anterior |
| `title` | El nombre del cartucho, para el equipo. Lo que ve quien juega es `level.title` de `nivel.json`: lo normal es que sean el mismo |
| `series`, `cast` | La serie del borrador, con sus papeles fijos (ver «Qué se puede dibujar y oír») |
| `voices` | Tus personajes, cada uno con una voz del banco: `{ "guia": "sofia", "volcan": "luca" }`. Vacío (`{}`) si solo hablan los papeles de la serie |
| `fps` | Siempre `30` |
| `boss` | `true` si es un jefe final; si no, `false` |
| `xpBudget` | El tope de XP de los retos entre todos, de 0 a 1000: ponlo igual a lo que dan todos a la primera (la plantilla, 30). La consola recorta lo que se pase y, sin presupuesto, rechaza la petición: la prueba completa falla. `expect.xpMin` de `soluciones.json` no puede pasar de aquí |
| `stars` | `{ "three": 1, "two": 4 }`: fallos como mucho para tres y para dos estrellas |
| `flags` | Los logros que el código puede activar con `X.flag` (con el prefijo) |
| `look` | `true` si dibujas a Bit como lo ha vestido quien juega |
| `audio` | `{ "music": [], "sfx": [], "amb": [] }`: los nombres que usa el nivel, del catálogo o de tu receta |

`book`, `course`, `unit`, `unitTitle`, `part` y `pages` son opcionales y solo internos: nunca se ven. No te hacen falta; si los pones, son texto, y nada de lo que ve quien juega cita un libro de texto.

**El reparto.** Los papeles fijos de la serie (`cast`) hablan con su voz. Cada personaje nuevo es un id en minúsculas, cifras y guiones (20 como mucho) que nunca es el de un papel de ninguna serie, con una voz del banco. Ninguna voz suena dos veces en un nivel, y los que hablan entre sí tienen que distinguirse bien. Una voz de otro idioma dice sus líneas en ese idioma.

### script.json

`{ "scenes": [ … ], "clips": [ … ] }`. Cada escena es `{ "id", "sec", "hud"?, "steps": [ … ] }`: `id` en minúsculas, cifras y guiones; `sec`, el nombre del capítulo; `hud`, el número del marcador de arriba (`HUD_LEVELS[n]` en el código). Cada paso es **una sola** de estas cosas:

| Paso | Qué hace |
|---|---|
| `{ "say": "v001", "who": "guia", "text": "Lo que se dice", "show"?: "Lo que se lee", "gap"?: 0.3 }` | Una voz. `say` es un id único en todo el guion; `who`, un papel de la serie o un personaje de `voices`; `text`, 400 caracteres como mucho. La historia sigue cuando termina, más `gap` segundos |
| `{ "pause": 1.5 }` | Tiempo sin voz (hasta 30 s) |
| `{ "mark": "nombre" }` | Una marca de tiempo para animar: `M('escena', 'nombre')` en el código |
| `{ "music": "nombre" }` | Cambia la música (`"stop"` la para) |
| `{ "sfx": "nombre", "at"?: 0.2, "gain"?: 0.8 }` | Un efecto (los que empiezan por `amb-` son ambientes) |
| `{ "overlap": 0.3 }` | La siguiente voz pisa a la anterior |
| `{ "gate": "id" }` | Un reto: la historia se para hasta que se resuelve. `M('escena', 'id')` es su segundo |

`clips` son las **líneas sueltas**, `{ "say", "who", "text" }`: lo que se dice al tocar (por qué una opción no vale, pistas, reacciones). No ocupan tiempo en la historia; el código las pide con `X.say(id)`. Si usas el kit de retos, declara siempre `r-first`, `r-ok1` y `r-ok2` (lo que dice al acertar).

### nivel.json

```json
{ "format": 1,
  "level": { "title": "Volcanes de Canarias", "sub": "MODO CIENCIA · Volcanes", "info": ["Descubre cómo nacieron", "las islas Canarias"], "icon": "flame", "reward": { "card": "vdc-magma" } },
  "cards": [ { "id": "vdc-magma", "name": "Magma", "sub": "Roca fundida", "who": "Roca fundida bajo la corteza", "fact": "Cuando sale a la superficie se llama lava.", "art": { "kind": "icon", "name": "flame", "ramp": "orange" } } ],
  "badges": [ { "id": "vdc-perfecto", "name": "Sin fallos", "desc": "Supera el nivel con tres estrellas.", "icon": "star", "goal": 1, "rule": { "level": "volcanes-de-canarias", "stars": 3 } } ],
  "questions": [ { "id": "vdc1", "type": "choice", "q": "¿Cómo se llama el magma que sale a la superficie?", "opts": ["Lava.", "Ceniza.", "Basalto.", "Vapor."], "a": 0, "why": "Fuera del volcán, el magma se llama lava." } ] }
```

- `level`: `title` (lo que ve quien juega en el mapa, la ficha y el envío: 240 px como mucho, mejor 140; la plantilla trae «El matraz aforado», pon el tuyo), `sub` (di de qué va, nunca un libro), `info` (dos renglones de 216 px), `icon` (de `icons`) y `reward` (un cromo de este nivel).
- `cards` (8 como mucho): `id`, `name`, `sub`, `who`, `fact` y `art`: `{ "kind": "icon", "name", "ramp"?, "bg"? }`, `{ "kind": "picto", "name", "bg"? }` o `{ "kind": "person", "base"? o "skin", "hair", "style", "clothes" }`.
- `badges` (8 como mucho): `id`, `name`, `desc`, `icon`, `goal: 1`, `rule` y `reward?`. Reglas: `{ "flag": "vdcVolcan" }`, `{ "flags": [ … ] }`, `{ "level": "<tu id>", "stars": 3 }`, `{ "level": "<tu id>", "done": true }` o `{ "level": "<tu id>", "maxMistakes": 0 }`. `level` es siempre el id de tu nivel.
- `questions` (30 como mucho) para el reto del día: `choice` (`opts` con 4 opciones y `a` de 0 a 3), `tf` (`a`: `true` o `false`), `fact` (`a`: `"hecho"` u `"opinión"`) u `order` (`opts`, de 2 a 6, ya en su orden). Todas llevan `why`, la explicación.

El validador mide cada texto con la letra de la consola: si algo no cabe, acórtalo.

### soluciones.json

```json
{ "format": 1,
  "gates": { "magma": [{ "tap": "opt", "data": 0 }], "ordena": [{ "tap": "ord", "data": 2 }, { "tap": "ord", "data": 0 }, { "tap": "ord", "data": 1 }, { "tap": "ord-ok" }] },
  "expect": { "stars": 3, "mistakes": 0, "xpMin": 60 } }
```

Cada reto del guion lleva su lista de acciones (vacía si se cierra solo): `{ "tap": zona, "data"? }`, `{ "hold": zona, "s": segundos }`, `{ "drag": [x0, y0, x1, y1] }` o `{ "drag": { "from": zona, "dx", "dy" } }`, `{ "wait": segundos }` y `{ "key": "Enter" }`. `expect` dice lo que tiene que salir al jugarlo así: `stars`, `mistakes`, `flags` y `xpMin`.

### El código (src/*.js)

Los ficheros de `src/` se juntan en orden alfabético y se ejecutan una vez al empezar el nivel.

- `defScene(id, (t, lt, sc) => { … }, { in, inDur, caps, capY, noHud })`: dibuja la escena `id` en cada fotograma. `t` es el tiempo de la historia (se para en los retos) y `lt`, el de la escena. `in` es la transición (las de `transitions`); `caps: false` quita los subtítulos y `capY` los mueve; `noHud` quita el marcador.
- `defInteraction(id, def)`: el reto `id`. Lo más fácil es el kit (ver «El kit de retos»). Un reto propio: `{ init(X), open(S, X), frame(S, X, dt), tap(S, X, zona, dato, P), drag(S, X, P), key(S, X, tecla), hits, clips }`; `S` lleva `status`, `attempts`, `wrong`, `hint` y lo que devuelva `init`, y se resuelve con `X.right(aLaPrimera)`, `X.wrong()`, `X.xp(n, 'motivo')` y `X.done()`. `X.done(s)` cierra el reto cuando acaba lo que la consola ya tenía en cola (las voces) y pasan `s` segundos: hasta entonces `status` sigue en `open`. En la escena, `ixState(id)` da su `S`.
- Hablar con la consola: `X.say(clip)`, `X.sfx(nombre)`, `X.xp(n, motivo)` (solo con un reto abierto), `X.right()`, `X.wrong()`, `X.flag(nombre)`, `X.done()`, `X.wait(s)`, `X.then(fn)`, `X.busy()`, `X.hush()`, `X.rand(k)` y `X.shuffle(lista o n, clave)` (una copia barajada, distinta en cada partida).
- El tiempo: `M('escena', 'marca')` (el segundo de una marca o de un reto), `win(t, a, d)` (de 0 a 1 entre `a` y `a + d`), `lerp`, `clamp`, `E` (curvas, como `E.out` y `E.back`) y `NOW`, que sigue corriendo con un reto abierto.
- El marcador: `HUD_LEVELS[1] = { name: 'MISIÓN 1', title: 'VOLCANES', ramp: C.orange }` y `"hud": 1` en las escenas. Su barra de XP se llena hasta `EPS.xpMax` (400 si no lo pones): `onEpisodeStart(E => { E.xpMax = 60; })`, con tu `xpBudget`.
- **Zonas táctiles**: `hit(id, x, y, w, h, { data })` al dibujar e `isOver(id)` para saber si el ratón está encima (ver «Las funciones de dibujo»). **Todo lo que se toca cambia con el ratón encima**: se levanta un poco, se aclara o lleva un borde claro. El kit ya lo hace; en una zona tuya, hazlo tú:

```js
const encima = isOver('lava');
rrect(200, 120 - (encima ? 1 : 0), 80, 30, 4, encima ? C.orange[4] : C.orange[3]);
hit('lava', 200, 120, 80, 30);
```

- **No hay**: red, almacenamiento, DOM, `setTimeout`, `setInterval`, `eval`, `Function`, `import`, `window`, `document` ni `globalThis`. El tiempo es `t`, `NOW` y `X.wait`.

### La receta (receta/*.js)

Opcional. Si la música del catálogo no basta, el nivel trae su receta: código que fabrica sonido con el chip de Pixelbook, sin grabaciones. `defSong('nombre', { bpm, bars }, (out, C) => { … })` hace una canción en bucle y `defSound('nombre', () => señal)`, un sonido suelto. Decláralos en `audio.music` y `audio.sfx` de `cart.json`. Nada de `Math.random` ni de relojes: el sonido sale igual cada vez. La plantilla no trae receta (su música, `quiz`, es del catálogo). La vista previa instantánea no la trae (`missing` lo dice): para oírla antes de publicar, pide una prueba completa, que la cocina.

## Maquetación

![La plantilla en una vista previa, después de dos fallos: el marcador, la pregunta, las opciones con dos tachadas, la pista, el retrato de quien habla, Bit, los subtítulos y el rótulo de la vista previa](https://pixelbook.videolibros.app/crear/plantilla.png)

La pantalla mide 480 × 270 píxeles: `x` va de izquierda a derecha y `y`, de arriba abajo (la captura, a ×2). Lo que pone la consola encima de tu dibujo:

| Qué | Dónde (`x`, `y`, ancho × alto) |
|---|---|
| El marcador, en las escenas con `hud` | La franja de arriba, unos 16 px: `name` y `title` desde (4, 4), 11 de alto, y la XP de `x` 338 a 455 |
| El botón de pausa | (456, 3), 20 × 18, siempre |
| Los subtítulos | El texto en `y` 237, con letra doble; su fondo oscuro, de `y` 231 a 258, centrado y de 420 de ancho como mucho |
| El rótulo de la vista previa | (333, 257), 145 × 11, y un marco rojo de 1 px alrededor |

Deja lo importante entre `x` 8 y 472 y entre `y` 24 y 228. Lo que dibujan el kit y el motor, por defecto o en la plantilla:

| Pieza | Dónde |
|---|---|
| `speakerDock(t, { allow })` | El retrato de quien habla: 68 × 68 en (6, 150), con su nombre encima (de `y` 144 a 154). Sale cuando habla alguien de `allow` (sin `allow`, solo `vega`, `galileo`, `milagros` y `daniel`; sin ficha en `PEOPLE`, con su nombre y sin retrato) |
| `drawChoice` | Una tarjeta por opción, por defecto desde (92, 70), de 296 × 30 y con 5 px entre una y otra (la plantilla, desde `y` 80). El número va a la izquierda; el texto, en uno o dos renglones |
| `drawHint` | La pista, en un panel de `x` 92 y 296 de ancho que acaba en `y` 214 y crece hacia arriba |
| `drawOrder` | Tarjetas de 20 de alto donde digas, con su número a la derecha (ver «El kit de retos») |
| `drawBins` | Cajas en fila de 52 de alto, desde `x` e `y` |
| `bit(cx, cy)` | Bit, centrado: en la plantilla, en (430, 206) |

La letra mide 7 px de alto por cada `s` (doble, 14), y un renglón, unos 10. `textWidth(texto, s)` dice lo que ocupa y `wrapText(texto, ancho)` lo parte en renglones.

## El kit de retos

Tres retos hechos, con su estado (`S`), sus zonas, su resalte con el ratón encima y sus voces. Se registran con `defInteraction(id, choiceIX({ … }))` y se dibujan en su escena con `ixState(id)`. Todos llevan en `S` `status` (`pending`, `open` o `done`), `attempts`, `wrong` y `hint`. Piden los efectos `correct` y `no` (y `orderIX`, `tap`, `pop` y `save`) y, al acertar, dicen `r-first` (a la primera), `r-ok1` o `r-ok2`.

**Cada partida es distinta.** El kit baraja solo, en cada partida de otra forma: las opciones de `choiceIX` (`S.opts`), las tarjetas de `orderIX` y las frases de `binIX` (`S.order`, y la de ahora en `S.item`). Lo que se toca es siempre el índice en tus etiquetas, nunca el puesto en pantalla, así que `soluciones.json` no cambia, salvo en `binIX` (ver más abajo). Tus voces no dicen «la primera» ni «la de arriba»: describen la respuesta. Solo se deja algo en su orden diciendo por qué, en el reto: `fixedOptions: 'motivo'` o `fixedOrder: 'motivo'` (un diálogo en el que cada turno contesta al anterior, una dificultad que sube).

**Cuándo cambia cada cosa.** El kit cambia `S` en el mismo toque que contesta, cuando ya suena `correct` o `no`. `S.status` pasa a `done` (con `S.tDone`) más tarde: cuando la consola cierra el reto, después de las voces que ya tenía en cola (la felicitación) y de la espera de `X.done`; entonces sigue la historia. Para que tu dibujo responda al toque, mira estos campos, no `status`:
- `choiceIX`. Al acertar: `S.locked = true`, `S.picked` (la buena), `S.tRight = NOW` y `S.attempts` + 1; se cierra tras la felicitación y `done` segundos (0,4). Al fallar: `S.wrong` (con la fallada), `S.lastWrong`, `S.tWrong = NOW` y `S.attempts` + 1; al segundo fallo, `S.hint = true` y `S.tHint`.
- `orderIX`. Con LISTO (o al tocar la última, con `auto`): `S.answer` (el orden) y `S.attempts` + 1. Si es el bueno, `S.locked = true` (no hay `tRight`) y se cierra tras la felicitación y 0,4 s; si no, `S.tWrong = NOW` y se vacían `S.seq` y `S.answer`. Sin `check`, `S.locked` al guardar y se cierra a los 0,3 s.
- `binIX`. Cada toque: `S.picks` (con la caja), `S.tPick = NOW`, `S.attempts` + 1 y el turno siguiente (`ixNext(S)`: `S.i` + 1 y `S.item`, la frase de ahora). Las frases salen en orden al azar (`S.order`), así que la contestada es `S.order[S.i - 1]` y su caja buena, `items[S.order[S.i - 1]]`. Tras la última, `S.locked = true`; se cierra tras la voz y 0,3 s (o `cool`, si es más).

Por ejemplo, para encender en tu dibujo la respuesta buena en cuanto se acierta (como hace `drawChoice`), con la felicitación aún sonando:

```js
const BUENA = 2;   // el índice de la buena en tus etiquetas, el mismo que correct
defInteraction('cumbre', choiceIX({ n: 4, correct: BUENA, wrong: { 0: 'x-c0', 1: 'x-c1', 3: 'x-c3' }, hint: 'h-cumbre' }));
defScene('s02-cumbre', (t, lt) => {
  const S = ixState('cumbre');
  // ... tu dibujo de las cuatro cumbres ...
  if (S && S.locked && S.picked === BUENA) ring(300, 120, 10 + R(8 * win(NOW, S.tRight, 0.3, E.out)), C.green[3], 2);
});
```

**`choiceIX`**, opción múltiple: `choiceIX({ n, correct, wrong: { 1: 'x-clip-1', 2: 'x-clip-2' }, hint: 'h-clip', xp: [30, 15, 5], label })`. Las opciones salen barajadas; con `fixedOptions: 'motivo'`, en tu orden.
- `correct` es el índice de la buena en tus etiquetas. Cada fallo dice su línea de `wrong` y deja la opción tachada; tras dos fallos dice `hint` y `S.hint` pasa a `true`.
- `xp`: la XP de acertar al primer intento, al segundo y después. `label`, el motivo de esos puntos.
- `S.wrong` es la lista de índices ya fallados; `S.opts[k]`, la opción que sale en el puesto `k`; `S.picked`, la buena al acertar.
- `drawChoice(S, etiquetas, { x, y, w, h, gap, cols, colW, t0, now })`, con las etiquetas en tu orden. Con `t0` y `now: t`, las tarjetas entran desde la derecha, una cada 0,08 s y en 0,25 s cada una. Como la historia se para en el reto, empieza `t0` al menos `0,25 + 0,08 × (n - 1)` s antes de su marca (la plantilla, 0,6 s).
- Zona `opt`, con `data` = el índice de la opción (no su puesto): `{ "tap": "opt", "data": 0 }`. Las teclas 1, 2, 3… eligen por puesto.

**`orderIX`**, ordenar: `orderIX({ n, correct: [2, 0, 1], check: true, auto: false, wrong: 'x-orden', xp: [30, 15, 5], label })`.
- Se tocan las tarjetas en orden: cada una recibe su número y tocar una numerada la quita. `correct` es el orden bueno, con los índices de tus etiquetas.
- **Pon `check: true`**: sin él no se corrige (solo guarda la respuesta, sin acierto ni XP). Con `auto: true` se corrige al tocar la última; si no, con LISTO. Un orden malo se borra entero y dice `wrong` (o `wrongFor(respuesta)`, con sus líneas en `wrongClips`). No tiene pista.
- `S.seq` son los índices tocados, en orden.
- `drawOrder(S, etiquetas, { pos: [[x, y, w?], …], letters, ok: [x, y, w, h], clear: [x, y, w, h] })`. `pos` da la esquina de arriba a la izquierda de cada tarjeta y, si quieres, su ancho (si no, el del texto, 150 como mínimo). Cada tarjeta mide 20 de alto, con su letra a la izquierda, el texto en un renglón y el número en un círculo pegado a su derecha (deja 12 px). Sepáralas 26 px: caben unas 6, aunque el código no pone tope a `n`. `ok` y `clear` son LISTO y BORRAR.
- Zonas: `ord` (`data` = el índice de la tarjeta), `ord-ok` y `ord-clear`.

**`binIX`**, clasificar: `binIX({ items: [0, 1, 1, 0], wrong: 'x-caja', xp: 20, label, quiet: true, cool: 0.6 })`.
- Las frases salen de una en una, en orden al azar, y `items[i]` es la caja buena de la frase `i`. **Las frases las dibujas tú**: la de ahora es la `S.item` de tu lista, nunca la `S.i`, que cuenta los turnos (al acabar, `S.i` vale `items.length` y `S.item`, `null`). Con `fixedOrder: 'motivo'`, en tu orden.
- Cada acierto da `xp` (así que `xpBudget` necesita `xp` por cada frase). Un fallo dice `wrong` (una línea para todas o `{ i: línea }`) y pasa a la siguiente. `quiet`: sin voz al acertar; `cool`: segundos sin admitir toques tras cada respuesta.
- `drawBins(S, [['HECHO', 'Se puede comprobar', C.teal], ['OPINIÓN', 'Un gusto', C.pink]], { x, y, w, h: 52, gap: 12, answer })`: una caja por entrada, en fila, con su texto en letra doble y su subtítulo. `answer` es la caja buena de la última frase contestada (`items[S.order[S.i - 1]]`): al acabar, esa sale en verde y la elegida, si falló, en rojo.
- Zona `bin`, con `data` = la caja. Como el orden cambia en cada partida, su solución va por frase: `{ "porItem": [[{ "tap": "bin", "data": 0 }], [{ "tap": "bin", "data": 1 }], …] }`, una lista por frase en el orden de `items`, y la partida automática toca la de la frase que sale.

**Además**: `drawHint(S, clip, x, y, w)` (la pista escrita), `button(id, x, y, w, h, texto, { ramp, icon, s, disabled, data })` (un botón con su zona y su resalte) y `stars(cx, cy, n)`.

## Las funciones de dibujo

Todo en píxeles de la pantalla. Los colores son índices de la paleta: `C.<rampa>[i]` o `K.white`, `K.ink`… Dentro de `ui(() => { … })` se dibuja sin cámara. Las opciones van en un objeto y las que no pongas toman su valor por defecto. La letra solo dibuja los caracteres que lista [referencia.md](https://pixelbook.videolibros.app/crear/referencia.md) en «La letra de la consola»; los demás salen como «?» y el validador lo avisa (`TEXT_GLYPH`), también en el código. Del teclado no tiene `` & < > \ ^ ` { | } ~ ``: para una flecha usa «→», «←» o «↓», o dibújala con `poly` y `line`.

| Función | Qué hace |
|---|---|
| `cls(c)` | Pinta la pantalla entera |
| `rect(x, y, w, h, c)`, `rrect(x, y, w, h, r, c)`, `frame(x, y, w, h, c)` | Rectángulo relleno, con las esquinas redondeadas (`r` px) y solo su borde de 1 px |
| `hline(x0, x1, y, c)`, `vline(x, y0, y1, c)`, `line(x0, y0, x1, y1, c)` | Líneas de 1 px |
| `circ(cx, cy, r, c)`, `ring(cx, cy, r, c, grosor)`, `ellipse(cx, cy, rx, ry, c)` | Círculo y elipse rellenos, por su centro, y aro |
| `poly([[x, y], [x, y], …], c)` | Polígono relleno: una lista de puntos `[x, y]` |
| `vgrad(x, y, w, h, [c0, c1, …])` | Degradado vertical tramado, de arriba abajo |
| `panel(x, y, w, h, c, { r, ramp, border, shadow })` | Panel de interfaz, con borde y sombra |
| `tag(texto, x, y, c, { s, align })` | Etiqueta de 11 de alto: `y` es su borde de arriba y `x`, el izquierdo (con `align` `'center'` o `'right'`, su centro o su borde derecho). Devuelve su ancho |
| `bubble(x, y, w, h, tx, ty, { fill })` | Bocadillo con la cola hacia (`tx`, `ty`); el texto, aparte |
| `bar(x, y, w, h, k, c, { bg, hi })` | Barra de progreso, con `k` de 0 a 1 |
| `drawText(texto, x, y, { s, c, align, ol })` | `y` es el techo de las mayúsculas, `s` el tamaño y `ol` el color del contorno. Devuelve su ancho |
| `slamText(texto, cx, cy, lt, { s, c })` | Texto que entra de golpe, centrado; `lt`, los segundos desde que sale |
| `icon(nombre, cx, cy, tam, rampa)` | Un icono, centrado, de `tam` px (`rampa`, como `C.teal`) |
| `picto(nombre, cx, cy, { s })` | Un pictograma, centrado, a escala `s` |
| `bust(x, y, FICHA, { t, who, expr })` | Un busto en una caja de 64 × 64 desde (`x`, `y`); con `who`, mueve la boca cuando habla |
| `bit(cx, cy, { t, expr, color, acc })` | Bit, centrado |
| `confetti(t, t0, { x, y, n, dur })` | Confeti que cae desde (`x`, `y`) a partir de `t0`, durante `dur` s (3,5) |
| `sparkle(x, y, t, c, { r })` | Un destello de cuatro puntas que late |

`isOver(clave)` dice si el ratón estaba encima de esa zona en el fotograma anterior: la consola lo calcula con las zonas que dibujaste en él. Dibuja cada zona en cada fotograma; si lleva `data`, dale su clave (`HITS[HITS.length - 1].key = id + ':' + data`) y pregunta por ella (`isOver('lava:2')`).

## Qué se puede dibujar y oír

Solo valen los nombres de esta lista: el validador rechaza los demás. La lista completa, con las fichas de las voces, está en [catalogo.json](https://pixelbook.videolibros.app/crear/catalogo.json).

### Áreas, series y papeles

Un borrador es de un área y de una serie (`cast`). Sin área es `libre`: de cualquier tema, sale en «Hechos por jugadores» con un estilo neutro y, si no dices la serie, va con `modo-ciencia`. Con un área de esta tabla, si no dices la serie, va la del área. Los papeles fijos de la serie hablan con su voz; tus personajes, con las del banco.

| Área (`area`) | Serie (`cast`) | Papeles fijos (`who`) |
|---|---|---|
| `ciencias` (Ciencias) | `modo-ciencia` (MODO CIENCIA) | `vega` (Vega), `bit` (Bit), `galileo` (Galileo Galilei), `milagros` (Milagros), `daniel` (Daniel), `megauv` (MEGA UV) |
| `geografia` (Geografía e Historia) | `en-directo` (EN DIRECTO) | `alba` (Alba), `nora` (Nora), `carmen` (Carmen), `luis` (Luis), `ivan` (Iván), `pedro` (Pedro), `lucia` (Lucía) |
| `musica` (Música) | `remix` (REMIX) | `lia` (Lía), `rec` (REC) |
| `lengua` (Lengua castellana) | `codigo-n` (CÓDIGO Ñ) | `dario` (Darío), `tomo` (Tomo) |
| `ingles` (Inglés) | `road-trip` (ROAD TRIP) |  |
| `literatura` (Literatura) | `clasicos` (CLÁSICOS) | `narrador` (Narrador), `narradora` (Narradora), `chico` (Chico joven), `chica` (Chica joven), `hombre` (Hombre adulto), `mujer` (Mujer adulta), `anciano` (Hombre mayor), `anciana` (Mujer mayor), `nino` (Niño), `nina` (Niña) |

En `clasicos` (Literatura) no hay presentadores: una voz que narra y los personajes del libro, cada uno con su voz. Cómo adaptar un clásico: [clasicos.md](https://pixelbook.videolibros.app/crear/clasicos.md).

### Dibujo

- **La pantalla**: 480 × 270 píxeles, con una paleta cerrada (ver «Maquetación»).
- **Rampas de color** (`C.<rampa>[i]`, de oscuro a claro; usa solo los índices de su rampa): de 3 tonos (`[0]` a `[2]`), `tvCyan`, `lluvia`; de 4 tonos (`[0]` a `[3]`), `ink`, `hairR`, `hairD`, `sky`, `marble`, `tvBlue`, `tvLand`, `papel`, `hilo`, `mapaViejo`, `sepia`, `indigo`, `terracota`, `acuarela`, `oliva`; de 5 tonos (`[0]` a `[4]`), `red`, `orange`, `yellow`, `lime`, `green`, `teal`, `blue`, `purple`, `pink`, `skinA`, `skinB`, `gold`, `tvNavy`, `tvRed`, `tvGold`, `chrome`, `corcho`, `cuero`, `mesa`, `oro`; de 6 tonos (`[0]` a `[5]`), `gray`, `warmgray`, `wood`, `noche`, `ambar`, `perg`.
- **Atajos** (`K.<nombre>`): `black`, `ink`, `dark`, `white`, `paper`, `hl`, `phase`, `who`.
- **Iconos** (`icon` en el código; `icon` de la ficha y de los logros; cromos `icon`): `trophy`, `eye`, `chart`, `flask`, `bulb`, `heart`, `stopwatch`, `book`, `check`, `cross`, `star`, `sun`, `lock`, `magnifier`, `floppy`, `warning`, `loop`, `question`, `note`, `thermo`, `gear`, `microscope`, `ruler`, `pendulum`, `megaphone`, `flame`, `chest`, `shield`, `crown`, `bolt`, `cards`, `pause`, `speaker`, `telescope`, `robot`.
- **Pictogramas** (`picto` en el código; cromos `picto`): `factory`, `truck`, `shop`, `person`, `bag`, `cart`, `laptop`, `flower`, `doctor`, `bus`, `tractor`, `cow`, `boat`, `tree`, `mine`, `power`, `crane`, `hospital`, `school`, `plane`, `sun`, `coin`, `coins`, `bread`, `phone`, `scissors`, `padlock`, `shield`, `bug`, `house`, `state`, `briefcase`, `gear`, `chart`, `card`, `video`, `wifi`, `cheese`, `hotel`, `pension`, `road`, `train`, `trash`, `jobless`, `drop`, `oven`, `solar`, `oil`, `mineral`, `bank`, `robot`, `flask`, `bulb`, `data`, `ai`, `ship`, `anchor`, `pin`, `contacts`, `mic`, `camera`, `flashlight`, `puzzle`, `student`, `handshake`, `contract`, `strike`, `clock`, `law`, `calc`, `worker`, `sack`.
- **Personas del motor** (`bust` y `PEOPLE`; cromos `person` con `base`): `vega`, `galileo`, `milagros`, `daniel`, `lia`, `dario`.
- **Ciudades** (`PLACES.<ciudad>` da `[longitud, latitud]`, para mapas y globos): `nuevaYork`, `madrid`, `valencia`, `alicante`, `shanghai`, `pekin`, `tokio`, `londres`, `rotterdam`, `venecia`, `estambul`, `moscu`, `xian`, `urumqi`, `kashgar`, `yibuti`, `mombasa`, `nairobi`, `colombo`, `calcuta`, `kualaLumpur`, `yakarta`, `hanoi`, `quanzhou`, `gwadar`, `teheran`, `atenas`, `dakar`, `singapur`, `suez`, `losAngeles`, `saoPaulo`, `lagos`, `bombay`, `sidney`, `hongKong`.
- **Transiciones** (`defScene(id, dibujo, { in })`): `cut`, `dissolve`, `wipe`, `iris`, `blocks`, `glitch`, `black`, `page`, `tv`.

### Sonido

- **Músicas** (`audio.music` y `{ "music" }`). Las sintetiza la consola y suenan también en la vista previa instantánea: `cathedral`, `theme`, `outro`, `level1`, `level2`, `quiz`, `beach`, `boss`, `menu`, `victory`. Las de EN DIRECTO se hacen al cocinar (en la prueba completa y al publicar) y solo suenan en esa serie (`en-directo`): `directo-cabecera`, `directo-bed`, `directo-bed2`, `directo-encuesta`, `directo-calle`, `directo-tec`, `directo-cierre`, `directo-conexion`, `directo-pensar`, `directo-mapa`, `directo-trabajo`, `directo-mercado`, `directo-galeon`, `directo-bangalore`, `directo-cumbre`, `directo-neon`, `directo-tiempo`, `expediente-noir`, `expediente-golpe`, `rumbo-viaje`.
- **Efectos** (`audio.sfx`, `{ "sfx" }` y `X.sfx`). Suenan también en la vista: `boot`, `level-start`, `countdown5`, `countdown4`, `scan`, `buzz`, `ding`, `update`, `error`, `achievement`, `select`, `phase`, `timecard`, `idea`, `release`, `stamp`, `combo`, `xp`, `correct`, `alarm`, `item`, `timelapse`, `victory`, `save`, `swish`, `blip`, `pop`, `heart`, `type`, `tick`, `plaf`, `hit`, `drip`, `wave`, `tap`, `no`, `zap`, `star`, `levelup`, `chest`, `flame`, `fill`, `slide`, `thud`. Los de EN DIRECTO, con `d:` delante, se hacen al cocinar y solo en esa serie: `d:tv-swoosh`, `d:tv-swoosh-back`, `d:tv-ding`, `d:tv-hit`, `d:tv-alert`, `d:ticker`, `d:map-pop`, `d:sun`, `d:thunder`, `d:countdown5`, `d:correct`, `d:coin`, `d:ratchet`, `d:clunk`, `d:notif`, `d:glitch`, `d:typing`, `d:key`, `d:bell-carriage`, `d:carriage`, `d:stamp`, `d:pin`, `d:shutter`, `d:paper`, `d:spot`, `d:string`, `d:sting-noir`, `d:page`, `d:pen`, `d:ink-stamp`, `d:tape`, `d:ship-horn`, `d:compass`, `d:swish`, `d:no`.
- **Ambientes** (`audio.amb` y `{ "sfx": "amb-…" }`; se ponen al cocinar, también en la prueba completa, y no en la vista instantánea): `amb-cathedral`, `amb-beach`, `amb-nuevayork`, `amb-calle`, `amb-puerto`, `amb-panaderia`, `amb-mar`, `amb-bangalore`, `amb-shenzhen`, `amb-lluvia`.
- **El kit de retos pide efectos**: si lo usas, declara en `audio.sfx` `correct` y `no` (`choiceIX` y `binIX`), y además `tap`, `pop` y `save` (`orderIX`). Si falta uno, la consola lo rechaza y la partida automática falla.
- **Chip de las recetas** (`receta/*.js`, a 24000 muestras por segundo): Piezas del sintetizador: `midiHz`, `noteNum`, `stereo`, `addMono`, `addStereo`, `pulse`, `tri`, `sine`, `noise`, `adsr`, `expDecay`, `lowpass`, `highpass`, `biquad`, `pingPong`, `reverb`, `softClip`, `fade`, `concat`, `mixInto`. Chiptune de MODO CIENCIA: `chip.lead`, `chip.arp`, `chip.bass`, `chip.pad`, `chip.kick`, `chip.snare`, `chip.hat`, `chip.openHat`. Instrumentos de EN DIRECTO: `white`, `saw`, `fm`, `brass`, `epiano`, `fmBell`, `synthBass`, `strings`, `pluck`, `guitar`, `upright`, `marimba`, `vibes`, `glock`, `flute`, `mutedTrumpet`, `timpani`, `orchHit`, `drums.kick`, `drums.kickSoft`, `drums.snare80`, `drums.tom`, `drums.hat`, `drums.clap`, `drums.ride`, `drums.brushTap`, `drums.brushSwish`, `drums.shaker`, `drums.cajonLow`, `drums.cajonSlap`, `drums.tambourine`. Edad Media: `wobble`, `zanfona`, `zanfonaGolpe`, `flautaPico`, `laud`, `laudRasgueo`, `salterio`, `mano.dum`, `mano.tek`, `mano.ka`, `mano.pandero`, `mano.sonajas`, `mano.madera`, `nacara`, `campana`, `coro`. Estudio de hoy: `rhodes`, `subBass`, `lofi.kick`, `lofi.snare`, `lofi.rim`, `lofi.hat`, `lofi.shaker`, `lofi.reverse`, `vinilo`, `varispeed`. Ayudas para escribir canciones: `hz`, `chord`, `bus`, `clock`, `melody`, `rand`, `toMono`, `mixdown`, `defSong`, `defSound`.

### Banco de voces

El banco tiene 69 voces para tus personajes (`voices` de `cart.json`). La lista, con la ficha de cada una y una muestra que dice la misma frase, está en [voces.md](https://pixelbook.videolibros.app/crear/voces.md) (se escuchan en [voces/](https://pixelbook.videolibros.app/crear/voces/)) y, en datos, en `voces` de [catalogo.json](https://pixelbook.videolibros.app/crear/catalogo.json).

Para elegir, piensa en cada personaje (su edad aparente, su carácter, de dónde es y cómo habla) y busca la ficha que le encaje: género, edad aparente, idioma, acento, registro, timbre y usos. Ninguna voz suena dos veces en un nivel: no le des a un personaje la voz de otro ni la de un papel de tu serie que hable en el nivel (la ficha lo dice en `papeles`). Los que hablan entre sí, que se distingan bien. Por ejemplo:

- `sofia`, Sofía: voz femenina, joven y media, en es-ES con acento castellano. Para chicas jóvenes, estudiantes, narración juvenil.
- `rafael`, Rafael: voz masculina, mayor y media, en es-ES con acento castellano. Para narración épica, leyendas, ancianos.
- `maria-andaluza`, María (andaluza): voz femenina, joven y media, en es-ES con acento andaluz. Para personajes andaluces, jóvenes, vecinas.

## Reglas del contenido

Quien juega es adolescente. Una persona del equipo revisa cada nivel con estas reglas, y el validador para muchas:

- **Rigor.** Cada dato, comprobado. Nada inventado que parezca verdad.
- **Lengua.** Español de España, con todas sus tildes. Frases que se entiendan al oírlas.
- **Nunca la raya larga** (el guion largo de los diálogos): usa coma, punto, dos puntos o paréntesis. Tampoco en los comentarios del código.
- **Sin comillas tipográficas**: la letra del juego no las dibuja. Cita con «» y escribe el apóstrofo recto (').
- **Sin enlaces, correos, teléfonos ni @usuarios**, ni dominios como algo.com.
- **Nunca cites un libro de texto**: ni su nombre, ni sus páginas, ni sus figuras o actividades numeradas. Un dato se cita por su fuente original (el INE, la NASA).
- **Los ids llevan el prefijo de tu borrador** (ver «Los ids»).
- **Todo lo que se toca cambia con el ratón encima** (ver «El código»).
- **Nada de** violencia gratuita, sustancias, contenido sexual, retos peligrosos, publicidad ni datos personales.
- **Nada copiado**: ni textos, ni personajes, ni canciones con derechos de otras personas. Una melodía, propia o de dominio público.
- **Los retos.** Cada uno tiene una sola respuesta correcta y la explica. Con más de dos opciones, al fallar se explica por qué y se vuelve a intentar; tras dos fallos, una pista.

## La plantilla en paquete

Es la plantilla que te da `POST /drafts`, para un borrador con el id `volcanes-de-canarias`, el prefijo `vdc` y la serie `modo-ciencia`: un nivel mínimo, con una pregunta, que pasa el validador tal cual. No se guarda sola: cambia el id y el prefijo por los de tu borrador, escribe tu nivel encima y guárdalo. Los mismos ficheros, sueltos, están en [plantilla/](https://pixelbook.videolibros.app/crear/plantilla/).

```text
PIXELBOOK-PAQUETE 1
=== cart.json ===
{
 "api": 1,
 "id": "volcanes-de-canarias",
 "version": "0.1.0",
 "series": "modo-ciencia",
 "cast": "modo-ciencia",
 "title": "Volcanes de Canarias",
 "fps": 30,
 "boss": false,
 "xpBudget": 30,
 "stars": {
  "three": 1,
  "two": 4
 },
 "flags": [],
 "look": true,
 "audio": {
  "music": [
   "quiz"
  ],
  "sfx": [
   "correct",
   "no"
  ],
  "amb": []
 },
 "voices": {
  "presentadora": "sofia",
  "ayudante": "luca"
 }
}
=== script.json ===
{
 "scenes": [
  {
   "id": "s00-pregunta",
   "sec": "Reto",
   "hud": 1,
   "steps": [
    {
     "music": "quiz"
    },
    {
     "say": "v001",
     "who": "presentadora",
     "text": "¿Qué instrumento mide volúmenes exactos gracias a una marca llamada aforo?"
    },
    {
     "gate": "aforo"
    },
    {
     "say": "v002",
     "who": "presentadora",
     "text": "Eso es: el matraz aforado.",
     "gap": 0.4
    }
   ]
  }
 ],
 "clips": [
  {
   "say": "x-aforo-1",
   "who": "ayudante",
   "text": "La probeta mide volúmenes, pero no tiene aforo: tiene muchas divisiones."
  },
  {
   "say": "x-aforo-2",
   "who": "ayudante",
   "text": "El vaso de precipitado solo da volúmenes aproximados."
  },
  {
   "say": "h-aforo",
   "who": "ayudante",
   "text": "Pista: busca el que tiene una sola marca en el cuello."
  },
  {
   "say": "r-first",
   "who": "ayudante",
   "text": "¡A la primera!"
  },
  {
   "say": "r-ok1",
   "who": "ayudante",
   "text": "¡Correcto!"
  },
  {
   "say": "r-ok2",
   "who": "ayudante",
   "text": "¡Bien visto!"
  }
 ]
}
=== nivel.json ===
{
 "format": 1,
 "level": {
  "title": "El matraz aforado",
  "sub": "Plantilla · Medir con precisión",
  "info": [
   "Elige el instrumento que mide",
   "volúmenes exactos"
  ],
  "icon": "flask",
  "reward": {
   "card": "vdc-matraz"
  }
 },
 "cards": [
  {
   "id": "vdc-matraz",
   "name": "Matraz aforado",
   "sub": "Material de laboratorio",
   "who": "Recipiente con una sola marca",
   "fact": "Su marca, el aforo, indica un volumen exacto. Se usa para preparar disoluciones.",
   "art": {
    "kind": "icon",
    "name": "flask",
    "ramp": "teal"
   }
  }
 ],
 "badges": [
  {
   "id": "vdc-aforo",
   "name": "Ojo de aforo",
   "desc": "Supera el nivel con tres estrellas.",
   "icon": "check",
   "goal": 1,
   "rule": {
    "level": "volcanes-de-canarias",
    "stars": 3
   }
  }
 ],
 "questions": [
  {
   "id": "vdc1",
   "type": "choice",
   "q": "¿Qué instrumento mide volúmenes exactos gracias a su aforo?",
   "opts": [
    "El matraz aforado.",
    "La probeta.",
    "El vaso de precipitado.",
    "El embudo."
   ],
   "a": 0,
   "why": "El matraz aforado tiene una sola marca, el aforo, que indica un volumen exacto."
  },
  {
   "id": "vdc2",
   "type": "tf",
   "q": "La probeta tiene una sola marca.",
   "a": false,
   "why": "La probeta tiene muchas divisiones: mide volúmenes, pero no tan exactos como el matraz aforado."
  }
 ]
}
=== soluciones.json ===
{
 "format": 1,
 "gates": {
  "aforo": [{"tap":"opt","data":0}]
 },
 "expect": {"stars":3,"mistakes":0,"xpMin":30}
}
=== src/00-plantilla.js ===
// Plantilla de Pixelbook: una escena con una pregunta de opción múltiple, hecha con el kit de retos. Es un punto de
// partida: cámbialo todo. El guion (script.json) dice qué se oye y cuándo; este código dibuja cada escena del guion y
// dice cómo funciona cada reto (cada { "gate" } del guion). Todo se prueba con la API, sin instalar nada: guarda los
// ficheros (PUT …/files), lee los errores (POST …/validate) y juega el nivel con el enlace de una vista previa
// instantánea (POST …/previews). Las funciones del motor, con su firma, están en la referencia:
// https://pixelbook.videolibros.app/crear/referencia.md (y en la guía para IAs, crear/guia-ia.md).

// El marcador de arriba (una franja de unos 16 px de alto) sale en las escenas con "hud": 1 en script.json. A la
// izquierda, name y title: el texto que quieras, en mayúsculas («CASO 1», «MISIÓN 2»). A la derecha, la XP del nivel.
HUD_LEVELS[1] = { name: 'RETO', title: 'MEDIR VOLÚMENES', ramp: C.teal };

// Las opciones del reto, en su orden: la buena es la 0 (correct: 0). En pantalla salen barajadas, en otro orden en cada
// partida: lo que se toca es el índice de la opción, nunca su puesto.
const OPCIONES = ['MATRAZ AFORADO', 'PROBETA', 'VASO DE PRECIPITADO'];

// El reto «aforo» ({ "gate": "aforo" } en script.json) es una pregunta de opción múltiple (choiceIX). Cada fallo dice su
// explicación (wrong: opción → línea suelta de clips en script.json) y deja la opción apagada. Tras dos fallos llega la
// pista (hint). xp: los puntos de acertar a la primera, a la segunda y después; xpBudget (cart.json) es el tope de todo
// el nivel. Al acertar, el kit dice r-first, r-ok1 o r-ok2 (también en clips), cierra el reto y la historia sigue.
defInteraction('aforo', choiceIX({ n: 3, correct: 0, wrong: { 1: 'x-aforo-1', 2: 'x-aforo-2' }, hint: 'h-aforo', xp: [30, 15, 5], label: 'Aforo' }));

// La escena «s00-pregunta» del guion. t es el tiempo de la historia; lt, el de la escena. La pantalla mide 480 × 270:
// la pregunta arriba, las opciones en el centro (drawChoice pone sus zonas táctiles y su resalte con el ratón encima),
// la pista escrita encima de los subtítulos (drawHint), el retrato de quien habla abajo a la izquierda (speakerDock: en
// allow, los personajes de voices en cart.json) y Bit, el robot de quien juega, abajo a la derecha, con su color y su
// accesorio (look: true en cart.json). Los subtítulos y el marcador los pone la consola: no los dibujes.
// Las opciones entran desde la derecha, una cada 0,08 s y en 0,25 s cada una, desde t0 (en el tiempo de la historia, que
// se para al abrirse el reto): empiezan 0,6 s antes de la marca del reto, así ya están quietas cuando se abre.
defScene('s00-pregunta', (t, lt) => {
  cls(C.ink[1]);
  ui(() => {
    panel(92, 30, 296, 30, C.ink[2], { r: 4 });
    drawText('¿Qué mide volúmenes exactos?', 240, 41, { c: K.white, align: 'center' });
    drawChoice(ixState('aforo'), OPCIONES, { x: 92, y: 80, w: 296, h: 30, t0: M('s00-pregunta', 'aforo') - 0.6, now: t });
    drawHint(ixState('aforo'), 'h-aforo');
  });
  speakerDock(t, { allow: ['presentadora', 'ayudante'] });
  bit(430, 206, { t, expr: 'happy', color: C[PLAYER.look.color] || C.teal, acc: PLAYER.look.acc });
}, { in: 'cut' });
=== FIN ===
```

## La API paso a paso

Sin clave. La base es `https://admin-api.videolibros.app/api/external/pixelbook/creator`. Las respuestas son JSON y cada error trae `code`, `message`, `hint` y `docs`, su explicación. Las rutas, en OpenAPI 3.1: `GET /openapi.json`.

```bash
API=https://admin-api.videolibros.app/api/external/pixelbook/creator

# 1. Empezar un borrador. Guarda el token: es la única vez que se ve. El borrador empieza vacío.
curl -s -X POST "$API/drafts" -H 'Content-Type: application/json' \
  -d '{"title": "Volcanes de Canarias", "idea": "Cómo nacieron las islas, con retos de ordenar y elegir."}'
# → { "draft": { "slug": "volcanes-de-canarias", "idPrefix": "vdc", "expiresAt": "…", … },
#     "token": "pbd_…", "template": { "files": { … } }, "docs": { … }, "next": { … } }
TOKEN=pbd_...                 # el token de la respuesta
SLUG=volcanes-de-canarias     # draft.slug de la respuesta (puede no ser el que pediste)

# 2. Guardar los ficheros: un paquete (o JSON {"files": {"ruta": "texto" | null}}). Devuelve la validación.
curl -s -X PUT "$API/cartridges/$SLUG/files" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: text/plain; charset=utf-8' --data-binary @paquete.txt
# → { "workspace": { … }, "changed": [ … ], "validation": { "ok": false, "errors": [ { "code", "file", "path", "msg", "hint" } ] }, "next": { … } }

# 3. Corregir y volver a guardar hasta que validation.ok sea true. Validar sin guardar nada:
curl -s -X POST "$API/cartridges/$SLUG/validate" -H "Authorization: Bearer $TOKEN"

# 4. La vista previa. Dale preview.url a tu persona para que juegue.
curl -s -X POST "$API/cartridges/$SLUG/previews" -H "Authorization: Bearer $TOKEN"
# → { "preview": { "url": "https://pixelbook.videolibros.app/?vista=…", "expiresAt": "…", "opensLeft": 30, "voices": "estimated", "missing": [] }, … }

# 5. Si hace falta, la prueba completa: capturas de cada escena y de cada reto, la partida automática y una vista
#    que ya suena con la receta, EN DIRECTO y los ambientes (trial.preview).
curl -s -X POST "$API/cartridges/$SLUG/trials" -H "Authorization: Bearer $TOKEN"
curl -s "$API/cartridges/$SLUG/trials/42" -H "Authorization: Bearer $TOKEN"   # cada pollAfterSeconds de next

# 6. Solo cuando tu persona lo pida: enviar. Dale confirm.url: lo abre, entra con su cuenta y confirma.
curl -s -X POST "$API/cartridges/$SLUG/versions" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -H "Idempotency-Key: $SLUG-0.1.0" -d '{"notes": "Primera versión."}'
# → 202 { "version": { "version": "0.1.0", "status": "awaiting_confirmation" }, "confirm": { "url": "https://pixelbook.videolibros.app/?jugar#enviar=…", "expiresAt": "…" } }

# 7. Seguir el estado, el enlace de confirmación y los comentarios del equipo.
curl -s "$API/cartridges/$SLUG" -H "Authorization: Bearer $TOKEN"
```

- **`next`**, el siguiente paso. En las respuestas de un borrador es un objeto, `{ "what", "method"?, "url"?, "link"? }`: `url` es lo que llamas con `method` y `link`, lo que das a tu persona. Dentro de una prueba (`trial.next`) y de una versión (`version.next`) es una lista de `{ "action", "why", "method"?, "href"?, "pollAfterSeconds"? }`; `pollAfterSeconds` dice cada cuánto volver a mirar.
- `POST /drafts` lee `title` (de 3 a 80 caracteres, obligatorio), `idea` (hasta 2000), `area` (opcional: sin ella, `libre`, un nivel de cualquier tema, que sale en «Hechos por jugadores» con un estilo neutro; si es de una asignatura, `ciencias`, `geografia`, `musica`, `lengua` o `literatura`), `cast` (la serie; si no, la del área, y con `libre`, `modo-ciencia`), `slug` e `idPrefix` (se respetan si están libres; si no, la respuesta dice `renamed: true`) y `strict` (con `true` no se renombra: `409 slug_taken` o `prefix_taken`).
- **Los ficheros.** El borrador empieza vacío: guarda tú la plantilla o tus ficheros. `PUT …/files` fusiona por ruta (lo que no mandas se queda); `null` en JSON o `DELETE /cartridges/{slug}/files/{ruta}` borran un fichero. `validate`, `previews`, `trials` y `versions` usan los ficheros guardados si no mandas `files`. La respuesta de guardar dice `changed` (lo que ha cambiado) y `removed` (lo que se ha borrado).
- `GET /` con el token, o `GET /cartridges/{slug}`, dice qué borrador es, sus ficheros, sus vistas, sus pruebas, sus versiones y qué hacer.
- **La vista previa** sale al momento, con el rótulo «VISTA PREVIA · SIN REVISAR». Las voces todavía no están: cada frase dura lo que tarda en leerse y se ve en los subtítulos. Suenan la música y los efectos del catálogo. `missing` dice lo que tu nivel usa y esta vista no trae: `receta`, `directo` (el sonido de EN DIRECTO) o `ambientes`; vacía, suena todo menos las voces. `missingHint` lo dice en una frase. Para oírlo antes de publicar, pide una prueba completa. Un enlace caduca a los 7 días y se abre 30 veces. Es siempre el mismo: en `GET` sale igual, con su `expiresAt`; cuando ya no abre, sale `expired: true` y `url` vacía, y basta con pedir otra vista.
- **La prueba completa** cocina la receta, el sonido de EN DIRECTO y los ambientes: su enlace (`trial.preview`) suena como el nivel publicado, salvo las voces, que siguen estimadas. Si pasa, el `next` del borrador da su vista antes que la instantánea. Puede esperar su turno (`status: "queued"`, con `queuePosition` y `queuedForSeconds`). Si lleva más de 10 minutos esperando, la cocina va con retraso: sigue trabajando con vistas previas y vuelve más tarde.
- **La versión** de un envío es la de `cart.json` (o `version` en el cuerpo): la primera, `0.1.0`, y cada envío, una mayor. Mientras espera la confirmación, su enlace se recupera con `GET /cartridges/{slug}`, en `confirm.url` (caduca a los 7 días; `confirm.expired` dice si ya ha caducado). Si `confirm.recoverable` es `false`, ese enlace no se puede volver a mostrar: envía otra vez la misma versión y te da uno nuevo. Mientras espera no se puede enviar otra: que tu persona la confirme o, para cambiar algo antes, retírala (`POST /cartridges/{slug}/versions/{versión}/withdraw`) y envía otra con una versión mayor.
- Límites de un borrador: 300 comprobaciones por hora (guardar cuenta como una), 30 vistas previas y 2 pruebas completas al día, 3 versiones al día y 10 borradores nuevos por hora desde la misma conexión. Un borrador sin usar se borra a los 30 días.
- Estados de una versión: `awaiting_confirmation` (espera a tu persona), `checking` (la cocina lo prueba), `script_review` (una persona revisa los textos), `changes_requested` (pedimos cambios: lee `feedback` y envía otra versión), `cooking` (voces y sonido), `qa_review` (alguien lo juega entero) y `published` (ya está en el juego).

## Las herramientas del MCP

El conector de Pixelbook está en `https://mcp.videolibros.app/pixelbook/mcp` (HTTP, sin autenticación). Hace lo mismo que la API; el token del borrador va en `borrador`.

| Herramienta | Argumentos | Qué hace |
|---|---|---|
| `pixelbook_guia` | `seccion?` | Esta guía: sin `seccion`, la guía rápida y la lista de secciones; con ella, esa sección (`todo`, la guía entera) |
| `pixelbook_referencia` | `seccion?` | La referencia del motor: las firmas de todas las funciones, la paleta y el chip de sonido, por secciones |
| `pixelbook_catalogo` | `tipo?` | Lo que se puede dibujar y oír |
| `pixelbook_empezar_nivel` | `titulo`, `idea`, `area?`, `serie?`, `slug?`, `prefijo?` | `POST /drafts` |
| `pixelbook_guardar_ficheros` | `borrador`, `ficheros?` o `paquete?` | `PUT …/files` |
| `pixelbook_validar` | `borrador` | `POST …/validate` |
| `pixelbook_vista_previa` | `borrador` | `POST …/previews` |
| `pixelbook_prueba_completa` | `borrador` | `POST …/trials` |
| `pixelbook_estado` | `borrador` | `GET …/cartridges/{slug}`, también el enlace de confirmación |
| `pixelbook_enviar` | `borrador`, `notas?` | `POST …/versions`, solo si tu persona lo pide |
| `pixelbook_retirar_version` | `borrador`, `version` | `POST …/versions/{versión}/withdraw`: retira una versión que espera |

## Los 25 errores más comunes del validador

Cada error del validador trae `code`, `file`, `path` (un puntero JSON o una línea del código), `msg` y, a veces, `hint`.

| Código | Qué pasa | Cómo se arregla |
|---|---|---|
| `JSON_INVALID` | Un JSON no se puede leer | Revisa las comas, las llaves y las comillas rectas. El JSON no admite comentarios ni comas al final |
| `TEXT_EM_DASH` | Hay una raya larga | Cámbiala por coma, punto, dos puntos o paréntesis, también en los comentarios |
| `TEXT_CURLY_QUOTES` | Hay comillas tipográficas | Cita con «» y escribe el apóstrofo recto |
| `TEXT_URL`, `TEXT_DOMAIN` | Hay un enlace o un dominio | Quítalo: en el juego no hay enlaces |
| `TEXT_BOOK_PAGES`, `TEXT_BOOK_REF` | Se cita un libro de texto o sus páginas | Quítalo y cita la fuente del dato |
| `CART_ID_SLUG`, `LEVEL_ID_SLUG` | El id no es el del borrador | Pon en `cart.json` el `slug` de tu borrador |
| `CART_CAST_ENCARGO` | La serie no es la del borrador | Pon en `cast` la serie de tu borrador |
| `CART_VERSION_ENCARGO` | La versión de `cart.json` no es la que envías | Pon la misma versión en los dos sitios |
| `CARD_ID_PREFIX`, `BADGE_ID_PREFIX` | Un cromo o un logro sin el prefijo | Llámalo `vdc-algo`, con tu prefijo |
| `QUESTION_ID_PREFIX` | Una pregunta sin el prefijo | Llámala `vdc1`, `vdc2`… |
| `FLAG_PREFIX` | Un logro de `cart.json` sin el prefijo | Llámalo `vdcPerfecto`: el prefijo y una mayúscula |
| `SCRIPT_UNKNOWN_ROLE` | Un `who` que no es de la serie ni de `voices` | Usa un papel de la serie o añade el personaje a `voices` |
| `VOICE_UNKNOWN` | Una voz que no está en el banco | Elige una de la lista de voces |
| `VOICE_SHARED` | Dos personajes (o un personaje y un papel) con la misma voz | Dale otra voz a uno de los dos |
| `CHARACTER_ID_RESERVED` | Un personaje se llama como un papel de una serie | Ponle otro id (`guia`, `volcan`, `la-abuela`) |
| `VOICE_ID_DUPLICATE` | Dos voces con el mismo `say` | Cada `say` es único en todo el guion, también en `clips` |
| `GATE_NO_INTERACTION` | Un reto del guion no tiene código | Añade `defInteraction('id', …)` |
| `SCRIPT_AUDIO_UNDECLARED` | El guion usa una música o un efecto que no está en `audio` | Añádelo a `audio.music`, `audio.sfx` o `audio.amb` de `cart.json` |
| `CART_MUSIC_UNKNOWN`, `CART_SFX_UNKNOWN` | Una música o un efecto que no existe | Usa uno de la lista o hazlo con la receta |
| `CODE_KIT_CLIPS` | Usas el kit de retos y faltan sus líneas | Añade a `clips` `r-first`, `r-ok1` y `r-ok2` |
| `CODE_COMPILE` | El código no compila | Revisa paréntesis, llaves y comas en esa línea |
| `CODE_FORBIDDEN_NAME` | El código usa algo de fuera de la consola | Quita `setTimeout`, `fetch`, `window`…: usa `t`, `NOW` y `X.wait` |
| `CODE_SAY_UNKNOWN`, `CODE_FLAG_UNDECLARED` | `X.say` o `X.flag` con un nombre que no existe | Declara la línea en `clips` o el logro en `flags` |
| `SOL_GATE_MISSING`, `SOL_ACTION` | Falta cómo se supera un reto o una acción está mal escrita | Pon en `soluciones.json` la lista de cada reto (`{ "tap": "opt", "data": 0 }`) |
| `NIVEL_TEXT_TOO_WIDE`, `QUESTION_OPTS` | Un texto de `nivel.json` no cabe, o las opciones no son las que tocan | Acórtalo; una pregunta `choice` lleva cuatro opciones distintas |

## Errores de la API

Cada error de la API tiene su explicación en https://pixelbook.videolibros.app/crear/#errores, con su ancla `#error-<código>`. Los más frecuentes: `invalid_package` (el paquete no tiene ningún marcador), `invalid_files` (mira `details`), `confirmation_pending` (una versión espera a tu persona: que la confirme o retírala), `version_not_increasing` (sube la versión, también en `cart.json`), `preview_quota` y `trial_quota` (has llegado al límite del día) y `creation_paused` (vuelve más tarde).
