Simple Minesweeper game written in 8th

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 ;

4 Likes

Here is an updated version with added sound support and game difficulty selector:

\
\ Minesweeper game using graph and tile.
\
needs nk/gui
needs nk/buttons
needs games/tile

\ Free version doesn't have sound support:
8thsku? constant sound?

sound? #if
  requires snd

  "snd/explosion.ogg" app:asset snd:new constant explosion-snd
  "snd/zero.ogg" app:asset snd:new constant zero-snd
#then

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 nk:flags@ nk:WINDOW_ROM n:band !if
      tile:rect@ >r marked? not nk:BUTTON_LEFT r@ true nk:clicked? and if
        mine? if
          show-mines
          explosion-img tile:img! tile:draw
          sound? if explosion-snd snd:play then
          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
            sound? if zero-snd snd:play then
          then
        then
      else
        nk:BUTTON_RIGHT r@ true nk:clicked? if
          marked? if
            tile-img tile:img!
          else
            flag-img tile:img!
          then mark tile:draw highlight
        then
      then rdrop
    then 
  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
      0 1 2 1 nk:grid nk:rect>local nk:grid-push
        "difficulty" nk:get "difficulty-sel" nk:get
        nk:get-row-height nk:widget-bounds 2 rect:@ nip 
        ROW-HEIGHT 3 n:* 2 a:close nk:combo "difficulty-sel" over nk:set
        level lookup dup difficulty @ n:= !if
          difficulty !
          init-board
        else
          drop
        then
    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 ],
    \ local window variables
    difficulty: ["Easy", "Normal", "Hard"],
    difficulty-sel: 1
  }

  nk:begin
    null { rows: [@ROW-HEIGHT, @ROW-HEIGHT, -1], cols: 1 } 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 ;

1 Like