patternrubyMinor
Minesweeper: how many mines are near?
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:
As can be seen in the provided test suite, a method
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
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
Code is readable and easy to understand.
When I looked through once again, I noticed that this part can be improved:
This methods definitions can be removed from
My way (written in "least resistance way" with tests):
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)
endThis 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
endCode 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)
endclass 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
endContext
StackExchange Code Review Q#116132, answer score: 3
Revisions (0)
No revisions yet.