Minesweeper game using CSS

HAML
// Match the SCSS variables here - rows = 9 - cols = 9 - count = rows * cols %form - count.times do |i| %input{:type => "checkbox", :id => "c#{i+1}"} %input{:type => "checkbox", :id => "f#{i+1}"} %input(type="radio" name="mode" id="modeMine" checked) %input(type="radio" name="mode" id="modeFlag") .actionSelector %label(for="modeMine") ⛏ %label(for="modeFlag") ???? .grid - count.times do |i| %label{:for => "c#{i+1}"} .flags - count.times do |i| %label{:for => "f#{i+1}"} %button.error(type="reset" tabindex="-1") Ooohhh ???? %br Click to try again %button.victory(type="reset" tabindex="-1") ????????✔???????????? %br Click to restart .infos .counter .timer - 3.times do .digit .separator - 2.times do .digit
SCSS
@import 'https://fonts.googleapis.com/css?family=Roboto+Mono:700'; // medium difficulty (16x16x40) takes a long time to compile and is slow to play :/ $rows: 9; $cols: 9; // don't forget to make the Haml match $mines: 10; $size: 24px; $colors: #0000ff, #008100, #ff1300, #000083, #810500, #2a9494, #000000, #808080; $count: $rows * $cols; @if $mines > $count { @error "More mines than blocks"; } $pos: (); @for $i from 0 to $mines { $mine: 0; $continue: true; @while $continue != null { $mine: random($count); $continue: index($pos, $mine); } $pos: append($pos, $mine); } body { min-height: 100vh; // strange margin behaviour thing cancelling padding: 1px; box-sizing: border-box; background: teal url(http://core0.staticworld.net/images/article/2014/04/windows-xp-bliss-start-screen-100259803-orig.jpg) center / cover no-repeat; counter-reset: mines $mines; } form { display: flex; flex-flow: column nowrap; align-items: center; } input { visibility: hidden; position: absolute; top: -99px; left: -99px; } input[id^="f"]:checked { counter-increment: mines -1; } .infos { order: 2; display: flex; flex-flow: row nowrap; justify-content: space-between; width: $cols * $size; } .timer { font-family: "Roboto Sans", monospace; font-size: 0; // Prevent white-space background: #ccc; border: 1px solid #808080; height: 2.25rem; line-height: 2.25rem; padding: 0 .5rem; .separator { display: inline-block; vertical-align: middle; font-size: 1rem; &:before { content: ':'; } } @keyframes digit { from { top: 0; } to { top: -1000%; } } @keyframes digitTo6 { from { top: 0; } to { top: -600%; } } @keyframes extend { from { width: 0; } 10%, to { width: auto; } } .digit { display: inline-block; position: relative; overflow: hidden; vertical-align: middle; font-size: 1rem; &:before { content: '0'; visibility: hidden; } // Size &:after { content: '0 \A 1 \A 2 \A 3 \A 4 \A 5 \A 6 \A 7 \A 8 \A 9'; position: absolute; top: 0; left: 0; animation: digit 1s steps(10) infinite paused; } &:nth-last-child(1):after { animation-duration: 10s; } &:nth-last-child(2):after { content: '0 \A 1 \A 2 \A 3 \A 4 \A 5'; animation-name: digitTo6; animation-timing-function: steps(6); animation-duration: 60s; } &:nth-last-child(4):after { animation-duration: 600s; } &:nth-last-child(5):after { animation-duration: 6000s; } &:nth-last-child(6) { width: 0; animation: extend 60000s steps(1) infinite paused; &:after { animation-duration: 60000s; } } } } .counter { display: inline-block; border: 1px solid #808080; background: #ccc; padding: 0 .5rem; font-size: 1.25rem; font-family: "Roboto Sans", monospace; height: 2.25rem; line-height: 2.25rem; &:before { content: '????'; font-size: 1rem; margin-right: .5em; } &:after { content: counter(mines); } } input[id^="c"]:checked ~ .infos .timer .digit { &, &:after { animation-play-state: running; } } .actionSelector { order: 1; text-align: center; margin: 10px; cursor: default; label { display: inline-block; position: relative; width: 1.8em; height: 1.8em; text-align: center; line-height: 1.8em; cursor: pointer; &:before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; transform: scale(0); border-radius: 50%; background: rgba(210, 210, 210, .8); box-sizing: border-box; border: 1px solid #808080; transition: transform .3s, border-radius .3s; transition-timing-function: cubic-bezier(.75,1.75,.75,.75); z-index: -1; } } } #modeMine:checked ~ .actionSelector label[for="modeMine"], #modeFlag:checked ~ .actionSelector label[for="modeFlag"] { cursor: default; &:before { transform: scale(1); border-radius: 2px; } } .grid { order: 3; user-select: none; position: relative; margin: 10px auto; width: $cols * 1em; height: $rows * 1em; font-size: $size; display: flex; flex-flow: row wrap; border: solid #808080; border-width: 1px 0 0 1px; label { display: block; position: relative; width: 1em; height: 1em; background: #c0c0c0; box-sizing: border-box; border: solid #808080; border-width: 0 1px 1px 0; flex: 0 0 (100% / $cols); overflow: hidden; cursor: pointer; pointer-events: none; &:before { content: ''; font-size: .9rem; font-family: 'Roboto Mono', monospace; font-weight: bold; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } &:after { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; box-sizing: border-box; background: #c0c0c0; border: 2px outset #ececec; font-size: .75rem; text-align: center; pointer-events: auto; } &:active:after { background: #bdbdbd; border: solid #999; border-width: 2px 0 0 2px; } } .flags { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-flow: row wrap; opacity: 0; visibility: hidden; } .error, .victory { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 0, 0, .75); color: #fff; font-family: "Century Gothic",CenturyGothic,AppleGothic,sans-serif; border: none; opacity: 0; visibility: hidden; transition: opacity .3s, visibility .3s; } .victory { background: rgba(0, 10, 0, .75); } } #modeFlag:checked ~ .grid .flags { visibility: visible; } #modeMine:checked ~ .grid:active ~ .infos .counter:before { content: '????' !important; } @function makeFlagCountSelector($n) { $sel: (); @for $i from 1 through $n { $sel: append($sel, "input[id^="f"]:checked ~ ", space); } @return $sel; } #{makeFlagCountSelector(1)} .infos .counter:before { content: '????'; } #{makeFlagCountSelector(round($mines / 3))} .infos .counter:before { content: '????'; } #{makeFlagCountSelector(round($mines / 2))} .infos .counter:before { content: '????'; } #{makeFlagCountSelector(round($mines * 2 / 3))} .infos .counter:before { content: '????'; } #{makeFlagCountSelector(round($mines * 3 / 4))} .infos .counter:before { content: '????'; } // almost there #{makeFlagCountSelector($mines - 1)} .infos .counter:before { content: '????'; } // 0 mines left but no victory screen : uh oh #{makeFlagCountSelector($mines)} .infos .counter:before { content: '????'; } // now you're just not even trying #{makeFlagCountSelector($mines + 1)} .infos .counter:before { content: '????'; } @if round(($count + $mines) / 2) > $mines + 1 { #{makeFlagCountSelector(round(($count + $mines) / 2))} .infos .counter:before { content: '????'; } } @if $count > $mines + 1 { // I mean ... yeah ... #{makeFlagCountSelector($count)} .infos .counter:before { content: '????'; } } $vals: (); $victorySelector: (); @for $i from 1 through $count { $val: 0; @if index($pos, $i) != null { $val: -1; .grid label:nth-child(#{$i}):before { content: '????'; font-size: .75rem; } #c#{$i}:checked { ~ .grid { .error { opacity: 1; visibility: visible; } > label:after { visibility: hidden; } label:nth-child(#{$i}) { background-color: #f00; } &:active ~ .infos .timer .digit { &, &:after { animation: none; } } } ~ .infos { .counter:before { content: '????' !important; } .timer .digit { &, &:after { animation-play-state: paused; } } } } } @else { $x: ($i - 1) % $cols; $y: floor(($i - 1) / $cols); $neighbours: 0; @for $dx from -1 through 1 { @for $dy from -1 through 1 { $nx: $x + $dx; $ny: $y + $dy; @if ($dx != 0 or $dy != 0) and $nx >= 0 and $nx < $cols and $ny >= 0 and $ny < $rows { $ni: $ny * $cols + $nx + 1; @if index($pos, $ni) { $neighbours: $neighbours + 1; } } } } $val: $neighbours; @if $neighbours > 0 { .grid label:nth-child(#{$i}):before { content: '#{$neighbours}'; color: nth($colors, $neighbours); } } } $vals: append($vals, $val); $victorySelector: append($victorySelector, "#f#{$i}" + if($val == -1, ":checked", ":not(:checked)") + " ~", space); } #{$victorySelector} .grid { > label:after { visibility: hidden; } .victory { opacity: 1; visibility: visible; } &:active ~ .infos .timer .digit { &, &:after { animation: none; } } } #{$victorySelector} .infos { .counter:before { content: '????'; } .timer .digit { &, &:after { animation-play-state: paused; } } } $handled: (); @function uncoverSelector($i, $direct: true) { $val: nth($vals, $i); $psel: if($direct or $val == 0, ("#c#{$i}:checked"), ()); $sel: ("label:nth-child(#{$i})"); @if $val == 0 { @if index($handled, $i) != null { @return null; } $handled: append($handled, $i) !global; $x: ($i - 1) % $cols; $y: floor(($i - 1) / $cols); @for $dx from -1 through 1 { @for $dy from -1 through 1 { $nx: $x + $dx; $ny: $y + $dy; @if ($dx != 0 or $dy != 0) and $nx >= 0 and $nx < $cols and $ny >= 0 and $ny < $rows { $result: uncoverSelector($ny * $cols + $nx + 1, false); // $result: null; @if $result != null { $psel: join($psel, nth($result, 1), comma); $sel: join($sel, nth($result, 2), comma); } } } } } @return ($psel, $sel); } @for $i from 1 through $count { #f#{$i}:checked { ~ .grid label:nth-child(#{$i}):after { content: '????'; pointer-events:none; visibility: visible !important; } ~ #modeFlag:checked ~ .grid .flags label:nth-child(#{$i}):after { pointer-events: auto; } } $result: uncoverSelector($i); @if $result != null { // using @each splits the selector into smaller blocks // (if a selector is too long it can break in some browsers) @each $psel in nth($result, 1) { #{$psel} ~ .grid { #{nth($result, 2)} { &:after { pointer-events: none; visibility: hidden; } } } } } }
JAVASCRIPT
Expand for more options Login