commodore_cox 1.0
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.
- data/Battleship.Rakefile +12 -0
- data/Rakefile +47 -0
- data/lib/commodore_cox/ai.rb +264 -0
- data/lib/commodore_cox/commodore_cox.rb +55 -0
- data/spec/commodore_cox/ai_spec.rb +108 -0
- data/spec/commodore_cox/commodore_cox_spec.rb +10 -0
- data/spec/spec_helper.rb +4 -0
- metadata +64 -0
data/Battleship.Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#
|
2
|
+
# DO NOT tamper with this file. It will lead to disqualification.
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
desc "Run all examples with RCov"
|
8
|
+
Spec::Rake::SpecTask.new('spec_with_rcov') do |t|
|
9
|
+
t.spec_files = FileList['spec/**/*.rb']
|
10
|
+
t.rcov = true
|
11
|
+
t.rcov_opts = ['-t', '--exclude', 'spec', '--no-html']
|
12
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'battleship_tournament/submit'
|
5
|
+
|
6
|
+
desc "Run all specs"
|
7
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
8
|
+
t.spec_files = FileList['spec/**/*.rb']
|
9
|
+
t.rcov = false
|
10
|
+
end
|
11
|
+
|
12
|
+
PKG_NAME = "commodore_cox"
|
13
|
+
PKG_VERSION = "1.0"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = PKG_NAME
|
17
|
+
s.version = PKG_VERSION
|
18
|
+
s.files = FileList['**/*'].to_a
|
19
|
+
s.require_path = 'lib'
|
20
|
+
s.test_files = Dir.glob('spec/*_spec.rb')
|
21
|
+
s.bindir = 'bin'
|
22
|
+
s.executables = []
|
23
|
+
s.summary = "Battleship Player:Commodore Cox"
|
24
|
+
s.rubyforge_project = "sparring"
|
25
|
+
s.homepage = "http://sparring.rubyforge.org/"
|
26
|
+
|
27
|
+
###########################################
|
28
|
+
##
|
29
|
+
## You are encouraged to modify the following
|
30
|
+
## spec attributes.
|
31
|
+
##
|
32
|
+
###########################################
|
33
|
+
s.description = "Commodore Cox"
|
34
|
+
s.author = "Andrew Cox"
|
35
|
+
s.email = "3coxy4@gmail.com"
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
39
|
+
pkg.need_zip = false
|
40
|
+
pkg.need_tar = false
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "Submit your player"
|
44
|
+
task :submit do
|
45
|
+
submitter = BattleshipTournament::Submit.new(PKG_NAME)
|
46
|
+
submitter.submit
|
47
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
class Array
|
2
|
+
def random
|
3
|
+
self[size*rand]
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module CommodoreCox
|
8
|
+
|
9
|
+
class AI
|
10
|
+
|
11
|
+
SHIPS = {
|
12
|
+
:carrier => 5,
|
13
|
+
:battleship => 4,
|
14
|
+
:destroyer => 3,
|
15
|
+
:submarine => 3,
|
16
|
+
:patrolship => 2
|
17
|
+
}
|
18
|
+
|
19
|
+
OPPOSITE = {
|
20
|
+
:north => :south,
|
21
|
+
:east => :west,
|
22
|
+
:south => :north,
|
23
|
+
:west => :east
|
24
|
+
}
|
25
|
+
|
26
|
+
attr_accessor :placestack, :direction, :hitstack
|
27
|
+
|
28
|
+
def setup
|
29
|
+
@hitstack = []
|
30
|
+
@placestack = []
|
31
|
+
@taken_squares = []
|
32
|
+
@backtracking_stack = []
|
33
|
+
@direction = nil
|
34
|
+
a = (1..10).to_a
|
35
|
+
a.each do |x|
|
36
|
+
a.each do |y|
|
37
|
+
Grid[y,x] = Square.new(y,x)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def hit_in_progress?
|
43
|
+
@hitstack.any?
|
44
|
+
end
|
45
|
+
|
46
|
+
def backtracking?
|
47
|
+
@backtracking_stack.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def direction?
|
51
|
+
!@direction.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def ships
|
55
|
+
SHIPS
|
56
|
+
end
|
57
|
+
|
58
|
+
def opposite
|
59
|
+
OPPOSITE
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_placement
|
63
|
+
@placestack.last.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def change_direction
|
67
|
+
@direction = opposite[@direction]
|
68
|
+
end
|
69
|
+
|
70
|
+
def pick_from_random_neighbour(square,set_direction=true)
|
71
|
+
neighbour = nil
|
72
|
+
neighbours = square.neighbours
|
73
|
+
if neighbours.any?
|
74
|
+
neighbour = neighbours.random
|
75
|
+
@direction = neighbour[0] if set_direction
|
76
|
+
neighbour = neighbour[1]
|
77
|
+
Grid.delete(neighbour)
|
78
|
+
end
|
79
|
+
neighbour
|
80
|
+
end
|
81
|
+
|
82
|
+
def pick_from_backtrack
|
83
|
+
return nil unless backtracking?
|
84
|
+
|
85
|
+
square = if direction?
|
86
|
+
@backtracking_stack.last.send(@direction)
|
87
|
+
else
|
88
|
+
pick_from_random_neighbour(@backtracking_stack.last,false)
|
89
|
+
end
|
90
|
+
if square.nil?
|
91
|
+
@backtracking_stack.pop
|
92
|
+
square = pick_from_backtrack
|
93
|
+
end
|
94
|
+
square
|
95
|
+
end
|
96
|
+
|
97
|
+
def pick_from_direction
|
98
|
+
square = @hitstack.last.send(@direction)
|
99
|
+
if square.nil?
|
100
|
+
change_direction
|
101
|
+
square = @hitstack.first.send(@direction)
|
102
|
+
if square.nil?
|
103
|
+
@direction = nil
|
104
|
+
square = smart_next_square
|
105
|
+
end
|
106
|
+
end
|
107
|
+
square
|
108
|
+
end
|
109
|
+
|
110
|
+
def smart_next_square
|
111
|
+
return nil unless hit_in_progress?
|
112
|
+
square = if backtracking?
|
113
|
+
pick_from_backtrack
|
114
|
+
elsif direction?
|
115
|
+
pick_from_direction
|
116
|
+
else
|
117
|
+
pick_from_random_neighbour(@hitstack.last)
|
118
|
+
end
|
119
|
+
unless square.nil?
|
120
|
+
Grid.delete(square)
|
121
|
+
end
|
122
|
+
square
|
123
|
+
end
|
124
|
+
|
125
|
+
def next_target(force=nil)
|
126
|
+
square = nil
|
127
|
+
unless force.nil?
|
128
|
+
square = Grid[force]
|
129
|
+
Grid.delete(force)
|
130
|
+
else
|
131
|
+
square = smart_next_square || Grid.pick
|
132
|
+
end
|
133
|
+
@placestack << square
|
134
|
+
square.to_s unless square.nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
def target_result(coordinates, was_hit, ship_sunk)
|
138
|
+
if was_hit
|
139
|
+
@hitstack << @placestack.last
|
140
|
+
end
|
141
|
+
if ship_sunk
|
142
|
+
# assuming the ship sunk was the last n squares on the stack
|
143
|
+
SHIPS[ship_sunk].times{@hitstack.pop}
|
144
|
+
@backtracking_stack = @hitstack.reverse
|
145
|
+
if @hitstack.size > 1
|
146
|
+
change_direction
|
147
|
+
else
|
148
|
+
@direction = nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
def place(ship,square=false,direction=false)
|
156
|
+
length = SHIPS[ship]
|
157
|
+
directions = {:x=>"horizontal",:y=>"vertical"}
|
158
|
+
direction ||= directions.keys.random
|
159
|
+
square = if square
|
160
|
+
Grid[square]
|
161
|
+
else
|
162
|
+
Grid.slice(length,direction,@taken_squares).random
|
163
|
+
end
|
164
|
+
@taken_squares << square.to_s
|
165
|
+
trying_squares = []
|
166
|
+
next_square = square
|
167
|
+
(length-1).times do
|
168
|
+
next_square = next_square.send(directions[direction])
|
169
|
+
return place(ship) if @taken_squares.include?(next_square.to_s)
|
170
|
+
trying_squares << next_square.to_s
|
171
|
+
end
|
172
|
+
@taken_squares += trying_squares
|
173
|
+
"#{square.to_s} #{directions[direction]}"
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
class Grid
|
179
|
+
|
180
|
+
@@ROWS = %w{ A B C D E F G H I J }
|
181
|
+
@@squares = {}
|
182
|
+
|
183
|
+
class << self
|
184
|
+
def [](y,x=nil)
|
185
|
+
y = index(y.ceil) unless y.is_a? String
|
186
|
+
x = x.ceil unless x.nil?
|
187
|
+
squares["#{y}#{x}"]
|
188
|
+
end
|
189
|
+
def []=(y,x,s)
|
190
|
+
y = index(y.ceil) unless y.is_a? String
|
191
|
+
squares["#{y}#{x.ceil}"] = s
|
192
|
+
end
|
193
|
+
|
194
|
+
def random
|
195
|
+
s = squares.to_a
|
196
|
+
s.random unless s.nil?
|
197
|
+
end
|
198
|
+
|
199
|
+
def pick
|
200
|
+
s = random
|
201
|
+
unless s.nil?
|
202
|
+
delete(s[0])
|
203
|
+
s[1]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def slice(length,direction,taken_squares=[])
|
208
|
+
n = 9 - length
|
209
|
+
@@squares.values.find_all do |square|
|
210
|
+
(1..n).include?(square.send(direction)) && !taken_squares.include?(square.to_s)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def delete(square)
|
215
|
+
@@squares.delete(square.to_s)
|
216
|
+
end
|
217
|
+
|
218
|
+
def squares
|
219
|
+
@@squares
|
220
|
+
end
|
221
|
+
def index(n)
|
222
|
+
@@ROWS[n-1] unless n < 1
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
class Square
|
230
|
+
|
231
|
+
attr_accessor :y, :x
|
232
|
+
|
233
|
+
def initialize(y,x)
|
234
|
+
@y, @x = y, x
|
235
|
+
end
|
236
|
+
|
237
|
+
def north
|
238
|
+
Grid[y-1,x]
|
239
|
+
end
|
240
|
+
def east
|
241
|
+
Grid[y,x+1]
|
242
|
+
end
|
243
|
+
def south
|
244
|
+
Grid[y+1,x]
|
245
|
+
end
|
246
|
+
def west
|
247
|
+
Grid[y,x-1]
|
248
|
+
end
|
249
|
+
|
250
|
+
# for use in ship placements
|
251
|
+
alias :horizontal :east
|
252
|
+
alias :vertical :south
|
253
|
+
|
254
|
+
def neighbours
|
255
|
+
h = { :north => north, :east => east, :west => west, :south => south }
|
256
|
+
h.delete_if{ |k,v| v.nil? }
|
257
|
+
h.to_a
|
258
|
+
end
|
259
|
+
def to_s
|
260
|
+
"#{Grid.index(y)}#{x}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'commodore_cox/ai'
|
2
|
+
module CommodoreCox
|
3
|
+
|
4
|
+
class CommodoreCox
|
5
|
+
|
6
|
+
def new_game(opponent_name)
|
7
|
+
@AI.setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def carrier_placement
|
11
|
+
@AI.place :carrier
|
12
|
+
end
|
13
|
+
|
14
|
+
def battleship_placement
|
15
|
+
@AI.place :battleship
|
16
|
+
end
|
17
|
+
|
18
|
+
def destroyer_placement
|
19
|
+
@AI.place :destroyer
|
20
|
+
end
|
21
|
+
|
22
|
+
def submarine_placement
|
23
|
+
@AI.place :submarine
|
24
|
+
end
|
25
|
+
|
26
|
+
def patrolship_placement
|
27
|
+
@AI.place :patrolship
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_target
|
31
|
+
@AI.next_target
|
32
|
+
end
|
33
|
+
|
34
|
+
def target_result(coordinates, was_hit, ship_sunk)
|
35
|
+
@AI.target_result(coordinates, was_hit, ship_sunk)
|
36
|
+
end
|
37
|
+
|
38
|
+
def enemy_targeting(coordinates)
|
39
|
+
end
|
40
|
+
|
41
|
+
def game_over(result, disqualification_reason=nil)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Non API methods #####################################
|
45
|
+
|
46
|
+
attr_reader :opponent, :targets, :enemy_targeted_sectors, :result, :disqualification_reason #:nodoc:
|
47
|
+
|
48
|
+
def initialize #:nodoc:
|
49
|
+
@AI = AI.new
|
50
|
+
@AI.setup
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
require 'commodore_cox/ai'
|
3
|
+
|
4
|
+
describe CommodoreCox::AI do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@ai = CommodoreCox::AI.new
|
8
|
+
@ai.setup
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should create random placements for all ships" do
|
12
|
+
@ai.ships.keys.each do |ship|
|
13
|
+
@ai.place(ship).should match /^[A-Z][0-9]{1,2}\s(horizontal|vertical)$/
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should not overlap 2 ships" do
|
18
|
+
@ai.place(:carrier,"A6",:y).should eql("A6 vertical")
|
19
|
+
@ai.place(:destroyer,"D5",:x).should_not eql("D5 horizontal")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should generate a next target 100 times" do
|
23
|
+
100.times{ @ai.next_target.should_not be_nil }
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not generate a 101st target" do
|
27
|
+
100.times{ @ai.next_target.should_not be_nil }
|
28
|
+
@ai.next_target.should be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should choose a neighbour on hit" do
|
32
|
+
@ai.next_target
|
33
|
+
square = @ai.placestack.last
|
34
|
+
neighbours = square.neighbours.map{ |e| e[1].to_s }
|
35
|
+
@ai.target_result(square.to_s,true,nil)
|
36
|
+
neighbours.should include(@ai.next_target)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should choose a neighbour in the correct direction in 2 successive hits" do
|
40
|
+
square = nil
|
41
|
+
2.times do
|
42
|
+
@ai.next_target
|
43
|
+
square = @ai.placestack.last
|
44
|
+
@ai.target_result("",true,nil)
|
45
|
+
end
|
46
|
+
@ai.direction.should_not be_nil
|
47
|
+
next_square = square.send(@ai.direction)
|
48
|
+
@ai.next_target.should eql(next_square.to_s) unless next_square.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should change direction and go back to the first hit on 2 successive hits, then a miss" do
|
52
|
+
@ai.next_target("E5")
|
53
|
+
@ai.target_result("",true,nil)
|
54
|
+
@ai.next_target
|
55
|
+
@ai.target_result("",true,nil)
|
56
|
+
direction = @ai.direction
|
57
|
+
@ai.next_target
|
58
|
+
@ai.target_result("",false,nil)
|
59
|
+
@ai.next_target
|
60
|
+
@ai.direction.should eql(@ai.opposite[direction])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should reset on sink" do
|
64
|
+
@ai.next_target("I2")
|
65
|
+
@ai.target_result("",true,nil)
|
66
|
+
@ai.next_target("J2")
|
67
|
+
@ai.target_result("",true,nil)
|
68
|
+
@ai.direction = :south
|
69
|
+
@ai.next_target
|
70
|
+
@ai.last_placement.should eql("H2")
|
71
|
+
@ai.target_result("",true,nil)
|
72
|
+
@ai.next_target
|
73
|
+
@ai.last_placement.should eql("G2")
|
74
|
+
@ai.target_result("",true,:battleship)
|
75
|
+
@ai.hitstack.size.should eql(0)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should backtrack to one of first hit's neighbours after sink with remaining hit" do
|
79
|
+
@ai.next_target("A2")
|
80
|
+
@ai.target_result("",true,nil)
|
81
|
+
@ai.next_target("A3")
|
82
|
+
@ai.target_result("",true,nil)
|
83
|
+
@ai.direction = :east
|
84
|
+
@ai.next_target("A4")
|
85
|
+
@ai.target_result("",true,nil)
|
86
|
+
@ai.next_target("A5")
|
87
|
+
@ai.target_result("",true,:submarine)
|
88
|
+
@ai.backtracking?.should be_true
|
89
|
+
@ai.next_target
|
90
|
+
["A1","B2"].should include(@ai.last_placement)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should backtrack in reverse direction after sink with remaining hits" do
|
94
|
+
@ai.next_target("A2")
|
95
|
+
@ai.target_result("",true,nil)
|
96
|
+
@ai.next_target("A3")
|
97
|
+
@ai.target_result("",true,nil)
|
98
|
+
@ai.direction = :east
|
99
|
+
@ai.next_target("A4")
|
100
|
+
@ai.target_result("",true,nil)
|
101
|
+
@ai.next_target("A5")
|
102
|
+
@ai.target_result("",true,:patrolship)
|
103
|
+
@ai.backtracking?.should be_true
|
104
|
+
@ai.next_target
|
105
|
+
@ai.last_placement.should eql("A1")
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: commodore_cox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Cox
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-21 00:00:00 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Commodore Cox
|
17
|
+
email: 3coxy4@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Battleship.Rakefile
|
26
|
+
- lib
|
27
|
+
- lib/commodore_cox
|
28
|
+
- lib/commodore_cox/ai.rb
|
29
|
+
- lib/commodore_cox/commodore_cox.rb
|
30
|
+
- pkg
|
31
|
+
- Rakefile
|
32
|
+
- spec
|
33
|
+
- spec/commodore_cox
|
34
|
+
- spec/commodore_cox/ai_spec.rb
|
35
|
+
- spec/commodore_cox/commodore_cox_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
has_rdoc: false
|
38
|
+
homepage: http://sparring.rubyforge.org/
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project: sparring
|
59
|
+
rubygems_version: 1.3.1
|
60
|
+
signing_key:
|
61
|
+
specification_version: 2
|
62
|
+
summary: Battleship Player:Commodore Cox
|
63
|
+
test_files: []
|
64
|
+
|