conway_deathmatch 0.4.1.1 → 0.5.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +13 -27
- data/VERSION +1 -1
- data/bin/conway_deathmatch +9 -8
- data/bin/proving_ground +11 -12
- data/conway_deathmatch.gemspec +6 -3
- data/lib/conway_deathmatch.rb +145 -2
- data/lib/conway_deathmatch/shapes.rb +6 -6
- data/test/{bench_board_state.rb → bench_grid.rb} +9 -10
- data/test/spec_helper.rb +4 -18
- data/test/test_grid.rb +141 -0
- data/test/test_shapes.rb +7 -7
- metadata +47 -6
- data/lib/conway_deathmatch/board_state.rb +0 -153
- data/test/test_board_state.rb +0 -132
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95413f3557aa8dcb568065213eb0e296c951aac2
|
4
|
+
data.tar.gz: 7640ea076629eaab5134bed4f11288155916de58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ce56f027fdcf40ed302a65a928c22b9b7e3677c3c755505941a3546d1391bfb5af6810f9e092e19845d55d3d3cfd533954930e815d9f8d676305fc09b714e10
|
7
|
+
data.tar.gz: abdedab43e2091b6a3f150e909685a90acaafe59022d73a6ce9003eb9d5b31a88fc2be44209238dd4a9339175c4043539ad85369a77106cc77bc489fa85ec33d
|
data/README.md
CHANGED
@@ -28,6 +28,17 @@ form several distinct islands and disjointed groups. For this project,
|
|
28
28
|
*deathmatch* refers to multiple populations with respective identities over
|
29
29
|
time (e.g. red vs blue).
|
30
30
|
|
31
|
+
Deathmatch Rules
|
32
|
+
---
|
33
|
+
Choose:
|
34
|
+
* Defensive: Alive cells never switch sides
|
35
|
+
- This is the rule followed by the *Immigration* variant of CGoL, I believe
|
36
|
+
* Aggressive: Alive cells survive with majority
|
37
|
+
- 3 neighbors: clear majority (e.g. 2 red, 1 blue)
|
38
|
+
- 2 neighbors: coin flip (e.g. 1 red, 1 blue)
|
39
|
+
* Friendly: Just count friendlies
|
40
|
+
- Enemies don't count, party on! (e.g. 3 red, 2 blue)
|
41
|
+
|
31
42
|
Usage
|
32
43
|
===
|
33
44
|
|
@@ -79,34 +90,9 @@ There is [another yaml file](https://github.com/rickhull/conway_deathmatch/blob/
|
|
79
90
|
Implementation
|
80
91
|
===
|
81
92
|
|
82
|
-
Just one file, aside from shape loading: [Have a look-see](https://github.com/rickhull/conway_deathmatch/blob/master/lib/conway_deathmatch
|
83
|
-
|
84
|
-
This implementation emphasizes simplicity and ease of understanding. Currently
|
85
|
-
there are minimal performance optimizations -- relating to avoiding unnecessary
|
86
|
-
bounds checking.
|
87
|
-
|
88
|
-
I would like to use this project to demonstrate the process of optimization,
|
89
|
-
ideally adding optimization on an optional, parallel, or otherwise
|
90
|
-
non-permanent basis -- i.e. maintain the simple, naive implementation for
|
91
|
-
reference and correctness.
|
93
|
+
Just one file, aside from shape loading: [Have a look-see](https://github.com/rickhull/conway_deathmatch/blob/master/lib/conway_deathmatch.rb)
|
92
94
|
|
93
|
-
Boundaries
|
94
|
-
---
|
95
|
-
Currently:
|
96
|
-
|
97
|
-
* Boundaries are static and fixed
|
98
|
-
* Points out of bounds are treated as always-dead and unable-to-be-populated.
|
99
|
-
|
100
|
-
Deathmatch rules
|
101
|
-
---
|
102
|
-
Choose:
|
103
|
-
* Defensive: Alive cells never switch sides
|
104
|
-
- This is the rule followed by the *Immigration* variant of CGoL, I believe
|
105
|
-
* Aggressive: Alive cells survive with majority
|
106
|
-
- 3 neighbors: clear majority
|
107
|
-
- 2 neighbors: coin flip
|
108
|
-
* Friendly: Just count friendlies
|
109
|
-
- Enemies don't count, party on!
|
95
|
+
Boundaries are toroidal, meaning that cells "wrap" at the edges, such that the left edge is adjacent to the right edge, and likewise with top and bottom. Thus, the grid has the topography of a torus (i.e. doughnut).
|
110
96
|
|
111
97
|
Inspiration
|
112
98
|
---
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0.3
|
data/bin/conway_deathmatch
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
gem 'slop', '~> 3'
|
3
4
|
require 'slop'
|
4
5
|
require 'conway_deathmatch'
|
6
|
+
require 'conway_deathmatch/shapes'
|
5
7
|
|
6
8
|
# process cmdline options
|
7
9
|
#
|
@@ -11,8 +13,8 @@ opts = Slop.parse(help: true,
|
|
11
13
|
optional_arguments: true) do
|
12
14
|
banner 'Usage: conway_deathmatch [options]'
|
13
15
|
|
14
|
-
on 'x', 'width=', '[int]
|
15
|
-
on 'y', 'height=', '[int]
|
16
|
+
on 'x', 'width=', '[int] Grid width', as: Integer
|
17
|
+
on 'y', 'height=', '[int] Grid height', as: Integer
|
16
18
|
# on 'D', 'dimensions=', '[str] width x height', as: String
|
17
19
|
on 'n', 'ticks=', '[int] Max number of ticks to generate', as: Integer
|
18
20
|
on 's', 'sleep=', '[flt] Sleep duration', as: Float
|
@@ -43,18 +45,17 @@ deathmatch = case opts[:deathmatch].to_s.downcase
|
|
43
45
|
|
44
46
|
# create game
|
45
47
|
#
|
46
|
-
|
47
|
-
b = BoardState.new(width, height)
|
48
|
+
b = ConwayDeathmatch.new(width, height)
|
48
49
|
|
49
50
|
# Multiple populations or not
|
50
51
|
if opts[:one] or opts[:two] or opts[:three]
|
51
52
|
b.deathmatch = deathmatch || :aggressive
|
52
|
-
Shapes.add(b, opts[:one], 1) if opts[:one]
|
53
|
-
Shapes.add(b, opts[:two], 2) if opts[:two]
|
54
|
-
Shapes.add(b, opts[:three], 3) if opts[:three]
|
53
|
+
ConwayDeathmatch::Shapes.add(b, opts[:one], 1) if opts[:one]
|
54
|
+
ConwayDeathmatch::Shapes.add(b, opts[:two], 2) if opts[:two]
|
55
|
+
ConwayDeathmatch::Shapes.add(b, opts[:three], 3) if opts[:three]
|
55
56
|
else
|
56
57
|
b.deathmatch = deathmatch
|
57
|
-
Shapes.add(b, shapes)
|
58
|
+
ConwayDeathmatch::Shapes.add(b, shapes)
|
58
59
|
end
|
59
60
|
|
60
61
|
# play game
|
data/bin/proving_ground
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'set'
|
4
|
+
gem 'slop', '~> 3'
|
4
5
|
require 'slop'
|
5
6
|
require 'conway_deathmatch'
|
7
|
+
require 'conway_deathmatch/shapes'
|
6
8
|
|
7
9
|
# process cmdline options
|
8
10
|
#
|
@@ -12,8 +14,8 @@ opts = Slop.parse(help: true,
|
|
12
14
|
optional_arguments: true) do
|
13
15
|
banner 'Usage: proving_ground [options]'
|
14
16
|
|
15
|
-
on 'w', 'width=', '[int]
|
16
|
-
on 'height=', '[int]
|
17
|
+
on 'w', 'width=', '[int] Grid width', as: Integer
|
18
|
+
on 'height=', '[int] Grid height', as: Integer
|
17
19
|
on 'n', 'num_ticks=', '[int] Max number of ticks to generate', as: Integer
|
18
20
|
on 'p', 'num_points=', '[int] Number of points to generate', as: Integer
|
19
21
|
on 'm', 'max_collisions=', '[int] Max number of collisions', as: Integer
|
@@ -64,14 +66,11 @@ def conclude!(results, w, h)
|
|
64
66
|
puts "shape_str: #{shape_str(res[1])}"
|
65
67
|
puts
|
66
68
|
}
|
67
|
-
puts "
|
69
|
+
puts "Grid: #{w}x#{h}"
|
68
70
|
|
69
71
|
exit 0
|
70
72
|
end
|
71
73
|
|
72
|
-
include ConwayDeathmatch
|
73
|
-
ALIVE = BoardState::ALIVE
|
74
|
-
|
75
74
|
results = { final: [0], peak: [0], score: [0], }
|
76
75
|
seen = Set.new
|
77
76
|
collisions = 0
|
@@ -92,24 +91,24 @@ loop {
|
|
92
91
|
end
|
93
92
|
seen << hsh
|
94
93
|
|
95
|
-
# initialize
|
96
|
-
b =
|
94
|
+
# initialize grid with generated shape
|
95
|
+
b = ConwayDeathmatch.new(width, height)
|
97
96
|
b.add_points(points)
|
98
97
|
|
99
98
|
current = peak = score = 0 # track population (results)
|
100
|
-
static_count = 0 # detect a stabilized
|
99
|
+
static_count = 0 # detect a stabilized grid
|
101
100
|
|
102
101
|
# iterate the game of life
|
103
102
|
num_ticks.times { |i|
|
104
103
|
b.tick
|
105
104
|
|
106
|
-
# evaluate
|
105
|
+
# evaluate grid
|
107
106
|
last = current
|
108
|
-
current = b.population[ALIVE]
|
107
|
+
current = b.population[ConwayDeathmatch::ALIVE]
|
109
108
|
peak = current if current > peak
|
110
109
|
score += current * i
|
111
110
|
|
112
|
-
# cease ticks for static or (soon-to-be) empty
|
111
|
+
# cease ticks for static or (soon-to-be) empty grids
|
113
112
|
break if current < 3
|
114
113
|
static_count = (current == last ? static_count + 1 : 0)
|
115
114
|
break if static_count > 3
|
data/conway_deathmatch.gemspec
CHANGED
@@ -12,15 +12,14 @@ Gem::Specification.new do |s|
|
|
12
12
|
'Rakefile',
|
13
13
|
'README.md',
|
14
14
|
'lib/conway_deathmatch.rb',
|
15
|
-
'lib/conway_deathmatch/board_state.rb',
|
16
15
|
'lib/conway_deathmatch/shapes.rb',
|
17
16
|
'lib/conway_deathmatch/shapes/classic.yaml',
|
18
17
|
'lib/conway_deathmatch/shapes/discovered.yaml',
|
19
18
|
'bin/conway_deathmatch',
|
20
19
|
'bin/proving_ground',
|
21
|
-
'test/
|
20
|
+
'test/bench_grid.rb',
|
22
21
|
'test/spec_helper.rb',
|
23
|
-
'test/
|
22
|
+
'test/test_grid.rb',
|
24
23
|
'test/test_shapes.rb',
|
25
24
|
]
|
26
25
|
s.executables = ['conway_deathmatch']
|
@@ -28,6 +27,10 @@ Gem::Specification.new do |s|
|
|
28
27
|
s.add_development_dependency "buildar", "~> 2"
|
29
28
|
s.add_development_dependency "minitest", "~> 5"
|
30
29
|
s.add_development_dependency "ruby-prof", "~> 0"
|
30
|
+
s.add_development_dependency "flog", "~> 4.0"
|
31
|
+
s.add_development_dependency "flay", "~> 2.0"
|
32
|
+
s.add_development_dependency "roodi", "~> 4.0"
|
33
|
+
|
31
34
|
# uncomment and set ENV['CODE_COVERAGE']
|
32
35
|
# s.add_development_dependency "simplecov", "~> 0.9.0"
|
33
36
|
s.required_ruby_version = "~> 2"
|
data/lib/conway_deathmatch.rb
CHANGED
@@ -1,2 +1,145 @@
|
|
1
|
-
require 'conway_deathmatch/
|
2
|
-
require '
|
1
|
+
#require 'conway_deathmatch/shapes'
|
2
|
+
#require 'lager'
|
3
|
+
|
4
|
+
# Provides a 2d array for the grid
|
5
|
+
# Implements standard and deathmatch evaluation rules
|
6
|
+
# Boundaries are toroidal: they wrap in each direction
|
7
|
+
#
|
8
|
+
class ConwayDeathmatch
|
9
|
+
#extend Lager
|
10
|
+
#log_to $stderr
|
11
|
+
class BoundsError < RuntimeError; end
|
12
|
+
|
13
|
+
DEAD = '.'
|
14
|
+
ALIVE = '0'
|
15
|
+
|
16
|
+
def self.new_grid(width, height)
|
17
|
+
grid = []
|
18
|
+
width.times { grid << Array.new(height, DEAD) }
|
19
|
+
grid
|
20
|
+
end
|
21
|
+
|
22
|
+
# nil for traditional, otherwise :aggressive, :defensive, or :friendly
|
23
|
+
attr_accessor :deathmatch
|
24
|
+
|
25
|
+
def initialize(width, height, deathmatch = nil)
|
26
|
+
@width = width
|
27
|
+
@height = height
|
28
|
+
@grid = self.class.new_grid(width, height)
|
29
|
+
@deathmatch = deathmatch
|
30
|
+
#@lager = self.class.lager
|
31
|
+
end
|
32
|
+
|
33
|
+
# Conway's Game of Life transition rules
|
34
|
+
def next_value(x, y)
|
35
|
+
# don't bother toroidaling, only called by #tick
|
36
|
+
n, birthright = neighbor_stats(x, y)
|
37
|
+
if @grid[x][y] != DEAD
|
38
|
+
(n == 2 or n == 3) ? birthright : DEAD
|
39
|
+
else
|
40
|
+
(n == 3) ? birthright : DEAD
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def value(x, y)
|
45
|
+
@grid[x % @width][y % @height]
|
46
|
+
end
|
47
|
+
|
48
|
+
# total (alive) neighbor count and birthright
|
49
|
+
def neighbor_stats(x, y)
|
50
|
+
x = x % @width
|
51
|
+
y = y % @height
|
52
|
+
npop = neighbor_population(x, y).tap { |h| h.delete(DEAD) }
|
53
|
+
|
54
|
+
case @deathmatch
|
55
|
+
when nil
|
56
|
+
[npop.values.reduce(0, :+), ALIVE]
|
57
|
+
|
58
|
+
when :aggressive, :defensive
|
59
|
+
# dead: determine majority (always 3, no need to sample for tie)
|
60
|
+
# alive: agg: determine majority (may tie at 2); def: cell_val
|
61
|
+
determine_majority = (@grid[x][y] == DEAD or @deathmatch == :aggressive)
|
62
|
+
total = 0
|
63
|
+
largest = 0
|
64
|
+
birthrights = []
|
65
|
+
npop.each { |sym, cnt|
|
66
|
+
total += cnt
|
67
|
+
return [0, DEAD] if total > 3 # [optimization]
|
68
|
+
if determine_majority
|
69
|
+
if cnt > largest
|
70
|
+
largest = cnt
|
71
|
+
birthrights = [sym]
|
72
|
+
elsif cnt == largest
|
73
|
+
birthrights << sym
|
74
|
+
end
|
75
|
+
end
|
76
|
+
}
|
77
|
+
[total, determine_majority ? (birthrights.sample || DEAD) : @grid[x][y]]
|
78
|
+
|
79
|
+
when :friendly
|
80
|
+
# [optimization] with knowledge of conway rules
|
81
|
+
# if DEAD, need 3 friendlies to qualify for birth sampling
|
82
|
+
# if ALIVE, npop simply has the friendly count
|
83
|
+
cell_val = if @grid[x][y] == DEAD
|
84
|
+
npop.reduce([]) { |memo, (sym,cnt)|
|
85
|
+
cnt == 3 ? memo + [sym] : memo
|
86
|
+
}.sample || DEAD
|
87
|
+
else
|
88
|
+
@grid[x][y]
|
89
|
+
end
|
90
|
+
# return [0, DEAD] if no one qualifies
|
91
|
+
[npop[cell_val] || 0, cell_val]
|
92
|
+
else
|
93
|
+
raise "unknown: #{@deathmatch.inspect}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# population of every neighboring entity, including DEAD
|
98
|
+
def neighbor_population(x, y)
|
99
|
+
x = x % @width
|
100
|
+
y = y % @height
|
101
|
+
neighbors = Hash.new(0)
|
102
|
+
(x-1).upto(x+1) { |xn|
|
103
|
+
(y-1).upto(y+1) { |yn|
|
104
|
+
xn = xn % @width
|
105
|
+
yn = yn % @height
|
106
|
+
neighbors[@grid[xn][yn]] += 1 unless (xn == x and yn == y)
|
107
|
+
}
|
108
|
+
}
|
109
|
+
neighbors
|
110
|
+
end
|
111
|
+
|
112
|
+
# generate the next grid table
|
113
|
+
def tick
|
114
|
+
new_grid = self.class.new_grid(@width, @height)
|
115
|
+
@width.times { |x|
|
116
|
+
@height.times { |y| new_grid[x][y] = next_value(x, y) }
|
117
|
+
}
|
118
|
+
@grid = new_grid
|
119
|
+
self
|
120
|
+
end
|
121
|
+
|
122
|
+
# set a single point
|
123
|
+
def populate(x, y, val = ALIVE)
|
124
|
+
@grid[x % @width][y % @height] = val
|
125
|
+
end
|
126
|
+
|
127
|
+
# set several points (2d array)
|
128
|
+
def add_points(points, x_off = 0, y_off = 0, val = ALIVE)
|
129
|
+
points.each { |point| populate(point[0] + x_off, point[1] + y_off, val) }
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
# for line-based text output, iterate over y-values first (i.e. per row)
|
134
|
+
def render_text
|
135
|
+
@grid.transpose.map { |row| row.join }.join("\n")
|
136
|
+
end
|
137
|
+
alias_method :render, :render_text
|
138
|
+
|
139
|
+
# full grid scan
|
140
|
+
def population
|
141
|
+
population = Hash.new(0)
|
142
|
+
@grid.each { |col| col.each { |val| population[val] += 1 } }
|
143
|
+
population
|
144
|
+
end
|
145
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
require 'conway_deathmatch
|
1
|
+
require 'conway_deathmatch'
|
2
2
|
require 'yaml'
|
3
3
|
|
4
4
|
module ConwayDeathmatch::Shapes
|
5
5
|
def self.load_yaml(filename)
|
6
6
|
YAML.load_file(File.join(__dir__, 'shapes', filename))
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
# memoize shapes/classic.yaml
|
10
10
|
def self.classic
|
11
11
|
@@classic ||= self.load_yaml('classic.yaml')
|
@@ -15,10 +15,10 @@ module ConwayDeathmatch::Shapes
|
|
15
15
|
def self.discovered
|
16
16
|
@@disovered ||= self.load_yaml('discovered.yaml')
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
# parse a string like "acorn 12 22 block 5 0 p 1 2 p 3 4 p 56 78"
|
20
20
|
# add known shapes
|
21
|
-
def self.add(
|
21
|
+
def self.add(grid, str, val = ConwayDeathmatch::ALIVE)
|
22
22
|
tokens = str.split
|
23
23
|
points = []
|
24
24
|
classic = self.classic
|
@@ -33,9 +33,9 @@ module ConwayDeathmatch::Shapes
|
|
33
33
|
points << [x, y]
|
34
34
|
else
|
35
35
|
found = classic[shape] || self.discovered.fetch(shape)
|
36
|
-
|
36
|
+
grid.add_points(found, x, y, val)
|
37
37
|
end
|
38
38
|
end
|
39
|
-
|
39
|
+
grid.add_points(points, 0, 0, val)
|
40
40
|
end
|
41
41
|
end
|
@@ -1,37 +1,36 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'minitest/benchmark'
|
3
|
-
|
4
3
|
require 'conway_deathmatch'
|
5
|
-
|
6
|
-
include ConwayDeathmatch
|
4
|
+
require 'conway_deathmatch/shapes'
|
7
5
|
|
8
6
|
BENCH_NEW_THRESH = (ENV['BENCH_NEW_THRESH'] || 0.9).to_f
|
9
7
|
BENCH_TICK_THRESH = (ENV['BENCH_TICK_THRESH'] || 0.9995).to_f
|
8
|
+
Shapes = ConwayDeathmatch::Shapes
|
10
9
|
|
11
|
-
describe "
|
10
|
+
describe "ConwayDeathmatch.new Benchmark" do
|
12
11
|
bench_range do
|
13
12
|
bench_exp 9, 9999, 3
|
14
13
|
end
|
15
14
|
|
16
15
|
bench_performance_linear "width*height", BENCH_NEW_THRESH do |n|
|
17
|
-
|
16
|
+
ConwayDeathmatch.new(n, n)
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
21
|
-
describe "
|
20
|
+
describe "ConwayDeathmatch#tick Benchmark" do
|
22
21
|
bench_range do
|
23
22
|
bench_exp 1, 100, 3
|
24
23
|
end
|
25
24
|
|
26
25
|
bench_performance_linear "acorn demo", BENCH_TICK_THRESH do |n|
|
27
|
-
b =
|
26
|
+
b = ConwayDeathmatch.new(70, 40)
|
28
27
|
Shapes.add(b, "acorn 50 18")
|
29
28
|
n.times { b.tick }
|
30
29
|
end
|
31
30
|
|
32
31
|
bench_performance_linear "aggressive deathmatch demo",
|
33
32
|
BENCH_TICK_THRESH do |n|
|
34
|
-
b =
|
33
|
+
b = ConwayDeathmatch.new(70, 40)
|
35
34
|
b.deathmatch = :aggressive
|
36
35
|
Shapes.add(b, "acorn 30 30", "1")
|
37
36
|
Shapes.add(b, "diehard 20 10", "2")
|
@@ -40,7 +39,7 @@ describe "BoardState#tick Benchmark" do
|
|
40
39
|
|
41
40
|
bench_performance_linear "defensive deathmatch demo",
|
42
41
|
BENCH_TICK_THRESH do |n|
|
43
|
-
b =
|
42
|
+
b = ConwayDeathmatch.new(70, 40)
|
44
43
|
b.deathmatch = :defensive
|
45
44
|
Shapes.add(b, "acorn 30 30", "1")
|
46
45
|
Shapes.add(b, "diehard 20 10", "2")
|
@@ -49,7 +48,7 @@ describe "BoardState#tick Benchmark" do
|
|
49
48
|
|
50
49
|
bench_performance_linear "friendly deathmatch demo",
|
51
50
|
BENCH_TICK_THRESH do |n|
|
52
|
-
b =
|
51
|
+
b = ConwayDeathmatch.new(70, 40)
|
53
52
|
b.deathmatch = :friendly
|
54
53
|
Shapes.add(b, "acorn 30 30", "1")
|
55
54
|
Shapes.add(b, "diehard 20 10", "2")
|
data/test/spec_helper.rb
CHANGED
@@ -6,22 +6,8 @@ end
|
|
6
6
|
require 'minitest/spec'
|
7
7
|
require 'minitest/autorun'
|
8
8
|
require 'conway_deathmatch'
|
9
|
+
require 'conway_deathmatch/shapes'
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
ALIVE = BoardState::ALIVE
|
14
|
-
|
15
|
-
SHAPE = "acorn"
|
16
|
-
SHAPE_STR = "#{SHAPE} 0 0"
|
17
|
-
POINTS_COUNT = 7
|
18
|
-
SHAPE_TICK_POINTS = [
|
19
|
-
[0, 1],
|
20
|
-
[1, 1],
|
21
|
-
[2, 1],
|
22
|
-
[4, 1],
|
23
|
-
[4, 2],
|
24
|
-
[5, 1],
|
25
|
-
[5, 2],
|
26
|
-
[5, 3],
|
27
|
-
]
|
11
|
+
ALIVE = ConwayDeathmatch::ALIVE
|
12
|
+
DEAD = ConwayDeathmatch::DEAD
|
13
|
+
Shapes = ConwayDeathmatch::Shapes
|
data/test/test_grid.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe ConwayDeathmatch do
|
4
|
+
describe "an empty grid" do
|
5
|
+
before do
|
6
|
+
@x = 5
|
7
|
+
@y = 5
|
8
|
+
@grid = ConwayDeathmatch.new(@x, @y)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "must have dead population" do
|
12
|
+
@grid.population[DEAD].must_equal @x * @y
|
13
|
+
@grid.population.keys.length.must_equal 1
|
14
|
+
end
|
15
|
+
|
16
|
+
it "must still be dead after a tick" do
|
17
|
+
@grid.tick.population[DEAD].must_equal @x*@y
|
18
|
+
@grid.population.keys.length.must_equal 1
|
19
|
+
end
|
20
|
+
|
21
|
+
it "must accept a block" do
|
22
|
+
@grid.populate 1,1
|
23
|
+
@grid.populate 1,2
|
24
|
+
@grid.populate 2,1
|
25
|
+
@grid.populate 2,2
|
26
|
+
|
27
|
+
@grid.population[DEAD].must_equal @x * @y - 4
|
28
|
+
@grid.population[ALIVE].must_equal 4
|
29
|
+
|
30
|
+
0.upto(4) { |x|
|
31
|
+
0.upto(4) { |y|
|
32
|
+
if x.between?(1, 2) and y.between?(1, 2)
|
33
|
+
@grid.value(x, y).must_equal ALIVE
|
34
|
+
else
|
35
|
+
@grid.value(x, y).must_equal DEAD
|
36
|
+
end
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "adding shapes" do
|
43
|
+
before do
|
44
|
+
@grid = ConwayDeathmatch.new(40, 40)
|
45
|
+
Shapes.add(@grid, "acorn 0 0")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "must recognize \"acorn 0 0\"" do
|
49
|
+
Shapes.classic.fetch("acorn").each { |xy_ary|
|
50
|
+
@grid.value(*xy_ary).must_equal ALIVE
|
51
|
+
}
|
52
|
+
@grid.population.fetch(ALIVE).must_equal 7
|
53
|
+
end
|
54
|
+
|
55
|
+
it "must tick correctly" do
|
56
|
+
@grid.tick
|
57
|
+
new_points = [
|
58
|
+
[0, 1],
|
59
|
+
[1, 1],
|
60
|
+
[2, 1],
|
61
|
+
[4, 1],
|
62
|
+
[4, 2],
|
63
|
+
[5, 1],
|
64
|
+
[5, 2],
|
65
|
+
[5, 3],
|
66
|
+
].each { |xy_ary|
|
67
|
+
@grid.value(*xy_ary).must_equal ALIVE
|
68
|
+
}
|
69
|
+
@grid.population.fetch(ALIVE).must_equal new_points.length
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "aggressive deathmatch" do
|
74
|
+
it "must allow survivors to switch sides" do
|
75
|
+
32.times {
|
76
|
+
@grid = ConwayDeathmatch.new(5, 3, :aggressive)
|
77
|
+
@grid.populate(1, 1, '1') # friendly
|
78
|
+
@grid.populate(2, 1, '1') # survivor
|
79
|
+
@grid.populate(3, 1, '2') # enemy
|
80
|
+
|
81
|
+
@grid.tick
|
82
|
+
break if @grid.value(2, 1) == '2'
|
83
|
+
}
|
84
|
+
|
85
|
+
@grid.population.fetch('1').must_equal 2
|
86
|
+
@grid.population.fetch('2').must_equal 1
|
87
|
+
0.upto(4) { |x|
|
88
|
+
0.upto(2) { |y|
|
89
|
+
if x == 2 and y.between?(0, 2)
|
90
|
+
@grid.value(x, y).must_equal(y == 1 ? '2' : '1')
|
91
|
+
else
|
92
|
+
@grid.value(x, y).must_equal DEAD
|
93
|
+
end
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "defensive deathmatch" do
|
100
|
+
it "must not allow survivors to switch sides" do
|
101
|
+
16.times {
|
102
|
+
@grid = ConwayDeathmatch.new(5, 3, :defensive)
|
103
|
+
@grid.populate(1, 1, '1') # friendly
|
104
|
+
@grid.populate(2, 1, '1') # survivor
|
105
|
+
@grid.populate(3, 1, '2') # enemy
|
106
|
+
@grid.tick
|
107
|
+
|
108
|
+
@grid.population.fetch('1').must_equal 3
|
109
|
+
0.upto(4) { |x|
|
110
|
+
0.upto(2) { |y|
|
111
|
+
if x == 2 and y.between?(0, 2)
|
112
|
+
@grid.value(x, y).must_equal '1'
|
113
|
+
else
|
114
|
+
@grid.value(x, y).must_equal DEAD
|
115
|
+
end
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "friendly deathmatch" do
|
123
|
+
it "must allow survivors with excess hostiles nearby" do
|
124
|
+
@grid = ConwayDeathmatch.new(5, 5, :friendly)
|
125
|
+
@grid.populate(1, 2, '1') # friendly
|
126
|
+
@grid.populate(2, 2, '1') # survivor
|
127
|
+
@grid.populate(3, 2, '1') # friendly
|
128
|
+
@grid.populate(2, 1, '2') # enemy
|
129
|
+
@grid.populate(2, 3, '2') # enemy
|
130
|
+
@grid.tick
|
131
|
+
|
132
|
+
@grid.population.fetch('1').must_equal 1
|
133
|
+
# (2,2) alive despite 4 neighbors, only 2 friendly; all else DEAD
|
134
|
+
0.upto(4) { |x|
|
135
|
+
0.upto(4) { |y|
|
136
|
+
@grid.value(x, y).must_equal (x == 2 && y == 2 ? '1' : DEAD)
|
137
|
+
}
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/test/test_shapes.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
require_relative './spec_helper'
|
2
2
|
|
3
3
|
describe Shapes do
|
4
|
-
it "must recognize
|
5
|
-
Shapes.classic.fetch(
|
4
|
+
it "must recognize acorn" do
|
5
|
+
Shapes.classic.fetch("acorn").must_be_instance_of Array
|
6
6
|
end
|
7
7
|
|
8
|
-
it "must confirm
|
9
|
-
@
|
10
|
-
Shapes.add(@
|
11
|
-
Shapes.classic.fetch(
|
12
|
-
@
|
8
|
+
it "must confirm acorn on the grid" do
|
9
|
+
@grid = ConwayDeathmatch.new(20, 20)
|
10
|
+
Shapes.add(@grid, "acorn 0 0")
|
11
|
+
Shapes.classic.fetch("acorn").each { |xy_ary|
|
12
|
+
@grid.value(*xy_ary).must_equal ALIVE
|
13
13
|
}
|
14
14
|
end
|
15
15
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: conway_deathmatch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rick Hull
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: slop
|
@@ -66,6 +66,48 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: flog
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: flay
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: roodi
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.0'
|
69
111
|
description: Deathmatch
|
70
112
|
email:
|
71
113
|
executables:
|
@@ -80,13 +122,12 @@ files:
|
|
80
122
|
- bin/proving_ground
|
81
123
|
- conway_deathmatch.gemspec
|
82
124
|
- lib/conway_deathmatch.rb
|
83
|
-
- lib/conway_deathmatch/board_state.rb
|
84
125
|
- lib/conway_deathmatch/shapes.rb
|
85
126
|
- lib/conway_deathmatch/shapes/classic.yaml
|
86
127
|
- lib/conway_deathmatch/shapes/discovered.yaml
|
87
|
-
- test/
|
128
|
+
- test/bench_grid.rb
|
88
129
|
- test/spec_helper.rb
|
89
|
-
- test/
|
130
|
+
- test/test_grid.rb
|
90
131
|
- test/test_shapes.rb
|
91
132
|
homepage: https://github.com/rickhull/conway_deathmatch
|
92
133
|
licenses:
|
@@ -108,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
149
|
version: '0'
|
109
150
|
requirements: []
|
110
151
|
rubyforge_project:
|
111
|
-
rubygems_version: 2.
|
152
|
+
rubygems_version: 2.5.2
|
112
153
|
signing_key:
|
113
154
|
specification_version: 4
|
114
155
|
summary: Conway's Game of Life
|
@@ -1,153 +0,0 @@
|
|
1
|
-
#require 'lager'
|
2
|
-
module ConwayDeathmatch; end # create namespace
|
3
|
-
|
4
|
-
# data structure for the board - 2d array
|
5
|
-
# implements standard and deathmatch evaluation
|
6
|
-
# static boundaries are treated as dead
|
7
|
-
#
|
8
|
-
class ConwayDeathmatch::BoardState
|
9
|
-
# extend Lager
|
10
|
-
# log_to $stderr
|
11
|
-
class BoundsError < RuntimeError; end
|
12
|
-
|
13
|
-
DEAD = '.'
|
14
|
-
ALIVE = '0'
|
15
|
-
|
16
|
-
def self.new_state(x_len, y_len)
|
17
|
-
state = []
|
18
|
-
x_len.times { state << Array.new(y_len, DEAD) }
|
19
|
-
state
|
20
|
-
end
|
21
|
-
|
22
|
-
# nil for traditional, otherwise :aggressive, :defensive, or :friendly
|
23
|
-
attr_accessor :deathmatch
|
24
|
-
|
25
|
-
def initialize(x_len, y_len, deathmatch = nil)
|
26
|
-
@x_len = x_len
|
27
|
-
@y_len = y_len
|
28
|
-
@state = self.class.new_state(x_len, y_len)
|
29
|
-
@deathmatch = deathmatch
|
30
|
-
# @lager = self.class.lager
|
31
|
-
end
|
32
|
-
|
33
|
-
# Conway's Game of Life transition rules
|
34
|
-
def next_value(x, y)
|
35
|
-
# don't bother toroidaling, only called by #tick
|
36
|
-
n, birthright = neighbor_stats(x, y)
|
37
|
-
if @state[x][y] != DEAD
|
38
|
-
(n == 2 or n == 3) ? birthright : DEAD
|
39
|
-
else
|
40
|
-
(n == 3) ? birthright : DEAD
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def value(x, y)
|
45
|
-
x = x % @x_len
|
46
|
-
y = y % @y_len
|
47
|
-
@state[x][y]
|
48
|
-
end
|
49
|
-
|
50
|
-
# total (alive) neighbor count and birthright
|
51
|
-
def neighbor_stats(x, y)
|
52
|
-
x = x % @x_len
|
53
|
-
y = y % @y_len
|
54
|
-
npop = neighbor_population(x, y).tap { |h| h.delete(DEAD) }
|
55
|
-
|
56
|
-
case @deathmatch
|
57
|
-
when nil
|
58
|
-
[npop.values.reduce(0, :+), ALIVE]
|
59
|
-
|
60
|
-
when :aggressive, :defensive
|
61
|
-
# dead: determine majority (always 3, no need to sample for tie)
|
62
|
-
# alive: agg: determine majority (may tie at 2); def: cell_val
|
63
|
-
determine_majority = (@state[x][y] == DEAD or @deathmatch == :aggressive)
|
64
|
-
total = 0
|
65
|
-
largest = 0
|
66
|
-
birthrights = []
|
67
|
-
npop.each { |sym, cnt|
|
68
|
-
total += cnt
|
69
|
-
return [0, DEAD] if total >= 4 # [optimization]
|
70
|
-
if determine_majority
|
71
|
-
if cnt > largest
|
72
|
-
largest = cnt
|
73
|
-
birthrights = [sym]
|
74
|
-
elsif cnt == largest
|
75
|
-
birthrights << sym
|
76
|
-
end
|
77
|
-
end
|
78
|
-
}
|
79
|
-
[total, determine_majority ? (birthrights.sample || DEAD) : @state[x][y]]
|
80
|
-
|
81
|
-
when :friendly
|
82
|
-
# [optimized] with knowledge of conway rules
|
83
|
-
# if DEAD, need 3 friendlies to qualify for birth sampling
|
84
|
-
# if ALIVE, npop simply has the friendly count
|
85
|
-
cell_val = if @state[x][y] == DEAD
|
86
|
-
npop.reduce([]) { |memo, (sym,cnt)|
|
87
|
-
cnt == 3 ? memo + [sym] : memo
|
88
|
-
}.sample || DEAD
|
89
|
-
else
|
90
|
-
@state[x][y]
|
91
|
-
end
|
92
|
-
# return [0, DEAD] if no one qualifies
|
93
|
-
[npop[cell_val] || 0, cell_val]
|
94
|
-
else
|
95
|
-
raise "unknown: #{@deathmatch.inspect}"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# population of every neighboring entity, including DEAD
|
100
|
-
def neighbor_population(x, y)
|
101
|
-
x = x % @x_len
|
102
|
-
y = y % @y_len
|
103
|
-
neighbors = Hash.new(0)
|
104
|
-
(x-1).upto(x+1) { |xn|
|
105
|
-
(y-1).upto(y+1) { |yn|
|
106
|
-
xn = xn % @x_len
|
107
|
-
yn = yn % @y_len
|
108
|
-
neighbors[@state[xn][yn]] += 1 unless (xn == x and yn == y)
|
109
|
-
}
|
110
|
-
}
|
111
|
-
neighbors
|
112
|
-
end
|
113
|
-
|
114
|
-
# generate the next state table
|
115
|
-
def tick
|
116
|
-
new_state = self.class.new_state(@x_len, @y_len)
|
117
|
-
@x_len.times { |x|
|
118
|
-
@y_len.times { |y| new_state[x][y] = next_value(x, y) }
|
119
|
-
}
|
120
|
-
@state = new_state
|
121
|
-
self
|
122
|
-
end
|
123
|
-
|
124
|
-
# set a single point
|
125
|
-
def populate(x, y, val = ALIVE)
|
126
|
-
x = x % @x_len
|
127
|
-
y = y % @y_len
|
128
|
-
@state[x][y] = val
|
129
|
-
end
|
130
|
-
|
131
|
-
# set several points (2d array)
|
132
|
-
def add_points(points, x_off = 0, y_off = 0, val = ALIVE)
|
133
|
-
points.each { |point|
|
134
|
-
x = (point[0] + x_off) % @x_len
|
135
|
-
y = (point[1] + y_off) % @y_len
|
136
|
-
@state[x][y] = val
|
137
|
-
}
|
138
|
-
self
|
139
|
-
end
|
140
|
-
|
141
|
-
# for line-based text output, iterate over y-values first (i.e. per row)
|
142
|
-
def render_text
|
143
|
-
@state.transpose.map { |row| row.join }.join("\n")
|
144
|
-
end
|
145
|
-
alias_method :render, :render_text
|
146
|
-
|
147
|
-
# full board scan
|
148
|
-
def population
|
149
|
-
population = Hash.new(0)
|
150
|
-
@state.each { |col| col.each { |val| population[val] += 1 } }
|
151
|
-
population
|
152
|
-
end
|
153
|
-
end
|
data/test/test_board_state.rb
DELETED
@@ -1,132 +0,0 @@
|
|
1
|
-
require_relative './spec_helper'
|
2
|
-
|
3
|
-
describe BoardState do
|
4
|
-
describe "an empty board" do
|
5
|
-
before do
|
6
|
-
@x = 5
|
7
|
-
@y = 5
|
8
|
-
@board = BoardState.new(@x, @y)
|
9
|
-
end
|
10
|
-
|
11
|
-
it "must have dead population" do
|
12
|
-
@board.population[DEAD].must_equal @x * @y
|
13
|
-
@board.population.keys.length.must_equal 1
|
14
|
-
end
|
15
|
-
|
16
|
-
it "must still be dead after a tick" do
|
17
|
-
@board.tick.population[DEAD].must_equal @x*@y
|
18
|
-
@board.population.keys.length.must_equal 1
|
19
|
-
end
|
20
|
-
|
21
|
-
it "must accept a block" do
|
22
|
-
@board.populate 1,1
|
23
|
-
@board.populate 1,2
|
24
|
-
@board.populate 2,1
|
25
|
-
@board.populate 2,2
|
26
|
-
|
27
|
-
@board.population[DEAD].must_equal @x * @y - 4
|
28
|
-
@board.population[ALIVE].must_equal 4
|
29
|
-
|
30
|
-
0.upto(4) { |x|
|
31
|
-
0.upto(4) { |y|
|
32
|
-
if x.between?(1, 2) and y.between?(1, 2)
|
33
|
-
@board.value(x, y).must_equal ALIVE
|
34
|
-
else
|
35
|
-
@board.value(x, y).must_equal DEAD
|
36
|
-
end
|
37
|
-
}
|
38
|
-
}
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
describe "adding shapes" do
|
43
|
-
before do
|
44
|
-
@board = BoardState.new(40, 40)
|
45
|
-
Shapes.add(@board, SHAPE_STR)
|
46
|
-
end
|
47
|
-
|
48
|
-
it "must recognize \"#{SHAPE_STR}\"" do
|
49
|
-
Shapes.classic.fetch(SHAPE).each { |xy_ary|
|
50
|
-
@board.value(*xy_ary).must_equal ALIVE
|
51
|
-
}
|
52
|
-
@board.population.fetch(ALIVE).must_equal POINTS_COUNT
|
53
|
-
end
|
54
|
-
|
55
|
-
it "must tick correctly" do
|
56
|
-
@board.tick
|
57
|
-
SHAPE_TICK_POINTS.each { |xy_ary|
|
58
|
-
@board.value(*xy_ary).must_equal ALIVE
|
59
|
-
}
|
60
|
-
@board.population.fetch(ALIVE).must_equal SHAPE_TICK_POINTS.length
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
describe "aggressive deathmatch" do
|
65
|
-
it "must allow survivors to switch sides" do
|
66
|
-
32.times {
|
67
|
-
@board = BoardState.new(5, 3, :aggressive)
|
68
|
-
@board.populate(1, 1, '1') # friendly
|
69
|
-
@board.populate(2, 1, '1') # survivor
|
70
|
-
@board.populate(3, 1, '2') # enemy
|
71
|
-
|
72
|
-
@board.tick
|
73
|
-
break if @board.value(2, 1) == '2'
|
74
|
-
}
|
75
|
-
|
76
|
-
@board.population.fetch('1').must_equal 2
|
77
|
-
@board.population.fetch('2').must_equal 1
|
78
|
-
0.upto(4) { |x|
|
79
|
-
0.upto(2) { |y|
|
80
|
-
if x == 2 and y.between?(0, 2)
|
81
|
-
@board.value(x, y).must_equal(y == 1 ? '2' : '1')
|
82
|
-
else
|
83
|
-
@board.value(x, y).must_equal DEAD
|
84
|
-
end
|
85
|
-
}
|
86
|
-
}
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
describe "defensive deathmatch" do
|
91
|
-
it "must not allow survivors to switch sides" do
|
92
|
-
16.times {
|
93
|
-
@board = BoardState.new(5, 3, :defensive)
|
94
|
-
@board.populate(1, 1, '1') # friendly
|
95
|
-
@board.populate(2, 1, '1') # survivor
|
96
|
-
@board.populate(3, 1, '2') # enemy
|
97
|
-
@board.tick
|
98
|
-
|
99
|
-
@board.population.fetch('1').must_equal 3
|
100
|
-
0.upto(4) { |x|
|
101
|
-
0.upto(2) { |y|
|
102
|
-
if x == 2 and y.between?(0, 2)
|
103
|
-
@board.value(x, y).must_equal '1'
|
104
|
-
else
|
105
|
-
@board.value(x, y).must_equal DEAD
|
106
|
-
end
|
107
|
-
}
|
108
|
-
}
|
109
|
-
}
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
describe "friendly deathmatch" do
|
114
|
-
it "must allow survivors with excess hostiles nearby" do
|
115
|
-
@board = BoardState.new(5, 5, :friendly)
|
116
|
-
@board.populate(1, 2, '1') # friendly
|
117
|
-
@board.populate(2, 2, '1') # survivor
|
118
|
-
@board.populate(3, 2, '1') # friendly
|
119
|
-
@board.populate(2, 1, '2') # enemy
|
120
|
-
@board.populate(2, 3, '2') # enemy
|
121
|
-
@board.tick
|
122
|
-
|
123
|
-
@board.population.fetch('1').must_equal 1
|
124
|
-
# (2,2) alive despite 4 neighbors, only 2 friendly; all else DEAD
|
125
|
-
0.upto(4) { |x|
|
126
|
-
0.upto(4) { |y|
|
127
|
-
@board.value(x, y).must_equal (x == 2 && y == 2 ? '1' : DEAD)
|
128
|
-
}
|
129
|
-
}
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|