Skip to content

Instantly share code, notes, and snippets.

@m4scosta
Created March 8, 2017 23:14
Show Gist options
  • Save m4scosta/609c4bd9ee4fa7736842d210ece3865b to your computer and use it in GitHub Desktop.
Save m4scosta/609c4bd9ee4fa7736842d210ece3865b to your computer and use it in GitHub Desktop.
require 'matrix'
require 'set'
# Minesweeper board cell
class Cell
attr_accessor :bomb, :clicked, :flagged, :neighbor_bombs
def initialize(x, y, bomb = false, neighbor_bombs = 0)
@x = x
@y = y
@neighbor_bombs = neighbor_bombs
@bomb = bomb
@clicked = false
@flagged = false
end
def click
@clicked = true
end
def neighbors_coords
yield @x - 1, @y - 1
yield @x - 1, @y
yield @x - 1, @y + 1
yield @x, @y + 1
yield @x + 1, @y + 1
yield @x + 1, @y
yield @x + 1, @y - 1
yield @x, @y - 1
end
def state(xray: false)
if xray || @clicked
state_revealed
else
state_not_revealed
end
end
def state_revealed
return :bomb if @bomb
return :covered unless @clicked
neighbor_bombs? ? @neighbor_bombs : :clear
end
def state_not_revealed
return :flag if @flagged
:covered
end
def neighbor_bombs?
@neighbor_bombs > 0
end
end
# Cell alias to facilitate bomb creation
class Bomb < Cell
def initialize(x, y)
super(x, y, bomb: true)
end
end
# Minesweeper board
class Board
attr_accessor :width, :height, :bombs, :total_cells, :clear_cells
attr_reader :random_bombs
def initialize(width, height, bombs)
@width = width
@height = height
@bombs = bombs
@total_cells = width * height
@clear_cells = @total_cells - bombs
@random_bombs = generate_bombs(bombs, @total_cells)
@matrix = generate_random_matrix
end
def generate_bombs(nbombs, maxindex)
numbers = Set.new
loop do
return numbers if numbers.size == nbombs
numbers << rand(maxindex)
end
end
def generate_random_matrix
Array.new(width) do |x|
Array.new(height) do |y|
cell = bomb?(x, y) ? Bomb.new(x, y) : Cell.new(x, y)
cell.neighbor_bombs = count_neighbor_bombs(cell)
cell
end
end
end
def count_neighbor_bombs(cell)
bombs = 0
cell.neighbors_coords do |x, y|
bombs += 1 if bomb?(x, y)
end
bombs
end
def bomb?(x, y)
return false unless inside_board(x, y)
linear_index = x * @width + y
@random_bombs.include?(linear_index)
end
def inside_board(x, y)
x >= 0 && y >= 0 && x <= width - 1 && y <= height - 1
end
def get_cell(x, y)
@matrix[x][y] if inside_board(x, y)
end
def state(xray: false)
@matrix.clone.map do |row|
row.map do |cell|
cell.state(xray: xray)
end
end
end
end
# Minesweeper game engine
class MinesweeperEngine
def initialize(board)
@board = board
@clear_cells_clicked = 0
@bomb_clicked = false
end
def play(x, y)
x -= 1
y -= 1
cell = @board.get_cell(x, y)
return false unless valid_click?(cell)
click_cell(cell)
true
end
def valid_click?(cell)
!(@bomb_clicked || cell.nil? || cell.clicked || cell.flagged)
end
def click_cell(cell)
cell.click
if cell.bomb
@bomb_clicked = true
else
@clear_cells_clicked += 1
click_neighbors_expanding(cell) unless cell.neighbor_bombs?
end
end
def click_neighbors_expanding(clicked_cell)
clicked_cell.neighbors_coords do |x, y|
cell = @board.get_cell(x, y)
unless cell.nil? || cell.clicked || cell.bomb || cell.flagged
cell.click
@clear_cells_clicked += 1
click_neighbors_expanding(cell)
end
end
end
def flag(x, y)
x -= 1
y -= 1
cell = @board.get_cell(x, y)
return false unless valid_flag?(cell)
cell.flagged = !cell.flagged
true
end
def valid_flag?(cell)
!cell.nil? && !cell.clicked
end
def still_playing?
!victory? && !@bomb_clicked
end
def victory?
@clear_cells_clicked == @board.clear_cells
end
def board_state(xray = false)
@board.state(xray: xray && !still_playing?)
end
end
def create_engine(width, height, bombs)
board = Board.new(width, height, bombs)
MinesweeperEngine.new(board)
end
require './engine'
RSpec.describe MinesweeperEngine, '#play' do
context 'when a valid cell is clicked' do
it 'should return true' do
engine = create_engine(5, 5, 0)
expect(engine.play(1, 1)).to be true
end
end
context 'when an invalid cell is clicked' do
it 'should return false' do
engine = create_engine(5, 5, 5)
expect(engine.play(100, 11)).to be false
expect(engine.play(-1, -1)).to be false
end
end
context 'when a valid cell is clicked twice' do
it 'should return false' do
engine = create_engine(5, 5, 5)
engine.play(1, 1)
expect(engine.play(1, 1)).to be false
end
end
context 'when a bomb is clicked' do
it 'should return false for any other moves' do
engine = create_engine(5, 5, 25)
engine.play(1, 1)
expect(engine.play(1, 1)).to be false
expect(engine.play(2, 2)).to be false
expect(engine.play(3, 3)).to be false
end
end
end
RSpec.describe MinesweeperEngine, '#flag' do
context 'when a valid cell is flagged' do
it 'should return true' do
board = Board.new(5, 5, 5)
engine = MinesweeperEngine.new(board)
expect(engine.flag(1, 1)).to be true
expect(board.get_cell(0, 0).flagged).to be true
end
end
context 'when a valid cell is flagged twice' do
it 'should return true' do
engine = create_engine(5, 5, 5)
expect(engine.flag(1, 1)).to be true
expect(engine.flag(1, 1)).to be true
end
end
context 'when a clicked cell is flagged' do
it 'should return false' do
engine = create_engine(5, 5, 5)
engine.play(1, 1)
expect(engine.flag(1, 1)).to be false
end
end
context 'when an invalid cell is flagged' do
it 'should return false' do
engine = create_engine(5, 5, 5)
engine.play(6, 6)
expect(engine.flag(6, 6)).to be false
end
end
end
RSpec.describe MinesweeperEngine, '#still_playing?' do
context 'when player has clicked all clear cells' do
it 'should return false' do
engine = create_engine(1, 1, 0)
engine.play(1, 1)
expect(engine.still_playing?).to be false
end
end
context 'when player has clicked a bomb' do
it 'should return false' do
engine = create_engine(1, 1, 1)
engine.play(1, 1)
expect(engine.still_playing?).to be false
end
end
end
RSpec.describe MinesweeperEngine, '#victory?' do
context 'when player has clicked all clear cells' do
it 'should return true' do
engine = create_engine(1, 1, 0)
engine.play(1, 1)
expect(engine.victory?).to be true
end
end
end
# rubocop:disable BlockLength
RSpec.describe MinesweeperEngine, '#board_state' do
context 'when game not finised' do
it 'should :flag for flagged cell' do
engine = create_engine(2, 2, 1)
engine.flag(1, 1)
board_state = engine.board_state
expect(board_state[0][0]).to be :flag
end
end
context 'when game not finished' do
it 'should :covered for non clicked cell' do
engine = create_engine(1, 1, 0)
expect(engine.board_state[0][0]).to be :covered
end
end
context 'when game finished' do
it 'should :clear for clear cell' do
engine = create_engine(1, 1, 0)
engine.play(1, 1)
board_state = engine.board_state(xray: true)
expect(board_state[0][0]).to be :clear
end
end
context 'when game not finised' do
it 'should :covered for non clicked bomb' do
engine = create_engine(1, 1, 1)
board_state = engine.board_state
expect(board_state[0][0]).to be :covered
end
end
context 'when game not finised' do
it 'should :covered for clicked bomb' do
engine = create_engine(1, 1, 1)
board_state = engine.board_state
expect(board_state[0][0]).to be :covered
end
end
context 'when game finised' do
it 'should :bomb for bomb cell' do
engine = create_engine(1, 1, 1)
engine.play(1, 1)
board_state = engine.board_state
expect(board_state[0][0]).to be :bomb
end
end
context 'when game not finised and xray is passed' do
it 'should :covered for bomb cell' do
engine = create_engine(1, 1, 1)
board_state = engine.board_state(xray: true)
expect(board_state[0][0]).to be :bomb
end
end
context 'when game finised and xray is passed' do
it 'should :covered for unclicked clear cell' do
board = Board.new(1, 2, 1)
engine = MinesweeperEngine.new(board)
clear_cell_x = 0
clear_cell_y = 0
if board.get_cell(0, 0).bomb
clear_cell_y = 1
engine.play(1, 1)
else
engine.play(1, 2)
end
board_state = engine.board_state(xray: true)
expect(board_state[clear_cell_x][clear_cell_y]).to be :covered
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment