GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Minesweeper JavaScript: 9 Minefield Scenes

A first click on a mine ruins the board, so 02 lays mines only after the reveal, 05 overlaps two numbers to find the certain cells, and 06 paints each

Minesweeper Is Understood Through Counting, Not Rules: Building Minesweeper in JavaScript Starts With Counting the Eight Neighbors of One Cell

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine scenes share one structure. Mine placement, numbers, opened cells and flags are values the board logic computes, and the screen only compares them with a scene clock, so what you watch is a computed board, not a staged effect. No library or CDN is used and the one random source is a seeded generator, so the same seed builds the same board.

Why the board logic and the canvas are split

The board is a bundle of arrays built by newBoard. placeMines, calcNums, revealFrom, chordList, pairHint and probMap only read and write those arrays, while drawCells compares each cell's open, blast and flag times with the scene clock sceneT. Each of the nine files carries the same board functions, so changing the look leaves the minesweeper rules untouched.

    function newBoard(cols, rows, count) {
        const b = { cols: cols, rows: rows, count: count, mine: [], num: [], st: [], openT: [], exT: [], flT: [], xT: [], placed: false };
        for (let i = 0; i < cols * rows; i += 1) {
            b.mine.push(false);
            b.num.push(0);
            b.st.push(0);
            b.openT.push(0);
            b.exT.push(-1);
            b.flT.push(-1);
            b.xT.push(-1);
        }
        return b;
    }

A round runs on a timetable

Each scene fills script with (time, action) pairs, and advance(dt) calls each action once its time has passed. When a round of LAP_S seconds ends, the seed switches by lap % 2 so two boards alternate, and the first round starts START_S seconds ahead so the first frame already shows a scene in progress.

The layer of calculation each scene adds

01 delays the spread by distance, 02 clears the area around the first click and relocates mines, 03 counts neighbors in a scan, and 04 adds a flag-count condition. 05 compares the closed-neighbor sets of two numbers, 06 enumerates arrangements with combination weights, 07 ties blast time to distance and drives a shock field, 08 recovers the remaining mines from the flag count, and 09 fades brightness by the angle a sonar line has passed.

Minesweeper JavaScript: 9 Scenes — a Flood Open That Spreads, a Safe First Move That Builds the Board Late, a Neighbor-Count Scan, a Chord Open, a Deduction Hint That Overlaps Constraints, a Probability Map, a Chain Shockwave, a Spring Flag Counter and a Sonar Hero

What nine renders measured for area, frames and intensity

The rows below come from the last full render of the nine demos.

Scene Cumulative area Moving frames Mean intensity
01 Flood open 24.9% 16/23 31.3
02 Safe first move 34.6% 20/23 58.1
03 Number scan 34.8% 23/23 71.3
04 Chord open 6.3% 13/23 38.9
05 Deduction hint 20.1% 15/23 56.5
06 Probability map 30.2% 14/23 50.6
07 Shockwave 63.4% 22/23 35.3
08 Flag counter 4.6% 22/23 78.1
09 Sonar hero 38.9% 23/23 48.4

07 shakes the whole board and covers the most area at 63.4%, while 08 changes little more than a digit, covers 4.6% and still has the highest mean intensity at 78.1. Only 03 and 09 moved in every frame, and 04 moved in 13 of 23 because the board waits between flags and the chord.

01A Flood Open That Spreads Like a Ripple From One Empty Cell

On the first board of a minesweeper learning site, one empty cell opens and its empty neighbors follow in order of distance, stopping at numbered cells. revealFrom pulls the nearest cell first and writes openT = t0 + d × RIP_DELAY, queuing neighbors only when the opened cell is 0. The first click at 0.5s opens 57 cells and the farthest sits at distance 14, so the spread ends at 1.4s.

A Flood Open That Spreads Like a Ripple From One Empty Cell
Open in a new tab

01 · A Flood Open That Spreads Like a Ripple From One Empty Cell — Measured area 24.889% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

RIP_DELAYrevealFromfloodSize
        const todo = src.slice();
        const out = [];
        while (todo.length > 0) {
            let k = 0;
            for (let j = 1; j < todo.length; j += 1) {
                if (todo[j].d < todo[k].d) {
                    k = j;
                }
            }
            const cur = todo.splice(k, 1)[0];
            if (b.st[cur.i] !== 0 || b.mine[cur.i]) {
                continue;
            }
            b.st[cur.i] = 1;
            b.openT[cur.i] = t0 + cur.d * delay;
            out.push(cur.i);
            if (b.num[cur.i] === 0) {
                for (const j of nbrs(b, cur.i)) {
                    if (b.st[j] === 0) {
                        todo.push({ i: j, d: cur.d + 1 });
                    }
                }
            }
        }
        return out;

02A Safe First Move That Lays the Mines After the First Reveal

To explain how a first-click loss is removed, the point of this scene is that the board stays empty until the first cell opens. placeMines shuffles every cell with the seed and puts mines on the first count cells, then moves any mine in the first cell or its eight neighbors to a free cell later in the shuffled order and returns the [from, to] pairs. Cell 55 was a mine on the naive board, and once 3 mines move out of the zone it holds none and 77 cells open.

A Safe First Move That Lays the Mines After the First Reveal
Open in a new tab

02 · A Safe First Move That Lays the Mines After the First Reveal — Measured area 34.646% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

placeMinesinZoneOPEN_DELAY
        if (safe >= 0) {
            zone[safe] = true;
            for (const j of nbrs(b, safe)) {
                zone[j] = true;
            }
        }
        const moves = [];
        let next = b.count;
        for (let i = 0; i < n; i += 1) {
            b.mine[i] = false;
        }
        for (let k = 0; k < b.count; k += 1) {
            const c = order[k];
            if (!zone[c]) {
                b.mine[c] = true;
                continue;
            }
            while (next < n && (zone[order[next]] || b.mine[order[next]])) {
                next += 1;
            }
            b.mine[order[next]] = true;
            moves.push([c, order[next]]);
            next += 1;
        }

03A Scan That Counts the Eight Neighbors to Fill In the Numbers

A rule explainer that shows how numbers are made fits a scan frame sweeping cell by cell. calcNums uses nbrs to pick the neighbors inside the board and count their mines, and the scene sets when each count floats up with bornT(i) = SCAN_START + i / SCAN_RATE. Scanning 128 cells at 32 per second ends at 4.3s, and the 106 non-mine cells hold 28 zeros, 43 ones, 24 twos, 7 threes and 4 fours.

A Scan That Counts the Eight Neighbors to Fill In the Numbers
Open in a new tab

03 · A Scan That Counts the Eight Neighbors to Fill In the Numbers — Measured area 34.759% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCAN_RATEnbrscalcNums
    function calcNums(b) {
        for (let i = 0; i < b.cols * b.rows; i += 1) {
            let n = 0;
            if (!b.mine[i]) {
                for (const j of nbrs(b, i)) {
                    if (b.mine[j]) {
                        n += 1;
                    }
                }
            }
            b.num[i] = n;
        }
    }

04A Chord Open That Reveals All Neighbors When Flags Match the Number

In a guide to the expert shortcut, the key condition is that neighbors open only when the flag count equals the number. chordList returns the closed neighbors only for an opened, nonzero number whose flagsAround equals it, and doChord blows any mine in that list through exT and lights a cross on a wrong flag. The first round plants two flags at 0.9s and 1.35s and opens the neighbors at 2.15s, while the wrong-flag round chords at 6.6s into a mine with 61 cells open.

A Chord Open That Reveals All Neighbors When Flags Match the Number
Open in a new tab

04 · A Chord Open That Reveals All Neighbors When Flags Match the Number — Measured area 6.338% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

CHORD_DELAYchordListflagsAround
    function chordList(b, i) {
        if (b.st[i] !== 1 || b.num[i] === 0 || flagsAround(b, i) !== b.num[i]) {
            return [];
        }
        const out = [];
        for (const j of nbrs(b, i)) {
            if (b.st[j] === 0) {
                out.push(j);
            }
        }
        return out;
    }

05A Deduction Hint That Overlaps Neighbor Constraints to Find Certain Cells

A hint feature that names the next move on a stuck board suits this overlap of two numbers. consOf collects each opened number's closed neighbors u and remaining mines r, and when one number's u sits wholly inside another's, pairHint settles safe or mine by the difference r_B − r_A. Hints from 150 boards of 16×8 (3,536 in all) were checked against the real mine layouts with 0 mismatches, and all 6 hints in the first demo round are two-number hints.

A Deduction Hint That Overlaps Neighbor Constraints to Find Certain Cells
Open in a new tab

05 · A Deduction Hint That Overlaps Neighbor Constraints to Find Certain Cells — Measured area 20.097% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

pairHintsubsetOfHINT_SHOW
    function pairHint(cs) {
        for (const B of cs) {
            if (B.r === 0 || B.r === B.u.length) {
                continue;
            }
            for (const A of cs) {
                if (A.c === B.c || A.u.length >= B.u.length || !subsetOf(A.u, B.u)) {
                    continue;
                }
                const diff = B.u.filter(function (v) { return A.u.indexOf(v) < 0; });
                const rd = B.r - A.r;
                if (rd === 0) {
                    return { kind: 0, cells: diff, a: A.c, b: B.c };
                }
                if (rd === diff.length) {
                    return { kind: 1, cells: diff, a: A.c, b: B.c };
                }
            }
        }
        return null;
    }

06A Probability Map That Paints Each Cell's Mine Chance as Color Depth When Nothing Is Certain

An explainer on picking the safest move where luck decides fits a map that paints each cell's odds as color depth. probMap enumerates by recursion every arrangement of the border cells touching opened numbers, weights the outside cells by comb(outside cells, remaining mines), and stops when the search nodes pass PROB_NODE_CAP. Comparing 25 boards of 6×4 against a direct count of arrangements gave a maximum difference of 0, and the palest cell in the demo is 13.8% in round 1, 13.9% in round 2 and 0% in round 3.

A Probability Map That Paints Each Cell's Mine Chance as Color Depth When Nothing Is Certain
Open in a new tab

06 · A Probability Map That Paints Each Cell's Mine Chance as Color Depth When Nothing Is Certain — Measured area 30.206% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

probMapcombPROB_NODE_CAP
        const M = b.count - flags;
        let total = 0;
        let innerMines = 0;
        const pf = [];
        for (let k = 0; k < F.length; k += 1) {
            pf.push(0);
        }
        for (let m = 0; m <= F.length; m += 1) {
            if (sol[m] === 0) {
                continue;
            }
            const w = comb(inner.length, M - m);
            if (w === 0) {
                continue;
            }
            total += w * sol[m];
            innerMines += w * sol[m] * (M - m);
            for (let k = 0; k < F.length; k += 1) {
                pf[k] += w * cm[m][k];
            }
        }

07A Shockwave Where a Stepped Mine Sets Off Every Mine on the Board in Sequence

A game result screen that needs a defeat effect works best with mines going off in the order a ring reaches them. startChain writes each mine's blast time as exT = sceneT + distance / RING_SPEED, and the shock field multiplies exp(−((d − r) / WAVE_W)²) by a time decay to push each cell outward by PUSH and back. The farthest of the 18 mines is 424px from the start, so the last blast comes at 0.3 + 424/170 = 2.79s.

A Shockwave Where a Stepped Mine Sets Off Every Mine on the Board in Sequence
Open in a new tab

07 · A Shockwave Where a Stepped Mine Sets Off Every Mine on the Board in Sequence — Measured area 63.435% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_SPEEDWAVE_SPEEDPUSH
            const r = age * WAVE_SPEED;
            const amp = Math.exp(-age * 1.6);
            for (let i = 0; i < COLS * ROWS; i += 1) {
                const ddx = cellCx(i) - cellCx(s);
                const ddy = cellCy(i) - cellCy(s);
                const d = Math.hypot(ddx, ddy);
                if (d < 1) {
                    continue;
                }
                const q = (d - r) / WAVE_W;
                const e = Math.exp(-q * q) * amp;
                dxs[i] += ddx / d * e * PUSH;
                dys[i] += ddy / d * e * PUSH;
                glow[i] += e;
            }

08A Flag Counter Where Flags Stamp In on a Spring and the Remaining-Mine Count Rolls Down

A counter in a mobile game HUD fits a meter that rolls down as each flag lands. remain() returns MINES − countSt(B, 2), and when the value changes it writes mT, then for 0.2s pushes the old digits down while the new ones drop in, with pad3 fixing the sign and three digits. The scene plants 20 flags, 14 right and 6 wrong, the last at 4.48s, and the meter runs from 18 down to −2 and comes back to 4 once the wrong flags are lifted.

A Flag Counter Where Flags Stamp In on a Spring and the Remaining-Mine Count Rolls Down
Open in a new tab

08 · A Flag Counter Where Flags Stamp In on a Spring and the Remaining-Mine Count Rolls Down — Measured area 4.63% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLAG_GAPpad3remain
        ctx.save();
        ctx.beginPath();
        ctx.rect(bx, 41, 104, 26);
        ctx.clip();
        const p = ease((sceneT - mT) / 0.2);
        text(pad3(mPrev), W / 2, 54 + p * 18, INK, 1 - p, 20, 800, 'center');
        text(pad3(mNow), W / 2, 54 - (1 - p) * 18, INK, p, 20, 800, 'center');
        ctx.restore();

09A Minefield Hero Where a Sonar Sweep Reveals the Numbers Under the Cells

A first screen for a game or security landing page suits a dark board where only the cells the sonar line has passed show their numbers. lagOf finds the angle by which the line has passed a cell's center direction, then exp(−lag / FADE_ANGLE) sets cell brightness and number opacity while exp(−lag / BLINK_ANGLE) sets a mine's blink. With OMEGA at 1.15 one sweep takes 5.46s, and a round LAP_S is two sweeps, 10.93s.

A Minefield Hero Where a Sonar Sweep Reveals the Numbers Under the Cells
Open in a new tab

09 · A Minefield Hero Where a Sonar Sweep Reveals the Numbers Under the Cells — Measured area 38.867% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

OMEGAFADE_ANGLElagOf
            const lag = lagOf(i, a);
            let k = Math.exp(-lag / FADE_ANGLE);
            const pa = sceneT - pingT[i];
            if (pa >= 0 && pa < 2) {
                k = Math.max(k, Math.exp(-pa / 0.7));
            }
            if (k < 0.04) {
                k = 0;
            }
            if (k > 0.3) {
                lit += 1;
            }

When You Turn the Minesweeper Game Principle Into Code, How Many States Does One Cell Need?

A cell holds three states and four timestamps

One cell splits into whether it is a mine, its neighbor count, three states (closed, open, flagged) and four times at which things happened. Keeping the times apart from the state lets a scene such as the spread change the state at once and delay only what is drawn.

Value Meaning Changed by
mine Whether the cell is a mine placeMines
num Neighboring mine count calcNums
st 0, 1, 2 Closed, open, flagged revealFrom, applyHint
openT, exT, flT, xT Times of opening, blast, flag and cross revealFrom, startChain, applyHint, doChord

Putting it into a minesweeper site

For a minesweeper site, 05's findHint(b) takes one board and returns one hint, so a single button can call it. The board logic does not know the screen, so swapping the canvas for DOM cells keeps the same function as it is.

Using it as a game-making exercise

If you write a minesweeper javascript tutorial or set a coding exercise, give 03's calcNums as the first task and 01's revealFrom as the second, so counting neighbors and spreading open become separate problems. Anyone looking for minesweeper javascript code to lift can start with 05's subset comparison and 06's probability map, which also work as diagrams in a strategy write-up.

Make the Board an Array, Place Mines After the First Click, Count Neighbors and Spread Open From Empty Cells: The Order in Which One Minesweeper Board Gets Built

The order of building, opening and counting hints

05's build shows the whole order at once. It makes a new board, calls placeMines with the center cell as the safe cell, spreads with revealFrom, runs findHint and applyHint six times on a copy to see whether the hints chain, and then picks a seed that meets the condition.

            B = newBoard(COLS, ROWS, MINES);
            placeMines(B, base + k, center);
            revealFrom(B, [{ i: center, d: 0 }], -9, 0);
            const c = cloneBoard(B);
            let pairs = 0;
            let mineHints = 0;
            let total = 0;
            for (let s = 0; s < 6; s += 1) {
                const h = findHint(c);
                if (!h) {
                    break;
                }
                total += 1;
                if (h.b >= 0) {
                    pairs += 1;
                }
                if (h.kind === 1) {
                    mineHints += 1;
                }
                applyHint(c, h, -9, 0);
            }
            if (total >= 6 && pairs >= 4 && mineHints >= 1 && countSt(B, 1) >= 30) {
                break;
            }
        }

The search that picks a board worth showing

A seed does not promise a good board, so each scene searches candidates. 01 takes the first SEED + k whose floodSize reaches MIN_FLOOD 44, 05 tries up to 120 times for a board meeting the condition above, and 06 tries up to 600 times for a board with 28 to 64 opened cells.

Where Minesweeper Breaks: a Board Whose First Click Is a Mine, a Spread That Reads Cells Past the Board Edge and a Meter Whose Flag Count Drifts From the Remaining Mines

A board whose first click is a mine

02 shows the board with mines laid first beside the board built after the first click, but its blast sits at 1.0s in the timetable, before the first round starts at 2.2s. So the failure contrast shows from the second round on, and the first round opens on the step that marks the safe zone.

A spread that reads cells past the board edge

A spread reads eight neighbors, and a border cell that finds neighbors by adding i + 1 makes the right-edge cell neighbor the left-edge cell of the next row. This build keeps the edge check in nbrs alone, so number counting, the spread and the hints all use the same neighbor list.

    function nbrs(b, i) {
        const x = i % b.cols;
        const y = Math.floor(i / b.cols);
        const out = [];
        for (let dy = -1; dy <= 1; dy += 1) {
            for (let dx = -1; dx <= 1; dx += 1) {
                if (dx === 0 && dy === 0) {
                    continue;
                }
                const nx = x + dx;
                const ny = y + dy;
                if (nx >= 0 && nx < b.cols && ny >= 0 && ny < b.rows) {
                    out.push(ny * b.cols + nx);
                }
            }
        }
        return out;
    }

A meter whose flag count drifts from the remaining mines

08 plants 20 flags against 18 mines, so the meter reaches −2, a depth that EXTRA_WRONG (2) sets. pad3 adds the sign and pads to three digits, so −2 reads -002, and because a flag count cannot tell right from wrong, the 6 wrong flags show only through the crosses of the check step.

A board with no certain cell is rare

Among 600 boards spread from the center cell in 06, only 19 had no 0% or 100% cell among the closed cells, and all of them had 9 to 18 opened cells. So the scene runs solveOut to clear every certain cell and uses boards with 28 to 64 opened cells.

The complete source of the nine scenes and the editable files are in the zip below, and the archive password is vh7z44k3.

Change the Board Size and Mine Density and How Far Does the Deduction Hint Reach? Columns, Rows, Mine Count and Seed Compared

Name Changed value Feel
Bigger board 01 COLS×ROWS 16×8 → 30×16 (MINES 16 → 60) Holding density at 12.5% takes the first spread from 57 to 121 cells, yet the farthest cell is 16 steps away instead of 14, so the spread lasts 1.6s versus 1.4s. The cell shrinks from 28px to 15px and the number glyph from 17px to 9px
Denser mines 02 MINES 22 → 40 At 17% to 31% density, 4 mines instead of 3 move out of the zone and the safe first move opens 9 cells instead of 77. Mines touch the zone ring, so only the middle cell is a 0 and the spread stops at distance 1
Slower spread 01 RIP_DELAY 0.1 → 0.25 The farthest cell opens at 3.5s instead of 1.4s, so the spread ends at 4.0s and briefly overlaps the second click at 3.9s. The press time of one cell, 0.28s, is unchanged, so each cell moves the same way

What to check before changing a value

A deduction hint comes only from opened numbers, so when the opened cells fall from 77 to 9 the material for hints shrinks with them. Growing the board shrinks the cell from 28px to 15px and makes taps harder to land, so on a touch screen pick the cell size before the board size.

One Opened Board Under Reduced Motion: Accessibility Through Sentences That Read Cell States and Keyboard Movement Between Cells

Arrow keys move, open and flag

All nine stages are role="application", and pressing an arrow key while focused stops the auto demo and hands control over. Enter and Space open the cell (a chord when it is an opened number), F plants a flag, P pauses, N takes the next action in the timetable and R restarts.

        if (k === 'ArrowLeft' || k === 'ArrowRight' || k === 'ArrowUp' || k === 'ArrowDown') {
            e.preventDefault();
            takeOver();
            moveCur(k === 'ArrowLeft' ? -1 : (k === 'ArrowRight' ? 1 : 0), k === 'ArrowUp' ? -1 : (k === 'ArrowDown' ? 1 : 0));
            showNow();
        }
        else if (k === 'Enter' || k === ' ') {
            e.preventDefault();
            takeOver();
            onAct(ky * gCols + kx);
            showNow();
        }
        else if (k === 'f' || k === 'F') {
            e.preventDefault();
            takeOver();
            onFlag(ky * gCols + kx);
            showNow();
        }

The status line stays quiet during the demo

The status line is aria-live="off" during the demo, so it is not read out as things happen, and it switches to polite the moment you take over. 0.6s before a round ends, the current state sentence is written once into a hidden role="status" paragraph.

    // Tell screen readers once that a demo round has ended, and do not read it again when the board repeats
    let doneSaid = false;
    function checkDone() {
        if (!manual && !doneSaid && sceneT >= LAP_S - 0.6) {
            doneSaid = true;
            doneP.textContent = statusText();
        }
    }

Reduced motion and a hidden tab

With prefers-reduced-motion on, there is no loop, and showRest runs the timetable up to REST_S seconds and draws one opened board. A hidden tab stops the loop and the first frame back drops its dt, so the hidden time does not pour in at once.

    function showRest() {
        stopLoop();
        lap = 0;
        sceneT = 0;
        evAt = 0;
        script = [];
        build();
        runTo(REST_S);
        showNow();

The number colors range from 5.7:1 to 14.3:1 contrast on the cream #fcf9f4 background.

Minesweeper Game in JavaScript FAQ: Building Without a Library, Choosing Number Colors and Handling a Board With No Certain Cell

Does a minesweeper game javascript project need a library

Canvas 2D is enough, and none of the nine scenes uses a library, WebGL or a CDN. Randomness comes from a seeded generator so the same input builds the same board, and the board functions do not know the DOM, so you can lift them out and run them in Node.

How do I pick number colors that read well

Separate colors let the distribution read at a glance, but color alone should not carry the meaning, so this build draws the digit too and keeps all 8 colors at 5.7:1 or more against the background. If the background changes, measure the contrast again.

What do I do when no cell is certain

Compute the odds as 06 does and pick the palest cell. In round 3 here the palest cell is 0%, so opening it leaves a certain safe cell.

For references, MDN's requestAnimationFrame and Canvas 2D pages cover the loop and drawing, while prefers-reduced-motion and live regions cover the motion setting and announcements. For the piece-placing rules see the jigsaw puzzle post, for tile merging the 2048 game post, and for path finding on a grid the maze generator post.

Enter the archive password

The password is inside this article. You will find it as you read.