conway_deathmatch 0.3.3.1 → 0.4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 37240d180a9787a7ded00f4eaed8fb4e76ae3e39
4
- data.tar.gz: 382b541b24dfac1f0a2cbd4ba76a576922ed7aa8
3
+ metadata.gz: 975e5f0b1449157c3a456fda6dca9bb11178bfa5
4
+ data.tar.gz: 4967c6a3e77b3933a087f3e15d87cf68664b12dc
5
5
  SHA512:
6
- metadata.gz: 294b292a4b0442a39e213ee17661c5247969b334da9851999f4b5a770c1a95e68f22f800702950cb5da706def0082c0358ebf34ef6d180262c27ea0261ceb3fc
7
- data.tar.gz: 3cf35cc4f6490da0d3f6dfaf55f05ad626bc9906e8533960056a049a158801310bb9df1ccc0665107a7b9d6590ade13b27e38bfb80457370142d106b95542e97
6
+ metadata.gz: 30eef27982bdb7c347def54e5b67cb1aea43d96545621254e55ecc57551f88b3c7071b5813cf2213f56ff337d508ffa9017732d9b5f9bc3f195a7930fb58f4d2
7
+ data.tar.gz: 3ab5fbced55739d812ae429d607202eda538c466789610be07a7235904c950ede14aaa891c4de9f21bc2ce98a9e417c212bb6c7b833cd0c7c31ba138267cb928
data/README.md CHANGED
@@ -15,15 +15,18 @@ a 2 dimensional board are populated, and the rules of the game determine the
15
15
  next state, generating interesting, unpredictable, and ultimately lifelike
16
16
  patterns over time.
17
17
 
18
- This project was inspired by http://gameoflifetotalwar.com/ (hereafter CGOLTW).
19
- You should check it out. It updates the classic set of rules, which support
20
- only a single population, for multiple populations which are able to compete
21
- for space and population.
18
+ Rules
19
+ ---
20
+ Cells die or stay dead, unless:
21
+ * Birth rule: 3 neighboring cells turn dead to alive
22
+ * Survival rule: 2 or 3 neighboring cells prevent a live cell from dying
22
23
 
23
- This project exists not to compete with CGOLTW but as a supplementary
24
- project for exploration and learning. My initial motivation was to make a
25
- "proving ground" for searching for simple shapes and patterns with high birth
26
- rates for determining successful CGOLTW strategies.
24
+ On "Deathmatch"
25
+ ---
26
+ The traditional set of rules tracks a single population, even though it may
27
+ form several distinct islands and disjointed groups. For this project,
28
+ *deathmatch* refers to multiple populations with respective identities over
29
+ time (e.g. red vs blue).
27
30
 
28
31
  Usage
29
32
  ===
@@ -45,13 +48,13 @@ Demo
45
48
  # defaults to 70x40 board and an acorn shape
46
49
  conway_deathmatch
47
50
 
48
- # multiplayer
49
- conway_deathmatch --one "acorn 30 30" --two "die_hard 20 10"
51
+ # deathmatch triggered by several populations
52
+ conway_deathmatch --one "acorn 30 30" --two "diehard 20 10"
50
53
 
51
54
  Available Shapes
52
55
  ---
53
56
 
54
- [Definitions](https://github.com/rickhull/conway_deathmatch/blob/master/lib/conway_deathmatch/shapes/classic.yaml)
57
+ A shape is simply a set of points. Classic shapes are [defined in a yaml file](https://github.com/rickhull/conway_deathmatch/blob/master/lib/conway_deathmatch/shapes/classic.yaml):
55
58
 
56
59
  * acorn
57
60
  * beacon
@@ -62,7 +65,7 @@ Available Shapes
62
65
  * block_engine_space (block engine, minimal footprint)
63
66
  * block_engine_stripe (block engine, 1 point tall)
64
67
  * boat
65
- * die_hard
68
+ * diehard
66
69
  * glider
67
70
  * loaf
68
71
  * lwss (lightweight spaceship)
@@ -70,45 +73,58 @@ Available Shapes
70
73
  * swastika
71
74
  * toad
72
75
 
76
+ There is [another yaml file](https://github.com/rickhull/conway_deathmatch/blob/master/lib/conway_deathmatch/shapes/discovered.yaml) with shapes discovered via [proving_ground](https://github.com/rickhull/conway_deathmatch/blob/master/bin/proving_ground).
77
+
78
+
73
79
  Implementation
74
80
  ===
75
81
 
76
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)
77
83
 
78
84
  This implementation emphasizes simplicity and ease of understanding. Currently
79
- there are no performance optimizations. I would like to use this project
80
- to demonstrate the process of optimization, ideally adding optimization on
81
- an optional, parallel, or otherwise non-permanent basis -- i.e. maintain the
82
- simple, naive implementation for reference and correctness.
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.
92
+
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!
83
110
 
84
111
  Inspiration
85
112
  ---
113
+ This project was inspired by http://gameoflifetotalwar.com/ (hereafter CGOLTW).
114
+ You should check it out. It updates the classic set of rules, which support
115
+ only a single population, for multiple populations which are able to compete
116
+ for space and population.
117
+
118
+ This project exists not to compete with CGOLTW but as a supplementary
119
+ project for exploration and learning. My initial motivation was to make a
120
+ "[proving ground](https://github.com/rickhull/conway_deathmatch/blob/master/bin/proving_ground)" for searching for simple shapes and patterns with high birth
121
+ rates for determining successful CGOLTW strategies.
122
+
86
123
  Coming into this project, I had significant background knowledge concerning
87
124
  Conway's Game of Life, but I could not have recited the basic rules in any
88
125
  form. After being inspired by competing in CGOLTW, I read their [one background
89
126
  page](http://gameoflifetotalwar.com/how-to-play) and then the
90
127
  [wikipedia page](http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). I
91
128
  deliberately avoided any knowledge of any other implementations,
92
- considering this project's implementation as of November 24 (2014) to be the
129
+ considering this project's implementation as of December 5 (2014) to be the
93
130
  naive, simple approach.
94
-
95
- Researched optimizations are now an immediate priority.
96
-
97
- Caveats
98
- ---
99
-
100
- ### Boundaries
101
-
102
- As currently implemented, this project uses fixed boundaries, and boundary
103
- behavior is not standardized to my knowledge. For this project, points out of
104
- bounds are treated as always-dead and unable-to-be-populated.
105
-
106
- ### Multiplayer
107
-
108
- The rules for multiplayer are not standardized. I read about the CGOLTW
109
- approach, and this project's approach is similar but different. In CGOLTW,
110
- there are always 3 populations, and one population (civilians) is special, in
111
- that civilians are born where there is birthright contention. Birthright
112
- contention happens when a new cell must be generated, but none of the
113
- neighboring parents have a unique plurality. For this project, birthright
114
- contention is resolved with a random selection (TODO).
data/Rakefile CHANGED
@@ -3,9 +3,11 @@ require 'buildar'
3
3
  Buildar.new do |b|
4
4
  b.gemspec_file = 'conway_deathmatch.gemspec'
5
5
  b.version_file = 'VERSION'
6
+ b.use_git = true
6
7
  end
7
8
 
8
9
  task default: %w[test bench]
10
+ task travis: %w[test bench ruby-prof]
9
11
 
10
12
  require 'rake/testtask'
11
13
  desc "Run tests"
@@ -21,3 +23,32 @@ Rake::TestTask.new do |t|
21
23
  t.pattern = "test/bench_*.rb"
22
24
  # t.warning = true
23
25
  end
26
+
27
+ desc "Generate code metrics reports"
28
+ task :code_metrics => [:flog, :flay, :roodi] do
29
+ end
30
+
31
+ desc "Run flog on lib/"
32
+ task :flog do
33
+ puts
34
+ sh "flog lib | tee metrics/flog"
35
+ end
36
+
37
+ desc "Run flay on lib/"
38
+ task :flay do
39
+ puts
40
+ sh "flay lib | tee metrics/flay"
41
+ end
42
+
43
+ desc "Run roodi on lib/"
44
+ task :roodi do
45
+ puts
46
+ sh "roodi -config=.roodi.yml lib | tee metrics/roodi"
47
+ end
48
+
49
+ # this runs against the installed gem lib, not git / filesystem
50
+ desc "Run ruby-prof on bin/conway_deathmatch (100 ticks)"
51
+ task "ruby-prof" do
52
+ sh ["ruby-prof -m1 bin/conway_deathmatch -- -n100 -s0 --renderfinal",
53
+ "| tee metrics/ruby-prof"].join(' ')
54
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3.1
1
+ 0.4.0.1
@@ -11,38 +11,51 @@ opts = Slop.parse(help: true,
11
11
  optional_arguments: true) do
12
12
  banner 'Usage: conway_deathmatch [options]'
13
13
 
14
- on 'w', 'width=', '[int] Board width', as: Integer
15
- on 'height=', '[int] Board height', as: Integer
16
- on 'n', 'num_ticks=', '[int] Max number of ticks to generate', as: Integer
14
+ on 'x', 'width=', '[int] Board width', as: Integer
15
+ on 'y', 'height=', '[int] Board height', as: Integer
16
+ # on 'D', 'dimensions=', '[str] width x height', as: String
17
+ on 'n', 'ticks=', '[int] Max number of ticks to generate', as: Integer
17
18
  on 's', 'sleep=', '[flt] Sleep duration', as: Float
18
19
  on 'p', 'points=', '[str] e.g. "acorn 50 18 p 1 2 p 3 4"', as: String
19
- on 'S', 'step', 'Hold ticks for user input'
20
- on 'silent', 'Only render the final state'
21
- on 'm', 'multiplayer', 'Use multiplayer evaluation'
22
- on 'one=', '[str] points for population "1"'
23
- on 'two=', '[str] points for population "2"'
24
- on 'three=', '[str] points for population "3"'
20
+ on 'g', 'step', 'Hold ticks for user input'
21
+ on 'r', 'renderfinal', 'Only render the final state'
22
+ on 'd', 'deathmatch',
23
+ '[str] single|aggressive|defensive|friendly', as: String
24
+ on 'one=', '[str] points for population "1"', as: String
25
+ on 'two=', '[str] points for population "2"', as: String
26
+ on 'three=', '[str] points for population "3"', as: String
25
27
  end
26
28
 
27
29
  width = opts[:width] || 70
28
30
  height = opts[:height] || 40
29
31
  shapes = opts[:points] || "acorn 50 18"
30
32
  slp = opts[:sleep] || 0.02
31
- n = opts[:num_ticks]
32
- render_continuous = (n.nil? or !opts.silent?)
33
+ n = opts[:ticks]
34
+ render_continuous = (n.nil? or !opts.renderfinal?)
35
+ deathmatch = case opts[:deathmatch].to_s.downcase
36
+ when '', 's', 'standard', 'single', 't', 'traditional' then nil
37
+ when 'a', 'aggressive' then :aggressive
38
+ when 'd', 'defensive' then :defensive
39
+ when 'f', 'friendly' then :friendly
40
+ else
41
+ raise "unknown: #{opts[:deathmatch]}"
42
+ end
33
43
 
34
44
  # create game
35
45
  #
36
46
  include ConwayDeathmatch
37
47
  b = BoardState.new(width, height)
38
- if opts.multiplayer? or opts[:one] or opts[:two] or opts[:three]
39
- b.multiplayer = true
40
- end
41
48
 
42
- Shapes.add(b, opts[:one], 1) if opts[:one]
43
- Shapes.add(b, opts[:two], 2) if opts[:two]
44
- Shapes.add(b, opts[:three], 3) if opts[:three]
45
- Shapes.add(b, shapes) unless b.multiplayer
49
+ # Multiple populations or not
50
+ if opts[:one] or opts[:two] or opts[:three]
51
+ 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]
55
+ else
56
+ b.deathmatch = deathmatch
57
+ Shapes.add(b, shapes)
58
+ end
46
59
 
47
60
  # play game
48
61
  #
@@ -55,7 +68,7 @@ while n.nil? or count <= n
55
68
  end
56
69
 
57
70
  b.tick
58
-
71
+
59
72
  if opts.step?
60
73
  gets
61
74
  else
@@ -66,7 +79,7 @@ end
66
79
 
67
80
  # finish
68
81
  #
69
- if n and opts.silent?
82
+ if n and opts.renderfinal?
70
83
  puts
71
84
  puts count
72
85
  puts b.render
data/bin/proving_ground CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'set'
3
4
  require 'slop'
4
5
  require 'conway_deathmatch'
5
6
 
@@ -14,44 +15,15 @@ opts = Slop.parse(help: true,
14
15
  on 'w', 'width=', '[int] Board width', as: Integer
15
16
  on 'height=', '[int] Board height', as: Integer
16
17
  on 'n', 'num_ticks=', '[int] Max number of ticks to generate', as: Integer
17
- on 'silent', 'Only render the final state'
18
18
  on 'p', 'num_points=', '[int] Number of points to generate', as: Integer
19
19
  on 'm', 'max_collisions=', '[int] Max number of collisions', as: Integer
20
20
  end
21
21
 
22
- width = opts[:width] || 40
23
- height = opts[:height] || 40
24
- n = opts[:num_ticks] || 40
25
- p = opts[:num_points] || 5
26
- max_c = opts[:max_collisions] || p ** 2
27
-
28
- # answers / output
29
- pop_final = 0
30
- pop_final_points = []
31
- pop_peak = 0
32
- pop_peak_points = []
33
- top_score = 0
34
- top_score_points = []
35
-
36
- def conclude!(final, final_points,
37
- peak, peak_points,
38
- score, score_points,
39
- w, h)
40
- puts
41
- puts "final: #{final}"
42
- puts "final_points: #{final_points}"
43
- puts "shape_str: #{shape_str(final_points)}"
44
- puts
45
- puts "peak: #{peak}"
46
- puts "peak_points: #{peak_points}"
47
- puts "shape_str: #{shape_str(peak_points)}"
48
- puts
49
- puts "score: #{score}"
50
- puts "score_points: #{score_points}"
51
- puts "shape_str: #{shape_str(score_points)}"
52
-
53
- exit 0
54
- end
22
+ num_points = opts[:num_points] || 5
23
+ width = opts[:width] || num_points * 5
24
+ height = opts[:height] || num_points * 5
25
+ num_ticks = opts[:num_ticks] || num_points * 5
26
+ max_c = opts[:max_collisions] || num_points ** 2
55
27
 
56
28
  # choose center point
57
29
  # choose next point within 2 units randomly
@@ -86,88 +58,80 @@ def shape_str(points)
86
58
  points.map { |point| "p #{point[0]} #{point[1]}" }.join(' ')
87
59
  end
88
60
 
89
- Signal.trap("INT") do
90
- conclude!(pop_final, pop_final_points,
91
- pop_peak, pop_peak_points,
92
- top_score, top_score_points,
93
- width, height)
61
+ def conclude!(results, w, h)
62
+ results.each { |k, res|
63
+ puts "#{k} population: #{res[0]}"
64
+ puts "shape_str: #{shape_str(res[1])}"
65
+ puts
66
+ }
67
+ puts "Board: #{w}x#{h}"
68
+
69
+ exit 0
94
70
  end
95
71
 
96
72
  include ConwayDeathmatch
97
73
  ALIVE = BoardState::ALIVE
98
- SEEN = {}
74
+
75
+ results = { final: [0], peak: [0], score: [0], }
76
+ seen = Set.new
99
77
  collisions = 0
100
78
 
79
+ Signal.trap("INT") { conclude!(results, width, height) }
80
+
101
81
  loop {
102
- # populate new board with random points
103
- #
104
- b = BoardState.new(width, height)
105
- points = generate_points(p, width, height)
82
+ # generate a random shape
83
+ points = generate_points(num_points, width, height)
106
84
 
107
85
  # have we seen these points before?
108
- #
109
- if SEEN[points]
86
+ if seen.member?(points.hash)
110
87
  collisions += 1
111
88
  puts "X" * collisions
112
- break if collisions > max_c
89
+ break if collisions > max_c # exit the loop, stop generating points
113
90
  next
114
- else
115
- SEEN[points] = true
116
91
  end
92
+ seen << points.hash
93
+
94
+ # initialize board with generated shape
95
+ b = BoardState.new(width, height)
117
96
  b.add_points(points)
118
97
 
119
- # establish vars outside block
120
- #
121
- pop = nil
122
- peak = 0
123
- static_cnt = 0
124
- score = 0
125
- ticks = 0
98
+ current = peak = score = 0 # track population (results)
99
+ static_count = 0 # detect a stabilized board
126
100
 
127
- # tick and track population
128
- #
129
- n.times { |i|
101
+ # iterate the game of life
102
+ num_ticks.times { |i|
130
103
  b.tick
131
- ticks += 1
132
104
 
133
105
  # evaluate board
134
- #
135
- last_pop = pop
136
- pop = b.population[ALIVE] || 0
137
- score += ticks * pop
138
- peak = pop if pop > peak
139
-
140
- # short-circuit static or (soon-to-be) empty boards
141
- #
142
- break if pop < 3
143
- static_cnt = (pop == last_pop ? static_cnt + 1 : 0)
144
- break if static_cnt > 3
106
+ last = current
107
+ current = b.population[ALIVE]
108
+ peak = current if current > peak
109
+ score += current * i
110
+
111
+ # cease ticks for static or (soon-to-be) empty boards
112
+ break if current < 3
113
+ static_count = (current == last ? static_count + 1 : 0)
114
+ break if static_count > 3
145
115
  }
146
116
 
147
- puts "#{pop} (#{peak}) [#{score}]"
117
+ puts "#{current} (#{peak}) [#{score}]"
148
118
 
149
119
  # track the highest populators
150
120
  #
151
- if pop > pop_final
152
- pop_final = pop
153
- pop_final_points = points
154
- puts "\tLargest final: #{pop_final}"
121
+ if current > results[:final][0]
122
+ results[:final] = [current, points]
123
+ puts "\tLargest final: #{current}"
155
124
  end
156
125
 
157
- if peak > pop_peak
158
- pop_peak = peak
159
- pop_peak_points = points
160
- puts "\tLargest peak: #{pop_peak}"
126
+ if peak > results[:peak][0]
127
+ results[:peak] = [peak, points]
128
+ puts "\tLargest peak: #{peak}"
161
129
  end
162
130
 
163
- if score > top_score
164
- top_score = score
165
- top_score_points = points
166
- puts "\tLargest score: #{top_score}"
131
+ if score > results[:score][0]
132
+ results[:score] = [score, points]
133
+ puts "\tLargest score: #{score}"
167
134
  end
168
135
  }
169
136
 
170
- conclude!(pop_final, pop_final_points,
171
- pop_peak, pop_peak_points,
172
- top_score, top_score_points,
173
- width, height)
137
+ conclude!(results, width, height)
@@ -26,5 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.executables = ['conway_deathmatch']
27
27
  s.add_development_dependency "buildar", "~> 2"
28
28
  s.add_development_dependency "minitest", "~> 5"
29
+ # uncomment and set ENV['CODE_COVERAGE']
30
+ # s.add_development_dependency "simplecov", "~> 0.9.0"
29
31
  s.required_ruby_version = "~> 2"
30
32
  end
@@ -1,30 +1,35 @@
1
+ # require 'lager'
1
2
  module ConwayDeathmatch; end # create namespace
2
3
 
3
4
  # data structure for the board - 2d array
4
- # implements standard and multiplayer evaluation
5
+ # implements standard and deathmatch evaluation
5
6
  # static boundaries are treated as dead
6
7
  #
7
8
  class ConwayDeathmatch::BoardState
9
+ # extend Lager
10
+ # log_to $stderr
8
11
  class BoundsError < RuntimeError; end
9
12
 
10
13
  DEAD = '.'
11
14
  ALIVE = '0'
12
-
15
+
13
16
  def self.new_state(x_len, y_len)
14
17
  state = []
15
18
  x_len.times { state << Array.new(y_len, DEAD) }
16
19
  state
17
20
  end
18
21
 
19
- attr_accessor :multiplayer
20
-
21
- def initialize(x_len, y_len)
22
+ # nil for traditional, otherwise :aggressive, :defensive, or :friendly
23
+ attr_accessor :deathmatch
24
+
25
+ def initialize(x_len, y_len, deathmatch = nil)
22
26
  @x_len = x_len
23
27
  @y_len = y_len
24
28
  @state = self.class.new_state(x_len, y_len)
25
- @multiplayer = false
29
+ @deathmatch = deathmatch
30
+ # @lager = self.class.lager
26
31
  end
27
-
32
+
28
33
  # Conway's Game of Life transition rules
29
34
  def next_value(x, y)
30
35
  n, birthright = neighbor_stats(x, y)
@@ -35,6 +40,53 @@ class ConwayDeathmatch::BoardState
35
40
  end
36
41
  end
37
42
 
43
+ # total (alive) neighbor count and birthright
44
+ def neighbor_stats(x, y)
45
+ npop = neighbor_population(x, y).tap { |h| h.delete(DEAD) }
46
+
47
+ case @deathmatch
48
+ when nil
49
+ [npop.values.reduce(0, :+), ALIVE]
50
+
51
+ when :aggressive, :defensive
52
+ # dead: determine majority (always 3, no need to sample for tie)
53
+ # alive: agg: determine majority (may tie at 2); def: cell_val
54
+ determine_majority = (@state[x][y] == DEAD or @deathmatch == :aggressive)
55
+ total = 0
56
+ largest = 0
57
+ birthrights = []
58
+ npop.each { |sym, cnt|
59
+ total += cnt
60
+ return [0, DEAD] if total >= 4 # [optimization]
61
+ if determine_majority
62
+ if cnt > largest
63
+ largest = cnt
64
+ birthrights = [sym]
65
+ elsif cnt == largest
66
+ birthrights << sym
67
+ end
68
+ end
69
+ }
70
+ [total, determine_majority ? (birthrights.sample || DEAD) : @state[x][y]]
71
+
72
+ when :friendly
73
+ # [optimized] with knowledge of conway rules
74
+ # if DEAD, need 3 friendlies to qualify for birth sampling
75
+ # if ALIVE, npop simply has the friendly count
76
+ cell_val = if @state[x][y] == DEAD
77
+ npop.reduce([]) { |memo, (sym,cnt)|
78
+ cnt == 3 ? memo + [sym] : memo
79
+ }.sample || DEAD
80
+ else
81
+ @state[x][y]
82
+ end
83
+ # return [0, DEAD] if no one qualifies
84
+ [npop[cell_val] || 0, cell_val]
85
+ else
86
+ raise "unknown: #{@deathmatch.inspect}"
87
+ end
88
+ end
89
+
38
90
  def value(x, y)
39
91
  in_bounds!(x,y)
40
92
  @state[x][y].dup
@@ -43,54 +95,27 @@ class ConwayDeathmatch::BoardState
43
95
  def in_bounds?(x, y)
44
96
  x.between?(0, @x_len - 1) and y.between?(0, @y_len - 1)
45
97
  end
46
-
98
+
47
99
  def in_bounds!(x, y)
48
100
  raise(BoundsError, "(#{x}, #{y})") unless in_bounds?(x, y)
49
101
  end
50
-
102
+
51
103
  # out of bounds considered dead
52
104
  def alive?(x, y)
53
105
  @state[x][y] != DEAD rescue false
54
106
  end
55
-
107
+
56
108
  # population of every neighboring entity, including DEAD
57
109
  def neighbor_population(x, y)
58
- outer_ring = (x == 0 or y == 0 or x == @x_len - 1 or y == @y_len - 1)
59
110
  neighbors = Hash.new(0)
60
- (x-1..x+1).each { |xn|
61
- next if outer_ring and !xn.between?(0, @x_len - 1)
62
- (y-1..y+1).each { |yn|
63
- next if (outer_ring and !yn.between?(0, @y_len - 1)) or
64
- (xn == x and yn == y)
65
- neighbors[@state[xn][yn]] += 1
111
+ (x-1 > 0 ? x-1 : 0).upto(x+1 < @x_len ? x+1 : @x_len - 1) { |xn|
112
+ (y-1 > 0 ? y-1 : 0).upto(y+1 < @y_len ? y+1 : @y_len - 1) { |yn|
113
+ neighbors[@state[xn][yn]] += 1 unless (xn == x and yn == y)
66
114
  }
67
115
  }
68
116
  neighbors
69
117
  end
70
118
 
71
- # total (alive) neighbor count and birthright
72
- def neighbor_stats(x, y)
73
- if @multiplayer
74
- total = 0
75
- largest = 0
76
- birthright = nil
77
- neighbor_population(x, y).each { |sym, cnt|
78
- total += cnt
79
- if cnt > largest
80
- largest = cnt
81
- birthright = sym
82
- end
83
- }
84
- [total, birthright]
85
- else
86
- count = 0
87
- neighbor_population(x, y).each { |sym, cnt|
88
- count += cnt unless sym == DEAD
89
- }
90
- [count, ALIVE]
91
- end
92
- end
93
-
94
119
  # generate the next state table
95
120
  def tick
96
121
  new_state = self.class.new_state(@x_len, @y_len)
@@ -106,7 +131,7 @@ class ConwayDeathmatch::BoardState
106
131
  in_bounds!(x, y)
107
132
  @state[x][y] = val
108
133
  end
109
-
134
+
110
135
  # set several points (2d array), ignore OOB
111
136
  def add_points(points, x_off = 0, y_off = 0, val = ALIVE)
112
137
  points.each { |point|
@@ -97,7 +97,7 @@ boat:
97
97
  - [1, 2]
98
98
  - [2, 1]
99
99
 
100
- die_hard:
100
+ diehard:
101
101
  - [0, 1]
102
102
  - [1, 1]
103
103
  - [1, 2]
@@ -57,6 +57,14 @@ b6: # trinary
57
57
  - [3, 2]
58
58
  - [4, 0]
59
59
 
60
+ c6: # cardioid
61
+ - [0, 0]
62
+ - [0, 1]
63
+ - [1, 1]
64
+ - [1, 2]
65
+ - [2, 0]
66
+ - [2, 1]
67
+
60
68
  a7:
61
69
  - [0, 3]
62
70
  - [1, 4]
@@ -83,3 +91,13 @@ c7: # urawizardarry
83
91
  - [4, 3]
84
92
  - [5, 4]
85
93
  - [5, 5]
94
+
95
+ a8: # A
96
+ - [0, 2]
97
+ - [1, 1]
98
+ - [1, 2]
99
+ - [2, 0]
100
+ - [2, 1]
101
+ - [3, 1]
102
+ - [3, 2]
103
+ - [4, 2]
@@ -29,11 +29,30 @@ describe "BoardState#tick Benchmark" do
29
29
  n.times { b.tick }
30
30
  end
31
31
 
32
- bench_performance_linear "multiplayer demo", BENCH_TICK_THRESH do |n|
32
+ bench_performance_linear "aggressive deathmatch demo",
33
+ BENCH_TICK_THRESH do |n|
33
34
  b = BoardState.new(70, 40)
34
- b.multiplayer = true
35
+ b.deathmatch = :aggressive
35
36
  Shapes.add(b, "acorn 30 30", "1")
36
- Shapes.add(b, "die_hard 20 10", "2")
37
+ Shapes.add(b, "diehard 20 10", "2")
38
+ n.times { b.tick }
39
+ end
40
+
41
+ bench_performance_linear "defensive deathmatch demo",
42
+ BENCH_TICK_THRESH do |n|
43
+ b = BoardState.new(70, 40)
44
+ b.deathmatch = :defensive
45
+ Shapes.add(b, "acorn 30 30", "1")
46
+ Shapes.add(b, "diehard 20 10", "2")
47
+ n.times { b.tick }
48
+ end
49
+
50
+ bench_performance_linear "friendly deathmatch demo",
51
+ BENCH_TICK_THRESH do |n|
52
+ b = BoardState.new(70, 40)
53
+ b.deathmatch = :friendly
54
+ Shapes.add(b, "acorn 30 30", "1")
55
+ Shapes.add(b, "diehard 20 10", "2")
37
56
  n.times { b.tick }
38
57
  end
39
58
  end
data/test/spec_helper.rb CHANGED
@@ -1,3 +1,8 @@
1
+ if ENV['CODE_COVERAGE']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
1
6
  require 'minitest/spec'
2
7
  require 'minitest/autorun'
3
8
  require 'conway_deathmatch'
@@ -7,7 +7,7 @@ describe BoardState do
7
7
  @y = 5
8
8
  @board = BoardState.new(@x, @y)
9
9
  end
10
-
10
+
11
11
  it "must have dead population" do
12
12
  @board.population[DEAD].must_equal @x * @y
13
13
  @board.population.keys.length.must_equal 1
@@ -26,6 +26,16 @@ describe BoardState do
26
26
 
27
27
  @board.population[DEAD].must_equal @x * @y - 4
28
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
+ }
29
39
  end
30
40
  end
31
41
 
@@ -50,4 +60,73 @@ describe BoardState do
50
60
  @board.population.fetch(ALIVE).must_equal SHAPE_TICK_POINTS.length
51
61
  end
52
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
53
132
  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.3.3.1
4
+ version: 0.4.0.1
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-05 00:00:00.000000000 Z
11
+ date: 2014-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: buildar