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.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ $: << File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+
3
+ require 'rubygems'
4
+ require 'spec'
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
+