Skip to content

Instantly share code, notes, and snippets.

@avdgaag
Created August 29, 2012 18:54
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 avdgaag/3517092 to your computer and use it in GitHub Desktop.
Save avdgaag/3517092 to your computer and use it in GitHub Desktop.
Solution to minefield code kata
class SurroundingRange
attr_reader :point, :field
def initialize(point, field)
@point, @field = point, field
end
def points
horizontal = [point.x - 1, 0].max..[point.x + 1, field.width - 1].min
vertical = [point.y - 1, 0].max..[point.y + 1, field.height - 1].min
horizontal.to_a.product(vertical.to_a).map { |x,y| Point.new(x,y) }.reject { |p| p === point }
end
end
class Point < Struct.new(:x, :y)
def ===(other)
other.x == self.x && other.y == self.y
end
end
class Field
BOMB = '*'
attr_reader :rows, :width, :height
def initialize(width, height)
@width, @height = width, height
@rows = []
end
def map
output = []
rows.each_with_index do |row, x|
line = ''
row.each_with_index do |char, y|
p = Point.new(x, y)
line << (bomb?(p) ? '*' : bombs_surrounding(p).to_s)
end
output << line
end
output.join("\n")
end
def bombs_surrounding(point)
range_around(point).select { |p| bomb?(p) }.count
end
def range_around(point)
SurroundingRange.new(point, self).points
end
def to_s
map
end
def <<(row)
@rows << row
self
end
def at(point)
rows.fetch(point.x).fetch(point.y)
end
def bomb?(point)
at(point) == BOMB
end
end
class InputReader
attr_reader :input
def initialize(input)
@input = input
end
def fields
fields = []
input.each_line do |line|
break if line.start_with?('0 0')
if line =~ /(\d+) (\d+)/
fields << Field.new($1.to_i, $2.to_i)
next
end
fields.last << line.chomp.split('')
end
fields
end
end
class Minesweeper
attr_reader :fields
def initialize(input)
@fields = InputReader.new(input).fields
end
def maps
i = 1
@fields.inject([]) do |output, field|
output << "Field ##{i}\n#{field.map}\n"
i += 1
output
end.join("\n")
end
end
describe SurroundingRange do
let(:field) { double width: 10, height: 10 }
subject { described_class.new(point, field) }
context 'when in the middle of a field' do
let(:point) { double x: 4, y: 4 }
it { should have(8).points }
its('points.first') { should be_kind_of(Point) }
end
context 'when in the horizontal edge of a field' do
let(:point) { double x: 0, y: 0 }
it { should have(3).points }
end
end
describe Field do
it 'finds a value by coordinates' do
field = described_class.new(2, 4) << %w[1 2 3 4] << %w[. a * b]
field.at(Point.new(0, 0)).should == '1'
field.at(Point.new(1, 2)).should == '*'
end
it 'knows when a square is a bomb' do
field = described_class.new(1, 4) << %w[. . * .]
field.bomb?(Point.new(0, 1)).should be_false
field.bomb?(Point.new(0, 2)).should be_true
end
it 'can count the number of bombs around a point' do
field = described_class.new(2, 4) << %w[. . * .] << %w[. . . *]
field.bombs_surrounding(Point.new(0, 0)).should == 0
field.bombs_surrounding(Point.new(0, 2)).should == 1
field.bombs_surrounding(Point.new(0, 3)).should == 2
end
it 'can generate a map with bomb counts' do
field = described_class.new(2, 4) << %w[. . * .] << %w[. . . *]
field.map.should == "01*2\n012*"
end
it 'can generate a map for a simpe field' do
field = described_class.new(1, 1) << %w[.]
field.map.should == "0"
end
end
describe InputReader do
subject { described_class.new(input) }
context 'without input' do
let(:input) { '' }
it { should have(0).fields }
end
context 'with just the input ending marker' do
let(:input) { '0 0' }
it { should have(0).fields }
end
context 'with a single field' do
let(:input) { "1 1\n.\n0 0" }
it { should have(1).fields }
end
context 'with multiple fields' do
let(:input) { "1 1\n.\n2 2\n..\n..\n0 0" }
it { should have(2).fields }
end
end
describe Minesweeper do
subject { described_class.new(input) }
context 'without input' do
let(:input) { '' }
its(:maps) { should == '' }
end
context 'with just the input ending marker' do
let(:input) { '0 0' }
its(:maps) { should == '' }
end
context 'with a single field' do
let(:input) { "1 1\n.\n0 0" }
its(:maps) { should == "Field #1\n0\n" }
end
context 'with multiple fields' do
let(:input) { "1 1\n.\n2 2\n..\n..\n0 0" }
its(:maps) { should == "Field #1\n0\n\nField #2\n00\n00\n" }
end
end
@avdgaag
Copy link
Author

avdgaag commented Aug 29, 2012

This reads IO input as long as it can find a new line of two numbers (greater than 0). If it does, it reads another set of lines (the number of the first number it found). This gets added to a Field object.

The field object knows its dimensions, its index in the list of all fields, and it can look up the value of any square in the field by using two coordinates x and y. It can then create a minesweeper map by looping every line and character and printing the number of bombs surrounding that square by generating a range of coordinates around a given set of coordinates and inspecting their values.

@avdgaag
Copy link
Author

avdgaag commented Aug 31, 2012

Same approach rewritten using TDD!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment