Files
pepe-runner-ascii/pepe_runner.lua
T
2026-05-15 10:22:52 +02:00

411 lines
13 KiB
Lua

-- Pepe Runner — port a ascii/Lua del joc original en Turbo Pascal (JailDesigner, 2000)
-- Fase 3: gravetat, escales, cordes, forats
-- Codis CP437 dels sprites del joc original (de TIPOS.PAS)
BUIT = 0
DINERS = 36 -- $
PEDRA = 219 -- bloc solid
ESCALA = 205 -- ═ (linia doble horitzontal, formant graons d'escala)
CORDA = 196 -- ─ (linia simple horitzontal)
BLOC1 = 176 -- ░
BLOC2 = 177 -- ▒
BLOC3 = 178 -- ▓
PEPE_C = 2 -- sprite del Pepe
MALO_C = 88 -- 'X' enemics
-- Colors (de TIPOS.PAS, paleta CGA — coincideix amb la d'ascii)
COL_PEDRA = COLOR_BROWN -- 6
COL_DINERS = COLOR_YELLOW -- 14
COL_ESCALA = COLOR_LIGHT_GRAY -- 7
COL_CORDA = COLOR_LIGHT_GRAY -- 7
COL_BUIT = COLOR_BLACK -- 0
-- Estats (de TIPOS.PAS — son bitflags per a SelectEstat dels enemics)
NORMAL = 0
PUJAR = 0x01
BAIXAR = 0x02
CAENT = 4
ESQUERRA = 0x10
DRETA = 0x20
-- Constants del joc
MAP_W = 40
MAP_H = 25
BLOC_OUT = 100 -- temps que dura un forat obert (de TIPOS.PAS)
TICS = 6 -- frames per tick de joc (60fps / 6 = 10 Hz)
NUM_MALOS = 3
TEMPS_IA = 30 -- iteracions del malo entre canvis de direccio
MALO_RATIO = 4 -- els malos van 1/4 del ritme del Pepe (com en RUNNER.PAS)
-- Estat global
mapa = {} -- mapa[x][y] = { tipo=, color=, temps= }
level = 1
pepe = { x=19, y=23, dibuix=PEPE_C, color=COLOR_WHITE, vides=0, estat=NORMAL }
malos = {}
score = 0
diners_pantalla = 0
game_tic = 0
function definir_glifs()
setchar(BUIT, 0,0,0,0,0,0,0,0)
setchar(PEDRA, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF)
setchar(DINERS, 0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00)
setchar(ESCALA, 0x00,0x00,0xFF,0x00,0x00,0xFF,0x00,0x00)
setchar(CORDA, 0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00)
setchar(BLOC1, 0x44,0x11,0x44,0x11,0x44,0x11,0x44,0x11)
setchar(BLOC2, 0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55)
setchar(BLOC3, 0xBB,0xEE,0xBB,0xEE,0xBB,0xEE,0xBB,0xEE)
setchar(PEPE_C, 0x7E,0x81,0xA5,0x81,0xBD,0x99,0x81,0x7E)
setchar(MALO_C, 0x00,0xC3,0x66,0x3C,0x18,0x3C,0x66,0xC3)
end
function color_de(tipo)
if tipo == PEDRA then return COL_PEDRA end
if tipo == DINERS then return COL_DINERS end
if tipo == ESCALA then return COL_ESCALA end
if tipo == CORDA then return COL_CORDA end
return COL_BUIT
end
-- Helper segur per llegir el tipus d'una cel·la (fora de mapa = pedra virtual)
function tipo_a(x, y)
if x < 0 or x >= MAP_W or y < 0 or y >= MAP_H then return PEDRA end
return mapa[x][y].tipo
end
function carregar_mapa(num)
filein("maps/"..tostr(num)..".map", 0, MAP_W*MAP_H)
diners_pantalla = 0
for x = 0, MAP_W-1 do
mapa[x] = {}
for y = 0, MAP_H-1 do
local tipo = peek(x*MAP_H + y)
mapa[x][y] = { tipo=tipo, color=color_de(tipo), temps=-1 }
if tipo == DINERS then diners_pantalla = diners_pantalla + 1 end
end
end
end
function pintar_mapa()
for x = 0, MAP_W-1 do
for y = 0, MAP_H-1 do
local c = mapa[x][y]
if c.tipo ~= BUIT then
color(c.color, COL_BUIT)
print(chr(c.tipo), x, y)
end
end
end
end
function pintar_pepe()
color(pepe.color, COL_BUIT)
print(chr(pepe.dibuix), pepe.x, pepe.y)
end
-- Marca una cel·la com a forat (sols si actualment es pedra)
function foradar(x, y)
if tipo_a(x, y) == PEDRA then
mapa[x][y].temps = BLOC_OUT
end
end
-- Pot Pepe cavar a esquerra/dreta? Condicions del MouPepe original:
-- - la cel·la diagonal-baix ha de ser pedra (per a obrir-hi forat)
-- - la cel·la lateral no pot ser pedra (per a que Pepe s'hi puga assomar)
-- - Pepe ha d'estar en estat normal (no caent)
function pot_cavar(dx)
return pepe.estat == NORMAL
and tipo_a(pepe.x+dx, pepe.y+1) == PEDRA
and tipo_a(pepe.x+dx, pepe.y) ~= PEDRA
end
-- Tic de joc del Pepe: input de moviment, gravetat, recollir diners, emparedat
function tic_pepe()
local actual = tipo_a(pepe.x, pepe.y)
local sotto = tipo_a(pepe.x, pepe.y+1)
-- Moviment vertical: Q/A (com en RUNNER.PAS, son if/else)
if btn(KEY_Q) then
if actual == ESCALA then pepe.y = pepe.y - 1 end
elseif btn(KEY_A) then
if sotto == ESCALA or sotto == BUIT or sotto == DINERS then
pepe.y = pepe.y + 1
end
end
-- Moviment horitzontal: O/P (no es pot moure si esta caent)
if btn(KEY_O) then
if tipo_a(pepe.x-1, pepe.y) ~= PEDRA and pepe.estat ~= CAENT then
pepe.x = pepe.x - 1
end
elseif btn(KEY_P) then
if tipo_a(pepe.x+1, pepe.y) ~= PEDRA and pepe.estat ~= CAENT then
pepe.x = pepe.x + 1
end
end
-- Si no passa res especial, estat = normal (gravetat pot canviar-ho mes avall)
pepe.estat = NORMAL
-- Emparedat: si la cel·la actual s'ha tornat pedra, Pepe mor
if tipo_a(pepe.x, pepe.y) == PEDRA then
mort_pepe()
return
end
-- Recollir diners
if tipo_a(pepe.x, pepe.y) == DINERS then
mapa[pepe.x][pepe.y].tipo = BUIT
score = score + 1
diners_pantalla = diners_pantalla - 1
end
-- Bordes X
if pepe.x < 0 then pepe.x = 0 end
if pepe.x > MAP_W-1 then pepe.x = MAP_W-1 end
-- Gravetat: si la cel·la actual es buit/diners i la de baix no es escala/pedra → cau
actual = tipo_a(pepe.x, pepe.y)
sotto = tipo_a(pepe.x, pepe.y+1)
if (sotto ~= ESCALA and sotto ~= PEDRA)
and (actual == BUIT or actual == DINERS) then
pepe.y = pepe.y + 1
pepe.estat = CAENT
end
-- Bordes Y
if pepe.y < 0 then pepe.y = 0 end
if pepe.y > MAP_H-1 then pepe.y = MAP_H-1 end
end
-- De moment, mort = respawn (vides i game over van en la Fase 5)
function mort_pepe()
pepe.vides = pepe.vides - 1
pepe.x = 19
pepe.y = 23
pepe.estat = NORMAL
end
-- ====================================================================
-- ENEMICS
-- ====================================================================
function init_malos()
malos = {
{ x= 9, y=2, color=COLOR_CYAN, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
{ x=20, y=2, color=COLOR_CYAN, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
{ x=39, y=2, color=COLOR_CYAN, estat=ESQUERRA, iaclock=0, carrega={ok=false, x=0, y=0} },
}
end
function pintar_malos()
for i = 1, NUM_MALOS do
local m = malos[i]
color(m.color, COL_BUIT)
print(chr(MALO_C), m.x, m.y)
end
end
-- Tria una nova direccio per a un enemic (port de SelectEstat de RUNNER.PAS).
-- 50% prob persegueix Pepe, 50% prob direccio aleatoria entre les valides.
-- Si no te suport sota els peus, override a CAENT.
function select_estat(m)
local nou = 0
if tipo_a(m.x+1, m.y) ~= PEDRA then nou = nou | DRETA end
if tipo_a(m.x-1, m.y) ~= PEDRA then nou = nou | ESQUERRA end
if tipo_a(m.x, m.y) == ESCALA then nou = nou | PUJAR end
if tipo_a(m.x, m.y+1) == ESCALA then nou = nou | BAIXAR end
local sestat = 0
if nou == 0 then sestat = 10 end -- atrapat: valor que cap case reconeix
local pX = (m.x > pepe.x) and ESQUERRA or DRETA
local pY = (m.y > pepe.y) and PUJAR or BAIXAR
if rnd(100) < 50 and (((nou & pX) == pX) or ((nou & pY) == pY)) then
if (nou & pX) == pX then sestat = pX else sestat = pY end
else
local x = rnd(4)
while sestat == 0 do
if x == 0 and (nou & DRETA) == DRETA then sestat = DRETA
elseif x == 1 and (nou & ESQUERRA) == ESQUERRA then sestat = ESQUERRA
elseif x == 2 and (nou & PUJAR) == PUJAR then sestat = PUJAR
elseif x == 3 and (nou & BAIXAR) == BAIXAR then sestat = BAIXAR
end
x = (x + 1) & 3
end
end
-- override de caiguda (igual que en el Pascal — no comprova corda aci,
-- la comprovacio amb corda es fa al tic_malos)
local sotto = tipo_a(m.x, m.y+1)
if sotto ~= PEDRA and sotto ~= ESCALA then sestat = CAENT end
return sestat
end
-- Si va horitzontal i hi ha escala adalt/abaix, 80% prob s'enganxa
function agafar_escala(m)
if m.estat == DRETA or m.estat == ESQUERRA then
if rnd(100) < 80 then
if tipo_a(m.x, m.y) == ESCALA then return PUJAR
elseif tipo_a(m.x, m.y+1) == ESCALA then return BAIXAR
end
end
end
return m.estat
end
-- Mort d'un enemic (per emparedat). Respawn a (39, 1).
-- A diferencia del Pascal, sols solta diners si en duia (l'original sempre
-- escrivia diners a (carrega.x, carrega.y), deixant un $ a (0,0) com a bug).
function mort_malo(m)
if m.carrega.ok then
local c = mapa[m.carrega.x][m.carrega.y]
c.tipo = DINERS
c.color = COL_DINERS
diners_pantalla = diners_pantalla + 1
end
m.x = 39; m.y = 1
m.color = COLOR_CYAN
m.estat = CAENT
m.iaclock = 0
m.carrega.ok = false
m.carrega.x = 0
m.carrega.y = 0
end
function tic_malos()
for i = 1, NUM_MALOS do
local m = malos[i]
if m.iaclock == 0 then m.estat = select_estat(m) end
m.estat = agafar_escala(m)
local actual = tipo_a(m.x, m.y)
local sotto = tipo_a(m.x, m.y+1)
-- caiguda (aquesta SI comprova corda — els malos s'agafen a la corda)
if sotto ~= PEDRA and sotto ~= ESCALA and actual ~= CORDA then
m.estat = CAENT
end
-- si toca terra i venia caent → reconsidera
if (sotto == PEDRA or sotto == ESCALA) and m.estat == CAENT then
m.estat = select_estat(m)
end
-- si vol pujar pero no esta en escala → reconsidera
if actual == BUIT and m.estat == PUJAR then
m.estat = select_estat(m)
end
-- si vol baixar pero te pedra sota → reconsidera
if sotto == PEDRA and m.estat == BAIXAR then
m.estat = select_estat(m)
end
-- aplicar moviment
if m.estat == DRETA then m.x = m.x + 1
elseif m.estat == ESQUERRA then m.x = m.x - 1
elseif m.estat == PUJAR then m.y = m.y - 1
elseif m.estat == BAIXAR then m.y = m.y + 1
elseif m.estat == CAENT then m.y = m.y + 1
end
-- bordes X (rebot)
if m.x < 0 then m.x = 0; m.estat = DRETA end
if m.x > MAP_W-1 then m.x = MAP_W-1; m.estat = ESQUERRA end
-- bordes Y (clamp)
if m.y < 0 then m.y = 0 end
if m.y > MAP_H-1 then m.y = MAP_H-1 end
-- agafar diners
if tipo_a(m.x, m.y) == DINERS and not m.carrega.ok then
mapa[m.x][m.y].tipo = BUIT
m.color = COLOR_LIGHT_CYAN
m.carrega.ok = true
m.carrega.x = m.x
m.carrega.y = m.y
end
-- emparedat → mort
if tipo_a(m.x, m.y) == PEDRA then
mort_malo(m)
end
m.iaclock = m.iaclock + 1
if m.iaclock == TEMPS_IA then m.iaclock = 0 end
end
end
function check_mort_per_malos()
for i = 1, NUM_MALOS do
if malos[i].x == pepe.x and malos[i].y == pepe.y then
mort_pepe()
return
end
end
end
-- Anima els forats: decrementa temps i cambia el tipus segons la fase
-- (idem case statement de CheckMapa al RUNNER.PAS)
function check_mapa()
for x = 0, MAP_W-1 do
for y = 0, MAP_H-1 do
local c = mapa[x][y]
local t = c.temps
if t == 0 then
c.temps = -1
c.tipo = PEDRA
c.color = COL_PEDRA
elseif t == 1 or t == BLOC_OUT-1 then
c.tipo = BLOC3; c.color = COL_PEDRA; c.temps = t - 1
elseif t == 2 or t == BLOC_OUT-2 then
c.tipo = BLOC2; c.color = COL_PEDRA; c.temps = t - 1
elseif t == 3 or t == BLOC_OUT-3 then
c.tipo = BLOC1; c.color = COL_PEDRA; c.temps = t - 1
elseif t == 4 or t == BLOC_OUT-4 then
c.tipo = BUIT; c.color = COL_BUIT; c.temps = t - 1
elseif t > 0 then
c.temps = t - 1
end
-- t == -1 → idle, no fer res
end
end
end
function init()
mode(1)
border(COLOR_BLUE)
color(COLOR_LIGHT_GRAY, COLOR_BLACK)
definir_glifs()
carregar_mapa(level)
init_malos()
cls()
end
function update()
-- Cavar es immediat (un sol forat per pulsacio)
if pepe.estat == NORMAL then
if btnp(KEY_SPACE) and pot_cavar(-1) then foradar(pepe.x-1, pepe.y+1) end
if btnp(KEY_M) and pot_cavar( 1) then foradar(pepe.x+1, pepe.y+1) end
end
-- Logica del joc: cada TICS frames
if (cnt() % TICS) == 0 then
game_tic = game_tic + 1
tic_pepe()
check_mort_per_malos()
if (game_tic % MALO_RATIO) == 0 then
tic_malos()
check_mort_per_malos()
end
check_mapa()
end
-- Render: cada frame
cls()
pintar_mapa()
pintar_malos()
pintar_pepe()
end