mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-27 21:23:30 -08:00
Translate program 55, the Game of Life, into ruby
This commit is contained in:
160
55 Life/ruby/life.rb
Executable file
160
55 Life/ruby/life.rb
Executable file
@@ -0,0 +1,160 @@
|
||||
#!ruby
|
||||
|
||||
# The Pattern class encapsulates everything we would want to know about a
|
||||
# pattern in our Game of Life: its size, its current pattern of alive and
|
||||
# dead cells, which generation it's on and how long it can run, and how
|
||||
# to accept input for itself, print itself, and most importantly iterate
|
||||
# from one generation to the next.
|
||||
|
||||
PATTERN_WIDTH = 80
|
||||
PATTERN_HEIGHT = 24
|
||||
|
||||
class Pattern
|
||||
|
||||
# Begin with a totally empty/dead pattern of fixed size at generation 0.
|
||||
|
||||
def initialize(max_generations: 10)
|
||||
@max_generations = max_generations
|
||||
@generation = 0
|
||||
@population = nil
|
||||
@counter = Array.new(PATTERN_HEIGHT) { Array.new(PATTERN_WIDTH, 0) }
|
||||
@invalid = false
|
||||
end
|
||||
|
||||
# Take input from the console and enter it into the pattern.
|
||||
|
||||
def get_input
|
||||
input = []
|
||||
done = false
|
||||
|
||||
# Accept the input from the user on the console
|
||||
|
||||
loop do
|
||||
print "? "
|
||||
line = gets.chomp
|
||||
break if line == 'DONE'
|
||||
line.gsub!(/^\./, ' ')
|
||||
input << line
|
||||
end
|
||||
|
||||
# Emit some blank space
|
||||
(1..10).each { puts }
|
||||
|
||||
# Center the input in the two-dimensional array
|
||||
input_width = input.map { |line| line.length }.max
|
||||
width_offset = ((PATTERN_WIDTH - input_width) / 2).floor
|
||||
height_offset = ((PATTERN_HEIGHT - input.count) / 2).floor
|
||||
# TODO emit error if width > PATTERN_WIDTH or line count > PATTERN_HEIGHT
|
||||
|
||||
# Start by setting each element to 0 if dead or a 1 if alive
|
||||
|
||||
input.each_index do |y|
|
||||
line = input[y].ljust(input_width)
|
||||
y_offset = y + height_offset
|
||||
@counter[y_offset] = [
|
||||
[0] * width_offset,
|
||||
line.split("").map { |char| char == ' ' ? 0 : 1 },
|
||||
[0] * PATTERN_WIDTH
|
||||
].flatten.take(PATTERN_WIDTH)
|
||||
end
|
||||
|
||||
@population = @counter.flatten.sum
|
||||
end
|
||||
|
||||
# Emit the pattern to the console.
|
||||
|
||||
def display
|
||||
puts "GENERATION:#{@generation}\tPOPULATION:#{@population}"
|
||||
puts "INVALID!"if @invalid
|
||||
|
||||
@counter.each do |row|
|
||||
puts row.map { |cell| ((cell == 0 || cell == 2) ? ' ' : 'X') }.join('')
|
||||
end
|
||||
end
|
||||
|
||||
# Iterate from one generation to the next, returning true if the
|
||||
# game of life should continue, and false if it should terminate.
|
||||
|
||||
def iterate
|
||||
@generation = @generation + 1
|
||||
return false if @generation > @max_generations
|
||||
|
||||
# Update the counter array with new values.
|
||||
# First, change each 2 (dying) to a 0 (dead)
|
||||
# and each 3 (born) to a 1 (alive)
|
||||
@counter.map! { |row| row.map! { |cell| cell >= 2 ? cell-2 : cell } }
|
||||
|
||||
# Now for each cell, count its neighbors and update it
|
||||
|
||||
@population = 0
|
||||
@counter.each_index do |rownum|
|
||||
@counter[rownum].each_index do |colnum|
|
||||
cell_value = @counter[rownum][colnum]
|
||||
|
||||
# If any cell on the border is set, our small algorithm is not
|
||||
# smart enough to correctly check its neighbors, so sadly our
|
||||
# pattern becomes invalid. We keep going though
|
||||
|
||||
@invalid = true if cell_value > 0 && (
|
||||
rownum == 0 || rownum == PATTERN_HEIGHT-1 || colnum == 0 || colnum == PATTERN_WIDTH-1
|
||||
)
|
||||
|
||||
# Count the cell's neighbors (not including itself)
|
||||
|
||||
neighbors = @counter[rownum-1..rownum+1].map { |row| row[colnum-1..colnum+1] }.flatten
|
||||
neighbor_count = neighbors.inject(0) do |sum, value|
|
||||
sum += (value == 1 || value == 2) ? 1 : 0
|
||||
end
|
||||
neighbor_count = neighbor_count - cell_value
|
||||
|
||||
# Update this cell based on its neighbor count, either leaving it
|
||||
# as a 0 or 1, or setting it to 2 (dying) or 3 (being born)
|
||||
|
||||
if cell_value == 0
|
||||
if neighbor_count == 3
|
||||
cell_value = 3
|
||||
@population = @population + 1
|
||||
end
|
||||
elsif neighbor_count < 2 || neighbor_count > 3
|
||||
cell_value = 2
|
||||
else
|
||||
@population = @population + 1
|
||||
end
|
||||
@counter[rownum][colnum] = cell_value
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# If every cell is dead, we are done. Otherwise, keep going up to the
|
||||
# maximum number of generations
|
||||
|
||||
@population > 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# The following program code makes use of the Pattern class to create,
|
||||
# iterate on, and display the Game of Life.
|
||||
|
||||
def display_banner
|
||||
puts " " * 34 + "LIFE"
|
||||
puts " " * 15 + "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"
|
||||
puts
|
||||
puts "PLEASE ENTER YOUR STARTING PATTERN, USING SPACE FOR AN EMPTY CELL"
|
||||
puts "AND AN 'X' FOR A FILLED CELL. YOUR PATTERN MAY BE UP TO #{PATTERN_HEIGHT} ROWS"
|
||||
puts "OF UP TO #{PATTERN_WIDTH} COLUMNS EACH. TYPE 'DONE' WHEN DONE."
|
||||
puts "ENTER YOUR PATTERN:"
|
||||
end
|
||||
|
||||
def main
|
||||
|
||||
display_banner
|
||||
|
||||
pattern = Pattern.new
|
||||
pattern.get_input
|
||||
pattern.display
|
||||
pattern.display while pattern.iterate
|
||||
|
||||
end
|
||||
|
||||
main
|
||||
Reference in New Issue
Block a user