conway_deathmatch 0.3.3.1 → 0.4.0.1

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: 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