Sketch 2026-01-25 22:50

This is a fully playable Minesweeper game built with p5.js. Players click to reveal cells, right-click to flag mines, and try to uncover all safe cells before hitting a mine. The game includes a timer, smart mine placement that avoids the first click, and automatic flood-fill revealing of empty regions.

🧪 Try This!

Experiment with the code by making these changes:

  1. Double the difficulty — More mines means a harder puzzle with fewer safe cells to find.
  2. Make a larger board — A bigger grid gives you more cells to reveal and makes strategies like edge-mapping matter more.
  3. Slow down the timer to test restart — Makes it easier to check that the timer correctly freezes and resets.
  4. Change mine and safe cell colors — Customizes the visual appearance; try blue mines on a green background for better contrast.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch implements the classic Minesweeper game as an interactive p5.js sketch. Players click cells to reveal them, discovering either empty space, a mine count, or a mine. Right-clicking (or shift-clicking) flags suspected mines. The game combines a 2D grid data structure, mouse interaction, collision detection, and recursive flood-fill algorithms to create a complete, playable game loop that feels responsive and fair.

The code is organized around a central grid array where each cell stores its mine status, neighbor mine count, and revealed/flagged state. You will learn how to structure complex game logic with nested loops, how recursion automatically reveals empty regions, how to defer mine placement until after the first click (avoiding frustrating instant losses), and how to detect win/loss conditions. This sketch demonstrates professional patterns for game development in p5.js.

⚙️ How It Works

  1. When setup() runs, it creates a 10×10 grid where each cell is initialized as a safe, unrevealed cell with a count of 0
  2. The first time the player clicks, the game starts the timer and places mines randomly everywhere EXCEPT in a 3×3 box around that first click, guaranteeing safety
  3. For each mine placed, calculateNumbers() scans all neighbors and increments their count—this number tells the player how many mines touch that cell
  4. When a player left-clicks a revealed cell, the reveal() function marks it as revealed; if it has 0 neighboring mines, reveal() recursively calls itself on all 8 neighbors, creating an automatic flood-fill of empty regions
  5. If the player clicks a mine, all mines are revealed and gameOver becomes true, freezing gameplay
  6. If the player reveals all safe cells (counting safe cells equals total cells minus mine count), gameWon becomes true
  7. The footer displays a timer counting seconds since the first click and a clickable Restart button

🎓 Concepts You'll Learn

2D arrays and grid data structuresRecursion and flood-fill algorithmsGame state managementMouse input and interactionCollision detection and boundary checkingNested loops and array iteration

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It configures the canvas and initializes the game board to a fresh state, ready for the first click.

function setup() {
  createCanvas(mapSize, mapSize + footerHeight);
  textAlign(CENTER, CENTER);
  initGrid();
}
Line-by-line explanation (3 lines)
createCanvas(mapSize, mapSize + footerHeight);
Creates a square canvas (300×330 by default) that holds the game grid plus a footer for the timer and restart button
textAlign(CENTER, CENTER);
Aligns all text to its center point, making numbers and flags appear centered in cells
initGrid();
Calls initGrid() to populate the grid array and reset all game state variables

initGrid()

initGrid() is called at the start and every time the player clicks Restart. It rebuilds the grid array from scratch and resets all game flags, ensuring a clean slate for each new game.

🔬 This nested loop builds the entire 10×10 grid. What happens if you change gridSize to 5 in the loop conditions? What about 20?

  for (let i = 0; i < gridSize; i++) {
    let row = [];
    for (let j = 0; j < gridSize; j++) {
      row.push({ mine: false, count: 0, revealed: false, flagged: false });
    }
    grid.push(row);
  }
function initGrid() {
  grid = [];
  for (let i = 0; i < gridSize; i++) {
    let row = [];
    for (let j = 0; j < gridSize; j++) {
      row.push({ mine: false, count: 0, revealed: false, flagged: false });
    }
    grid.push(row);
  }

  gameOver = false;
  gameWon = false;
  started = false;
  firstClick = true;
  seconds = 0;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

nested-for-loop Nested Grid Population for (let i = 0; i < gridSize; i++) { ... for (let j = 0; j < gridSize; j++) { ... } }

Creates a 10×10 2D array where each cell is an object with mine, count, revealed, and flagged properties

grid = [];
Empties the grid array, erasing any previous game board so a fresh one can be built
for (let i = 0; i < gridSize; i++) {
Outer loop iterates once for each row (0 to 9, creating 10 rows total)
let row = [];
Creates an empty array for this row's cells
for (let j = 0; j < gridSize; j++) {
Inner loop iterates once for each column (0 to 9, creating 10 cells per row)
row.push({ mine: false, count: 0, revealed: false, flagged: false });
Creates a cell object with four properties: mine (false = safe), count (0 = no neighboring mines yet), revealed (false = hidden), flagged (false = not flagged)
grid.push(row);
Adds the completed row to the grid array
gameOver = false; gameWon = false; started = false; firstClick = true; seconds = 0;
Resets all game state variables so the game is ready to play fresh

placeMines(avoidI, avoidJ)

placeMines() is called AFTER the first click, ensuring the player's first move is always safe. This is a key Minesweeper design pattern: mines are placed after you reveal your first cell, not before, so you can never lose immediately.

🔬 These lines pick a random cell and reject it if it already has a mine or is too close to the first click. What happens if you remove the second if-statement entirely? Try it and play a few games—does the first click still feel safe?

    let i = floor(random(gridSize));
    let j = floor(random(gridSize));

    if (grid[i][j].mine) continue;
    if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;
function placeMines(avoidI, avoidJ) {
  let placed = 0;
  while (placed < mineCount) {
    let i = floor(random(gridSize));
    let j = floor(random(gridSize));

    if (grid[i][j].mine) continue;
    if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;

    grid[i][j].mine = true;
    placed++;
  }

  calculateNumbers();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

while-loop Mine Placement Loop while (placed < mineCount) {

Keeps trying random positions until exactly mineCount mines are successfully placed

conditional Duplicate Mine Prevention if (grid[i][j].mine) continue;

Skips this iteration if the random cell already has a mine, ensuring no duplicates

conditional First-Click Safety Zone if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;

Avoids placing mines in a 3×3 box around the first click, guaranteeing it is always safe

let placed = 0;
Initializes a counter to track how many mines have been successfully placed
while (placed < mineCount) {
Loops until placed equals mineCount (e.g., 13 mines); the loop keeps running as long as placed is less than the target
let i = floor(random(gridSize));
Picks a random row index from 0 to 9
let j = floor(random(gridSize));
Picks a random column index from 0 to 9, so (i, j) is now a random cell
if (grid[i][j].mine) continue;
If that cell already has a mine, skip to the next loop iteration and try a different random position
if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;
If this cell is within 1 step of the first click (the 3×3 box around avoidI, avoidJ), skip it to keep that zone mine-free
grid[i][j].mine = true;
Mark this cell as containing a mine
placed++;
Increment the placed counter so we get closer to placing mineCount total
calculateNumbers();
After all mines are placed, scan the grid and fill in the count property for each cell based on neighboring mines

calculateNumbers()

calculateNumbers() runs once after placeMines() completes. It fills in the count property for every safe cell, representing how many mines surround it. This count appears to the player when they click an empty cell.

function calculateNumbers() {
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      if (grid[i][j].mine) {
        forEachNeighbor(i, j, (ni, nj) => grid[ni][nj].count++);
      }
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional-with-callback Neighbor Count Increment forEachNeighbor(i, j, (ni, nj) => grid[ni][nj].count++);

For each mine, visits all 8 neighbors and increments their count property

for (let i = 0; i < gridSize; i++) {
Outer loop iterates through all rows
for (let j = 0; j < gridSize; j++) {
Inner loop iterates through all columns, so (i, j) visits every cell in the grid
if (grid[i][j].mine) {
Checks if the current cell contains a mine
forEachNeighbor(i, j, (ni, nj) => grid[ni][nj].count++);
If this cell is a mine, call forEachNeighbor to visit all 8 surrounding cells and increment their count by 1

forEachNeighbor(i, j, fn)

forEachNeighbor() is a utility function that handles the geometry of visiting all 8 neighbors of a cell while respecting grid boundaries. It takes a callback function fn and applies it to each neighbor, making code like calculateNumbers() and reveal() cleaner and more reusable.

🔬 This nested loop generates all 8 neighbor offsets. What if you change the second loop to only go `dj < 1` (stopping before 1)? This would check only 5 neighbors instead of 8—which ones would be missing?

  for (let di = -1; di <= 1; di++) {
    for (let dj = -1; dj <= 1; dj++) {
      if (di === 0 && dj === 0) continue;
function forEachNeighbor(i, j, fn) {
  for (let di = -1; di <= 1; di++) {
    for (let dj = -1; dj <= 1; dj++) {
      if (di === 0 && dj === 0) continue;
      let ni = i + di;
      let nj = j + dj;
      if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize) {
        fn(ni, nj);
      }
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

nested-for-loop 8-Direction Offset Loop for (let di = -1; di <= 1; di++) { for (let dj = -1; dj <= 1; dj++) {

Generates all 8 offset combinations (-1,-1) through (1,1), used to reach all neighbors

conditional Skip Self if (di === 0 && dj === 0) continue;

Prevents the function from treating the cell itself (offset 0,0) as a neighbor

conditional Boundary Validation if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize) {

Ensures the neighbor cell is within the grid bounds before calling the callback function

for (let di = -1; di <= 1; di++) {
Loops through -1, 0, 1 representing row offsets: up, same row, down
for (let dj = -1; dj <= 1; dj++) {
Loops through -1, 0, 1 representing column offsets: left, same column, right
if (di === 0 && dj === 0) continue;
Skips when di and dj are both 0 (the cell itself); we only want the 8 surrounding cells, not the center
let ni = i + di; let nj = j + dj;
Calculates the actual row and column of the neighbor: add the offset to the original position
if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize) {
Checks that the neighbor is inside the grid (row and column must be >= 0 and < 10); edge cells have fewer than 8 neighbors
fn(ni, nj);
Calls the function fn (passed in as a parameter) with the neighbor's indices, executing whatever action the caller requested

draw()

draw() is the main animation loop, called 60 times per second. It redraws the entire game board every frame, updating positions and states as needed. This is standard p5.js architecture: clear, render, repeat.

function draw() {
  background(220);
  drawGrid();
  drawCells();
  drawFooter();

  if (gameWon) drawMessage("You Win!");
  if (gameOver) drawMessage("Game Over");
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Win Message Display if (gameWon) drawMessage("You Win!");

Displays a win message if the player has revealed all safe cells

conditional Loss Message Display if (gameOver) drawMessage("Game Over");

Displays a game over message if the player clicked a mine

background(220);
Clears the canvas with a light gray background, erasing the previous frame
drawGrid();
Draws the black grid lines that separate cells
drawCells();
Draws all cell content: unrevealed cells stay dark, revealed cells show their mine status or neighbor count, and flagged cells show an F
drawFooter();
Draws the timer and Restart button in the 30-pixel footer below the grid
if (gameWon) drawMessage("You Win!");
If gameWon is true, draws a large win message centered on the screen
if (gameOver) drawMessage("Game Over");
If gameOver is true, draws a large game over message centered on the screen

drawGrid()

drawGrid() renders the black borders between cells. By looping from 1 to 9 and multiplying by cellSize, it places lines exactly where grid cells meet. The outer edges don't need lines because the canvas boundary provides them.

function drawGrid() {
  stroke(0);
  for (let i = 1; i < gridSize; i++) {
    line(i * cellSize, 0, i * cellSize, mapSize);
    line(0, i * cellSize, mapSize, i * cellSize);
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Grid Line Drawing for (let i = 1; i < gridSize; i++) {

Draws both vertical and horizontal grid lines, creating the 10×10 cell matrix

stroke(0);
Sets the line color to black for all lines drawn after this point
for (let i = 1; i < gridSize; i++) {
Loops from i=1 to i=9 (not 0 or 10); drawing lines only where cells meet, not at the edges
line(i * cellSize, 0, i * cellSize, mapSize);
Draws a vertical line at x = i * cellSize, running from y=0 to y=mapSize; this creates a vertical grid line
line(0, i * cellSize, mapSize, i * cellSize);
Draws a horizontal line at y = i * cellSize, running from x=0 to x=mapSize; this creates a horizontal grid line

drawCells()

drawCells() renders every cell's visual state. Unrevealed cells are drawn implicitly as the background since we don't draw anything for them. Revealed safe cells show their neighbor count (or blank if count is 0). Mines appear red. Flags appear as black Fs on top.

🔬 This block draws revealed cells with their mine status or count number. What happens if you remove the `if (!c.mine && c.count > 0)` condition entirely and always draw the count? You'd see numbers on empty cells and on mines too—what would you see?

      if (c.revealed) {
        fill(c.mine ? color(255, 0, 0, 150) : color(255, 255, 200));
        square(x, y, cellSize);

        if (!c.mine && c.count > 0) {
          fill(0);
          text(c.count, x + cellSize / 2, y + cellSize / 2);
        }
      }
function drawCells() {
  textSize(cellSize * 0.6);

  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      let c = grid[i][j];
      let x = j * cellSize;
      let y = i * cellSize;

      if (c.revealed) {
        fill(c.mine ? color(255, 0, 0, 150) : color(255, 255, 200));
        square(x, y, cellSize);

        if (!c.mine && c.count > 0) {
          fill(0);
          text(c.count, x + cellSize / 2, y + cellSize / 2);
        }
      }

      if (c.flagged) {
        fill(0);
        text("F", x + cellSize / 2, y + cellSize / 2);
      }
    }
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Revealed Cell Rendering if (c.revealed) { fill(...); square(...); if (!c.mine && c.count > 0) { ... } }

Draws revealed cells with appropriate background color and text (mine in red, safe with count in light yellow)

conditional Flagged Cell Flag Display if (c.flagged) { fill(0); text("F", ...); }

Displays the letter F on flagged cells, regardless of whether they are revealed or hidden

textSize(cellSize * 0.6);
Sets font size to 60% of cell size, so numbers and F letters scale with the grid and fit neatly inside cells
for (let i = 0; i < gridSize; i++) {
Outer loop iterates through all rows
for (let j = 0; j < gridSize; j++) {
Inner loop iterates through all columns, visiting every cell
let c = grid[i][j];
Stores a reference to the current cell object for convenience
let x = j * cellSize; let y = i * cellSize;
Calculates the pixel position (x, y) of the top-left corner of this cell based on its grid position
if (c.revealed) {
Only draw the cell's content if it has been revealed
fill(c.mine ? color(255, 0, 0, 150) : color(255, 255, 200));
Ternary operator: if the cell contains a mine, fill red (with transparency); otherwise fill light yellow
square(x, y, cellSize);
Draws a filled square the size of a cell at the calculated position
if (!c.mine && c.count > 0) {
If the cell is safe (not a mine) AND has at least 1 neighboring mine, draw the count
fill(0); text(c.count, x + cellSize / 2, y + cellSize / 2);
Set fill to black and draw the count number centered in the cell
if (c.flagged) {
If the cell is flagged, draw an F on top (regardless of whether it's revealed)
fill(0); text("F", x + cellSize / 2, y + cellSize / 2);
Set fill to black and draw the letter F centered in the cell

drawFooter()

drawFooter() renders the bottom strip that displays the timer and the clickable Restart button. The timer only counts when the game is active, freezing when the player wins or loses.

function drawFooter() {
  fill(0);
  textSize(16);

  if (started && !gameOver && !gameWon) {
    seconds = floor((millis() - startTime) / 1000);
  }

  text("⏱ " + seconds, 40, mapSize + footerHeight / 2);
  text("Restart", mapSize - 40, mapSize + footerHeight / 2);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Timer Calculation if (started && !gameOver && !gameWon) { seconds = floor((millis() - startTime) / 1000); }

Updates the timer in real-time, converting milliseconds elapsed to whole seconds

fill(0); textSize(16);
Sets text color to black and font size to 16 pixels for the footer text
if (started && !gameOver && !gameWon) {
Only update the timer if the game has started AND hasn't ended (win or loss); stops the clock once game is over
seconds = floor((millis() - startTime) / 1000);
millis() returns total milliseconds since sketch started; subtract startTime (recorded on first click) and divide by 1000 to get seconds, then floor() rounds down to a whole number
text("⏱ " + seconds, 40, mapSize + footerHeight / 2);
Draws the timer text (clock emoji + seconds) at position (40, 330) in the footer, left-aligned
text("Restart", mapSize - 40, mapSize + footerHeight / 2);
Draws the word "Restart" at position (260, 330) in the footer, right-aligned, clickable in mousePressed()

drawMessage(msg)

drawMessage() is a simple utility that displays large text centered on the screen when the game ends. It's called from draw() only when gameWon or gameOver is true.

function drawMessage(msg) {
  fill(0);
  textSize(48);
  text(msg, width / 2, height / 2);
}
Line-by-line explanation (3 lines)
fill(0);
Sets text color to black
textSize(48);
Sets font size to large (48 pixels) for prominent display
text(msg, width / 2, height / 2);
Draws the passed message (either "You Win!" or "Game Over") centered on the canvas

reveal(i, j)

reveal() is the core of Minesweeper's smooth gameplay. When you click an empty cell, it recursively reveals all neighbors, which in turn reveal their neighbors, creating a cascading wave of revealed cells. This flood-fill algorithm is stopped only when it reaches cells with mine counts (where the cascade boundary sits), making the game feel alive.

🔬 This recursive flood-fill only spreads to neighbors if count is 0 (no adjacent mines). What happens if you change the condition to `c.count < 2` so it spreads even through cells with 1 mine neighbor? Try clicking a low-number cell and watch the cascade grow larger.

  c.revealed = true;

  if (c.count === 0 && !c.mine) {
    forEachNeighbor(i, j, reveal);
  }
function reveal(i, j) {
  let c = grid[i][j];
  if (c.revealed || c.flagged) return;

  c.revealed = true;

  if (c.count === 0 && !c.mine) {
    forEachNeighbor(i, j, reveal);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

recursive-call Flood-Fill Recursion forEachNeighbor(i, j, reveal);

Recursively reveals all neighbors when landing on an empty cell, propagating outward until hitting cells with mine counts

let c = grid[i][j];
Gets the cell object at the clicked position
if (c.revealed || c.flagged) return;
If the cell is already revealed or flagged, do nothing and exit early—prevents re-revealing cells and respects flagged cells
c.revealed = true;
Marks the cell as revealed so it will be drawn in draw Cells()
if (c.count === 0 && !c.mine) {
Checks if the cell is safe (not a mine) AND has no neighboring mines (count is 0)
forEachNeighbor(i, j, reveal);
If true, recursively call reveal on all 8 neighbors; they will each check the same condition and recurse further, creating a flood-fill that automatically opens empty regions

revealAllMines()

revealAllMines() is called immediately after the player clicks a mine and loses. It exposes all remaining hidden mines, letting the player see what they hit and what they avoided. This provides visual closure and helps teach board awareness for the next game.

function revealAllMines() {
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      if (grid[i][j].mine) {
        grid[i][j].revealed = true;
      }
    }
  }
}
Line-by-line explanation (4 lines)
for (let i = 0; i < gridSize; i++) {
Loops through all rows
for (let j = 0; j < gridSize; j++) {
Loops through all columns, visiting every cell
if (grid[i][j].mine) {
Checks if the current cell contains a mine
grid[i][j].revealed = true;
Marks the mine as revealed so it appears red on the next draw call

mousePressed()

mousePressed() is the engine of user interaction. It routes clicks to restart the game, toggle flags, start the timer, place mines, reveal cells, and detect win/loss conditions. The order of if-statements is critical: check end states first, then parse the click location, then handle game logic.

🔬 These lines handle flagging: right-click OR shift+click toggles the flag, and left-clicking a flagged cell is ignored. What happens if you remove the `if (c.flagged) return;` line entirely? Players could left-click through flags and reveal cells without unflagging them first.

  if (mouseButton === RIGHT || keyIsDown(SHIFT)) {
    if (!c.revealed) c.flagged = !c.flagged;
    return;
  }

  if (c.flagged) return;
function mousePressed() {
  // Restart button
  if (mouseY > mapSize && mouseX > mapSize - 80) {
    initGrid();
    return;
  }

  if (gameOver || gameWon) return;
  if (mouseX > mapSize || mouseY > mapSize) return;

  if (!started) {
    started = true;
    startTime = millis();
  }

  let j = floor(mouseX / cellSize);
  let i = floor(mouseY / cellSize);
  let c = grid[i][j];

  if (mouseButton === RIGHT || keyIsDown(SHIFT)) {
    if (!c.revealed) c.flagged = !c.flagged;
    return;
  }

  if (c.flagged) return;

  // First click safety
  if (firstClick) {
    placeMines(i, j);
    firstClick = false;
  }

  if (c.mine) {
    c.revealed = true;
    revealAllMines();
    gameOver = true;
    return;
  }

  reveal(i, j);
  checkWin();
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Restart Button Detection if (mouseY > mapSize && mouseX > mapSize - 80) { initGrid(); return; }

Detects clicks in the footer area and resets the game

conditional Game End State Check if (gameOver || gameWon) return;

Prevents any clicks from being processed after the game ends

conditional Canvas Boundary Check if (mouseX > mapSize || mouseY > mapSize) return;

Ignores clicks outside the game grid (after restart button area)

conditional First Click Detection if (!started) { started = true; startTime = millis(); }

Starts the timer on the first click

conditional Flag Right-Click Handler if (mouseButton === RIGHT || keyIsDown(SHIFT)) { if (!c.revealed) c.flagged = !c.flagged; return; }

Right-click or shift-click toggles the flag on unrevealed cells

conditional Prevent Left-Click on Flagged if (c.flagged) return;

Prevents revealing flagged cells even with left-click

conditional Mine Click Detection if (c.mine) { c.revealed = true; revealAllMines(); gameOver = true; return; }

Ends the game if a mine is clicked

// Restart button
Comment marking the start of restart button logic
if (mouseY > mapSize && mouseX > mapSize - 80) {
Checks if the click is below the grid (Y > 300) AND to the right (X > 220, near right side); this is the restart button area
initGrid(); return;
Resets the game and exits mousePressed early, preventing other logic from running
if (gameOver || gameWon) return;
Prevents further clicks from being processed if the game has already ended
if (mouseX > mapSize || mouseY > mapSize) return;
Ignores clicks outside the game grid (except restart button, already handled above)
if (!started) { started = true; startTime = millis(); }
On the first click, sets started to true and records the current time in startTime so the timer can count seconds from this moment
let j = floor(mouseX / cellSize); let i = floor(mouseY / cellSize);
Converts mouse pixel coordinates to grid indices: divide by cellSize (30) and floor the result to get the row and column
let c = grid[i][j];
Gets the cell object at the clicked position for easy reference
if (mouseButton === RIGHT || keyIsDown(SHIFT)) {
Checks if the click is a right-click OR if shift key is held; allows both methods to flag a cell
if (!c.revealed) c.flagged = !c.flagged;
If the cell is not revealed, toggle its flagged state (true becomes false, false becomes true)
if (c.flagged) return;
If the cell is flagged, exit early to prevent revealing it; you must unflag first
// First click safety
Comment marking the first-click mine placement logic
if (firstClick) { placeMines(i, j); firstClick = false; }
On the first left-click, call placeMines() to randomly scatter mines everywhere except the 3×3 box around this cell, then set firstClick to false so this only happens once
if (c.mine) {
Checks if the clicked cell contains a mine
c.revealed = true; revealAllMines(); gameOver = true; return;
If it's a mine, mark it as revealed (so it appears red), reveal all other mines, set gameOver to true (freezing the game), and exit
reveal(i, j);
If it's not a mine, call reveal() to mark it and any connected empty neighbors as revealed
checkWin();
After revealing a cell, check if all safe cells are now revealed; if so, set gameWon to true

checkWin()

checkWin() is the victory detector. It counts revealed safe cells and compares to the total number of safe cells. In Minesweeper, you win by revealing all non-mine cells without clicking a single mine—this function validates that condition.

function checkWin() {
  let safe = 0;
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      if (grid[i][j].revealed && !grid[i][j].mine) safe++;
    }
  }

  if (safe === gridSize * gridSize - mineCount) {
    gameWon = true;
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

nested-for-loop Safe Cell Counter for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++) { if (grid[i][j].revealed && !grid[i][j].mine) safe++; } }

Counts how many safe cells have been revealed by iterating through the entire grid

let safe = 0;
Initializes a counter for revealed safe cells
for (let i = 0; i < gridSize; i++) {
Loops through all rows
for (let j = 0; j < gridSize; j++) {
Loops through all columns, visiting every cell
if (grid[i][j].revealed && !grid[i][j].mine) safe++;
If the cell is both revealed AND not a mine (safe), increment the counter
if (safe === gridSize * gridSize - mineCount) {
Compares the count of revealed safe cells to the expected total (100 cells - 13 mines = 87 safe cells); if they match, all safe cells have been revealed
gameWon = true;
Sets gameWon to true, triggering the win message and freezing the board on the next draw call

📦 Key Variables

grid array

2D array storing all cell objects; grid[i][j] holds a cell with properties mine, count, revealed, flagged

let grid = [];
gridSize number

The width and height of the game board in cells (10 = 10×10 grid)

const gridSize = 10;
mapSize number

The width and height of the playable area in pixels (300 = 300×300 canvas, not counting footer)

const mapSize = 300;
footerHeight number

Height in pixels of the footer area below the grid, housing timer and restart button

const footerHeight = 30;
cellSize number

Width and height of each cell in pixels; automatically calculated as mapSize / gridSize (300 / 10 = 30 pixels)

const cellSize = mapSize / gridSize;
mineCount number

Total number of mines scattered on the board; calculated as 1/8 of total cells plus 1

const mineCount = Math.floor((gridSize * gridSize) / 8) + 1;
gameOver boolean

Flag set to true when the player clicks a mine, freezing gameplay and triggering the loss message

let gameOver = false;
gameWon boolean

Flag set to true when all safe cells are revealed, freezing gameplay and triggering the win message

let gameWon = false;
started boolean

Flag set to true on the first click, triggering timer start; prevents timer from running before gameplay begins

let started = false;
startTime number

Stores the millisecond value (from millis()) when the first click happens, used to calculate elapsed time

let startTime = 0;
seconds number

Current elapsed game time in whole seconds, displayed in the footer timer

let seconds = 0;
firstClick boolean

Flag set to true initially; when first left-click happens, mines are placed and this flag becomes false, preventing future mine placement

let firstClick = true;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG mousePressed()

Clicking the restart button in the footer while hovering near boundaries might also trigger cell clicks due to overlap detection logic—the order of conditional checks doesn't fully prevent double-action clicks

💡 Move the restart button check to the very beginning of mousePressed() before any grid calculations, ensuring it is evaluated first and returns immediately, preventing any grid logic from executing on footer clicks

FEATURE drawCells()

Unrevealed cells are invisible (drawn as background), making the board hard to see and click precisely; players have no visual indicator of unrevealed cells

💡 Add an else clause to the `if (c.revealed)` block: draw an empty square with a darker fill (e.g., fill(150)) for unrevealed cells, giving them visual presence and making the grid boundaries clearer

STYLE mousePressed()

The restart button boundary check uses magic number 80 without explanation; if you change mapSize or want to adjust button size, the coordinate becomes unclear

💡 Define a constant like `const restartButtonWidth = 80;` at the top, then use `mouseX > mapSize - restartButtonWidth` to make the code self-documenting

PERFORMANCE checkWin()

checkWin() scans the entire 10×10 grid every time a cell is revealed, doing O(n²) work repeatedly

💡 Track a running count of revealed safe cells as they're revealed, incrementing it in the reveal() function instead of recounting the entire grid each time—reduces checkWin() to an O(1) comparison

🔄 Code Flow

Code flow showing setup, initgrid, placemines, calculatenumbers, foreachneighbor, draw, drawgrid, drawcells, drawfooter, drawmessage, reveal, revealallmines, mousepressed, checkwin

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> initgrid[initGrid] initgrid --> grid-creation-loop[grid-creation-loop] grid-creation-loop --> mine-placement-loop[mine-placement-loop] mine-placement-loop --> duplicate-check[duplicate-check] duplicate-check --> safety-zone-check[safety-zone-check] safety-zone-check --> mine-placement-loop mine-placement-loop --> draw[draw loop] draw --> drawgrid[drawGrid] drawgrid --> grid-line-loop[grid-line-loop] grid-line-loop --> drawcells[drawCells] drawcells --> revealed-cell-drawing[revealed-cell-drawing] revealed-cell-drawing --> flagged-cell-drawing[flagged-cell-drawing] drawcells --> drawfooter[drawFooter] drawfooter --> timer-update[timer-update] drawfooter --> drawmessage[drawMessage] draw --> mousepressed[mousePressed] mousepressed --> game-end-check[game-end-check] game-end-check -->|if game is active| first-click-timer[first-click-timer] first-click-timer --> placemines[placeMines] placemines --> calculatenumbers[calculateNumbers] calculatenumbers --> foreachneighbor[forEachNeighbor] foreachneighbor --> neighbor-delta-loop[neighbor-delta-loop] neighbor-delta-loop --> center-skip[center-skip] center-skip --> bounds-check[bounds-check] bounds-check --> neighbor-iteration[neighbor-iteration] neighbor-iteration --> flood-fill-recursion[flood-fill-recursion] flood-fill-recursion -->|if empty cell| flood-fill-recursion mousepressed --> mine-click-handler[mine-click-handler] mine-click-handler -->|if mine clicked| revealallmines[revealAllMines] mousepressed --> flag-toggle[flag-toggle] flag-toggle --> flagged-cell-check[flagged-cell-check] flagged-cell-check -->|if flagged cell| mousepressed mousepressed --> checkwin[checkWin] checkwin --> safe-cell-count[safe-cell-count] safe-cell-count --> win-message[win-message] win-message --> drawmessage checkwin --> lose-message[lose-message] lose-message --> drawmessage click setup href "#fn-setup" click initgrid href "#fn-initgrid" click draw href "#fn-draw" click drawgrid href "#fn-drawgrid" click drawcells href "#fn-drawcells" click drawfooter href "#fn-drawfooter" click drawmessage href "#fn-drawmessage" click mousepressed href "#fn-mousepressed" click placemines href "#fn-placemines" click calculatenumbers href "#fn-calculatenumbers" click foreachneighbor href "#fn-foreachneighbor" click revealallmines href "#fn-revealallmines" click checkwin href "#fn-checkwin" click grid-creation-loop href "#sub-grid-creation-loop" click mine-placement-loop href "#sub-mine-placement-loop" click duplicate-check href "#sub-duplicate-check" click safety-zone-check href "#sub-safety-zone-check" click neighbor-iteration href "#sub-neighbor-iteration" click neighbor-delta-loop href "#sub-neighbor-delta-loop" click center-skip href "#sub-center-skip" click bounds-check href "#sub-bounds-check" click win-message href "#sub-win-message" click lose-message href "#sub-lose-message" click grid-line-loop href "#sub-grid-line-loop" click revealed-cell-drawing href "#sub-revealed-cell-drawing" click flagged-cell-drawing href "#sub-flagged-cell-drawing" click timer-update href "#sub-timer-update" click flood-fill-recursion href "#sub-flood-fill-recursion" click game-end-check href "#sub-game-end-check" click canvas-bounds-check href "#sub-canvas-bounds-check" click first-click-timer href "#sub-first-click-timer" click flag-toggle href "#sub-flag-toggle" click flagged-cell-check href "#sub-flagged-cell-check" click mine-click-handler href "#sub-mine-click-handler" click safe-cell-count href "#sub-safe-cell-count"

❓ Frequently Asked Questions

What visual elements does the Sketch 2026-01-25 22:50 create on the canvas?

The sketch visually represents a grid-based minefield, where cells can display numbers indicating nearby mines, along with a footer that shows game status messages.

How can users interact with the Sketch 2026-01-25 22:50?

Users can interact with the sketch by clicking on cells to reveal them, aiming to avoid mines while uncovering the grid.

What creative coding concepts does this sketch demonstrate?

This sketch demonstrates concepts of grid-based game design, randomization for mine placement, and event-driven programming for game state management.

Preview

Sketch 2026-01-25 22:50 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-01-25 22:50 - Code flow showing setup, initgrid, placemines, calculatenumbers, foreachneighbor, draw, drawgrid, drawcells, drawfooter, drawmessage, reveal, revealallmines, mousepressed, checkwin
Code Flow Diagram