! Copyright (C) 2017 John Benediktsson ! See http://factorcode.org/license.txt for BSD license USING: accessors arrays assocs calendar colors.constants combinators combinators.short-circuit destructors formatting fry images.loader kernel locals math math.order math.parser namespaces opengl opengl.textures random sequences timers ui ui.commands ui.gadgets ui.gadgets.toolbar ui.gadgets.tracks ui.gadgets.worlds ui.gestures ui.pens.solid ui.render words ; IN: minesweeper CONSTANT: neighbors { { -1 -1 } { -1 0 } { -1 1 } { 0 -1 } { 0 1 } { 1 -1 } { 1 0 } { 1 1 } } SYMBOLS: +flagged+ +question+ +clicked+ ; TUPLE: cell #adjacent mined? state ; : make-cells ( rows cols -- cells ) '[ _ [ cell new ] replicate ] replicate ; :: cell-at ( cells row col -- cell/f ) row cells ?nth [ col swap ?nth ] [ f ] if* ; : cells-dim ( cells -- rows cols ) [ length ] [ first length ] bi ; : unmined-cell ( cells -- cell ) f [ dup mined?>> ] [ drop dup random random ] do while nip ; : #mines ( cells -- n ) [ [ mined?>> ] count ] map-sum ; : #flagged ( cells -- n ) [ [ state>> +flagged+ = ] count ] map-sum ; : #mines-remaining ( cells -- n ) [ #mines ] [ #flagged ] bi - ; : place-mines ( cells n -- cells ) [ dup unmined-cell t >>mined? drop ] times ; : adjacent-mines ( cells row col -- #mines ) neighbors [ first2 [ + ] bi-curry@ bi* cell-at [ mined?>> ] [ f ] if* ] with with with count ; :: each-cell ( ... cells quot: ( ... row col cell -- ... ) -- ... ) cells [| row | [| cell col | row col cell quot call ] each-index ] each-index ; inline :: update-counts ( cells -- cells ) cells [| row col cell | cells row col adjacent-mines cell #adjacent<< ] each-cell cells ; : reset-cells ( cells -- cells ) [ cells-dim make-cells ] [ #mines place-mines ] bi update-counts ; : won? ( cells -- ? ) [ [ { [ state>> +clicked+ = ] [ mined?>> ] } 1|| ] all? ] all? ; : lost? ( cells -- ? ) [ [ { [ state>> +clicked+ = ] [ mined?>> ] } 1&& ] any? ] any? ; : game-over? ( cells -- ? ) { [ lost? ] [ won? ] } 1|| ; : new-game? ( cells -- ? ) [ [ state>> +clicked+ = ] any? ] any? not ; DEFER: click-cell-at :: click-cells-around ( cells row col -- ) neighbors [ first2 [ row + ] [ col + ] bi* :> ( row' col' ) cells row' col' cell-at [ { [ mined?>> ] [ state>> +question+ = ] } 1|| [ cells row' col' click-cell-at drop ] unless ] when* ] each ; :: click-cell-at ( cells row col -- ? ) cells row col cell-at [ cells new-game? [ ! first click shouldn't be a mine dup mined?>> [ cells unmined-cell t >>mined? drop f >>mined? cells update-counts drop ] when ] when dup state>> { +clicked+ +flagged+ } member? [ drop f ] [ +clicked+ >>state { [ mined?>> not ] [ #adjacent>> 0 = ] } 1&& [ cells row col click-cells-around ] when t ] if ] [ f ] if* ; :: mark-cell-at ( cells row col -- ? ) cells row col cell-at [ dup state>> { { +clicked+ [ +clicked+ ] } { +flagged+ [ +question+ ] } { +question+ [ f ] } { f [ +flagged+ ] } } case >>state drop t ] [ f ] if* ; TUPLE: grid-gadget < gadget cells timer textures start end ; :: ( rows cols mines -- gadget ) grid-gadget new rows cols make-cells mines place-mines update-counts >>cells H{ } clone >>textures dup '[ _ relayout-1 ] f 1 seconds >>timer COLOR: gray >>interior ; M: grid-gadget graft* [ timer>> start-timer ] [ call-next-method ] bi ; M: grid-gadget ungraft* [ dup find-gl-context [ values dispose-each H{ } clone ] change-textures timer>> stop-timer ] [ call-next-method ] bi ; M: grid-gadget pref-dim* cells>> cells-dim [ 32 * ] bi@ swap 58 + 2array ; :: cell-image-path ( cell game-over? -- image-path ) game-over? cell mined?>> and [ cell state>> +clicked+ = "mineclicked.gif" "mine.gif" ? ] [ cell state>> { { +question+ [ "question.gif" ] } { +flagged+ [ game-over? "misflagged.gif" "flagged.gif" ? ] } { +clicked+ [ cell mined?>> [ "mine.gif" ] [ cell #adjacent>> 0 or number>string "open" ".gif" surround ] if ] } { f [ "blank.gif" ] } } case ] if "vocab:minesweeper/_resources/" prepend ; : digit-image-path ( ch -- image-path ) "vocab:minesweeper/_resources/digit%c.gif" sprintf ; :: smiley-image-path ( won? lost? clicking? -- image-path ) { { [ lost? ] [ "vocab:minesweeper/_resources/smileylost.gif" ] } { [ won? ] [ "vocab:minesweeper/_resources/smileywon.gif" ] } { [ clicking? ] [ "vocab:minesweeper/_resources/smileyuhoh.gif" ] } [ "vocab:minesweeper/_resources/smiley.gif" ] } cond ; : cached-texture ( path gadget -- texture ) textures>> [ load-image { 0 0 } ] cache ; :: draw-mines ( n gadget -- ) n "%03d" sprintf [ 26 * 3 + 6 2array [ digit-image-path gadget cached-texture { 26 46 } swap draw-scaled-texture ] with-translation ] each-index ; :: draw-smiley ( gadget -- ) gadget pref-dim first :> width width 2/ 26 - 3 2array [ gadget cells>> won? gadget cells>> lost? hand-buttons get-global empty? not gadget hand-click-rel second 58 >= and smiley-image-path gadget cached-texture { 52 52 } swap draw-scaled-texture ] with-translation ; :: draw-timer ( n gadget -- ) gadget pref-dim first :> width n "%03d" sprintf [ 3 swap - 26 * width swap - 3 - 6 2array [ digit-image-path gadget cached-texture { 26 46 } swap draw-scaled-texture ] with-translation ] each-index ; :: draw-cells ( gadget -- ) gadget cells>> game-over? :> game-over? gadget cells>> [| row col cell | col row [ 32 * ] bi@ 58 + 2array [ cell game-over? cell-image-path gadget cached-texture { 32 32 } swap draw-scaled-texture ] with-translation ] each-cell ; :: elapsed-time ( gadget -- n ) gadget start>> [ gadget end>> now or swap time- duration>seconds ] [ 0 ] if* ; M: grid-gadget draw-gadget* { [ cells>> #mines-remaining ] [ draw-mines ] [ draw-smiley ] [ elapsed-time ] [ draw-timer ] [ draw-cells ] } cleave ; :: on-click ( gadget -- ) gadget hand-rel first2 :> ( w h ) h 58 < [ h 3 55 between? gadget pref-dim first 2/ w - abs 26 < and [ gadget [ reset-cells ] change-cells f >>start f >>end relayout-1 ] when ] [ h 58 - w [ 32 /i ] bi@ :> ( row col ) gadget cells>> :> cells cells game-over? [ cells row col click-cell-at [ gadget start>> [ now gadget start<< ] unless cells game-over? [ now gadget end<< ] when gadget relayout-1 ] when ] unless ] if ; :: on-mark ( gadget -- ) gadget hand-rel first2 :> ( w h ) h 58 >= [ h 58 - w [ 32 /i ] bi@ :> ( row col ) gadget cells>> :> cells cells game-over? [ cells row col mark-cell-at [ gadget start>> [ now gadget start<< ] unless cells game-over? [ now gadget end<< ] when gadget relayout-1 ] when ] unless ] when ; : new-game ( gadget rows cols mines -- ) [ make-cells ] dip place-mines update-counts >>cells f >>start f >>end relayout-window ; : com-easy ( gadget -- ) 7 7 10 new-game ; : com-medium ( gadget -- ) 15 15 40 new-game ; : com-hard ( gadget -- ) 15 30 99 new-game ; grid-gadget "toolbar" f { { T{ key-down { sym "1" } } com-easy } { T{ key-down { sym "2" } } com-medium } { T{ key-down { sym "3" } } com-hard } } define-command-map grid-gadget "gestures" [ { { T{ button-down { # 1 } } [ relayout-1 ] } { T{ button-up { # 1 } } [ on-click ] } { T{ button-up { # 3 } } [ on-mark ] } { T{ key-down { sym " " } } [ on-mark ] } } assoc-union ] change-word-prop TUPLE: minesweeper-gadget < track ; : ( -- gadget ) vertical minesweeper-gadget new-track 7 7 10 [ format-toolbar f track-add ] [ 1 track-add ] bi ; M: minesweeper-gadget focusable-child* children>> second ; MAIN-WINDOW: run-minesweeper { { title "Minesweeper" } { window-controls { normal-title-bar close-button minimize-button } } } >>gadgets ;