diff --git a/.gitignore b/.gitignore index 7014d68..44037f8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ $RECYCLE.BIN/ .LSOverride # Icon must end with two \r -Icon +Icon # Thumbnails ._* @@ -67,3 +67,10 @@ Temporary Items # .nfs files are created when an open file is removed but is still being accessed .nfs* +# ---> Rebotes +# Binari de l'intèrpret ascii (es compila a part) +/ascii +# Fitxer de rècord generat pel joc +/records + + diff --git a/ASCII_API.md b/ASCII_API.md new file mode 100644 index 0000000..657a413 --- /dev/null +++ b/ASCII_API.md @@ -0,0 +1,275 @@ +# ASCII — Referencia del intérprete Lua + +Documento extraído del código fuente en `c:/mingw/gitea/ascii/` (principalmente `ascii.cpp`, `lua.cpp`, `play.cpp`, `ascii.h`). Sirve como guía para portar Pepe Runner desde Turbo Pascal a Lua. + +> Versión analizada del intérprete: v0.6.1 aprox (según el mensaje del boot ROM en `lua.cpp`). + +--- + +## 1. Modelo de ejecución + +Cada juego/programa es **un solo fichero `.lua`** que define dos funciones globales: + +```lua +function init() + -- se llama una sola vez al arrancar +end + +function update() + -- se llama cada frame (~60 FPS, vsync) +end +``` + +- El intérprete se invoca como `ascii.exe nombre_juego.lua`. Si no se pasa argumento, intenta cargar `game.lua`. +- También se puede **arrastrar y soltar** un `.lua` sobre la ventana para cargarlo. +- **F5** reinicia el juego (re-llama a `init()` y vuelve a empezar el bucle). +- **ESC** pausa la ejecución y abre una consola de depuración (`> ` prompt). Los comandos `run` y `cont` la cierran. Comandos prefijados con `?` evalúan e imprimen (ej.: `?1+1`). +- Se usa la versión estándar de Lua que está vendorizada en `ascii/lua/` (con `luaL_openlibs`), así que están disponibles `string`, `math`, `table`, etc. + +--- + +## 2. Modos de pantalla — `mode(n)` + +| Modo | Resolución carácter | Resolución pixel | Notas | +|------|--------------------:|-----------------:|-------| +| 0 | 80 × 30 | 640 × 240 | Color único global (no por-carácter) — usado para depuración / texto. `current_color` aplica a toda la pantalla. | +| 1 | 40 × 30 | 320 × 240 | **Modo por defecto.** Color por carácter. | +| 2 | 20 × 15 | 160 × 120 | Mitad de resolución. Cómodo para tiles grandes (ej.: sokoban). | +| 3 | 32 × 24 | 256 × 192 | Estilo "ZX Spectrum" (con bordes anchos). | + +Cada carácter es **8×8 píxeles**. Los gráficos son texto coloreado, no píxeles libres — el "lienzo" es una matriz de celdas (carácter + atributo de color). + +--- + +## 3. Paleta de colores (16, CGA/EGA) + +Constantes Lua predefinidas (`lua.cpp` líneas 610-625): + +| Código | Constante | Aprox. | +|-------:|------------------------|---------------| +| 0 | `COLOR_BLACK` | #000000 | +| 1 | `COLOR_BLUE` | #0000AA | +| 2 | `COLOR_GREEN` | #00AA00 | +| 3 | `COLOR_CYAN` | #00AAAA | +| 4 | `COLOR_RED` | #AA0000 | +| 5 | `COLOR_MAGENTA` | #AA00AA | +| 6 | `COLOR_BROWN` | #AA5500 | +| 7 | `COLOR_LIGHT_GRAY` | #AAAAAA | +| 8 | `COLOR_DARK_GRAY` | #555555 | +| 9 | `COLOR_LIGHT_BLUE` | #5555FF | +| 10 | `COLOR_LIGHT_GREEN` | #55FF55 | +| 11 | `COLOR_LIGHT_CYAN` | #55FFFF | +| 12 | `COLOR_LIGHT_RED` | #FF5555 | +| 13 | `COLOR_LIGHT_MAGENTA` | #FF55FF | +| 14 | `COLOR_YELLOW` | #FFFF55 | +| 15 | `COLOR_WHITE` | #FFFFFF | + +El atributo de color de una celda es 1 byte: nibble bajo = INK (tinta), nibble alto = PAPER (fondo). + +--- + +## 4. API — Funciones expuestas a Lua + +### Pantalla y color + +| Función | Descripción | +|---------|-------------| +| `mode(n)` | Cambia modo de pantalla (0-3) y hace cls. | +| `cls([chr=32])` | Limpia con el carácter dado (32 = espacio). En modo ≠0 además rellena el color attr. | +| `ink(c)` | Color de tinta (0-15). | +| `paper(c)` | Color de fondo (0-15). | +| `border(c)` | Color del borde de la ventana. | +| `color(ink, paper, [border])` | Combina los tres. | +| `locate(x, y)` | Posiciona cursor en celda (x, y). | +| `print(str, [x, y])` | Imprime `str` (sin salto de línea). Si se dan x,y, primero hace `locate`. | +| `crlf()` | CR + LF (mueve cursor a inicio de siguiente línea). | + +### Entrada + +| Función | Descripción | +|---------|-------------| +| `btn(k)` | `true` si la tecla `k` está pulsada *en este frame* (estado SDL_GetKeyboardState). | +| `btnp(k)` | `true` solo en el frame en que la tecla se pulsa (edge). | +| `mousex()` / `mousey()` | Posición del ratón en **coordenadas de carácter** (ya escalado al modo). | +| `mousewheel()` | Delta de la rueda en este frame. | +| `mousebutton(i)` | `true` si el botón `i` está pulsado (1=izq, 2=medio, 3=der; usa `SDL_BUTTON(i)`). | + +> **Nota**: `whichbtn()` está declarado en `ascii.h` y existe en C++, pero **no está expuesto a Lua** (no aparece en los `lua_setglobal` de `lua.cpp`). Para detectar qué tecla se ha pulsado en un frame hay que iterar con `btnp()` sobre las constantes `KEY_*`. + +> **Bug de `tostr` con negativos**: la implementación de `tostr()` en `lua.cpp` (función `intToStr`) hace `(x % 10) + '0'` que con `x = -1` produce `-1 + 48 = 47`, o sea `'/'`. Por tanto `tostr(-1)` devuelve `"/"`, `tostr(-2)` devuelve `"."`, etc. Si vas a imprimir un número que puede ser negativo, usa `string.format("%d", n)` (que sí maneja signo) o clampa con `max(0, n)` antes de pasar a `tostr`. + +Códigos de tecla — todos definidos como globales `KEY_*` en Lua. Lista completa (de `lua.cpp` 502-608): + +``` +KEY_A..KEY_Z = 4..29 +KEY_1..KEY_0 = 30..39 (1=30, 2=31, ..., 9=38, 0=39) +KEY_RETURN=40 KEY_ESCAPE=41 KEY_BACKSPACE=42 KEY_TAB=43 KEY_SPACE=44 +KEY_MINUS=45 KEY_EQUALS=46 KEY_LEFTBRACKET=47 KEY_RIGHTBRACKET=48 +KEY_BACKSLASH=49 KEY_NONUSHASH=50 KEY_SEMICOLON=51 KEY_APOSTROPHE=52 +KEY_GRAVE=53 KEY_COMMA=54 KEY_PERIOD=55 KEY_SLASH=56 KEY_CAPSLOCK=57 +KEY_F1..KEY_F12 = 58..69 +KEY_PRINTSCREEN=70 KEY_SCROLLLOCK=71 KEY_PAUSE=72 +KEY_INSERT=73 KEY_HOME=74 KEY_PAGEUP=75 KEY_DELETE=76 KEY_END=77 KEY_PAGEDOWN=78 +KEY_RIGHT=79 KEY_LEFT=80 KEY_DOWN=81 KEY_UP=82 +KEY_NUMLOCKCLEAR=83 KEY_KP_DIVIDE=84 KEY_KP_MULTIPLY=85 KEY_KP_MINUS=86 KEY_KP_PLUS=87 KEY_KP_ENTER=88 +KEY_KP_1..KEY_KP_0 = 89..98 KEY_KP_PERIOD=99 +KEY_NONUSBACKSLASH=100 KEY_APPLICATION=101 +KEY_LCTRL=224 KEY_LSHIFT=225 KEY_LALT=226 KEY_LGUI=227 +KEY_RCTRL=228 KEY_RSHIFT=229 KEY_RALT=230 KEY_RGUI=231 +``` + +(Son los SDL2 scancodes.) + +### Matemáticas + +`abs(x)`, `ceil(x)`, `flr(x)`, `sgn(x)`, `sin(x)`, `cos(x)`, `atan2(dx, dy)`, `sqrt(x)`, `max(a,b)`, `min(a,b)`, `mid(a,b,c)` (devuelve el del medio, equivalente a `clamp`). + +`rnd(n)` devuelve un entero en `[0, n-1]` (`rand()%n`). `srand(seed)` siembra el RNG. + +### Strings + +| Función | Descripción | +|---------|-------------| +| `tostr(v)` | Convierte valor a string. Soporta nil, function, table (formato `{k=v,...}`), number, boolean, string. | +| `strlen(s)` | Longitud en bytes. | +| `ascii(s, i)` | Código del byte en índice `i` (0-based). | +| `chr(n)` | String de un solo carácter cuyo código es `n`. | +| `substr(s, start, length)` | Subcadena. | + +> Nota: Lua estándar también está disponible, así que `string.format`, `string.sub`, etc., funcionan. Pero los demos usan estas helpers. + +### Memoria + +| Función | Descripción | +|---------|-------------| +| `peek(addr)` | Lee 1 byte de la VRAM/memoria (0..0x1FFF). | +| `poke(addr, val)` | Escribe 1 byte. | +| `memcpy(dst, src, size)` | Copia bytes en la memoria del fantasy console. | +| `setchar(idx, b0..b7)` | Define los 8 bytes del carácter `idx` en el char-ROM (sobrescribe la fuente). | + +**Mapa de memoria** (8 KB total, `mem[8192]`): + +- `0x0000` (0): char_screen (matriz de códigos de carácter por celda) +- Tras char_screen viene color_screen (offset = `screen_width * screen_height`) +- `0x0A00` (2560 = `MEM_CHAR_OFFSET`): char-ROM (definición de glifos, 8 bytes por carácter, 256 chars = 2048 bytes) +- `0x1200` (4608 = `MEM_BOOT_OFFSET`): zona de boot/recursos de ROM + +Para los modos 1 y 2 los color_screen offsets son 1200 y 300 respectivamente; en modo 3 es 768; en modo 0 no hay color_screen por celda (color global). + +### Audio + +**Sonido simple:** +- `sound(freq, len)` — onda cuadrada a `freq` Hz durante `len` (en algo similar a centésimas de segundo; `audio_len = len*44.1`). +- `nosound()` — silencio inmediato. + +**Mini-lenguaje MML — `play(str)`:** + +Sintaxis tipo BASIC `PLAY` / MML. Tokens (case-sensitive, minúsculas): + +| Token | Significado | +|-------|-------------| +| `c d e f g a b` | Nota. Acepta sufijo `#` o `+` (sostenido) o `-` (bemol). Luego dígito 0-9 para duración. | +| `r` | Silencio. Acepta dígito de duración. | +| `o<0-7>` | Octava absoluta. | +| `>` `<` | Sube / baja octava. | +| `l<0-9>` | Longitud por defecto para notas sin duración. | +| `v<0-9>` | Volumen (se traduce a `(d-0)<<4`). | +| `t<0-9>` | Tempo. | + +Duraciones: índice 0-9 → tabla `{313,625,938,1250,1875,2500,3750,5000,7500,10000}` (de "redonda" a "trentaidosava", aproximadamente). + +Ejemplo (de `breakout.lua`): + +```lua +play("l0o3bagfedc") -- escala descendente como sonido de game-over +play("o5l0c") -- pitido agudo (rebote) +``` + +### Ficheros y portapapeles + +| Función | Descripción | +|---------|-------------| +| `load([filename])` | Reinicia y carga otro `.lua` (o el mismo si filename=nil). | +| `fileout(name, addr, size)` | Vuelca `size` bytes de memoria a un binario. | +| `filein(name, addr, size)` | Carga un binario a memoria. | +| `toclipboard(str)` | Copia al portapapeles del SO. | +| `fromclipboard()` | Lee del portapapeles (máx 1023 chars). | + +### Utilidades de tiempo / frame + +| Función | Descripción | +|---------|-------------| +| `time()` | Milisegundos desde inicio (`SDL_GetTicks()`). | +| `cnt()` | Contador de frames desde el último `rst()`. | +| `rst()` | Resetea el contador de frames a 0. | +| `log(str)` | Imprime en la consola de debug (no en pantalla). | + +--- + +## 5. Caracteres especiales útiles + +Los demos usan códigos > 127 que corresponden a glifos definidos en `rom.c` (el char-ROM por defecto). Algunos vistos: + +- `\003` (3) — un bloque relleno (usado en pong para los compases) +- `\016` (16) — cubo de caja (en sokoban) +- `\127` (127) — pared en sokoban (redefinido con `setchar(127, ...)`) +- `\143`, `\154`, `\150`, `\156`, `\149` — esquinas y trazos de marcos +- `\248`, `\250`, `\251` — sprite animado de "OK" en sokoban +- `\233` — pelota en breakout +- `\131` — pala en breakout +- `\001` — cubo de tetromino (redefinido con `setchar(1, 0xff,0x81,...)`) + +Para usarlos siempre se puede hacer `setchar(idx, b0..b7)` con la bitmap deseada y luego imprimirlo con `print(chr(idx), x, y)`. + +--- + +## 6. Patrón típico de un juego + +```lua +function init() + mode(1) + cls() + -- estado inicial + player = {x=10, y=15} + score = 0 +end + +function update() + -- input + if btnp(KEY_LEFT) then player.x = player.x - 1 end + if btnp(KEY_RIGHT) then player.x = player.x + 1 end + + -- lógica + -- ... + + -- render (no hay vsync explícito; el bucle ya hace flip al final) + cls() + color(COLOR_WHITE, COLOR_BLACK) + print("\248", player.x, player.y) + print("SCORE: "..tostr(score), 0, 0) +end +``` + +**Cosas a recordar:** + +- No se "pintan" píxeles; se imprime un código de carácter en una celda y se le asocia un atributo de color (ink + paper). Para gráficos personalizados, redefinir glifos con `setchar`. +- El bucle de render lo hace el motor C++ después de `update()` — no hay que llamar a ningún `flip`. +- El `state machine` típico se hace asignando `update = otra_funcion` (ver `demos/sokoban.lua`). +- Las coordenadas de pantalla son **enteras y por celda**, no por píxel. (0,0) es esquina superior izquierda. +- Para depurar: `log("mensaje")` o pulsar ESC y usar la consola con `?variable`. + +--- + +## 7. Cómo compilar el intérprete + +Desde `c:/mingw/gitea/ascii/`: + +```sh +make windows +``` + +Requiere MinGW (g++) y SDL2 para Windows. Produce `ascii.exe`. Para correr Pepe Runner: + +```sh +ascii.exe pepe_runner.lua +``` diff --git a/README.md b/README.md index 259528f..89cf33d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ # rebotes_amstrad_ascii -Versió del joc "Rebotes" del manual de Amstrad per a ascii \ No newline at end of file +Port a la fantasy console **ascii** del juego *Rebotes* publicado por +AMSOFT en 1984 (Alexander Martin), originalmente escrito en BASIC de Amstrad CPC. + +## El juego + +Breakout/Arkanoid clásico: mueves una pala en la base de la pantalla y haces +rebotar una pelota contra cuatro filas de ladrillos. Los ladrillos de la fila +superior dan más puntos. Tienes 5 pelotas. Al limpiar la pantalla, se +regenera y sigues con los puntos y las pelotas que llevabas. + +## Controles + +- **Flechas izda/dcha** (o **O / P**) — mover la pala +- **ESPACIO** — empezar / volver desde menús +- **I** — instrucciones desde el título +- **A..Z** — introducir nombre al batir el récord + +## Cómo ejecutar + +Necesitas el intérprete de la fantasy console ascii: + +```sh +ascii rebotes.lua +``` + +El récord se guarda en un fichero `records` junto al juego. + +## Ficheros + +- `rebotes.lua` — el juego. +- `rebotes.bas` — listado BASIC original del Amstrad CPC, como referencia. +- `ASCII_API.md` — referencia del intérprete Lua de ascii. +- `chuleta_font_ascii.png` — tabla de glifos del charset de ascii. + +## Agradecimientos + +- A **JailDoctor** por crear la fantasy console ascii y mantener su charset y + paleta tan cómodos para portar clásicos. +- A **Alexander Martin** y **AMSOFT** (1984) por el juego original. +- A la **AUA — Amstrad Users Association** por conservar el código fuente del + original y hacerlo accesible para esta versión. +- A **Claude** (Anthropic) por la ayuda en el port a Lua/ascii. diff --git a/chuleta_font_ascii.png b/chuleta_font_ascii.png new file mode 100644 index 0000000..5356fa5 Binary files /dev/null and b/chuleta_font_ascii.png differ diff --git a/rebotes.bas b/rebotes.bas new file mode 100644 index 0000000..d78ef42 --- /dev/null +++ b/rebotes.bas @@ -0,0 +1,69 @@ +10 'REBOTES, por Alexander Martin +20 'copyright (c) AMSOFT 1984 +30 ' +40 MODE 1:BORDER 1:INK 0,1:INK 1,26:INK 2,24:INK 3,6 +50 SPEED KEY 15,2 +60 ENV 1,1,18,0,11,0,10 +70 ENT 1,10,2,2 +80 ENV 3,1,0,16,5,-3,2 +90 ENV 2,5,3,3,1,-21,22,9,-3,2 +100 ENT -2,10,2,2,5,-7,1,2,11,3,2,-4,8 +110 ' +120 ' +130 MOVE 30,32:DRAWR 0,400,1:MOVE 610,32:DRAWR 0,400,1 +140 PEN 3:LOCATE 3,1:PRINT STRING$(36,143) +150 PEN 2:LOCATE 3,2:PRINT STRING$(36,143) +160 PEN 1:FOR r=5 TO 6:LOCATE 3,r:PRINT STRING$(36,143):NEXT r +170 bx=9 +180 pelotas=5:puntos=0 +190 PEN 1:GOSUB 680:CLEAR INPUT +200 IF INKEY$<>CHR$(32) AND JOY(0)<16 THEN 200 +210 LOCATE 3,23:PRINT SPACE$(36):LOCATE 1,24:PRINT SPACE$(40); +220 GOSUB 690:GOSUB 660:GOTO 280 +230 ' +240 ' +250 LOCATE bx,24:PRINT" ";STRING$(4,131);" ":RETURN +260 ' +270 ' +280 xa=1:ya=1:IF INT(RND*2)=1 THEN xa=-xa +290 PEN 1:GOSUB 250 +300 ORIGIN 0,400 +310 x=bx+4:y=11:x1=x:y1=y +320 ' +330 ' +340 x1=x+xa:y1=y+ya +350 IF x1=3 OR x1=38 THEN xa=-xa +360 GOSUB 540 +370 IF y1=24 AND x1>bx+1 AND x1bx+5)OR(x0 THEN ya=-ya:xz=x1:yz=y1:y1=y1+ya:GOSUB 590:IF t=2 THEN puntos=puntos+10:GOSUB 660 +420 IF t=3 THEN puntos=puntos+20:GOSUB 660 +430 IF t=1 THEN puntos=puntos+5:GOSUB 660 +440 IF y1=1 THEN ya=1 +450 LOCATE x,y:PRINT" ":LOCATE x1,y1:PRINT CHR$(231):x=x1 :y=y1 +460 IF y=1 OR x=3 OR x=38 THEN SOUND 129,78,8,7,1,1 +470 GOTO 340 +480 ' +490 ' +500 pelotas=pelotas-1:SOUND 132,19,46,12,2,2:IF pelotas=0 THEN GOTO 620 +510 GOSUB 660:GOTO 280 +520 ' +530 ' +540 IF (INKEY(8)=0 OR INKEY(74)=0) AND bx>2 THEN bx=bx-2: RETURN +550 IF (INKEY(1)=0 OR INKEY(75)=0) AND bx<32 THEN bx=bx+2 :RETURN +560 RETURN +570 ' +580 ' +590 LOCATE xz,yz:PRINT" ":RETURN +600 ' +610 ' +620 IF puntos>=maximo THEN maximo=puntos +630 GOSUB 660:puntos=0:pelotas=5:GOTO 130 +640 ' +650 ' +660 SOUND 130,0,20,13,3,0,31:LOCATE 1,25:PRINT "Maximo";maximo; +670 LOCATE 15,25:PRINT "Puntos";puntos:LOCATE 30,25:PRINT "Pelotas";pelotas:RETURN +680 LOCATE 3,23:PRINT"Pulse barra espaciadora para empezar":RETURN +690 LOCATE 1,25:PRINT SPACE$(40);:RETURN diff --git a/rebotes.lua b/rebotes.lua new file mode 100644 index 0000000..fe01a21 --- /dev/null +++ b/rebotes.lua @@ -0,0 +1,629 @@ +-- ============================================================ +-- REBOTES — port a la fantasy console "ascii" del original +-- de Alexander Martin para Amstrad CPC (AMSOFT, 1984). +-- Sergi Valor, 2026. +-- ============================================================ + +-- ============================================================ +-- CONFIGURACION (todo lo tuneable vive aqui) +-- ============================================================ + +-- Pantalla y layout +MODO = 1 +ANCHO = 40 +ALTO = 30 + +-- Zona de juego (donde se mueve la pelota) +PELOTA_X_MIN = 2 +PELOTA_X_MAX = 37 +TECHO_Y = 1 + +-- Filas de ladrillos (idéntico al original: filas 1, 2, 5, 6). +-- Estructura: { fila, color, puntos, glifo } +FILAS_LADRILLOS = nil -- se inicializa más abajo cuando ya hay paleta + +-- Pala +PALA_Y = 27 +PALA_W = 4 +PALA_X_MIN = PELOTA_X_MIN +PALA_X_MAX = PELOTA_X_MAX - PALA_W + 1 +PALA_X_INI = 18 +PALA_STEP = 1 -- paso por movimiento (el original era 2) + +-- Pelota +PELOTA_GLIFO = 231 +PELOTA_Y_INI = 11 -- como el original +TICS_PELOTA = 5 -- frames entre movimientos de pelota (~12 Hz) +TICS_PALA = 2 -- auto-repeat del input de pala + +-- Pelotas iniciales (vidas) +PELOTAS_INI = 5 + +-- Puntuaciones por fila de ladrillo (se asignan en FILAS_LADRILLOS abajo) + +-- Duraciones de transición (ms, vía time()) +MS_PERDIDA = 800 +MS_PANTALLA = 600 +MS_GAMEOVER_MIN = 800 + +-- ============================================================ +-- PALETA (mapeo aproximado CPC firmware → CGA disponible) +-- ============================================================ +COL_FONDO = COLOR_BLUE -- el original usa INK 0,1 = azul oscuro +COL_PELOTA = COLOR_WHITE +COL_PALA = COLOR_LIGHT_GRAY +COL_PARED = COLOR_LIGHT_GRAY +COL_TEXTO = COLOR_LIGHT_GRAY +COL_TITULO = COLOR_YELLOW +COL_PROMPT = COLOR_LIGHT_GREEN +COL_RECORD = COLOR_YELLOW +COL_GAMEOVER = COLOR_LIGHT_RED +COL_HUD_FG = COLOR_LIGHT_GRAY +COL_HUD_BG = COLOR_BLACK -- HUD sobre la misma franja del juego + +-- Colores de las 3 "categorías" de ladrillos del original: +-- PEN 3 (naranja, fila 1, 20 pts), PEN 2 (cyan, fila 2, 10 pts), PEN 1 (claro, filas 5-6, 5 pts) +COL_LADR_TOP = COLOR_LIGHT_RED -- naranja ≈ rojo claro +COL_LADR_MID = COLOR_LIGHT_CYAN -- pastel cyan +COL_LADR_LOW = COLOR_WHITE -- pastel claro + +-- ============================================================ +-- GLIFOS (códigos que vamos a redefinir con setchar) +-- ============================================================ +LADRILLO = 143 +PALA_GLIFO = 131 +PARED_GLIFO = 149 -- decorativa (cols 0 y 39) +-- PELOTA_GLIFO ya definido arriba (231) + +-- ============================================================ +-- TIPOS DE CELDA +-- ============================================================ +T_VACIO = 0 +T_LADRILLO = 1 + +-- ============================================================ +-- ESTADOS DEL JUEGO +-- ============================================================ +ESTADO_TITULO = "titulo" +ESTADO_INSTRUC = "instruc" +ESTADO_JUEGO = "juego" +ESTADO_PERDIDA = "perdida" +ESTADO_PANTALLA = "pantalla" -- pantalla limpiada: pausa breve y regenera +ESTADO_GAMEOVER = "gameover" + +-- ============================================================ +-- ESTADO GLOBAL +-- ============================================================ +mapa = {} +pelota = { x=0, y=0, xa=1, ya=1, ultimo_tic=0 } +pala = { x=PALA_X_INI, ultimo_tic=0 } +pelotas = PELOTAS_INI +puntos = 0 +maximo = 0 +nom_record = "AAA" +ladrillos_vivos = 0 + +estado = ESTADO_TITULO +estado_t0_ms = 0 +estado_t0_fr = 0 + +record_letra_idx = 1 +hay_nuevo_record = false + +-- ============================================================ +-- UTILIDADES DE ESTADO +-- ============================================================ +function set_estado(nuevo) + estado = nuevo + estado_t0_ms = time() + estado_t0_fr = cnt() +end + +function t_estado_ms() return time() - estado_t0_ms end +function t_estado_fr() return cnt() - estado_t0_fr end + +-- ============================================================ +-- GLIFOS PERSONALIZADOS +-- ============================================================ +function definir_glifos() + -- Ladrillo: macizo con junta horizontal (filas 2 y 5 vacías para sugerir mortero) + setchar(LADRILLO, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF) + -- Pala: superficie convexa, cuerpo macizo + setchar(PALA_GLIFO, 0x00, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x00) + -- Pelota: esfera pequeña + setchar(PELOTA_GLIFO,0x00, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x00) + -- Pared decorativa: trazo vertical central + setchar(PARED_GLIFO, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x18) +end + +-- ============================================================ +-- INICIALIZACIÓN DEL MAPA +-- Cada celda guarda { tipo, color, puntos }. Las celdas vacías +-- tienen puntos=0 para simplificar el código de colisión. +-- ============================================================ +function init_mapa() + for x = 0, ANCHO-1 do + mapa[x] = {} + for y = 0, ALTO-1 do + mapa[x][y] = { tipo=T_VACIO, color=COL_FONDO, puntos=0 } + end + end +end + +-- Las filas de ladrillos del original. Se llama después de cargar la paleta. +function configurar_filas() + FILAS_LADRILLOS = { + { fila=1, color=COL_LADR_TOP, puntos=20 }, + { fila=2, color=COL_LADR_MID, puntos=10 }, + { fila=5, color=COL_LADR_LOW, puntos=5 }, + { fila=6, color=COL_LADR_LOW, puntos=5 }, + } +end + +function generar_ladrillos() + init_mapa() + ladrillos_vivos = 0 + for _, fl in ipairs(FILAS_LADRILLOS) do + for x = PELOTA_X_MIN, PELOTA_X_MAX do + mapa[x][fl.fila] = { tipo=T_LADRILLO, color=fl.color, puntos=fl.puntos } + ladrillos_vivos = ladrillos_vivos + 1 + end + end +end + +function tipo_en(x, y) + if x < 0 or x >= ANCHO or y < 0 or y >= ALTO then return T_VACIO end + return mapa[x][y].tipo +end + +-- ============================================================ +-- RENDER +-- ============================================================ +function pintar_fondo() + color(COL_TEXTO, COL_FONDO) + cls() +end + +function pintar_paredes() + color(COL_PARED, COL_FONDO) + for y = 0, PALA_Y do + print(chr(PARED_GLIFO), 0, y) + print(chr(PARED_GLIFO), ANCHO - 1, y) + end +end + +function pintar_mapa() + for x = 0, ANCHO-1 do + for y = 0, ALTO-1 do + local c = mapa[x][y] + if c.tipo == T_LADRILLO then + color(c.color, COL_FONDO) + print(chr(LADRILLO), x, y) + end + end + end +end + +function pintar_pala() + color(COL_PALA, COL_FONDO) + for i = 0, PALA_W - 1 do + print(chr(PALA_GLIFO), pala.x + i, PALA_Y) + end +end + +function pintar_pelota() + color(COL_PELOTA, COL_FONDO) + print(chr(PELOTA_GLIFO), pelota.x, pelota.y) +end + +function pintar_hud() + color(COL_HUD_FG, COL_HUD_BG) + local blank = "" + for i = 1, ANCHO do blank = blank .. " " end + print(blank, 0, ALTO - 1) + print("Maximo "..string.format("%04d", maximo), 1, ALTO - 1) + print("Puntos "..string.format("%04d", puntos), 15, ALTO - 1) + print("Pelotas "..tostr(pelotas), 29, ALTO - 1) +end + +-- ============================================================ +-- LÓGICA DE LA PALA +-- ============================================================ +function tic_pala() + if (cnt() - pala.ultimo_tic) < TICS_PALA then return end + if btn(KEY_LEFT) or btn(KEY_O) then + pala.x = pala.x - PALA_STEP + if pala.x < PALA_X_MIN then pala.x = PALA_X_MIN end + pala.ultimo_tic = cnt() + elseif btn(KEY_RIGHT) or btn(KEY_P) then + pala.x = pala.x + PALA_STEP + if pala.x > PALA_X_MAX then pala.x = PALA_X_MAX end + pala.ultimo_tic = cnt() + end +end + +function pelota_sobre_pala(px, py) + return py == PALA_Y and px >= pala.x and px < pala.x + PALA_W +end + +-- ============================================================ +-- LÓGICA DE LA PELOTA +-- ============================================================ +function reset_pelota() + pelota.x = pala.x + flr(PALA_W / 2) + pelota.y = PELOTA_Y_INI + pelota.xa = (rnd(2) == 0) and 1 or -1 + pelota.ya = 1 + pelota.ultimo_tic = cnt() +end + +function destruir_ladrillo(x, y) + local m = mapa[x][y] + puntos = puntos + m.puntos + mapa[x][y] = { tipo=T_VACIO, color=COL_FONDO, puntos=0 } + ladrillos_vivos = ladrillos_vivos - 1 + sfx_ladrillo() +end + +function tic_pelota() + local nx = pelota.x + pelota.xa + local ny = pelota.y + pelota.ya + + -- Rebote en paredes laterales + if nx < PELOTA_X_MIN then + nx = PELOTA_X_MIN + pelota.xa = -pelota.xa + sfx_pared() + elseif nx > PELOTA_X_MAX then + nx = PELOTA_X_MAX + pelota.xa = -pelota.xa + sfx_pared() + end + + -- Rebote en techo + if ny < TECHO_Y then + ny = TECHO_Y + pelota.ya = -pelota.ya + sfx_pared() + end + + -- Rebote en pala + if pelota_sobre_pala(nx, ny) then + pelota.ya = -pelota.ya + ny = PALA_Y - 1 + sfx_pala() + -- Efecto angular: si la pelota venía de fuera de la pala, invierte xa + if pelota.x < pala.x or pelota.x >= pala.x + PALA_W then + pelota.xa = -pelota.xa + -- Re-ajustar nx con el xa nuevo, sin pasar de paredes + nx = pelota.x + pelota.xa + if nx < PELOTA_X_MIN then nx = PELOTA_X_MIN end + if nx > PELOTA_X_MAX then nx = PELOTA_X_MAX end + end + end + + -- Colisión con ladrillo en (nx, ny) + if tipo_en(nx, ny) == T_LADRILLO then + destruir_ladrillo(nx, ny) + pelota.ya = -pelota.ya + -- Mantener nx, ny pero anular el movimiento vertical "dentro" del ladrillo: + -- la pelota rebota antes de entrar, así que ny se reposiciona una fila atrás. + ny = pelota.y + end + + -- Pelota perdida: ha caído por debajo de la línea de la pala + if ny > PALA_Y then + pelota.x = nx + pelota.y = ny + pelota_perdida() + return + end + + pelota.x = nx + pelota.y = ny +end + +function pelota_perdida() + pelotas = pelotas - 1 + sfx_perdida() + if pelotas <= 0 then + if puntos > maximo then + maximo = puntos + nom_record = "AAA" + record_letra_idx = 1 + hay_nuevo_record = true + else + hay_nuevo_record = false + end + sfx_gameover() + set_estado(ESTADO_GAMEOVER) + else + set_estado(ESTADO_PERDIDA) + end +end + +-- ============================================================ +-- TRANSICIONES Y PARTIDA +-- ============================================================ +function nueva_partida() + puntos = 0 + pelotas = PELOTAS_INI + pala.x = PALA_X_INI + generar_ladrillos() + reset_pelota() +end + +function siguiente_pantalla() + generar_ladrillos() + pala.x = PALA_X_INI + reset_pelota() +end + +-- ============================================================ +-- RECORDS (igual patrón que bombardero: 5 bytes score + 3 bytes nombre) +-- ============================================================ +function cargar_record() + local f = io.open("records", "rb") + if not f then return end + local data = f:read(8) + f:close() + if not data or #data < 8 then return end + local b = { string.byte(data, 1, 8) } + maximo = b[1]*10000 + b[2]*1000 + b[3]*100 + b[4]*10 + b[5] + if b[6] >= 32 and b[6] < 127 + and b[7] >= 32 and b[7] < 127 + and b[8] >= 32 and b[8] < 127 then + nom_record = string.char(b[6], b[7], b[8]) + end +end + +function guardar_record() + local f = io.open("records", "wb") + if not f then return end + local p = maximo + local d5 = flr(p / 10000); p = p - d5*10000 + local d4 = flr(p / 1000); p = p - d4*1000 + local d3 = flr(p / 100); p = p - d3*100 + local d2 = flr(p / 10); p = p - d2*10 + local d1 = p + f:write(string.char(d5, d4, d3, d2, d1, + string.byte(nom_record, 1) or 65, + string.byte(nom_record, 2) or 65, + string.byte(nom_record, 3) or 65)) + f:close() +end + +-- ============================================================ +-- SFX +-- ============================================================ +function sfx_pala() sound(800, 3) end +function sfx_pared() sound(400, 2) end +function sfx_ladrillo() sound(1600, 4) end +function sfx_perdida() play("l1o3cba") end +function sfx_select() sound(2000, 3) end +function sfx_gameover() play("l2o3bal1gfedco2c") end +function sfx_pantalla() play("l1o4ceg>c") end + +-- ============================================================ +-- ESTADOS — TÍTULO +-- ============================================================ +function update_titulo() + pintar_fondo() + + color(COL_TITULO, COL_FONDO) + print("R E B O T E S", 13, 4) + color(COL_TEXTO, COL_FONDO) + print("(Alexander Martin / AMSOFT 1984)", 4, 6) + print("port a ascii — Sergi Valor, 2026", 4, 8) + + -- Filitas de ladrillos decorativas + color(COL_LADR_TOP, COL_FONDO) + for x = 14, 25 do print(chr(LADRILLO), x, 11) end + color(COL_LADR_MID, COL_FONDO) + for x = 14, 25 do print(chr(LADRILLO), x, 12) end + color(COL_LADR_LOW, COL_FONDO) + for x = 14, 25 do print(chr(LADRILLO), x, 13) end + + color(COL_PALA, COL_FONDO) + for i = 0, PALA_W - 1 do print(chr(PALA_GLIFO), 18 + i, 16) end + color(COL_PELOTA, COL_FONDO) + print(chr(PELOTA_GLIFO), 19, 15) + + color(COL_RECORD, COL_FONDO) + print("RECORD "..string.format("%04d", maximo).." "..nom_record, 12, 19) + + if (cnt() // 30) % 2 == 0 then + color(COL_PROMPT, COL_FONDO) + print("Pulsa ESPACIO para jugar", 8, 22) + end + color(COL_TEXTO, COL_FONDO) + print("I = instrucciones", 11, 24) + + if btnp(KEY_SPACE) then + sfx_select() + nueva_partida() + set_estado(ESTADO_JUEGO) + elseif btnp(KEY_I) then + sfx_select() + set_estado(ESTADO_INSTRUC) + end +end + +-- ============================================================ +-- ESTADOS — INSTRUCCIONES +-- ============================================================ +function update_instruc() + pintar_fondo() + color(COL_TITULO, COL_FONDO) + print("- INSTRUCCIONES -", 11, 1) + + color(COL_TEXTO, COL_FONDO) + print("Mueve la pala con las flechas", 5, 4) + print("izquierda y derecha (o O / P).", 5, 5) + + print("Rompe los ladrillos con la", 5, 8) + print("pelota. Puntos por fila:", 5, 9) + + color(COL_LADR_TOP, COL_FONDO) + print(chr(LADRILLO).." 20 puntos (fila superior)", 5, 11) + color(COL_LADR_MID, COL_FONDO) + print(chr(LADRILLO).." 10 puntos (segunda fila)", 5, 12) + color(COL_LADR_LOW, COL_FONDO) + print(chr(LADRILLO)..chr(LADRILLO).." 5 puntos (filas bajas)", 5, 13) + + color(COL_TEXTO, COL_FONDO) + print("Tienes "..tostr(PELOTAS_INI).." pelotas.", 5, 16) + print("Limpia la pantalla y se", 5, 18) + print("regenera con otra tanda.", 5, 19) + + if (cnt() // 30) % 2 == 0 then + color(COL_PROMPT, COL_FONDO) + print("Pulsa una tecla para volver", 6, 23) + end + + if btnp(KEY_SPACE) or btnp(KEY_RETURN) or btnp(KEY_ESCAPE) then + sfx_select() + set_estado(ESTADO_TITULO) + end +end + +-- ============================================================ +-- ESTADOS — JUEGO +-- ============================================================ +function update_juego() + tic_pala() + + if (cnt() - pelota.ultimo_tic) >= TICS_PELOTA then + pelota.ultimo_tic = cnt() + tic_pelota() + if estado ~= ESTADO_JUEGO then return end + if ladrillos_vivos <= 0 then + sfx_pantalla() + set_estado(ESTADO_PANTALLA) + return + end + end + + pintar_fondo() + pintar_paredes() + pintar_mapa() + pintar_pala() + pintar_pelota() + pintar_hud() +end + +-- ============================================================ +-- ESTADOS — PELOTA PERDIDA (pausa breve antes de lanzar la siguiente) +-- ============================================================ +function update_perdida() + pintar_fondo() + pintar_paredes() + pintar_mapa() + pintar_pala() + pintar_hud() + + color(COL_GAMEOVER, COL_FONDO) + print(" PELOTA PERDIDA ", 12, 14) + + if t_estado_ms() >= MS_PERDIDA then + reset_pelota() + set_estado(ESTADO_JUEGO) + end +end + +-- ============================================================ +-- ESTADOS — PANTALLA LIMPIADA +-- ============================================================ +function update_pantalla() + pintar_fondo() + pintar_paredes() + pintar_pala() + pintar_hud() + + color(COL_PROMPT, COL_FONDO) + print(" PANTALLA LIMPIA! ", 11, 14) + + if t_estado_ms() >= MS_PANTALLA then + siguiente_pantalla() + set_estado(ESTADO_JUEGO) + end +end + +-- ============================================================ +-- ESTADOS — GAME OVER (+ entrada de nombre si récord) +-- ============================================================ +function update_gameover() + pintar_fondo() + pintar_paredes() + pintar_mapa() + pintar_pala() + pintar_hud() + + color(COL_GAMEOVER, COL_FONDO) + print(" G A M E O V E R ", 10, 12) + + color(COL_TEXTO, COL_FONDO) + print("Puntos: "..string.format("%04d", puntos), 14, 15) + + if hay_nuevo_record then + color(COL_RECORD, COL_FONDO) + print("NUEVO RECORD!", 13, 17) + print("Nombre: "..nom_record, 14, 19) + if (cnt() // 30) % 2 == 0 then + color(COL_PROMPT, COL_FONDO) + print("Pulsa A..Z (3 letras)", 9, 21) + end + + if t_estado_ms() >= MS_GAMEOVER_MIN then + for sc = KEY_A, KEY_Z do + if btnp(sc) then + local lt = string.char(65 + sc - KEY_A) + nom_record = string.sub(nom_record, 1, record_letra_idx-1) + ..lt.. + string.sub(nom_record, record_letra_idx+1) + record_letra_idx = record_letra_idx + 1 + sfx_select() + if record_letra_idx > 3 then + guardar_record() + hay_nuevo_record = false + set_estado(ESTADO_TITULO) + end + return + end + end + end + else + if (cnt() // 30) % 2 == 0 then + color(COL_PROMPT, COL_FONDO) + print("Pulsa ESPACIO para volver", 8, 20) + end + if t_estado_ms() >= MS_GAMEOVER_MIN and btnp(KEY_SPACE) then + set_estado(ESTADO_TITULO) + end + end +end + +-- ============================================================ +-- BUCLE PRINCIPAL +-- ============================================================ +function init() + mode(MODO) + border(COL_FONDO) + color(COL_TEXTO, COL_FONDO) + definir_glifos() + configurar_filas() + init_mapa() + cargar_record() + set_estado(ESTADO_TITULO) + cls() +end + +function update() + if estado == ESTADO_TITULO then update_titulo() + elseif estado == ESTADO_INSTRUC then update_instruc() + elseif estado == ESTADO_JUEGO then update_juego() + elseif estado == ESTADO_PERDIDA then update_perdida() + elseif estado == ESTADO_PANTALLA then update_pantalla() + elseif estado == ESTADO_GAMEOVER then update_gameover() + end +end