simplay/bofrev

View on GitHub
src/grid.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'game_field'
require 'point2f'
require 'drawable'
require 'drawables/grid_box'
require 'color'

# Grid is the Data Structure for an arbitrary 2d-(M x N) pixel game.
# A grid has actually (M+2)x(N+2) pixels. There is an border around the grid.
#
# Benefits:
#   + That can be used to perform intersection tests.
#     e.g. if hit border pixel, then we detected a collision.
#
#   + Allows to define Gui-pixel resolution independent from number of game cells.
#
# Internal representation is encoded initially as the following:
#
#   B B .. B B
#   B 0 .. 0 B
#   .        .
#   .        .
#   B 0 .. 0 B
#   B B .. B B
#
# where B depicts a border cell,
class Grid < Drawable

  include Enumerable

  # Build a new Grid of Dimension (width x height)
  #
  # @example: Grid.new(8,5).to_s
  #   #=>
  #     2 2 2 2 2 2 2 2 2 2
  #     2 0 0 0 0 0 0 0 0 2
  #     2 0 0 0 0 0 0 0 0 2
  #     2 0 0 0 0 0 0 0 0 2
  #     2 0 0 0 0 0 0 0 0 2
  #     2 0 0 0 0 0 0 0 0 2
  #     2 3 3 3 3 3 3 3 3 2
  #
  # @param width [Integer] width of grid
  # @param height [Integer] height of grid
  def initialize(width, height, show_grid=false, cell_default_type=:free)
    super(Point2f.new, true)
    # assign dimensions
    @inner_width = width
    @inner_height = height

    # Internal data-structure of a the grid.
    @data = build_empty_grid(cell_default_type)

    specify_borders
    encode_grid_neighborhood

    @grid_box = GridBox.new
    @grid_is_shown = show_grid
  end

  # Set the type of all inner game fields equal to a given type.
  #
  # @param given_type [Symbol] a known symbol from GameFieldTypeConstants
  def set_field_types_to(given_type)
    each do |field|
      field.type = given_type
    end
  end

  # Overwrites our game fields with those from other grid.
  #
  # @param other_grid [Grid] other grid we want to use for copying.
  def overwrite_us_with(other_grid)
    inner_height_iter.each do |row_idx|
      other_grid.inner_row_at(row_idx).each_with_index do |other_field, idx|
        set_field_value_at(idx+1, row_idx, other_field.value)
        set_field_color_at(idx+1, row_idx, other_field.color)
        set_field_type_at(idx+1, row_idx, other_field.type)
      end
    end
    nil
  end

  # Iterate over all GameField instances of this Grid and apply a given block.
  def each(&block)
    inner_height_iter.each do |row_idx|
      inner_row_at(row_idx).each_with_index do |other_field, idx|
        field = field_at(idx+1, row_idx)
        if block_given?
          block.call field
        else
          yield field
        end
      end
    end
  end

  # Draw this Grid onto a given canvas.
  #
  # @hint: The GameField instances of this Grid are drawn sequentiually.
  #   The grid box is only drawn, if the flag @frid_is_shown is true.
  # @param canvas [Canvas] draw onto this canvas.
  def draw_onto(canvas)
    each do |field|
      field.draw_onto(canvas)
    end
    @grid_box.draw_onto(canvas) if @grid_is_shown
  end

  # Get total width of grid that is the 2 Border pixels
  # plus M from the grid kernel (sides).
  #
  # @return [Integer] total width of grid - kernel + border
  def total_width
    @inner_width + 2
  end

  # Get total height of grid that is the 2 Border pixels
  # plus N from the grid kernel (floor & ceil).
  #
  # @return [Integer] total height of grid
  def total_height
    @inner_height + 2
  end

  # Grid height without border (top and bottom border).
  #
  # @hint: corresponds to Grid#total_height - 2
  # @return [Integer] inner height of grid.
  def inner_height
    @inner_height
  end

  # Grid width without border (left and right border).
  #
  # @hint: corresponds to Grid#total_width - 2
  # @return [Integer] inner width of grid.
  def inner_width
    @inner_width
  end

  # Height cell Range of grid's inner cells
  # (i.e. grid without border cells).
  # @return [Range] of inner height indices in grid.
  def inner_height_iter
    (1..@inner_height)
  end

  # Width cell Range of grid's inner cells
  # (i.e. grid without border cells).
  # @return [Range] of inner width indices in grid.
  def inner_width_iter
    (1..@inner_width)
  end

  # Get row at given index.
  # @param idx [Integer] row index
  # @hint starts counting at 0 and ends at @grid.size-1
  def row_at(idx)
    @data[idx]
  end

  # Get row :idx at given index excluding its border cells.
  #   @param idx [Integer] row index
  #   @hint starts counting at 1 and ends at @grid.size-2
  def inner_row_at(idx)
    row_at(idx)[1..-2]
  end

  # Get game field at position (x,y) in MxN grid.
  #
  # @param x [Integer] row index
  # @param y [Integer] column index
  # @return [GameField] at given (x,y) location.
  def field_at(x, y)
    # NB: lookup is inverted, since array is build internally this way:
    # see Grid#build_empty_grid
    @data[y][x]
  end

  # Assign a GameField field at a given location (x,y) in the grid.
  #
  # @param x [Integer] row index
  # @param y [Integer] column index
  # @param field [GameField] game field used for update
  def set_field_at(x, y, field)
    @data[y][x] = field
  end

  # Update a whole inner row bz an array of game fields.
  # Assumption: Dimensionality of given field array matches inner row length
  # and the order of the elements in the array corresponds to the row.
  #
  # @param row_idx inner row index
  # @param fields [Array] of GameField instances we use
  #   for updating the target row.
  def set_inner_row_at(row_idx, fields)
    fields.each_with_index do |field, col_idx|
      set_field_at(col_idx+1, row_idx, field)
    end
  end

  # Assign a new game color at a given field location (x,y) in the grid.
  #
  # @param x [Integer] row index
  # @param y [Integer] column index
  # @param color [Color] game field color
  def set_field_color_at(x, y, color)
    @data[y][x].color = color
  end

  # Assign a new game type at a given field location (x,y) in the grid.
  #
  # @hint: See GameFieldTypeConstants for a list of all types.
  # @param x [Integer] row index
  # @param y [Integer] column index
  # @param type [Symbol] now game field state
  def set_field_type_at(x, y, type)
    @data[y][x].type = type
  end

  # Assign a new game color at a given field location (x,y) in the grid.
  #
  # @param x [Integer] row index
  # @param y [Integer] column index
  # @param value [Integer] game field value
  def set_field_value_at(x, y, value)
    @data[y][x].value = value
  end

  # Wipe out the GameField at location (x,y).
  #
  # @hint: Wipe out means, set the type and color
  #   of the target GameField to its default values.
  # @param x [Integer] row index
  # @param y [Integer] column index
  def flush_field_at(x, y)
    field_at(x,y).wipe_out
  end

  # Pretty String representation of this Grid.
  #
  # @hint: Encode its GameField instances as numbers.
  #   See GameField#to_i
  # @return [String] Matrix form of grid encoded as string.
  def to_s
    grid_as_string = ''
    @data.each do |row|
      row.each do |field|
        grid_as_string += "#{field.to_i} "
      end
      grid_as_string += "\n"
    end
    grid_as_string
  end

  protected

  # Create an empty game grid cells
  #
  # @hint: (1..4).map do (1..2).map do 1 end end
  #   #=> [[1, 1], [1, 1], [1, 1], [1, 1]]
  #
  # @return [Array[Array]] an array of arrays encoding the game grid.
  def build_empty_grid(type = :free)
    @data = (1..total_height).map do |idx|
      (1..total_width).map do |idy|
        GameField.new(Color.white, type, Point2f.new(idx-1, idy-1))
      end
    end
  end

  # Mark Grid Borders as special pixels
  def specify_borders
    # setup y border
    total_width.times do |idy|
      set_field_at(idy, 0, GameField.new(Color.black, :border))
      set_field_at(idy, total_height-1, GameField.new(Color.black, :ground_border))
    end

    # setup x border
    total_height.times do |idx|
      set_field_at(0, idx, GameField.new(Color.black, :border))
      set_field_at(total_width-1, idx, GameField.new(Color.black, :border))
    end
  end

  # Encode 4-Neighborhood information to every game field (except borders).
  def encode_grid_neighborhood

    # only iterate over kernel: we do not care about border cells
    inner_height_iter.each do |idy|
      inner_width_iter.each do |idx|
        cell = field_at(idx, idy)
        neighbors = {
            :right => field_at(idx+1, idy), :left => field_at(idx-1, idy),
            :bottom => field_at(idx, idy+1), :top => field_at(idx, idy-1),
            :top_left => field_at(idx-1, idy-1), :top_right => field_at(idx+1, idy-1),
            :bottom_left => field_at(idx-1, idy+1), :bottom_right => field_at(idx+1, idy+1)
        }
        cell.assign_neighborhood(neighbors)
      end
    end
  end

end