Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active November 15, 2021 20:12
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JoshCheek/26c1663e266c8a407c99859f11217158 to your computer and use it in GitHub Desktop.
Save JoshCheek/26c1663e266c8a407c99859f11217158 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
def initialize(mine:, clicked:, marked:, x:, y:, count:)
@x, @y, @mine, @clicked, @marked, @count =
x, y, mine, clicked, marked, count
end
attr_reader :x, :y, :count
def mine?() @mine end
def clicked?() @clicked end
def marked?() @marked end
def with(overrides)
self.class.new x: x, y: y, count: count,
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_accessor :on_change
def initialize(w, h)
self.on_change ||= -> _cell { nil }
self.w, self.h, = w, h
self.on_change = on_change
self.board = Array.new h do |y|
Array.new w do |x|
Cell.new x: x, y: y, mine: false, marked: false, clicked: false, count: 0
end
end
end
def each(&block)
board.each { |row| row.each &block }
end
def add_mine(x, y)
raise "Already a mine at (#{x}, #{y})" if mine? x, y
cell = board[y][x].with(mine: true)
board[y][x] = cell
neighbours_of(x, y, true).each { |n|
board[n.y][n.x] = n.with(count: n.count.succ)
on_change.call n
}
on_change.call cell
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
cell = cell.with clicked: true
board[y][x] = cell
on_change.call cell
self
end
def mark(x, y)
coords_are_valid! x, y
old_cell = board[y][x]
new_cell = old_cell.toggle_mark_if_possible
board[y][x] = new_cell
on_change.call new_cell if old_cell.marked? != new_cell.marked?
self
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
def valid_coords?(x, y)
0 <= x && 0 <= y && x < w && y < h
end
private
attr_reader :board
attr_writer :w, :h, :board
def spread(cell)
return if cell.clicked?
return if cell.mine?
cell = cell.with clicked: true
board[cell.y][cell.x] = cell
on_change.call cell
return if 0 < cell.count
neighbours_of(cell.x, cell.y, true).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 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.to_draw = []
to_draw << lambda do
minesweeper.each { |cell| Cell.new(self, side_length, border_width, cell).draw }
end
minesweeper.on_change = lambda do |cell|
to_draw << lambda do
Cell.new(self, side_length, border_width, cell).draw
end
end
end
LEFT_CLICK = 1
def handle_event(event, n)
minesweeper.over? || if event.kind_of? SDL::Event::Mouseup
x = event.x / side_length
y = (h-event.y-1) / side_length
if minesweeper.valid_coords? x, y
if event.button == LEFT_CLICK
minesweeper.click(x, y)
else
minesweeper.mark(x, y)
end
end
if minesweeper.won?
to_draw << method(:draw_smiley_face)
elsif minesweeper.lost?
to_draw << method(:display_mines)
end
end
super
end
def draw(n)
to_draw.shift.call while to_draw.any?
end
def display_mines
to_draw << lambda do
minesweeper.each do |cell|
next unless cell.mine?
Cell.new(self, side_length, border_width, cell).draw_mine
end
end
end
def draw_smiley_face
radius = [w, h].min/2
circle w/2, h/2, radius, :yellow, true
circle w/5, 3*h/5, radius/5, :black, true
circle 3*w/5, 3*h/5, radius/5, :black, true
(Math::PI*5/4).step(to: (Math::PI*7/4), by: 0.01).each_cons(2) do |ø1, ø2|
x1 = Math.cos(ø1)*radius/2+w/2
y1 = Math.sin(ø1)*radius/2+h/2
x2 = Math.cos(ø2)*radius/2+w/2
y2 = Math.sin(ø2)*radius/2+h/2
line x1, y1, x2, y2, :black
line x1+1, y1, x2+1, y2, :black
line x1-1, y1, x2-1, y2, :black
line x1, y1+1, x2, y2+1, :black
line x1, y1-1, x2, y2-1, :black
end
end
class Cell
attr_reader :canvas, :cell, :side, :border
def initialize(canvas, side, border, cell)
@canvas, @cell = canvas, cell
@side, @border = side, border
end
def x() cell.x end
def y() cell.y end
def count() cell.count end
def clicked?() cell.clicked? end
def marked?() cell.marked? end
def mine?() cell.mine? 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 draw
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(20, 15)
25.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 = MineSweeperDisplay.new 40, ms
msd.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment