Files
mist/utils/maze.rb
T
syedm 045200bf9f Props are now collectible;
Props logic refractor;
maze coordinates cleanup
2026-03-28 18:09:58 +00:00

288 lines
7.1 KiB
Ruby

class MazeData
def initialize(width, height, standalone: false)
@width = width
@height = height
@grid = Array.new(height) { Array.new(width, 0) }
@boss_rooms = []
@start_room = nil
crate_rooms!
carve_passages_from(0, 0)
fix_room_entrances!
delete_random_walls!
return if standalone
$bus.on(:start_room_coords) do
next @start_room
end
$bus.on(:room?) do |gx, gy|
([@start_room] + @boss_rooms).any? do |rx, ry|
gx.between?(rx, rx + BOSS_ROOM_SIZE) &&
gy.between?(ry, ry + BOSS_ROOM_SIZE)
end
end
return unless DEBUG
print_debug
$bus.on(:boss_room_coords) do
next @boss_rooms.first ? [@boss_rooms.first[0] * 2 + 1, @boss_rooms.first[1] * 2 + 1] : nil
end
end
def solve(x1, y1, x2, y2)
# explain why dijkstra's is fine here:
# A* hueristics make it possible to chose a longer path through rooms instead of a shorter path through corridors, which is not what we want for enemy pathfinding
# Dijkstra's is also simpler to implement since we don't need to worry about the heuristic function, and the maze is not large enough for performance to be a concern
distances = Array.new(@height) { Array.new(@width, Float::INFINITY) }
visited = Array.new(@height) { Array.new(@width, false) }
previous = Array.new(@height) { Array.new(@width, nil) }
distances[y1][x1] = 0
queue = [[y1, x1]]
while !queue.empty?
cy, cx = queue.shift
next if visited[cy][cx]
visited[cy][cx] = true
return build_path(previous, x1, y1, x2, y2) if cx == x2 && cy == y2
# can check NSEW walls here to determine which neighbors to add to the queue (no need to check teh neighbors)
[N, S, E, W].each do |direction|
next if (@grid[cy][cx] & direction) == 0 # wall in this direction
nx, ny = cx + DX[direction], cy + DY[direction]
next unless ny.between?(0, @height - 1) && nx.between?(0, @width - 1)
alt = distances[cy][cx] + 1
if alt < distances[ny][nx]
distances[ny][nx] = alt
previous[ny][nx] = [cx, cy]
queue << [ny, nx]
end
end
end
return nil
end
def build_path(previous, x1, y1, x2, y2)
path = []
cx, cy = x2, y2
while cx != x1 || cy != y1
return nil unless previous[cy][cx]
path << [cx, cy]
cx, cy = previous[cy][cx]
end
path << [x1, y1]
path.reverse!
end
def width
@width * 2 + 1
end
def height
@height * 2 + 1
end
def wall_type(gx, gy)
mask = 0
return mask unless wall_at?(gx, gy)
mask |= N if gy > 0 && wall_at?(gx, gy - 1)
mask |= S if gy < height - 1 && wall_at?(gx, gy + 1)
mask |= E if gx < width - 1 && wall_at?(gx + 1, gy )
mask |= W if gx > 0 && wall_at?(gx - 1, gy )
return mask
end
def wall_at?(gx, gy)
return true if gx == 0 || gy == 0 || gx == width - 1 || gy == height - 1
@boss_rooms.each do |rx, ry|
display_x1 = rx * 2 + 1
display_y1 = ry * 2 + 1
display_x2 = display_x1 + (BOSS_ROOM_SIZE - 1) * 2
display_y2 = display_y1 + (BOSS_ROOM_SIZE - 1) * 2
return false if gx.between?(display_x1, display_x2) && gy.between?(display_y1, display_y2)
end
return false if gx.odd? && gy.odd?
if gx.odd? && gy.even?
cx = (gx - 1) / 2
cy = (gy - 1) / 2
return (@grid[cy][cx] & S) == 0
end
if gx.even? && gy.odd?
cx = (gx - 1) / 2
cy = (gy - 1) / 2
return (@grid[cy][cx] & E) == 0
end
true
end
def print_debug
wall_chars = {
0 => " ", # NONE
1 => "╵", # N
2 => "╷", # S
3 => "│", # NS
4 => "╴", # W
5 => "┘", # NW
6 => "┐", # WS
7 => "┤", # WNS
8 => "╶", # E
9 => "└", # NE
10 => "┌", # ES
11 => "├", # ENS
12 => "─", # EW
13 => "┴", # EWN
14 => "┬", # EWS
15 => "┼" # EWNS
}
(0...self.height).each do |y|
(0...self.width).each do |x|
print wall_chars[wall_type(x, y)]
end
puts
end
end
private
BOSS_ROOM_SIZE = 3
BOSS_ROOM = 16
N, S, W, E = 1, 2, 4, 8
DX = { E => 1, W => -1, N => 0, S => 0 }
DY = { E => 0, W => 0, N => -1, S => 1 }
OPPOSITE = { E => W, W => E, N => S, S => N }
def room_free?(x, y)
(y - 2...(y + BOSS_ROOM_SIZE + 2)).each do |j|
(x - 2...(x + BOSS_ROOM_SIZE + 2)).each do |i|
return false if @grid[j][i] & BOSS_ROOM != 0
end
end
true
end
def fix_room_entrances!
(@boss_rooms + [@start_room]).each do |x, y|
dir = [N, S, E, W].sample
cx = x + BOSS_ROOM_SIZE / 2
cy = y + BOSS_ROOM_SIZE / 2
case dir
when N
@grid[y][cx] |= N
@grid[y - 1][cx] |= S
when S
by = y + BOSS_ROOM_SIZE - 1
@grid[by][cx] |= S
@grid[by + 1][cx] |= N
when W
@grid[cy][x] |= W
@grid[cy][x - 1] |= E
when E
bx = x + BOSS_ROOM_SIZE - 1
@grid[cy][bx] |= E
@grid[cy][bx + 1] |= W
end
end
end
def crate_rooms!
base = Math.sqrt(@width * @height / 200.0).round
num_rooms = rand((base - 1)..(base))
num_rooms = (num_rooms < 1 ? 1 : num_rooms) + 1 # for start room
attempts = 0
rooms = []
while rooms.size < num_rooms && attempts < 10
attempts += 1
x = rand(2..(@width - BOSS_ROOM_SIZE - 2))
y = rand(2..(@height - BOSS_ROOM_SIZE - 2))
next unless room_free?(x, y)
(y...(y + BOSS_ROOM_SIZE)).each do |j|
(x...(x + BOSS_ROOM_SIZE)).each do |i|
cell = N | S | E | W
cell |= BOSS_ROOM
cell &= ~N if j == y
cell &= ~S if j == y + BOSS_ROOM_SIZE - 1
cell &= ~W if i == x
cell &= ~E if i == x + BOSS_ROOM_SIZE - 1
@grid[j][i] = cell
end
end
rooms << [x, y]
end
@start_room = rooms.shift
@boss_rooms = rooms
end
def carve_passages_from(cx, cy)
directions = [N, S, E, W].shuffle
directions.each do |direction|
nx, ny = cx + DX[direction], cy + DY[direction]
if ny.between?(0, @height - 1) && nx.between?(0, @width - 1) && @grid[ny][nx] == 0
@grid[cy][cx] |= direction
@grid[ny][nx] |= OPPOSITE[direction]
carve_passages_from(nx, ny)
end
end
end
def delete_random_walls!
(0...@height).each do |y|
(0...@width).each do |x|
next if rand > 0.05 # 5% chance to delete a wall
# skip boss rooms
next if (@grid[y][x] & BOSS_ROOM) != 0
# pick random direction
dir = [N, S, E, W].sample
nx, ny = x + DX[dir], y + DY[dir]
# bounds check
next unless ny.between?(0, @height - 1) && nx.between?(0, @width - 1)
# skip if neighbor is boss room
next if (@grid[ny][nx] & BOSS_ROOM) != 0
# remove wall both sides
@grid[y][x] |= dir
@grid[ny][nx] |= OPPOSITE[dir]
end
end
end
end
if __FILE__ == $0
maze = MazeData.new(80, 25, standalone: true)
maze.print_debug
end