Skip to content

Instantly share code, notes, and snippets.

@mickstevens
Forked from JoshCheek/minesweeper.rb
Created February 26, 2017 19:29
Show Gist options
  • Save mickstevens/6d93028ef53d1836bb43d747559cb13f to your computer and use it in GitHub Desktop.
Save mickstevens/6d93028ef53d1836bb43d747559cb13f to your computer and use it in GitHub Desktop.
Mine Sweeper
# vid @ https://twitter.com/josh_cheek/status/835884161047080960
# and @ https://vimeo.com/205773556
require 'graphics'
class MineSweeper
class Cell
attr_reader :x, :y
def initialize(mine:, clicked:, marked:, x:, y:)
@x, @y, @mine, @clicked, @marked = x, y, mine, clicked, marked
end
def mine?
@mine
end
def clicked?
@clicked
end
def marked?
@marked
end
def with(overrides)
self.class.new x: x, y: y, mine: mine?,
clicked: clicked?, marked: marked?,
**overrides
end
def clickable?
!marked? && !clicked?
end
def toggle_mark_if_possible
return self if clicked?
with marked: !marked?
end
end
attr_reader :w, :h
attr_reader :num_mines
def initialize(w, h)
self.w, self.h, self.num_mines = w, h, 0
self.board = Array.new h do |y|
Array.new w do |x|
Cell.new x: x, y: y, mine: false, marked: false, clicked: false
end
end
end
def add_mine(x, y)
raise "Already a mine at (#{x}, #{y})" if mine? x, y
board[y][x] = board[y][x].with(mine: true)
self.num_mines += 1
self
end
def mine?(x, y)
coords_are_valid! x, y
board[y][x].mine?
end
def click(x, y)
coords_are_valid! x, y
cell = board[y][x]
return unless cell.clickable?
spread cell
board[y][x] = cell.with clicked: true
self
end
def mark(x, y)
coords_are_valid! x, y
board[y][x] = board[y][x].toggle_mark_if_possible
self
end
def each
return to_enum :each unless block_given?
board.each_with_index do |row, y|
row.each_with_index do |cell, x|
yield x, y, cell.clicked?, cell.marked?, cell.mine?, count(x, y)
end
end
end
def over?
won? || lost?
end
def lost?
board.any? { |row| row.any? { |cell| cell.clicked? && cell.mine? } }
end
def won?
board.all? { |row| row.all? { |cell| cell.clicked? != cell.mine? } }
end
private
attr_reader :board
attr_writer :w, :h, :num_mines, :board
def spread(cell)
return if cell.clicked?
return if cell.mine?
board[cell.y][cell.x] = cell.with clicked: true
neighbours_of(cell.x, cell.y, false).each { |n| spread board[n.y][n.x] }
end
def coords_are_valid!(x, y)
return if valid_coords? x, y
raise "Coords (#{x}, #{y}) are not on the grid! (0...#{w}, 0...#{h}), excluding upper bounds"
end
def valid_coords?(x, y)
0 <= x && 0 <= y && x < w && y < h
end
def count(x, y)
neighbours_of(x, y, true).count &:mine?
end
def neighbours_of(x, y, include_diagonals)
neighbour_coords_of(x, y, include_diagonals).map { |nx, ny| board[ny][nx] }
end
def neighbour_coords_of(x, y, include_diagonals)
coords_around(x, y, include_diagonals).select { |nx, ny| valid_coords? nx, ny }
end
def coords_around(x, y, include_diagonals)
coords = [[x, y+1], [x-1, y], [x+1, y], [x, y-1]]
if include_diagonals
coords << [x-1, y+1]
coords << [x+1, y+1]
coords << [x-1, y-1]
coords << [x+1, y-1]
end
coords
end
end
class MineSweeperDisplay < Graphics::Simulation
# Based on http://www.crisgdwrites.com/wp-content/uploads/2016/06/minesweeper_tiles.jpg
# there's also this, if that isn't sufficient http://www.freeminesweeper.org/welcome.php
BG_GRAY = [192, 192, 192]
HIGHLIGHT = [255, 255, 255]
SHADOW = [128, 128, 128]
MINE_RED = [255, 0, 0]
ONE = [ 66, 0, 255]
TWO = [ 0, 136, 0]
THREE = [255, 0, 0]
FOUR = [ 29, 0, 130]
FIVE = [140, 0, 0]
SIX = [ 0, 132, 131]
SEVEN = [ 0, 0, 0]
EIGHT = [128, 128, 128]
attr_accessor :side_length, :border_width, :minesweeper, :cells, :to_draw
def initialize(side_length, minesweeper)
self.side_length = side_length
self.border_width = 2
self.minesweeper = minesweeper
super minesweeper.w*side_length, minesweeper.h*side_length, 24
color.default_proc = -> h, k { k }
self.font = find_font 'Verdana Bold', 3*side_length/4 # looks the same as Tahoma bold to me
self.cells = []
self.to_draw = []
to_draw << -> { clear SHADOW }
minesweeper.each do |x, y, is_clicked, is_marked, is_mine, count|
cell = Cell.new(self, x, y, side_length, border_width, is_clicked, is_marked, is_mine, count)
cells[y] ||= []
cells[y][x] = cell
to_draw << cell
end
end
def handle_event(event, n)
minesweeper.over? || case event
when SDL::Event::Mousedown
# #<SDL::Event::Mousedown:0x007ff252120f38 @button=1, @press=true, @x=281, @y=237>
when SDL::Event::Mouseup
# #<SDL::Event::Mouseup:0x007ff25212b6b8 @button=1, @press=false, @x=281, @y=237>
x = event.x
y = h-event.y-1
cell = cells.flatten.find { |c| c.cover? x, y }
if cell && event.button == 1 # left click
minesweeper.click(cell.x, cell.y)
elsif cell # right click
minesweeper.mark(cell.x, cell.y)
end
minesweeper.each do |x, y, is_clicked, is_marked, is_mine, count|
cell = cells[y][x]
next if cell.clicked? == is_clicked &&
cell.mine? == is_mine &&
cell.marked? == is_marked &&
cell.count == count
cell = Cell.new self, x, y, side_length, border_width, is_clicked, is_marked, is_mine, count
cells[y][x] = cell
to_draw << cell
end
if minesweeper.won?
# to_draw << lambda { text "You win!", w/2-100, h/2, :black }
elsif minesweeper.lost?
to_draw << method(:display_mines)
end
end
super
end
def draw(*)
to_draw.shift.call while to_draw.any?
end
def display_mines
to_draw << lambda do
cells.each do |row|
row.each { |cell| cell.draw_mine if cell.mine? }
end
end
end
class Cell
# x and y are the cell indexes
attr_reader :canvas, :x, :y, :side, :border, :clicked, :marked, :mine, :count
alias clicked? clicked
alias marked? marked
alias mine? mine
def initialize(canvas, x, y, side, border, clicked, marked, mine, count)
@canvas = canvas
@x, @y, @side, @border = x, y, side, border
@clicked, @marked, @mine, @count = clicked, marked, mine, count
end
def inspect
"#<Cell @ (#{x}, #{y})#{' mine' if mine?}#{' clicked' if clicked?} #{' marked' if marked?} #{count}neighbours>"
end
def cover?(pixel_x, pixel_y)
l <= pixel_x && pixel_x <= r &&
b <= pixel_y && pixel_y <= t
end
def call
if !clicked? && !marked?
draw_raised
elsif marked?
draw_marked
elsif mine?
draw_mine
else
draw_clicked
end
end
def draw_raised
fill_bg BG_GRAY
border.times do |offset|
canvas.line *left(offset), HIGHLIGHT
canvas.line *top(offset), HIGHLIGHT
canvas.line *right(offset), SHADOW
canvas.line *bottom(offset), SHADOW
end
end
def draw_marked
draw_raised
canvas.rect l+side*0.2, b+side*0.1, side*0.6, side*0.1, :black, true # base
canvas.rect l+side*0.4, b+side*0.1, side*0.2, side*0.2, :black, true # pedestal
canvas.rect l+side*0.475, b+side*0.1, side*0.075, side*0.7, :black, true # pole
canvas.rect l+side*0.2, b+side*0.6, side*0.35, side*0.3, :red, true # flag
end
def draw_mine
fill_bg MINE_RED
border.times do |offset|
canvas.line *left(offset), SHADOW
canvas.line *top(offset), SHADOW
end
canvas.circle l+side/2, b+side/2, side*3/10, :black, true
canvas.rect l+side*0.45, b+side*0.1, side*0.1, side*0.8, :black, true
canvas.rect l+side*0.1, b+side*0.45, side*0.8, side*0.1, :black, true
canvas.circle l+2*side/5, b+3*side/5, side/15, :white, true
end
def draw_clicked
fill_bg BG_GRAY
border.times do |offset|
canvas.line l(offset), b, l(offset), t, SHADOW
canvas.line l, t(offset), r, t(offset), SHADOW
end
color = num_color
string = count.to_s
surface = canvas.font.render canvas.screen, string, color
x = l + (side-surface.w)/2
y = b + (side-surface.h)/2
canvas.text string, x, y, color
end
def fill_bg(color)
canvas.rect l, b, r-l, t-b, color, true
end
def left(o) [l(o), b(o), l(o), t(o)] end
def right(o) [r(o), b(o), r(o), t(o)] end
def top(o) [l(o), t(o), r(o), t(o)] end
def bottom(o) [l(o), b(o), r(o), b(o)] end
# Graphics starts at bottom left, so a higher y is at the top
def l(o=0) side*x + o + 1 end
def b(o=0) side*y + o + 1 end
def r(o=0) side*(x+1) - o end
def t(o=0) side*(y+1) - o end
def num_color(num=count)
case num
when 0 then BG_GRAY
when 1 then ONE
when 2 then TWO
when 3 then THREE
when 4 then FOUR
when 5 then FIVE
when 6 then SIX
when 7 then SEVEN
when 8 then EIGHT
else raise num.inspect
end
end
end
end
ms = MineSweeper.new(25, 20)
msd = MineSweeperDisplay.new 40, ms
200.times do
loop do
x = rand ms.w
y = rand ms.h
next if ms.mine? x, y
ms.add_mine x, y
break
end
end
msd.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment