interferoman 1.2
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/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
|
+
|