Files
basic-computer-games/55_Life/ruby/life.rb
Chris Reuter d26dbf036a Removed spaces from top-level directory names.
Spaces tend to cause annoyances in a Unix-style shell environment.
This change fixes that.
2021-11-21 18:30:21 -05:00

161 lines
4.5 KiB
Ruby
Executable File

#!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