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