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 +4 -4
- data/README.md +54 -38
- data/Rakefile +31 -0
- data/VERSION +1 -1
- data/bin/conway_deathmatch +33 -20
- data/bin/proving_ground +52 -88
- data/conway_deathmatch.gemspec +2 -0
- data/lib/conway_deathmatch/board_state.rb +66 -41
- data/lib/conway_deathmatch/shapes/classic.yaml +1 -1
- data/lib/conway_deathmatch/shapes/discovered.yaml +18 -0
- data/test/bench_board_state.rb +22 -3
- data/test/spec_helper.rb +5 -0
- data/test/test_board_state.rb +80 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 975e5f0b1449157c3a456fda6dca9bb11178bfa5
|
4
|
+
data.tar.gz: 4967c6a3e77b3933a087f3e15d87cf68664b12dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
#
|
49
|
-
conway_deathmatch --one "acorn 30 30" --two "
|
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
|
-
[
|
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
|
-
*
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
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.
|
1
|
+
0.4.0.1
|
data/bin/conway_deathmatch
CHANGED
@@ -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 '
|
15
|
-
on
|
16
|
-
on '
|
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 '
|
20
|
-
on
|
21
|
-
on '
|
22
|
-
|
23
|
-
on '
|
24
|
-
on '
|
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[:
|
32
|
-
render_continuous = (n.nil? or !opts.
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
Shapes.add(b,
|
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.
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
max_c = opts[:max_collisions] ||
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
136
|
-
|
137
|
-
score +=
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
break if
|
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 "#{
|
117
|
+
puts "#{current} (#{peak}) [#{score}]"
|
148
118
|
|
149
119
|
# track the highest populators
|
150
120
|
#
|
151
|
-
if
|
152
|
-
|
153
|
-
|
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 >
|
158
|
-
|
159
|
-
|
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 >
|
164
|
-
|
165
|
-
|
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!(
|
171
|
-
pop_peak, pop_peak_points,
|
172
|
-
top_score, top_score_points,
|
173
|
-
width, height)
|
137
|
+
conclude!(results, width, height)
|
data/conway_deathmatch.gemspec
CHANGED
@@ -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
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
@
|
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
|
61
|
-
|
62
|
-
|
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|
|
@@ -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]
|
data/test/bench_board_state.rb
CHANGED
@@ -29,11 +29,30 @@ describe "BoardState#tick Benchmark" do
|
|
29
29
|
n.times { b.tick }
|
30
30
|
end
|
31
31
|
|
32
|
-
bench_performance_linear "
|
32
|
+
bench_performance_linear "aggressive deathmatch demo",
|
33
|
+
BENCH_TICK_THRESH do |n|
|
33
34
|
b = BoardState.new(70, 40)
|
34
|
-
b.
|
35
|
+
b.deathmatch = :aggressive
|
35
36
|
Shapes.add(b, "acorn 30 30", "1")
|
36
|
-
Shapes.add(b, "
|
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
data/test/test_board_state.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2014-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: buildar
|