-- 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) NUM_FASES = 10 -- mapes 1..10 (el 0 esta reservat per al titol) VIDES_INI = 3 -- l'original arrancava amb 0 (1 vida); 3 es mes raonable -- Estats del joc (maquina d'estats global) ESTAT_TITLE = "title" ESTAT_PLAYING = "playing" ESTAT_GAMEOVER = "gameover" ESTAT_ENTERNAME = "entername" -- Estat global mapa = {} -- mapa[x][y] = { tipo=, color=, temps= } level = 1 pepe = { x=19, y=23, dibuix=PEPE_C, color=COLOR_WHITE, vides=VIDES_INI, estat=NORMAL } malos = {} score = 0 diners_pantalla = 0 game_tic = 0 hi_score = 0 nom_hi_score = "AAA" estat_joc = ESTAT_TITLE estat_inici = 0 enter_name_idx = 1 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 -- ==================================================================== -- SFX (l'original no tenia so — afegim els minims raonables) -- ==================================================================== function sfx_coin() sound(2000, 40) end function sfx_dig() play("l0o2c") end function sfx_die() play("l1o4cl0o3bagfed") end function sfx_malo_die() play("l0o3ao4c") end function sfx_level() play("l1o4ceg") end function sfx_gameover() play("l1o3bal0gfedco2c") 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 -- Final pantalla: si arriba a la fila 1, passa al nivel seguent if pepe.y == 1 then fase_nova() return end -- 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 sfx_coin() 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 function mort_pepe() pepe.vides = pepe.vides - 1 pepe.x = 19 pepe.y = 23 pepe.estat = NORMAL sfx_die() end -- Inicialitza tot per a una nova partida (reset complet) function inicialitzacio() level = 1 score = 0 pepe.vides = VIDES_INI pepe.x = 19; pepe.y = 23; pepe.estat = NORMAL carregar_mapa(level) init_malos() game_tic = 0 end -- Avanca al nivell seguent (sense reset de score ni vides) function fase_nova() level = level + 1 if level > NUM_FASES then level = 1 end pepe.x = 19; pepe.y = 23; pepe.estat = NORMAL carregar_mapa(level) init_malos() sfx_level() end -- ==================================================================== -- ESTATS DEL JOC (title / playing / game over / enter name) -- ==================================================================== function set_estat(nou) estat_joc = nou estat_inici = cnt() end function temps_estat() return cnt() - estat_inici end -- Records I/O. Usem io.open (estandard de Lua) en lloc de filein/fileout -- per a poder gestionar el cas de fitxer inexistent sense petar. -- Format: 6 bytes = 3 (centenes, desenes, unitats del score) + 3 (lletres nom) function carregar_records() local f = io.open("records", "rb") if not f then return end local data = f:read(6) f:close() if not data or #data < 6 then return end local b = { string.byte(data, 1, 6) } hi_score = b[1]*100 + b[2]*10 + b[3] -- Validar que les lletres del nom siguen imprimibles if b[4] >= 32 and b[4] < 127 and b[5] >= 32 and b[5] < 127 and b[6] >= 32 and b[6] < 127 then nom_hi_score = string.char(b[4], b[5], b[6]) end end function guardar_records() local f = io.open("records", "wb") if not f then return end f:write(string.char( flr(hi_score / 100), flr((hi_score % 100) / 10), hi_score % 10, string.byte(nom_hi_score, 1) or 65, string.byte(nom_hi_score, 2) or 65, string.byte(nom_hi_score, 3) or 65 )) f:close() end -- Quina lletra A-Z s'ha pulsat este frame (escaneig manual amb btnp, -- perque ascii no exposa whichbtn() al Lua malgrat estar al ascii.h). function lletra_pulsada() for sc = KEY_A, KEY_Z do if btnp(sc) then return string.char(65 + sc - KEY_A) end end return nil end -- ----- TITLE ----- function init_title() carregar_mapa(0) -- mapa 0 es l'art del titol end function update_title() if btnp(KEY_SPACE) then inicialitzacio() set_estat(ESTAT_PLAYING) return end cls() pintar_mapa() -- "PRESS SPACE TO PLAY" parpadejant if flr(cnt() / 30) % 2 == 0 then color(COLOR_WHITE, COLOR_BLACK) print("PRESS SPACE TO PLAY", 10, 22) end pintar_hud() end -- ----- GAME OVER ----- function update_gameover() -- Render congelat: ultim estat del joc + overlay "GAME OVER" cls() pintar_mapa() pintar_malos() pintar_pepe() color(COLOR_LIGHT_RED, COLOR_BLACK) print("G A M E O V E R", 11, 12) pintar_hud() -- Despres de 2 segons (120 frames), transicio if temps_estat() > 120 then if score > hi_score then hi_score = score nom_hi_score = "AAA" enter_name_idx = 1 set_estat(ESTAT_ENTERNAME) else init_title() set_estat(ESTAT_TITLE) end end end -- ----- ENTER NAME ----- function update_entername() cls() color(COLOR_LIGHT_RED, COLOR_BLACK) print("NOU RECORD!", 14, 10) color(COLOR_YELLOW, COLOR_BLACK) print("SCORE "..string.format("%03d", hi_score), 15, 12) color(COLOR_WHITE, COLOR_BLACK) print("NOM: "..nom_hi_score, 16, 15) print("(A-Z)", 17, 17) pintar_hud() local lletra = lletra_pulsada() if lletra then nom_hi_score = string.sub(nom_hi_score, 1, enter_name_idx-1) ..lletra.. string.sub(nom_hi_score, enter_name_idx+1) enter_name_idx = enter_name_idx + 1 if enter_name_idx > 3 then guardar_records() init_title() set_estat(ESTAT_TITLE) end end end -- ----- PLAYING ----- function update_playing() -- 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); sfx_dig() end if btnp(KEY_M) and pot_cavar( 1) then foradar(pepe.x+1, pepe.y+1); sfx_dig() 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() if pepe.vides < 0 then sfx_gameover() set_estat(ESTAT_GAMEOVER) return end end -- Render cls() pintar_mapa() pintar_malos() pintar_pepe() pintar_hud() 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) -- El comptador diners_pantalla NO canvia: l'enemic agafant/soltant es -- transitori, sols compta el que el Pepe recull definitivament. if m.carrega.ok then local c = mapa[m.carrega.x][m.carrega.y] c.tipo = DINERS c.color = COL_DINERS 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 sfx_malo_die() 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 -- Si Pepe ha recollit tots els diners, fa apareixer una escala a la columna 0 -- des de la fila 1 cap avall, parant si troba pedra. (CheckMapaComplet) function check_mapa_complet() if diners_pantalla > 0 then return end for j = 1, MAP_H-2 do if mapa[0][j].tipo == PEDRA then break end mapa[0][j].tipo = ESCALA mapa[0][j].color = COL_ESCALA 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 check_mapa_complet() end -- HUD: rotul inferior amb level/score/vides/hi-score sobre banda blava function pintar_hud() color(COLOR_LIGHT_GRAY, COLOR_BLUE) local blank = " " print(blank, 0, 25) print(blank, 0, 26) print(blank, 0, 27) print(" LEVEL "..string.format("%02d", level), 0, 26) print("SCORE "..string.format("%03d", score), 14, 26) print("LIVES "..tostr(pepe.vides), 28, 26) print("HI-SCORE "..string.format("%03d", hi_score).." "..nom_hi_score, 9, 27) end function init() wintitle("© 2000 Pepe Runner — JailDesigner") mode(1) border(COLOR_BLUE) color(COLOR_LIGHT_GRAY, COLOR_BLACK) definir_glifs() carregar_records() init_title() set_estat(ESTAT_TITLE) cls() end function update() if btnp(KEY_ESCAPE) then os.exit(0) end if estat_joc == ESTAT_TITLE then update_title() elseif estat_joc == ESTAT_PLAYING then update_playing() elseif estat_joc == ESTAT_GAMEOVER then update_gameover() elseif estat_joc == ESTAT_ENTERNAME then update_entername() end end