368 lines
9.5 KiB
Lua
368 lines
9.5 KiB
Lua
|
local assdraw = require 'mp.assdraw'
|
||
|
|
||
|
local W = 0
|
||
|
local H = 0
|
||
|
-- 2D array of size W*H that's addressed via gField[x][y]. Each entry is a
|
||
|
-- table, with the following fields:
|
||
|
-- is_mine: true or false
|
||
|
-- area_mines: mine count in the 3x3 surrounding area
|
||
|
-- is_covered: true or false, for visibility
|
||
|
-- flag: flag put by user, one of FLAG_*
|
||
|
local gField = nil
|
||
|
|
||
|
local FLAG_NONE = "" -- not flagged
|
||
|
local FLAG_MINE = "⚑" -- "⚐" -- flagged as containing mine
|
||
|
local FLAG_MAYBE_MINE = "!" -- flagged as maybe containing mine
|
||
|
local FLAG_MAYBE_SAFE = "?" -- flagged as maybe empty
|
||
|
|
||
|
local STATUS_PLAYING = "playing"
|
||
|
local STATUS_WON = "won"
|
||
|
local STATUS_LOST = "lost"
|
||
|
|
||
|
local gStatus = nil
|
||
|
|
||
|
local gMines = 0
|
||
|
|
||
|
local gX = 0
|
||
|
local gY = 0
|
||
|
|
||
|
local gNeedRefresh = false
|
||
|
local gHidden = true
|
||
|
local gTransparent = false
|
||
|
|
||
|
local PRESETS = {
|
||
|
-- taken from kmines
|
||
|
{ name = "easy", w = 9, h = 9, mines = 10 },
|
||
|
{ name = "medium", w = 16, h = 16, mines = 40 },
|
||
|
{ name = "hard", w = 30, h = 16, mines = 99 },
|
||
|
}
|
||
|
local gCurrentPreset = 2
|
||
|
|
||
|
function init_field()
|
||
|
local preset = PRESETS[gCurrentPreset]
|
||
|
gStatus = STATUS_PLAYING
|
||
|
gField = {}
|
||
|
W = preset.w
|
||
|
H = preset.h
|
||
|
gMines = math.min(preset.mines, W * H - 1)
|
||
|
gX = 1
|
||
|
gY = 1
|
||
|
|
||
|
for x = 1, W do
|
||
|
gField[x] = {}
|
||
|
for y = 1, H do
|
||
|
gField[x][y] = {
|
||
|
is_mine = false,
|
||
|
area_mines = 0,
|
||
|
is_covered = true,
|
||
|
flag = FLAG_NONE,
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- place mines using the dumbfuck algorithm
|
||
|
local place_mines = gMines
|
||
|
while place_mines > 0 do
|
||
|
local x = math.random(1, W)
|
||
|
local y = math.random(1, H)
|
||
|
if not gField[x][y].is_mine then
|
||
|
gField[x][y].is_mine = true
|
||
|
place_mines = place_mines - 1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- pick a random start position (also using dumbfuck algorithm)
|
||
|
for i = 1, 1000000 do
|
||
|
local x = math.random(1, W)
|
||
|
local y = math.random(1, H)
|
||
|
if not gField[x][y].is_mine then
|
||
|
gX = x
|
||
|
gY = y
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- compute proximities after mines have been placed
|
||
|
for y = 1, H do
|
||
|
for x = 1, W do
|
||
|
local tile = gField[x][y]
|
||
|
for a_x = -1, 1 do
|
||
|
for a_y = -1, 1 do
|
||
|
local t_x = x + a_x
|
||
|
local t_y = y + a_y
|
||
|
if t_x >= 1 and t_x <= W and t_y >= 1 and t_y <= H and
|
||
|
gField[t_x][t_y].is_mine
|
||
|
then
|
||
|
tile.area_mines = tile.area_mines + 1
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
uncover()
|
||
|
|
||
|
check_status()
|
||
|
|
||
|
gNeedRefresh = true
|
||
|
end
|
||
|
|
||
|
function uncover_at(x, y)
|
||
|
if x < 1 or x > W or y < 1 or y > H then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local tile = gField[x][y]
|
||
|
|
||
|
if not tile.is_covered then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
tile.is_covered = false
|
||
|
|
||
|
if tile.is_mine then
|
||
|
return -- lost anyway
|
||
|
end
|
||
|
|
||
|
-- uncover mines as far as it goes
|
||
|
-- apparently, the standard thing to do is recursively uncovering all
|
||
|
-- tiles which have 0 neightbours - tiles with 1 or more neighbours are
|
||
|
-- uncovered, but not recursively
|
||
|
if tile.area_mines == 0 then
|
||
|
for a_x = -1, 1 do
|
||
|
for a_y = -1, 1 do
|
||
|
uncover_at(x + a_x, y + a_y)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
gNeedRefresh = true
|
||
|
end
|
||
|
|
||
|
function check_status()
|
||
|
if gStatus ~= STATUS_PLAYING then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local won = true
|
||
|
local lost = false
|
||
|
|
||
|
for y = 1, H do
|
||
|
for x = 1, W do
|
||
|
local tile = gField[x][y]
|
||
|
won = won and (tile.is_mine == tile.is_covered)
|
||
|
lost = lost or (tile.is_mine and not tile.is_covered)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if lost then
|
||
|
gStatus = STATUS_LOST
|
||
|
gNeedRefresh = true
|
||
|
elseif won then
|
||
|
gStatus = STATUS_WON
|
||
|
gNeedRefresh = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function uncover()
|
||
|
if gStatus ~= STATUS_PLAYING and not gField[gX][gY].is_covered then
|
||
|
init_field()
|
||
|
render()
|
||
|
return
|
||
|
end
|
||
|
uncover_at(gX, gY)
|
||
|
check_status()
|
||
|
render()
|
||
|
end
|
||
|
|
||
|
function flag()
|
||
|
local tile = gField[gX][gY]
|
||
|
local cycle = {FLAG_NONE, FLAG_MINE, FLAG_MAYBE_MINE, FLAG_MAYBE_SAFE}
|
||
|
for i = 1, #cycle do
|
||
|
if tile.flag == cycle[i] then
|
||
|
tile.flag = cycle[(i - 1 + 1) % #cycle + 1]
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
if not tile.is_covered then
|
||
|
tile.flag = FLAG_NONE
|
||
|
end
|
||
|
force_render()
|
||
|
end
|
||
|
|
||
|
function move(x, y)
|
||
|
gX = math.min(math.max(gX + x, 1), W)
|
||
|
gY = math.min(math.max(gY + y, 1), H)
|
||
|
|
||
|
force_render()
|
||
|
end
|
||
|
|
||
|
function force_render()
|
||
|
gNeedRefresh = true
|
||
|
render()
|
||
|
end
|
||
|
|
||
|
function render()
|
||
|
if not gNeedRefresh then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
if gHidden then
|
||
|
mp.set_osd_ass(1280, 720, "")
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local canvas_w = 1280
|
||
|
local canvas_h = 720
|
||
|
local dw, dh, da = mp.get_osd_size()
|
||
|
if dw ~= nil and dw > 0 and dh > 0 then
|
||
|
canvas_w = dw / dh * canvas_h
|
||
|
end
|
||
|
|
||
|
local tile_wh = 32
|
||
|
|
||
|
local o_x = canvas_w / 2 - tile_wh * W / 2
|
||
|
local o_y = canvas_h / 2 - tile_wh * (H + 2) / 2 + tile_wh
|
||
|
|
||
|
local ass = assdraw.ass_new()
|
||
|
|
||
|
local transp = nil
|
||
|
if gTransparent then
|
||
|
transp = "{\\1a&HA0&\\3a&HA0&}"
|
||
|
end
|
||
|
|
||
|
-- some shitty background
|
||
|
ass:new_event()
|
||
|
ass:append("{\\1c&Ha3a3a3&\\1a&H30&}")
|
||
|
if transp then
|
||
|
ass:append(transp)
|
||
|
end
|
||
|
ass:pos(o_x - tile_wh, o_y - tile_wh)
|
||
|
ass:draw_start()
|
||
|
ass:rect_cw(0, 0, (W + 2) * tile_wh, (H + 2) * tile_wh)
|
||
|
-- grid
|
||
|
local function grid_line(x0, y0, x1, y1)
|
||
|
ass:new_event()
|
||
|
ass:append("{\\bord0.5}")
|
||
|
if transp then
|
||
|
ass:append(transp)
|
||
|
end
|
||
|
ass:pos(x0, y0)
|
||
|
ass:draw_start()
|
||
|
ass:coord(0, 0)
|
||
|
ass:line_to(x1 - x0, y1 - y0)
|
||
|
end
|
||
|
for x = 0, W do
|
||
|
local p_x = x * tile_wh + o_x
|
||
|
grid_line(p_x, o_y, p_x, o_y + tile_wh * H)
|
||
|
end
|
||
|
for y = 0, H do
|
||
|
local p_y = y * tile_wh + o_y
|
||
|
grid_line(o_x, p_y, o_x + tile_wh * W, p_y)
|
||
|
end
|
||
|
|
||
|
local function draw_sym(x, y, sym, c)
|
||
|
ass:new_event()
|
||
|
ass:pos(x, y)
|
||
|
ass:append("{\\an5\\fs25\\bord0\\1c&H" .. c .. "&\\b1}" .. sym)
|
||
|
end
|
||
|
|
||
|
for x = 1, W do
|
||
|
for y = 1, H do
|
||
|
local tile = gField[x][y]
|
||
|
local p_x = (x - 1) * tile_wh + tile_wh / 2 + o_x
|
||
|
local p_y = (y - 1) * tile_wh + tile_wh / 2 + o_y
|
||
|
local wh = tile_wh - 4
|
||
|
local sym = nil
|
||
|
if tile.is_covered then
|
||
|
ass:new_event()
|
||
|
if transp then
|
||
|
ass:append(transp)
|
||
|
end
|
||
|
ass:pos(p_x, p_y)
|
||
|
ass:draw_start()
|
||
|
ass:round_rect_cw(-wh / 2, -wh / 2, wh / 2, wh / 2, 5)
|
||
|
ass:draw_stop()
|
||
|
elseif tile.is_mine then
|
||
|
draw_sym(p_x, p_y, "💣", "0000FF")
|
||
|
elseif tile.area_mines > 0 then
|
||
|
draw_sym(p_x, p_y, tile.area_mines, "000000")
|
||
|
end
|
||
|
if tile.flag ~= FLAG_NONE then
|
||
|
draw_sym(p_x, p_y, tile.flag, "FF0000")
|
||
|
end
|
||
|
if x == gX and y == gY then
|
||
|
local wh = tile_wh - 12
|
||
|
ass:new_event()
|
||
|
ass:append("{\\1a&HFF&}")
|
||
|
ass:pos(p_x, p_y)
|
||
|
ass:draw_start()
|
||
|
ass:rect_cw(-wh / 2, -wh / 2, wh / 2, wh / 2)
|
||
|
ass:draw_stop()
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local banner = nil
|
||
|
if gStatus == STATUS_WON then
|
||
|
banner = "You may have won, but actually you just wasted time."
|
||
|
elseif gStatus == STATUS_LOST then
|
||
|
banner = "You lost (and wasted time)."
|
||
|
end
|
||
|
if banner then
|
||
|
ass:new_event()
|
||
|
ass:pos(o_x + tile_wh * W / 2, o_y - tile_wh - 10)
|
||
|
ass:append("{\\fs40\\b1\\an2}" .. banner)
|
||
|
end
|
||
|
|
||
|
mp.set_osd_ass(canvas_w, canvas_h, ass.text)
|
||
|
end
|
||
|
|
||
|
mp.observe_property("osd-width", "native", force_render)
|
||
|
mp.observe_property("osd-height", "native", force_render)
|
||
|
|
||
|
init_field()
|
||
|
force_render()
|
||
|
|
||
|
function toggle_transp()
|
||
|
gTransparent = not gTransparent
|
||
|
force_render()
|
||
|
end
|
||
|
|
||
|
function cycle_preset()
|
||
|
gCurrentPreset = (gCurrentPreset + 1 - 1) % #PRESETS + 1
|
||
|
init_field()
|
||
|
render()
|
||
|
end
|
||
|
|
||
|
function toggle_show()
|
||
|
if gHidden then
|
||
|
gHidden = false
|
||
|
|
||
|
local REP = {repeatable = true}
|
||
|
mp.add_forced_key_binding("left", "mines-left", function() move(-1, 0) end, REP)
|
||
|
mp.add_forced_key_binding("right", "mines-right", function() move(1, 0) end, REP)
|
||
|
mp.add_forced_key_binding("up", "mines-up", function() move(0, -1) end, REP)
|
||
|
mp.add_forced_key_binding("down", "mines-down", function() move(0, 1) end, REP)
|
||
|
mp.add_forced_key_binding("space", "mines-uncover", uncover)
|
||
|
mp.add_forced_key_binding("b", "mines-flag", flag)
|
||
|
mp.add_forced_key_binding("t", "mines-transp", toggle_transp)
|
||
|
mp.add_forced_key_binding("w", "mines-preset", cycle_preset)
|
||
|
else
|
||
|
gHidden = true
|
||
|
|
||
|
mp.remove_key_binding("mines-left")
|
||
|
mp.remove_key_binding("mines-right")
|
||
|
mp.remove_key_binding("mines-up")
|
||
|
mp.remove_key_binding("mines-down")
|
||
|
mp.remove_key_binding("mines-uncover")
|
||
|
mp.remove_key_binding("mines-flag")
|
||
|
mp.remove_key_binding("mines-transp")
|
||
|
mp.remove_key_binding("mines-preset")
|
||
|
end
|
||
|
|
||
|
force_render()
|
||
|
end
|
||
|
|
||
|
mp.add_forced_key_binding("ctrl+x", "mines-show", toggle_show)
|