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 +3 -0
- data/.rdebugrc +4 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +8 -0
- data/.yardopts +5 -0
- data/Gemfile +13 -0
- data/LICENSE.md +20 -0
- data/README.md +47 -0
- data/Rakefile +11 -0
- data/cucumber.yml +4 -0
- data/examples/glider.txt +3 -0
- data/examples/lwss.txt +4 -0
- data/examples/pulsar.txt +15 -0
- data/features/gameplay.feature +62 -0
- data/features/multi_generation_gameplay.feature +41 -0
- data/features/step_definitions/gameplay_steps.rb +16 -0
- data/features/support/env.rb +2 -0
- data/lib/game_of_life.rb +6 -0
- data/lib/game_of_life/board.rb +191 -0
- data/lib/game_of_life/cell.rb +37 -0
- data/lib/game_of_life/game.rb +34 -0
- data/lib/game_of_life/inputters/simple_string_inputter.rb +46 -0
- data/lib/game_of_life/outputters/simple_string_outputter.rb +37 -0
- data/lib/game_of_life/rules.rb +21 -0
- data/notes.md +52 -0
- data/scripts/play_game.rb +27 -0
- data/spec/game_of_life/board_spec.rb +69 -0
- data/spec/game_of_life/cell_spec.rb +13 -0
- data/spec/game_of_life/inputters/simple_string_inputter_spec.rb +23 -0
- data/spec/spec_helper.rb +1 -0
- data/tasks.txt +33 -0
- metadata +141 -0
data/.gitignore
ADDED
data/.rdebugrc
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2@game_of_life --create
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
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
data/examples/glider.txt
ADDED
data/examples/lwss.txt
ADDED
data/examples/pulsar.txt
ADDED
@@ -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
|
data/lib/game_of_life.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|