1. Propósito de este manual
Este manual está dirigido a desarrolladores que quieran reimplementar el intérprete de QBJr. en otro lenguaje, target o plataforma. Documenta el contrato de cada pieza del sistema para mantener compatibilidad con programas .qbjr existentes.
Para uso del Player como usuario final → Manual del Player. Para la especificación completa del lenguaje → Especificación. Para la historia de las decisiones y bugs → Evolución del Proyecto.
📌 REFERENCIALa implementación de referencia está en JavaScript vanilla (browser). Los contratos documentados son independientes de la plataforma. El §Portabilidad cubre consideraciones específicas para C++/SDL2 y C#/Godot.
2. Pipeline de ejecución
El pipeline es completamente lineal y síncrono, con un único punto de asincronía controlada (yield en el Generator del intérprete):
▼Fuente .qbjrTexto plano UTF-8. Soporte de acentos y ñ.
▼qbjr-lexer.jsTokenizador. Produce: [{type, value, line, col}]
▼qbjr-parser.jsParser recursive descent. Produce: { body:[], subs:{}, errors:[] }
▼qbjr-interpreter.jsGenerator function*. Recorre el AST nodo a nodo. yield "frame" → requestAnimationFrame
▼Módulos de runtimecanvas · input · sprites · audio · layers · tiles · physics · files · text
▼qbjr-engine.jsCoordinador. Ordena el tick por frame y copia virtual→display (CSS pixel-perfect).
📌 DECISIÓN CENTRALEl intérprete usa JavaScript Generators (function*) para ejecución cooperativa. Cada ciclo del game loop, el engine llama gen.next() hasta encontrar yield "frame" o result.done. Esto elimina timers, callbacks y async/await en el código del intérprete. En otras plataformas: C++20 coroutines, C# async/await, Python generators.
3. 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 — los sprites atraviesan superficies en un frame de retraso visible.
// Cada frame en qbjr-engine.js:
1. inputRT.frameReset() // limpiar estados click/suelta del frame anterior
2. gen.next() × N (límite: 5000) // ejecutar QBJr. hasta yield "frame"
3. layersRT.tick(dt) // avanzar scroll y parallax
4. tilesRT.tick(layersRT) // dibujar tiles en sus 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 sobre virtual canvas
8. copiarADisplay() // escalar al display canvas
⚠ BUG HISTÓRICO — dt = 0Calcular const _dt = now - lastFrame antes de lastFrame = now. El orden inverso produce _dt = 0 en todos los frames. La física queda estática y las animaciones no avanzan sin error visible.
4. Lexer
| TIPO | DESCRIPCIÓN |
| KEYWORD | Palabras clave normalizadas a UPPERCASE internamente. |
| IDENTIFIER | Variables y subprogramas. Preserva el case original del fuente. |
| NUMBER | Literales numéricos: enteros y flotantes. |
| STRING | Literales de texto entre comillas dobles. Soporte acentos y ñ. |
| OPERATOR | = + - * / ^ \ < > <= >= <> |
| LPAREN/RPAREN | ( ) |
| LBRACKET/RBRACKET | [ ] — índices de arreglos. |
| NEWLINE | Fin de instrucción. Significativo: no se pueden poner dos comandos en una línea. |
| EOF | Fin del stream. |
⚠ REGLA #1 — OPERADORES LÓGICOSAND OR XOR NOT MOD DIV son KEYWORD solo en MAYÚSCULAS. En minúsculas son IDENTIFIER válidos. and, or, not pueden ser nombres de variable. Esta es la regla más importante del lexer.
⚠ REGLA #2 — LA LETRA AA es IDENTIFIER en el lexer, nunca KEYWORD. El parser la reconoce contextualmente como separador de rango en PARA i DE 1 A 10. Si fuera KEYWORD, sería imposible usarla como nombre de variable.
5. Parser
Recursive descent — 8 niveles de precedencia
parseExpr() → OR, XOR (menor precedencia)
parseAnd() → AND
parseNot() → NOT (unario)
parseComparison() → = <> < > <= >= COLISIONA (infijo)
parseAddSub() → + -
parseMulDiv() → * / MOD DIV
parsePower() → ^ \ (asocia a la derecha)
parseUnary() → - (negación unaria)
parsePrimary() → literales, identifiers, llamadas, paréntesis
Comandos de dos palabras
El parser detecta pares de keywords adyacentes y los emite como un único nodo Command con guión bajo. El mapa twoWordCmds tiene 30+ pares:
CREAR + SPRITE → CREAR_SPRITE CREAR + TILESET → CREAR_TILESET
FISICA + GRAVEDAD → FISICA_GRAVEDAD PONER + TILE → PONER_TILE
CARGAR + DATOS → CARGAR_DATOS FIN + SI → FIN_SI
FIN + MIENTRAS → FIN_MIENTRAS FIN + PARA → FIN_PARA ... (30+)
Pre-pasada de subprogramas
📌Los bloques DEFINIR…FIN_DEFINIR se extraen en una primera pasada antes del parse principal. Esto permite invocarlos con HACER en cualquier punto del programa, incluyendo antes de su definición textual. El AST resultante: { body:[], subs:{ NOMBRE:[nodos] } }.
Estructura del AST
// Raíz:
{ body:[nodo,...], subs:{ NOMBRE:[nodo,...] }, errors:[...] }
// Nodos más comunes:
{type:"Command", cmd:"CREAR_SPRITE", args:[...]}
{type:"Assign", target:"x", index:null, value:expr}
{type:"If", cond:expr, thenBlock:[...], elseIfs:[...], elseBlock:[...]}
{type:"While", cond:expr, body:[...]}
{type:"For", var:"i", from:expr, to:expr, step:expr, body:[...]}
{type:"Select", expr:expr, cases:[{val,body}], defaultCase:[...]}
{type:"Call", name:"NOMBRE"} // HACER subprograma
{type:"FuncCall", name:"SEN", args:[...]}
{type:"BinaryExpr",op:"+", left:expr, right:expr}
{type:"UnaryExpr", op:"NOT", operand:expr}
{type:"Identifier",name:"x"}
{type:"IndexedIdent",name:"arr", index:expr}
{type:"Literal", value:42}
{type:"KeyCombo", keys:["FLECHA_ARRIBA","Z"]}
⚠ TRAMPA — SI NO (else)"SI NO" en el stream de tokens parece el inicio de "SI NOT condición". En parseBlock(): si el token actual es SI y el siguiente es NO, detener el bloque sin consumir esos tokens. Luego parsear SI+NO como bloque else. Sin este lookahead, el else nunca ejecuta y no hay error de parseo — el bug es silencioso.
6. Intérprete
Arquitectura Generator
// El intérprete cede el control al game loop:
function* ejecutarNodo(nodo, estado) {
// ... lógica del nodo ...
yield SIG_FRAME; // "esperar al próximo frame"
}
// El engine avanza el generator cada frame:
let steps = 0;
do {
result = gen.next();
steps++;
} while (!result.done && result.value !== SIG_FRAME && steps < 5000);
// Si steps >= 5000: advertencia, no bloqueo. El programa sigue.
Señales del generator
| SEÑAL | SIGNIFICADO |
| yield "frame" (SIG_FRAME) | Cede control al game loop. El engine retoma en el próximo requestAnimationFrame. |
| {__signal__:"break"} (SIG_BREAK) | Propaga ESCAPAR hacia arriba en la cadena de generators anidados. |
Estado del intérprete
const estado = {
vars: {}, // variables globales { clave: valor }
subs: {}, // subprogramas { NOMBRE: [nodos] }
paleta: {}, // paleta de colores activa
colorTrazo: "#FFF",
colorRelleno: "#FFF",
colorFondo: "#000",
grosor: 1,
errores: [],
finalizar: false,
pantalla: { w:256, h:240 },
runtime: { canvas, input, sprites, audio, layers, tiles, physics, files, text },
};
7. Helpers críticos
Estos helpers son el resultado de bugs reales. Cada uno resuelve una ambigüedad específica del lenguaje que no puede resolverse con un evaluador naïve.
varKey(name, estado)
Lookup case-insensitive de variables. Busca: exacto → lowercase → uppercase → case-insensitive completo. Retorna la clave canónica existente o name si es nueva.
→ Usar en toda asignación y lectura de variable.
rawS(i)
En Command: retorna el nombre literal del Identifier en args[i] SIN evaluar como variable. PALETA retro → rawS devuelve "retro", no vars["retro"] que sería 0.
→ Usar en: PALETA, COLOR (nombre), VOLTEAR, FONDO, SCROLL y todo cmd con constantes nombradas.
argNom(i)
En FuncCall: retorna el nombre raw del rawArg[i] SIEMPRE como literal, nunca evalúa. Los args de FuncCall se evalúan antes del switch — argNom accede al rawArg antes de esa evaluación.
→ Usar en: COLISIONA(), EN_SUELO(), FIN_ARCHIVO(), MOUSE_ENCIMA(), VEL_X(), VEL_Y(), COLISIONA_TILE().
argStr(i) v0.9.3+
Semántica híbrida para LEER_DATO: si el Identifier existe como variable string no vacía → usa su valor (uso dinámico: salaKey="sala_3"). Si es TODO_MAYÚSCULAS → lowercase como clave literal. Si es mixto no-variable → nombre literal como clave. Nunca usar argNom() para LEER_DATO.
→ Usar SOLO en: LEER_DATO. La distinción argStr/argNom es la fuente del bug T-04.
yieldPromise(promise)
Suspende el generator hasta que la Promise se resuelva (máximo 5 segundos). El game loop sigue corriendo mientras espera. Hace la asincronía completamente transparente para el programador QBJr.
→ Usar en: ABRIR (fetch/localStorage) y CARGAR_DATOS.
hasFlag(name) / N(i) / S(i)
hasFlag: VERDADERO si el nombre literal aparece en args del Command actual. Para flags opcionales sin valor. N(i): alias de evalExpr(args[i]) para números. S(i): alias String(evalExpr(args[i])) para strings evaluados.
→ hasFlag en: RIGIDO, RELLENO, BUCLE, CERRAR, HITBOX. N(i) y S(i) para coordenadas y nombres evaluados.
8. Runtime Canvas
⚠ FIRMA CRÍTICA — rectangulo()La firma correcta es rectangulo(x, y, ancho, alto). Implementarla como (x, y, alto, ancho) hace que todas las barras horizontales aparezcan verticales. Test inmediato: RECTANGULO 10 10 100 20 RELLENO debe dibujar una barra horizontal de 100×20.
⚠ textBaselinectx.textBaseline = "top" antes de cada fillText(). Sin esto el texto se corta en la parte superior.
| MÉTODO | DESCRIPCIÓN |
| crear(virtualCanvas) | Constructor. El canvas virtual es el destino exclusivo del intérprete. |
| limpiar() | fillRect completo con colorFondo. |
| punto(x,y) | fillRect(x,y,1,1). Color de trazo. |
| linea(coords[], cerrar, relleno) | beginPath/moveTo/lineTo. N puntos. cerrar cierra el path. |
| circulo(x,y,r,relleno) | arc(). |
| rectangulo(x,y,ancho,alto,relleno) | ancho antes que alto. |
| triangulo(x1,y1,x2,y2,x3,y3,relleno) | Path de 3 vértices. |
| poligono(x,y,r,lados,relleno) | Cálculo trigonométrico + path. |
| texto(x,y,str,sombreado) | fillText. textBaseline="top" obligatorio. |
| textoMD(x,y,ancho,str,alto?) | Pipeline markdown. alto = 5° arg opcional. |
| alturaMD(ancho,str) | Calcula altura sin dibujar. Retorna number. |
| rellenar(x,y) | Flood fill por scanline sobre ImageData. |
| setColor / setColorTrazo / setColorRelleno / setColorFondo | Estado de color. |
| setGrosor(px) | lineWidth. |
10. Runtime Sprites
| MÉTODO | DESCRIPCIÓN |
| crearSprite(nombre, img?) | CREAR SPRITE. img es URL o null para sprites con animaciones. |
| mostrar(sprite, inst, x, y) | MOSTRAR. Crea la instancia si no existe. |
| ocultar(sprite, inst?) | OCULTAR. Sin inst: oculta todas. |
| borrar(sprite) | BORRAR. Destruye sprite e instancias. |
| mover(sprite, inst, x, y, soloHitbox) | MOVER / MOVER HITBOX. |
| rotar(sprite, inst, grados, soloHitbox) | ROTAR / ROTAR HITBOX. |
| escalar(sprite, inst, factor, soloHitbox) | ESCALAR / ESCALAR HITBOX. |
| voltear(sprite, inst, horizontal) | VOLTEAR HORIZONTAL/VERTICAL. |
| crearAnimacion(nom, sprite, frames[]) | CREAR ANIMACION. |
| reproducirAnim(anim, sprite, inst, loop, n) | REPRODUCIR animación. |
| tick(ctx, dt) | Llamar cada frame. Avanza animaciones y dibuja sprites. |
| colisiona(sprite, inst?) | COLISIONA() unario — AABB vs todos los sprites. |
| colisionaEntre(spriteA, spriteB) | A COLISIONA B infijo — AABB entre dos sprites. |
| colisionaTile(aabb) | COLISIONA_TILE() — vs tiles rígidos activos. |
11. Runtime Audio
| MÉTODO | DESCRIPCIÓN |
| crearSonido(nombre, url) | CREAR SONIDO — SFX via fetch + decodeAudioData. |
| crearMusica(nombre, url) | CREAR MUSICA — BGM, loop por defecto. |
| reproducir(nombre, loop, vol, paneo) | REPRODUCIR. AudioBufferSourceNode + GainNode + StereoPannerNode. |
| detener(nombre, fadeMs?) | DETENER con fundido opcional via GainNode ramp. |
| setVolumen(nombre, 0-100) | VOLUMEN. silencio=0 bajo=25 medio=50 alto=75 maximo=100. |
| setPaneo(nombre, -50a50) | PANEO. izquierda=-50 centro=0 derecha=50. |
| crearPista(nombre, secuencia) | CREAR PISTA — síntesis por OscillatorNode. |
| reproducirPista(nombre, loop, canal) | REPRODUCIR pista en canal. |
| setTempo(bpm) | TEMPO — negra_ms = 60000 / BPM. |
📌 AUTOPLAYAudioContext debe inicializarse de forma lazy — el primer sonido se activa en respuesta a un gesto del usuario. El browser bloquea el audio hasta entonces.
12. Runtime Files
⚠ reset() en stop()Si filesRT.reset() no se llama en engine.stop(), los datasets de la sesión anterior persisten en memoria. Al reiniciar, CARGAR_DATOS puede encontrar datos residuales.
| MÉTODO | DESCRIPCIÓN |
| abrir(ruta, nombre, modo) | ABRIR. LECTURA: fetch del servidor. ESCRITURA/ANEXAR: localStorage. |
| leer(nombre, varCallback) | LEER — siguiente línea del archivo. |
| escribir(nombre, texto) | ESCRIBIR — una línea. |
| cerrar(nombre) | CERRAR y persistir en localStorage si era ESCRITURA. |
| finArchivo(nombre) | FIN_ARCHIVO() — VERDADERO si no hay más líneas. |
| cargarDatos(ruta, nombre) | CARGAR_DATOS — parsea .qdat y almacena en memoria. |
| leerDato(datos, seccion, clave) | LEER_DATO() — usa argStr() para la clave, no argNom(). |
| reset() | Limpiar todos los datasets en memoria. Llamar en engine.stop(). |
13. Engine
| MÉTODO | DESCRIPCIÓN |
| crear({virtualCanvas, displayCanvas, onError, onEnd, onLog}) | Factory principal. |
| cargar(fuenteQBJr) | Lexea + parsea. No ejecuta. Retorna lista de errores de parseo. |
| play() | Inicia el game loop (RAF). Si estaba pausado, reanuda. |
| pause() | Cancela el RAF sin destruir el generator. play() retoma exactamente donde estaba. |
| stop() | Termina el programa, destruye el generator, llama filesRT.reset(). |
| clearPause() | Limpia el flag de PAUSAR programático. Llamar al presionar START. |
| getState() | Retorna: idle | running | paused | stopped. |
⚠ TWO-CANVASSi virtualCanvas === displayCanvas, copiarADisplay() hace drawImage(canvas, canvas). En Chrome/WebKit es un no-op. La pantalla queda negra sin error. Verificar al inicio: if (virtual === display) throw Error("canvas must be different objects").
14. Funciones de cadena avanzadas
Incorporadas en v0.9.8 a partir del desarrollo de horror.qbjr. Todas indexadas base-1 (el primer carácter es posición 1), coherentes con QBasic.
| FUNCIÓN | DESCRIPCIÓN | EQUIVALENTE QB |
| BUSCAR(texto, aguja [, desde]) | Posición 1-based de la primera ocurrencia. 0 si no encuentra. | INSTR |
| EXTRAER(texto, desde [, largo]) | Subcadena desde la posición, de largo caracteres. | MID$ |
| CORTAR_IZQ(texto, n) | Primeros n caracteres. | LEFT$ |
| CORTAR_DER(texto, n) | Últimos n caracteres. | RIGHT$ |
| REEMPLAZAR(texto, viejo, nuevo) | Reemplaza todas las ocurrencias. | — |
| REPETIR(texto, n) | Repite el texto n veces. | STRING$ |
| CARACTER(n) | Carácter por código Unicode. | CHR$ |
| CODIGO(texto) | Código Unicode del primer carácter. | ASC |
📌La implementación de referencia hace la conversión interna de base-1 a base-0 de JavaScript. En otros lenguajes donde los strings ya son base-1 (Pascal, VBA), no es necesaria la conversión.
15. Trampas conocidas para implementadores
Estas son las divergencias entre el diseño original y la implementación real que más tiempo costaron. Cada una tiene un síntoma concreto y una solución probada.
T-01
Two-canvas: mismo objeto para virtual y display
🔴 PROBLEMASi virtualCanvas === displayCanvas, copiarADisplay() hace drawImage(canvas, canvas) sobre sí mismo. En Chrome/WebKit es un no-op. La pantalla queda negra sin error visible.
✅ SOLUCIÓNUsar dos elementos <canvas> distintos en el DOM. Assertion al inicio: if (virtual === display) throw Error("canvas must be different objects").
T-02
RECTANGULO: args al revés
🔴 PROBLEMAImplementar rectangulo(x,y,alto,ancho) en lugar de (x,y,ancho,alto) hace que todas las barras horizontales aparezcan verticales. El bug es invisible con rectángulos cuadrados.
✅ SOLUCIÓNTest inmediato obligatorio: RECTANGULO 10 10 100 20 RELLENO debe ser una barra horizontal ancha (100px ancho, 20px alto).
T-03
dt calculado después de lastFrame
🔴 PROBLEMAInvertir el orden: lastFrame = now; const _dt = now - lastFrame; produce _dt = 0 siempre. La física queda estática y las animaciones no avanzan.
✅ SOLUCIÓNSiempre: const _dt = now - lastFrame; ANTES de lastFrame = now;
T-04
argNom() para LEER_DATO
🔴 PROBLEMACon argNom(), LEER_DATO(mapa salaKey n) retorna siempre el literal "salaKey" como sección, ignorando que es una variable con valor "sala_3". Todas las salas fallan silenciosamente.
✅ SOLUCIÓNUsar argStr() para LEER_DATO. Semántica híbrida: variable string no vacía → su valor; TODO_MAYÚSCULAS → lowercase; mixto no-variable → literal.
T-05
Nombre de variable igual a clave de .qdat
🔴 PROBLEMAargStr() usa el valor de la variable si existe. Si la variable archivo ya tiene valor "SALA_VESTIBULO.qdat" de la sala anterior, LEER_DATO(mapa salaKey archivo) busca "SALA_VESTIBULO.qdat" como clave en vez de "archivo".
✅ SOLUCIÓNNombrar la variable archivoSala en vez de archivo. Documentar esta restricción: los nombres de variables no deben coincidir con claves del .qdat que se va a leer.
T-06
TECLA_A..Z removidas
🔴 PROBLEMALas constantes TECLA_A, TECLA_Z, TECLA_0..9 no existen en QBJr. El handler de KeyCombo que las incluya viola la spec pedagógica del autor.
✅ SOLUCIÓNMapear caracteres a KeyboardEvent.code: "W"→"KeyW", "5"→"Digit5". El programador usa TECLADO("W"), TECLADO("5"). Solo las teclas especiales tienen constantes (FLECHA_*, BARRA_ESPACIADORA, etc.).
T-07
SI NO (else) silencioso
🔴 PROBLEMA"SI NO" en el stream parece el inicio de "SI NOT condicion". parseBlock() procesando el cuerpo de un OTRO SI no sabe detenerse. El bloque else queda vacío sin error de parseo.
✅ SOLUCIÓNEn parseBlock(): si token actual es SI y siguiente es NO, detener sin consumir. Luego parsear SI+NO como bloque else.
16. Mapa de portabilidad
El núcleo del intérprete (Lexer, Parser, Intérprete) puede correr en cualquier entorno con JavaScript (Node.js, Deno, QuickJS). Los runtimes son la única capa que necesita reimplementarse por plataforma.
| COMPONENTE | JS (REFERENCIA) | PLATAFORMA NATIVA |
| Lexer | qbjr-lexer.js | Reescribir. El mapa de KEYWORDS es el contrato. |
| Parser | qbjr-parser.js | Reescribir. La estructura del AST es el contrato. |
| Intérprete | qbjr-interpreter.js | Generators → C++20 coroutines / C# async/await / Python generators. |
| Canvas RT | Canvas 2D API | SDL_Renderer / Godot CanvasItem / SFML / OpenGL. |
| Input RT | KeyboardEvent + Touch | SDL_Event / Godot Input / Win32 WM_KEY*. |
| Sprites RT | Canvas + Image | SDL_Texture / Godot Sprite2D / SFML Sprite. |
| Audio RT | Web Audio API | SDL_mixer / miniaudio / Godot AudioStreamPlayer. |
| Layers RT | OffscreenCanvas | Render textures / Godot CanvasLayer. |
| Tiles RT | OffscreenCanvas | Tilemap del engine destino. |
| Physics RT | JS puro | Reimplementar o usar motor nativo con contrato equivalente. |
| Files RT | fetch + localStorage | fopen/fclose / File API del SO. |
| Text RT | Canvas 2D fillText | Cualquier renderer con measureText equivalente. |
| Engine | requestAnimationFrame | SDL game loop / Godot _process() / Unity Update(). |
17. Orden de implementación recomendado
| ORDEN | MÓDULO | NOTAS |
| 1 | Lexer + Parser | Sin runtime. Verificar AST correcto con los tests de la §18. |
| 2 | Canvas RT (básico) | Verificar RECTANGULO con barra horizontal inmediatamente. |
| 3 | Input RT | Imprescindible para cualquier programa interactivo. |
| 4 | Engine | Verificar two-canvas y dt antes de lastFrame antes de avanzar. |
| 5 | Sprites RT | Depende de Canvas e Input. |
| 6 | Audio RT | Independiente. Cuidado con el requisito de gesto de usuario. |
| 7 | Layers RT | Independiente. |
| 8 | Tiles RT | Depende de Layers. |
| 9 | Physics RT | Depende de Sprites y Tiles. Verificar el orden del tick. |
| 10 | Files RT | Asincrónico. Requiere yieldPromise o equivalente. |
| 11 | Text RT | Depende de Canvas (fillText, measureText). |
18. Suite de tests mínima
Estos tests deben pasar antes de conectar el intérprete a ningún runtime. Cubren los casos más propensos a errores de implementación.
' Test 1: variables de una letra — deben ser IDENTIFIER, no operadores
x = 5 y = 10 r = 3
IMPRIMIR x + y + r ' debe imprimir 18
' Test 2: AND OR NOT en mayúsculas solo; minúsculas son variables
DECLARAR and COMO BOOLEANO
and = VERDADERO
SI and AND NOT FALSO ENTONCES IMPRIMIR "ok" FIN SI ' debe imprimir "ok"
' Test 3: precedencia — multiplicación antes que suma
x = 2 + 3 * 4
IMPRIMIR x ' debe ser 14, no 20
' Test 4: SI NO (else)
SI FALSO ENTONCES
IMPRIMIR "mal"
SI NO
IMPRIMIR "bien" ' debe imprimir esto
FIN SI
' Test 5: subprogramas con variables globales
x = 5
DEFINIR doble x = x * 2 FIN DEFINIR
HACER doble HACER doble
IMPRIMIR x ' debe ser 20
' Test 6: arreglos base-1
DECLARAR arr[3] COMO ENTERO
arr[1] = 10 arr[2] = 20 arr[3] = 30
IMPRIMIR arr[2] ' debe ser 20
' Test 7: PARA con acumulador
s = 0
PARA i DE 1 A 10
s = s + i
FIN PARA
IMPRIMIR s ' debe ser 55
' Test 8: SEGUN / CASO OTRO
x = 99
SEGUN x
CASO 1: IMPRIMIR "uno"
CASO OTRO: IMPRIMIR "otro" ' debe imprimir esto
FIN SEGUN