game_of_life 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ .yardoc/
3
+ doc/
data/.rdebugrc ADDED
@@ -0,0 +1,4 @@
1
+ set autoeval on
2
+ set history save on
3
+ set listsize 12
4
+ set autolist on
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2@game_of_life --create
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - rbx
7
+ - rbx
8
+ - ree
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --readme README.md - notes.md
2
+ --title 'Game Of Life Documentation'
3
+ --output-dir doc
4
+ --protected
5
+ --private
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rake"
4
+ # gem "ruby-debug19"
5
+
6
+ group :test do
7
+ gem "rspec"
8
+ gem "cucumber"
9
+ end
10
+
11
+ gem "yard", :group => [:development, :test]
12
+ # Need redcarpet for markdown formatting (using yard)
13
+ gem "redcarpet", :group => [:development, :test]
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Arnab Deka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Conway's Game of Life, in Ruby
2
+ [![Build Status](https://secure.travis-ci.org/arnab/game_of_life.png?branch=master)][travis]
3
+ [travis]: http://travis-ci.org/arnab/game_of_life
4
+
5
+ ## About
6
+ See the [notes file](https://github.com/arnab/game_of_life/blob/master/notes.md) for details about my thoughts.
7
+
8
+ ## Setup
9
+ * If you are using rvm, as soon as you cd into this directory a gemset will be created, wherein all the gems will be installed by bundler
10
+ * So the only steps you need to take are:
11
+ 1. `gem install bundler`
12
+ 2. `bundle install`
13
+
14
+ ## How to play
15
+ * Run the cucumber steps to see examples.
16
+ * You can play with the code with the simple script provided:
17
+ `
18
+ ./scripts/play_game.rb examples/pulsar.txt
19
+ `
20
+
21
+ ## Documentation
22
+ ### Hosted
23
+ * Available at [http://rubydoc.info/github/arnab/game_of_life/](http://rubydoc.info/github/arnab/game_of_life/)
24
+
25
+ ### Build your own
26
+ * Follow setup steps above if you have not already
27
+ * Generate the yard dpcumentation by running: `yard server --reload`
28
+ * Then you can see it at http://localhost:8808/
29
+
30
+ ## Supported Rubies
31
+ [Tested against][travis] the following Ruby implementations:
32
+
33
+ * Ruby 1.8.7
34
+ * Ruby 1.9.2
35
+ * Ruby 1.9.3
36
+ * [Rubinius][]
37
+ * [Ruby Enterprise Edition][ree]
38
+
39
+ [rubinius]: http://rubini.us/
40
+ [ree]: http://www.rubyenterpriseedition.com/
41
+
42
+ ##Copyright
43
+ Copyright (c) 2012 Arnab Deka.
44
+ See [LICENSE][] for details.
45
+
46
+ [license]: https://github.com/arnab/game_of_life/blob/master/LICENSE.md
47
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'yard'
2
+ require 'yard/rake/yardoc_task'
3
+ require 'rspec/core/rake_task'
4
+ require 'cucumber/rake/task'
5
+
6
+ YARD::Rake::YardocTask.new
7
+ RSpec::Core::RakeTask.new
8
+ Cucumber::Rake::Task.new
9
+
10
+ task :test => [:spec, :cucumber]
11
+ task :default => :test
data/cucumber.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ default: --profile sane
3
+ sane: --format pretty --tags ~@wip
4
+ wip: --tags @wip
@@ -0,0 +1,3 @@
1
+ - X -
2
+ - X X
3
+ X - X
data/examples/lwss.txt ADDED
@@ -0,0 +1,4 @@
1
+ - X - - X
2
+ X - - - -
3
+ X - - - X
4
+ X X X X -
@@ -0,0 +1,15 @@
1
+ - - - - - - - - - - - - - - -
2
+ - - - X X X - - - X X X - - -
3
+ - - - - - - - - - - - - - - -
4
+ - X - - - - X - X - - - - X -
5
+ - X - - - - X - X - - - - X -
6
+ - X - - - - X - X - - - - X -
7
+ - - - X X X - - - X X X - - -
8
+ - - - - - - - - - - - - - - -
9
+ - - - X X X - - - X X X - - -
10
+ - X - - - - X - X - - - - X -
11
+ - X - - - - X - X - - - - X -
12
+ - X - - - - X - X - - - - X -
13
+ - - - - - - - - - - - - - - -
14
+ - - - X X X - - - X X X - - -
15
+ - - - - - - - - - - - - - - -
@@ -0,0 +1,62 @@
1
+ Feature: Gameplay of the Game of Life
2
+ In order to observe the Game of Life being played out
3
+ As an observer
4
+ I want various scenarios of the game to change the state of the board correctly
5
+
6
+ Scenario: A: Block pattern
7
+ Given that the game is seeded with:
8
+ """
9
+ X X
10
+ X X
11
+ """
12
+ When the next tick occurs
13
+ Then the board's state should change to:
14
+ """
15
+ X X
16
+ X X
17
+ """
18
+
19
+ Scenario: B: Boat pattern
20
+ Given that the game is seeded with:
21
+ """
22
+ X X -
23
+ X - X
24
+ - X -
25
+ """
26
+ When the next tick occurs
27
+ Then the board's state should change to:
28
+ """
29
+ X X -
30
+ X - X
31
+ - X -
32
+ """
33
+
34
+ Scenario: C: Blinker pattern
35
+ Given that the game is seeded with:
36
+ """
37
+ - X -
38
+ - X -
39
+ - X -
40
+ """
41
+ When the next tick occurs
42
+ Then the board's state should change to:
43
+ """
44
+ - - -
45
+ X X X
46
+ - - -
47
+ """
48
+
49
+ Scenario: D: Toad pattern
50
+ Given that the game is seeded with:
51
+ """
52
+ - X X X
53
+ X X X -
54
+ """
55
+ When the next tick occurs
56
+ Then the board's state should change to:
57
+ """
58
+ - - X -
59
+ X - - X
60
+ X - - X
61
+ - X - -
62
+ """
@@ -0,0 +1,41 @@
1
+ Feature: Multi-generation Gameplay of the Game of Life
2
+ In order to observe the Game of Life being played out for multiple generations
3
+ As an observer
4
+ I want various scenarios of the game to change the state of the board continuously
5
+
6
+ Scenario: A: Glider pattern
7
+ Given that the game is seeded with:
8
+ """
9
+ - X -
10
+ - X X
11
+ X - X
12
+ """
13
+ When the next tick occurs
14
+ Then the board's state should change to:
15
+ """
16
+ - X X
17
+ X - X
18
+ - - X
19
+ """
20
+ When the next tick occurs
21
+ Then the board's state should change to:
22
+ """
23
+ - X X -
24
+ - - X X
25
+ - X - -
26
+ """
27
+ When the next tick occurs
28
+ Then the board's state should change to:
29
+ """
30
+ - X X X
31
+ - - - X
32
+ - - X -
33
+ """
34
+ When the next tick occurs
35
+ Then the board's state should change to:
36
+ """
37
+ - - X -
38
+ - - X X
39
+ - X - X
40
+ - - - -
41
+ """
@@ -0,0 +1,16 @@
1
+ def game
2
+ @game ||= GameOfLife::Game.new
3
+ end
4
+
5
+ Given /^that the game is seeded with:$/ do |raw_input|
6
+ formatted_seed_data = GameOfLife::Inputters::SimpleStringInputter.new.parse raw_input
7
+ game.seed(formatted_seed_data)
8
+ end
9
+
10
+ When /^the next tick occurs$/ do
11
+ game.tick
12
+ end
13
+
14
+ Then /^the board's state should change to:$/ do |expected_formatted_output|
15
+ game.board.view.should == expected_formatted_output
16
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH << File.expand_path('../../../lib', __FILE__)
2
+ require 'game_of_life'
@@ -0,0 +1,6 @@
1
+ require 'game_of_life/game'
2
+ require 'game_of_life/board'
3
+ require 'game_of_life/cell'
4
+ require 'game_of_life/rules'
5
+ require 'game_of_life/outputters/simple_string_outputter'
6
+ require 'game_of_life/inputters/simple_string_inputter'
@@ -0,0 +1,191 @@
1
+ module GameOfLife
2
+ # Raised when a {Board} gets into an invalid shape
3
+ class InvalidBoardError < RuntimeError; end;
4
+
5
+ # The board used in the game. Holds the {Cell}s.
6
+ class Board
7
+
8
+ # The {Cell}s in this board,internally maintained as a 2D Array of {Cell}s.
9
+ # The x-coordinate increases horizontally and is always positive.
10
+ # The y-coordinate increases vertically and is always positive.
11
+ # Internally, the cells are arranged as a 2D array. The first-level Array indexed
12
+ # with the y-coordinates. It contains an Array of {Cell}s, whose position is
13
+ # the x-coordinate.
14
+ # @note Use {#each_cell}, {#each_row} etc. methods to access the cells individually.
15
+ attr_reader :cells
16
+
17
+ # Creates the board
18
+ # @param [2D Array<Symbol>] seed_data the data for the {Cell}s in the board, as an 2D array.
19
+ # @example seed_data looks like
20
+ # the output of SimpleStringInputter#parse
21
+ # @raise [InvalidBoardError] if the seed_data is not in the shape of a square,
22
+ # or if all elements are not present
23
+ def initialize(seed_data)
24
+ @cells = Array.new(Array.new)
25
+ seed_with!(seed_data)
26
+ begin
27
+ validate
28
+ rescue InvalidBoardError => ex
29
+ # Add the seed_data into the error message, so the caller gets a clue
30
+ raise InvalidBoardError, ex.message + " [seed data was: #{seed_data.inspect}]"
31
+ end
32
+ end
33
+
34
+ # @param [OutPutter] the outputter that you want to use. Defaults to {Outputters::SimpleStringOutputter}
35
+ def view(outputter = Outputters::SimpleStringOutputter.new)
36
+ outputter.render(self)
37
+ end
38
+
39
+ def each_row(&block)
40
+ @cells.each { |row| yield row }
41
+ end
42
+
43
+ def each_row_with_index(&block)
44
+ @cells.each_with_index { |row, i| yield row, i }
45
+ end
46
+
47
+ def each_cell(&block)
48
+ @cells.flatten.each { |cell| yield cell }
49
+ end
50
+
51
+ # Find the cell at a given pair of co-ordinates. Following Array symantics,
52
+ # this method returns nil if nothing exists at that location or if the
53
+ # location is out of the board. To avoid Array's negative index symantics it
54
+ # returns nill if a negative index is passed.
55
+ # @param [Integer] x the x-coordinate
56
+ # @param [Integer] y the y-coordinate
57
+ # @return [Cell] or nil
58
+ def cell_at(x, y)
59
+ return nil if (x < 0 || y < 0)
60
+ @cells[y][x] if @cells[y]
61
+ end
62
+
63
+ # Finds the neighbors of a given {Cell}'s co-ordinates. The neighbors are the eight cells
64
+ # that surround the given one.
65
+ # @param [Integer] x the x-coordinate of the cell you find the neighbors of
66
+ # @param [Integer] y the y-coordinate of the cell you find the neighbors of
67
+ # @return [Array<Cell>]
68
+ def neighbors_of_cell_at(x, y)
69
+ neighbors = coords_of_neighbors(x, y).map { |x, y| self.cell_at(x, y) }
70
+ neighbors.reject {|n| n.nil?}
71
+ end
72
+
73
+ # This is the first stage in a Game's #tick.
74
+ # @see Game#tick
75
+ def reformat_for_next_generation!
76
+ # create an array of dead cells and insert it as the first and last row of cells
77
+ dead_cells = (1..@cells.first.size).map { Cell.new }
78
+ # don't forget to deep copy the dead_cells
79
+ @cells.unshift Marshal.load(Marshal.dump(dead_cells))
80
+ @cells.push Marshal.load(Marshal.dump(dead_cells))
81
+
82
+ # also insert a dead cell at the left and right of each row
83
+ @cells.each do |row|
84
+ row.unshift Cell.new
85
+ row.push Cell.new
86
+ end
87
+
88
+ # validate to see if we broke the board
89
+ validate
90
+ end
91
+
92
+ # Goes through each {Cell} and marks it (using {Rules}) to signal it's state for the next
93
+ # generation. This prevents modifying any {Cell} in-place as each generation is a pure
94
+ # function of the previous. Once all {Cell}s are marked, it sweeps across them gets them
95
+ # to change their state if they were marked to.
96
+ # @see #mark_changes_for_next_generation
97
+ # @see #sweep_changes_for_next_generation!
98
+ def mark_and_sweep_for_next_generation!
99
+ mark_changes_for_next_generation
100
+ sweep_changes_for_next_generation!
101
+ end
102
+
103
+ # This is the third and last stage in a Game's #tick.
104
+ # @see Game#tick
105
+ def shed_dead_weight!
106
+ # Remove the first and last rows if all cells are dead
107
+ @cells.shift if @cells.first.all? { |cell| cell.dead? }
108
+ @cells.pop if @cells.last.all? { |cell| cell.dead? }
109
+
110
+ # Remove the first cell of every row, if they are all dead
111
+ first_columns = @cells.map { |row| row.first }
112
+ if first_columns.all? { |cell| cell.dead? }
113
+ @cells.each { |row| row.shift }
114
+ end
115
+
116
+ # Remove the last cell of every row, if they are all dead
117
+ last_columns = @cells.map { |row| row.last }
118
+ if last_columns.all? { |cell| cell.dead? }
119
+ @cells.each { |row| row.pop }
120
+ end
121
+
122
+ validate
123
+ end
124
+
125
+ private
126
+ def validate
127
+ num_o_rows = @cells.size
128
+ columns_in_each_row = @cells.map(&:size)
129
+ unless columns_in_each_row.uniq.size == 1
130
+ msg = "Unequal number of columns, #{columns_in_each_row.inspect} in different rows found"
131
+ raise InvalidBoardError, msg
132
+ end
133
+
134
+ num_o_columns = columns_in_each_row.uniq.first
135
+ num_o_elements = @cells.flatten.reject {|d| d.nil? }.size
136
+ unless (num_o_rows * num_o_columns) == num_o_elements
137
+ msg = "Not a rectangular shape: " +
138
+ "rows(#{num_o_rows}) x columns(#{num_o_columns}) != total elements(#{num_o_elements})]. "
139
+ raise InvalidBoardError, msg
140
+ end
141
+ end
142
+
143
+ def seed_with!(data)
144
+ data.each_with_index do |row, y|
145
+ @cells << []
146
+ row.each_with_index do |state, x|
147
+ @cells[y] << Cell.new(state)
148
+ end
149
+ end
150
+ end
151
+
152
+ def mark_changes_for_next_generation
153
+ self.each_row_with_index do |cells, y|
154
+ cells.each_with_index do |cell, x|
155
+ cell.should_live_in_next_generation =
156
+ Rules.should_cell_live?(self, cell, x, y)
157
+ end
158
+ end
159
+ end
160
+
161
+ def sweep_changes_for_next_generation!
162
+ self.each_cell { |cell| cell.change_state_if_needed! }
163
+ end
164
+
165
+ # Calculates the co-ordinates of neighbors of a given pair of co-ordinates.
166
+ # @param [Integer] x the x-coordinate
167
+ # @param [Integer] y the y-coordinate
168
+ # @return [Array<Integer, Integer>] the list of neighboring co-ordinates
169
+ # @example
170
+ # coords_of_neighbors(1,1) =>
171
+ # [
172
+ # [0, 0], [0, 1], [0, 2],
173
+ # [1, 0], [1, 2],
174
+ # [2, 0], [2, 1], [2, 2],
175
+ # ]
176
+ # @note This method returns all possible co-ordinate pairs of neighbors,
177
+ # so it can contain coordinates of cells not in the board, or negative ones.
178
+ # @see #neighbors_of_cell_at
179
+ def coords_of_neighbors(x, y)
180
+ coords_of_neighbors = []
181
+ (x - 1).upto(x + 1).each do |neighbors_x|
182
+ (y - 1).upto(y + 1).each do |neighbors_y|
183
+ next if (x == neighbors_x) && (y == neighbors_y)
184
+ coords_of_neighbors << [neighbors_x, neighbors_y]
185
+ end
186
+ end
187
+ coords_of_neighbors
188
+ end
189
+
190
+ end
191
+ end
@@ -0,0 +1,37 @@
1
+ module GameOfLife
2
+ class Board
3
+ # An indivisual cell in the Game {Board}
4
+ class Cell
5
+ STATES = [:live, :dead]
6
+ attr_accessor :state, :should_live_in_next_generation
7
+
8
+ # Creates a {Cell}. A cell starts out as dead unless explicitly set alive.
9
+ def initialize(initial_state=:dead)
10
+ raise ArgumentError, "Unknown state #{initial_state}" unless STATES.include? initial_state
11
+ @state = initial_state
12
+ end
13
+
14
+ def change_state_if_needed!
15
+ if self.should_live_in_next_generation
16
+ self.state = :live
17
+ else
18
+ self.state = :dead
19
+ end
20
+ end
21
+
22
+ def alive?
23
+ state == :live
24
+ end
25
+
26
+ def dead?
27
+ ! alive?
28
+ end
29
+
30
+ def to_s
31
+ fields = %w{state should_live_in_next_generation}
32
+ important_details = fields.map {|attr| "#{attr}:#{self.send(attr)}"}
33
+ "#{super} <#{important_details.join(' ')}>"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module GameOfLife
2
+ class Game
3
+ # the Game Board
4
+ attr_reader :board
5
+
6
+ # Takes the formatted input data and seeds the {Board} with it
7
+ # @param [string] formatted_input
8
+ # @example
9
+ # [
10
+ # [:dead, :live, :live, :live],
11
+ # [:live, :live, nil, :dead]
12
+ # ]
13
+ def seed(formatted_input)
14
+ @board = Board.new(formatted_input)
15
+ end
16
+
17
+ # The event by which the board transitions from the current to the next generation/state.
18
+ # There are three stages in the process:
19
+ # * reformat the board: Each tick can make the neighboring dead cells
20
+ # (which are not technically in the board now) cells alive. So reformat the board to
21
+ # add a new row at the top and bottom an a column at the left and right (all dead cells)
22
+ # * mark and sweep for the next generation
23
+ # * shed the dead weight: In stage 1, we added a layer of dead cells around the current board.
24
+ # If the whole layer is still dead, just remove it as we don't want to keep adding layers of
25
+ # dead weight on every tick.
26
+ # @see Board#mark_and_sweep_for_next_generation!
27
+ def tick
28
+ @board.reformat_for_next_generation!
29
+ @board.mark_and_sweep_for_next_generation!
30
+ @board.shed_dead_weight!
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ module GameOfLife
2
+ # Inputters can parse various kinds of data into a format that is easily converted to {Board}'s {Board::Cell Cell}s. Some forms of possible inputters:
3
+ # JSONInputter would accept JSON objects (which can be posted by a website for example)
4
+ # SimpleStringInputter can be used by tests to verify results
5
+ # FileInputter can read files in a given format
6
+ module Inputters
7
+ # parses an input string for the board data
8
+ class SimpleStringInputter
9
+ # Parses board's data from a string.
10
+ # @example Will parse this input string
11
+ # X - X
12
+ # - - -
13
+ # - X -
14
+ # @example into this output structure:
15
+ # [
16
+ # [:live, :dead, :live],
17
+ # [:dead, :dead, :dead],
18
+ # [:dead, :live, :dead]
19
+ # ]
20
+ # @param [String] raw_input the given string
21
+ # @return [2D Array<Symbol>] formatted board data
22
+ # @raise [ArgumentError] if anything other then an 'X' or '-' as the state
23
+ def parse(raw_input)
24
+ rows = raw_input.split("\n")
25
+ rows.map! { |row| row.split(' ') }
26
+ rows.map! do |row|
27
+ row.map! { |symbol| convert_cryptic_symbol_to_state(symbol) }
28
+ end
29
+ rows
30
+ end
31
+
32
+ private
33
+ def convert_cryptic_symbol_to_state(symbol)
34
+ case symbol
35
+ when 'X'
36
+ :live
37
+ when '-'
38
+ :dead
39
+ else
40
+ raise ArgumentError, "Don't know how to convert #{symbol} into a Cell's state"
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,37 @@
1
+ module GameOfLife
2
+ # Outputters can display/render a board in a specific way. Some forms of possible outputters:
3
+ # JSONOutputter would return the board as a JSON object (which can be used by a website for example)
4
+ # SimpleStringOutputter can be used by tests to verify results
5
+ # ConsoleOutputter can render it appropriately for a console-application
6
+ module Outputters
7
+ # Renders a board as a simple string, that can be used to inspect it from, say a test
8
+ class SimpleStringOutputter
9
+ # Renders the given board as a simple string. Cells are delimited by spaces and rows by newlnes
10
+ # A live cell is marked 'X' and a dead cell with a '-'
11
+ # @param [GameOfLife::Board] board the given board
12
+ # @return [String] the board rendered as a string
13
+ def render(board)
14
+ output = ""
15
+ board.each_row do |row|
16
+ row.each do |cell|
17
+ output << simplified_state(cell.state)
18
+ output << " "
19
+ end
20
+ output.strip!
21
+ output << "\n"
22
+ end
23
+ output.chomp
24
+ end
25
+
26
+ private
27
+ def simplified_state(state)
28
+ case state
29
+ when :live
30
+ 'X'
31
+ when :dead
32
+ '-'
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module GameOfLife
2
+ module Rules
3
+
4
+ # The rules followed are:
5
+ # 1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
6
+ # 2. Any live cell with two or three live neighbours lives on to the next generation.
7
+ # 3. Any live cell with more than three live neighbours dies, as if by overcrowding.
8
+ # 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
9
+ def should_cell_live?(board, cell, x, y)
10
+ live_neighbors_count = board.neighbors_of_cell_at(x, y).select { |n| n.alive? }.size
11
+ case cell.state
12
+ when :live
13
+ (2..3).include? live_neighbors_count
14
+ when :dead
15
+ live_neighbors_count == 3
16
+ end
17
+ end
18
+
19
+ module_function :should_cell_live?
20
+ end
21
+ end
data/notes.md ADDED
@@ -0,0 +1,52 @@
1
+ These are basically just my raw thoughts about the game. See my updates from 1/7 Saturday.
2
+
3
+ # Objects
4
+
5
+ ## GameOfLife
6
+ Top level namespace (module). Has the following things:
7
+
8
+ ### Game
9
+ * incorporates rules
10
+ * gets the input (*seed*)
11
+ * and waits for *tick*s
12
+ * @see the Game class' #tick documentation for more details
13
+
14
+ ### Board
15
+ * has a grid (2d array) of Cells
16
+ * checks integrity after every tick, like:
17
+ - is it a filled shape (rectangular)?
18
+ - we don't do any bounds check as we try not to have our API dictate the client to pass in a bound and then data
19
+ - as long as the board's shape is good and every cell is filled, we are fine
20
+
21
+ #### Cell
22
+ * state: :live or :dead
23
+
24
+ ### Rules
25
+ * Any live cell with fewer than two live neighbours dies, as if by loneliness.
26
+ * Any live cell with more than three live neighbours dies, as if by overcrowding.
27
+ * Any live cell with two or three live neighbours lives, unchanged, to the next generation.
28
+ * Any dead cell with exactly three live neighbours comes to life.
29
+
30
+ ### Player
31
+ * do we need one? there isn't really a real player, but we can assume one which seeds and then observes the Game#ticks *
32
+
33
+ # Analysis
34
+ Space-complexity wise an infinite grid might be tricky because each tick results in a board which is a pure function of the previous state of the board. In other words, if it's really infinite, or very large, it will be impossible to copy the board before every manipulation. Three options that I can see (following discussions assume there are (n x n) *Cell*s in the *Board*):
35
+
36
+ * Mark-And-Sweep: Keep some sort of a has\_changed signifier in each *Cell*. Instead of trying to change the state in-place, flip this boolean field. After all the *Cell*s have been touched, use this field to go through once more and flip the state if required.
37
+ - This increases by the space required by O(n-squared * some-constant-amount-for-a-boolean), which is O(n-squared)
38
+ * Another simple optimization can be to go row-by-row. Every time we reach row n, where n > 2, the (n-2)th row can be flushed (i.e. their state can be changed) since no *Cell* in (n-2) row can be n row's neighbor. This way only 2 rows need to copied in memory at a time.
39
+ - This increases the space required by O(n). But if n is again very large (and not n-squared) this will still not be efficient.
40
+ * A third approach could be to walk the *Board* in a radially outward manner, flushing the earliest-seen-*Cell*s as soon as we are in a neighborhood which is one row/column away from the first generation.
41
+ - This would be very optimal but will be complicated to write. Is it worth it is the question.
42
+
43
+ ## Assumptions and Conslusions
44
+ Given all these considerations and reassurance from my recruiter that we are looking for a *simple* OO Design (and not space-complexity) I am going with alternative 1: mark and sweep (instead of duplicating the data on every tick).
45
+
46
+ # Updates
47
+ ## 1/7 Saturday
48
+ So it turns out some of my assumptions were wrong. Given the `X` and `-` markings in the example for live and dead cells I thought that once seeded the size of the board was constant. It dawns on me now that that the "infiniteness" of the board plays on every tick/generation change. Basically, `X` are live cells but everything else (in all directions) is dead (the ones inside the playing area are marked explicitly with a `-`). So we'll need some changes, but that's why software is designed with principles of design in mind. Let's see how our code adopts.
49
+
50
+ I am going to continue and complete the mark-and-sweep part. That will get scenario C passing. To tackle Scenario D (Toad, which changes the size of the board) as the first pas, we can implement the [self-constructing pattern][1], basically create a new generation of cells and kill the old one. I suspect it won't perform optimally (given we are using a 2D array) but we can leave the optimization for later (perhaps a 2D LinkedList will fare better).
51
+
52
+ [1]: http://en.wikipedia.org/wiki/Conway's_Game_of_Life#Self-replication
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby -KU
2
+
3
+ $LOAD_PATH << './lib'
4
+ require "game_of_life"
5
+
6
+ game = GameOfLife::Game.new
7
+
8
+ file_name = ARGV[0]
9
+ raise "Please provide a seed file as an argument. See inside the examples directory for examples." unless file_name
10
+ raw_input = File.readlines file_name
11
+ raw_input = raw_input.map { |line| line.chomp }.join("\n")
12
+
13
+ formatted_seed_data = GameOfLife::Inputters::SimpleStringInputter.new.parse raw_input
14
+ game.seed(formatted_seed_data)
15
+
16
+ puts "Starting with:"
17
+ puts game.board.view
18
+
19
+ i = 1
20
+ while(true)
21
+ print "\e[2J\e[f"
22
+ puts "Generation: #{i}"
23
+ game.tick
24
+ puts game.board.view
25
+ i += 1
26
+ sleep(1)
27
+ end
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+
3
+ module GameOfLife
4
+ describe Board do
5
+ describe "#initialize" do
6
+ it "should raise Error when seed data contains unequal number of fields across rows" do
7
+ seed_data = [
8
+ [:live, :dead], [:live, :dead, :live]
9
+ ]
10
+ expect {
11
+ Board.new(seed_data)
12
+ }.to raise_error InvalidBoardError, /unequal number of columns/i
13
+ end
14
+
15
+ it "should NOT raise Error when rows != columns, but is a rectangular shape" do
16
+ seed_data = [
17
+ [:live, :dead, :live],
18
+ [:live, :dead, :live]
19
+ ]
20
+ expect {
21
+ Board.new(seed_data)
22
+ }.to_not raise_error InvalidBoardError
23
+ end
24
+
25
+ end
26
+
27
+ describe "#cell_at" do
28
+ let(:board) {
29
+ Board.new([
30
+ [ :live, :live ],
31
+ [ :live, :live ],
32
+ ])
33
+ }
34
+ it "should return nil if the coordinates are negative" do
35
+ board.cell_at(-1, -1).should be_nil
36
+ end
37
+
38
+ it "should return nil if the coordinates are outside the board" do
39
+ board.cell_at(26, 11).should be_nil
40
+ end
41
+ end
42
+
43
+ describe "#neighbors_of" do
44
+ let(:board) {
45
+ Board.new([
46
+ [ :live, :live, :live ],
47
+ [ :live, :live, :live ],
48
+ [ :live, :live, :live ],
49
+ ])
50
+ }
51
+
52
+ it "should return 3 neighbors for the cell at (0,0)" do
53
+ board.neighbors_of_cell_at(0, 0).should have(3).items
54
+ end
55
+
56
+ it "should return 3 neighbors for the cell at (2,2)" do
57
+ board.neighbors_of_cell_at(2, 2).should have(3).items
58
+ end
59
+
60
+ it "should return 5 neighbors for the cell at (1,0)" do
61
+ board.neighbors_of_cell_at(1, 0).should have(5).items
62
+ end
63
+
64
+ it "should return 8 neighbors for the cell at (1,1)" do
65
+ board.neighbors_of_cell_at(1, 1).should have(8).items
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ require "spec_helper"
2
+
3
+ module GameOfLife
4
+ describe Board::Cell do
5
+ describe "#initialize" do
6
+ it "should raise Error when an unknown state is given" do
7
+ expect {
8
+ Board::Cell.new(:in_limbo)
9
+ }.to raise_error ArgumentError, /in_limbo/
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ module GameOfLife
4
+ describe Inputters::SimpleStringInputter do
5
+ describe "#parse" do
6
+ let(:inputter) { GameOfLife::Inputters::SimpleStringInputter.new }
7
+ it "should raise Error when an unknown symbol (other than X or -) is given" do
8
+ str = ["X Y -", "Y Y -"].join("\n")
9
+ expect {
10
+ inputter.parse(str)
11
+ }.to raise_error ArgumentError, /Y/
12
+ end
13
+
14
+ it "should not raise Error when X and - are the only symbols used" do
15
+ str = ["X - -", "- X -"].join("\n")
16
+ expect {
17
+ inputter.parse(str)
18
+ }.to_not raise_error ArgumentError
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ require 'game_of_life'
data/tasks.txt ADDED
@@ -0,0 +1,33 @@
1
+ size status name
2
+ -- ------ ---------------------------------------------
3
+ 1 done set up a bare git repo
4
+ 1 done set up rvmrc
5
+ 1 done set up gemfile
6
+ 1 done set up rake
7
+ 1 done setup cucumber
8
+ 1 done setup rspec
9
+ 1 done set up yard
10
+ 1 done add notes.txt with thoughts
11
+ 2 done think and create actual stories *
12
+ 1 done add a README.markdown to show to run
13
+
14
+ * Stories that came out of the thinking story above
15
+ 2 done cucumber features for first scenarios
16
+ 2 done implementation of game using the strategy/template pattern to take an outputter
17
+ 2 done seeding the board
18
+ 3 done calculate the next state of the board after a tick
19
+ 1 done set the next state as calculated above
20
+ 1 done an outputter to produce console like output
21
+ 2 done cucumber features for various scenarios
22
+ 1 done inputter also as a template, like outputter
23
+ 1 done integrity check of board
24
+ 1 done clarify Toad Pattern (D) with recruiter
25
+ 1 done grid can be rectangular, not necessarily square
26
+ 2 done implement self-replication (add the neighboring row/columns and clean up if needed)
27
+ 1 done board should use outputter only in view
28
+ 1 done board should use inputter only in parse
29
+ 1 done board should not hold on to a game
30
+ 2 done add a CLI so we can do file input and test that way
31
+ 1 done some cucumber steps with multiple ticks
32
+ 1 done complete README
33
+ 1 done complete documentation
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: game_of_life
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - arnab (Arnab Deka)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-12 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &2152022520 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *2152022520
25
+ - !ruby/object:Gem::Dependency
26
+ name: cucumber
27
+ requirement: &2152022100 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2152022100
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2152021600 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 2.0.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2152021600
47
+ - !ruby/object:Gem::Dependency
48
+ name: yard
49
+ requirement: &2152021180 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2152021180
58
+ - !ruby/object:Gem::Dependency
59
+ name: redcarpet
60
+ requirement: &2152020720 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2152020720
69
+ description: A Ruby library that encaptulates the logic of the game
70
+ email:
71
+ - arnab.deka+game_of_life@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rdebugrc
78
+ - .rspec
79
+ - .rvmrc
80
+ - .travis.yml
81
+ - .yardopts
82
+ - Gemfile
83
+ - LICENSE.md
84
+ - README.md
85
+ - Rakefile
86
+ - cucumber.yml
87
+ - examples/glider.txt
88
+ - examples/lwss.txt
89
+ - examples/pulsar.txt
90
+ - features/gameplay.feature
91
+ - features/multi_generation_gameplay.feature
92
+ - features/step_definitions/gameplay_steps.rb
93
+ - features/support/env.rb
94
+ - lib/game_of_life.rb
95
+ - lib/game_of_life/board.rb
96
+ - lib/game_of_life/cell.rb
97
+ - lib/game_of_life/game.rb
98
+ - lib/game_of_life/inputters/simple_string_inputter.rb
99
+ - lib/game_of_life/outputters/simple_string_outputter.rb
100
+ - lib/game_of_life/rules.rb
101
+ - notes.md
102
+ - scripts/play_game.rb
103
+ - spec/game_of_life/board_spec.rb
104
+ - spec/game_of_life/cell_spec.rb
105
+ - spec/game_of_life/inputters/simple_string_inputter_spec.rb
106
+ - spec/spec_helper.rb
107
+ - tasks.txt
108
+ homepage: https://github.com/arnab/game_of_life
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.6
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Conway's Game of Life, in Ruby
132
+ test_files:
133
+ - features/gameplay.feature
134
+ - features/multi_generation_gameplay.feature
135
+ - features/step_definitions/gameplay_steps.rb
136
+ - features/support/env.rb
137
+ - spec/game_of_life/board_spec.rb
138
+ - spec/game_of_life/cell_spec.rb
139
+ - spec/game_of_life/inputters/simple_string_inputter_spec.rb
140
+ - spec/spec_helper.rb
141
+ has_rdoc: