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
-#
-