Skip to content

Instantly share code, notes, and snippets.

@jadon1979
Created April 19, 2024 17:06
Show Gist options
  • Select an option

  • Save jadon1979/183dca7a80060cfdd8f4fbac76e689bb to your computer and use it in GitHub Desktop.

Select an option

Save jadon1979/183dca7a80060cfdd8f4fbac76e689bb to your computer and use it in GitHub Desktop.
A code test for a working command line version of Battleship.
require_relative './ship.rb'
# Represents the game board
class Board
FLEET = {
carrier: 5,
battleship: 4,
cruiser: 3,
submarine: 2,
destroyer: 2
}.freeze
BOARD_SYMBOLS = {
water: '-',
miss: 'x',
hit: '!'
}.freeze
attr_accessor :ships
# Initializes and generates a new game board with a specified board size.
#
# @param board_size [Integer] the size of the game board.
def initialize(grid_size)
@grid_size = grid_size
@grid = Array.new(@grid_size * @grid_size, BOARD_SYMBOLS[:water])
@ships = Array.new(FLEET.length)
@total_sunk = 0
generate
end
# Display the game board's current state
def display
letters = grid_letters.to_a[0...@grid_size]
puts column_headers
@grid.each_slice(@grid_size) { |row| puts "#{letters.shift[0]}: #{row.join(' ')}" }
end
# Fires at the game board with the specified coordinates.
#
# @param coord [String] the coordinates to fire at (e.g., "A1").
def fire(coord)
row = grid_letters[coord.downcase[0]]
col = coord[1].to_i - 1
# Make sure that the coords are not off the grid
return unless row && col && (row >= 0 && row < @grid_size) && (col >= 0 && col < @grid_size)
# Get the element at the translated grid coordinate
pos = @grid[row * @grid_size + col]
if [ BOARD_SYMBOLS[:water], BOARD_SYMBOLS[:miss] ].include?(pos)
puts "MISS!"
@grid[row * @grid_size + col] = BOARD_SYMBOLS[:miss]
else
ships[pos].hit
@total_sunk += 1 if ships[pos].sunk?
@grid[row * 10 + col] = BOARD_SYMBOLS[:hit]
end
end
# Check if all ships have been sunk.
def fleet_destroyed?
@total_sunk >= FLEET.length
end
private
# Iterate the Fleet and attempt to add the ships
def generate
FLEET.each_with_index do |(ship, size), index|
loop do
coords = random_coords(size)
direction = [ :horizontal, :vertical ].sample
next unless valid_coords?(coords, direction, size)
add_ship(ship, coords, direction, index)
break;
end
end
end
# Checks if the coordinates are valid.
#
# @param coords [Array<Integer>] the row and column indices
# @param direction [Symbol] the direction to check the coordinates.
# @param size [Integer] the size of the ship (number of cells it occupies).
# @return [Boolean] true if the coordinates are valid, otherwise false.
def valid_coords?(coords, direction, size)
starts, ends = coords
size.times do |i|
row, col = next_coords(direction, starts, ends, i)
return false if row >= @grid_size || col >= @grid_size || @grid[row * @grid_size + col] != BOARD_SYMBOLS[:water]
end
true
end
# Add the ship to it's proper indices.
#
# @param coords [Symbol] the key name of the ship in FLEET
# @param coords [Array<Integer>] the row and column indices
# @param direction [Symbol] the direction to check the coordinates.
# @param [Integer] the index of the ship
def add_ship(ship, coords, direction, index)
starts, ends = coords
size = FLEET[ship]
size.times do |i|
row, col = next_coords(direction, starts, ends, i)
@grid[row * @grid_size + col] = index
end
@ships[index] = Ship.new(ship, size)
end
# Adjust the row or column index per direction
#
# @param direction [Symbol] the direction to point the coordinates.
# @param starts [Integer] the row index
# @param ends [Integer] the column index
# @param index [Integer] the index to add to the coord
# @return [Array<Integer>] The index adjusted coordinates
def next_coords(direction, starts, ends, index)
direction == :horizontal ? [starts, ends + index] : [starts + index, ends]
end
# Setup the column headers for the game board
# @return [String] the formated column headers
def column_headers
@column_headers ||= " #{(1...@grid_size + 1).to_a.join(' ')}"
end
# Setup the row references to allow for letter and integer combinations
# @return [Hash] A hash of letters with their associated index
def grid_letters
@grid_letters ||= ('a'...'z').each_with_index.to_h
end
# Return random coordinates for ship placement
#
# @param size [Integer] the range to generate the coordinates
# @return [Array<Integer>] The Randomly generated coordinates per size
def random_coords(size)
[rand(@grid_size), rand(@grid_size - size + 1)]
end
end
require_relative './player.rb'
# Represents the game. Manages the flow of the game and players' turns.
class Game
attr_reader :player1, :player2
# Initializes a new game with two players and a specified board size.
#
# @param board_size [Integer] the size of the game board.
def initialize(board_size = 10)
@player1 = Player.new("Player 1", board_size)
@player2 = Player.new("Player 2", board_size)
@current_player = @player1
@opponent_player = @player2
end
# Starts the game loop. Players take turns firing at each other's boards until one player wins.
def play
loop do
take_turn(@current_player, @opponent_player)
break if game_over?
switch_players
end
puts "Game over!"
end
private
# Executes a single turn in the game.
#
# @param current_player [Player] the player taking the turn.
# @param opponent_player [Player] the opponent player.
def take_turn(current_player, opponent_player)
current_player.display_board
opponent_player.display_board
puts "#{current_player.name}, enter coordinates to fire:"
coord = gets.chomp
current_player.fire(opponent_player.board, coord)
end
# Switches the current player with the opponent player.
def switch_players
@current_player, @opponent_player = @opponent_player, @current_player
end
# Checks if the game is over (i.e., if one player's ships are all sunk).
#
# @return [Boolean] true if the game is over, otherwise false.
def game_over?
@player1.board.fleet_destroyed? || @player2.board.fleet_destroyed?
end
end
require_relative './board.rb'
# Represents a player in the game. Manages the player's name, game board, and firing actions.
class Player
attr_reader :name, :board
# Initializes a new player with the specified name and creates a new game board.
#
# @param name [String] the name of the player.
# @param board_size [Integer] the size of the game board.
def initialize(name, board_size)
@name = name
@board = Board.new(board_size)
end
# Fires at the opponent's game board with the specified coordinates.
#
# @param opponent_board [Board] the opponent's game board.
# @param coord [String] the coordinates to fire at (e.g., "A1").
def fire(opponent_board, coord)
opponent_board.fire(coord)
end
# Displays the player's own game board.
def display_board
puts "#{name}'s Board:"
board.display
end
end
# Represents a ship piece for the board
class Ship
attr_reader :name, :size
attr_accessor :hits
# Initializes a new ship with the specified name and size.
#
# @param name [Symbol] the name of the ship.
# @param size [Integer] the size of the ship (number of cells it occupies).
def initialize(name, size)
@name = name
@size = size
@hits = 0
end
# Registers a hit on the ship.
def hit
puts "HIT!"
@hits += 1
puts "You sank my #{name}" if sunk?
end
# Checks if the ship has been sunk (all cells hit).
#
# @return [Boolean] true if the ship has been sunk, otherwise false.
def sunk?
hits >= size
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment