diff --git a/bomberman.lua b/bomberman.lua index 09d17f2..6d08d67 100644 --- a/bomberman.lua +++ b/bomberman.lua @@ -7,43 +7,44 @@ -- script: lua -- constants -TILE_SIZE = 16 -PLAYER_SIZE = 12 -BOMB_TIMER = 90 -EXPLOSION_TIMER = 30 +local TILE_SIZE = 16 +local PLAYER_SIZE = 12 +local BOMB_TIMER = 90 +local EXPLOSION_TIMER = 30 -EMPTY = 0 -SOLID_WALL = 1 -BREAKABLE_WALL = 2 +local EMPTY = 0 +local SOLID_WALL = 1 +local BREAKABLE_WALL = 2 -MOVE_SPEED = 2 +local 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 +local ASTRONAUT_BLUE = 256 +local ASTRONAUT_RED = 257 +local BOMB_SPRITE = 258 +local BREAKABLE_WALL_SPRITE = 259 +local SOLID_WALL_SPRITE = 260 --- game state -GAME_STATE_SPLASH = 0 -GAME_STATE_MENU = 1 -GAME_STATE_PLAYING = 2 +-- game state constants +local GAME_STATE_SPLASH = 0 +local GAME_STATE_MENU = 1 +local GAME_STATE_PLAYING = 2 -game_state = GAME_STATE_SPLASH -splash_timer = 90 -- 1.5 seconds at 60fps -menu_selection = 1 -- 1 = Play, 2 = Exit +-- game state variables +local game_state = GAME_STATE_SPLASH +local splash_timer = 90 -- 1.5 seconds at 60fps +local menu_selection = 1 -- 1 = Play, 2 = Exit -players = {} -powerups = {} -bombs = {} -explosions = {} -winner = nil -win_timer = 0 -score = {0, 0} +local players = {} +local powerups = {} +local bombs = {} +local explosions = {} +local winner = nil +local win_timer = 0 +local score = {0, 0} -- map (1=solid wall, 2=breakable wall) -map = { +local 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}, @@ -55,7 +56,20 @@ map = { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} } -function create_player(gridX, gridY, color, is_ai) +-- initial map for reset +local initial_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} +} + +local function create_player(gridX, gridY, color, is_ai) return { gridX = gridX, gridY = gridY, @@ -73,14 +87,7 @@ function create_player(gridX, gridY, color, is_ai) } 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() +local function init_powerups() powerups = {} for row = 1, 9 do for col = 1, 15 do @@ -91,6 +98,507 @@ function init_powerups() end end +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local 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 + +local function draw_bomb_sprite(x, y) + spr(BOMB_SPRITE, x, y, 0, 2) +end + +local 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 + +local 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 + +local function set_winner(player_num) + winner = player_num + win_timer = 60 + score[player_num] = score[player_num] + 1 +end + +local 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 + +local function restart_game() + winner = nil + win_timer = 0 + bombs = {} + explosions = {} + + -- reset map from initial + for row = 1, 9 do + for col = 1, 15 do + map[row][col] = initial_map[row][col] + end + end + + for _, p in ipairs(players) do + reset_player_entity(p) + end + init_powerups() +end + +local 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 + +local function update_splash() + cls(0) + + -- title with line break + print("Bomberman", 85, 50, 12, false, 2) + print("Clone", 100, 70, 12, false, 2) + + splash_timer = splash_timer - 1 + if splash_timer <= 0 then + game_state = GAME_STATE_MENU + end +end + +local function update_menu() + cls(0) + + -- title + print("Bomberman", 85, 30, 12, false, 2) + print("Clone", 100, 50, 12, false, 2) + + -- menu options + local play_color = (menu_selection == 1) and 11 or 15 + local exit_color = (menu_selection == 2) and 11 or 15 + + -- selection indicator + if menu_selection == 1 then + print(">", 85, 90, 11) + else + print(">", 85, 110, 11) + end + + print("Play", 95, 90, play_color) + print("Exit", 95, 110, exit_color) + + -- handle input + if btnp(0) then -- up + menu_selection = 1 + elseif btnp(1) then -- down + menu_selection = 2 + elseif btnp(4) then -- A button (select) + if menu_selection == 1 then + game_state = GAME_STATE_PLAYING + init_game() + else + exit() + end + end +end + function TIC() if game_state == GAME_STATE_SPLASH then update_splash() @@ -113,7 +621,7 @@ function TIC() end -- update all players - for i, player in ipairs(players) do + for _, player in ipairs(players) do update_player_movement(player) if player.is_ai then update_ai(player) @@ -181,505 +689,6 @@ function TIC() 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 update_splash() - cls(0) - - -- title with line break - print("Bomberman", 85, 50, 12, false, 2) - print("Clone", 100, 70, 12, false, 2) - - splash_timer = splash_timer - 1 - if splash_timer <= 0 then - game_state = GAME_STATE_MENU - end -end - -function update_menu() - cls(0) - - -- title - print("Bomberman", 85, 30, 12, false, 2) - print("Clone", 100, 50, 12, false, 2) - - -- menu options - local play_color = (menu_selection == 1) and 11 or 15 - local exit_color = (menu_selection == 2) and 11 or 15 - - -- selection indicator - if menu_selection == 1 then - print(">", 85, 90, 11) - else - print(">", 85, 110, 11) - end - - print("Play", 95, 90, play_color) - print("Exit", 95, 110, exit_color) - - -- handle input - if btnp(0) then -- up - menu_selection = 1 - elseif btnp(1) then -- down - menu_selection = 2 - elseif btnp(4) then -- A button (select) - if menu_selection == 1 then - game_state = GAME_STATE_PLAYING - init_game() - else - exit() - end - end -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