joshua_son_of_nun 1.0

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
data/README.rdoc ADDED
@@ -0,0 +1,15 @@
1
+ == Joshua, Son of Nun
2
+
3
+ This is my entry for the ruby battleship sparring tournament (http://sparring.rubyforge.org/battleship/index.html).
4
+
5
+ I also built a game simulator so testing of my player didn't have to depend on LimeLight. You can run 100 games like so:
6
+
7
+ ruby game_simulator.rb 100
8
+
9
+ You can also specify which strategy you would like each player to have. So, if you want to run 200 games where player 1 uses the Random strategy and player 2 uses the Knight strategy, you would use the following command:
10
+
11
+ ruby game_simulator.rb Random Knight 200
12
+
13
+ == License
14
+
15
+ This is mine. Don't steal it! :-)
data/Rakefile ADDED
@@ -0,0 +1,54 @@
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.spec_opts = ['--options', 'spec/spec.opts']
10
+ t.rcov = false
11
+ end
12
+ task :default => :spec
13
+
14
+ PKG_NAME = "joshua_son_of_nun"
15
+ PKG_VERSION = "1.0"
16
+
17
+ spec = Gem::Specification.new do |s|
18
+ s.name = PKG_NAME
19
+ s.version = PKG_VERSION
20
+ s.files = FileList['**/*'].to_a.reject!{ |f| f =~ /report/ }
21
+ s.require_path = 'lib'
22
+ s.test_files = Dir.glob('spec/*_spec.rb')
23
+ s.bindir = 'bin'
24
+ s.executables = []
25
+ s.summary = "Battleship Player:joshua_son_of_nun"
26
+ s.rubyforge_project = "sparring"
27
+ s.homepage = "http://sparring.rubyforge.org/"
28
+
29
+ ###########################################
30
+ ##
31
+ ## You are encouraged to modify the following
32
+ ## spec attributes.
33
+ ##
34
+ ###########################################
35
+ s.description = "(using scary WWF wrestler voice) Just like the walls of Jericho, the same will happen to all battleship opponents of Joshua, son of Nun!!"
36
+ s.author = "Steve Iannopollo"
37
+ s.email = "steve@iannopollo.com"
38
+ end
39
+
40
+ Rake::GemPackageTask.new(spec) do |pkg|
41
+ pkg.need_zip = false
42
+ pkg.need_tar = false
43
+ end
44
+
45
+ desc "Submit your player"
46
+ task :submit do
47
+ submitter = BattleshipTournament::Submit.new(PKG_NAME)
48
+ submitter.submit
49
+ end
50
+
51
+ desc 'Locally build the gem'
52
+ task :make_gem do
53
+ `cd #{File.expand_path(File.dirname(__FILE__))} && rm -f pkg/joshua_son_of_nun-1.0.gem && rake gem && sudo gem uninstall joshua_son_of_nun && sudo gem install pkg/joshua_son_of_nun-1.0.gem`
54
+ end
data/game_simulator.rb ADDED
@@ -0,0 +1,151 @@
1
+ require 'init'
2
+
3
+ OLD_STRATEGIES = JoshuaSonOfNun::Strategy.strategies.dup
4
+ JoshuaSonOfNun::Strategy.module_eval do
5
+ class << self
6
+ attr_accessor :strategies
7
+ end
8
+ end
9
+ JoshuaSonOfNun::Strategy.strategies = OLD_STRATEGIES
10
+
11
+ def Space(string)
12
+ coordinates, orientation = string.split(' ')
13
+ row, column = coordinates.scan(/(\w)(\d{1,2})/).first
14
+ JoshuaSonOfNun::Space.new(row, column, orientation)
15
+ end
16
+
17
+ class GameSimulator
18
+ attr_reader :current_player, :game_over, :opponent,
19
+ :player_one_strategy, :player_two_strategy,
20
+ :player_one, :player_two, :players, :results,
21
+ :ship_placement, :ships
22
+
23
+ alias_method :game_over?, :game_over
24
+
25
+ def initialize(player_one_strategy, player_two_strategy)
26
+ @player_one_strategy, @player_two_strategy = player_one_strategy, player_two_strategy
27
+ @player_one = JoshuaSonOfNun::Player.new
28
+ @player_two = JoshuaSonOfNun::Player.new
29
+ tag_players!
30
+
31
+ @moves = []
32
+ @results = {@player_one.name => [], @player_two.name => []}
33
+ end
34
+
35
+ def determine_damage(target)
36
+ hit, sunk = nil
37
+ ship_placement[opponent.name].each do |ship, spaces|
38
+ space = spaces.delete(target)
39
+ if space
40
+ hit = true
41
+ sunk = spaces.empty?
42
+ break
43
+ end
44
+ end
45
+ @game_over = ship_placement[opponent.name].values.flatten.empty?
46
+ [hit, sunk]
47
+ end
48
+
49
+ def fire_on_opponent
50
+ target = current_player.next_target
51
+ ship_hit, ship_sunk = determine_damage(target)
52
+
53
+ current_player.target_result(target.to_s, ship_hit, ship_sunk)
54
+ opponent.enemy_targeting(target.to_s)
55
+ end
56
+
57
+ def force_strategy(strategy)
58
+ JoshuaSonOfNun::Strategy.strategies = [strategy]
59
+ end
60
+
61
+ def gather_ship_placement
62
+ players.each do |player|
63
+ placement = {}
64
+ ships.each do |ship|
65
+ placement[ship] = Space(player.send("#{ship}_placement")).spaces_for_placement(player.instance_variable_get("@#{ship}").length)
66
+ end
67
+
68
+ ship_placement[player.name] = placement
69
+ end
70
+ end
71
+
72
+ def prepare_report
73
+ @filename = File.dirname(__FILE__) + "/reports/#{strategy_name(@player_one)}_vs_#{strategy_name(@player_two)}.csv"
74
+ `test -f #{@filename} && echo '' || echo '#{strategy_name(@player_one)},#{strategy_name(@player_two)},moves' > #{@filename}`
75
+ end
76
+
77
+ def report_results
78
+ result = (@current_player == @player_one ? '1,0,' : '0,1,') + (@moves.last/2.0).ceil.to_s
79
+ `echo '#{result}' >> #{@filename}`
80
+ print "\e[32m.\e[0m"; STDOUT.flush
81
+ end
82
+
83
+ def reset
84
+ @players = [@player_one, @player_two]
85
+
86
+ force_strategy(player_one_strategy) unless player_one_strategy.nil?
87
+ @player_one.new_game(@player_two.name)
88
+ force_strategy(player_two_strategy) unless player_two_strategy.nil?
89
+ @player_two.new_game(@player_one.name)
90
+ prepare_report
91
+
92
+ @ship_placement = {}
93
+ @ships = [:battleship, :carrier, :destroyer, :patrolship, :submarine]
94
+ gather_ship_placement
95
+
96
+ @moves << 0
97
+ @game_over = false
98
+ end
99
+
100
+ def run!
101
+ reset
102
+
103
+ until game_over?
104
+ switch_turns
105
+ fire_on_opponent
106
+ end
107
+
108
+ @results[@current_player.name] << strategy_name(@current_player)
109
+ report_results
110
+ end
111
+
112
+ def strategy_name(player)
113
+ player.instance_variable_get('@strategy').class.name.split("::").last
114
+ end
115
+
116
+ def summarized_results
117
+ summary = results.collect do |player, winning_strategies|
118
+ output = "#{player}:"
119
+ output << " #{winning_strategies.size} wins"
120
+ strategy_count = OLD_STRATEGIES.uniq.collect do |strategy|
121
+ count = winning_strategies.select {|s| s == strategy}.size
122
+ "#{strategy} => #{count}"
123
+ end
124
+ output << " (#{strategy_count * ', '})"
125
+ end.reverse * "\n"
126
+ average_moves = @moves.inject(0) {|sum, n| sum += n} / (2 * @moves.size.to_f)
127
+ summary << "\n#{average_moves} moves per game\n"
128
+ summary
129
+ end
130
+
131
+ def switch_turns
132
+ @current_player = @players.shift
133
+ @players << @current_player
134
+ @opponent = @players.first
135
+ @moves << @moves.pop + 1
136
+ end
137
+
138
+ def tag_players!
139
+ class << player_one
140
+ def name; 'player_one'; end
141
+ end
142
+ class << player_two
143
+ def name; 'player_two'; end
144
+ end
145
+ end
146
+ end
147
+
148
+ number_of_games = (ARGV.pop || 21).to_i
149
+ simulator = GameSimulator.new(ARGV.shift, ARGV.shift)
150
+ number_of_games.times {simulator.run!}
151
+ puts '', simulator.summarized_results, ''
data/init.rb ADDED
@@ -0,0 +1,13 @@
1
+ $: << File.expand_path(File.dirname(__FILE__) + '/lib')
2
+
3
+ require 'joshua_son_of_nun/ship'
4
+ require 'joshua_son_of_nun/board'
5
+ require 'joshua_son_of_nun/space'
6
+
7
+ require 'joshua_son_of_nun/strategy/base'
8
+ require 'joshua_son_of_nun/strategy/random'
9
+ require 'joshua_son_of_nun/strategy/diagonal'
10
+ require 'joshua_son_of_nun/strategy/knight'
11
+ require 'joshua_son_of_nun/strategy/targeting_reaction'
12
+
13
+ require 'joshua_son_of_nun/player'
@@ -0,0 +1,39 @@
1
+ module JoshuaSonOfNun
2
+ class Board
3
+ ROWS = ('A'..'J').to_a
4
+ COLUMNS = ('1'..'10').to_a
5
+
6
+ attr_reader :occupied_spaces, :valid_spaces
7
+
8
+ def initialize
9
+ @occupied_spaces, @valid_spaces = [], []
10
+ ROWS.each do |row|
11
+ COLUMNS.each do |column|
12
+ @valid_spaces << Space.new(row, column)
13
+ end
14
+ end
15
+ end
16
+
17
+ def adjacent_to_occupied_spaces?(space, ship_length)
18
+ spaces = space.spaces_for_placement(ship_length)
19
+ occupied_spaces.inject(false) do |memo, occupied_space|
20
+ memo |= spaces.inject(false) {|m, s| m |= s.adjacent?(occupied_space)}
21
+ end
22
+ end
23
+
24
+ def accomodate?(space, ship_length)
25
+ space.spaces_for_placement(ship_length).inject(true) do |success, possible_space|
26
+ success &= !occupied_spaces.include?(possible_space) && valid_spaces.include?(possible_space)
27
+ end
28
+ end
29
+
30
+ def placement(ship_length)
31
+ space = nil
32
+ while space.nil? || !accomodate?(space, ship_length) || adjacent_to_occupied_spaces?(space, ship_length)
33
+ space = Space.generate
34
+ end
35
+ occupied_spaces.concat(space.spaces_for_placement(ship_length))
36
+ space.to_s
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../init')
2
+
3
+ module JoshuaSonOfNun
4
+ JoshuaSonOfNun = Player
5
+ end
@@ -0,0 +1,54 @@
1
+ module JoshuaSonOfNun
2
+ class Player
3
+ def battleship_placement
4
+ @battleship.initial_placement
5
+ end
6
+
7
+ def carrier_placement
8
+ @carrier.initial_placement
9
+ end
10
+
11
+ def destroyer_placement
12
+ @destroyer.initial_placement
13
+ end
14
+
15
+ def patrolship_placement
16
+ @patrolship.initial_placement
17
+ end
18
+
19
+ def submarine_placement
20
+ @submarine.initial_placement
21
+ end
22
+
23
+ def next_target
24
+ @strategy.next_target.to_s
25
+ end
26
+
27
+ def target_result(coordinates, was_hit, ship_sunk)
28
+ @strategy.register_result!(was_hit, ship_sunk)
29
+ end
30
+
31
+ def enemy_targeting(*args); end
32
+ def game_over(*args); end
33
+
34
+ private
35
+ def reset(*args)
36
+ srand
37
+
38
+ @personal_board = Board.new
39
+ @opponent_board = Board.new
40
+
41
+ @battleship = Battleship.new(@personal_board)
42
+ @carrier = Carrier.new(@personal_board)
43
+ @destroyer = Destroyer.new(@personal_board)
44
+ @patrolship = Patrolship.new(@personal_board)
45
+ @submarine = Submarine.new(@personal_board)
46
+
47
+ @strategy = Strategy.select(@opponent_board)
48
+ end
49
+
50
+ alias_method :initialize, :reset
51
+ alias_method :new_game, :reset
52
+ public :initialize, :new_game
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ module JoshuaSonOfNun
2
+ class Ship
3
+ class << self
4
+ attr_accessor :length
5
+ end
6
+
7
+ attr_reader :initial_placement
8
+
9
+ def initialize(board)
10
+ @board = board
11
+ @initial_placement = @board.placement(length)
12
+ end
13
+
14
+ def length
15
+ self.class.length
16
+ end
17
+ end
18
+
19
+ class Battleship < Ship
20
+ self.length = 4
21
+ end
22
+
23
+ class Carrier < Ship
24
+ self.length = 5
25
+ end
26
+
27
+ class Destroyer < Ship
28
+ self.length = 3
29
+ end
30
+
31
+ class Patrolship < Ship
32
+ self.length = 2
33
+ end
34
+
35
+ class Submarine < Ship
36
+ self.length = 3
37
+ end
38
+ end
@@ -0,0 +1,172 @@
1
+ module JoshuaSonOfNun
2
+ class Space
3
+ ROWS = Board::ROWS
4
+ COLUMNS = Board::COLUMNS
5
+
6
+ def self.directions
7
+ [:southeast, :southwest, :northeast, :northwest]
8
+ end
9
+
10
+ def self.generate
11
+ new(ROWS[rand(10).to_i], COLUMNS[rand(10).to_i], %w(horizontal vertical)[rand(2)])
12
+ end
13
+
14
+ attr_reader :row, :column, :orientation
15
+
16
+ def initialize(row, column, orientation = nil)
17
+ raise 'Invalid space' unless ROWS.include?(row) && COLUMNS.include?(column)
18
+
19
+ @row, @column, @orientation = row, column, orientation
20
+ end
21
+
22
+ def adjacent?(other)
23
+ adjacent_row_range.include?(other.row_index) && adjacent_column_range.include?(other.column_index)
24
+ end
25
+
26
+ def column_index
27
+ COLUMNS.index(column)
28
+ end
29
+
30
+ def crosswise_spaces
31
+ top = (index = row_index - 1) < 0 ? nil : Space.new(ROWS[index], column)
32
+ right = (index = column_index + 1) > 9 ? nil : Space.new(row, COLUMNS[index])
33
+ bottom = (index = row_index + 1) > 9 ? nil : Space.new(ROWS[index], column)
34
+ left = (index = column_index - 1) < 0 ? nil : Space.new(row, COLUMNS[index])
35
+ [top, right, bottom, left].compact
36
+ end
37
+
38
+ def linear_spaces(other, all_illegal_spaces = [], successful_illegal_spaces = [])
39
+ top, left, bottom, right = nil
40
+ illegal_spaces = all_illegal_spaces.dup.concat([self, other]).uniq
41
+ successful_spaces = successful_illegal_spaces.dup
42
+
43
+ if row_index == other.row_index
44
+ left, right = assign_left_and_right(other.column_index, illegal_spaces, successful_spaces)
45
+ elsif column_index == other.column_index
46
+ top, bottom = assign_top_and_bottom(other.row_index, illegal_spaces, successful_spaces)
47
+ end
48
+
49
+ [top, left, bottom, right].compact
50
+ end
51
+
52
+ def linear?(other)
53
+ row_index == other.row_index || column_index == other.column_index
54
+ end
55
+
56
+ def row_index
57
+ ROWS.index(row)
58
+ end
59
+
60
+ def spaces_for_placement(ship_length)
61
+ args = case orientation
62
+ when 'horizontal': lambda {|i| [row, COLUMNS[column_index + i].to_s]}
63
+ when 'vertical': lambda {|i| [ROWS[row_index + i].to_s, column]}
64
+ end
65
+
66
+ (0..(ship_length - 1)).collect {|i| Space.new(*args.call(i)) rescue nil}
67
+ end
68
+
69
+ def spaces_in_knighted_move(direction)
70
+ row_offset, column_offset = case direction
71
+ when :northeast: [[-1, 2], [-2, 1]][rand(2)]
72
+ when :southeast: [[1, 2], [2, 1]][rand(2)]
73
+ when :southwest: [[1, -2], [2, -1]][rand(2)]
74
+ when :northwest: [[-1, -2], [-2, -1]][rand(2)]
75
+ end
76
+
77
+ r_index, c_index = row_index + row_offset, column_index + column_offset
78
+ if r_index < 0 || c_index < 0 || r_index > 9 || c_index > 9
79
+ nil
80
+ else
81
+ Space.new(ROWS[r_index], COLUMNS[c_index])
82
+ end
83
+ end
84
+
85
+ def spaces_on_diagonal(direction)
86
+ spaces, boundary_reached = [], false
87
+ until boundary_reached
88
+ row_offset, column_offset = case direction
89
+ when :northeast: [-1, 1]
90
+ when :southeast: [1, 1]
91
+ when :southwest: [1, -1]
92
+ when :northwest: [-1, -1]
93
+ end
94
+
95
+ seed_space = spaces.last || self
96
+ r_index, c_index = seed_space.row_index + row_offset, seed_space.column_index + column_offset
97
+
98
+ if r_index < 0 || c_index < 0 || r_index > 9 || c_index > 9
99
+ boundary_reached = true
100
+ else
101
+ spaces << Space.new(ROWS[r_index], COLUMNS[c_index])
102
+ end
103
+ end
104
+
105
+ spaces
106
+ end
107
+
108
+ def to_s
109
+ [row.to_s + column.to_s, orientation].compact * ' '
110
+ end
111
+ alias_method :inspect, :to_s
112
+
113
+ def ==(other)
114
+ to_s == other.to_s
115
+ end
116
+
117
+ private
118
+ def adjacent_column_range
119
+ ((column_index == 0 ? 0 : column_index-1)..column_index+1)
120
+ end
121
+
122
+ def adjacent_row_range
123
+ ((row_index == 0 ? 0 : row_index-1)..row_index+1)
124
+ end
125
+
126
+ def assign_left_and_right(other_column_index, illegal_spaces, successful_spaces)
127
+ left_index = (column_index < other_column_index ? column_index : other_column_index)
128
+ right_index = left_index + 1
129
+
130
+ surrounding_linear_spaces(left_index, right_index, illegal_spaces, successful_spaces) do |i, row, column|
131
+ Space.new(row, COLUMNS[i])
132
+ end
133
+ end
134
+
135
+ def assign_top_and_bottom(other_row_index, illegal_spaces, successful_spaces)
136
+ top_index = (row_index < other_row_index ? row_index : other_row_index)
137
+ bottom_index = top_index + 1
138
+
139
+ surrounding_linear_spaces(top_index, bottom_index, illegal_spaces, successful_spaces) do |i, row, column|
140
+ Space.new(ROWS[i], column)
141
+ end
142
+ end
143
+
144
+ def surrounding_linear_spaces(side_one_index, side_two_index, illegal_spaces, successful_spaces, &space_creation_block)
145
+ side_one_spaces = (0..side_one_index).collect {|i| space_creation_block.call(i, row, column)}
146
+ side_two_spaces = (side_two_index..9).collect {|i| space_creation_block.call(i, row, column)}
147
+
148
+ illegal_spaces.each do |space|
149
+ side_one_spaces.delete(space)
150
+ side_two_spaces.delete(space)
151
+ end
152
+
153
+ spaces = {
154
+ :side_one_space => side_one_spaces.pop,
155
+ :side_two_space => side_two_spaces.shift
156
+ }
157
+
158
+ unless illegal_spaces.empty? || successful_spaces.empty?
159
+ unsuccessful_spaces = illegal_spaces.reject {|s| successful_spaces.include?(s)}
160
+
161
+ [:side_one_space, :side_two_space].each do |key|
162
+ space = spaces[key]
163
+ spaces[key] = nil if !space.nil? &&
164
+ unsuccessful_spaces.detect {|s| space.adjacent?(s)} &&
165
+ !successful_spaces.detect {|s| space.adjacent?(s)}
166
+ end
167
+ end
168
+
169
+ [spaces[:side_one_space], spaces[:side_two_space]]
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,47 @@
1
+ module JoshuaSonOfNun
2
+ module Strategy
3
+ def self.strategies
4
+ [['Random']*4, 'Diagonal', ['Knight']*2].flatten
5
+ end
6
+
7
+ def self.select(board)
8
+ const_get(strategies[rand(strategies.size)]).new(board)
9
+ end
10
+
11
+ class Base
12
+ attr_reader :current_target, :expended_targets, :immediate_targets,
13
+ :possible_targets, :successful_targets, :targets
14
+
15
+ def initialize(board)
16
+ @possible_targets = board.valid_spaces.dup
17
+ @targets = assign_targets
18
+ @expended_targets, @successful_targets, @immediate_targets = [], [], []
19
+ end
20
+
21
+ def next_target
22
+ new_target = immediate_targets.shift || targets.first
23
+ targets.delete(new_target)
24
+
25
+ @current_target = new_target
26
+ expended_targets << @current_target
27
+ @current_target
28
+ end
29
+
30
+ def register_result!(ship_hit, ship_sunk)
31
+ if ship_hit
32
+ successful_targets << current_target
33
+ @immediate_targets = TargetingReaction.new(self, ship_sunk).react!
34
+ end
35
+ end
36
+
37
+ private
38
+ def choose_target(index = rand(possible_targets.size))
39
+ possible_targets.delete(possible_targets[index])
40
+ end
41
+
42
+ def random_direction
43
+ Space.directions[rand(Space.directions.size)]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ module JoshuaSonOfNun
2
+ module Strategy
3
+ class Diagonal < Base
4
+ private
5
+ def assign_targets
6
+ targets = []
7
+ until possible_targets.empty?
8
+ target = choose_target
9
+ diagonal_targets = target.spaces_on_diagonal(random_direction)
10
+ targets << target
11
+ diagonal_targets.each do |diagonal_target|
12
+ targets << possible_targets.delete(diagonal_target)
13
+ end
14
+ end
15
+ targets.compact
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module JoshuaSonOfNun
2
+ module Strategy
3
+ class Knight < Base
4
+ private
5
+ def assign_targets
6
+ targets = [choose_target]
7
+ until possible_targets.empty?
8
+ next_target = targets.last.spaces_in_knighted_move(random_direction)
9
+ next_target = choose_target if targets.include?(next_target) || next_target.nil?
10
+ possible_targets.delete(next_target)
11
+ targets << next_target
12
+ end
13
+ targets
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module JoshuaSonOfNun
2
+ module Strategy
3
+ class Random < Base
4
+ private
5
+ def assign_targets
6
+ targets = []
7
+ possible_targets.size.times do |n|
8
+ targets << choose_target(rand(100 - n))
9
+ end
10
+ targets
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ module JoshuaSonOfNun
2
+ module Strategy
3
+ class TargetingReaction
4
+ attr_reader :strategy
5
+
6
+ def initialize(strategy, ship_sunk)
7
+ @strategy, @ship_sunk = strategy, ship_sunk
8
+ end
9
+
10
+ def react!
11
+ return [] if @ship_sunk
12
+
13
+ if targets_lined_up?
14
+ targets = strategy.successful_targets
15
+ reject_expended(targets[-2].linear_spaces(targets.last, strategy.expended_targets, strategy.successful_targets))
16
+ else
17
+ reject_expended(strategy.current_target.crosswise_spaces)
18
+ end
19
+ end
20
+
21
+ protected
22
+ def reject_expended(spaces)
23
+ spaces.reject do |space|
24
+ strategy.expended_targets.include?(space)
25
+ end
26
+ end
27
+
28
+ def targets_lined_up?
29
+ targets = strategy.successful_targets
30
+ targets.size > 1 && targets[-2].linear?(targets.last)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe JoshuaSonOfNun::Board do
4
+ before do
5
+ @model = JoshuaSonOfNun::Board.new
6
+ end
7
+
8
+ it "should know about which spaces are valid" do
9
+ @model.valid_spaces.should include(Space('A1'))
10
+ @model.valid_spaces.should include(Space('A10'))
11
+ end
12
+
13
+ it "should calculate whether it can accomodate a ship at a certain space" do
14
+ @model.accomodate?(Space('A1 horizontal'), 5).should be_true
15
+ @model.accomodate?(Space('A10 horizontal'), 5).should be_false
16
+ @model.accomodate?(Space('H1 vertical'), 3).should be_true
17
+ @model.accomodate?(Space('H1 vertical'), 4).should be_false
18
+ end
19
+
20
+ it "should keep track of it's occupied spaces" do
21
+ @model.placement(4)
22
+ @model.occupied_spaces.size.should == 4
23
+
24
+ @model.placement(5)
25
+ @model.occupied_spaces.size.should == 9
26
+ end
27
+
28
+ it "should be able to tell if a space is adjacent to the occupied spaces" do
29
+ space = Space('D3 horizontal')
30
+ @model.occupied_spaces.concat(space.spaces_for_placement(5)) # D3, D4, D5, D6, D7
31
+
32
+ @model.adjacent_to_occupied_spaces?(Space('C3 horizontal'), 5).should be_true
33
+ @model.adjacent_to_occupied_spaces?(Space('E3 horizontal'), 5).should be_true
34
+ @model.adjacent_to_occupied_spaces?(Space('E3 vertical'), 2).should be_true
35
+ @model.adjacent_to_occupied_spaces?(Space('C8 vertical'), 2).should be_true
36
+ @model.adjacent_to_occupied_spaces?(Space('E2 vertical'), 2).should be_true
37
+
38
+ @model.adjacent_to_occupied_spaces?(Space('A1 horizontal'), 5).should be_false
39
+ @model.adjacent_to_occupied_spaces?(Space('A1 vertical'), 5).should be_false
40
+ @model.adjacent_to_occupied_spaces?(Space('A4 vertical'), 2).should be_false
41
+ @model.adjacent_to_occupied_spaces?(Space('C9 vertical'), 5).should be_false
42
+ end
43
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe JoshuaSonOfNun::Player do
4
+ before do
5
+ @model = JoshuaSonOfNun::Player.new
6
+ end
7
+
8
+ it "should conform to the battleship API" do
9
+ @model.carrier_placement.should_not be_nil
10
+ @model.battleship_placement.should_not be_nil
11
+ @model.destroyer_placement.should_not be_nil
12
+ @model.submarine_placement.should_not be_nil
13
+ @model.patrolship_placement.should_not be_nil
14
+
15
+ 100.times do
16
+ @model.next_target.should_not == ''
17
+ end
18
+
19
+ lambda {@model.target_result('A1', false, false)}.should_not raise_error
20
+ lambda {@model.new_game('bob')}.should_not raise_error
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Ships" do
4
+ before do
5
+ @board = JoshuaSonOfNun::Board.new
6
+ @model = JoshuaSonOfNun::Battleship.new(@board)
7
+ end
8
+
9
+ it "should have a length" do
10
+ @model.length.should == 4
11
+ end
12
+
13
+ it "should have an initial placement on the board" do
14
+ @model.initial_placement.should_not be_nil
15
+ @model.initial_placement.should =~ /[A-Z]\d{1,2} [horizontal|vertical]/
16
+ end
17
+ end
@@ -0,0 +1,73 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe JoshuaSonOfNun::Space do
4
+ before do
5
+ @model = Space('A1')
6
+ end
7
+
8
+ it "should know which spaces are close to other spaces" do
9
+ @model.adjacent?(Space('A1')).should be_true
10
+ @model.adjacent?(Space('A2')).should be_true
11
+ @model.adjacent?(Space('B1')).should be_true
12
+ @model.adjacent?(Space('B2')).should be_true
13
+ Space('E5').adjacent?(Space('D6')).should be_true
14
+
15
+ @model.adjacent?(Space('A10')).should be_false
16
+ @model.adjacent?(Space('C3')).should be_false
17
+ Space('H7').adjacent?(Space('J7')).should be_false
18
+ end
19
+
20
+ it "should return the spaces for placement for a ship of given length" do
21
+ Space('A1 horizontal').spaces_for_placement(4).should ==
22
+ space_array('A1', 'A2', 'A3', 'A4')
23
+
24
+ Space('A1 vertical').spaces_for_placement(5).should ==
25
+ space_array('A1', 'B1', 'C1', 'D1', 'E1')
26
+
27
+ Space('A8 horizontal').spaces_for_placement(5).should ==
28
+ space_array('A8', 'A9', 'A10').concat([nil, nil])
29
+ end
30
+
31
+ it "should know which spaces are crosswise to the given space" do
32
+ @model.crosswise_spaces.should == space_array('A2', 'B1')
33
+ Space('C10').crosswise_spaces.should == space_array('B10', 'D10', 'C9')
34
+ Space('E5').crosswise_spaces.should == space_array('D5', 'E6', 'F5', 'E4')
35
+ Space('A7').crosswise_spaces.should == space_array('A8', 'B7', 'A6')
36
+ end
37
+
38
+ it "should know which spaces are in the diagonal of a given direction" do
39
+ @model = Space('E5')
40
+ @model.spaces_on_diagonal(:northeast).should == space_array('D6', 'C7', 'B8', 'A9')
41
+ @model.spaces_on_diagonal(:southeast).should == space_array('F6', 'G7', 'H8', 'I9', 'J10')
42
+ @model.spaces_on_diagonal(:southwest).should == space_array('F4', 'G3', 'H2', 'I1')
43
+ @model.spaces_on_diagonal(:northwest).should == space_array('D4', 'C3', 'B2', 'A1')
44
+ end
45
+
46
+ it "should know which spaces are in an 'L' shape to the given square and direction" do
47
+ @model.spaces_in_knighted_move(:northeast).should be_nil
48
+ space_array('C2', 'B3').should include(@model.spaces_in_knighted_move(:southeast))
49
+ @model.spaces_in_knighted_move(:southwest).should be_nil
50
+ @model.spaces_in_knighted_move(:northwest).should be_nil
51
+
52
+ @model = Space('E5')
53
+ space_array('C6', 'D7').should include(@model.spaces_in_knighted_move(:northeast))
54
+ space_array('G6', 'F7').should include(@model.spaces_in_knighted_move(:southeast))
55
+ space_array('F3', 'G4').should include(@model.spaces_in_knighted_move(:southwest))
56
+ space_array('D3', 'C4').should include(@model.spaces_in_knighted_move(:northwest))
57
+ end
58
+
59
+ describe "linear spaces" do
60
+ it "should know which spaces are linear to two given spaces, and return the two immediate closest spaces" do
61
+ Space('E6').linear_spaces(Space('E5')).should == space_array('E4', 'E7')
62
+ Space('E5').linear_spaces(Space('D5')).should == space_array('C5', 'F5')
63
+ @model.linear_spaces(Space('A2')).should == [Space('A3')]
64
+ @model.linear_spaces(Space('B1')).should == [Space('C1')]
65
+ end
66
+
67
+ it "should not return spaces that are beyond spaces that have been targeted but were not successful" do
68
+ expended = space_array('A2', 'A3', 'A4', 'A5', 'A7')
69
+ successful = space_array('A4', 'A5')
70
+ Space('A4').linear_spaces(Space('A5'), expended, successful).should == [Space('A6')]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,110 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe JoshuaSonOfNun::Strategy::Random do
4
+ before do
5
+ @board = JoshuaSonOfNun::Board.new
6
+ @model = JoshuaSonOfNun::Strategy::Random.new(@board)
7
+ end
8
+
9
+ it "should target all spaces" do
10
+ @model.targets.uniq.size.should == 100
11
+ end
12
+
13
+ it "should shift targets out of the array to gather targets" do
14
+ target = nil
15
+ lambda do
16
+ @model.next_target
17
+ @model.next_target
18
+ target = @model.next_target
19
+ end.should change(@model.targets, :size).by(-3)
20
+ @model.current_target.should == target
21
+ end
22
+
23
+ it "should receive results of the current target, and react accordingly" do
24
+ target = @model.targets.delete(Space('E5'))
25
+ @model.instance_variable_set '@current_target', target
26
+ @model.register_result! true, false
27
+
28
+ @model.next_target.should == Space('D5') # First crosswise space to attack
29
+ @model.next_target.should == Space('E6')
30
+ @model.next_target.should == Space('F5')
31
+ @model.next_target.should == Space('E4')
32
+ end
33
+
34
+ it "should not target differently if nothing was hit" do
35
+ target = @model.targets.delete(Space('E5'))
36
+ @model.instance_variable_set '@current_target', target
37
+ next_target = @model.targets.first
38
+ @model.register_result! false, false
39
+
40
+ @model.next_target.should == next_target
41
+ end
42
+
43
+ it "should target in a line if two or more targets have been hit" do
44
+ @model.instance_variable_set '@immediate_targets', [Space('E5')]
45
+ @model.next_target; @model.register_result! true, false # first shot
46
+
47
+ @model.instance_variable_set '@immediate_targets', [Space('E6')]
48
+ @model.next_target; @model.register_result! true, false # second shot
49
+
50
+ @model.next_target.should == Space('E4')
51
+ @model.register_result! false, false
52
+
53
+ @model.next_target.should == Space('E7')
54
+ @model.register_result! true, false
55
+
56
+ @model.next_target.should == Space('E8')
57
+ end
58
+
59
+ it "should track successful targets" do
60
+ target_one = @model.next_target
61
+ @model.register_result! true, false
62
+
63
+ target_two = @model.next_target
64
+ @model.register_result! true, false
65
+
66
+ @model.successful_targets.should == [target_one, target_two]
67
+ end
68
+ end
69
+
70
+ describe JoshuaSonOfNun::Strategy::Diagonal do
71
+ before do
72
+ JoshuaSonOfNun::Space.stub!(:directions).and_return([:southeast])
73
+
74
+ @board = JoshuaSonOfNun::Board.new
75
+ @model = JoshuaSonOfNun::Strategy::Diagonal.new(@board)
76
+ end
77
+
78
+ it "should target all spaces" do
79
+ @model.targets.uniq.size.should == 100
80
+ end
81
+
82
+ it "should search across the board in a diagonal fashion" do
83
+ target = Space('E5')
84
+ expected_next_target = Space('F6')
85
+
86
+ # One of these spaces may have already been placed in the targeting array,
87
+ # so checking to make sure at least one of these spaces is the next space
88
+ space_array('F6', 'G7', 'H8', 'I9', 'J10').should include(@model.targets[@model.targets.index(target) + 1])
89
+ end
90
+ end
91
+
92
+ describe JoshuaSonOfNun::Strategy::Knight do
93
+ before do
94
+ JoshuaSonOfNun::Space.stub!(:directions).and_return([:southeast])
95
+
96
+ @board = JoshuaSonOfNun::Board.new
97
+ @model = JoshuaSonOfNun::Strategy::Knight.new(@board)
98
+ end
99
+
100
+ it "should target all spaces" do
101
+ @model.targets.uniq.size.should == 100
102
+ # The specifics of this strategy are not tested here because the next move
103
+ # from space E5 may have already been placed in the targeting array,
104
+ # making the order very difficult to test since sometimes the test will
105
+ # pass, other times the test will fail. So, testing whether or not we have
106
+ # all the targets is good enough.
107
+ #
108
+ # Also, moving like a knight is tested in the space_spec
109
+ end
110
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,7 @@
1
+ --colour
2
+ --format
3
+ progress
4
+ --loadby
5
+ mtime
6
+ --reverse
7
+ --backtrace
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require 'init'
5
+
6
+ module Kernel
7
+ require 'cgi'
8
+
9
+ def rputs(*args)
10
+ puts *["<pre>", args.collect {|a| CGI.escapeHTML(a.inspect)}, "</pre>"]
11
+ end
12
+ end
13
+
14
+ def Space(string)
15
+ coordinates, orientation = string.split(' ')
16
+ row, column = coordinates.scan(/(\w)(\d{1,2})/).first
17
+ JoshuaSonOfNun::Space.new(row, column, orientation)
18
+ end
19
+
20
+ def space_array(*coordinates)
21
+ coordinates.collect {|c| Space(c)}
22
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: joshua_son_of_nun
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Steve Iannopollo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-29 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: (using scary WWF wrestler voice) Just like the walls of Jericho, the same will happen to all battleship opponents of Joshua, son of Nun!!
17
+ email: steve@iannopollo.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - Battleship.Rakefile
26
+ - game_simulator.rb
27
+ - init.rb
28
+ - lib
29
+ - lib/joshua_son_of_nun
30
+ - lib/joshua_son_of_nun/board.rb
31
+ - lib/joshua_son_of_nun/joshua_son_of_nun.rb
32
+ - lib/joshua_son_of_nun/player.rb
33
+ - lib/joshua_son_of_nun/ship.rb
34
+ - lib/joshua_son_of_nun/space.rb
35
+ - lib/joshua_son_of_nun/strategy
36
+ - lib/joshua_son_of_nun/strategy/base.rb
37
+ - lib/joshua_son_of_nun/strategy/diagonal.rb
38
+ - lib/joshua_son_of_nun/strategy/knight.rb
39
+ - lib/joshua_son_of_nun/strategy/random.rb
40
+ - lib/joshua_son_of_nun/strategy/targeting_reaction.rb
41
+ - pkg
42
+ - Rakefile
43
+ - README.rdoc
44
+ - spec
45
+ - spec/joshua_son_of_nun
46
+ - spec/joshua_son_of_nun/board_spec.rb
47
+ - spec/joshua_son_of_nun/player_spec.rb
48
+ - spec/joshua_son_of_nun/ship_spec.rb
49
+ - spec/joshua_son_of_nun/space_spec.rb
50
+ - spec/joshua_son_of_nun/strategy_spec.rb
51
+ - spec/spec.opts
52
+ - spec/spec_helper.rb
53
+ has_rdoc: false
54
+ homepage: http://sparring.rubyforge.org/
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project: sparring
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: Battleship Player:joshua_son_of_nun
79
+ test_files: []
80
+