Dungeon Generation (#80)

Hi all,

I did the quiz :slight_smile: I am new to Ruby so any tips are appreciated.

Hooray! :slight_smile: Iโ€™ve had a quick squiz through your code, and I think I
understand whatโ€™s going on. Iโ€™ll play with it a bit more tonight.

Cheers,
Benjohn

Hi all,

I did the quiz :slight_smile: I am new to Ruby so any tips are appreciated.

View at

http://curi.us/code/dungeon.html

or below:

dungeon.rb Version 1.0

Elliot T.

May 31, 2006

This is my first Ruby Q. entry

For Ruby Q. #80

Ruby Quiz - Dungeon Generation (#80)

Generates an ASCII dungeon with an evolutionary algorithm. Makes

random

changes and calls undo if the evaluate method returns a lower number.

Continues for a while, then makes sure to get valid output.

It works but could benefit from tuning various numbers and some new

features

in the evaluate function. It could also be faster. Sample output at

bottom.

class Tile
attr_accessor :x, :y
@@TileType = Struct.new(:graphic, :frequency, :walkable)
@@data = {
:wall => @@TileType.new(โ€œ#โ€, 250, false),
:open => @@TileType.new(" โ€œ, 120, true),
:water => @@TileType.new(โ€~โ€œ, 10, true),
:stairs_up => @@TileType.new(โ€<โ€œ, 1, true),
:stairs_down => @@TileType.new(โ€>", 1, true)
}
def initialize x, y, type = :any
@x = x
@y = y
if type == :any
@type_history = [get_rand_tile]
else
@type_history = [type]
end
end
def type
@type_history.last
end
def to_s
@@data[@type_history.last].graphic
end
def get_rand_tile
total = @@data.inject(0) { |total, pair| total + pair
[1].frequency }
@@data.each do |k,v|
return k if rand(total) < v.frequency
total -= v.frequency
end
end
def random_change
@type_history << get_rand_tile
end
def undo(n=1)
n.times do
@type_history.pop
end
end
def walkable?
@@data[@type_history.last].walkable
end
end

class Map
def initialize(height,width)
@height = height
@width = width
@map = []
@changeable_tiles = []
@last_changed = []
width.times do |i|
column = []
height.times do |j|
if (j == 0) or (j == width - 1) or (i == 0) or (i == width - 1)
column << Tile.new(i, j, :wall)
else
tmp = Tile.new(i, j)
column << tmp
@changeable_tiles << tmp
end
end
@map << column
end
@changeable_tiles = @changeable_tiles.sort_by {rand}
# x = @changeable_tiles.shift
# x.become_stairs_up
# x = @changeable_tiles.shift
# x.become_stairs_down
end
def to_s
# old version that put # around the output
# โ€˜#โ€™ * (@width+2) + โ€œ\nโ€ + (@map.collect { |row| โ€˜#โ€™ +
row.collect {|tile| tile.to_s}.join(โ€œโ€) + โ€˜#โ€™ }.join โ€œ\nโ€) + โ€œ\nโ€ +
โ€˜#โ€™ * (@width+2)
@map.collect { |row| row.collect {|tile| tile.to_s}.join
(โ€œโ€) }.join โ€œ\nโ€
end

def update n=1
n.times do
x = @changeable_tiles[rand(@changeable_tiles.length)]
x.random_change
@last_changed << x
end
end

def undo n=1
n.times do
@last_changed.pop.undo
end
end

def path_between start, destination, exclude = []
return false if start.nil?
return true if start == destination
return false unless start.walkable?
return false if exclude.include?(start)
exclude << start
path_between(self.down(start), destination, exclude) or
path_between(self.up(start), destination, exclude) or path_between
(self.left(start), destination, exclude) or path_between(self.right
(start), destination, exclude)
end

def path_between2 start, destination
g = find_group(start)
g.include?(destination)
end

def find_group start, walkable = true, group = []
return group if start.nil?
return group unless start.walkable? == walkable
return group if group.include?(start)
group << start
find_group(self.down(start), walkable, group)
find_group(self.up(start), walkable, group)
find_group(self.left(start), walkable, group)
find_group(self.right(start), walkable, group)
return group
end

def count_groups walkable = true
tiles = @map.flatten.select { |tile| tile.walkable? == walkable }
count = 0
while tiles.any?
count += 1
tiles -= find_group(tiles[0], walkable)
end
count
end

def left tile
@map[tile.x - 1][tile.y] rescue nil
end
def right tile
@map[tile.x + 1][tile.y] rescue nil
end
def down tile
@map[tile.x][tile.y - 1] rescue nil
end
def up tile
@map[tile.x][tile.y + 1] rescue nil
end

def stair_distance
(find_one(:stairs_up).x - find_one(:stairs_down).x).abs +
(find_one(:stairs_up).y - find_one(:stairs_down).y).abs
end

def find_one tile_type
@map.flatten.detect {|tile| tile_type == tile.type}
end

def number_of tile_type
@map.flatten.inject(0) do |total, tile|
tile.type == tile_type ? total + 1 : total
end
end

def valid?
return false unless number_of(:stairs_up) == 1
return false unless number_of(:stairs_down) == 1
return false unless path_between(find_one(:stairs_up), find_one
(:stairs_down))
true
end

def evaluate
score = 0
score -= 200 unless valid?
if (number_of(:stairs_up) == 1) && (number_of(:stairs_down) == 1)
score += 200 * stair_distance
end
score -= 100 * count_groups(true)
score -= 70 * count_groups(false)
end
end

map = Map.new 15,15

tmp = map.to_s
map.update 50
map.undo 40
map.update 50
map.undo 60
raise โ€œundo bugโ€ unless tmp == map.to_s

valid_steps = 0
e = map.evaluate
n = 25
undos = 0

2000.times do
map.update n
if map.evaluate >= e
e = map.evaluate
else
map.undo n
undos += 1
end
end

until map.valid?
map.update 1
valid_steps += 1
end

puts map
puts โ€œsteps to validate #{valid_steps}โ€
puts โ€œundos #{undos}โ€
puts โ€œstair distance is #{map.stair_distance}โ€
puts map.count_groups
puts map.count_groups(false)
puts map.evaluate

=begin
Sample Output:

###############

##### ##

## ## ###

#

> ##

##

####### ## # ##

### ##

## #

#### #~~###

#### ##

####~ #####

# ####

<##

###############
steps to validate 0
undos 1959
stair distance is 11
4
1
1730
=end

โ€“ Elliot T.