lifelike 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +132 -0
- data/Rakefile +12 -0
- data/bin/lifelike +6 -0
- data/lib/lifelike/cli/options.rb +65 -0
- data/lib/lifelike/cli.rb +25 -0
- data/lib/lifelike/error.rb +26 -0
- data/lib/lifelike/grid.rb +60 -0
- data/lib/lifelike/lifelike_cellular_automaton/cell.rb +33 -0
- data/lib/lifelike/lifelike_cellular_automaton/cell_serializer.rb +43 -0
- data/lib/lifelike/lifelike_cellular_automaton/rules.rb +47 -0
- data/lib/lifelike/lifelike_cellular_automaton/world.rb +24 -0
- data/lib/lifelike/lifelike_cellular_automaton/world_serializer.rb +31 -0
- data/lib/lifelike/lifelike_cellular_automaton/world_string_analyzer.rb +82 -0
- data/lib/lifelike/lifelike_cellular_automaton.rb +48 -0
- data/lib/lifelike/runner.rb +23 -0
- data/lib/lifelike/version.rb +3 -0
- data/lib/lifelike.rb +8 -0
- data/lifelike.gemspec +29 -0
- data/spec/cli_spec.rb +90 -0
- data/spec/options_spec.rb +45 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/world_string_analyzer_spec.rb +99 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b0f8ff229a6f20aac5756a3122d51847238cb654
|
4
|
+
data.tar.gz: d47628a5cdfe439f30cbe59c964aea9f01ffe8b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c423f41ddcb779269309a091582f9e573c3ffd4aa514dde9de51cd97aa902fc018f282a8095f7e855b1a3427ef9094f8a033f4abca578b0bbfefcf4d5994b3f3
|
7
|
+
data.tar.gz: 591891b01d827562c74dc27e7d6e9a9adcf8baf254d41522ba3c3c2a1333a5d18f3e5018b36fb5f898953e55e762670f7379230414dc14d99b727912fa127f92
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Metrics/LineLength:
|
2
|
+
Exclude:
|
3
|
+
- spec/*
|
4
|
+
|
5
|
+
Metrics/MethodLength:
|
6
|
+
Max: 9
|
7
|
+
|
8
|
+
Lint/HandleExceptions:
|
9
|
+
Exclude:
|
10
|
+
- Rakefile
|
11
|
+
|
12
|
+
Style/TrivialAccessors:
|
13
|
+
AllowPredicates: true
|
14
|
+
|
15
|
+
Style/TrailingComma:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Style/RaiseArgs:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Style/Documentation:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/RegexpLiteral:
|
25
|
+
MaxSlashes: 0
|
26
|
+
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Max Holder
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Lifelike
|
2
|
+
|
3
|
+
[](https://travis-ci.org/mxhold/lifelike)
|
5
|
+
[](https://codeclimate.com/github/mxhold/lifelike)
|
7
|
+
[](https://codeclimate.com/github/mxhold/lifelike)
|
9
|
+
|
10
|
+
Lifelike plays [Conway's Game of
|
11
|
+
Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) by default but it
|
12
|
+
can be used to simulate any [Life-like cellular
|
13
|
+
automata](https://en.wikipedia.org/wiki/Life-like_cellular_automaton).
|
14
|
+
|
15
|
+
## Rationale
|
16
|
+
|
17
|
+
This gem is inspired by the [Global Day of
|
18
|
+
Coderetreat](http://globalday.coderetreat.org/) where participants attempt to
|
19
|
+
implement Conway's Game of Life under various constraints.
|
20
|
+
|
21
|
+
The goal of this implementation is to make the code as well-factored as possible
|
22
|
+
rather than as efficient as possible.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
$ gem install lifelike
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
By default, Lifelike plays [Conway's Game of
|
31
|
+
Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life).
|
32
|
+
|
33
|
+
It gets it initial state from standard input or a file and then prints the next
|
34
|
+
generation:
|
35
|
+
|
36
|
+
```bash
|
37
|
+
$ echo "...\nooo\n..." > blinker
|
38
|
+
$ cat blinker
|
39
|
+
...
|
40
|
+
ooo
|
41
|
+
...
|
42
|
+
$ cat blinker | lifelike
|
43
|
+
.o.
|
44
|
+
.o.
|
45
|
+
.o.
|
46
|
+
$ lifelike blinker
|
47
|
+
.o.
|
48
|
+
.o.
|
49
|
+
.o.
|
50
|
+
```
|
51
|
+
|
52
|
+
### Multiple generations
|
53
|
+
|
54
|
+
You can also provide a number of generations to simulate before printing:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
$ echo "..o..\no.o..\n.oo..\n....." > glider
|
58
|
+
$ cat glider
|
59
|
+
..o..
|
60
|
+
o.o..
|
61
|
+
.oo..
|
62
|
+
.....
|
63
|
+
$ cat glider | lifelike -c 4
|
64
|
+
.....
|
65
|
+
...o.
|
66
|
+
.o.o.
|
67
|
+
..oo.
|
68
|
+
```
|
69
|
+
|
70
|
+
### Alternate dead/alive characters
|
71
|
+
|
72
|
+
You don't have to use `o` and `.` to represent life and death.
|
73
|
+
Lifelike will be smart and try to guess based on the input provided:
|
74
|
+
|
75
|
+
```bash
|
76
|
+
$ echo "000\n111\n000" | lifelike
|
77
|
+
010
|
78
|
+
010
|
79
|
+
010
|
80
|
+
$ echo " \nXXX\n " | lifelike
|
81
|
+
X
|
82
|
+
X
|
83
|
+
X
|
84
|
+
```
|
85
|
+
|
86
|
+
You can use any combination of two of the following characters (which are in
|
87
|
+
order here from what lifelike will consider more dead to more alive):
|
88
|
+
|
89
|
+
<space> _ . , o O 0 1 x * X # @
|
90
|
+
|
91
|
+
### Game of Life variants
|
92
|
+
|
93
|
+
You can define the rules Lifelike will use with the `-r` flag.
|
94
|
+
|
95
|
+
By default, it uses the rules for Conway's Game of Life, which are `B3/S23`.
|
96
|
+
|
97
|
+
This format of specifying the rules means:
|
98
|
+
|
99
|
+
- It takes 3 live neighbors for a cell to be **b**orn
|
100
|
+
- It takes 2 or 3 live neighbors for a cell to **s**tay alive
|
101
|
+
|
102
|
+
One interesting variant is called
|
103
|
+
[Seeds](https://en.wikipedia.org/wiki/Seeds_(cellular_automaton)) which has the
|
104
|
+
rule `B2/S`, meaning:
|
105
|
+
|
106
|
+
- It takes 2 live neighbors for a cell to be born
|
107
|
+
- No cells ever survive
|
108
|
+
|
109
|
+
This is how you would make Lifelike play this variant:
|
110
|
+
|
111
|
+
```bash
|
112
|
+
$ echo ".......\n.......\n..oo..\n.......\n......." | lifelike -r "B2/S"
|
113
|
+
.......
|
114
|
+
..oo...
|
115
|
+
......
|
116
|
+
..oo...
|
117
|
+
.......
|
118
|
+
$ echo ".......\n.......\n..oo..\n.......\n......." | lifelike -r "B2/S" -c 2
|
119
|
+
..oo...
|
120
|
+
.......
|
121
|
+
.o..o.
|
122
|
+
.......
|
123
|
+
..oo...
|
124
|
+
```
|
125
|
+
|
126
|
+
## Contributing
|
127
|
+
|
128
|
+
1. Fork it ( https://github.com/mxhold/lifelike/fork )
|
129
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
130
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
131
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
132
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/lifelike
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
module Lifelike
|
3
|
+
module CLI
|
4
|
+
class Options
|
5
|
+
def self.parse!(argv)
|
6
|
+
new.parse!(argv)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@options = default_options
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse!(argv)
|
14
|
+
option_parser.parse!(argv)
|
15
|
+
@options
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def default_options
|
21
|
+
{
|
22
|
+
generations: 1,
|
23
|
+
rule_string: 'B3/S23'
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
28
|
+
def option_parser
|
29
|
+
OptionParser.new do |opts|
|
30
|
+
opts.banner = 'Usage: lifelike [options] [file]'
|
31
|
+
opts.separator 'Options:'
|
32
|
+
|
33
|
+
opts.on(
|
34
|
+
'-c [iterations]',
|
35
|
+
'--count [iterations]',
|
36
|
+
'Number of iterations to perform' \
|
37
|
+
"(default #{default_options[:generations]})"
|
38
|
+
) do |c|
|
39
|
+
@options[:generations] = c.to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on(
|
43
|
+
'-r [rule_string]',
|
44
|
+
'--rules [rule_string]',
|
45
|
+
'Rules for the life-like cellular automaton ' \
|
46
|
+
"(default #{default_options[:rules]})"
|
47
|
+
) do |rule_string|
|
48
|
+
@options[:rule_string] = rule_string
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('-h', '--help', 'Prints this message') do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-v', '--version', 'Prints the version') do
|
57
|
+
puts Lifelike::VERSION
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/lifelike/cli.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'lifelike/cli/options'
|
2
|
+
module Lifelike
|
3
|
+
module CLI
|
4
|
+
# Conventional exit codes from BSD's sysexits.h
|
5
|
+
# See: https://www.freebsd.org/cgi/man.cgi?query=sysexits
|
6
|
+
EX_USAGE = 64 # Command was used incorrectly
|
7
|
+
EX_DATAERR = 65 # Input data was incorrect
|
8
|
+
|
9
|
+
def self.invoke
|
10
|
+
options = Options.parse!(ARGV)
|
11
|
+
puts Runner.new(ARGF.read, options).run
|
12
|
+
exit
|
13
|
+
rescue OptionParser::ParseError, UnparsableRuleStringError => e
|
14
|
+
report_error e
|
15
|
+
exit EX_USAGE
|
16
|
+
rescue UnexpectedCharacterError, InsufficientValidCharacterError => e
|
17
|
+
report_error e
|
18
|
+
exit EX_DATAERR
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.report_error(error)
|
22
|
+
$stderr.puts "lifelike: #{error}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class InsufficientValidCharacterError < LifelikeError
|
6
|
+
def initialize(valid_characters)
|
7
|
+
super 'Insufficient characters for determining life/death. ' \
|
8
|
+
'Expected two of the following characters: ' \
|
9
|
+
"#{valid_characters.map { |c| "\"#{c}\"" }.join(', ') }"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class UnexpectedCharacterError < LifelikeError
|
14
|
+
def initialize(character, expected:)
|
15
|
+
super "Unexpected character: \"#{character}\" " \
|
16
|
+
"Expected: #{expected.map { |c| "\"#{c}\"" }.join(' or ')}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class UnparsableRuleStringError < LifelikeError
|
21
|
+
def initialize(rule_string)
|
22
|
+
super "Unparsable rule string: \"#{rule_string}\" " \
|
23
|
+
'Expected something like: "B3/S23"'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class Grid
|
3
|
+
ROW_DELIMITER = "\n"
|
4
|
+
CELL_DELIMITER = ''
|
5
|
+
|
6
|
+
def initialize(rows)
|
7
|
+
@rows = rows
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_s(string)
|
11
|
+
new(
|
12
|
+
string.split(ROW_DELIMITER).map do |row_string|
|
13
|
+
row_string.split(CELL_DELIMITER).map do |char|
|
14
|
+
yield(char)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def map_with_neighbors
|
21
|
+
self.class.new(
|
22
|
+
@rows.map.with_index do |row, row_index|
|
23
|
+
row.map.with_index do |cell, col_index|
|
24
|
+
yield(cell, neighbors(row_index, col_index))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
@rows.map do |row|
|
32
|
+
row.map do |cell|
|
33
|
+
yield(cell)
|
34
|
+
end.join(CELL_DELIMITER)
|
35
|
+
end.join(ROW_DELIMITER)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def neighbors(row_index, col_index)
|
41
|
+
neighbor_shifts.flat_map do |row_shift, col_shift|
|
42
|
+
nonwrapping_fetch(row_index + row_shift, col_index + col_shift)
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:disable all
|
47
|
+
def neighbor_shifts
|
48
|
+
[
|
49
|
+
[-1, -1], [-1, 0], [-1, 1],
|
50
|
+
[ 0, -1], [ 0, 1],
|
51
|
+
[ 1, -1], [ 1, 0], [ 1, 1],
|
52
|
+
]
|
53
|
+
end
|
54
|
+
# rubocop:enable all
|
55
|
+
|
56
|
+
def nonwrapping_fetch(row_index, col_index)
|
57
|
+
@rows.fetch(row_index, [])[col_index] if row_index >= 0 && col_index >= 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class Cell
|
4
|
+
def initialize(alive, rules)
|
5
|
+
@alive = alive
|
6
|
+
@rules = rules
|
7
|
+
end
|
8
|
+
|
9
|
+
def alive?
|
10
|
+
@alive
|
11
|
+
end
|
12
|
+
|
13
|
+
def tick(neighbors)
|
14
|
+
@neighbors = neighbors
|
15
|
+
self.class.new(alive_next?, @rules)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def alive_next?
|
21
|
+
if alive?
|
22
|
+
@rules.survives?(alive_neighbor_count)
|
23
|
+
else
|
24
|
+
@rules.born?(alive_neighbor_count)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def alive_neighbor_count
|
29
|
+
@neighbors.select(&:alive?).size
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class CellSerializer
|
4
|
+
def initialize(alive_char:, dead_char:, rules:)
|
5
|
+
@alive_char = alive_char
|
6
|
+
@dead_char = dead_char
|
7
|
+
@rules = rules
|
8
|
+
end
|
9
|
+
|
10
|
+
def dump(cell)
|
11
|
+
if cell.alive?
|
12
|
+
@alive_char
|
13
|
+
else
|
14
|
+
@dead_char
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def load(char)
|
19
|
+
Cell.new(alive?(char), @rules)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def alive?(char)
|
25
|
+
case char
|
26
|
+
when @alive_char
|
27
|
+
true
|
28
|
+
when @dead_char
|
29
|
+
false
|
30
|
+
else
|
31
|
+
handle_unexpected_character(char)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_unexpected_character(char)
|
36
|
+
fail UnexpectedCharacterError.new(
|
37
|
+
char,
|
38
|
+
expected: [@alive_char, @dead_char]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class Rules
|
4
|
+
def initialize(string)
|
5
|
+
@rule_string = RuleString.new(string)
|
6
|
+
end
|
7
|
+
|
8
|
+
def survives?(alive_neighbor_count)
|
9
|
+
@rule_string.alive_neighbors_to_survive.include?(alive_neighbor_count)
|
10
|
+
end
|
11
|
+
|
12
|
+
def born?(alive_neighbor_count)
|
13
|
+
@rule_string.alive_neighbors_to_be_born.include?(alive_neighbor_count)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class RuleString
|
18
|
+
# See: http://www.conwaylife.com/wiki/Rule#Rules
|
19
|
+
# Example: B3/S23
|
20
|
+
def initialize(string)
|
21
|
+
@string = string
|
22
|
+
end
|
23
|
+
|
24
|
+
def alive_neighbors_to_be_born
|
25
|
+
numbers_after('B')
|
26
|
+
end
|
27
|
+
|
28
|
+
def alive_neighbors_to_survive
|
29
|
+
numbers_after('S')
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def numbers_after(letter)
|
35
|
+
if (characters = numeric_characters_after(letter))
|
36
|
+
characters.split('').map(&:to_i)
|
37
|
+
else
|
38
|
+
fail UnparsableRuleStringError.new(@string)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def numeric_characters_after(letter)
|
43
|
+
@string[/#{letter}(\d*)/, 1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class World
|
4
|
+
attr_reader :cell_grid
|
5
|
+
def initialize(cell_grid)
|
6
|
+
@cell_grid = cell_grid
|
7
|
+
end
|
8
|
+
|
9
|
+
def tick(generations)
|
10
|
+
(1..generations).reduce(self) do |world, _|
|
11
|
+
world.tick_once
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def tick_once
|
16
|
+
self.class.new(
|
17
|
+
@cell_grid.map_with_neighbors do |cell, neighbors|
|
18
|
+
cell.tick(neighbors)
|
19
|
+
end
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class WorldSerializer
|
4
|
+
def initialize(cell_serializer)
|
5
|
+
@cell_serializer = cell_serializer
|
6
|
+
end
|
7
|
+
|
8
|
+
def load(string)
|
9
|
+
World.new(string_to_cell_grid(string))
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump(world)
|
13
|
+
cell_grid_to_string(world.cell_grid)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def string_to_cell_grid(string)
|
19
|
+
Grid.from_s(string) do |char|
|
20
|
+
@cell_serializer.load(char)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def cell_grid_to_string(cell_grid)
|
25
|
+
cell_grid.to_s do |cell|
|
26
|
+
@cell_serializer.dump(cell)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class LifelikeCellularAutomaton
|
3
|
+
class WorldStringAnalyzer
|
4
|
+
def initialize(string, default_dead_char: ' ', default_alive_char: 'X')
|
5
|
+
@world_string = string
|
6
|
+
@default_dead_char = default_dead_char
|
7
|
+
@default_alive_char = default_alive_char
|
8
|
+
end
|
9
|
+
|
10
|
+
def dead_char
|
11
|
+
case valid_chars.size
|
12
|
+
when 0
|
13
|
+
fail InsufficientValidCharacterError.new(allowed_chars_by_aliveness)
|
14
|
+
when 1
|
15
|
+
deadlike_char || @default_dead_char
|
16
|
+
else
|
17
|
+
least_alive_valid_char
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def alive_char
|
22
|
+
case valid_chars.size
|
23
|
+
when 0
|
24
|
+
fail InsufficientValidCharacterError.new(allowed_chars_by_aliveness)
|
25
|
+
when 1
|
26
|
+
lifelike_char || @default_alive_char
|
27
|
+
else
|
28
|
+
most_alive_valid_char
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def deadlike_char
|
35
|
+
valid_char if deadlike.include?(valid_char)
|
36
|
+
end
|
37
|
+
|
38
|
+
def lifelike_char
|
39
|
+
valid_char if lifelike.include?(valid_char)
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_char
|
43
|
+
valid_chars.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def least_alive_valid_char
|
47
|
+
valid_chars_by_aliveness.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def most_alive_valid_char
|
51
|
+
valid_chars_by_aliveness.last
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid_chars_by_aliveness
|
55
|
+
valid_chars.take(2).sort_by { |char| aliveness(char) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_chars
|
59
|
+
@world_string.chars.uniq.select do |char|
|
60
|
+
allowed_chars_by_aliveness.include?(char)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Roughly in order from most dead-like to most alive-like
|
65
|
+
def allowed_chars_by_aliveness
|
66
|
+
deadlike + lifelike
|
67
|
+
end
|
68
|
+
|
69
|
+
def deadlike
|
70
|
+
[' ', '_', '.', ',', 'o', 'O', '0']
|
71
|
+
end
|
72
|
+
|
73
|
+
def lifelike
|
74
|
+
['1', 'x', '*', 'X', '#', '@']
|
75
|
+
end
|
76
|
+
|
77
|
+
def aliveness(char)
|
78
|
+
allowed_chars_by_aliveness.index(char)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'lifelike/lifelike_cellular_automaton/rules'
|
2
|
+
require 'lifelike/lifelike_cellular_automaton/world'
|
3
|
+
require 'lifelike/lifelike_cellular_automaton/cell'
|
4
|
+
require 'lifelike/lifelike_cellular_automaton/cell_serializer'
|
5
|
+
require 'lifelike/lifelike_cellular_automaton/world_serializer'
|
6
|
+
require 'lifelike/lifelike_cellular_automaton/world_string_analyzer'
|
7
|
+
module Lifelike
|
8
|
+
class LifelikeCellularAutomaton
|
9
|
+
def initialize(initial_world_string, rule_string)
|
10
|
+
@initial_world_string = initial_world_string
|
11
|
+
@rules = Rules.new(rule_string)
|
12
|
+
end
|
13
|
+
|
14
|
+
def tick(generations)
|
15
|
+
world_serializer.dump(initial_world.tick(generations))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def initial_world
|
21
|
+
world_serializer.load(@initial_world_string)
|
22
|
+
end
|
23
|
+
|
24
|
+
def world_serializer
|
25
|
+
@world_serializer ||= WorldSerializer.new(cell_serializer)
|
26
|
+
end
|
27
|
+
|
28
|
+
def cell_serializer
|
29
|
+
CellSerializer.new(
|
30
|
+
alive_char: alive_char,
|
31
|
+
dead_char: dead_char,
|
32
|
+
rules: @rules
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def alive_char
|
37
|
+
world_string_analyzer.alive_char
|
38
|
+
end
|
39
|
+
|
40
|
+
def dead_char
|
41
|
+
world_string_analyzer.dead_char
|
42
|
+
end
|
43
|
+
|
44
|
+
def world_string_analyzer
|
45
|
+
@analyzer ||= WorldStringAnalyzer.new(@initial_world_string)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Lifelike
|
2
|
+
class Runner
|
3
|
+
def initialize(initial_world_string, rule_string:, generations:)
|
4
|
+
@initial_world_string = initial_world_string
|
5
|
+
@rule_string = rule_string
|
6
|
+
@generations = generations
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
final_world_string
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def final_world_string
|
16
|
+
lifelike_cellular_automaton.tick(@generations)
|
17
|
+
end
|
18
|
+
|
19
|
+
def lifelike_cellular_automaton
|
20
|
+
LifelikeCellularAutomaton.new(@initial_world_string, @rule_string)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/lifelike.rb
ADDED
data/lifelike.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lifelike/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'lifelike'
|
8
|
+
spec.version = Lifelike::VERSION
|
9
|
+
spec.authors = ['Max Holder']
|
10
|
+
spec.email = ['mxhold@gmail.com']
|
11
|
+
spec.summary = 'Simulates Life-like cellular automata'
|
12
|
+
spec.description = 'A gem for playing Conway\'s Game of Life and other ' \
|
13
|
+
'Life-like cellular automata'
|
14
|
+
spec.homepage = 'https://github.com/mxhold/lifelike'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
25
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
26
|
+
spec.add_development_dependency 'rubocop'
|
27
|
+
|
28
|
+
spec.required_ruby_version = '~> 2.1'
|
29
|
+
end
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'lifelike'
|
2
|
+
require 'lifelike/cli'
|
3
|
+
RSpec.describe Lifelike::CLI, :integration do
|
4
|
+
context 'no arguments' do
|
5
|
+
it 'prints the input after a generation' do
|
6
|
+
stub_const('ARGV', [])
|
7
|
+
allow(ARGF).to receive(:read) { "010\n010\n010" }
|
8
|
+
allow(Lifelike::CLI).to receive(:exit)
|
9
|
+
expect do
|
10
|
+
Lifelike::CLI.invoke
|
11
|
+
end.to output("000\n111\n000\n").to_stdout
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'given 2 as an argument' do
|
16
|
+
it 'prints the input after 2 generations' do
|
17
|
+
stub_const('ARGV', ['-c', '2'])
|
18
|
+
allow(ARGF).to receive(:read) { "010\n010\n010" }
|
19
|
+
allow(Lifelike::CLI).to receive(:exit)
|
20
|
+
expect do
|
21
|
+
Lifelike::CLI.invoke
|
22
|
+
end.to output("010\n010\n010\n").to_stdout
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'alternate rules specified' do
|
27
|
+
it 'prints the input after a generation using the alternate rules' do
|
28
|
+
stub_const('ARGV', ['-r', 'B2/S'])
|
29
|
+
allow(ARGF).to receive(:read) { "0000000\n0000000\n0011000\n0000000\n0000000" }
|
30
|
+
allow(Lifelike::CLI).to receive(:exit)
|
31
|
+
expect do
|
32
|
+
Lifelike::CLI.invoke
|
33
|
+
end.to output("0000000\n0011000\n0000000\n0011000\n0000000\n").to_stdout
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'alternate live/dead characters' do
|
38
|
+
it 'prints the output with the same characters' do
|
39
|
+
stub_const('ARGV', [])
|
40
|
+
allow(ARGF).to receive(:read) { ".o.\n.o.\n.o." }
|
41
|
+
allow(Lifelike::CLI).to receive(:exit)
|
42
|
+
expect do
|
43
|
+
Lifelike::CLI.invoke
|
44
|
+
end.to output("...\nooo\n...\n").to_stdout
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'invalid arguments' do
|
49
|
+
it 'prints an error and exits with the appropriate exit code' do
|
50
|
+
stub_const('ARGV', ['-dsaf'])
|
51
|
+
expect(Lifelike::CLI).to receive(:exit).with(64)
|
52
|
+
expect do
|
53
|
+
Lifelike::CLI.invoke
|
54
|
+
end.to output(/invalid option/).to_stderr
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'unparsable rule string' do
|
59
|
+
it 'prints an error and exits with the appropriate exit code' do
|
60
|
+
stub_const('ARGV', ['-r', 'QWSD'])
|
61
|
+
allow(ARGF).to receive(:read) { 'o.o' }
|
62
|
+
expect(Lifelike::CLI).to receive(:exit).with(64)
|
63
|
+
expect do
|
64
|
+
Lifelike::CLI.invoke
|
65
|
+
end.to output(/unparsable rule string/i).to_stderr
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'insufficient valid characters' do
|
70
|
+
it 'raises an error and exits with the appropriate exit code' do
|
71
|
+
stub_const('ARGV', [])
|
72
|
+
allow(ARGF).to receive(:read) { 'wyr' }
|
73
|
+
allow(Lifelike::CLI).to receive(:exit).with(65)
|
74
|
+
expect do
|
75
|
+
Lifelike::CLI.invoke
|
76
|
+
end.to output(/insufficient characters/i).to_stderr
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'unexpected character' do
|
81
|
+
it 'raises an error and exits with the appropriate exit code' do
|
82
|
+
stub_const('ARGV', [])
|
83
|
+
allow(ARGF).to receive(:read) { 'o.o.W' }
|
84
|
+
allow(Lifelike::CLI).to receive(:exit).with(65)
|
85
|
+
expect do
|
86
|
+
Lifelike::CLI.invoke
|
87
|
+
end.to output(/unexpected character/i).to_stderr
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'lifelike/cli/options'
|
2
|
+
RSpec.describe Lifelike::CLI::Options do
|
3
|
+
describe '.parse' do
|
4
|
+
it 'has defaults' do
|
5
|
+
expect(described_class.parse!([])).to eql(generations: 1, rule_string: 'B3/S23')
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'sets the generations' do
|
9
|
+
expect(described_class.parse!(['-c', '1'])).to include(generations: 1)
|
10
|
+
expect(described_class.parse!(['--count', '1'])).to include(generations: 1)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'sets the rule_string' do
|
14
|
+
expect(described_class.parse!(['-r', 'B2/S3'])).to include(rule_string: 'B2/S3')
|
15
|
+
expect(described_class.parse!(['--rule', 'B2/S3'])).to include(rule_string: 'B2/S3')
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'displays the help with -h' do
|
19
|
+
allow_any_instance_of(described_class).to receive(:exit)
|
20
|
+
expect do
|
21
|
+
described_class.parse!(['-h'])
|
22
|
+
end.to output(/Usage/).to_stdout
|
23
|
+
expect do
|
24
|
+
described_class.parse!(['--help'])
|
25
|
+
end.to output(/Usage/).to_stdout
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'displays the version with -v' do
|
29
|
+
allow_any_instance_of(described_class).to receive(:exit)
|
30
|
+
stub_const('Lifelike::VERSION', '1.2.3')
|
31
|
+
expect do
|
32
|
+
described_class.parse!(['-v'])
|
33
|
+
end.to output("1.2.3\n").to_stdout
|
34
|
+
expect do
|
35
|
+
described_class.parse!(['--version'])
|
36
|
+
end.to output("1.2.3\n").to_stdout
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'raises an exception on unknown options' do
|
40
|
+
expect do
|
41
|
+
described_class.parse!(['-z'])
|
42
|
+
end.to raise_exception(OptionParser::InvalidOption)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
6
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
7
|
+
# files.
|
8
|
+
#
|
9
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
10
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
11
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
12
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
13
|
+
# a separate helper file that requires the additional dependencies and performs
|
14
|
+
# the additional setup, and require it from the spec files that actually need
|
15
|
+
# it.
|
16
|
+
#
|
17
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
18
|
+
# users commonly want.
|
19
|
+
#
|
20
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
21
|
+
RSpec.configure do |config|
|
22
|
+
# rspec-expectations config goes here. You can use an alternate
|
23
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
24
|
+
# assertions if you prefer.
|
25
|
+
config.expect_with :rspec do |expectations|
|
26
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
27
|
+
# and `failure_message` of custom matchers include text for helper methods
|
28
|
+
# defined using `chain`, e.g.:
|
29
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
30
|
+
# # => "be bigger than 2 and smaller than 4"
|
31
|
+
# ...rather than:
|
32
|
+
# # => "be bigger than 2"
|
33
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
34
|
+
end
|
35
|
+
|
36
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
37
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
38
|
+
config.mock_with :rspec do |mocks|
|
39
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
40
|
+
# a real object. This is generally recommended, and will default to
|
41
|
+
# `true` in RSpec 4.
|
42
|
+
mocks.verify_partial_doubles = true
|
43
|
+
end
|
44
|
+
|
45
|
+
# These two settings work together to allow you to limit a spec run
|
46
|
+
# to individual examples or groups you care about by tagging them with
|
47
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
48
|
+
# get run.
|
49
|
+
config.filter_run :focus
|
50
|
+
config.run_all_when_everything_filtered = true
|
51
|
+
|
52
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
53
|
+
# recommended. For more details, see:
|
54
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
55
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
56
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
57
|
+
config.disable_monkey_patching!
|
58
|
+
|
59
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
60
|
+
# be too noisy due to issues in dependencies.
|
61
|
+
config.warnings = true
|
62
|
+
|
63
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
64
|
+
# file, and it's useful to allow more verbose output when running an
|
65
|
+
# individual spec file.
|
66
|
+
if config.files_to_run.one?
|
67
|
+
# Use the documentation formatter for detailed output,
|
68
|
+
# unless a formatter has already been configured
|
69
|
+
# (e.g. via a command-line flag).
|
70
|
+
config.default_formatter = 'doc'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Print the 10 slowest examples and example groups at the
|
74
|
+
# end of the spec run, to help surface which specs are running
|
75
|
+
# particularly slow.
|
76
|
+
config.profile_examples = 10
|
77
|
+
|
78
|
+
# Run specs in random order to surface order dependencies. If you find an
|
79
|
+
# order dependency and want to debug it, you can fix the order by providing
|
80
|
+
# the seed, which is printed after each run.
|
81
|
+
# --seed 1234
|
82
|
+
config.order = :random
|
83
|
+
|
84
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
85
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
86
|
+
# test failures related to randomization by passing the same `--seed` value
|
87
|
+
# as the one that triggered the failure.
|
88
|
+
Kernel.srand config.seed
|
89
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative '../lib/lifelike/error'
|
2
|
+
require_relative '../lib/lifelike/lifelike_cellular_automaton/world_string_analyzer'
|
3
|
+
|
4
|
+
RSpec.describe Lifelike::LifelikeCellularAutomaton::WorldStringAnalyzer do
|
5
|
+
context 'given at least two valid characters' do
|
6
|
+
describe 'detecting alive/dead character' do
|
7
|
+
matcher :consider do |deadlike|
|
8
|
+
match do |subject|
|
9
|
+
analyzer = subject.new("#{deadlike}#{@lifelike}")
|
10
|
+
analyzer.dead_char == deadlike && analyzer.alive_char == @lifelike
|
11
|
+
end
|
12
|
+
|
13
|
+
chain :deader_looking_than, :lifelike
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { described_class }
|
17
|
+
|
18
|
+
it { is_expected.to consider(' ').deader_looking_than('_') }
|
19
|
+
it { is_expected.to consider('_').deader_looking_than('.') }
|
20
|
+
it { is_expected.to consider('.').deader_looking_than(',') }
|
21
|
+
it { is_expected.to consider(',').deader_looking_than('o') }
|
22
|
+
it { is_expected.to consider('o').deader_looking_than('O') }
|
23
|
+
it { is_expected.to consider('O').deader_looking_than('0') }
|
24
|
+
it { is_expected.to consider('0').deader_looking_than('1') }
|
25
|
+
it { is_expected.to consider('1').deader_looking_than('x') }
|
26
|
+
it { is_expected.to consider('x').deader_looking_than('*') }
|
27
|
+
it { is_expected.to consider('*').deader_looking_than('X') }
|
28
|
+
it { is_expected.to consider('X').deader_looking_than('#') }
|
29
|
+
it { is_expected.to consider('#').deader_looking_than('@') }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'given a single valid character' do
|
34
|
+
describe 'detecting whether it should be considered alive or dead' do
|
35
|
+
matcher :consider do |character|
|
36
|
+
match do |subject|
|
37
|
+
analyzer = subject.new(character, default_dead_char: 'd', default_alive_char: 'a')
|
38
|
+
fail if %w(d a).include?(character)
|
39
|
+
if @consider_dead
|
40
|
+
analyzer.dead_char == character
|
41
|
+
else
|
42
|
+
analyzer.alive_char == character
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
chain(:to_be_dead) { @consider_dead = true }
|
47
|
+
chain(:to_be_alive) { @consider_dead = false }
|
48
|
+
end
|
49
|
+
|
50
|
+
subject { described_class }
|
51
|
+
|
52
|
+
it { is_expected.to consider(' ').to_be_dead }
|
53
|
+
it { is_expected.to consider('_').to_be_dead }
|
54
|
+
it { is_expected.to consider('.').to_be_dead }
|
55
|
+
it { is_expected.to consider(',').to_be_dead }
|
56
|
+
it { is_expected.to consider('o').to_be_dead }
|
57
|
+
it { is_expected.to consider('O').to_be_dead }
|
58
|
+
it { is_expected.to consider('0').to_be_dead }
|
59
|
+
|
60
|
+
it { is_expected.to consider('1').to_be_alive }
|
61
|
+
it { is_expected.to consider('x').to_be_alive }
|
62
|
+
it { is_expected.to consider('*').to_be_alive }
|
63
|
+
it { is_expected.to consider('X').to_be_alive }
|
64
|
+
it { is_expected.to consider('#').to_be_alive }
|
65
|
+
it { is_expected.to consider('@').to_be_alive }
|
66
|
+
end
|
67
|
+
describe 'default characters' do
|
68
|
+
context 'provided char was considered dead' do
|
69
|
+
subject { described_class.new(' ') }
|
70
|
+
it 'uses "X" as the default alive char' do
|
71
|
+
expect(subject.alive_char).to eql 'X'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
context 'provided char was considered alive' do
|
75
|
+
subject { described_class.new('X') }
|
76
|
+
it 'uses " " as the default dead char' do
|
77
|
+
expect(subject.dead_char).to eql ' '
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'given no valid characters' do
|
84
|
+
describe '#alive_char' do
|
85
|
+
it 'raises a InsufficientValidCharacterError' do
|
86
|
+
expect do
|
87
|
+
described_class.new('').alive_char
|
88
|
+
end.to raise_error(Lifelike::InsufficientValidCharacterError)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
describe '#dead_char' do
|
92
|
+
it 'raises a InsufficientValidCharacterError' do
|
93
|
+
expect do
|
94
|
+
described_class.new('').dead_char
|
95
|
+
end.to raise_error(Lifelike::InsufficientValidCharacterError)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lifelike
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max Holder
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: A gem for playing Conway's Game of Life and other Life-like cellular
|
84
|
+
automata
|
85
|
+
email:
|
86
|
+
- mxhold@gmail.com
|
87
|
+
executables:
|
88
|
+
- lifelike
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rspec"
|
94
|
+
- ".rubocop.yml"
|
95
|
+
- ".ruby-version"
|
96
|
+
- ".travis.yml"
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- bin/lifelike
|
102
|
+
- lib/lifelike.rb
|
103
|
+
- lib/lifelike/cli.rb
|
104
|
+
- lib/lifelike/cli/options.rb
|
105
|
+
- lib/lifelike/error.rb
|
106
|
+
- lib/lifelike/grid.rb
|
107
|
+
- lib/lifelike/lifelike_cellular_automaton.rb
|
108
|
+
- lib/lifelike/lifelike_cellular_automaton/cell.rb
|
109
|
+
- lib/lifelike/lifelike_cellular_automaton/cell_serializer.rb
|
110
|
+
- lib/lifelike/lifelike_cellular_automaton/rules.rb
|
111
|
+
- lib/lifelike/lifelike_cellular_automaton/world.rb
|
112
|
+
- lib/lifelike/lifelike_cellular_automaton/world_serializer.rb
|
113
|
+
- lib/lifelike/lifelike_cellular_automaton/world_string_analyzer.rb
|
114
|
+
- lib/lifelike/runner.rb
|
115
|
+
- lib/lifelike/version.rb
|
116
|
+
- lifelike.gemspec
|
117
|
+
- spec/cli_spec.rb
|
118
|
+
- spec/options_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
- spec/world_string_analyzer_spec.rb
|
121
|
+
homepage: https://github.com/mxhold/lifelike
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata: {}
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - "~>"
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '2.1'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.4.5
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Simulates Life-like cellular automata
|
145
|
+
test_files:
|
146
|
+
- spec/cli_spec.rb
|
147
|
+
- spec/options_spec.rb
|
148
|
+
- spec/spec_helper.rb
|
149
|
+
- spec/world_string_analyzer_spec.rb
|