Skip to content

Instantly share code, notes, and snippets.

@nhessler
Forked from abachman/conway.rb
Last active September 21, 2015 14:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhessler/32c97dee0e42bfceedf8 to your computer and use it in GitHub Desktop.
Save nhessler/32c97dee0e42bfceedf8 to your computer and use it in GitHub Desktop.
An implementation of Conway's Game of Life in Ruby based on Sets
# Conway's Game of Life
# 2015-09-18
# by
# Nathan Hessler https://twitter.com/spune
# Adam Bachman https://twitter.com/abachman
# at Ruby DCamp 2015 http://rubydcamp.org/
# awesome test library
def assert(value)
if !value
raise "Failed"
else
print '.'
end
end
def assert_equal(expect, value)
if expect != value
raise "expected #{value} to equal #{expect}"
else
print '.'
end
end
# # guard-style autotesting with https://github.com/emcrisostomo/fswatch
# if fork
# exec "fswatch -o conway.rb | xargs -n1 -I{} ruby conway.rb"
# end
puts
print 'running '
############################
# IMPLEMENTATION GOES HERE #
############################
require 'set'
require 'io/console'
RULES = [
[true, 2],
[true, 3],
[false, 3],
]
# Message passing, down the pile
class World
def initialize(cells=nil)
@cells = cells || Set.new
end
def add_cell(cell)
@cells.add cell
end
def add_coord(x, y)
@cells.add Cell.at(x, y)
end
def candidates
Set.new(cells.map {|c| c.neighbors.to_a}.flatten)
end
def neighbor_count(cell)
# add 1 for every neighbor that is already a member of @cells
cell.neighbors.inject(0) {|memo, obj|
(obj != cell && @cells.include?(obj)) ? memo + 1 : memo
}
end
def step
# add candidates that survive or are born to the next round
World.new(Set.new(candidates.select do |cell|
RULES.include?([@cells.include?(cell), neighbor_count(cell)])
end))
end
def cells
@cells
end
def ==(other)
@cells == other.cells
end
def to_s
@cells.inspect
end
end
# make sure we have the cartesian set of [1, -1, 0] and [1, -1, 0]
OFFSETS = Set.new([1, 1, 0, 0, -1, -1].permutation(2).map {|c| c})
CELL_LIBRARY = {}
class Cell
attr_reader :x, :y
# only ever allocate one instance of Cell at a given x, y
def self.at(x, y)
# 2**x * 3**y is safe as a key function since it is guaranteed to produce a
# unique value for each pair of given x, y
#
# https://en.wikipedia.org/wiki/Fundamental_theorem_of_arithmetic
CELL_LIBRARY[2 ** x * 3 ** y] ||= new(x, y)
end
def initialize(x, y)
@x = x
@y = y
end
def coordinates
@coordinates ||= [@x, @y]
end
def neighbors
@neighbors ||= Set.new(OFFSETS.map {|(x, y)|
Cell.at(x + @x, y + @y)
})
end
def ==(other)
self.coordinates == other.coordinates
end
def eql?(other)
self == other
end
def hash
coordinates.hash
end
end
# test cell API
cell = Cell.at(0, 0)
assert_equal [0, 0], cell.coordinates
# test cell neighbor method
expect = Set.new(
[[-1,-1], [-1, 0], [0, -1], [1, 0], [0, 0], [0,1], [1,1], [1,-1], [-1,1]].map {|coord|
Cell.at(*coord)
}
)
assert_equal expect.size, cell.neighbors.size
assert_equal expect, cell.neighbors
# test cell comparison
cell2 = Cell.at(0,0)
assert_equal cell, cell2
# test stable world
world = World.new
world.add_coord(0, 0)
world.add_coord(0, 1)
world.add_coord(1, 0)
world.add_coord(1, 1)
assert_equal(world, world.step)
# test dying world
empty_world = World.new
world = World.new
world.add_coord(0, 0)
assert_equal(empty_world, world.step)
# test oscillator world
world = World.new
world.add_coord(0, 0)
world.add_coord(0, 1)
world.add_coord(0, 2)
world2 = World.new
world2.add_coord(0, 1)
world2.add_coord(-1, 1)
world2.add_coord(1, 1)
assert_equal(world2, world.step)
# Rendering the R-Pentamino
#
# - - - - -
# - - x x -
# - - x - -
# - x x - -
# - - x - -
# - - - - -
#
r = World.new
winsize = IO.console.winsize
width = winsize[1] - 1
height = winsize[0]
# 0, 0 is in the top left corner, offset pattern to the middle of the window
hw = (width / 2).floor
hh = (height / 2).floor
r.add_coord(0 + hw, 1 + hh)
r.add_coord(0 + hw, 2 + hh)
r.add_coord(1 + hw, 0 + hh)
r.add_coord(1 + hw, 1 + hh)
r.add_coord(2 + hw, 1 + hh)
# render the active world within the terminal
class Renderer
def initialize(width, height)
min_x, max_x = 0, width
min_y, max_y = 0, height
@x_range = (min_x..max_x)
@y_range = (min_y..max_y)
end
def draw(world)
page = ""
@y_range.each do |y|
line = ""
@x_range.each do |x|
line += world.cells.include?(Cell.at(x, y)) ? '💩' : ' '
end
page += line
end
puts page
end
end
renderer = Renderer.new(width, height)
1000.times do
renderer.draw(r)
r = r.step
sleep 0.1
end
###############
puts ' done'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment