interferoman 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+