├── .gitignore
├── minesweeper.gif
├── public
├── favicon.ico
└── index.html
├── README.md
├── .github
└── workflows
│ └── gh-pages.yaml
├── elm.json
└── src
├── Grid.elm
└── Main.elm
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | public/*.js
3 | elm-stuff/
4 |
--------------------------------------------------------------------------------
/minesweeper.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dfarr/minesweeper/HEAD/minesweeper.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dfarr/minesweeper/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # minesweeper
2 |
3 | [](https://dfarr.github.io/minesweeper)
4 |
5 | An event sourced version of Minesweeper with a really cool replay slider, written in Elm.
6 |
7 | - [Play the game](https://dfarr.github.io/minesweeper)
8 | - [Read the blog](https://dfarr.medium.com/event-sourcing-minesweeper-65f0d497e6a7)
9 |
10 | 
11 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yaml:
--------------------------------------------------------------------------------
1 | name: GitHub Pages
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build-and-publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | - name: Install
13 | uses: jorelali/setup-elm@v2
14 | with:
15 | elm-version: 0.19.1
16 | - name: Build
17 | run: elm make --optimize --output public/main.js src/Main.elm
18 | - name: Publish
19 | uses: peaceiris/actions-gh-pages@v3
20 | with:
21 | github_token: ${{ secrets.GITHUB_TOKEN }}
22 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "elm/browser": "1.0.2",
10 | "elm/core": "1.0.5",
11 | "elm/html": "1.0.0",
12 | "elm/json": "1.1.3",
13 | "elm/random": "1.0.0",
14 | "elm/time": "1.0.0",
15 | "elm-community/list-extra": "8.2.4"
16 | },
17 | "indirect": {
18 | "elm/url": "1.0.0",
19 | "elm/virtual-dom": "1.0.2"
20 | }
21 | },
22 | "test-dependencies": {
23 | "direct": {},
24 | "indirect": {}
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Minesweeper
5 |
6 |
7 |
8 |
9 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/Grid.elm:
--------------------------------------------------------------------------------
1 | module Grid exposing
2 | ( Grid, Coord
3 | , repeat, indexToCoord, coordToIndex, withCoord
4 | , map, indexedMap, mapRelationship, indexedMapRelationship, mapIdentity
5 | )
6 |
7 | import List.Extra
8 |
9 |
10 | -- MODELS
11 |
12 | type alias Grid a
13 | = List (List a)
14 |
15 | type alias Coord
16 | = ( Int, Int )
17 |
18 | type alias F a b
19 | = ( Coord, a ) -> b
20 |
21 | type Relationship
22 | = Identity
23 | | Neighbor
24 | | Neither
25 |
26 |
27 | -- FUNCTIONS
28 |
29 | repeat : Int -> Int -> a -> Grid a
30 | repeat m n =
31 | List.repeat (m * n) >> List.Extra.groupsOf n
32 |
33 | indexToCoord : Int -> Int -> Coord
34 | indexToCoord m i =
35 | ( remainderBy m i, i // m )
36 |
37 | coordToIndex : Int -> Coord -> Int
38 | coordToIndex m ( i, j ) =
39 | m * j + i
40 |
41 | withCoord : (Coord -> a -> b) -> Int -> Int -> a -> b
42 | withCoord f i j =
43 | f ( i, j )
44 |
45 | relationship : Coord -> Coord -> Relationship
46 | relationship ( i1, j1 ) ( i2, j2 ) =
47 | case ( i1-i2 |> abs, j1-j2 |> abs ) of
48 | ( 0, 0 ) -> Identity
49 | ( 0, 1 ) -> Neighbor
50 | ( 1, 0 ) -> Neighbor
51 | ( 1, 1 ) -> Neighbor
52 | _ -> Neither
53 |
54 | mapRelationship : (a -> b) -> (a -> b) -> (a -> b) -> Coord -> Coord -> a -> b
55 | mapRelationship f1 f2 f3 =
56 | indexedMapRelationship (f1 << Tuple.second) (f2 << Tuple.second) (f3 << Tuple.second)
57 |
58 | indexedMapRelationship : F a b -> F a b -> F a b -> Coord -> Coord -> a -> b
59 | indexedMapRelationship f1 f2 f3 c1 c2 =
60 | case relationship c1 c2 of
61 | Identity ->
62 | f1 << Tuple.pair c2
63 | Neighbor ->
64 | f2 << Tuple.pair c2
65 | Neither ->
66 | f3 << Tuple.pair c2
67 |
68 | map : (a -> b) -> (a -> b) -> (a -> b) -> Coord -> Grid a -> Grid b
69 | map f1 f2 f3 =
70 | indexedMap (f1 << Tuple.second) (f2 << Tuple.second) (f3 << Tuple.second)
71 |
72 | indexedMap : F a b -> F a b -> F a b -> Coord -> Grid a -> Grid b
73 | indexedMap f1 f2 f3 c1 =
74 | withCoord (indexedMapRelationship f1 f2 f3 c1)
75 | >> List.indexedMap
76 | |> List.indexedMap
77 |
78 | mapIdentity : a -> Coord -> Grid a -> Grid a
79 | mapIdentity x =
80 | indexedMap (always x) Tuple.second Tuple.second
81 |
--------------------------------------------------------------------------------
/src/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (..)
2 |
3 | import Browser exposing (Document)
4 | import Html exposing (Html, a, button, div, h1, input, table, tr, td, text, span, strong)
5 | import Html.Attributes exposing (attribute, href, style, type_, value)
6 | import Html.Events exposing (custom, onBlur, onClick, onInput, onMouseDown, onMouseUp)
7 | import Json.Decode as Decode exposing (Decoder)
8 | import Random
9 | import Set exposing (Set)
10 | import Time
11 |
12 | import Grid exposing (Grid, Coord)
13 |
14 |
15 |
16 | -- MAIN
17 |
18 | main =
19 | Browser.document { init = init, update = update, subscriptions = subscriptions, view = view }
20 |
21 |
22 | subscriptions : Model -> Sub Msg
23 | subscriptions model =
24 | Time.every 1000 Tick
25 |
26 |
27 | -- MODEL
28 |
29 | type alias Model =
30 | { index : Int
31 | , clock : Int
32 | , mouse : Bool
33 | , mines : Set Coord
34 | , board : ( Board Int, Board String )
35 | , state : { initial : State, current : State }
36 | , events : List Event
37 | }
38 |
39 | type State
40 | = Initial Coord (Grid Square)
41 | | Pending (Grid Square)
42 | | Success (Grid Square)
43 | | Failure (Grid Square)
44 |
45 | type alias Board a =
46 | { rows : a, cols : a, mines : a }
47 |
48 | type Square
49 | = Hidden Value
50 | | Marked Value
51 | | Exposed Value
52 |
53 | type Value
54 | = Safe Int
55 | | Mine
56 |
57 |
58 | -- INIT
59 |
60 | initBoard : Board Int
61 | initBoard =
62 | { rows = 16, cols = 16, mines = 40 }
63 |
64 | initModel : Board Int -> Model
65 | initModel board =
66 | let
67 | initial =
68 | Hidden (Safe 0)
69 | |> Grid.repeat board.rows board.cols
70 | |> Initial ( 0, 0 )
71 | board_ =
72 | { rows = board.rows |> String.fromInt
73 | , cols = board.cols |> String.fromInt
74 | , mines = board.mines |> String.fromInt
75 | }
76 | in
77 | { index = 0
78 | , clock = 0
79 | , mouse = False
80 | , mines = Set.empty
81 | , board = ( board, board_ )
82 | , state = { initial = initial, current = initial }
83 | , events = []
84 | }
85 |
86 | initCommand : Board Int -> Cmd Msg
87 | initCommand { rows, cols } =
88 | Random.int 0 (rows * cols - 1)
89 | |> Random.generate (Setup << Grid.indexToCoord rows)
90 |
91 | init : () -> ( Model, Cmd Msg )
92 | init _ =
93 | ( initModel initBoard, initCommand initBoard )
94 |
95 |
96 | -- UPDATE
97 |
98 | type Msg
99 | = NoOp
100 | | Tick Time.Posix
101 | | Reset
102 | | Setup Coord
103 | | Mouse Bool
104 | | Click Event
105 | | Replay Int
106 | | SetBoard (Board String)
107 |
108 | type Event
109 | = Clicked Coord Square
110 | | Flagged Coord Square
111 | | Special Coord Square
112 |
113 | update : Msg -> Model -> ( Model, Cmd Msg )
114 | update message model =
115 | let
116 | { index, clock, mines, board, state, events } = model
117 | ( intBoard, strBoard ) = board
118 | in
119 | case ( message, state.current ) of
120 | ( Tick _, Pending _ ) ->
121 | let
122 | clock_ =
123 | case index == List.length events of
124 | True -> clock + 1
125 | False -> clock
126 | in
127 | ( { model | clock = clock_ }, Cmd.none )
128 | ( Reset, _ ) ->
129 | let
130 | rows =
131 | strBoard.rows
132 | |> String.toInt
133 | |> Maybe.withDefault intBoard.rows
134 | |> max 1
135 | cols =
136 | strBoard.cols
137 | |> String.toInt
138 | |> Maybe.withDefault intBoard.cols
139 | |> max 1
140 | mines_ =
141 | strBoard.mines
142 | |> String.toInt
143 | |> Maybe.withDefault intBoard.mines
144 | |> max 0
145 | |> min (rows * cols - 1)
146 | board_ =
147 | { rows = rows, cols = cols, mines = mines_ }
148 | in
149 | ( initModel board_, initCommand board_ )
150 |
151 | ( Setup coord, Initial _ grid ) ->
152 | let
153 | mines_ =
154 | Set.insert coord mines
155 | ( current, command ) =
156 | case Set.size mines_ == intBoard.mines + 1 of
157 | True ->
158 | ( mines_
159 | |> Set.toList
160 | |> List.foldl (Grid.map toMine increment identity) grid
161 | |> Initial coord
162 | , Cmd.none )
163 | False ->
164 | ( state.current, initCommand intBoard )
165 | in
166 | ( { model | mines = mines_, state = { initial = current, current = current } }, command )
167 |
168 | ( Click event, _ ) ->
169 | let
170 | current =
171 | apply event state.current
172 | ( index_, events_ ) =
173 | case current == state.current of
174 | True -> ( index, events )
175 | False -> ( index + 1, [ event ] |> (++) (List.take index events) )
176 | in
177 | ( { model | index = index_, state = { state | current = current }, events = events_ }, Cmd.none )
178 |
179 | ( Mouse value, _ ) ->
180 | ( { model | mouse = value }, Cmd.none )
181 |
182 | ( Replay value, _ ) ->
183 | let
184 | current =
185 | events
186 | |> List.take value
187 | |> List.foldl apply state.initial
188 | in
189 | ( { model | index = value, state = { state | current = current } }, Cmd.none )
190 |
191 | ( SetBoard board_, _ ) ->
192 | ( { model | board = ( intBoard, board_ ) }, Cmd.none )
193 |
194 | _ ->
195 | ( model, Cmd.none )
196 |
197 |
198 | -- APPLY
199 |
200 | apply : Event -> State -> State
201 | apply event state =
202 | case ( state, event ) of
203 | ( Initial mine grid, Clicked coord (Hidden value) ) ->
204 | let
205 | coord_ =
206 | case value of
207 | Mine -> coord
208 | Safe _ -> mine
209 | square1 =
210 | grid
211 | |> Grid.map (always False) isMine (always False) coord_
212 | |> List.concat
213 | |> List.filter identity
214 | |> List.length
215 | |> Hidden << Safe
216 | square2 =
217 | Hidden value
218 | |> Grid.mapRelationship (always square1) decrement identity coord coord_
219 | in
220 | grid
221 | |> Grid.map (always square1) decrement identity coord_
222 | |> Pending
223 | |> apply (Clicked coord square2)
224 |
225 | ( Initial mine grid, Flagged coord (Hidden value) ) ->
226 | grid
227 | |> Grid.mapIdentity (Marked value) coord
228 | |> Initial mine
229 |
230 | ( Initial mine grid, Flagged coord (Marked value) ) ->
231 | grid
232 | |> Grid.mapIdentity (Hidden value) coord
233 | |> Initial mine
234 |
235 | ( Pending grid, Clicked coord (Hidden (Safe 0)) ) ->
236 | grid
237 | |> clickNeighbors coord
238 | |> List.foldl apply (grid |> Grid.mapIdentity (Safe 0 |> Exposed) coord |> check)
239 |
240 | ( Pending grid, Clicked coord (Hidden Mine) ) ->
241 | grid
242 | |> Grid.mapIdentity (Exposed Mine) coord
243 | |> Failure
244 |
245 | ( Pending grid, Clicked coord (Hidden value) ) ->
246 | grid
247 | |> Grid.mapIdentity (Exposed value) coord
248 | |> check
249 |
250 | ( Pending grid, Flagged coord (Hidden value) ) ->
251 | grid
252 | |> Grid.mapIdentity (Marked value) coord
253 | |> Pending
254 |
255 | ( Pending grid, Flagged coord (Marked value) ) ->
256 | grid
257 | |> Grid.mapIdentity (Hidden value) coord
258 | |> Pending
259 |
260 | ( Pending grid, Special coord (Exposed (Safe value)) ) ->
261 | let
262 | flagged =
263 | grid
264 | |> Grid.map (always False) marked (always False) coord
265 | |> List.concat
266 | |> List.filter identity
267 | |> List.length
268 | in
269 | case flagged == value of
270 | True ->
271 | grid
272 | |> clickNeighbors coord
273 | |> List.sortWith minesFirst
274 | |> List.foldl apply (Pending grid)
275 | False ->
276 | grid |> Pending
277 |
278 | _ -> state
279 |
280 | mapValue : (Int -> Int) -> Square -> Square
281 | mapValue f square =
282 | case square of
283 | Hidden (Safe value) ->
284 | f value |> Safe |> Hidden
285 | Marked (Safe value) ->
286 | f value |> Safe |> Marked
287 | _ ->
288 | square
289 |
290 | increment : Square -> Square
291 | increment =
292 | mapValue <| (+) 1
293 |
294 | decrement : Square -> Square
295 | decrement =
296 | mapValue <| (+) -1
297 |
298 | clickNeighbors : Coord -> Grid Square -> List Event
299 | clickNeighbors coord =
300 | Grid.indexedMap (always Nothing) click (always Nothing) coord
301 | >> List.concat
302 | >> List.filterMap identity
303 |
304 | click : ( Coord, Square ) -> Maybe Event
305 | click ( coord, square ) =
306 | case square of
307 | Hidden _ -> Just (Clicked coord square)
308 | _ -> Nothing
309 |
310 | check : Grid Square -> State
311 | check grid =
312 | case grid |> List.concat |> List.all found of
313 | True -> Success grid
314 | False -> Pending grid
315 |
316 | found : Square -> Bool
317 | found square =
318 | case square of
319 | Hidden Mine -> True
320 | Marked Mine -> True
321 | Exposed (Safe _) -> True
322 | _ -> False
323 |
324 | hidden : Square -> Bool
325 | hidden square =
326 | case square of
327 | Hidden _ -> True
328 | _ -> False
329 |
330 | marked : Square -> Bool
331 | marked square =
332 | case square of
333 | Marked _ -> True
334 | _ -> False
335 |
336 | exposed : Square -> Bool
337 | exposed square =
338 | case square of
339 | Exposed _ -> True
340 | _ -> False
341 |
342 | isMine : Square -> Bool
343 | isMine square =
344 | case square of
345 | Hidden Mine -> True
346 | Marked Mine -> True
347 | _ -> False
348 |
349 | toMine : Square -> Square
350 | toMine square =
351 | case square of
352 | Marked _ -> Marked Mine
353 | _ -> Hidden Mine
354 |
355 | minesFirst : Event -> Event -> Order
356 | minesFirst e1 e2 =
357 | case ( e1, e2 ) of
358 | ( Clicked _ (Hidden Mine), _ ) -> LT
359 | ( _, Clicked _ (Hidden Mine) ) -> GT
360 | _ -> EQ
361 |
362 |
363 | -- VIEW
364 |
365 | view : Model -> Document Msg
366 | view model =
367 | { title = "Minesweeper"
368 | , body = [ render model ]
369 | }
370 |
371 | render : Model -> Html Msg
372 | render { index, clock, mouse, board, state, events } =
373 | let
374 | grid_ =
375 | case state.current of
376 | Initial _ grid -> grid
377 | Pending grid -> grid
378 | Success grid -> grid
379 | Failure grid -> grid
380 | emoji =
381 | case ( state.current, mouse ) of
382 | ( Initial _ _, True ) -> "😮"
383 | ( Initial _ _, False ) -> "🙂"
384 | ( Pending _, True ) -> "😮"
385 | ( Pending _, False ) -> "🙂"
386 | ( Success _, _ ) -> "😎"
387 | ( Failure _, _ ) -> "😵"
388 | in
389 | div []
390 | [ renderGrid grid_ emoji
391 | , renderFoot (board |> Tuple.second) clock grid_ index (events |> List.length)
392 | ]
393 |
394 | renderGrid : Grid Square -> String -> Html Msg
395 | renderGrid grid emoji =
396 | div []
397 | [ h1
398 | [ style "text-align" "center"
399 | , style "font-size" "48px"
400 | , onClick Reset
401 | ]
402 | [ text emoji ]
403 | , table tableStyle
404 | (grid
405 | |> List.indexedMap (List.indexedMap << Grid.withCoord renderSquare)
406 | |> List.map (tr trStyle))
407 | ]
408 |
409 | renderSquare : Coord -> Square -> Html Msg
410 | renderSquare coord square =
411 | case square of
412 | Hidden _ ->
413 | td tdStyle
414 | [ hiddenButtonAttributes coord square |> renderButton "" "" ]
415 | Marked _ ->
416 | td tdStyle
417 | [ hiddenButtonAttributes coord square |> renderButton "" "🚩" ]
418 | Exposed Mine ->
419 | td tdStyle
420 | [ exposedButtonAttributes coord square |> renderButton "" "💣" ]
421 | Exposed (Safe 1) ->
422 | td tdStyle
423 | [ exposedButtonAttributes coord square |> renderButton "blue" "1" ]
424 | Exposed (Safe 2) ->
425 | td tdStyle
426 | [ exposedButtonAttributes coord square |> renderButton "green" "2" ]
427 | Exposed (Safe 3) ->
428 | td tdStyle
429 | [ exposedButtonAttributes coord square |> renderButton "red" "3" ]
430 | Exposed (Safe 4) ->
431 | td tdStyle
432 | [ exposedButtonAttributes coord square |> renderButton "darkblue" "4" ]
433 | Exposed (Safe 5) ->
434 | td tdStyle
435 | [ exposedButtonAttributes coord square |> renderButton "maroon" "5" ]
436 | Exposed (Safe 6) ->
437 | td tdStyle
438 | [ exposedButtonAttributes coord square |> renderButton "darkcyan" "6" ]
439 | Exposed (Safe 7) ->
440 | td tdStyle
441 | [ exposedButtonAttributes coord square |> renderButton "purple" "7" ]
442 | Exposed (Safe 8) ->
443 | td tdStyle
444 | [ exposedButtonAttributes coord square |> renderButton "grey" "8" ]
445 | _ ->
446 | td tdStyle
447 | [ exposedButtonAttributes coord square |> renderButton "" "" ]
448 |
449 | renderButton : String -> String -> List (Html.Attribute Msg) -> Html Msg
450 | renderButton color text_ attributes =
451 | button
452 | attributes
453 | [ strong [ style "color" color ] [ text text_ ] ]
454 |
455 | renderFoot : Board String -> Int -> Grid Square -> Int -> Int -> Html Msg
456 | renderFoot board clock grid index events =
457 | div
458 | [ style "bottom" "0"
459 | , style "position" "fixed"
460 | , style "text-align" "center"
461 | , style "width" "100%"
462 | , style "font-family" "monospace"
463 | , style "background" "rgb(239, 239, 239)"
464 | , style "border-top" "1px solid rgb(169, 169, 169)"
465 | ]
466 | [ table [ style "width" "100%", style "padding" "10px 15px" ]
467 | [ tr []
468 | [ td [ style "width" "20%" ] [ renderInputLeft board ]
469 | , td [ style "width" "60%" ] [ renderSlider index events ]
470 | , td [ style "width" "20%" ] [ renderInputRight board ]
471 | ]
472 | ]
473 | , table
474 | [ style "width" "100%"
475 | , style "padding" "5px 15px"
476 | , style "font-size" "14px"
477 | , style "background" "rgb(219, 219, 219)"
478 | ]
479 | [ tr []
480 | [ td [ style "text-align" "left" ] [ renderStatsLeft (grid |> List.concat) clock index events ]
481 | , td [ style "text-align" "right" ] [ renderStatsRight ]
482 | ]
483 | ]
484 | ]
485 |
486 | renderInputLeft : Board String -> Html Msg
487 | renderInputLeft board =
488 | span []
489 | [ input
490 | [ type_ "text"
491 | , style "color" "rgb(20, 20, 20)"
492 | , style "text-align" "center"
493 | , style "font-size" "20px"
494 | , style "background" "transparent"
495 | , style "border-width" "0px 0px 2px 0px"
496 | , style "border-color" "rgb(20, 20, 20)"
497 | , style "border-radius" "0px"
498 | , style "width" "36px"
499 | , value board.cols
500 | , onInput (\cols -> SetBoard { board | cols = cols })
501 | , onBlur Reset
502 | ] []
503 | , span [ style "font-size" "18px" ] [ text " ✖️ " ]
504 | , input
505 | [ type_ "text"
506 | , style "color" "rgb(20, 20, 20)"
507 | , style "text-align" "center"
508 | , style "font-size" "20px"
509 | , style "background" "transparent"
510 | , style "border-width" "0px 0px 2px 0px"
511 | , style "border-color" "rgb(20, 20, 20)"
512 | , style "border-radius" "0px"
513 | , style "width" "36px"
514 | , value board.rows
515 | , onInput (\rows -> SetBoard { board | rows = rows })
516 | , onBlur Reset
517 | ] []
518 | ]
519 |
520 | renderSlider : Int -> Int -> Html Msg
521 | renderSlider index events =
522 | input
523 | [ type_ "range"
524 | , Html.Attributes.min "0"
525 | , Html.Attributes.max (events |> String.fromInt)
526 | , value (index |> String.fromInt)
527 | , style "-webkit-appearance" "none"
528 | , style "background" "rgb(219, 219, 219)"
529 | , style "border" "2px solid rgb(169, 169, 169)"
530 | , style "border-radius" "20px"
531 | , style "width" "100%"
532 | , style "height" "20px"
533 | , onInput (Replay << Maybe.withDefault index << String.toInt)
534 | ] []
535 |
536 | renderInputRight : Board String -> Html Msg
537 | renderInputRight board =
538 | span []
539 | [ input
540 | [ type_ "text"
541 | , style "color" "rgb(20, 20, 20)"
542 | , style "text-align" "center"
543 | , style "font-size" "20px"
544 | , style "background" "transparent"
545 | , style "border-width" "0px 0px 2px 0px"
546 | , style "border-color" "rgb(20, 20, 20)"
547 | , style "border-radius" "0px"
548 | , style "width" "36px"
549 | , value board.mines
550 | , onInput (\mines -> SetBoard { board | mines = mines })
551 | , onBlur Reset
552 | ] []
553 | , span [ style "font-size" "18px" ] [ text " 💣 " ]
554 | ]
555 |
556 | renderStatsLeft : List Square -> Int -> Int -> Int -> Html Msg
557 | renderStatsLeft squares clock index events =
558 | let
559 | hidden_ =
560 | squares
561 | |> List.filter hidden
562 | |> List.length
563 | |> String.fromInt
564 | exposed_ =
565 | squares
566 | |> List.filter exposed
567 | |> List.length
568 | |> String.fromInt
569 | flagged_ =
570 | squares
571 | |> List.filter marked
572 | |> List.length
573 | |> String.fromInt
574 | in
575 | text
576 | ("event = " ++ (String.fromInt index) ++ " of " ++ (String.fromInt events)
577 | ++ " | hidden = " ++ hidden_
578 | ++ " | exposed = " ++ exposed_
579 | ++ " | flagged = " ++ flagged_
580 | ++ " | time = " ++ (String.fromInt clock) ++ "s")
581 |
582 | renderStatsRight : Html Msg
583 | renderStatsRight =
584 | a [ href "https://github.com/dfarr/minesweeper" ] [ text "dfarr/minesweeper" ]
585 |
586 |
587 | -- STYLE
588 |
589 | tableStyle : List (Html.Attribute Msg)
590 | tableStyle =
591 | [ style "margin" "0 auto 120px auto"
592 | , attribute "cellpadding" "0"
593 | , attribute "cellspacing" "0"
594 | , attribute "border" "1"
595 | , onRightClick NoOp
596 | ]
597 |
598 | trStyle : List (Html.Attribute Msg)
599 | trStyle =
600 | [ style "padding" "0px"
601 | , style "margin" "0px"
602 | ]
603 |
604 | tdStyle : List (Html.Attribute Msg)
605 | tdStyle =
606 | [ style "padding" "0px"
607 | , style "margin" "0px"
608 | , style "width" "35px"
609 | , style "height" "35px"
610 | ]
611 |
612 | buttonStyle : List (Html.Attribute Msg)
613 | buttonStyle =
614 | [ style "font-size" "18px"
615 | , style "padding" "0px"
616 | , style "margin" "0px"
617 | , style "border" "0px"
618 | , style "width" "100%"
619 | , style "height" "100%"
620 | ]
621 |
622 | hiddenButtonStyle : List (Html.Attribute Msg)
623 | hiddenButtonStyle =
624 | buttonStyle ++
625 | [ style "background" "rgb(239, 239, 239)" ]
626 |
627 | exposedButtonStyle : List (Html.Attribute Msg)
628 | exposedButtonStyle =
629 | buttonStyle ++
630 | [ style "background" "rgb(219, 219, 219)" ]
631 |
632 | buttonActions : List (Html.Attribute Msg)
633 | buttonActions =
634 | [ onMouseDown (Mouse True)
635 | , onMouseUp (Mouse False)
636 | ]
637 |
638 | hiddenButtonActions : Coord -> Square -> List (Html.Attribute Msg)
639 | hiddenButtonActions coord square =
640 | buttonActions ++
641 | [ onClick (Clicked coord square |> Click)
642 | , onRightClick (Flagged coord square |> Click)
643 | ]
644 |
645 | exposedButtonActions : Coord -> Square -> List (Html.Attribute Msg)
646 | exposedButtonActions coord square =
647 | buttonActions ++
648 | [ onRightClick (Special coord square |> Click) ]
649 |
650 | hiddenButtonAttributes : Coord -> Square -> List (Html.Attribute Msg)
651 | hiddenButtonAttributes coord square =
652 | hiddenButtonActions coord square
653 | ++ hiddenButtonStyle
654 |
655 | exposedButtonAttributes : Coord -> Square -> List (Html.Attribute Msg)
656 | exposedButtonAttributes coord square =
657 | exposedButtonActions coord square
658 | ++ exposedButtonStyle
659 |
660 |
661 | -- CLICKS
662 |
663 | type alias MsgWithOptions =
664 | { message: Msg
665 | , preventDefault: Bool
666 | , stopPropagation: Bool
667 | }
668 |
669 | onRightClick : Msg -> Html.Attribute Msg
670 | onRightClick message =
671 | custom "contextmenu" (msgWithOptionsDecoder message)
672 |
673 | msgWithOptionsDecoder : Msg -> Decoder MsgWithOptions
674 | msgWithOptionsDecoder message =
675 | Decode.map (toMsgWithOptions message) (Decode.field "button" Decode.int)
676 |
677 | toMsgWithOptions : Msg -> Int -> MsgWithOptions
678 | toMsgWithOptions message id =
679 | case id of
680 | 2 -> { message = message, preventDefault = True, stopPropagation = True }
681 | _ -> { message = NoOp, preventDefault = False, stopPropagation = False }
682 |
--------------------------------------------------------------------------------