An image showcasing the user interface mimicking the original

I built a modern Minesweeper for the web

Sept 1, 2025

To-do list, tic-tac-toe, snake, Tetris — all great baby's first program choices for sure. I, however, was sort of frustrated with the selection of Minesweeper reimplementations on the web, so here we are.

Check it out at the memorable vanity domain https://minesweepe.rs, where you will be served a static site with no ads, no tracking, and no writing your record times into your localStorage without explicit permission (hey GDPR). The code is libre (AGPLv3) and available at
https://git.sr.ht/~eppuh/Minesweepe.rs. It's in Scala.

Now, the previous version of this post was titled "I Built the Best Minesweeper Online", and that is true for myself, but recently a new entrant appeared: https://llamasweeper.com. There's such a tremendous amount of effort behind it, the development is still ongoing, and it offers features which used to be unique to mine (and much more), therefore I'm giving the title of the best to them.

Below, I'm going to be focusing on the things which I added to the old guard. I've been thinking about rewriting the whole thing — maybe next year — so some details might not be the same by the time you're reading this. If you see something's changed, just know that things must be even better now :)

Responsive design

With every version I could find online, my biggest issue was the lack of responsive design. So, when you open up my version, whether that be on desktop or mobile, you are greeted with the screen full to the brim of that which you came for.

A screenshot from Chromium on Android with beginner level open
A screenshot from desktop Firefox with intermediate level open
↑ Interm. level on the desktop hits the horizontal edges first.
← Bgnr. level on my narrow phone reaches the sides first instead.

On a large desktop monitor this is of course not ideal, so there is a zoom slider in the settings.

For a tad more extra space or concentration, there is also 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

The PWA works offline! The installation happens via "Add to home screen" or some similar option in a compatible browser, in case you weren't aware. Fundamentally, Minesweeper is a solo game, so there shouldn't be a need for an internet connection.

I do understand why sites such as https://minesweeper.online require a connection, as they have global leaderboards, which requires some anti-cheating measures. People enjoy having a fair, social and competitive aspect to their games, but my version is there for the more casual playing sessions.

There are of course better, native applications available, which should all work offline as well, but the strength of a PWA is in its universality: it works the same both on desktop and mobile, on iOS and Android. It would be cool if someone were to make a truly multiplatform native version using the Qt framework or Flutter, but that would be a lot of effort for a game with a small player base, each member of which already having their good-enough favourite.

Proper touch support

I want to see the little smiley react worringly as I lay my finger on the screen. I want to be able to slide around and see the animation move along without the viewport scrolling anywhere. I want to add a second or even a third finger and have them all tracked independently. Et voilà:

All animations follow touch and each finger.

Why doesn't every version provide multitouch? Other than older tech, nicheness, or maybe an unfair advantage over a mouse, the big downside is that you cannot pan or zoom anymore, which means that you aren't going to be able to play very large levels on tiny screens. Thus, in its current iteration, the game is perhaps the most at its element on a tablet.

In fact, here's the developer of the aforementioned https://llamasweeper.com proving the potential of multiple inputs at once on a large screen:

A screenshot of an intermediate board solved in 10.585 seconds.

Llama's sorcery with an edit by me on the left to highlight the amazing time.

They had a nice 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.

Speaking of, llama's site has an interesting option of enabling scrolling when touching an empty opened cell — an idea which I might steal, though I've thought of other solutions as well; one could be to alter the portions of the CSS grid, but that's for the later rewrite.

Clean graphics

The original Minesweeper is a game with sharp retro graphics, so I do not want to see any curved lines or fuzziness. My version copies the original graphics quite faithfully and with crisp svgs.

A very small detail I adjusted was the crossed-out mine graphic. Originally, 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.

Two black mines with a red cross over them.

Original on the left, mine on the right.

Additionally, I made sure that both the mine counter and the timer can grow dynamically just a bit if necessary. The maximum sizes could just be calculated at the start as implemented by https://minesweeper.us, but this works and doesn't seem to be a thing anywhere else:

Flags go brrr.

The way I wrote this behaviour restricts the maximum size of a custom field to 100x100, because that guarantees the counter cannot go below -9999, which the code would not be able to handle. Oh, and as an aside, the minimum height is 1, which is sort of funny.

Further aside, many of the newer implementations do not allow flagging before a cell has been opened! I do not know the reason for that, as the original works like mine.

One last small graphical flair is that the menus mimick Windows 3.11, which is where Minesweeper was first introduced. It doesn't affect gameplay in any way and is not that accurate, but it's there.

The settings menu with options for chording, zoom and levels.

Windows used to look good.

The algorithms

These are not necessarily that interesting, so I'll try to keep it brief.

Since recursion is bad in the Javascript land, I use a simple breadth-first search (BFS) to do the flood fill on empty cells, while with 3BV I had to opt for a variation of BFS with a pre-allocated array and a moving pointer to keep things performant.

I paid some extra attention to the way in which mines should be distributed — not because there is any performance to be gained or lost there, but just because:

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 possibly has a growth rate of O(m log 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 it's just O(m). I tested this empirically and the randomness of the shuffle seems to hold. Here's the extremely simple code block, squeezed to fit on a mobile screen, just to have something to look at:

def init() =
  // add mines
  for i <- 0 until nofmines do
    matrix(i).state=MineClosed
  end for
  // shuffle
  for i <- 0 until nofmines do
    val j =
      i + rand.nextInt(matrix.length - i)
    val t = matrix(i)
    matrix(i) = matrix(j)
    matrix(j) = t
  end for
  // ...
end init

Now, 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 one more issue: 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.

MH.ZiNi

I'm giving this algorithm its own chapter, since it's possibly a semi-novelty. There's 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), which means they see the full board from the start. In the case of HZiNi, the algorithm starts by opening every single zero area, which is basically impossible for a human to achieve outsided of the beginner level.

Enter what I'm tentatively dubbing More Human ZiNi (MH.ZiNi). It still has UPK, but the main difference is that it starts from where the player themself started and then goes on from there in a similar manner to HZiNi:

  1. Calculate the premium for each open cell
  2. Find the one with the highest premium
  3. Go back to 1 if closed cells remain

I'm still contemplating whether I should add a step there — before the heavy use of UPK — to instead do the more human thing of chording a cell if the number of opened cells were to be higher than the number of clicks required to chord. That sometimes even leads to more optimal solves, as the current way can be shortsighted (on purpose).

MH.ZiNi adds a more realistic efficiency goal for the player without me having to implement a proper solver. It can be beaten and it can even lose against 3BV, at least on beginner level.

The missing features

This is not a perfect creation. I'm quickly going to list some of the worst shortcomings:


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.

In summary

I did not mention it anywhere, but you can start a new game with spacebar and change the chording method in the settings.

Anyway, what I added to the status quo:

Altogether, I am proud of the result: https://minesweepe.rs.

Thanks for reading.


< Go back.