Last active
November 15, 2021 20:12
-
-
Save JoshCheek/26c1663e266c8a407c99859f11217158 to your computer and use it in GitHub Desktop.
Mine Sweeper
This file contains hidden or 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
# 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