From bd42e74360d956a5e1ff3547c249a247b760108b Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Thu, 4 Dec 2025 11:22:37 +0100 Subject: [PATCH] rewrite to lua --- bomberman.lua | 654 ++++++++++++++++++++++++++++++++++++++++++ bomberman.rb | 768 -------------------------------------------------- 2 files changed, 654 insertions(+), 768 deletions(-) create mode 100644 bomberman.lua delete mode 100644 bomberman.rb diff --git a/bomberman.lua b/bomberman.lua new file mode 100644 index 0000000..32e26d6 --- /dev/null +++ b/bomberman.lua @@ -0,0 +1,654 @@ +-- title: Bomberman Clone +-- author: Zsolt Tasnadi +-- desc: Simple Bomberman clone for TIC-80 +-- site: http://teletype.hu +-- license: MIT License +-- version: 0.1 +-- script: lua + +-- constants +TILE_SIZE = 16 +PLAYER_SIZE = 12 +BOMB_TIMER = 90 +EXPLOSION_TIMER = 30 + +EMPTY = 0 +SOLID_WALL = 1 +BREAKABLE_WALL = 2 + +MOVE_SPEED = 2 + +-- sprite indices (in SPRITES section, starts at 256) +ASTRONAUT_BLUE = 256 +ASTRONAUT_RED = 257 +BOMB_SPRITE = 258 +BREAKABLE_WALL_SPRITE = 259 +SOLID_WALL_SPRITE = 260 + +-- game state +players = {} +powerups = {} +bombs = {} +explosions = {} +winner = nil +win_timer = 0 +score = {0, 0} + +-- map (1=solid wall, 2=breakable wall) +map = { + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {1,0,0,2,2,2,0,2,0,2,2,2,0,0,1}, + {1,0,1,2,1,2,1,2,1,2,1,2,1,0,1}, + {1,2,2,2,0,2,2,0,2,2,0,2,2,2,1}, + {1,2,1,0,1,0,1,0,1,0,1,0,1,2,1}, + {1,2,2,2,0,2,2,0,2,2,0,2,2,2,1}, + {1,0,1,2,1,2,1,2,1,2,1,2,1,0,1}, + {1,0,0,2,2,2,0,2,0,2,2,2,0,0,1}, + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} +} + +function create_player(gridX, gridY, color, is_ai) + return { + gridX = gridX, + gridY = gridY, + pixelX = (gridX - 1) * TILE_SIZE, + pixelY = (gridY - 1) * TILE_SIZE, + moving = false, + maxBombs = 1, + activeBombs = 0, + color = color, + is_ai = is_ai, + moveTimer = 0, + bombCooldown = 0, + spawnX = gridX, + spawnY = gridY + } +end + +function init_game() + players = {} + table.insert(players, create_player(2, 2, 12, false)) -- human player (blue) + table.insert(players, create_player(14, 8, 2, true)) -- AI enemy (red) + init_powerups() +end + +function init_powerups() + powerups = {} + for row = 1, 9 do + for col = 1, 15 do + if map[row][col] == BREAKABLE_WALL and math.random() < 0.3 then + table.insert(powerups, {gridX = col, gridY = row, type = "bomb"}) + end + end + end +end + +init_game() + +function TIC() + cls(6) -- green background + + if winner then + win_timer = win_timer - 1 + draw_win_screen() + if btnp(4) and win_timer <= 0 then + restart_game() + end + return + end + + -- update all players + for i, player in ipairs(players) do + update_player_movement(player) + if player.is_ai then + update_ai(player) + else + handle_human_input(player) + end + end + + -- update bombs + for i = #bombs, 1, -1 do + local bomb = bombs[i] + bomb.timer = bomb.timer - 1 + if bomb.timer <= 0 then + explode(bomb.x, bomb.y) + if bomb.owner then + bomb.owner.activeBombs = bomb.owner.activeBombs - 1 + end + table.remove(bombs, i) + end + end + + -- update explosions + for i = #explosions, 1, -1 do + local expl = explosions[i] + expl.timer = expl.timer - 1 + if expl.timer <= 0 then + table.remove(explosions, i) + end + end + + -- check powerup pickup + for _, player in ipairs(players) do + for i = #powerups, 1, -1 do + local pw = powerups[i] + if map[pw.gridY][pw.gridX] == EMPTY and + player.gridX == pw.gridX and player.gridY == pw.gridY then + player.maxBombs = player.maxBombs + 1 + table.remove(powerups, i) + end + end + end + + -- check death by explosion + for idx, player in ipairs(players) do + for _, expl in ipairs(explosions) do + local explGridX = math.floor(expl.x / TILE_SIZE) + 1 + local explGridY = math.floor(expl.y / TILE_SIZE) + 1 + if player.gridX == explGridX and player.gridY == explGridY then + local winner_idx = (idx == 1) and 2 or 1 + set_winner(winner_idx) + return + end + end + end + + -- check human death by touching AI + local human = players[1] + for _, player in ipairs(players) do + if player.is_ai and human.gridX == player.gridX and human.gridY == player.gridY then + set_winner(2) + return + end + end + + draw_game() +end + +function draw_game() + -- draw map + for row = 1, 9 do + for col = 1, 15 do + local tile = map[row][col] + local drawX = (col - 1) * TILE_SIZE + local drawY = (row - 1) * TILE_SIZE + if tile == SOLID_WALL then + spr(SOLID_WALL_SPRITE, drawX, drawY, 0, 2) + elseif tile == BREAKABLE_WALL then + spr(BREAKABLE_WALL_SPRITE, drawX, drawY, 0, 2) + end + end + end + + -- draw powerups + for _, pw in ipairs(powerups) do + if map[pw.gridY][pw.gridX] == EMPTY then + local drawX = (pw.gridX - 1) * TILE_SIZE + local drawY = (pw.gridY - 1) * TILE_SIZE + rect(drawX + 3, drawY + 3, 10, 10, 6) + print("B", drawX + 5, drawY + 5, 0) + end + end + + -- draw bombs + for _, bomb in ipairs(bombs) do + draw_bomb_sprite(bomb.x, bomb.y) + end + + -- draw explosions + for _, expl in ipairs(explosions) do + rect(expl.x, expl.y, TILE_SIZE, TILE_SIZE, 6) + end + + -- draw players + for idx, player in ipairs(players) do + draw_player_sprite(player.pixelX, player.pixelY, idx == 1) + end + + -- score display + print(score[1]..":"..score[2], 5, 2, 12) + print("ARROWS:MOVE A:BOMB", 60, 2, 15) + local human = players[1] + local available = human.maxBombs - human.activeBombs + print("BOMBS:"..available.."/"..human.maxBombs, 180, 2, 11) +end + +function set_winner(player_num) + winner = player_num + win_timer = 60 + score[player_num] = score[player_num] + 1 +end + +function draw_win_screen() + cls(0) + rect(20, 30, 200, 80, 12) + rect(22, 32, 196, 76, 0) + print("PLAYER "..winner.." WON!", 70, 55, 12, false, 2) + if win_timer <= 0 or math.floor(win_timer / 15) % 2 == 0 then + print("Press A to restart", 70, 80, 12) + end +end + +function restart_game() + winner = nil + win_timer = 0 + bombs = {} + explosions = {} + + map = { + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, + {1,0,0,2,2,2,0,2,0,2,2,2,0,0,1}, + {1,0,1,2,1,2,1,2,1,2,1,2,1,0,1}, + {1,2,2,2,0,2,2,0,2,2,0,2,2,2,1}, + {1,2,1,0,1,0,1,0,1,0,1,0,1,2,1}, + {1,2,2,2,0,2,2,0,2,2,0,2,2,2,1}, + {1,0,1,2,1,2,1,2,1,2,1,2,1,0,1}, + {1,0,0,2,2,2,0,2,0,2,2,2,0,0,1}, + {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} + } + + for _, p in ipairs(players) do + reset_player_entity(p) + end + init_powerups() +end + +function update_player_movement(player) + local targetX = (player.gridX - 1) * TILE_SIZE + local targetY = (player.gridY - 1) * TILE_SIZE + + if player.pixelX < targetX then + player.pixelX = math.min(player.pixelX + MOVE_SPEED, targetX) + player.moving = true + elseif player.pixelX > targetX then + player.pixelX = math.max(player.pixelX - MOVE_SPEED, targetX) + player.moving = true + elseif player.pixelY < targetY then + player.pixelY = math.min(player.pixelY + MOVE_SPEED, targetY) + player.moving = true + elseif player.pixelY > targetY then + player.pixelY = math.max(player.pixelY - MOVE_SPEED, targetY) + player.moving = true + else + player.moving = false + end + + if player.bombCooldown > 0 then + player.bombCooldown = player.bombCooldown - 1 + end +end + +function handle_human_input(player) + if player.moving then return end + + local newGridX = player.gridX + local newGridY = player.gridY + + if btn(0) then + newGridY = player.gridY - 1 + elseif btn(1) then + newGridY = player.gridY + 1 + elseif btn(2) then + newGridX = player.gridX - 1 + elseif btn(3) then + newGridX = player.gridX + 1 + end + + if can_move_to(newGridX, newGridY) then + player.gridX = newGridX + player.gridY = newGridY + end + + if btnp(4) then + place_bomb(player) + end +end + +function update_ai(player) + if player.moving then return end + + local in_danger = is_dangerous(player.gridX, player.gridY) + + if in_danger then + local dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}} + local best_dir = nil + local best_safe = false + + for _, dir in ipairs(dirs) do + local newX = player.gridX + dir[1] + local newY = player.gridY + dir[2] + if can_move_to(newX, newY) then + local safe = not is_dangerous(newX, newY) + if safe and not best_safe then + best_dir = dir + best_safe = true + elseif not best_dir then + best_dir = dir + end + end + end + + if best_dir then + player.gridX = player.gridX + best_dir[1] + player.gridY = player.gridY + best_dir[2] + end + player.moveTimer = 0 + return + end + + player.moveTimer = player.moveTimer + 1 + if player.moveTimer < 20 then return end + + player.moveTimer = 0 + ai_move_and_bomb(player) +end + +function is_dangerous(gridX, gridY) + for _, expl in ipairs(explosions) do + local explGridX = math.floor(expl.x / TILE_SIZE) + 1 + local explGridY = math.floor(expl.y / TILE_SIZE) + 1 + if gridX == explGridX and gridY == explGridY then + return true + end + end + + for _, bomb in ipairs(bombs) do + local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1 + local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1 + + if gridX == bombGridX and gridY == bombGridY then + return true + end + + if gridY == bombGridY then + if math.abs(gridX - bombGridX) <= 1 then + if gridX < bombGridX then + if map[gridY][gridX + 1] ~= SOLID_WALL then return true end + elseif gridX > bombGridX then + if map[gridY][gridX - 1] ~= SOLID_WALL then return true end + end + end + end + + if gridX == bombGridX then + if math.abs(gridY - bombGridY) <= 1 then + if gridY < bombGridY then + if map[gridY + 1][gridX] ~= SOLID_WALL then return true end + elseif gridY > bombGridY then + if map[gridY - 1][gridX] ~= SOLID_WALL then return true end + end + end + end + end + + return false +end + +function in_blast_zone(gridX, gridY, bombGridX, bombGridY) + if gridX == bombGridX and gridY == bombGridY then + return true + end + + if gridY == bombGridY and math.abs(gridX - bombGridX) <= 1 then + if gridX < bombGridX then + return map[gridY][gridX + 1] ~= SOLID_WALL + elseif gridX > bombGridX then + return map[gridY][gridX - 1] ~= SOLID_WALL + end + end + + if gridX == bombGridX and math.abs(gridY - bombGridY) <= 1 then + if gridY < bombGridY then + return map[gridY + 1][gridX] ~= SOLID_WALL + elseif gridY > bombGridY then + return map[gridY - 1][gridX] ~= SOLID_WALL + end + end + + return false +end + +function has_adjacent_breakable_wall(gridX, gridY) + local dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}} + for _, dir in ipairs(dirs) do + local checkX = gridX + dir[1] + local checkY = gridY + dir[2] + if checkX >= 1 and checkX <= 15 and checkY >= 1 and checkY <= 9 then + if map[checkY][checkX] == BREAKABLE_WALL then + return true + end + end + end + return false +end + +function has_escape_route(gridX, gridY) + local dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}} + for _, dir in ipairs(dirs) do + local newX = gridX + dir[1] + local newY = gridY + dir[2] + if can_move_to(newX, newY) and not is_dangerous(newX, newY) then + for _, dir2 in ipairs(dirs) do + local safeX = newX + dir2[1] + local safeY = newY + dir2[2] + if can_move_to(safeX, safeY) then + return true + end + end + end + end + return false +end + +function escape_from_own_bomb(player) + local bombGridX = player.gridX + local bombGridY = player.gridY + local dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}} + + local best_dir = nil + local best_score = -999 + + for _, dir in ipairs(dirs) do + local newX = player.gridX + dir[1] + local newY = player.gridY + dir[2] + + if can_move_to(newX, newY) then + local sc = 0 + + if not in_blast_zone(newX, newY, bombGridX, bombGridY) then + sc = sc + 100 + end + + for _, dir2 in ipairs(dirs) do + local checkX = newX + dir2[1] + local checkY = newY + dir2[2] + if not (checkX == bombGridX and checkY == bombGridY) then + if can_move_to(checkX, checkY) then + sc = sc + 10 + if not in_blast_zone(checkX, checkY, bombGridX, bombGridY) then + sc = sc + 20 + end + end + end + end + + if sc > best_score then + best_score = sc + best_dir = dir + end + end + end + + if best_dir then + player.gridX = player.gridX + best_dir[1] + player.gridY = player.gridY + best_dir[2] + end +end + +function place_bomb(player) + if player.activeBombs >= player.maxBombs then return end + + local bombX = (player.gridX - 1) * TILE_SIZE + local bombY = (player.gridY - 1) * TILE_SIZE + + for _, b in ipairs(bombs) do + if b.x == bombX and b.y == bombY then + return + end + end + + table.insert(bombs, {x = bombX, y = bombY, timer = BOMB_TIMER, owner = player}) + player.activeBombs = player.activeBombs + 1 +end + +function ai_move_and_bomb(player) + local human = players[1] + if not human then return end + + local dx = human.gridX - player.gridX + local dy = human.gridY - player.gridY + local dist = math.abs(dx) + math.abs(dy) + + local should_bomb = false + if dist <= 2 then should_bomb = true end + if has_adjacent_breakable_wall(player.gridX, player.gridY) then + should_bomb = true + end + + if should_bomb and player.activeBombs < player.maxBombs and player.bombCooldown <= 0 then + if has_escape_route(player.gridX, player.gridY) then + place_bomb(player) + player.bombCooldown = 90 + escape_from_own_bomb(player) + return + end + end + + local dirs = {} + if dx > 0 then table.insert(dirs, {1, 0}) + elseif dx < 0 then table.insert(dirs, {-1, 0}) + end + if dy > 0 then table.insert(dirs, {0, 1}) + elseif dy < 0 then table.insert(dirs, {0, -1}) + end + + local all_dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}} + for _, d in ipairs(all_dirs) do + table.insert(dirs, d) + end + + for _, dir in ipairs(dirs) do + local newGridX = player.gridX + dir[1] + local newGridY = player.gridY + dir[2] + if can_move_to(newGridX, newGridY) and not is_dangerous(newGridX, newGridY) then + player.gridX = newGridX + player.gridY = newGridY + return + end + end +end + +function can_move_to(gridX, gridY) + if gridX < 1 or gridY < 1 or gridX > 15 or gridY > 9 then + return false + end + if map[gridY][gridX] >= SOLID_WALL then + return false + end + return true +end + +function draw_player_sprite(x, y, is_player1) + local sprite_id = is_player1 and ASTRONAUT_BLUE or ASTRONAUT_RED + spr(sprite_id, x, y, 0, 2) +end + +function draw_bomb_sprite(x, y) + spr(BOMB_SPRITE, x, y, 0, 2) +end + +function reset_player_entity(player) + player.gridX = player.spawnX + player.gridY = player.spawnY + player.pixelX = (player.spawnX - 1) * TILE_SIZE + player.pixelY = (player.spawnY - 1) * TILE_SIZE + player.moving = false + player.maxBombs = 1 + player.activeBombs = 0 + player.bombCooldown = 0 +end + +function explode(bombX, bombY) + table.insert(explosions, {x = bombX, y = bombY, timer = EXPLOSION_TIMER}) + + local gridX = math.floor(bombX / TILE_SIZE) + 1 + local gridY = math.floor(bombY / TILE_SIZE) + 1 + + -- horizontal explosion + for _, dir in ipairs({-1, 1}) do + local explX = bombX + dir * TILE_SIZE + local eGridX = gridX + dir + if eGridX >= 1 and eGridX <= 15 then + local tile = map[gridY][eGridX] + if tile == EMPTY then + table.insert(explosions, {x = explX, y = bombY, timer = EXPLOSION_TIMER}) + elseif tile == BREAKABLE_WALL then + map[gridY][eGridX] = EMPTY + table.insert(explosions, {x = explX, y = bombY, timer = EXPLOSION_TIMER}) + end + end + end + + -- vertical explosion + for _, dir in ipairs({-1, 1}) do + local explY = bombY + dir * TILE_SIZE + local eGridY = gridY + dir + if eGridY >= 1 and eGridY <= 9 then + local tile = map[eGridY][gridX] + if tile == EMPTY then + table.insert(explosions, {x = bombX, y = explY, timer = EXPLOSION_TIMER}) + elseif tile == BREAKABLE_WALL then + map[eGridY][gridX] = EMPTY + table.insert(explosions, {x = bombX, y = explY, timer = EXPLOSION_TIMER}) + end + end + end +end + +-- +-- 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc +-- 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c +-- 003:eccccccccc888888caaaaaaaca888888cacccccccacccccccacc0ccccacc0ccc +-- 004:ccccceee8888cceeaaaa0cee888a0ceeccca0cccccca0c0c0cca0c0c0cca0c0c +-- 017:cacccccccaaaaaaacaaacaaacaaaaccccaaaaaaac8888888cc000cccecccccec +-- 018:ccca00ccaaaa0ccecaaa0ceeaaaa0ceeaaaa0cee8888ccee000cceeecccceeee +-- 019:cacccccccaaaaaaacaaacaaacaaaaccccaaaaaaac8888888cc000cccecccccec +-- 020:ccca00ccaaaa0ccecaaa0ceeaaaa0ceeaaaa0cee8888ccee000cceeecccceeee +-- + +-- +-- 000:00000000ffffffff00000000ffffffff +-- 001:0123456789abcdeffedcba9876543210 +-- 002:0123456789abcdef0123456789abcdef +-- + +-- +-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000 +-- + +-- +-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +-- + +-- +-- 000:00cccc000c1cc1c00ccccccc00cccc000c0cc0c00c0cc0c00000000000000000 +-- 001:00222200021221200222222200222200020220200202202000000000000000000 +-- 002:00043000001111000111111001111110011111100011110000011000000000000 +-- 003:ddd1ddddddd1dddd1111111ddddd1dddddddd1dd1111111ddd1ddddddd1ddddd +-- 004:8888888888888888888888888888888888888888888888888888888888888888 +-- + +-- +-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57 +-- diff --git a/bomberman.rb b/bomberman.rb deleted file mode 100644 index ff9aafb..0000000 --- a/bomberman.rb +++ /dev/null @@ -1,768 +0,0 @@ -# title: Bomberman Clone -# author: Zsolt Tasnadi -# desc: Simple Bomberman clone for TIC-80 -# site: http://teletype.hu -# license: MIT License -# version: 0.1 -# script: ruby - -# constants -TILE_SIZE = 16 -PLAYER_SIZE = 12 -BOMB_TIMER = 90 -EXPLOSION_TIMER = 30 - -EMPTY = 0 -SOLID_WALL = 1 -BREAKABLE_WALL = 2 - -# create a new player/enemy entity -def create_player(gridX, gridY, color, is_ai = false) - { - gridX: gridX, - gridY: gridY, - pixelX: gridX * TILE_SIZE, - pixelY: gridY * TILE_SIZE, - moving: false, - maxBombs: 1, - activeBombs: 0, - color: color, - is_ai: is_ai, - moveTimer: 0, - bombCooldown: 0, - spawnX: gridX, - spawnY: gridY - } -end - -# players array (first is human, rest are AI) -$players = [] -$players << create_player(1, 1, 12, false) # human player (blue) -$players << create_player(13, 7, 2, true) # AI enemy (red) - -# powerups (extra bombs hidden under breakable walls) -$powerups = [] - -# game objects -$bombs = [] -$explosions = [] - -# game state -$winner = nil -$win_timer = 0 -$score = [0, 0] # wins for player 1 and player 2 - -# animation speed (pixels per frame) -MOVE_SPEED = 2 - -# 1=solid wall, 2=breakable wall -$map = [ - [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], - [1,0,0,2,2,2,0,2,0,2,2,2,0,0,1], - [1,0,1,2,1,2,1,2,1,2,1,2,1,0,1], - [1,2,2,2,0,2,2,0,2,2,0,2,2,2,1], - [1,2,1,0,1,0,1,0,1,0,1,0,1,2,1], - [1,2,2,2,0,2,2,0,2,2,0,2,2,2,1], - [1,0,1,2,1,2,1,2,1,2,1,2,1,0,1], - [1,0,0,2,2,2,0,2,0,2,2,2,0,0,1], - [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] -] - -def init_powerups - $powerups = [] - # find all breakable walls and randomly place powerups under some - (0..8).each do |row| - (0..14).each do |col| - if $map[row][col] == BREAKABLE_WALL && rand < 0.3 - $powerups << { gridX: col, gridY: row, type: :bomb } - end - end - end -end - -init_powerups - -def TIC - cls(6) # green background - - # if there's a winner, show message and wait for restart - if $winner - $win_timer -= 1 - draw_win_screen - if btnp(4) && $win_timer <= 0 - restart_game - end - return - end - - # update all players - $players.each do |player| - update_player_movement(player) - - if player[:is_ai] - update_ai(player) - else - handle_human_input(player) - end - end - - # update bombs - $bombs.reverse_each do |bomb| - bomb[:timer] -= 1 - if bomb[:timer] <= 0 - explode(bomb[:x], bomb[:y]) - $bombs.delete(bomb) - bomb[:owner][:activeBombs] -= 1 if bomb[:owner] - end - end - - # update explosions - $explosions.reverse_each do |expl| - expl[:timer] -= 1 - $explosions.delete(expl) if expl[:timer] <= 0 - end - - # check powerup pickup for all players - $players.each do |player| - $powerups.reverse_each do |pw| - if $map[pw[:gridY]][pw[:gridX]] == EMPTY && - player[:gridX] == pw[:gridX] && player[:gridY] == pw[:gridY] - player[:maxBombs] += 1 - $powerups.delete(pw) - end - end - end - - # check death by explosion for all players - $players.each_with_index do |player, idx| - $explosions.each do |expl| - explGridX = (expl[:x] / TILE_SIZE).floor - explGridY = (expl[:y] / TILE_SIZE).floor - if player[:gridX] == explGridX && player[:gridY] == explGridY - # other player wins - winner_idx = (idx == 0) ? 2 : 1 - set_winner(winner_idx) - return - end - end - end - - # check human player death by touching AI enemy - human = $players[0] - $players.each do |player| - if player[:is_ai] && human[:gridX] == player[:gridX] && human[:gridY] == player[:gridY] - set_winner(2) # AI wins - return - end - end - - draw_game -end - -def draw_game - # draw map - (0..8).each do |row| - (0..14).each do |col| - tile = $map[row][col] - drawX = col * TILE_SIZE - drawY = row * TILE_SIZE - if tile == SOLID_WALL - spr(SOLID_WALL_SPRITE, drawX, drawY, 0, 2) - elsif tile == BREAKABLE_WALL - spr(BREAKABLE_WALL_SPRITE, drawX, drawY, 0, 2) - end - end - end - - # draw powerups (only visible when wall is destroyed) - $powerups.each do |pw| - if $map[pw[:gridY]][pw[:gridX]] == EMPTY - drawX = pw[:gridX] * TILE_SIZE - drawY = pw[:gridY] * TILE_SIZE - rect(drawX + 3, drawY + 3, 10, 10, 6) - print("B", drawX + 5, drawY + 5, 0) - end - end - - # draw bombs - $bombs.each do |bomb| - draw_bomb_sprite(bomb[:x], bomb[:y]) - end - - # draw explosions - $explosions.each do |expl| - rect(expl[:x], expl[:y], TILE_SIZE, TILE_SIZE, 6) - end - - # draw all players - $players.each_with_index do |player, idx| - draw_player_sprite(player[:pixelX], player[:pixelY], idx == 0) - end - - # score display - print("#{$score[0]}:#{$score[1]}", 5, 2, 12) - - print("ARROWS:MOVE A:BOMB", 60, 2, 15) - human = $players[0] - available = human[:maxBombs] - human[:activeBombs] - print("BOMBS:#{available}/#{human[:maxBombs]}", 180, 2, 11) -end - -def set_winner(player_num) - $winner = player_num - $win_timer = 60 # delay before allowing restart - $score[player_num - 1] += 1 -end - -def draw_win_screen - # black background - cls(0) - - # white border frame - rect(20, 30, 200, 80, 12) # outer white - rect(22, 32, 196, 76, 0) # inner black - - # winner text (white on black) - text = "PLAYER #{$winner} WON!" - print(text, 70, 55, 12, false, 2) - - # restart prompt (blink effect) - if $win_timer <= 0 || ($win_timer / 15) % 2 == 0 - print("Press A to restart", 70, 80, 12) - end -end - -def restart_game - $winner = nil - $win_timer = 0 - $bombs = [] - $explosions = [] - - # reset map - $map = [ - [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], - [1,0,0,2,2,2,0,2,0,2,2,2,0,0,1], - [1,0,1,2,1,2,1,2,1,2,1,2,1,0,1], - [1,2,2,2,0,2,2,0,2,2,0,2,2,2,1], - [1,2,1,0,1,0,1,0,1,0,1,0,1,2,1], - [1,2,2,2,0,2,2,0,2,2,0,2,2,2,1], - [1,0,1,2,1,2,1,2,1,2,1,2,1,0,1], - [1,0,0,2,2,2,0,2,0,2,2,2,0,0,1], - [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] - ] - - # reset players - $players.each { |p| reset_player_entity(p) } - - # reset powerups - init_powerups -end - -# common movement animation for all players -def update_player_movement(player) - targetX = player[:gridX] * TILE_SIZE - targetY = player[:gridY] * TILE_SIZE - - if player[:pixelX] < targetX - player[:pixelX] = [player[:pixelX] + MOVE_SPEED, targetX].min - player[:moving] = true - elsif player[:pixelX] > targetX - player[:pixelX] = [player[:pixelX] - MOVE_SPEED, targetX].max - player[:moving] = true - elsif player[:pixelY] < targetY - player[:pixelY] = [player[:pixelY] + MOVE_SPEED, targetY].min - player[:moving] = true - elsif player[:pixelY] > targetY - player[:pixelY] = [player[:pixelY] - MOVE_SPEED, targetY].max - player[:moving] = true - else - player[:moving] = false - end - - player[:bombCooldown] -= 1 if player[:bombCooldown] > 0 -end - -# handle human player input -def handle_human_input(player) - return if player[:moving] - - newGridX = player[:gridX] - newGridY = player[:gridY] - - if btn(0) - newGridY = player[:gridY] - 1 - elsif btn(1) - newGridY = player[:gridY] + 1 - elsif btn(2) - newGridX = player[:gridX] - 1 - elsif btn(3) - newGridX = player[:gridX] + 1 - end - - if can_move_to?(newGridX, newGridY) - player[:gridX] = newGridX - player[:gridY] = newGridY - end - - # place bomb - if btnp(4) - place_bomb(player) - end -end - -# AI player logic -def update_ai(player) - return if player[:moving] - - # check if in danger - react immediately, no delay! - in_danger = is_dangerous?(player[:gridX], player[:gridY]) - - if in_danger - # find any safe direction and move there NOW - dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]] - - # try to find best escape direction - best_dir = nil - best_safe = false - - dirs.each do |dir| - newX = player[:gridX] + dir[0] - newY = player[:gridY] + dir[1] - if can_move_to?(newX, newY) - safe = !is_dangerous?(newX, newY) - if safe && !best_safe - best_dir = dir - best_safe = true - elsif !best_dir - best_dir = dir - end - end - end - - if best_dir - player[:gridX] += best_dir[0] - player[:gridY] += best_dir[1] - end - player[:moveTimer] = 0 - return - end - - # normal movement with timer - player[:moveTimer] += 1 - return if player[:moveTimer] < 20 - - player[:moveTimer] = 0 - ai_move_and_bomb(player) -end - -# check if a position is dangerous (bomb blast zone or explosion) -def is_dangerous?(gridX, gridY) - # check explosions - $explosions.each do |expl| - explGridX = (expl[:x] / TILE_SIZE).floor - explGridY = (expl[:y] / TILE_SIZE).floor - return true if gridX == explGridX && gridY == explGridY - end - - # check bombs and their blast zones - $bombs.each do |bomb| - bombGridX = (bomb[:x] / TILE_SIZE).floor - bombGridY = (bomb[:y] / TILE_SIZE).floor - - # bomb position - return true if gridX == bombGridX && gridY == bombGridY - - # horizontal blast zone - if gridY == bombGridY - if (gridX - bombGridX).abs <= 1 - # check if wall blocks the blast - if gridX < bombGridX - return true if $map[gridY][gridX + 1] != SOLID_WALL - elsif gridX > bombGridX - return true if $map[gridY][gridX - 1] != SOLID_WALL - end - end - end - - # vertical blast zone - if gridX == bombGridX - if (gridY - bombGridY).abs <= 1 - # check if wall blocks the blast - if gridY < bombGridY - return true if $map[gridY + 1][gridX] != SOLID_WALL - elsif gridY > bombGridY - return true if $map[gridY - 1][gridX] != SOLID_WALL - end - end - end - end - - false -end - -# find escape direction away from danger -def get_escape_direction(gridX, gridY) - return nil unless is_dangerous?(gridX, gridY) - - # find direction away from nearest bomb - $bombs.each do |bomb| - bombGridX = (bomb[:x] / TILE_SIZE).floor - bombGridY = (bomb[:y] / TILE_SIZE).floor - - # if on same row as bomb, move vertically - if gridY == bombGridY && (gridX - bombGridX).abs <= 1 - return [0, -1] if can_move_to?(gridX, gridY - 1) && !is_dangerous?(gridX, gridY - 1) - return [0, 1] if can_move_to?(gridX, gridY + 1) && !is_dangerous?(gridX, gridY + 1) - end - - # if on same column as bomb, move horizontally - if gridX == bombGridX && (gridY - bombGridY).abs <= 1 - return [-1, 0] if can_move_to?(gridX - 1, gridY) && !is_dangerous?(gridX - 1, gridY) - return [1, 0] if can_move_to?(gridX + 1, gridY) && !is_dangerous?(gridX + 1, gridY) - end - end - - nil -end - -# try to escape in any safe direction -def try_escape_any_direction(player) - dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]].shuffle - dirs.each do |dir| - newX = player[:gridX] + dir[0] - newY = player[:gridY] + dir[1] - if can_move_to?(newX, newY) && !is_dangerous?(newX, newY) - player[:gridX] = newX - player[:gridY] = newY - return - end - end -end - -# escape immediately after placing bomb - choose best direction -def escape_from_own_bomb(player) - bombGridX = player[:gridX] - bombGridY = player[:gridY] - dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]] - - # find the best escape direction - best_dir = nil - best_score = -999 - - dirs.each do |dir| - newX = player[:gridX] + dir[0] - newY = player[:gridY] + dir[1] - - next unless can_move_to?(newX, newY) - - score = 0 - - # strongly prefer positions outside our new bomb's blast zone - if !in_blast_zone?(newX, newY, bombGridX, bombGridY) - score += 100 - end - - # count escape routes from this position (excluding back to bomb) - dirs.each do |dir2| - checkX = newX + dir2[0] - checkY = newY + dir2[1] - next if checkX == bombGridX && checkY == bombGridY # don't count going back - if can_move_to?(checkX, checkY) - score += 10 - # extra points if that position is also safe - score += 20 unless in_blast_zone?(checkX, checkY, bombGridX, bombGridY) - end - end - - if score > best_score - best_score = score - best_dir = dir - end - end - - # move if we found a direction - if best_dir - player[:gridX] += best_dir[0] - player[:gridY] += best_dir[1] - end -end - -# check if there's a breakable wall adjacent to position -def has_adjacent_breakable_wall?(gridX, gridY) - dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]] - dirs.each do |dir| - checkX = gridX + dir[0] - checkY = gridY + dir[1] - if checkX >= 0 && checkX <= 14 && checkY >= 0 && checkY <= 8 - return true if $map[checkY][checkX] == BREAKABLE_WALL - end - end - false -end - -# check if a position would be in blast zone of a bomb at bombX, bombY -def in_blast_zone?(gridX, gridY, bombGridX, bombGridY) - # same position as bomb - return true if gridX == bombGridX && gridY == bombGridY - - # horizontal blast (1 tile range) - if gridY == bombGridY && (gridX - bombGridX).abs <= 1 - # check if wall blocks blast - if gridX < bombGridX - return $map[gridY][gridX + 1] != SOLID_WALL - elsif gridX > bombGridX - return $map[gridY][gridX - 1] != SOLID_WALL - end - end - - # vertical blast (1 tile range) - if gridX == bombGridX && (gridY - bombGridY).abs <= 1 - if gridY < bombGridY - return $map[gridY + 1][gridX] != SOLID_WALL - elsif gridY > bombGridY - return $map[gridY - 1][gridX] != SOLID_WALL - end - end - - false -end - -# check if there's a safe path to escape from bomb blast zone -# uses BFS to find if any safe tile is reachable -def has_safe_escape_route?(gridX, gridY) - bombGridX = gridX - bombGridY = gridY - - # BFS to find safe position - visited = {} - queue = [] - - # start from adjacent positions (first move after placing bomb) - dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]] - dirs.each do |dir| - newX = gridX + dir[0] - newY = gridY + dir[1] - if can_move_to?(newX, newY) && !is_dangerous?(newX, newY) - queue << [newX, newY, 1] # position and distance - visited["#{newX},#{newY}"] = true - end - end - - while !queue.empty? - cx, cy, dist = queue.shift - - # check if this position is safe (outside blast zone) - unless in_blast_zone?(cx, cy, bombGridX, bombGridY) - return true - end - - # if we can move 3+ steps, we should be able to escape - # (bomb timer gives us enough time) - if dist >= 3 - return true - end - - # explore neighbors - dirs.each do |dir| - newX = cx + dir[0] - newY = cy + dir[1] - key = "#{newX},#{newY}" - if can_move_to?(newX, newY) && !visited[key] && !is_dangerous?(newX, newY) - visited[key] = true - queue << [newX, newY, dist + 1] - end - end - end - - false -end - -# simple check for escape route (legacy, kept for compatibility) -def has_escape_route?(gridX, gridY) - has_safe_escape_route?(gridX, gridY) -end - -# place bomb for any player -def place_bomb(player) - return if player[:activeBombs] >= player[:maxBombs] - - bombX = player[:gridX] * TILE_SIZE - bombY = player[:gridY] * TILE_SIZE - already_bomb = $bombs.any? { |b| b[:x] == bombX && b[:y] == bombY } - - unless already_bomb - $bombs << { x: bombX, y: bombY, timer: BOMB_TIMER, owner: player } - player[:activeBombs] += 1 - end -end - -# AI movement and bombing logic -def ai_move_and_bomb(player) - # find nearest human player to chase - human = $players.find { |p| !p[:is_ai] } - return unless human - - dx = human[:gridX] - player[:gridX] - dy = human[:gridY] - player[:gridY] - dist = dx.abs + dy.abs - - # decide if should place bomb - should_bomb = false - should_bomb = true if dist <= 2 - should_bomb = true if has_adjacent_breakable_wall?(player[:gridX], player[:gridY]) - - # place bomb if should and can - if should_bomb && player[:activeBombs] < player[:maxBombs] && player[:bombCooldown] <= 0 - if has_escape_route?(player[:gridX], player[:gridY]) - place_bomb(player) - player[:bombCooldown] = 90 - escape_from_own_bomb(player) - return - end - end - - # move toward human player - dirs = [] - - if dx > 0 - dirs << [1, 0] - elsif dx < 0 - dirs << [-1, 0] - end - - if dy > 0 - dirs << [0, 1] - elsif dy < 0 - dirs << [0, -1] - end - - dirs += [[0, -1], [0, 1], [-1, 0], [1, 0]].shuffle - - dirs.each do |dir| - newGridX = player[:gridX] + dir[0] - newGridY = player[:gridY] + dir[1] - if can_move_to?(newGridX, newGridY) && !is_dangerous?(newGridX, newGridY) - player[:gridX] = newGridX - player[:gridY] = newGridY - return - end - end -end - -def can_move_to?(gridX, gridY) - return false if gridX < 0 || gridY < 0 || gridX > 14 || gridY > 8 - return false if $map[gridY][gridX] >= SOLID_WALL - true -end - -# sprite indices (in SPRITES section, starts at 256) -# 8x8 sprites scaled to ~12-16 pixels -ASTRONAUT_BLUE = 256 # sprite 0 -ASTRONAUT_RED = 257 # sprite 1 -BOMB_SPRITE = 258 # sprite 2 -BREAKABLE_WALL_SPRITE = 259 # sprite 3 - brick pattern -SOLID_WALL_SPRITE = 260 # sprite 4 - solid gray - -def draw_player_sprite(x, y, is_player1) - sprite_id = is_player1 ? ASTRONAUT_BLUE : ASTRONAUT_RED - # 8x8 sprite with scale=2 -> 16x16, offset to center - spr(sprite_id, x, y, 0, 2) -end - -def draw_bomb_sprite(x, y) - # 8x8 sprite with scale=2 -> 16x16 - spr(BOMB_SPRITE, x, y, 0, 2) -end - -# reset any player to spawn position -def reset_player_entity(player) - player[:gridX] = player[:spawnX] - player[:gridY] = player[:spawnY] - player[:pixelX] = player[:spawnX] * TILE_SIZE - player[:pixelY] = player[:spawnY] * TILE_SIZE - player[:moving] = false - player[:maxBombs] = 1 - player[:activeBombs] = 0 - player[:bombCooldown] = 0 -end - -def explode(bombX, bombY) - $explosions << { x: bombX, y: bombY, timer: EXPLOSION_TIMER } - - # horizontal explosion - [-1, 1].each do |dir| - explX = bombX + dir * TILE_SIZE - gridX = (explX / TILE_SIZE).floor - gridY = (bombY / TILE_SIZE).floor - if gridX >= 0 && gridX <= 14 - tile = $map[gridY][gridX] - if tile == EMPTY - $explosions << { x: explX, y: bombY, timer: EXPLOSION_TIMER } - elsif tile == BREAKABLE_WALL - $map[gridY][gridX] = EMPTY - $explosions << { x: explX, y: bombY, timer: EXPLOSION_TIMER } - end - end - end - - # vertical explosion - [-1, 1].each do |dir| - explY = bombY + dir * TILE_SIZE - gridX = (bombX / TILE_SIZE).floor - gridY = (explY / TILE_SIZE).floor - if gridY >= 0 && gridY <= 8 - tile = $map[gridY][gridX] - if tile == EMPTY - $explosions << { x: bombX, y: explY, timer: EXPLOSION_TIMER } - elsif tile == BREAKABLE_WALL - $map[gridY][gridX] = EMPTY - $explosions << { x: bombX, y: explY, timer: EXPLOSION_TIMER } - end - end - end -end - - -# -# 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc -# 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c -# 003:eccccccccc888888caaaaaaaca888888cacccccccacccccccacc0ccccacc0ccc -# 004:ccccceee8888cceeaaaa0cee888a0ceeccca0cccccca0c0c0cca0c0c0cca0c0c -# 017:cacccccccaaaaaaacaaacaaacaaaaccccaaaaaaac8888888cc000cccecccccec -# 018:ccca00ccaaaa0ccecaaa0ceeaaaa0ceeaaaa0cee8888ccee000cceeecccceeee -# 019:cacccccccaaaaaaacaaacaaacaaaaccccaaaaaaac8888888cc000cccecccccec -# 020:ccca00ccaaaa0ccecaaa0ceeaaaa0ceeaaaa0cee8888ccee000cceeecccceeee -# 032:00cccc0000cccc000ccccccc0ccc1ccc0cc111cc0ccc1cccccccccccccc00ccc -# 033:00000000000000000c0000c00c0000c00cc00cc000cccc0000cccc0000000000 -# 048:0cc00ccc0cc00ccc00c00cc000c00cc0000cc000000cc00000000000000c0c00 -# 049:00000000000000000000000000000000000000000000000000000000000c0c00 -# 064:00222200022222200222f2220222f22202222222002222000022220000200200 -# 065:00000000000000000200002002000020022002200022220000222200000c0c00 -# - -# -# 000:00000000ffffffff00000000ffffffff -# 001:0123456789abcdeffedcba9876543210 -# 002:0123456789abcdef0123456789abcdef -# - -# -# 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000 -# - -# -# 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -# - -# -# 000:00cccc000c1cc1c00ccccccc00cccc000c0cc0c00c0cc0c00000000000000000 -# 001:00222200021221200222222200222200020220200202202000000000000000000 -# 002:00043000001111000111111001111110011111100011110000011000000000000 -# 003:ddd1ddddddd1dddd1111111ddddd1dddddddd1dd1111111ddd1ddddddd1ddddd -# 004:8888888888888888888888888888888888888888888888888888888888888888 -# - -# -# 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57 -# -