I wrote a simple Minesweeper game in 8th programming language. It uses a builtin graph support to make code simpler and shorter. There a still some missing bits from the GUI, mainly a game difficulty selector.
\
\ Minesweeper game using graph and tile.
\
needs nk/gui
needs nk/buttons
needs games/tile
libbin font/Roboto-Regular.ttf
42 constant FONT-HEIGHT
FONT-HEIGHT 1.75 n:* constant ROW-HEIGHT
{ font: @font/Roboto-Regular.ttf, size: @FONT-HEIGHT } font:new "font1" font:atlas! drop
670 constant WIDTH
656 constant HEIGHT
10 constant NUM-TILES
NUM-TILES n:sqr constant TOTAL-TILES
"gfx/tile.png" app:asset img:new constant tile-img
"gfx/flag.png" app:asset img:new constant flag-img
"gfx/mine.png" app:asset img:new constant mine-img
"gfx/explosion.png" app:asset img:new constant explosion-img
[ "gfx/n0.png",
"gfx/n1.png",
"gfx/n2.png",
"gfx/n3.png",
"gfx/n4.png",
"gfx/n5.png",
"gfx/n6.png",
"gfx/n7.png",
"gfx/n8.png" ] ( app:asset img:new ) a:map constant num-images
\ tile states
1 constant UNSELECTED
2 constant SELECTED
16 constant MARKED
32 constant MINE
\ max number of mines
15 constant EASY
20 constant NORMAL
25 constant HARD
[@EASY, @NORMAL, @HARD] constant level
NORMAL var, difficulty
nullvar board
: random-tile \ -- n
rand-pcg TOTAL-TILES n:mod ;
: place-mine \ gr -- gr
gr:nodes random-tile a:_@ "state" tile:m@ dup MINE n:band !if
MINE n:bor "state" swap tile:m! drop "mines" gr:m@ 0 ?: n:1+ "mines" swap gr:m!
else
2drop
then ;
: marked? \ tile -- tile T
"state" tile:m@ MARKED n:band ;
: mark \ tile -- tile
"state" tile:m@ MARKED n:bxor "state" swap tile:m! ;
: selected? \ tile -- tile T
"state" tile:m@ SELECTED n:band ;
: select \ tile -- tile
"state" tile:m@ SELECTED n:bor "state" swap tile:m! ;
: mine? \ tile -- tile T
"state" tile:m@ MINE n:band ;
: mines? \ tile -- tile n
"index" tile:m@ board @ "mc" gr:m@ nip swap a:_@ ;
: show-mines
board @ gr:nodes nip
( mine? if select mine-img tile:img! tile:draw then ) a:map drop ;
: index> \ n1 n2 -- [row,col]
n:/mod swap 2 a:close ;
: build-graph \ a cols -- gr
>r a:new 2 a:close ["nodes", "edges"] swap m:zip gr:new
\ connect and filter edges
gr:connect gr:edges
( [ 0,1] a:_@ ( r@ index> ) a:map a:open ( n:- n:abs 2 n:< ) a:2map a:open and ) a:filter rdrop
gr:edges! ;
: score1+
board @ "score" gr:m@ 0 ?: n:1+ "score" swap gr:m! drop ;
: neighbors? \ tile -- tile a
"index" tile:m@ board @ gr:nodes 3rev gr:neighbors nip a:_@ ;
: finished?
board @ "finished" gr:m@ nip ;
: filter-mines \ a -- a
( [0,1] a:_@ over gr:nodes nip swap a:_@ ( mine? nip n:>bool ) a:map a:open or not ) a:filter ;
: filter-cascade \ a -- a
( [0,1] a:_@ over "mc" gr:m@ nip swap a:_@ ' n:>bool a:map a:open and not ) a:filter ;
: filter-on-init \ a -- a
filter-mines filter-cascade ;
: traverse \ n --
board @ false rot
( >r marked? !if over "mc" gr:m@ nip r@ a:_@ num-images swap a:_@ tile:img! tile:draw
select score1+ then drop rdrop ) gr:traverse drop ;
: build-mine-count-table \ gr -- gr
a:new swap false 0
( nip dup>r gr:neighbors over gr:nodes nip swap a:_@ ( mine? nip n:>bool >n ) a:map
' n:+ 0 a:reduce 2 pick r> rot a:! drop ) gr:traverse "mc" rot gr:m! ;
: highlight \ tile -- tile
tile:rect@ 0 [255,255,255,128] nk:fill-rect ;
: render-cell \ tile --
tile:draw finished? if
drop ;;
then
selected? not swap tile:rect@ nk:hovered? rot and if
highlight
tile:rect@ >r marked? not nk:BUTTON_LEFT r@ false nk:clicked? and if
mine? if
show-mines
explosion-img tile:img! tile:draw
board @ "finished" true gr:m! drop
else
mines? if
mines? num-images swap a:_@ tile:img! tile:draw
select score1+
else
"index" tile:m@ traverse
then
then
else
nk:BUTTON_RIGHT r@ false nk:clicked? if
marked? if
tile-img tile:img!
else
flag-img tile:img!
then mark tile:draw highlight
then
then rdrop
then drop ;
: tile:unselected \ [row,col] -- tile
tile-img true tile:new
"state" UNSELECTED tile:m!
' render-cell tile:render! ;
: draw-board
board @ gr:nodes nip ' tile:render a:each! drop ;
: init-board
( dup>r NUM-TILES index> tile:unselected "index" r> tile:m! ) 0 TOTAL-TILES n:1- a:generate
NUM-TILES build-graph
"finished" false gr:m!
"score" 0 gr:m!
' place-mine difficulty @ times
build-mine-count-table
gr:edges filter-on-init gr:edges!
board ! ;
: toolbar
nk:widget if
{ rows: 1, cols: 3, cgap: 8, margin: 8 } nk:layout-grid-begin
0 1 0 1 nk:grid nk:rect>local nk:grid-push
"Restart" ' init-board nk:button-label
nk:layout-grid-end
else
drop
then ;
: info-panel
nk:widget if
{ rows: 1, cols: 2, margin: 32 } nk:layout-grid-begin
0 1 0 1 nk:grid nk:rect>local nk:grid-push
board @ "score" gr:m@ TOTAL-TILES rot "mines" gr:m@ nip n:- n:/ 100 n:* n:int
"CLEARED: %3d %%" s:strfmt nk:TEXT_LEFT "white" nk:label-colored
0 1 1 1 nk:grid nk:rect>local nk:grid-push
board @ "score" gr:m@ nip "SCORE: %3d" s:strfmt nk:TEXT_RIGHT "white" nk:label-colored
nk:layout-grid-end
else
drop
then ;
: sweeper-board
nk:widget if
{ rows: @NUM-TILES, cols: @NUM-TILES, margin: 16 } nk:layout-grid-begin
draw-board
0 NUM-TILES 0 NUM-TILES nk:grid -8 rect:shrink 2 1 "black" nk:stroke-rect
finished? !if
TOTAL-TILES board @ "score" gr:m@ swap "mines" gr:m@ nip n:+ n:- !if
board @ "finished" true gr:m! drop
show-mines
then
then
nk:layout-grid-end
else
drop
then ;
: new-win
{
name: "main",
wide: @WIDTH,
high: @HEIGHT,
resizable: false,
title: "Minesweeper"
} nk:win ;
: main-render
{
bg: "darkgray",
padding: [0,0],
flags: [ @nk:WINDOW_NO_SCROLLBAR ]
}
nk:begin
null { rows: [@ROW-HEIGHT, @ROW-HEIGHT, -1], cols: 1, margin: 4 } nk:layout-grid-begin
0 1 0 1 nk:grid nk:rect>local nk:grid-push toolbar
2 1 0 1 nk:grid nk:rect>local nk:grid-push sweeper-board
1 1 0 1 nk:grid nk:rect>local nk:grid-push info-panel
1 1 0 1 nk:grid 4 2 "lightgray" nk:stroke-rect
nk:layout-grid-end
nk:end ;
: app:main
' init-board w:is nk:rendering
new-win ' main-render -1 nk:render-loop ;

