I Built the Best Minesweeper Online
Foss, clean and responsive.
Updated on Jul 30 — see the very end!
Updated on Aug 12 — a new ZiNi variant!
To-do list, tic-tac-toe, snake, Tetris — all great baby's first program choices for sure. However, I was personally kind of frustrated with the selection of Minesweeper reimplementations on the web, so here we are.
Check it out at https://minesweepe.rs — a static site with no ads, no tracking, no writing your record times into your localStorage without explicit permission (hey GDPR).
The code is libre and available at https://git.sr.ht/~eppuh/Minesweepe.rs .
Now, to address the title: the notion of tHe BeSt is obviously highly subjective; the following are a few great minefields available online for your sweeping pleasure:
-
minesweeperonline.com
- the old school version with Kamil Murański's expert times all the way from 2013!
-
minesweeper.online
- currently the most popular site.
-
mineswifter.com
- provides daily challenges.
-
llamasweeper.com
- a new entrant still in development with unique features. I learned of this site after having mostly finished mine and used their board editor to verify my 3BV code <3 They also have a page listing many, many more sites and resources for Minesweeper enthusiasts.
Those are all mostly snappy and provide useful features. Yet, each has inconsistencies or some behaviour which I wish were different. As this post is about my creation, I will focus on what it adds to the table.
The Frustrations
Responsive design
With every version I could find online, one of my biggest issues was the lack of responsive design. So, when you first open up my version, whether that be on desktop or mobile, you are greeted with the screen as full as possible of that which you came for.
Mobile view. See how the board goes all the way to the edge.
Desktop view.
The field will fill up your browser window up to whichever border it hits first. If that scares you, there is a zoom slider in the settings.
Most of the magic happens in this snippet of css:
:root {
/* Level selection adjusts w & h */
--width: 9;
--height: 11;
--vh: 100dvh;
--vw: 100dvw;
}
.field {
display: grid;
grid-template-columns:
repeat(var(--width), 1fr);
grid-template-rows:
repeat(var(--height), 1fr);
width:var(--vw);
height:var(--vh);
max-width: calc(
var(--vh) *
(var(--width) / var(--height))
);
max-height: calc(
var(--vw) *
(var(--height) / var(--width))
);
}
Furthermore, the expert level will automatically swap from 30x16 to 16x30 when your window is taller than it is wide.
For a tad more extra space or concentration, there is a fullscreen button in the settings. It works on everything but darn Safari — the Internet Explorer of the 2020s — which does not allow such dangerous options for anything but video elements. To hide the browser UI elements on Apple, you need to install the site as a Progressive Web App (PWA).
Offline & multiplatform usage
Of the four mentioned "competitors" at the beginning, two provide a PWA. Yet, they all still require an internet connection! Fear not, as my PWA works fully offline. The installation happens via "Add to home screen" or some similar option in the browser, in case you weren't aware.
There are of course better, native applications available, but the strength of a PWA is in its universality: it works the same both on desktop and mobile, on iOS and Android.
Also, I personally try to avoid installing closed source software as much as possible. There seems to be one good native open source clone, Antimine , but it's exclusive to Android.
Full touch support
I want to see a reaction when I put my finger on the screen. I want to be able to slide my finger and not have the window scroll. I want multi-touch. Et voilà:
All animations follow touch and each finger.
The downside is that zooming is disabled, as that would conflict with multifinger sliding. This means that you aren't going to be able to play large levels on the tiniest of screens, but I find expert playable with my phone on full screen.
I must note that llamasweeper.com has almost all of this as well, but as of this writing there's a technical limitation holding the animation back.
Graphics
You're looking at CSS and SVGs, which result in sharp retro graphics. Everything copies the original except for the missing game area borders — I thought they would be annoying to implement and just waste space. Overall, of the ones trying to mimick the look, I think mine looks the cleanest.
A little detail I would like to point out is the crossed-out mine graphic. In the original game, the red cross is not centered to the mine, so I went and "fixed" that. It does not look odd in the zoomed-in image below, but in the game I found the off-centered one to stick out distractingly.
Original on the left, mine on the right.
Additionally, I made sure that both the mine counter and the timer can grow just a bit if necessary. Here I want to shout out minesweeper.us , as they have also solved this issue in their own way.
Flags go brrr.
The way I wrote this behaviour means the maximum size of a custom field is 100x100, because that guarantees the counter cannot go below -9999, which the code would not be able to handle.
Oh and the minimum height is 1, which is sort of funny.
One last small graphical flair is that the menus mimick Windows 3.11, which is where Minesweeper was first introduced. It doesn't really affect gameplay in any way and is not that accurate, but it's there.
Windows used to look good.
The Algorithms
The code is not the prettiest nor the most performant, but it does the job and you're going to be seeing some of it. Scala 3 boo! 👻
Since recursion is bad in the Javascript land, I use a simple breadth-first search to do the flood fill on empty cells. Here's the code, horizontally squeezed a bit to make it fit on a mobile screen:
private val q =
mutable.Queue.empty[Cell]
def propagate(cell: Cell) =
if cell.count == 0 {
q.enqueue(cell)
while cellsLeft > 0
&& q.nonEmpty
do
val current = q.dequeue()
for n <- current.neighbours do
if n.state == SafeClosed {
n.tap()
if n.count == 0 {
q.enqueue(n)
}
}
end for
end while
}
if cellsLeft == 0 {
winningProcedure()
}
end propagate
If you're familiar with BFS, there's nothing new here. It finds all of the connected cells with a count of 0 and taps them for you.
The problem with BFS though is that there are lots of allocations, so using it in the 3BV calculation crashes the browser. To solve this, I had to opt for a preallocated array and a moving pointer:
lazy val bbbv: Int =
val stack =
Array.fill[Cell](rows*cols)(null)
var clicks = 0
var i = 0
while i < matrix.length do
val c = matrix(i)
if c.state == SafeClosed
|| c.state == SafeOpened
|| c.state == Flagged(SafeClosed)
{
if c.count == 0
&& !c.accountedFor {
clicks += 1
var stackPointer = 0
stack(stackPointer) = c
stackPointer += 1
c.accountedFor = true
while stackPointer > 0 do
stackPointer -= 1
val cur =
stack(stackPointer)
var j = 0
while
j < cur.neighbours.length
do
val n = cur.neighbours(j)
if n.count == 0
&& !n.accountedFor {
n.accountedFor = true
stack(stackPointer) = n
stackPointer += 1
}
j += 1
end while
end while
else if c.count > 0
&& !c.neighbours
.exists(_.count == 0)
{
clicks +=1
}
}
i += 1
end while
clicks
end bbbv
The inner if
counts all zero areas by doing a flood fill, while
the else if
has the easier job of counting all non-zero cells without a zero as a neighbour.
But the algorithm to which I paid the most attention was the one for the distribution of mines:
A naïve way to go about things is to have a while-loop pick a random number until all the mines are placed, which I suppose is O(m) where m is the number of mines. Initially, this was what I did upon the first click, and it worked fine. The thing is, if you create a mine-dense custom field, as the while-loop places more and more mines, it gets increasingly difficult to randomly pick a free cell. I was not happy.
Next, I went with shuffling the array holding the cells, but this has the opposite problem: even for a sparse field in which m << n, where n is the length of the array, it will do an O(n) shuffle. That is far from optimal.
Finally, a realisation: I can place the mines at the beginning of the array and stop the Fisher-Yates shuffle after just m loops, meaning I get the O(m) of the first attempt with the niceties of the second! I tested this empirically and the randomness of the shuffle seems to hold.
The final code block, I promise, with even some comments within:
private def init() =
// add mines
for i <- 0 until nofmines do
arr(i).state = MineClosed
end for
// shuffle with Fisher-Yates
for i <- 0 until nofmines do
val j = i
+ r.nextInt(arr.length-i)
val t = matrix(i)
matrix(i) = matrix(j)
matrix(j) = t
end for
// add numbers
for i <- matrix.indices do
val cell = matrix(i)
cell.row = i / cols
cell.col = i % cols
numberize(cell)
end for
end init
I went for placing the mines as soon as a new field is requested, instead of at the point of the first click, in order to lower latency where it matters. This decision lead to perhaps the final algorithmic problem: safe first click.
I believe in the original game, only the single cell you click on is guaranteed to be safe and any mine that might have been there goes to the first available free space starting from top left. For better playability, especially in expert mode, I wanted the 3x3 area around the first click to be safe, but that would mean that there is an alteration to the randomness of mine placement; the top left will always be more dangerous.
Thus, an idea: for each mine in the 3x3, pick a random index in the array from which to start the hunt for a new abode and loop back to the start if necessary. I find this to be a simple but effective improvement over the original logic.
Update Aug 12: There is this thing called ZiNi , which aims to calculate the lowest number of clicks required for a board to be solved. Its variant, Human ZiNi (HZini), is claimed to be more humanlike and comparable to an actual solve.
However, every ZiNi method starts with an advantage over any fleshly being, that being Unfair Prior Knowledge (UPK). In the case of HZiNi, the algorithm starts by opening every single zero area, which is basically impossible for a human to achieve outside of the beginner level.
Enter More Human ZiNi (MH.ZiNi). This possibly unique variant starts from where the player started and then goes on from there in a similar manner to HZiNi:
- Calculate premium for each open cell
- Find the one with the highest premium
- Chord if premium ≥ 0
- Else open a zero if any is touching our opened area
- Else open a possible safe cell touching our opened area
- Else open a random zero anywhere if available (heavy use of UPK here)
- Else open the first closed cell going from top-left to bottom-right
- Go back to 1 if closed cells remain
MH.ZiNi adds a a nice, sometimes beatable goal for the player without me having to implement an actual solver. Its play is so much more human that it can even produce a result higher than 3BV, but that happens almost exclusively on beginner level with very low 3BV scores.
The Missing Features
This is not a perfection creation. I am just quickly going to list some of the worst shortcomings:
- Lack of a no-guessing mode
- many papers have been written on how to effectively create solvable fields, indicating that it is not a simple problem. I myself have no desire for such a feature anyway.
- Lack of a global leaderboard
- there's no server/backend with which to communicate.
- Lack of stats for custom level
- maybe later.
- Lack of import/export
- this too I could implement at some future time.
- Lack of replay
- I suppose adding import/export would alleviate this slightly.
- Lack of a zoom-in implementation for the poorly-sighted or the less-dexterous
- possible to add, but would require significant work.
- Lack of settings such as:
- how many milliseconds to hold touch to flag a cell (currently a very short time)
- When to vibrate when holding (only Chrome and derivaties support such an option)
Chording with something else besides left+right click on desktop (a Redditor pointed out that this means that touchpads can't chord!)
I should mention that some existing features break on some alternative browsers — even on some that are based on Blink. As an example, DuckDuckGo seems to ignore many of the ways I use to make touch work well. It is what it is.
I consider this a finished project, or at the very least something to which I am not thinking of returning anytime soon. It's open source — fork it! I'm not expecting anything, since nobody will read this, but I would probably be happy to merge cool stuff.
In summary
I did not mention it anywhere, but you can start a new game quickly with the space bar.
Anyway, what I added to the status quo:
- Responsive design
- Offline usage
- Touch optimisation
- Sharp, authentic graphics
- Attention to detail (subjective)
- Libre license!
Altogether, I am proud of the result: minesweepe.rs
Thanks for reading.
Update Jul 30
Here's some proof that the game is indeed playable.
My personal best on beginner level :)
Llama's sorcery with an edit to highlight the time.
Llama, the developer of llamasweeper.com and the #1 ranked player on beginner level by efficiency , had a heartwarming comment to add:
This is niche, but the touch performance is SO GOOD on mobile. I got a 10s game on int just now (will send screenshot later). I’m one of a few people that can play using both hands at once, so the multitouch feature is excellent for that, and makes it better even than lots of native apps.
Feels good man.