lifelike 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
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
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lifelike.gemspec
4
+ gemspec
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
+ [![Build
4
+ Status](https://travis-ci.org/mxhold/lifelike.svg?branch=master)](https://travis-ci.org/mxhold/lifelike)
5
+ [![Code
6
+ Climate](https://codeclimate.com/github/mxhold/lifelike/badges/gpa.svg)](https://codeclimate.com/github/mxhold/lifelike)
7
+ [![Test
8
+ Coverage](https://codeclimate.com/github/mxhold/lifelike/badges/coverage.svg)](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
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ require 'rubocop/rake_task'
8
+ RuboCop::RakeTask.new(:rubocop)
9
+
10
+ task default: [:spec, :rubocop]
11
+ rescue LoadError
12
+ end
data/bin/lifelike ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lifelike'
4
+ require 'lifelike/cli'
5
+
6
+ Lifelike::CLI.invoke
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Lifelike
2
+ VERSION = '1.0.0'
3
+ end
data/lib/lifelike.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'lifelike/version'
2
+ require 'lifelike/error'
3
+ require 'lifelike/grid'
4
+ require 'lifelike/lifelike_cellular_automaton'
5
+ require 'lifelike/runner'
6
+
7
+ module Lifelike
8
+ end
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
@@ -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