HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

Minesweeper: how many mines are near?

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
howareminesmanynearminesweeper

Problem

Minesweeper in Ruby (from exercism.io):


Write a program that adds the numbers to a minesweeper board.


Minesweeper is a popular game where the user has to find the mines
using numeric hints that indicate how many mines are directly adjacent
(horizontally, vertically, diagonally) to a square.


In this exercise you have to create some code that counts the number
of mines adjacent to a square.


Transforms boards like this (where * indicates a mine):

+-----+
| * * |
|  *  |
|  *  |
|     |
+-----+




into this:

+-----+
|1*3*1|
|13*31|
| 2*2 |
| 111 |
+-----+


As can be seen in the provided test suite, a method Board.transform is required whose input and output are arrays of strings, representing the rows of the board. A ValueError must be raised in case of an invalid character or a non-border cell at the edge of the board.

My solution:

```
class ValueError < RuntimeError
end

class Position
attr_reader :y, :x

def initialize(y, x)
@y, @x = y, x
end

def neighbors
Position.each(y-1 .. y+1, x-1 .. x+1)
end

def self.each(ys, xs)
return enum_for(__method__, ys, xs) unless block_given?

ys.each do |y|
xs.each do |x|
yield Position.new(y, x)
end
end
end

def ==(other)
to_a == other.to_a
end

alias eql? ==

def hash
to_a.hash
end

def to_a
[y, x]
end
end

class Cell
BLANK_CELL_CHAR = ' '
MINE_CELL_CHAR = '*'
BORDER_CELL_REGEX = /\A[-+|]\z/

def self.from_char(char)
case char
when BLANK_CELL_CHAR
BlankCell.new
when MINE_CELL_CHAR
MineCell.new
when BORDER_CELL_REGEX
BorderCell.new(char)
else
raise ValueError, "Illegal character"
end
end

def blank?
false
end

def mine?
false
end

def border?
false
end
end

class BlankCell < Cell
attr_accessor :num_adjacent_mines

def initialize(num_adjacent_mines = nil) # nil means unknown
@num_adjacent_mines = num_adjacent_m

Solution

If you're using some kind of TDD all classes must be test-covered. Adding classes that are not mentioned in tests at least confusing, at most - leads to unnecessary complexity.

What is the difference between Cell, MineCell, Bordercell and BlankCell? They are storing input/output methods but must be responsible for business logic, I think. They are acting like higher level of Position abstraction. BTW, Position.each - good "trick".

Code is readable and easy to understand.

When I looked through once again, I noticed that this part can be improved:

def blank?
  false
end
def mine?
  false
end
def border?
  false
end
######
def blank?
  self.is_a?(BlankCell)
end
def mine?
  self.is_a?(MineCell)
end
def border?
  self.is_a?(BorderCell)
end


This methods definitions can be removed from Cell's subclasses.

My way (written in "least resistance way" with tests):

class ValueError < RuntimeError; end

class Board

  def self.transform(input)
    validate(input)
    out = mark_field(substitute(input[1..-2]))
    to_s(out, input)
  end

private

  class << self
    def mark_field(field)
      field.each.with_index.with_object(field) do |(row, r_indx), obj|
        row.each.with_index do |cell, c_indx|
          increment_cells(r_indx, c_indx, obj) unless cell
        end
      end
    end

    def substitute(input)
      input.map do |line|
        line.gsub(/[^*\s]/, '').chars.map{ |ch| ch == '*' ? nil : 0 }
      end
    end

    def closest_cells(row, col, field)
      arr = (row-1..row+1).flat_map{ |r| [r].product([*(col-1..col+1)])}
      arr.reject{ |r, c| r < 0 || c < 0 || [r, c] == [row, col] || !(field[r] && field[r][c]) }
    end

    def increment_cells(row, col, arr)
      closest_cells(row, col, arr).each { |r, c| arr[r][c] += 1 }
    end

    def to_s(out, input)
      input[1..-2] = out.map do |line|
        "|#{line.map{ |c| c == 0 ? ' ' : c ? c : '*' }.join}|"
      end
      input
    end

    def validate(input)
      raise ValueError unless input.map(&:size).uniq.count == 1 &&
        input.reject{ |line| line.count('|') == 2 }.size == 2 &&
        !input.join.gsub!(/[^*\s\|\+\-]/, '')
    end
  end

end

Code Snippets

def blank?
  false
end
def mine?
  false
end
def border?
  false
end
######
def blank?
  self.is_a?(BlankCell)
end
def mine?
  self.is_a?(MineCell)
end
def border?
  self.is_a?(BorderCell)
end
class ValueError < RuntimeError; end

class Board

  def self.transform(input)
    validate(input)
    out = mark_field(substitute(input[1..-2]))
    to_s(out, input)
  end

private

  class << self
    def mark_field(field)
      field.each.with_index.with_object(field) do |(row, r_indx), obj|
        row.each.with_index do |cell, c_indx|
          increment_cells(r_indx, c_indx, obj) unless cell
        end
      end
    end

    def substitute(input)
      input.map do |line|
        line.gsub(/[^*\s]/, '').chars.map{ |ch| ch == '*' ? nil : 0 }
      end
    end

    def closest_cells(row, col, field)
      arr = (row-1..row+1).flat_map{ |r| [r].product([*(col-1..col+1)])}
      arr.reject{ |r, c| r < 0 || c < 0 || [r, c] == [row, col] || !(field[r] && field[r][c]) }
    end

    def increment_cells(row, col, arr)
      closest_cells(row, col, arr).each { |r, c| arr[r][c] += 1 }
    end

    def to_s(out, input)
      input[1..-2] = out.map do |line|
        "|#{line.map{ |c| c == 0 ? ' ' : c ? c : '*' }.join}|"
      end
      input
    end

    def validate(input)
      raise ValueError unless input.map(&:size).uniq.count == 1 &&
        input.reject{ |line| line.count('|') == 2 }.size == 2 &&
        !input.join.gsub!(/[^*\s\|\+\-]/, '')
    end
  end

end

Context

StackExchange Code Review Q#116132, answer score: 3

Revisions (0)

No revisions yet.