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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c99458e1f16832079d5dec01f15b43b05d0062e
4
- data.tar.gz: d99447fcdf8a6ef51f8078e5c9783dabd44a5649
3
+ metadata.gz: 95413f3557aa8dcb568065213eb0e296c951aac2
4
+ data.tar.gz: 7640ea076629eaab5134bed4f11288155916de58
5
5
  SHA512:
6
- metadata.gz: 8528c9fcb640e4291b7d1adeb50c68fe0e555af1000f201322f0c0174ef5de4f7cb36bc49d8ffb680214d35b25e84b1b451ed56c3c22ee39e74df02528ba3fef
7
- data.tar.gz: 8da6e47b4acd9b7fa42fa391875e642661437aceaa3d841e3fad91456de294f235abe2aeafa85145b6536d4cf2bb1ef47a5a8cfa7a572c1add1f68a92763d6e7
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/board_state.rb)
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.4.1.1
1
+ 0.5.0.3
@@ -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] Board width', as: Integer
15
- on 'y', 'height=', '[int] Board height', as: Integer
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
- include ConwayDeathmatch
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] Board width', as: Integer
16
- on 'height=', '[int] Board height', as: Integer
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 "Board: #{w}x#{h}"
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 board with generated shape
96
- b = BoardState.new(width, height)
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 board
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 board
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 boards
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
@@ -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/bench_board_state.rb',
20
+ 'test/bench_grid.rb',
22
21
  'test/spec_helper.rb',
23
- 'test/test_board_state.rb',
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"
@@ -1,2 +1,145 @@
1
- require 'conway_deathmatch/board_state'
2
- require 'conway_deathmatch/shapes'
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/board_state'
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(board, str, val = BoardState::ALIVE)
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
- board.add_points(found, x, y, val)
36
+ grid.add_points(found, x, y, val)
37
37
  end
38
38
  end
39
- board.add_points(points, 0, 0, val)
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 "BoardState.new Benchmark" do
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
- BoardState.new(n, n)
16
+ ConwayDeathmatch.new(n, n)
18
17
  end
19
18
  end
20
19
 
21
- describe "BoardState#tick Benchmark" do
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 = BoardState.new(70, 40)
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 = BoardState.new(70, 40)
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 = BoardState.new(70, 40)
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 = BoardState.new(70, 40)
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
- include ConwayDeathmatch
11
-
12
- DEAD = BoardState::DEAD
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 #{SHAPE}" do
5
- Shapes.classic.fetch(SHAPE).must_be_instance_of Array
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 #{SHAPE} on the board" do
9
- @board = BoardState.new(20, 20)
10
- Shapes.add(@board, SHAPE_STR)
11
- Shapes.classic.fetch(SHAPE).each { |xy_ary|
12
- @board.value(*xy_ary).must_equal ALIVE
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.1.1
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: 2014-12-27 00:00:00.000000000 Z
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/bench_board_state.rb
128
+ - test/bench_grid.rb
88
129
  - test/spec_helper.rb
89
- - test/test_board_state.rb
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.4.5
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
@@ -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