1. Visión general

El Reverse Player es un intérprete tree-walk implementado en JavaScript vanilla, sin dependencias externas. Corre en el browser mediante Canvas 2D API, Web Audio API y fetch(). Está empaquetado como un conjunto de archivos modulares que se sirven desde un servidor HTTP o se consolidan en un único HTML autocontenido.

La separación entre el núcleo del intérprete (Lexer, Parser, Intérprete) y los módulos de runtime es la decisión arquitectónica central. El intérprete recorre el AST y llama a un objeto runtime cuyo contrato es independiente de la plataforma. Ver Manual del Intérprete para el detalle de cada contrato.

2. Decisiones de diseño fundamentales

Estas decisiones fueron establecidas en el diseño original y sobrevivieron sin cambios a toda la implementación:

Tree-walk interpreter con Generators
El intérprete recorre el AST nodo a nodo. Los Generators de JavaScript permiten suspender la ejecución (yield) sin bloquear el browser. Esto implementa el game loop, ESPERAR y requestAnimationFrame de forma elegante sin callbacks.
Resolución virtual independiente
QBJr. dibuja sobre un canvas interno a la resolución especificada. Un segundo canvas visible escala el resultado con pixel-perfect scaling (imageSmoothingEnabled = false). Los dos canvas deben ser objetos distintos.
Input unificado
Teclado físico, mouse y botones táctiles se abstraen en un único conjunto de estados booleanos. Los botones táctiles despachan KeyboardEvent sintéticos. QBJr. no distingue la fuente del input.
Módulos desacoplados
Cada subsistema es independiente y accesible a través del objeto runtime. Reemplazar qbjr-canvas.js por una implementación SDL2 no requiere tocar el intérprete.
Sin eval() ni Function()
Las expresiones QBJr. se evalúan con un evaluador propio con precedencia correcta. No se construye código JavaScript en tiempo de ejecución.

3. Pipeline completo

Fuente .qbjr (texto plano UTF-8) │ ▼ qbjr-lexer.js Tokens [ {type, value, line, col} ] │ ▼ qbjr-parser.js AST { body:[], subs:{}, errors:[] } │ ▼ qbjr-interpreter.js (Generator function*) │ ├──► yield "frame" ──► requestAnimationFrame │ ▼ runtime modules canvas · input · sprites · audio · layers tiles · physics · files · text │ ▼ qbjr-engine.js copiarADisplay() ──► display canvas (CSS scale)

4. Orden de tick por frame

⚠ ORDEN CRÍTICOphysicsRT.tick() debe ir después de tilesRT.tick(). Si se invierte, la resolución de colisiones usa el estado de tiles del frame anterior.
1. inputRT.frameReset() // limpiar estados click/suelta 2. gen.next() × N (límite: 5000) // ejecutar QBJr. hasta yield 3. layersRT.tick(dt) // avanzar scroll 4. tilesRT.tick(layersRT) // dibujar tiles en capas 5. physicsRT.tick(dt, sprites, tiles) // física — DESPUÉS de tiles 6. spritesRT.tick(ctx, dt) // animar y dibujar sprites 7. layersRT.composite(ctx, vw, vh) // componer capas 8. copiarADisplay() // escalar al display

5. Sistema two-canvas

El Player usa dos elementos <canvas> separados. El canvas virtual (#virtual-screen, oculto) tiene la resolución QBJr. El canvas de display (#screen, visible) recibe una copia escalada cada frame.

// El intérprete dibuja siempre sobre virtualCanvas QBJrCanvas.crear(virtualCanvas); // El engine copia al display cada frame function copiarADisplay() { const scale = Math.min( displayCanvas.width / virtualCanvas.width, displayCanvas.height / virtualCanvas.height ); const dw = Math.floor(virtualCanvas.width * scale); const dh = Math.floor(virtualCanvas.height * scale); const ox = Math.floor((displayCanvas.width - dw) / 2); const oy = Math.floor((displayCanvas.height - dh) / 2); dctx.fillStyle = "#000"; dctx.fillRect(0, 0, displayCanvas.width, displayCanvas.height); dctx.imageSmoothingEnabled = false; dctx.drawImage(virtualCanvas, ox, oy, dw, dh); }

6. Estructura de archivos

reverse-player/ ├── index.html ← Player UI (700+ líneas) ├── qbjr-engine.js ← Coordinador del game loop ├── demo.qbjr ← Programa demo incluido │ ├── interpreter/ │ ├── qbjr-lexer.js ← Tokenizador │ ├── qbjr-parser.js ← Parser AST │ └── qbjr-interpreter.js ← Ejecutor con Generators │ └── runtime/ ├── qbjr-canvas.js ← Dibujo 2D ├── qbjr-input.js ← Teclado, mouse, touch ├── qbjr-sprites.js ← Sprites y animaciones ├── qbjr-audio.js ← Síntesis + archivos de audio ├── qbjr-layers.js ← Capas y parallax ├── qbjr-tiles.js ← Motor de tiles ├── qbjr-physics.js ← Física rudimentaria ├── qbjr-files.js ← Archivos y datos └── qbjr-text.js ← Markdown extendido (v2.2)

7. Lexer — qbjr-lexer.js

Transforma el código fuente en una secuencia plana de tokens tipados. Es el único módulo que toca el texto fuente.

ASPECTODETALLE
TokensKEYWORD · IDENTIFIER · NUMBER · STRING · OPERATOR · LPAREN/RPAREN · LBRACKET/RBRACKET · NEWLINE · EOF · ERROR
AND OR XOR NOTKEYWORD solo en MAYÚSCULAS. En minúsculas son IDENTIFIER.
La letra AIDENTIFIER en el lexer. El parser la reconoce contextualmente en PARA i DE 1 A 10.
Case sensitivityKeywords normalizadas a UPPERCASE. Variables preservan el case original.

8. Parser — qbjr-parser.js

Recursive descent manual. Precedencia de operadores en 8 niveles (Pratt-style). Pre-pasada de subprogramas para habilitar HACER antes de DEFINIR.

ASPECTODETALLE
TécnicaRecursive descent con 8 niveles de precedencia.
Pre-pasadaLos bloques DEFINIR se extraen antes del parse principal.
Dos palabras30+ pares de keywords emitidos como un único nodo (CREAR_SPRITE, PONER_TILE, etc.).
COLISIONA infijoSe trata en parseComparison() como operador de comparación.
ErroresSe acumulan sin abortar. Múltiples errores reportados en una pasada.

9. Intérprete — qbjr-interpreter.js

Recorre el AST con JavaScript Generators. Cinco helpers críticos abstraen las ambigüedades del lenguaje:

HELPERPROPÓSITO
varKey(name, estado)Lookup case-insensitive de variables. Retorna la clave canónica.
rawS(i)Retorna el nombre literal de un Identifier SIN evaluar como variable. Para constantes nombradas (PALETA, COLOR, etc.).
argNom(i)Retorna el nombre raw de un arg de FuncCall. Para sprites y archivos en funciones (EN_SUELO, FIN_ARCHIVO, etc.).
argStr(i)Semántica híbrida para LEER_DATO. Variable string no vacía → su valor. TODO_MAYÚSCULAS → clave literal. Mixto → nombre literal.
yieldPromise(p)Suspende el generator hasta que la Promise se resuelva. Para ABRIR y CARGAR_DATOS.

10. Canvas RT — qbjr-canvas.js

Implementa todos los comandos de dibujo sobre el canvas virtual. Firma crítica: rectangulo(x, y, ancho, alto) — ancho antes que alto. Requiere ctx.textBaseline = "top" antes de cada fillText.

11. Input RT — qbjr-input.js

Abstrae teclado, mouse y touch en estados booleanos. Los botones táctiles despachan KeyboardEvent sintéticos. frameReset() limpia estados de frame al inicio de cada tick.

12. Sprites RT — qbjr-sprites.js

Motor de sprites e instancias. Cada instancia tiene hitbox independiente del visual. Animaciones por fotogramas. Colisiones AABB. tick(ctx, dt) avanza animaciones y dibuja.

13. Audio RT — qbjr-audio.js

Dos subsistemas: archivos externos (AudioBufferSourceNode) y síntesis por pistas (OscillatorNode). AudioContext inicializado de forma lazy por política de autoplay. Notas musicales en español (Do4, Re#3, Lab5) con conversión a Hz por temperamento igual.

14. Layers RT — qbjr-layers.js

6 capas como OffscreenCanvas independientes. Capas 0-2: parallax. Capa 3: principal. Capa 4: frontal. Capa 5: HUD estático. composite() dibuja todas en orden sobre el virtual canvas.

15. Tiles RT — qbjr-tiles.js

Tilesets como imágenes divididas. Tilemaps como grillas por capa. Pre-renderizado en OffscreenCanvas con flag dirty — solo redibujan los tiles modificados. Tiles RIGIDO generan hitboxes automáticas para la física.

16. Physics RT — qbjr-physics.js

Integración Euler semi-implícita. dt clampado a 50ms (anti-tunneling básico). Resolución AABB separación mínima por eje. Fricción en suelo (vx × 0.85/frame). Velocidad máxima 1200px/s.

⚠ LIMITACIÓN CONOCIDAA velocidades altas o con tiles muy delgados puede ocurrir tunneling. Solución práctica: tiles de al menos 8px de grosor y velocidades razonables. La solución correcta (swept AABB) está fuera del scope de QBJr.

17. Files RT — qbjr-files.js

LECTURA via fetch(), ESCRITURA via localStorage con prefijo "qbjr:". Formato .qdat parseado a secciones y claves. reset() debe llamarse en engine.stop() para limpiar datasets de la sesión anterior.

18. Text RT — qbjr-text.js

Pipeline de markdown en 4 etapas: parseBlocks → parseInline → wrapTokens → renderInlineLine. Soporta énfasis, color (nombre o hex), encabezados, subíndice, superíndice. El 5° argumento de TEXTO_MD limita la altura de render.

19. Engine — qbjr-engine.js

MÉTODODESCRIPCIÓN
cargar(fuente)Lexea + parsea. No ejecuta. Retorna errores de parseo.
play()Inicia el RAF loop. Reanuda si estaba pausado.
pause()Cancela RAF sin destruir el generator.
stop()Termina, destruye el generator, llama filesRT.reset().
clearPause()Limpia el flag de PAUSAR programático. Llamar al presionar START.
📌 LÍMITE DE PASOSSi el generator no hace yield en 5000 pasos, el engine emite una advertencia y fuerza el avance al próximo frame. Evita que loops muy pesados bloqueen el browser sin terminar el programa.

20. Estado actual del proyecto (v0.9.31)

Todos los módulos planificados están implementados. El stack está completo. La versión 1.0.0 del motor no existe todavía: el criterio es especificación cerrada + intérprete sin bugs estructurales conocidos + suite de demos representativa completa.

MÓDULOVERSIÓN INCORPORADAESTADO
qbjr-lexer.jsv0.1.0✓ AND/OR/XOR/NOT case-sensitive. Soporte español.
qbjr-parser.jsv0.1.0✓ 30+ comandos dos palabras. COLISIONA infijo. argStr.
qbjr-interpreter.jsv0.1.0✓ Generators. varKey, rawS, argNom, argStr, yieldPromise.
qbjr-canvas.jsv0.1.0✓ Flood fill. Press Start 2P. textBaseline top.
qbjr-input.jsv0.2.0✓ Teclado+touch+mouse. frameReset. Combos.
qbjr-sprites.jsv0.4.0✓ Sprites/instancias. Animaciones. AABB.
qbjr-audio.jsv0.5.0✓ Web Audio + síntesis. Pistas/figuras/dinámicas.
qbjr-layers.jsv0.6.0✓ 6 capas. Parallax. Scroll/RUTA.
qbjr-tiles.jsv0.7.0✓ Tilesets/tilemaps. Tiles rígidos. COLISIONA_TILE.
qbjr-physics.jsv0.8.0✓ Gravedad. Velocidad. Resolución AABB vs tiles.
qbjr-files.jsv0.9.0✓ ABRIR/LEER/ESCRIBIR. Formato .qdat.
qbjr-text.jsv0.9.16+✓ Markdown extendido. TEXTO_MD con alturaMax.
qbjr-engine.jsv0.2.0✓ RAF. Two-canvas. Orden de tick correcto.

21. Portabilidad

Los módulos de runtime son la única capa que necesita reimplementarse por plataforma. El núcleo (Lexer, Parser, Intérprete) puede correr sin cambios en Node.js, Deno o QuickJS.

PLATAFORMAEQUIVALENTES
Generators JSC++20 coroutines · C# async/await · Python generators
Canvas 2D APISDL_Renderer · Godot CanvasItem · SFML
Web Audio APISDL_mixer · miniaudio · Godot AudioStreamPlayer
requestAnimationFrameSDL game loop · Godot _process() · Unity Update()
fetch + localStoragefopen/fclose · File API del SO
OffscreenCanvas (layers/tiles)Render textures · Godot CanvasLayer
📌Para el detalle completo de contratos de interfaz y orden de implementación recomendado, ver el Manual del Intérprete.