Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dnagir
Created March 6, 2010 09:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dnagir/97a9a2dc128b96a1e448 to your computer and use it in GitHub Desktop.
Save dnagir/97a9a2dc128b96a1e448 to your computer and use it in GitHub Desktop.
# Dmitriy Nagirnyak (dnagir at gmail dot com) for
# the Ruby Challenge 7 (Broadsides)
# http://rubylearning.com/blog/2010/02/23/rpcfn-broadsides-7
# Add small helpers to the String for grid navigation
class String
# Returns the x-coordinate which is the letter on the board (B in B5)
def x(from = 0)
self[0,1][0] - 65 + from # 65.chr = 'A'
end
# Returns the y-coordinate which is the number on the board (5 in B5)
def y(from = 0)
self[1,2].to_i - 1 + from
end
def to_up(default = nil)
pos = y(1)
return default if pos == 1
self[0,1] + pos.pred.to_s
end
def to_dn(default = nil)
pos = y(1)
return default if pos == 10
self[0,1] + pos.succ.to_s
end
def to_left(default = nil)
pos = x(65)
return default if pos == 65
pos.pred.chr + self[1,2]
end
def to_right(default = nil)
pos = x(65)
return default if pos == 74 # 'J'
pos.succ.chr + self[1,2]
end
end
# Just want to navigate to any side as much as I want with no worries: "A1".to_left.to_left => nil
class NilClass
def to_up(default = nil)
default
end
def to_dn(default = nil)
default
end
def to_left(default = nil)
default
end
def to_right(default = nil)
default
end
end
# Discovers ships by shooting at positions 'strategically'.
# This ensures all ships will BE hit with minimal number of shots.
class ShipDiscovery
def initialize(guide_option = nil)
@guide = [
5,2,1,4,1,5,1,4,3,1, # 0
1,5,4,1,3,1,5,3,1,2, # 1
2,4,5,2,1,4,1,5,2,4, # 2
4,1,2,5,4,1,3,1,5,1, # 3
1,3,1,4,5,2,1,4,1,5, # 4
5,1,4,1,2,5,4,1,3,1, # 5
1,5,1,3,1,4,5,2,1,4, # 6
4,1,5,1,4,1,2,5,4,1, # 7
1,3,1,5,1,3,1,4,5,2, # 8
2,1,4,1,5,1,4,2,1,5 # 9
]
guide_option = rand(5) if guide_option == :transform
# Change the guide for discovery shots according one of rules
@guide = case guide_option
when 1
@guide.reverse # Reverse the whole sequence
when 2
(0..9).map do |l|
@guide[l*10, 10].reverse
end.flatten # Reverse horizontally
when 3
(0..9).map do |col|
(0..9).map { |l| @guide[l*10 + col] }.reverse
end.flatten # Rotate right
when 4
(0..9).map do |col|
(0..9).map { |l| @guide[l*10 + col] }
end.reverse.flatten # Transpose
else @guide
end
end
def next
# Find the maximum element
ship_size = @guide.reject { |p| p == nil }.max || 0
available_positions = @guide.each_with_index.map { |p,i| p == ship_size ? i : nil }.compact
return nil if available_positions.empty?
next_pos = available_positions[rand available_positions.length]
@guide[next_pos] = nil # Do not shot here anymore, been already
to_location next_pos
end
private
def to_location(pos)
"#{(pos % 10 + 65).chr}#{pos / 10 + 1}"
end
end
# The player itslef
class Player
attr_accessor :field
def initialize
@discovery = ShipDiscovery.new(:transform)
@field = {}
@shot_queue = []
end
# Returns the player name as used on the server
def name
File.basename(__FILE__, '.rb')
end
def shootable?(here)
return false unless here
# there are MISSes all around - cannot be the target
# Do not take into account out-of-bounds
around = [here.to_left, here.to_right, here.to_up, here.to_dn].reject { |l| l.nil? }
around = around.map { |l| field[l] } # from locations to the values
return false if !around.empty? && around.all? { |l| l == :miss }
field[here].nil?
end
# Returns next position from the sequence to shot at. Examples: A1, B10, J9
def next_target
# First shot at the priority positions
begin
here = @shot_queue.shift
end while !shootable?(here) && !@shot_queue.empty?
# Then shot according to ship discovery
while !shootable?(here) do
here = @discovery.next
break if here.nil?
end
field[here] = :shot if here
here
end
# Returns ships' positions as per server. Includes only the actual positions with no message.
def allocate_ships
options =[
"5:A1:V 4:J5:V 3:A8:H 3:I1:V 2:!?:H".sub('!', ('C'..'G').sort_by { rand }.first).sub('?', (1..9).sort_by{ rand }.first.to_s),
"5:I1:V 4:H6:V 3:E10:H 3:D7:V 2:!?:V".sub('!', ('A'..'G').sort_by { rand }.first).sub('?', (1..4).sort_by{ rand }.first.to_s),
"5:E3:V 4:A2:V 3:A7:V 3:J5:V 2:!?:V".sub('!', ('G'..'I').sort_by { rand }.first).sub('?', (1..9).sort_by{ rand }.first.to_s),
"5:F10:H 4:A10:H 3:A2:V 3:H1:H 2:!?:H".sub('!', ('C'..'I').sort_by { rand }.first).sub('?', (3..8).sort_by{ rand }.first.to_s),
"5:B1:H 4:J2:V 3:J7:V 3:F10:H 2:A?:V".sub('?', (rand(8)+2).to_s),
"5:F6:H 4:D5:V 3:E4:H 3:F9:H 2:!1:H".sub('!', (rand(9)+65).chr),
"5:F10:H 4:B1:H 3:A2:V 3:A6:V 2:J?:V".sub('?', (rand(7)+1).to_s),
"5:F1:V 4:A6:H 3:H6:H 3:F8:V 2:!?:V".sub('!', ['A','B','C','H','I','J'][rand 6]).sub('?', [1,2,8,9][rand 4].to_s),
]
options[rand options.length]
end
# Receives count
# Returns space delimited positions. Such as "A1 J10"
def shot(count)
shots = (1..count).map do
next_target || 'A1' # Out of shots already, just satisfy min-shots requirement
end * ' '
shots
end
# Expecting string: "A1:hit B10:miss"
# Processes the result, marks the field appropriately, changes the sequence of shots accordingly.
def process_shot_result(shot_result)
shot_result.split(/:|\s/).each_slice(2) do |location, result|
field[location] = result.to_sym
has_hit(location) if field[location] == :hit
end
end
# Callback for an on-target shot
def has_hit(hit)
asap = []
asap.push hit.to_left, hit.to_left.to_left if field[hit.to_right] == :hit
asap.push hit.to_right, hit.to_right.to_right if field[hit.to_left] == :hit
asap.push hit.to_up, hit.to_up.to_up if field[hit.to_dn] == :hit
asap.push hit.to_dn, hit.to_dn if field[hit.to_up] == :hit
if asap.empty?
# Force shooting around
asap = [ hit.to_left,
hit.to_right,
hit.to_up,
hit.to_dn
].reject { |l| l.nil? }
end
asap.each { |l| @shot_queue.push l }
end
end
#
# This line, required by the rules, ensures the server sees our responses as
# soon as they are written.
#
$stdout.sync = true
me = Player.new
# The main loop
ARGF.each_line do |line|
case line
when /\AACTION SHIPS\b/
puts "SHIPS #{me.allocate_ships}"
when /\AACTION SHOTS (\d)/
puts 'SHOTS ' + me.shot($1.to_i)
when /\AINFO SHOTS (\w+) (.+)/
if $1 == me.name
me.process_shot_result $2
end
when /\AACTION FINISH\b/
puts "FINISH"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment