Skip to content

Instantly share code, notes, and snippets.

@idettman
Created May 16, 2019 04:46
Show Gist options
  • Select an option

  • Save idettman/72471ab4048af9c569579355a26e51fd to your computer and use it in GitHub Desktop.

Select an option

Save idettman/72471ab4048af9c569579355a26e51fd to your computer and use it in GitHub Desktop.
Pure CSS minesweeper
// 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

Pure CSS minesweeper

I wanted to see if I could do a fully functioning minesweeper with CSS only. And it works !
I'm pretty satisfied with the less than 300 400 (with the timer) lines of Sass, even if it quickly gets really slow to compile and to play with bigger grids.

The win detection is based on the flags. I know it's not like the actual game but it makes the logic much easier. The grid is only randomized at compile time (when you hit run) not between restarts (it would make the CSS huge, it is already big as it is).

Yes, emojis were used and maybe even abused.

Issues : in Edge, the victory screen does not trigger.

A Pen by Bali Balo on CodePen.

License.

@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(https://www.hdwallpapers.in/walls/windows_xp_bliss-wide.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;
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment