ssoroka_takes_the_win 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Battleship.Rakefile +12 -0
- data/Rakefile +47 -0
- data/lib/ssoroka_takes_the_win/ssoroka_takes_the_win.rb +306 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/ssoroka_takes_the_win/ssoroka_takes_the_win_spec.rb +56 -0
- metadata +61 -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 = "ssoroka_takes_the_win"
|
13
|
+
PKG_VERSION = "1.0"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = PKG_NAME
|
17
|
+
s.version = PKG_VERSION
|
18
|
+
s.files = FileList['**/*'].reject{|f| f =~ /^pkg/}.to_a
|
19
|
+
s.require_path = 'lib'
|
20
|
+
s.test_files = Dir.glob('spec/*_spec.rb')
|
21
|
+
s.bindir = 'bin'
|
22
|
+
s.executables = []
|
23
|
+
s.summary = "Battleship Player:ssoroka takes the win"
|
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 battleship player"
|
34
|
+
s.author = "Steven Soroka"
|
35
|
+
s.email = "ssoroka78@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,306 @@
|
|
1
|
+
module SsorokaTakesTheWin
|
2
|
+
|
3
|
+
# Battleship Player
|
4
|
+
#
|
5
|
+
# Battleship is board game between two players. See http://en.wikipedia.org/wiki/Battleship for more information and
|
6
|
+
# game rules.
|
7
|
+
#
|
8
|
+
# A player represents the conputer AI to play a game of Battleship. It should know how to place ships and target
|
9
|
+
# the opponents ships.
|
10
|
+
#
|
11
|
+
# This version of Battleship is played on a 10 x 10 grid where rows are labled by the letters A - J and
|
12
|
+
# columns are labled by the numbers 1 - 10. At the start of the game, each player will be asked for ship placements.
|
13
|
+
# Once the ships are placed, play proceeeds by each player targeting one square on their opponents map. A player
|
14
|
+
# may only target one square, reguardless of whether it resulted in a hit or not, before changing turns with her opponent.
|
15
|
+
#
|
16
|
+
class SsorokaTakesTheWin
|
17
|
+
|
18
|
+
# This method is called at the beginning of each game. A player may only be instantiated once and used to play many games.
|
19
|
+
# So new_game should reset any internal state acquired in previous games so that it is prepared for a new game.
|
20
|
+
#
|
21
|
+
# The name of the opponent player is passed in. This allows for the possibility to learn opponent strategy and
|
22
|
+
# play the game differently based on the opponent.
|
23
|
+
#
|
24
|
+
def new_game(opponent_name)
|
25
|
+
reset
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the placement of the carrier. A carrier consumes 5 squares.
|
29
|
+
#
|
30
|
+
# The return value is a string that describes the placements of the ship.
|
31
|
+
# The placement string must be in the following format:
|
32
|
+
#
|
33
|
+
# "#{ROW}#{COL} #{ORIENTATION}"
|
34
|
+
#
|
35
|
+
# eg
|
36
|
+
#
|
37
|
+
# A1 horizontal # the ship will occupy A1, A2, A3, A4, and A5
|
38
|
+
# A1 vertical # the ship will occupy A1, B1, C1, D1, and E1
|
39
|
+
# F5 horizontal # the ship will occupy F5, F6, F7, F8, and F9
|
40
|
+
# F5 vertical # the ship will occupy F5, G5, H5, I5, and J5
|
41
|
+
#
|
42
|
+
# The ship must not fall off the edge of the map. For example, a carrier placement of 'A8 horizontal' would
|
43
|
+
# not leave enough space in the A row to accomidate the carrier since it requires 5 squares.
|
44
|
+
#
|
45
|
+
# Ships may not overlap with other ships. For example a carrier placement of 'A1 horizontal' and a submarine
|
46
|
+
# placement of 'A1 vertical' would be invalid because bothe ships are trying to occupy the square A1.
|
47
|
+
#
|
48
|
+
# Invalid ship placements will result in disqualification of the player.
|
49
|
+
#
|
50
|
+
def carrier_placement
|
51
|
+
return "G1 horizontal"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the placement of the battleship. A battleship consumes 4 squares.
|
55
|
+
#
|
56
|
+
# See carrier_placement for details on ship placement
|
57
|
+
#
|
58
|
+
def battleship_placement
|
59
|
+
return "F8 vertical"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the placement of the destroyer. A destroyer consumes 3 squares.
|
63
|
+
#
|
64
|
+
# See carrier_placement for details on ship placement
|
65
|
+
#
|
66
|
+
def destroyer_placement
|
67
|
+
return "C5 vertical"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the placement of the submarine. A submarine consumes 3 squares.
|
71
|
+
#
|
72
|
+
# See carrier_placement for details on ship placement
|
73
|
+
#
|
74
|
+
def submarine_placement
|
75
|
+
return "B7 horizontal"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the placement of the patrolship. A patrolship consumes 2 squares.
|
79
|
+
#
|
80
|
+
# See carrier_placement for details on ship placement
|
81
|
+
#
|
82
|
+
def patrolship_placement
|
83
|
+
return "I3 horizontal"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the coordinates of the players next target. This method will be called once per turn. The player
|
87
|
+
# should return target coordinates as a string in the form of:
|
88
|
+
#
|
89
|
+
# "#{ROW}#{COL}"
|
90
|
+
#
|
91
|
+
# eg
|
92
|
+
#
|
93
|
+
# A1 # the square in Row A and Column 1
|
94
|
+
# F5 # the square in Row F and Column 5
|
95
|
+
#
|
96
|
+
# Since the map contains only 10 rows and 10 columns, the ROW should be A, B, C, D, E, F, G H, I, or J. And the
|
97
|
+
# COL should be 1, 2, 3, 4, 5, 6, 7, 8, 9, or 10
|
98
|
+
#
|
99
|
+
# Returning coordinates outside the range or in an invalid format will result in the players disqualification.
|
100
|
+
#
|
101
|
+
# It is illegal to target a sector more than once. Doing so will also result in disqualification.
|
102
|
+
#
|
103
|
+
def next_target
|
104
|
+
target = target_for_current_shot
|
105
|
+
@shots_taken += 1
|
106
|
+
return target
|
107
|
+
end
|
108
|
+
|
109
|
+
# target_result will be called by the system after a call to next_target. The paramters supplied inform the player
|
110
|
+
# of the results of the target.
|
111
|
+
#
|
112
|
+
# coordinates : string. The coordinates targeted. It will be the same value returned by the previous call to next_target
|
113
|
+
# was_hit : boolean. true if the target was occupied by a ship. false otherwise.
|
114
|
+
# ship_sunk : symbol. nil if the target did not result in the sinking of a ship. If the target did result in
|
115
|
+
# in the sinking of a ship, the ship type is supplied (:carrier, :battleship, :destroyer, :submarine, :patrolship).
|
116
|
+
#
|
117
|
+
# An intelligent player will use the information to better play the game. For example, if the result indicates a
|
118
|
+
# hit, a player my choose to target neighboring squares to hit and sink the remainder of the ship.
|
119
|
+
#
|
120
|
+
def target_result(coordinates, was_hit, ship_sunk)
|
121
|
+
@grid.set(@last_r, @last_c, ship_sunk || (was_hit ? :hit : :miss))
|
122
|
+
if ship_sunk
|
123
|
+
@ships_left -= [ship_sunk]
|
124
|
+
# try to mark all the hits with ship type
|
125
|
+
DIRECTIONS.each{|y, x|
|
126
|
+
all_hits = true
|
127
|
+
(1..(SHIP_TYPES[ship_sunk]-1)).each{|p|
|
128
|
+
r = @last_r + y * p
|
129
|
+
c = @last_c + x * p
|
130
|
+
all_hits = false unless @grid.valid_target?(r, c) && @grid.get(r, c) == :hit
|
131
|
+
}
|
132
|
+
if all_hits
|
133
|
+
(1..(SHIP_TYPES[ship_sunk]-1)).each{|p|
|
134
|
+
r = @last_r + y * p
|
135
|
+
c = @last_c + x * p
|
136
|
+
@grid.set(r, c, ship_sunk)
|
137
|
+
}
|
138
|
+
break
|
139
|
+
end
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# enemy_targeting is called by the system to inform a player of their apponents move. When the opponent targets
|
145
|
+
# a square, this method is called with the coordinates.
|
146
|
+
#
|
147
|
+
# Players may use this information to understand an opponents targeting strategy and place ships differently
|
148
|
+
# in subsequent games.
|
149
|
+
#
|
150
|
+
def enemy_targeting(coordinates)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Called by the system at the end of a game to inform the player of the results.
|
154
|
+
#
|
155
|
+
# result : 1 of 3 possible values (:victory, :defeate, :disqualified)
|
156
|
+
# disqualification_reason : nil unless the game ended as the result of a disqualification. In the event of a
|
157
|
+
# disqualification, this paramter will hold a string description of the reason for disqualification. Both
|
158
|
+
# players will be informed of the reason.
|
159
|
+
#
|
160
|
+
# :victory # indicates the player won the game
|
161
|
+
# :defeat # indicates the player lost the game
|
162
|
+
# :disqualified # indicates the player was disqualified
|
163
|
+
#
|
164
|
+
def game_over(result, disqualification_reason=nil)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Non API methods #####################################
|
168
|
+
|
169
|
+
attr_reader :opponent, :targets, :enemy_targeted_sectors, :result, :disqualification_reason #:nodoc:
|
170
|
+
|
171
|
+
def initialize #:nodoc:
|
172
|
+
reset
|
173
|
+
end
|
174
|
+
|
175
|
+
private ###############################################
|
176
|
+
|
177
|
+
SHIP_TYPES = {:carrier => 5, :battleship => 4, :destroyer => 3,
|
178
|
+
:submarine => 3, :patrolship => 2}
|
179
|
+
DIRECTIONS = [[-1, 0], [0, 1], [1, 0], [0, -1]]
|
180
|
+
|
181
|
+
def reset
|
182
|
+
@shots_taken = 0
|
183
|
+
@grid = Grid.new
|
184
|
+
@ships_left = SHIP_TYPES.keys
|
185
|
+
@ships_dead = []
|
186
|
+
end
|
187
|
+
|
188
|
+
ROWS = %w{ A B C D E F G H I J }
|
189
|
+
def target_for_current_shot
|
190
|
+
# r, c = destroy_hit_ships || find_ships || random_shot
|
191
|
+
score_grid
|
192
|
+
@last_r, @last_c = pick_best
|
193
|
+
coord_to_target(@last_r, @last_c)
|
194
|
+
end
|
195
|
+
|
196
|
+
def coord_to_target(r, c)
|
197
|
+
"#{ROWS[r]}#{c + 1}"
|
198
|
+
end
|
199
|
+
|
200
|
+
def score_grid
|
201
|
+
n = smallest_ship_size
|
202
|
+
@grid_score = Grid.new
|
203
|
+
@grid_score.each_cell{|row, col|
|
204
|
+
@grid_score.set(row, col, 0)
|
205
|
+
# - 1000 if already shot at
|
206
|
+
if @grid.get(row, col)
|
207
|
+
@grid_score.dec(row, col, 1000)
|
208
|
+
else
|
209
|
+
# + 100 if there's a damaged ship nearby
|
210
|
+
DIRECTIONS.each{|dir_y, dir_x|
|
211
|
+
r = row + dir_y
|
212
|
+
c = col + dir_x
|
213
|
+
if @grid.valid_target?(r, c) && @grid.get(r, c) == :hit
|
214
|
+
@grid_score.inc(row, col, 100)
|
215
|
+
r = row + dir_y * 2
|
216
|
+
c = col + dir_x * 2
|
217
|
+
@grid_score.inc(row, col, 200) if @grid.valid_target?(r, c) && @grid.get(r, c) == :hit
|
218
|
+
end
|
219
|
+
}
|
220
|
+
|
221
|
+
# - 10 if there's a guess in a direction within n - 1
|
222
|
+
DIRECTIONS.each{|dir_y, dir_x|
|
223
|
+
has_close_guess = false
|
224
|
+
(1..n-1).each{|multiplier|
|
225
|
+
r = row + (dir_y * multiplier)
|
226
|
+
c = col + (dir_x * multiplier)
|
227
|
+
has_close_guess = true if @grid.valid_target?(r, c) && @grid.get(r, c)
|
228
|
+
}
|
229
|
+
@grid_score.dec(row, col, 10) if has_close_guess
|
230
|
+
}
|
231
|
+
end
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
def pick_best
|
236
|
+
best_score = -999
|
237
|
+
best_coords = [[0, 0]]
|
238
|
+
@grid_score.each_cell{|row, col, cell|
|
239
|
+
if cell > best_score
|
240
|
+
best_score = cell
|
241
|
+
best_coords = [[row, col]]
|
242
|
+
elsif cell == best_score
|
243
|
+
best_coords << [row, col]
|
244
|
+
end
|
245
|
+
}
|
246
|
+
best_coords[rand(best_coords.size)]
|
247
|
+
end
|
248
|
+
|
249
|
+
def smallest_ship_size
|
250
|
+
# the size of the smallest ship left
|
251
|
+
@ships_left.inject(5) {|smallest, ship|
|
252
|
+
smallest = (SHIP_TYPES[ship] < smallest) ? SHIP_TYPES[ship] : smallest
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
def random_shot
|
257
|
+
begin
|
258
|
+
r, c = rand(10), rand(10)
|
259
|
+
end until !@grid[r][c]
|
260
|
+
[r, c]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class Grid
|
265
|
+
def initialize
|
266
|
+
reset
|
267
|
+
end
|
268
|
+
|
269
|
+
def reset
|
270
|
+
@grid = Array.new(10)
|
271
|
+
@grid.each_with_index{|a, i| @grid[i] = Array.new(10) }
|
272
|
+
end
|
273
|
+
|
274
|
+
def valid_target?(row, col)
|
275
|
+
(0..9).include?(row) && (0..9).include?(col)
|
276
|
+
end
|
277
|
+
|
278
|
+
def each_cell
|
279
|
+
(0..9).each{|row|
|
280
|
+
(0..9).each{|col|
|
281
|
+
yield row, col, @grid[row][col]
|
282
|
+
}
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
286
|
+
def get(row, col)
|
287
|
+
@grid[row][col]
|
288
|
+
end
|
289
|
+
|
290
|
+
def [](row, col)
|
291
|
+
@grid[row][col]
|
292
|
+
end
|
293
|
+
|
294
|
+
def set(row, col, val)
|
295
|
+
@grid[row][col] = val
|
296
|
+
end
|
297
|
+
|
298
|
+
def dec(row, col, val = 1)
|
299
|
+
@grid[row][col] -= val
|
300
|
+
end
|
301
|
+
|
302
|
+
def inc(row, col, val = 1)
|
303
|
+
@grid[row][col] += val
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
require 'ssoroka_takes_the_win/ssoroka_takes_the_win'
|
3
|
+
|
4
|
+
describe SsorokaTakesTheWin::SsorokaTakesTheWin do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@game = SsorokaTakesTheWin::SsorokaTakesTheWin.new
|
8
|
+
@grid = @game.instance_variable_get("@grid")
|
9
|
+
end
|
10
|
+
|
11
|
+
def run_scores
|
12
|
+
@game.send :score_grid
|
13
|
+
@grid_score = @game.instance_variable_get("@grid_score")
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be instantiable with no paramters" do
|
17
|
+
|
18
|
+
lambda { SsorokaTakesTheWin::SsorokaTakesTheWin.new }.should_not raise_error
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should score cell 0, 0 poorly (-10) if the cell right of it is a miss" do
|
23
|
+
@grid = @game.instance_variable_get("@grid")
|
24
|
+
@grid.set(0, 1, :miss)
|
25
|
+
run_scores
|
26
|
+
@grid_score.get(0, 0).should == -10
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should score cell 0, 0 poorly (-20) if the cells right and below it are a miss" do
|
30
|
+
@grid.set(0, 1, :miss)
|
31
|
+
@grid.set(1, 0, :miss)
|
32
|
+
run_scores
|
33
|
+
@grid_score.get(0, 0).should == -20
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not pick cells that you've previously shot at" do
|
37
|
+
(0..99).to_a.each{|i|
|
38
|
+
if i != 50
|
39
|
+
@grid.set(i / 10, i % 10, :miss)
|
40
|
+
end
|
41
|
+
}
|
42
|
+
run_scores
|
43
|
+
best = @game.send :pick_best
|
44
|
+
best.should == [5, 0]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should guess next to a hit" do
|
48
|
+
@grid.set(0, 1, :hit)
|
49
|
+
@grid.set(1, 0, :hit)
|
50
|
+
@grid.set(1, 1, :miss)
|
51
|
+
run_scores
|
52
|
+
best = @game.send :pick_best
|
53
|
+
best.should == [0, 0]
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ssoroka_takes_the_win
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steven Soroka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-12-01 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A battleship player
|
17
|
+
email: ssoroka78@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Battleship.Rakefile
|
26
|
+
- lib
|
27
|
+
- lib/ssoroka_takes_the_win
|
28
|
+
- lib/ssoroka_takes_the_win/ssoroka_takes_the_win.rb
|
29
|
+
- Rakefile
|
30
|
+
- spec
|
31
|
+
- spec/spec_helper.rb
|
32
|
+
- spec/ssoroka_takes_the_win
|
33
|
+
- spec/ssoroka_takes_the_win/ssoroka_takes_the_win_spec.rb
|
34
|
+
has_rdoc: false
|
35
|
+
homepage: http://sparring.rubyforge.org/
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project: sparring
|
56
|
+
rubygems_version: 1.2.0
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: Battleship Player:ssoroka takes the win
|
60
|
+
test_files: []
|
61
|
+
|