From d197289a06a2898fe0693814272735f3f1268cbe Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Thu, 4 Dec 2025 10:27:01 +0100 Subject: [PATCH] enemy as player --- bomberman.rb | 457 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 318 insertions(+), 139 deletions(-) diff --git a/bomberman.rb b/bomberman.rb index 8c7c143..9ed2ad0 100644 --- a/bomberman.rb +++ b/bomberman.rb @@ -16,16 +16,29 @@ EMPTY = 0 SOLID_WALL = 1 BREAKABLE_WALL = 2 -# player (grid position and pixel position for animation) -$player = { - gridX: 1, - gridY: 1, - pixelX: 16, - pixelY: 16, - moving: false, - maxBombs: 1, - activeBombs: 0 -} +# 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 = [] @@ -34,16 +47,6 @@ $powerups = [] $bombs = [] $explosions = [] -# enemy (grid position and pixel position for animation) -$enemy = { - gridX: 13, - gridY: 7, - pixelX: 208, - pixelY: 112, - moving: false, - moveTimer: 0 -} - # animation speed (pixels per frame) MOVE_SPEED = 2 @@ -77,18 +80,14 @@ init_powerups def TIC cls(0) - # handle player movement - update_player + # update all players + $players.each do |player| + update_player_movement(player) - # place bomb (only if we have available bombs) - if btnp(4) && $player[:activeBombs] < $player[:maxBombs] - bombX = $player[:gridX] * TILE_SIZE - bombY = $player[:gridY] * TILE_SIZE - # check if there's already a bomb at this position - already_bomb = $bombs.any? { |b| b[:x] == bombX && b[:y] == bombY } - unless already_bomb - $bombs << { x: bombX, y: bombY, timer: BOMB_TIMER } - $player[:activeBombs] += 1 + if player[:is_ai] + update_ai(player) + else + handle_human_input(player) end end @@ -98,7 +97,7 @@ def TIC if bomb[:timer] <= 0 explode(bomb[:x], bomb[:y]) $bombs.delete(bomb) - $player[:activeBombs] -= 1 + bomb[:owner][:activeBombs] -= 1 if bomb[:owner] end end @@ -127,18 +126,19 @@ def TIC if $map[pw[:gridY]][pw[:gridX]] == EMPTY drawX = pw[:gridX] * TILE_SIZE drawY = pw[:gridY] * TILE_SIZE - # draw bomb powerup as small circle with B rect(drawX + 3, drawY + 3, 10, 10, 6) print("B", drawX + 5, drawY + 5, 0) end end - # check powerup pickup - $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) + # 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 @@ -152,125 +152,310 @@ def TIC rect(expl[:x], expl[:y], TILE_SIZE, TILE_SIZE, 6) end - # update enemy - update_enemy + # draw all players + $players.each do |player| + rect(player[:pixelX] + 2, player[:pixelY] + 2, PLAYER_SIZE, PLAYER_SIZE, player[:color]) + end - # draw player (centered in tile) - rect($player[:pixelX] + 2, $player[:pixelY] + 2, PLAYER_SIZE, PLAYER_SIZE, 12) - - # draw enemy (centered in tile) - rect($enemy[:pixelX] + 2, $enemy[:pixelY] + 2, PLAYER_SIZE, PLAYER_SIZE, 2) - - # check player death by explosion (grid-based) - $explosions.each do |expl| - explGridX = (expl[:x] / TILE_SIZE).floor - explGridY = (expl[:y] / TILE_SIZE).floor - if $player[:gridX] == explGridX && $player[:gridY] == explGridY - reset_player + # check death by explosion for all players + $players.each do |player| + $explosions.each do |expl| + explGridX = (expl[:x] / TILE_SIZE).floor + explGridY = (expl[:y] / TILE_SIZE).floor + if player[:gridX] == explGridX && player[:gridY] == explGridY + reset_player_entity(player) + end end end - # check player death by enemy (grid-based) - if $player[:gridX] == $enemy[:gridX] && $player[:gridY] == $enemy[:gridY] - reset_player - end - - # check enemy death by explosion (grid-based) - $explosions.each do |expl| - explGridX = (expl[:x] / TILE_SIZE).floor - explGridY = (expl[:y] / TILE_SIZE).floor - if $enemy[:gridX] == explGridX && $enemy[:gridY] == explGridY - reset_enemy + # 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] + reset_player_entity(human) end end print("ARROWS:MOVE A:BOMB", 50, 2, 15) - available = $player[:maxBombs] - $player[:activeBombs] - print("BOMBS:#{available}/#{$player[:maxBombs]}", 170, 2, 11) + human = $players[0] + available = human[:maxBombs] - human[:activeBombs] + print("BOMBS:#{available}/#{human[:maxBombs]}", 170, 2, 11) end -def update_player - targetX = $player[:gridX] * TILE_SIZE - targetY = $player[:gridY] * TILE_SIZE +# common movement animation for all players +def update_player_movement(player) + targetX = player[:gridX] * TILE_SIZE + targetY = player[:gridY] * TILE_SIZE - # animate toward target position - 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 + 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 + player[:moving] = false end - # only accept input when not moving - return if $player[:moving] + player[:bombCooldown] -= 1 if player[:bombCooldown] > 0 +end - newGridX = $player[:gridX] - newGridY = $player[:gridY] +# 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 + newGridY = player[:gridY] - 1 elsif btn(1) - newGridY = $player[:gridY] + 1 + newGridY = player[:gridY] + 1 elsif btn(2) - newGridX = $player[:gridX] - 1 + newGridX = player[:gridX] - 1 elsif btn(3) - newGridX = $player[:gridX] + 1 + newGridX = player[:gridX] + 1 end - # check if new grid position is valid if can_move_to?(newGridX, newGridY) - $player[:gridX] = newGridX - $player[:gridY] = newGridY + player[:gridX] = newGridX + player[:gridY] = newGridY + end + + # place bomb + if btnp(4) + place_bomb(player) end end -def update_enemy - targetX = $enemy[:gridX] * TILE_SIZE - targetY = $enemy[:gridY] * TILE_SIZE +# AI player logic +def update_ai(player) + return if player[:moving] - # animate toward target position - if $enemy[:pixelX] < targetX - $enemy[:pixelX] = [$enemy[:pixelX] + MOVE_SPEED, targetX].min - $enemy[:moving] = true - elsif $enemy[:pixelX] > targetX - $enemy[:pixelX] = [$enemy[:pixelX] - MOVE_SPEED, targetX].max - $enemy[:moving] = true - elsif $enemy[:pixelY] < targetY - $enemy[:pixelY] = [$enemy[:pixelY] + MOVE_SPEED, targetY].min - $enemy[:moving] = true - elsif $enemy[:pixelY] > targetY - $enemy[:pixelY] = [$enemy[:pixelY] - MOVE_SPEED, targetY].max - $enemy[:moving] = true + player[:moveTimer] += 1 + return if player[:moveTimer] < 20 + + player[:moveTimer] = 0 + + # check if in danger + danger_dir = get_escape_direction(player[:gridX], player[:gridY]) + + if danger_dir + newGridX = player[:gridX] + danger_dir[0] + newGridY = player[:gridY] + danger_dir[1] + if can_move_to?(newGridX, newGridY) && !is_dangerous?(newGridX, newGridY) + player[:gridX] = newGridX + player[:gridY] = newGridY + else + try_escape_any_direction(player) + end else - $enemy[:moving] = false + ai_move_and_bomb(player) + end +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 - # only move when animation is complete - return if $enemy[:moving] + # check bombs and their blast zones + $bombs.each do |bomb| + bombGridX = (bomb[:x] / TILE_SIZE).floor + bombGridY = (bomb[:y] / TILE_SIZE).floor - $enemy[:moveTimer] += 1 - return if $enemy[:moveTimer] < 30 + # bomb position + return true if gridX == bombGridX && gridY == bombGridY - $enemy[:moveTimer] = 0 + # 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 - # pick random direction + # 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 +def escape_from_own_bomb(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) + player[:gridX] = newX + player[:gridY] = newY + return + end + 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]] - dir = rand(4) - newGridX = $enemy[:gridX] + dirs[dir][0] - newGridY = $enemy[:gridY] + dirs[dir][1] + 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 - if can_move_to?(newGridX, newGridY) - $enemy[:gridX] = newGridX - $enemy[:gridY] = newGridY +# check if enemy has a safe escape route after placing bomb +def has_escape_route?(gridX, gridY) + dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]] + dirs.each do |dir| + newX = gridX + dir[0] + newY = gridY + dir[1] + # check if can move there and it's not in bomb blast zone + if can_move_to?(newX, newY) + # check one more step for safety + dirs.each do |dir2| + safeX = newX + dir2[0] + safeY = newY + dir2[1] + if can_move_to?(safeX, safeY) + return true + end + end + end + end + false +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 @@ -280,22 +465,16 @@ def can_move_to?(gridX, gridY) true end -def reset_player - $player[:gridX] = 1 - $player[:gridY] = 1 - $player[:pixelX] = 16 - $player[:pixelY] = 16 - $player[:moving] = false - $player[:maxBombs] = 1 - $player[:activeBombs] = 0 -end - -def reset_enemy - $enemy[:gridX] = 13 - $enemy[:gridY] = 7 - $enemy[:pixelX] = 208 - $enemy[:pixelY] = 112 - $enemy[:moving] = false +# 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)