Created
March 8, 2017 23:14
-
-
Save m4scosta/609c4bd9ee4fa7736842d210ece3865b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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