joshua_son_of_nun 1.0

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
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
+