-
-
Save mickstevens/6d93028ef53d1836bb43d747559cb13f to your computer and use it in GitHub Desktop.
Mine Sweeper
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
# 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