interferoman 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Battleship.Rakefile +12 -0
- data/Rakefile +47 -0
- data/lib/interferoman/battleship_board.rb +123 -0
- data/lib/interferoman/interferoman.rb +463 -0
- data/spec/interferoman/battleship_board_spec.rb +208 -0
- data/spec/interferoman/interferoman_spec.rb +379 -0
- data/spec/spec_helper.rb +4 -0
- metadata +63 -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 = "interferoman"
|
13
|
+
PKG_VERSION = "1.2"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = PKG_NAME
|
17
|
+
s.version = PKG_VERSION
|
18
|
+
s.files = FileList['**/*'].to_a.reject!{ |f| f =~ /pkg/ }
|
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:interferoman"
|
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 = "A decent yet still flawed battleship player"
|
34
|
+
s.author = "Alf Mikula"
|
35
|
+
s.email = "amikula@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,123 @@
|
|
1
|
+
module Interferoman
|
2
|
+
class BattleshipBoard
|
3
|
+
@@sizes = {:carrier => 5,
|
4
|
+
:battleship => 4,
|
5
|
+
:destroyer => 3,
|
6
|
+
:submarine => 3,
|
7
|
+
:patrolship => 2}
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@board = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def place(ship, row, column, orientation)
|
14
|
+
for_ship(ship, row, column, orientation) do |r, c|
|
15
|
+
self[r, c] = ship
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def collides?(ship, row, column, orientation)
|
20
|
+
for_ship(ship, row, column, orientation) do |r, c|
|
21
|
+
return true unless self[r, c].nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def size_of(ship)
|
28
|
+
@@sizes[ship]
|
29
|
+
end
|
30
|
+
|
31
|
+
def []=(row, column, value)
|
32
|
+
@board[index_for(row, column)] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](row, column)
|
36
|
+
@board[index_for(row, column)]
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_blank?(row, column, direction=nil)
|
40
|
+
if direction == nil
|
41
|
+
self[row, column] == nil
|
42
|
+
else
|
43
|
+
begin
|
44
|
+
self[*get_position(row, column, direction)] == nil
|
45
|
+
rescue
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_position(row, column, movement)
|
52
|
+
case movement
|
53
|
+
when :north
|
54
|
+
return [row-1, column] if row > 0
|
55
|
+
when :south
|
56
|
+
return [row+1, column] if row < 9
|
57
|
+
when :east
|
58
|
+
return [row, column+1] if column < 9
|
59
|
+
when :west
|
60
|
+
return [row, column-1] if column > 0
|
61
|
+
end
|
62
|
+
|
63
|
+
raise ArgumentError.new("Invalid movement: #{row}, #{column}, #{movement}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def first_blank(row, column)
|
67
|
+
[:east, :south, :west, :north].each do |direction|
|
68
|
+
if is_blank?(row, column, direction)
|
69
|
+
return get_position(row, column, direction) << direction
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_room_for_ship(row, column, size)
|
77
|
+
return false unless self[row, column].nil?
|
78
|
+
|
79
|
+
blanks_to_east = number_of_blanks_in_direction(row, column, :east)
|
80
|
+
blanks_to_west = number_of_blanks_in_direction(row, column, :west)
|
81
|
+
return true if blanks_to_east + blanks_to_west + 1 >= size
|
82
|
+
|
83
|
+
blanks_to_north = number_of_blanks_in_direction(row, column, :north)
|
84
|
+
blanks_to_south = number_of_blanks_in_direction(row, column, :south)
|
85
|
+
return true if blanks_to_north + blanks_to_south + 1 >= size
|
86
|
+
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
def number_of_blanks_in_direction(row, column, direction)
|
91
|
+
begin
|
92
|
+
next_position = get_position(row, column, direction)
|
93
|
+
if self[*next_position].nil?
|
94
|
+
return 1 + number_of_blanks_in_direction(next_position[0],
|
95
|
+
next_position[1], direction)
|
96
|
+
else
|
97
|
+
return 0
|
98
|
+
end
|
99
|
+
rescue
|
100
|
+
return 0
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def index_for(row, column)
|
106
|
+
row*10 + column
|
107
|
+
end
|
108
|
+
|
109
|
+
def for_ship(ship, row, column, orientation)
|
110
|
+
if (orientation == :vertical)
|
111
|
+
last_row = row + size_of(ship) - 1
|
112
|
+
(row..last_row).each do |r|
|
113
|
+
yield(r, column)
|
114
|
+
end
|
115
|
+
else
|
116
|
+
last_column = column + size_of(ship) - 1
|
117
|
+
(column..last_column).each do |c|
|
118
|
+
yield(row, c)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,463 @@
|
|
1
|
+
require 'interferoman/battleship_board'
|
2
|
+
|
3
|
+
module Interferoman
|
4
|
+
|
5
|
+
# Battleship Player
|
6
|
+
#
|
7
|
+
# Battleship is board game between two players. See
|
8
|
+
# http://en.wikipedia.org/wiki/Battleship for more information
|
9
|
+
# and game rules.
|
10
|
+
#
|
11
|
+
# A player represents the conputer AI to play a game of Battleship.
|
12
|
+
# It should know how to place ships and target the opponents
|
13
|
+
# ships.
|
14
|
+
#
|
15
|
+
# This version of Battleship is played on a 10 x 10 grid where
|
16
|
+
# rows are labled by the letters A - J and columns are labled by
|
17
|
+
# the numbers 1 - 10. At the start of the game, each player will
|
18
|
+
# be asked for ship placements. Once the ships are placed, play
|
19
|
+
# proceeeds by each player targeting one square on their opponents
|
20
|
+
# map. A player may only target one square, reguardless of whether
|
21
|
+
# it resulted in a hit or not, before changing turns with her
|
22
|
+
# opponent.
|
23
|
+
#
|
24
|
+
class Interferoman
|
25
|
+
|
26
|
+
# This method is called at the beginning of each game. A player
|
27
|
+
# may only be instantiated once and used to play many games.
|
28
|
+
# So new_game should reset any internal state acquired in
|
29
|
+
# previous games so that it is prepared for a new game.
|
30
|
+
#
|
31
|
+
# The name of the opponent player is passed in. This allows
|
32
|
+
# for the possibility to learn opponent strategy and play the
|
33
|
+
# game differently based on the opponent.
|
34
|
+
#
|
35
|
+
def new_game(opponent_name)
|
36
|
+
reset
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the placement of the carrier. A carrier consumes 5 squares.
|
40
|
+
#
|
41
|
+
# The return value is a string that describes the placements of the ship.
|
42
|
+
# The placement string must be in the following format:
|
43
|
+
#
|
44
|
+
# "#{ROW}#{COL} #{ORIENTATION}"
|
45
|
+
#
|
46
|
+
# eg
|
47
|
+
#
|
48
|
+
# A1 horizontal # the ship will occupy A1, A2, A3, A4, and A5
|
49
|
+
# A1 vertical # the ship will occupy A1, B1, C1, D1, and E1
|
50
|
+
# F5 horizontal # the ship will occupy F5, F6, F7, F8, and F9
|
51
|
+
# F5 vertical # the ship will occupy F5, G5, H5, I5, and J5
|
52
|
+
#
|
53
|
+
# The ship must not fall off the edge of the map. For example,
|
54
|
+
# a carrier placement of 'A8 horizontal' would not leave enough
|
55
|
+
# space in the A row to accomodate the carrier since it requires
|
56
|
+
# 5 squares.
|
57
|
+
#
|
58
|
+
# Ships may not overlap with other ships. For example a carrier
|
59
|
+
# placement of 'A1 horizontal' and a submarine placement of 'A1
|
60
|
+
# vertical' would be invalid because both ships are trying to
|
61
|
+
# occupy the square A1.
|
62
|
+
#
|
63
|
+
# Invalid ship placements will result in disqualification of the player.
|
64
|
+
#
|
65
|
+
def carrier_placement
|
66
|
+
return random_placement(:carrier)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the placement of the battleship. A battleship consumes 4 squares.
|
70
|
+
#
|
71
|
+
# See carrier_placement for details on ship placement
|
72
|
+
#
|
73
|
+
def battleship_placement
|
74
|
+
return random_placement(:battleship)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the placement of the destroyer. A destroyer consumes 3 squares.
|
78
|
+
#
|
79
|
+
# See carrier_placement for details on ship placement
|
80
|
+
#
|
81
|
+
def destroyer_placement
|
82
|
+
return random_placement(:destroyer)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the placement of the submarine. A submarine consumes 3 squares.
|
86
|
+
#
|
87
|
+
# See carrier_placement for details on ship placement
|
88
|
+
#
|
89
|
+
def submarine_placement
|
90
|
+
return random_placement(:submarine)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the placement of the patrolship. A patrolship consumes 2 squares.
|
94
|
+
#
|
95
|
+
# See carrier_placement for details on ship placement
|
96
|
+
#
|
97
|
+
def patrolship_placement
|
98
|
+
return random_placement(:patrolship)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns the coordinates of the players next target. This
|
102
|
+
# method will be called once per turn. The player should return
|
103
|
+
# target coordinates as a string in the form of:
|
104
|
+
#
|
105
|
+
# "#{ROW}#{COL}"
|
106
|
+
#
|
107
|
+
# eg
|
108
|
+
#
|
109
|
+
# A1 # the square in Row A and Column 1
|
110
|
+
# F5 # the square in Row F and Column 5
|
111
|
+
#
|
112
|
+
# Since the map contains only 10 rows and 10 columns, the ROW
|
113
|
+
# should be A, B, C, D, E, F, G H, I, or J. And the COL should
|
114
|
+
# be 1, 2, 3, 4, 5, 6, 7, 8, 9, or 10
|
115
|
+
#
|
116
|
+
# Returning coordinates outside the range or in an invalid
|
117
|
+
# format will result in the players disqualification.
|
118
|
+
#
|
119
|
+
# It is illegal to target a sector more than once. Doing so
|
120
|
+
# will also result in disqualification.
|
121
|
+
#
|
122
|
+
def next_target
|
123
|
+
target_for_current_shot
|
124
|
+
end
|
125
|
+
|
126
|
+
# target_result will be called by the system after a call to
|
127
|
+
# next_target. The parameters supplied inform the player of the
|
128
|
+
# results of the target.
|
129
|
+
#
|
130
|
+
# coordinates : string. The coordinates targeted. It will
|
131
|
+
# be the same value returned by the previous call
|
132
|
+
# to next_target
|
133
|
+
# was_hit : boolean. true if the target was occupied by
|
134
|
+
# a ship. false otherwise.
|
135
|
+
# ship_sunk : symbol. nil if the target did not result in
|
136
|
+
# the sinking of a ship. If the target did result in
|
137
|
+
# the sinking of a ship, the ship type is supplied
|
138
|
+
# (:carrier, :battleship, :destroyer, :submarine,
|
139
|
+
# :patrolship).
|
140
|
+
#
|
141
|
+
# An intelligent player will use the information to better play
|
142
|
+
# the game. For example, if the result indicates a hit, a
|
143
|
+
# player my choose to target neighboring squares to hit and
|
144
|
+
# sink the remainder of the ship.
|
145
|
+
#
|
146
|
+
def target_result(coordinates, was_hit, ship_sunk)
|
147
|
+
row, column = *from_coord(coordinates)
|
148
|
+
|
149
|
+
target_result_internal(row, column, was_hit, ship_sunk)
|
150
|
+
end
|
151
|
+
|
152
|
+
# enemy_targeting is called by the system to inform a player
|
153
|
+
# of their opponent's move. When the opponent targets a square,
|
154
|
+
# this method is called with the coordinates.
|
155
|
+
#
|
156
|
+
# Players may use this information to understand an opponent's
|
157
|
+
# targeting strategy and place ships differently in subsequent
|
158
|
+
# games.
|
159
|
+
#
|
160
|
+
def enemy_targeting(coordinates)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Called by the system at the end of a game to inform the player
|
164
|
+
# of the results.
|
165
|
+
#
|
166
|
+
# result : 1 of 3 possible values (:victory, :defeate, :disqualified)
|
167
|
+
# disqualification_reason : nil unless the game ended as the
|
168
|
+
# result of a disqualification. In the event of a
|
169
|
+
# disqualification, this paramter will hold a string
|
170
|
+
# description of the reason for disqualification. Both
|
171
|
+
# players will be informed of the reason.
|
172
|
+
# :victory # indicates the player won the game
|
173
|
+
# :defeat # indicates the player lost the game
|
174
|
+
# :disqualified # indicates the player was disqualified
|
175
|
+
#
|
176
|
+
def game_over(result, disqualification_reason=nil)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Non API methods #####################################
|
180
|
+
|
181
|
+
attr_reader :opponent, :targets, :enemy_targeted_sectors, :result,
|
182
|
+
:disqualification_reason, :unknown_hits #:nodoc:
|
183
|
+
|
184
|
+
attr_accessor :state
|
185
|
+
|
186
|
+
def initialize #:nodoc:
|
187
|
+
reset
|
188
|
+
end
|
189
|
+
|
190
|
+
private ###############################################
|
191
|
+
|
192
|
+
def target_result_internal(row, column, was_hit, ship_sunk)
|
193
|
+
@opponent_board[row, column] = was_hit
|
194
|
+
|
195
|
+
if (ship_sunk)
|
196
|
+
previous_smallest = @ships[0]
|
197
|
+
|
198
|
+
@ships.delete(ship_sunk)
|
199
|
+
|
200
|
+
if @ships.size > 0
|
201
|
+
if @ships[0] != previous_smallest
|
202
|
+
@target_list = target_list(@opponent_board.size_of(@ships[0]))
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
remove_ship_targets(row, column, @state, ship_sunk)
|
207
|
+
transition_to_unknown_hit
|
208
|
+
return
|
209
|
+
elsif (was_hit)
|
210
|
+
unknown_hits << [row, column]
|
211
|
+
end
|
212
|
+
|
213
|
+
case self.state
|
214
|
+
when :east_2
|
215
|
+
if (was_hit)
|
216
|
+
if @opponent_board.is_blank?(row, column, :east)
|
217
|
+
set_next_target(row, column+1)
|
218
|
+
else
|
219
|
+
transition_to_west(row, column)
|
220
|
+
end
|
221
|
+
else
|
222
|
+
transition_to_west(row, column)
|
223
|
+
end
|
224
|
+
when :south_2
|
225
|
+
if (was_hit)
|
226
|
+
if @opponent_board.is_blank?(row, column, :south)
|
227
|
+
set_next_target(row+1, column)
|
228
|
+
else
|
229
|
+
transition_to_north(row, column)
|
230
|
+
end
|
231
|
+
else
|
232
|
+
transition_to_north(row, column)
|
233
|
+
end
|
234
|
+
when :random
|
235
|
+
if (was_hit)
|
236
|
+
result = @opponent_board.first_blank(row, column)
|
237
|
+
unless result.nil?
|
238
|
+
next_row, next_column, direction = *result
|
239
|
+
|
240
|
+
self.state = direction
|
241
|
+
set_next_target(next_row, next_column)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
when :east
|
245
|
+
if (was_hit)
|
246
|
+
if @opponent_board.is_blank?(row, column, :east)
|
247
|
+
self.state = :east_2
|
248
|
+
set_next_target(row, column+1)
|
249
|
+
else
|
250
|
+
transition_to_west(row, column)
|
251
|
+
end
|
252
|
+
else
|
253
|
+
transition_to_first_blank(row, column-1)
|
254
|
+
end
|
255
|
+
when :west
|
256
|
+
if (was_hit)
|
257
|
+
if (@opponent_board.is_blank?(row, column, :west))
|
258
|
+
set_next_target(row, column-1)
|
259
|
+
else
|
260
|
+
transition_to_first_blank(row, column)
|
261
|
+
end
|
262
|
+
else
|
263
|
+
transition_to_first_blank(row, column+1)
|
264
|
+
end
|
265
|
+
when :south
|
266
|
+
if (was_hit)
|
267
|
+
if (row < 9)
|
268
|
+
self.state = :south_2
|
269
|
+
set_next_target(row+1, column)
|
270
|
+
else
|
271
|
+
transition_to_north(row, column)
|
272
|
+
end
|
273
|
+
else
|
274
|
+
transition_to_first_blank(row-1, column)
|
275
|
+
end
|
276
|
+
when :north
|
277
|
+
if was_hit
|
278
|
+
set_next_target(row-1, column)
|
279
|
+
else
|
280
|
+
transition_to_first_blank(row+1, column)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def set_next_target(row, column)
|
286
|
+
if (0..9).include?(row) && (0..9).include?(column) &&
|
287
|
+
@opponent_board.is_blank?(row, column)
|
288
|
+
target = to_coord(row, column)
|
289
|
+
@target_list.push(target)
|
290
|
+
@specified_target = true
|
291
|
+
else
|
292
|
+
transition_to_unknown_hit
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def transition_to_unknown_hit
|
297
|
+
while self.unknown_hits.size > 0 &&
|
298
|
+
@opponent_board.first_blank(*self.unknown_hits[0]) == nil
|
299
|
+
self.unknown_hits.shift
|
300
|
+
end
|
301
|
+
|
302
|
+
if self.unknown_hits.size > 0
|
303
|
+
transition_to_first_blank(*self.unknown_hits[0])
|
304
|
+
else
|
305
|
+
@state = :random
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def remove_ship_targets(row, column, state, ship_sunk)
|
310
|
+
case state
|
311
|
+
when :east, :east_2
|
312
|
+
(0...@opponent_board.size_of(ship_sunk)).each do |i|
|
313
|
+
self.unknown_hits.delete([row, column-i])
|
314
|
+
end
|
315
|
+
when :south, :south_2
|
316
|
+
(0...@opponent_board.size_of(ship_sunk)).each do |i|
|
317
|
+
self.unknown_hits.delete([row-i, column])
|
318
|
+
end
|
319
|
+
when :west
|
320
|
+
(0...@opponent_board.size_of(ship_sunk)).each do |i|
|
321
|
+
self.unknown_hits.delete([row, column+i])
|
322
|
+
end
|
323
|
+
when :north
|
324
|
+
(0...@opponent_board.size_of(ship_sunk)).each do |i|
|
325
|
+
self.unknown_hits.delete([row+i, column])
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def transition_to_first_blank(row, column)
|
331
|
+
transition_info = @opponent_board.first_blank(row, column)
|
332
|
+
if transition_info.nil?
|
333
|
+
transition_to_unknown_hit
|
334
|
+
else
|
335
|
+
next_row, next_column, direction = transition_info
|
336
|
+
self.state = direction
|
337
|
+
set_next_target(next_row, next_column)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def reset
|
342
|
+
self.state = :random
|
343
|
+
@target_list = target_list(2)
|
344
|
+
@specified_target = false
|
345
|
+
@my_board = BattleshipBoard.new
|
346
|
+
@opponent_board = BattleshipBoard.new
|
347
|
+
@ships = [:patrolship, :destroyer, :submarine, :battleship, :carrier]
|
348
|
+
@unknown_hits = []
|
349
|
+
end
|
350
|
+
|
351
|
+
SEARCH_PATTERNS = [
|
352
|
+
[
|
353
|
+
[]
|
354
|
+
],
|
355
|
+
[
|
356
|
+
[true]
|
357
|
+
],
|
358
|
+
[
|
359
|
+
[false, true],
|
360
|
+
[true, false]
|
361
|
+
],
|
362
|
+
[
|
363
|
+
[true, false, false],
|
364
|
+
[false, true, false],
|
365
|
+
[false, false, true]
|
366
|
+
],
|
367
|
+
[
|
368
|
+
[false, false, false, true],
|
369
|
+
[false, true, false, false],
|
370
|
+
[false, false, true, false],
|
371
|
+
[true, false, false, false]
|
372
|
+
],
|
373
|
+
[
|
374
|
+
[false, false, false, false, true],
|
375
|
+
[false, false, true, false, false],
|
376
|
+
[true, false, false, false, false],
|
377
|
+
[false, false, false, true, false],
|
378
|
+
[false, true, false, false, false]
|
379
|
+
]
|
380
|
+
]
|
381
|
+
|
382
|
+
def target_list(pattern)
|
383
|
+
retval = []
|
384
|
+
|
385
|
+
(0..9).each do |row|
|
386
|
+
(0..9).each do |column|
|
387
|
+
retval << to_coord(row, column) if SEARCH_PATTERNS[pattern][row%pattern][column%pattern]
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
retval.sort_by{|e| rand}
|
392
|
+
end
|
393
|
+
|
394
|
+
ROWS = %w{ A B C D E F G H I J }
|
395
|
+
ORIENTATIONS = [:horizontal, :vertical]
|
396
|
+
def target_for_current_shot
|
397
|
+
while (target=@target_list.pop)
|
398
|
+
row, column = *from_coord(target)
|
399
|
+
break if @opponent_board.is_blank?(row, column) &&
|
400
|
+
(@specified_target ||
|
401
|
+
@opponent_board.
|
402
|
+
has_room_for_ship(row, column, @opponent_board.size_of(@ships[0])))
|
403
|
+
end
|
404
|
+
|
405
|
+
if (target == nil)
|
406
|
+
@target_list = target_list(1)
|
407
|
+
|
408
|
+
while (target=@target_list.pop)
|
409
|
+
break if @opponent_board.is_blank?(*from_coord(target))
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
@specified_target = false
|
414
|
+
|
415
|
+
return target
|
416
|
+
end
|
417
|
+
|
418
|
+
def random_placement(ship)
|
419
|
+
size = @my_board.size_of(ship)
|
420
|
+
while(true)
|
421
|
+
row = rand(10-size+1)
|
422
|
+
column = rand(10-size+1)
|
423
|
+
orientation = ORIENTATIONS[rand(2)]
|
424
|
+
|
425
|
+
unless @my_board.collides?(ship, row, column, orientation)
|
426
|
+
@my_board.place(ship, row, column, orientation)
|
427
|
+
return to_coord(row, column) + " " + orientation.to_s
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def to_coord(row, column)
|
433
|
+
"" << (?A+row) << (column+1).to_s
|
434
|
+
end
|
435
|
+
|
436
|
+
def from_coord(coordinates)
|
437
|
+
row = coordinates[0] - ?A
|
438
|
+
column = coordinates[1..-1].to_i - 1
|
439
|
+
|
440
|
+
[row, column]
|
441
|
+
end
|
442
|
+
|
443
|
+
def transition_to_west(row, column)
|
444
|
+
column -= 1
|
445
|
+
while (@opponent_board[row, column])
|
446
|
+
column -= 1
|
447
|
+
end
|
448
|
+
set_next_target(row, column)
|
449
|
+
self.state = :west
|
450
|
+
end
|
451
|
+
|
452
|
+
def transition_to_north(row, column)
|
453
|
+
row -= 1
|
454
|
+
while (@opponent_board[row, column])
|
455
|
+
row -= 1
|
456
|
+
end
|
457
|
+
set_next_target(row, column)
|
458
|
+
self.state = :north
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
require 'interferoman/battleship_board'
|
3
|
+
|
4
|
+
describe Interferoman::BattleshipBoard do
|
5
|
+
before :each do
|
6
|
+
@board = Interferoman::BattleshipBoard.new
|
7
|
+
end
|
8
|
+
|
9
|
+
describe(:place) do
|
10
|
+
it "should allow placements of ships" do
|
11
|
+
lambda{@board.place(:carrier, 0, 0, :vertical)}.should_not raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "should place the symbol for the ship in the proper squares" do
|
15
|
+
it "(vertically)" do
|
16
|
+
@board.place(:carrier, 0, 0, :vertical)
|
17
|
+
(0..4).each do |row|
|
18
|
+
@board[row,0].should == :carrier
|
19
|
+
@board[row,1].should_not == :carrier
|
20
|
+
end
|
21
|
+
|
22
|
+
@board[5, 0].should_not == :carrier
|
23
|
+
end
|
24
|
+
|
25
|
+
it "(horizontally)" do
|
26
|
+
@board.place(:patrolship, 4, 4, :horizontal)
|
27
|
+
(4..5).each do |column|
|
28
|
+
@board[3,column].should_not == :patrolship
|
29
|
+
@board[4,column].should == :patrolship
|
30
|
+
@board[5,column].should_not == :patrolship
|
31
|
+
end
|
32
|
+
|
33
|
+
@board[4, 3].should_not == :patrolship
|
34
|
+
@board[4, 6].should_not == :patrolship
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe :size_of do
|
40
|
+
it "should know the sizes of the ships" do
|
41
|
+
@board.size_of(:carrier).should == 5
|
42
|
+
@board.size_of(:battleship).should == 4
|
43
|
+
@board.size_of(:destroyer).should == 3
|
44
|
+
@board.size_of(:submarine).should == 3
|
45
|
+
@board.size_of(:patrolship).should == 2
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe(:collides?) do
|
50
|
+
it "should return true if pieces collide" do
|
51
|
+
@board.place(:carrier, 1, 1, :vertical)
|
52
|
+
@board.collides?(:battleship, 2, 0, :horizontal).should be_true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return false if pieces do not collide" do
|
56
|
+
@board.place(:carrier, 1, 1, :vertical)
|
57
|
+
@board.collides?(:battleship, 2, 0, :vertical).should be_false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe :[] do
|
62
|
+
it "should store and return the value set" do
|
63
|
+
@board[0,0] = :foo
|
64
|
+
@board[1,3] = :bar
|
65
|
+
|
66
|
+
@board[0,0].should == :foo
|
67
|
+
@board[1,3].should == :bar
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe :is_blank? do
|
72
|
+
it "should return true if the location at the coordinates has a nil value" do
|
73
|
+
@board.is_blank?(5, 5).should be_true
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should return false if the location at the coordinates is not nil" do
|
77
|
+
@board[5, 5] = true
|
78
|
+
|
79
|
+
@board.is_blank?(5, 5).should be_false
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "with direction argument" do
|
83
|
+
before :each do
|
84
|
+
@board[5, 5] = true
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should return false if current position is 5, 5 and direction is north, south, east, or west" do
|
88
|
+
@board.is_blank?(5, 5, :north).should be_true
|
89
|
+
@board.is_blank?(5, 5, :south).should be_true
|
90
|
+
@board.is_blank?(5, 5, :east).should be_true
|
91
|
+
@board.is_blank?(5, 5, :west).should be_true
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should return true if current position is one of from 5, 5 and direction takes us to 5, 5" do
|
95
|
+
@board.is_blank?(6, 5, :north).should be_false
|
96
|
+
@board.is_blank?(4, 5, :south).should be_false
|
97
|
+
@board.is_blank?(5, 4, :east).should be_false
|
98
|
+
@board.is_blank?(5, 6, :west).should be_false
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should return false if current position is at edge and direction takes us off the edge" do
|
102
|
+
@board.is_blank?(0, 5, :north).should be_false
|
103
|
+
@board.is_blank?(9, 5, :south).should be_false
|
104
|
+
@board.is_blank?(5, 9, :east).should be_false
|
105
|
+
@board.is_blank?(5, 0, :west).should be_false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe :get_position do
|
111
|
+
it "should know how to move the cursor north, south, east, or west" do
|
112
|
+
@board.get_position(7, 3, :north).should == [6, 3]
|
113
|
+
@board.get_position(3, 8, :south).should == [4, 8]
|
114
|
+
@board.get_position(3, 2, :east).should == [3, 3]
|
115
|
+
@board.get_position(6, 2, :west).should == [6, 1]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should throw an exception when at the edge of the board and asked to move off" do
|
119
|
+
lambda{@board.get_position(0, 3, :north)}.should raise_error(ArgumentError)
|
120
|
+
lambda{@board.get_position(9, 8, :south)}.should raise_error(ArgumentError)
|
121
|
+
lambda{@board.get_position(3, 9, :east)}.should raise_error(ArgumentError)
|
122
|
+
lambda{@board.get_position(6, 0, :west)}.should raise_error(ArgumentError)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe :first_blank do
|
127
|
+
it "should return the first blank found by looking clockwise starting with :east" do
|
128
|
+
@board[5, 5] = true
|
129
|
+
@board.first_blank(5, 5).should == [5, 6, :east]
|
130
|
+
@board.first_blank(5, 4).should == [6, 4, :south]
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should return nil if no blank is found" do
|
134
|
+
@board[5, 5] = true
|
135
|
+
@board[5, 7] = true
|
136
|
+
@board[4, 6] = true
|
137
|
+
@board[6, 6] = true
|
138
|
+
|
139
|
+
@board.first_blank(5, 6).should == nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe :has_room_for_ship do
|
144
|
+
it "should always return false if the space specified is not nil" do
|
145
|
+
@board[5, 5] = false
|
146
|
+
|
147
|
+
@board.has_room_for_ship(5, 5, 2).should == false
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should see the correct horizontal space when there are no limits" do
|
151
|
+
@board[4, 5] = false
|
152
|
+
@board[6, 5] = false
|
153
|
+
|
154
|
+
@board.has_room_for_ship(5, 5, 5).should == true
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should see the correct horizontal space when there is a limit" do
|
158
|
+
@board[5, 2] = false
|
159
|
+
@board[5, 7] = false
|
160
|
+
@board[4, 5] = false
|
161
|
+
@board[6, 5] = false
|
162
|
+
|
163
|
+
@board.has_room_for_ship(5, 5, 5).should == false
|
164
|
+
@board.has_room_for_ship(5, 5, 4).should == true
|
165
|
+
@board.has_room_for_ship(5, 5, 3).should == true
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should see the correct horizontal space when it's against the edge" do
|
169
|
+
@board[5, 3] = false
|
170
|
+
@board[4, 0] = false
|
171
|
+
@board[6, 0] = false
|
172
|
+
|
173
|
+
@board.has_room_for_ship(5, 0, 5).should == false
|
174
|
+
@board.has_room_for_ship(5, 0, 4).should == false
|
175
|
+
@board.has_room_for_ship(5, 0, 3).should == true
|
176
|
+
@board.has_room_for_ship(5, 0, 2).should == true
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should see the correct vertical space when there are no limits" do
|
180
|
+
@board[5, 4] = false
|
181
|
+
@board[5, 6] = false
|
182
|
+
|
183
|
+
@board.has_room_for_ship(5, 5, 5).should == true
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should see the correct horizontal space when there is a limit" do
|
187
|
+
@board[2, 5] = false
|
188
|
+
@board[7, 5] = false
|
189
|
+
@board[5, 4] = false
|
190
|
+
@board[5, 6] = false
|
191
|
+
|
192
|
+
@board.has_room_for_ship(5, 5, 5).should == false
|
193
|
+
@board.has_room_for_ship(5, 5, 4).should == true
|
194
|
+
@board.has_room_for_ship(5, 5, 3).should == true
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should see the correct vertical space when it's against the edge" do
|
198
|
+
@board[3, 5] = false
|
199
|
+
@board[0, 4] = false
|
200
|
+
@board[0, 6] = false
|
201
|
+
|
202
|
+
@board.has_room_for_ship(0, 5, 5).should == false
|
203
|
+
@board.has_room_for_ship(0, 5, 4).should == false
|
204
|
+
@board.has_room_for_ship(0, 5, 3).should == true
|
205
|
+
@board.has_room_for_ship(0, 5, 2).should == true
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,379 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
require 'interferoman/interferoman'
|
3
|
+
require 'interferoman/battleship_board'
|
4
|
+
|
5
|
+
describe Interferoman::Interferoman do
|
6
|
+
before :each do
|
7
|
+
@player = Interferoman::Interferoman.new
|
8
|
+
@player.new_game("test")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should be instantiable with no parameters" do
|
12
|
+
|
13
|
+
lambda { Interferoman::Interferoman.new }.should_not raise_error
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe :next_target do
|
18
|
+
it "should only return (A-J)(1-10)" do
|
19
|
+
100.times do
|
20
|
+
target = @player.next_target
|
21
|
+
violated("Invalid coordinates #{target}") unless
|
22
|
+
target =~ /^[A-J]([1-9]|10)$/
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should never repeat a target" do
|
27
|
+
board = Interferoman::BattleshipBoard.new
|
28
|
+
100.times do |i|
|
29
|
+
target = @player.next_target
|
30
|
+
row = target[0] - ?A
|
31
|
+
column = target[1..-1].to_i
|
32
|
+
if board[row, column]
|
33
|
+
violated("#{target} targeted a second time on round #{i}")
|
34
|
+
end
|
35
|
+
|
36
|
+
@player.target_result(target, false, nil)
|
37
|
+
|
38
|
+
board[row, column] = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should never repeat a target, even with garbage feedback" do
|
43
|
+
board = Interferoman::BattleshipBoard.new
|
44
|
+
100.times do |i|
|
45
|
+
target = @player.next_target
|
46
|
+
row = target[0] - ?A
|
47
|
+
column = target[1..-1].to_i
|
48
|
+
if board[row, column]
|
49
|
+
violated("#{target} targeted a second time on round #{i}")
|
50
|
+
end
|
51
|
+
|
52
|
+
hit = rand(2) == 0
|
53
|
+
if (hit && rand(10) == 0)
|
54
|
+
ship = [:carrier, :battleship, :submarine, :destroyer,
|
55
|
+
:patrolship][rand(5)]
|
56
|
+
else
|
57
|
+
ship = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
@player.target_result(target, hit, ship)
|
61
|
+
|
62
|
+
board[row, column] = true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe :target_result do
|
68
|
+
it "should search for the ship when it gets a hit" do
|
69
|
+
target = @player.next_target
|
70
|
+
hit_row, hit_column = to_coordinates(target)
|
71
|
+
@player.target_result(target, true, nil)
|
72
|
+
next_target = @player.next_target
|
73
|
+
|
74
|
+
next_row, next_column = to_coordinates(next_target)
|
75
|
+
next_row.should be_close(hit_row, 1.001)
|
76
|
+
next_column.should be_close(hit_column, 1.001)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should start out in the 'random' state" do
|
80
|
+
@player.state.should == :random
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should stay in the 'random' state when it gets a miss" do
|
84
|
+
@player.state = :random
|
85
|
+
@player.target_result('C3', false, nil)
|
86
|
+
@player.state.should == :random
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should transition to the 'east' state when state is 'random' and it gets a hit" do
|
90
|
+
@player.state = :random
|
91
|
+
@player.target_result('A1', true, nil)
|
92
|
+
@player.state.should == :east
|
93
|
+
@player.next_target.should == 'A2'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should transition to 'east_2' state when it's in east and gets a hit" do
|
97
|
+
@player.state = :east
|
98
|
+
@player.target_result('A5', true, nil)
|
99
|
+
@player.state.should == :east_2
|
100
|
+
@player.next_target.should == 'A6'
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should transition to the 'south' state when state is 'random' and it gets a hit on the right edge" do
|
104
|
+
@player.state = :random
|
105
|
+
@player.target_result('A10', true, nil)
|
106
|
+
@player.state.should == :south
|
107
|
+
@player.next_target.should == 'B10'
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should transition to the 'south' state when state is 'random' and the next square to the right is a known state" do
|
111
|
+
@player.state = :random
|
112
|
+
|
113
|
+
@player.target_result('A10', false, nil)
|
114
|
+
|
115
|
+
@player.target_result('A9', true, nil)
|
116
|
+
@player.state.should == :south
|
117
|
+
@player.next_target.should == 'B9'
|
118
|
+
end
|
119
|
+
|
120
|
+
[:east, :west,
|
121
|
+
:south, :north].each do |state|
|
122
|
+
it "should transition to 'random' when state is '#{state}' and a ship is sunk" do
|
123
|
+
@player.state = state
|
124
|
+
@player.target_result('E5', true, :battleship)
|
125
|
+
@player.state.should == :random
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should transition to 'west' when state is 'east' and it misses" do
|
130
|
+
@player.state = :east
|
131
|
+
@player.target_result('A9', true, nil)
|
132
|
+
|
133
|
+
@player.state.should == :east_2
|
134
|
+
@player.next_target.should == 'A10'
|
135
|
+
@player.target_result('A10', false, nil)
|
136
|
+
|
137
|
+
@player.state.should == :west
|
138
|
+
@player.next_target.should == 'A8'
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should transition to 'west' when it's in north, gets a miss, and all other directions are known" do
|
142
|
+
@player.target_result('C7', true, nil)
|
143
|
+
@player.target_result('C8', false, nil)
|
144
|
+
@player.target_result('D7', false, nil)
|
145
|
+
|
146
|
+
@player.state = :north
|
147
|
+
@player.target_result('B7', false, nil)
|
148
|
+
|
149
|
+
@player.state.should == :west
|
150
|
+
@player.next_target.should == 'C6'
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should transition to 'south' when state is 'east' and it misses" do
|
154
|
+
@player.target_result('B5', true, nil)
|
155
|
+
|
156
|
+
@player.state = :east
|
157
|
+
@player.target_result('B6', false, nil)
|
158
|
+
|
159
|
+
@player.state.should == :south
|
160
|
+
@player.next_target.should == 'C5'
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should transition to 'south' when state is 'random' and it hits, but squares to the right and left are known states" do
|
164
|
+
@player.target_result('C4', false, nil)
|
165
|
+
@player.target_result('C6', false, nil)
|
166
|
+
@player.target_result('C5', true, nil)
|
167
|
+
|
168
|
+
@player.state.should == :south
|
169
|
+
@player.next_target.should == 'D5'
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should transition to 'west' when state is 'east' and it reaches the edge" do
|
173
|
+
@player.state = :east
|
174
|
+
@player.target_result('A9', true, nil)
|
175
|
+
|
176
|
+
@player.state.should == :east_2
|
177
|
+
@player.next_target.should == 'A10'
|
178
|
+
@player.target_result('A10', true, nil)
|
179
|
+
|
180
|
+
@player.state.should == :west
|
181
|
+
@player.next_target.should == 'A8'
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should transition to 'west' when state is 'east' and it reaches a known state (hit or miss)" do
|
185
|
+
@player.target_result('A5', false, nil)
|
186
|
+
|
187
|
+
@player.state = :east
|
188
|
+
@player.target_result('A4', true, nil)
|
189
|
+
|
190
|
+
@player.state.should == :west
|
191
|
+
@player.next_target.should == 'A3'
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should stay in 'west' when it hits" do
|
195
|
+
@player.state = :west
|
196
|
+
@player.target_result('B6', true, nil)
|
197
|
+
|
198
|
+
@player.state.should == :west
|
199
|
+
@player.next_target.should == 'B5'
|
200
|
+
end
|
201
|
+
|
202
|
+
it "should transition to 'south' when state is 'west' and it misses" do
|
203
|
+
@player.target_result('B6', true, nil)
|
204
|
+
@player.state = :west
|
205
|
+
|
206
|
+
@player.target_result('B5', true, nil)
|
207
|
+
@player.state.should == :west
|
208
|
+
@player.next_target.should == 'B4'
|
209
|
+
|
210
|
+
@player.target_result('B4', false, nil)
|
211
|
+
@player.state.should == :south
|
212
|
+
@player.next_target.should == 'C5'
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should transition to 'south' when state is 'west' and it reaches the edge" do
|
216
|
+
@player.target_result('B2', true, nil)
|
217
|
+
@player.state = :west
|
218
|
+
|
219
|
+
@player.target_result('B1', true, nil)
|
220
|
+
@player.state.should == :south
|
221
|
+
@player.next_target.should == 'C1'
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should transition to 'south_2' when it gets a hit" do
|
225
|
+
@player.state = :south
|
226
|
+
|
227
|
+
@player.target_result('B3', true, nil)
|
228
|
+
@player.state.should == :south_2
|
229
|
+
@player.next_target.should == 'C3'
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should transition to 'north' when it's in south and gets a miss" do
|
233
|
+
@player.state = :south
|
234
|
+
|
235
|
+
@player.target_result('B4', true, nil)
|
236
|
+
@player.state.should == :south_2
|
237
|
+
@player.next_target.should == 'C4'
|
238
|
+
|
239
|
+
@player.target_result('C4', false, nil)
|
240
|
+
@player.state.should == :north
|
241
|
+
@player.next_target.should == 'A4'
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should transition to 'north' when it's in random, and it gets a hit but east, south, and west are known" do
|
245
|
+
@player.target_result('B4', false, nil)
|
246
|
+
@player.target_result('C3', false, nil)
|
247
|
+
@player.target_result('B2', false, nil)
|
248
|
+
|
249
|
+
@player.target_result('B3', true, nil)
|
250
|
+
@player.state.should == :north
|
251
|
+
@player.next_target.should == 'A3'
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should transition to 'north' when it reaches the edge" do
|
255
|
+
@player.state = :south
|
256
|
+
|
257
|
+
@player.target_result('J9', true, nil)
|
258
|
+
@player.state.should == :north
|
259
|
+
@player.next_target.should == 'I9'
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should stay in 'north' when it gets a hit" do
|
263
|
+
@player.state = :north
|
264
|
+
|
265
|
+
@player.target_result('J8', true, nil)
|
266
|
+
@player.state.should == :north
|
267
|
+
@player.next_target.should == 'I8'
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should seek out the unknown ship when it sinks a ship and another ship has also been hit" do
|
271
|
+
@player.target_result('J7', true, nil)
|
272
|
+
@player.state = :north
|
273
|
+
@player.target_result('I7', true, nil)
|
274
|
+
@player.target_result('H7', true, :patrolship)
|
275
|
+
|
276
|
+
@player.state.should == :east
|
277
|
+
@player.next_target.should == 'J8'
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "ship placements" do
|
282
|
+
it "should not collide" do
|
283
|
+
board = Interferoman::BattleshipBoard.new
|
284
|
+
|
285
|
+
[:carrier,:battleship,:destroyer,:submarine,:patrolship].each do |ship|
|
286
|
+
placement = [ship,
|
287
|
+
*coordinates_for(@player.send(ship.to_s + "_placement"))]
|
288
|
+
board.collides?(*placement).should_not be_true
|
289
|
+
board.place(*placement)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def coordinates_for(placement)
|
294
|
+
coords, orientation = placement.split(' ')
|
295
|
+
row = coords[0] - ?A
|
296
|
+
column = coords[1..-1].to_i
|
297
|
+
orientation = orientation.to_sym
|
298
|
+
|
299
|
+
[row, column, orientation]
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
describe :unknown_hits do
|
304
|
+
it "should start out as an empty array" do
|
305
|
+
@player.unknown_hits.should == []
|
306
|
+
end
|
307
|
+
|
308
|
+
it "should keep track of hits for which we don't know the target" do
|
309
|
+
@player.target_result('D4', true, nil)
|
310
|
+
@player.unknown_hits.should == [[3,3]]
|
311
|
+
|
312
|
+
@player.target_result('B5', true, nil)
|
313
|
+
@player.unknown_hits.should == [[3,3], [1, 4]]
|
314
|
+
end
|
315
|
+
|
316
|
+
it "should remove hits when a ship is sunk" do
|
317
|
+
@player.target_result('D4', true, nil)
|
318
|
+
@player.target_result('D5', true, nil)
|
319
|
+
@player.target_result('D6', true, nil)
|
320
|
+
|
321
|
+
@player.unknown_hits.should == [[3,3], [3,4], [3,5]]
|
322
|
+
@player.state = :east
|
323
|
+
@player.target_result('D7', true, :submarine)
|
324
|
+
|
325
|
+
@player.unknown_hits.should == [[3,3]]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
describe :remove_ship_targets do
|
330
|
+
it "should remove targets to the west if the state is :east" do
|
331
|
+
@player.unknown_hits << [1,2] << [1,3]
|
332
|
+
@player.send(:remove_ship_targets, 1, 4, :east, :submarine)
|
333
|
+
|
334
|
+
@player.unknown_hits.should == []
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should remove targets to the west if the state is :east_2" do
|
338
|
+
@player.unknown_hits << [3,4] << [3,5]
|
339
|
+
@player.send(:remove_ship_targets, 3, 6, :east_2, :patrolship)
|
340
|
+
|
341
|
+
@player.unknown_hits.should == [[3,4]]
|
342
|
+
end
|
343
|
+
|
344
|
+
it "should remove targets to the north if the state is :south" do
|
345
|
+
@player.unknown_hits << [3,5] << [4,5] << [5,5]
|
346
|
+
@player.send(:remove_ship_targets, 6, 5, :south, :battleship)
|
347
|
+
|
348
|
+
@player.unknown_hits.should == []
|
349
|
+
end
|
350
|
+
|
351
|
+
it "should remove targets to the north if the state is :south_2" do
|
352
|
+
@player.unknown_hits << [3,8] << [4,8] << [5,8] << [6,8] << [7,8]
|
353
|
+
@player.send(:remove_ship_targets, 8, 8, :south_2, :carrier)
|
354
|
+
|
355
|
+
@player.unknown_hits.should == [[3,8]]
|
356
|
+
end
|
357
|
+
|
358
|
+
it "should remove targets to the east if the state is :west" do
|
359
|
+
@player.unknown_hits << [3,5] << [3,6] << [3,7]
|
360
|
+
@player.send(:remove_ship_targets, 3, 4, :west, :destroyer)
|
361
|
+
|
362
|
+
@player.unknown_hits.should == [[3,7]]
|
363
|
+
end
|
364
|
+
|
365
|
+
it "should remove targets to the south if the state is :north" do
|
366
|
+
@player.unknown_hits << [3,5] << [4,5] << [5,5]
|
367
|
+
@player.send(:remove_ship_targets, 2, 5, :north, :battleship)
|
368
|
+
|
369
|
+
@player.unknown_hits.should == []
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def to_coordinates(target)
|
374
|
+
row = target[0] - ?A
|
375
|
+
column = target[1..-1].to_i
|
376
|
+
|
377
|
+
[row, column]
|
378
|
+
end
|
379
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interferoman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.2"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alf Mikula
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-28 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A decent yet still flawed battleship player
|
17
|
+
email: amikula@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Battleship.Rakefile
|
26
|
+
- lib
|
27
|
+
- lib/interferoman
|
28
|
+
- lib/interferoman/battleship_board.rb
|
29
|
+
- lib/interferoman/interferoman.rb
|
30
|
+
- Rakefile
|
31
|
+
- spec
|
32
|
+
- spec/interferoman
|
33
|
+
- spec/interferoman/battleship_board_spec.rb
|
34
|
+
- spec/interferoman/interferoman_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
has_rdoc: false
|
37
|
+
homepage: http://sparring.rubyforge.org/
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project: sparring
|
58
|
+
rubygems_version: 1.2.0
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: Battleship Player:interferoman
|
62
|
+
test_files: []
|
63
|
+
|