points
This commit is contained in:
328
bomberman.rb
328
bomberman.rb
@@ -1,8 +1,8 @@
|
|||||||
# title: game title
|
# title: Bomberman Clone
|
||||||
# author: game developer, email, etc.
|
# author: Zsolt Tasnadi
|
||||||
# desc: short description
|
# desc: Simple Bomberman clone for TIC-80
|
||||||
# site: website link
|
# site: http://teletype.hu
|
||||||
# license: MIT License (change this to your license of choice)
|
# license: MIT License
|
||||||
# version: 0.1
|
# version: 0.1
|
||||||
# script: ruby
|
# script: ruby
|
||||||
|
|
||||||
@@ -47,6 +47,11 @@ $powerups = []
|
|||||||
$bombs = []
|
$bombs = []
|
||||||
$explosions = []
|
$explosions = []
|
||||||
|
|
||||||
|
# game state
|
||||||
|
$winner = nil
|
||||||
|
$win_timer = 0
|
||||||
|
$score = [0, 0] # wins for player 1 and player 2
|
||||||
|
|
||||||
# animation speed (pixels per frame)
|
# animation speed (pixels per frame)
|
||||||
MOVE_SPEED = 2
|
MOVE_SPEED = 2
|
||||||
|
|
||||||
@@ -80,6 +85,16 @@ init_powerups
|
|||||||
def TIC
|
def TIC
|
||||||
cls(0)
|
cls(0)
|
||||||
|
|
||||||
|
# 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
|
# update all players
|
||||||
$players.each do |player|
|
$players.each do |player|
|
||||||
update_player_movement(player)
|
update_player_movement(player)
|
||||||
@@ -107,6 +122,44 @@ def TIC
|
|||||||
$explosions.delete(expl) if expl[:timer] <= 0
|
$explosions.delete(expl) if expl[:timer] <= 0
|
||||||
end
|
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
|
# draw map
|
||||||
(0..8).each do |row|
|
(0..8).each do |row|
|
||||||
(0..14).each do |col|
|
(0..14).each do |col|
|
||||||
@@ -131,17 +184,6 @@ def TIC
|
|||||||
end
|
end
|
||||||
end
|
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
|
|
||||||
|
|
||||||
# draw bombs
|
# draw bombs
|
||||||
$bombs.each do |bomb|
|
$bombs.each do |bomb|
|
||||||
circ(bomb[:x] + 8, bomb[:y] + 8, 6, 2)
|
circ(bomb[:x] + 8, bomb[:y] + 8, 6, 2)
|
||||||
@@ -157,29 +199,63 @@ def TIC
|
|||||||
rect(player[:pixelX] + 2, player[:pixelY] + 2, PLAYER_SIZE, PLAYER_SIZE, player[:color])
|
rect(player[:pixelX] + 2, player[:pixelY] + 2, PLAYER_SIZE, PLAYER_SIZE, player[:color])
|
||||||
end
|
end
|
||||||
|
|
||||||
# check death by explosion for all players
|
# score display
|
||||||
$players.each do |player|
|
print("#{$score[0]}:#{$score[1]}", 5, 2, 12)
|
||||||
$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 human player death by touching AI enemy
|
print("ARROWS:MOVE A:BOMB", 60, 2, 15)
|
||||||
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)
|
|
||||||
human = $players[0]
|
human = $players[0]
|
||||||
available = human[:maxBombs] - human[:activeBombs]
|
available = human[:maxBombs] - human[:activeBombs]
|
||||||
print("BOMBS:#{available}/#{human[:maxBombs]}", 170, 2, 11)
|
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
|
end
|
||||||
|
|
||||||
# common movement animation for all players
|
# common movement animation for all players
|
||||||
@@ -238,26 +314,45 @@ end
|
|||||||
def update_ai(player)
|
def update_ai(player)
|
||||||
return if player[:moving]
|
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
|
player[:moveTimer] += 1
|
||||||
return if player[:moveTimer] < 20
|
return if player[:moveTimer] < 20
|
||||||
|
|
||||||
player[:moveTimer] = 0
|
player[:moveTimer] = 0
|
||||||
|
ai_move_and_bomb(player)
|
||||||
# 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
|
|
||||||
ai_move_and_bomb(player)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if a position is dangerous (bomb blast zone or explosion)
|
# check if a position is dangerous (bomb blast zone or explosion)
|
||||||
@@ -344,17 +439,51 @@ def try_escape_any_direction(player)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# escape immediately after placing bomb
|
# escape immediately after placing bomb - choose best direction
|
||||||
def escape_from_own_bomb(player)
|
def escape_from_own_bomb(player)
|
||||||
dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]].shuffle
|
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|
|
dirs.each do |dir|
|
||||||
newX = player[:gridX] + dir[0]
|
newX = player[:gridX] + dir[0]
|
||||||
newY = player[:gridY] + dir[1]
|
newY = player[:gridY] + dir[1]
|
||||||
if can_move_to?(newX, newY)
|
|
||||||
player[:gridX] = newX
|
next unless can_move_to?(newX, newY)
|
||||||
player[:gridY] = newY
|
|
||||||
return
|
score = 0
|
||||||
|
|
||||||
|
# strongly prefer positions outside our new bomb's blast zone
|
||||||
|
if !in_blast_zone?(newX, newY, bombGridX, bombGridY)
|
||||||
|
score += 100
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -371,27 +500,88 @@ def has_adjacent_breakable_wall?(gridX, gridY)
|
|||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if enemy has a safe escape route after placing bomb
|
# check if a position would be in blast zone of a bomb at bombX, bombY
|
||||||
def has_escape_route?(gridX, gridY)
|
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 = [[0, -1], [0, 1], [-1, 0], [1, 0]]
|
||||||
dirs.each do |dir|
|
dirs.each do |dir|
|
||||||
newX = gridX + dir[0]
|
newX = gridX + dir[0]
|
||||||
newY = gridY + dir[1]
|
newY = gridY + dir[1]
|
||||||
# check if can move there and it's not in bomb blast zone
|
if can_move_to?(newX, newY) && !is_dangerous?(newX, newY)
|
||||||
if can_move_to?(newX, newY)
|
queue << [newX, newY, 1] # position and distance
|
||||||
# check one more step for safety
|
visited["#{newX},#{newY}"] = true
|
||||||
dirs.each do |dir2|
|
end
|
||||||
safeX = newX + dir2[0]
|
end
|
||||||
safeY = newY + dir2[1]
|
|
||||||
if can_move_to?(safeX, safeY)
|
while !queue.empty?
|
||||||
return true
|
cx, cy, dist = queue.shift
|
||||||
end
|
|
||||||
|
# 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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
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
|
# place bomb for any player
|
||||||
def place_bomb(player)
|
def place_bomb(player)
|
||||||
return if player[:activeBombs] >= player[:maxBombs]
|
return if player[:activeBombs] >= player[:maxBombs]
|
||||||
|
|||||||
Reference in New Issue
Block a user