Recently I updated the mine-laying algorithm and the timing system for my Minesweeper game. The reason was that a couple days ago someone cheated and uploaded a completely ridiculous score to the leaderboard. There was already a "verification mechanism" for scores, but it was super crude — it basically just stitched together random timestamps to cover up the data being uploaded.
When I first wrote this game, LLM tools didn't exist yet. Simple string manipulation plus obfuscating the source code might've been enough to stop players from actually figuring out what the code was doing. But now that LLM tools are here, any "trick" on the frontend is basically pointless. Obfuscated code used to make code harder to read, but now you can just hand the obfuscated code to an LLM and it'll instantly reconstruct what the code is really doing — and the reconstructed code might even be cleaner than the author's original. LLMs can also directly analyze code vulnerabilities and write scripts to cheat, and the server side is pretty much helpless against it.
But the reason I still upgraded the algorithm isn't to fight cheating itself — it's because I think this algorithm is reasonable, and it's just how it should be designed.
In my discussion with DeepSeek, it offered three strategies for mine placement: First, place mines locally — which is what I did before (now upgraded to the second). Second, use a "seed" approach, handing part of the control over mine placement to the server. Third, let the server fully decide, meaning every single click locally has to fetch cell info from the server.
Rule out the first one right away, and the third one gets ruled out without even thinking hard, because Minesweeper is a game where you're racing against the clock. If every step has to ask the server, the experience would be terrible. So that leaves only the second one. I spent some time these past couple days understanding the whole process, and it goes roughly like this:
The server generates a seed
|
hands it to the browser
|
the browser, at the moment the user clicks a cell, calculates the board based on the seed + the first coordinate, and uploads it to the server
|
the server calculates the same board using the seed + first coordinate
|
the player submits their score and action log
|
the server applies the player's actions to the board it calculated to determine whether the player cheated
The core of this algorithm is that the seed can generate a fixed sequence of random numbers through an algorithm. That sequence never changes — meaning one seed corresponds to one fixed number sequence.
The actual mine placement goes roughly like this:
The browser gets the seed
|
gets the user's first coordinate
|
calculates the coordinates around the first coordinate, merges them together as excluded coordinates, since the first click can't have a mine.
|
generates an empty board (table) — a one-dimensional array containing all cell coordinates, minus the excluded coordinates
|
uses a shuffle algorithm to shuffle from the start, with the number of shuffles equal to the total number of mines
|
the shuffle algorithm:
for (let i = 0; i < mines; i++) {
const j = i + prng.randBelow(pool.length - i);
const tmp = pool[i];
pool[i] = pool[j];
pool[j] = tmp;
}
prng.randBelow returns a "random number" between `0 ~ n`, and that random number is actually determined by the seed. So the shuffle algorithm determines the fairness of the distribution process, but what ultimately determines the distribution is the random number sequence generated by the seed.
|
The shuffle algorithm runs `mines` times, which is the number of mines, and then that portion is sliced out — that's the final mine coordinates.
So is this algorithm useful? Honestly, not really. It makes cheating a bit harder for players, but again, because LLMs exist now, an LLM can just write a script that precisely controls each step, and cheating is still easy. The most fatal problem is still that the game runs in the browser, and the browser is open to the user. Users can easily get the seed and the source code, and with an LLM providing the operations, cheating is basically unstoppable.
But again, the reason I still upgraded the algorithm isn't to fight cheating itself — it's because I think this algorithm is reasonable, and it's just how it should be designed.
(Translated with DeepSeek)