├── .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 | [![GitHub Pages](https://github.com/dfarr/minesweeper/workflows/GitHub%20Pages/badge.svg)](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 | ![Minesweeper](minesweeper.gif) 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 | --------------------------------------------------------------------------------