Minesweeper

This sketch implements the classic Minesweeper game in p5.js with a 10x10 grid where players click to reveal cells and right-click to flag suspected mines. The game includes mine placement that avoids the first click, automatic cell flooding for empty cells, and a timer tracking game duration.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the board bigger — Increase mapSize to make the canvas and cells larger, giving you more space to play
  2. Add more mines — Increase the mineCount constant to make the game harder by placing more mines
  3. Change the background color — Lower the background value from 220 to a darker shade, making the grid stand out more
  4. Make mines easier to spot — Change the mine color from red (255, 0, 0) to a brighter yellow so they're more obvious when revealed
  5. Increase the grid size — Make the grid 12x12 instead of 10x10, creating a larger board with more cells to reveal
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the classic Minesweeper game entirely in p5.js. Players click cells to reveal them, right-click to flag suspected mines, and must uncover all safe cells without hitting a mine. The game features a 10x10 grid, mines that intelligently avoid the first click, automatic cell flooding that reveals empty regions, and a live timer that counts seconds until victory or defeat. It's a beautiful example of how p5.js can power real games with interactive graphics and game state management.

The code is organized around a 2D grid array where each cell stores whether it contains a mine, how many adjacent mines surround it, whether it's been revealed, and whether it's been flagged. You'll learn how the draw loop handles rendering, how mouse events drive gameplay, how recursive flood-fill algorithms work, and how to manage complex game state across multiple variables. By studying this sketch, you'll understand the architectural patterns that make larger p5.js projects possible.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 300x300 canvas with a 30-pixel footer, then initGrid() creates a 10x10 grid of empty cell objects with properties like mine, count, revealed, and flagged.
  2. On the first mouse click, placeMines() randomly distributes mines across the grid while avoiding a 3x3 area around the clicked cell, then calculateNumbers() counts how many mines surround each non-mine cell.
  3. Every frame, draw() renders the grid lines, draws each cell (revealed cells show either a number or red for mines; flagged cells show 'F'), and displays the timer in the footer.
  4. When a player clicks a cell, mousePressed() checks if it's a flag action (right-click or Shift+click) or a reveal action. Flagging toggles the flagged property; revealing calls reveal() which recursively exposes empty cells and their neighbors in a flood-fill pattern.
  5. If a revealed cell contains a mine, gameOver becomes true and revealAllMines() exposes every mine on the board. If all safe cells are revealed, checkWin() detects victory and sets gameWon to true.
  6. The timer increments each frame using millis() and only runs while the game is in progress—pausing on win or loss.

🎓 Concepts You'll Learn

2D arrays and grid data structuresMouse events and input handlingRecursive flood-fill algorithmsGame state managementCollision and neighbor detectionTimer and frame-based animation

📝 Code Breakdown

setup()

setup() runs once when the sketch loads. It prepares the canvas and initializes the game grid.

function setup() {
  createCanvas(mapSize, mapSize + footerHeight);
  textAlign(CENTER, CENTER);
  initGrid();
}
Line-by-line explanation (3 lines)
createCanvas(mapSize, mapSize + footerHeight);
Creates a square canvas 300x330 pixels (300 for the game grid plus 30 for the footer showing the timer and restart button)
textAlign(CENTER, CENTER);
Tells p5.js to draw all text centered at the x,y coordinates you provide—useful for centering numbers and 'F' flags in cells
initGrid();
Calls the function that builds the empty 10x10 grid and resets all game variables to their starting state

initGrid()

initGrid() creates a fresh 2D array and resets all game variables. The nested for-loops pattern (for-in-for) is fundamental to working with grids in games.

🔬 These nested loops build a 2D array (array of arrays). How many cell objects are created if gridSize is 10? What if you change the inner loop condition to j < gridSize + 5?

  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:

for-loop Row creation loop for (let i = 0; i < gridSize; i++)

Creates 10 rows for the grid

for-loop Cell creation loop for (let j = 0; j < gridSize; j++)

Creates 10 cells in each row, 100 total

calculation Cell object row.push({ mine: false, count: 0, revealed: false, flagged: false });

Creates one cell object with four properties tracking its state

grid = [];
Empties the grid array so a restart creates a fresh board instead of duplicating cells
for (let i = 0; i < gridSize; i++)
Loops 10 times to create each row of the grid
let row = [];
Creates an empty array that will hold 10 cells for this row
for (let j = 0; j < gridSize; j++)
Loops 10 times to create each cell in the current row
row.push({ mine: false, count: 0, revealed: false, flagged: false });
Creates one cell object with properties: mine (is it a mine?), count (how many adjacent mines), revealed (has player clicked it?), flagged (has player marked it?)
grid.push(row);
Adds the completed row to the main grid array
gameOver = false; gameWon = false; started = false; firstClick = true; seconds = 0;
Resets all game state variables so a new game begins in a fresh state

placeMines(avoidI, avoidJ)

placeMines() is called only once per game, after the player's first click. This ensures the first move is always safe—a cornerstone of Minesweeper design. The abs() function measures distance, making the safety zone check elegant and compact.

🔬 These two if-statements are gatekeepers—they skip invalid positions. What if you removed the second one entirely? Would mines ever be placed in the 3x3 safety zone around your first click?

    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:

calculation Random cell selection let i = floor(random(gridSize)); let j = floor(random(gridSize));

Picks a random grid position to try placing a mine

conditional Avoid duplicates if (grid[i][j].mine) continue;

Skips this position if a mine is already there

conditional First-click safety zone if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;

Skips the 3x3 area around the first click so the first move is always safe

let placed = 0;
Initializes a counter that tracks how many mines have been successfully placed
while (placed < mineCount)
Repeats until the desired number of mines (13 in a 10x10 grid) have been placed
let i = floor(random(gridSize));
Picks a random row from 0 to 9
let j = floor(random(gridSize));
Picks a random column from 0 to 9
if (grid[i][j].mine) continue;
If a mine already exists at this position, skip it and try another random cell (continue jumps to the next loop iteration)
if (abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1) continue;
If this position is within 1 row and 1 column of the clicked cell (the 3x3 safety zone), skip it and try another random cell—ensures the first click always hits an empty cell
grid[i][j].mine = true;
Places a mine at this position by setting its mine property to true
placed++;
Increments the counter by 1, moving closer to the goal
calculateNumbers();
After all mines are placed, calculates how many mines surround each non-mine cell

calculateNumbers()

calculateNumbers() runs after all mines are placed. It walks every cell, and whenever it finds a mine, it tells all 8 neighbors to increment their count. This number is later displayed as a hint to the player.

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:

for-loop Nested grid iteration for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++)

Loops through every cell in the grid

conditional Mine detection if (grid[i][j].mine)

Only processes cells that contain mines

calculation Count increment forEachNeighbor(i, j, (ni, nj) => grid[ni][nj].count++);

Increments the count of every cell adjacent to this mine

for (let i = 0; i < gridSize; i++)
Loops through each row of the grid
for (let j = 0; j < gridSize; j++)
Loops through each column in the current row
if (grid[i][j].mine)
Checks if the current cell contains a mine
forEachNeighbor(i, j, (ni, nj) => grid[ni][nj].count++);
For every neighbor of this mine cell, increments their count property by 1. The arrow function (ni, nj) => grid[ni][nj].count++ is passed to forEachNeighbor, which calls it for each adjacent cell.

forEachNeighbor(i, j, fn)

forEachNeighbor() is a utility function that encapsulates the common pattern of visiting all 8 neighbors of a grid cell. It's reused by calculateNumbers() and reveal(), showing how good function design reduces code duplication.

🔬 These nested loops create the 8 neighbors of a cell (like the king's move in chess). What if you changed the condition to only allow di === 0 OR dj === 0 (removing the diagonal check)? How many neighbors would you have?

  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 (8 lines)

🔧 Subcomponents:

for-loop Neighbor offset loops for (let di = -1; di <= 1; di++) { for (let dj = -1; dj <= 1; dj++)

Generates all 8 direction offsets around a cell

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

Excludes the center cell itself from neighbors

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

Only calls the function for neighbors that exist within the grid

function forEachNeighbor(i, j, fn)
Accepts a cell position (i, j) and a callback function fn that will be called for each neighbor
for (let di = -1; di <= 1; di++)
Loops through row offsets: -1 (row above), 0 (same row), +1 (row below)
for (let dj = -1; dj <= 1; dj++)
Loops through column offsets: -1 (column left), 0 (same column), +1 (column right)
if (di === 0 && dj === 0) continue;
Skips the center cell (when both offsets are 0) because we only want the 8 neighbors, not the cell itself
let ni = i + di;
Calculates the actual row index of the neighbor by adding the offset to the original row
let nj = j + dj;
Calculates the actual column index of the neighbor by adding the offset to the original column
if (ni >= 0 && ni < gridSize && nj >= 0 && nj < gridSize)
Checks that the neighbor position is within the grid boundaries before using it—prevents accessing cells outside the array
fn(ni, nj);
Calls the callback function with the neighbor's position, allowing the caller to perform any action on that neighbor

draw()

draw() is the main animation loop, running 60 times per second. Each frame it clears the canvas and calls helper functions to render different parts of the game.

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

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

🔧 Subcomponents:

calculation Canvas clear background(220);

Repaints the entire canvas with light gray, erasing previous frame

conditional Victory display if (gameWon) drawMessage("You Win!");

Displays the victory message when the player has won

conditional Defeat display if (gameOver) drawMessage("Game Over");

Displays the game over message when the player has hit a mine

background(220);
Fills the entire canvas with light gray (220 brightness on a 0-255 scale), clearing the previous frame
drawGrid();
Calls the function that draws the black lines forming the 10x10 grid
drawCells();
Calls the function that draws each cell—either revealed (showing a number or red mine) or flagged (showing 'F')
drawFooter();
Calls the function that draws the timer and restart button at the bottom
if (gameWon) drawMessage("You Win!");
If the player has won, displays 'You Win!' in large text centered on the screen
if (gameOver) drawMessage("Game Over");
If the player has hit a mine, displays 'Game Over' in large text centered on the screen

drawGrid()

drawGrid() is simple but foundational—it draws the visual grid lines that separate cells. The loop starts at 1 (not 0) because the edges of the canvas are already the first and last grid lines.

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++)

Creates 9 vertical and 9 horizontal lines to form a 10x10 grid

stroke(0);
Sets the line color to black (0 brightness)
for (let i = 1; i < gridSize; i++)
Loops from 1 to 9 (skipping 0), creating 9 divisions that split the canvas into 10 equal sections
line(i * cellSize, 0, i * cellSize, mapSize);
Draws a vertical line at position i*cellSize (every 30 pixels), spanning from top (0) to bottom (mapSize=300)
line(0, i * cellSize, mapSize, i * cellSize);
Draws a horizontal line at position i*cellSize, spanning from left (0) to right (mapSize=300)

drawCells()

drawCells() is responsible for the visual feedback—showing players what is revealed, what numbers hint at mine locations, and what they've flagged. The nested loop structure lets it check every cell's state.

🔬 This block renders revealed cells in two colors, then draws numbers on safe cells with adjacent mines. What happens if you comment out the 'square()' call? Will you still see the 'F' flags?

      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 (14 lines)

🔧 Subcomponents:

for-loop Cell rendering loop for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++)

Iterates through all 100 cells to render them

conditional Revealed cell display if (c.revealed)

Only renders cells that have been clicked and revealed

conditional Mine or safe coloring fill(c.mine ? color(255, 0, 0, 150) : color(255, 255, 200));

Uses a ternary operator to choose red for mines or light yellow for safe cells

conditional Adjacent mine count if (!c.mine && c.count > 0)

Displays the number of adjacent mines only for safe cells with neighbors

conditional Flag display if (c.flagged)

Displays an 'F' on cells the player has flagged

textSize(cellSize * 0.6);
Sets the text size to 60% of the cell size (18 pixels) so numbers and 'F' flags fit nicely in cells
for (let i = 0; i < gridSize; i++)
Loops through each row of the grid
for (let j = 0; j < gridSize; j++)
Loops through each column in the current row
let c = grid[i][j];
Stores a reference to the current cell object for easier access
let x = j * cellSize;
Calculates the pixel x-coordinate of the cell's top-left corner (column * 30)
let y = i * cellSize;
Calculates the pixel y-coordinate of the cell's top-left corner (row * 30)
if (c.revealed)
Checks if this cell has been clicked and revealed
fill(c.mine ? color(255, 0, 0, 150) : color(255, 255, 200));
Uses a ternary operator: if c.mine is true, use semi-transparent red (255,0,0,150); otherwise use light yellow (255,255,200)
square(x, y, cellSize);
Draws a square at position (x,y) with side length cellSize (30 pixels), filling it with the color set by fill()
if (!c.mine && c.count > 0)
Only displays a number if the cell is safe (not a mine) AND has at least one adjacent mine
fill(0);
Changes the fill color to black for the number text
text(c.count, x + cellSize / 2, y + cellSize / 2);
Draws the count (1-8) at the center of the cell—textAlign(CENTER,CENTER) from setup() centers it
if (c.flagged)
Checks if this cell has been right-clicked and flagged
text("F", x + cellSize / 2, y + cellSize / 2);
Draws the letter 'F' at the center of the cell

drawFooter()

drawFooter() displays two pieces of UI: a live timer that counts up during gameplay, and a clickable restart button. The timer only increments while the game is active, pausing on win or loss.

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 (6 lines)

🔧 Subcomponents:

conditional Timer update if (started && !gameOver && !gameWon)

Only updates the timer while the game is in progress

calculation Timer rendering text("⏱ " + seconds, 40, mapSize + footerHeight / 2);

Shows the elapsed time

fill(0);
Sets text color to black
textSize(16);
Sets the text size to 16 pixels
if (started && !gameOver && !gameWon)
Only updates the timer if the game has started AND is not over (from hitting a mine) AND is not won (from revealing all safe cells)
seconds = floor((millis() - startTime) / 1000);
Calculates elapsed time by subtracting the game start time from the current time (millis()), dividing by 1000 to convert milliseconds to seconds, then using floor() to round down to a whole number
text("⏱ " + seconds, 40, mapSize + footerHeight / 2);
Draws the timer display (clock emoji + number) at position (40, 315) in the footer
text("Restart", mapSize - 40, mapSize + footerHeight / 2);
Draws the word 'Restart' at position (260, 315) on the right side of the footer—clicking this area calls initGrid()

drawMessage(msg)

drawMessage() is a simple utility function that displays large centered text. It's called by draw() when the game ends, showing 'You Win!' or 'Game Over'.

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 the text size to 48 pixels, making it large and visible
text(msg, width / 2, height / 2);
Draws the message string centered on the screen at (width/2, height/2) because textAlign was set to CENTER,CENTER in setup()

reveal(i, j)

reveal() is a recursive function that reveals one cell and, if it's empty, reveals all its neighbors (which may reveal their neighbors, and so on). This creates the flood-fill effect where clicking one empty cell opens up a whole region—a hallmark of Minesweeper.

🔬 This is flood-fill: when you reveal an empty cell, it automatically reveals its neighbors. What if you changed the condition to c.count <= 1? Would more cells open automatically—or would the recursion never stop?

  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:

conditional Already revealed check if (c.revealed || c.flagged) return;

Prevents re-revealing cells and stops revealing flagged cells

conditional Recursive flood-fill if (c.count === 0 && !c.mine)

Triggers automatic reveal of all adjacent cells if this cell is empty

let c = grid[i][j];
Gets a reference to the cell at position (i,j)
if (c.revealed || c.flagged) return;
If this cell is already revealed or is flagged, stop the function immediately—don't reveal it again
c.revealed = true;
Marks this cell as revealed, so it will be displayed with a color and potentially a number
if (c.count === 0 && !c.mine)
Checks if this cell is safe (not a mine) AND has no adjacent mines (count is 0)
forEachNeighbor(i, j, reveal);
If the cell is empty, recursively calls reveal() on all 8 neighbors, which themselves call reveal() on their neighbors if empty, creating a flood-fill effect that opens up empty regions

revealAllMines()

revealAllMines() is called when the player hits a mine (loses the game). It exposes every mine on the board so the player can see where all the mines were—a classic Minesweeper pattern.

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)

🔧 Subcomponents:

for-loop Mine iteration and reveal for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++)

Checks every cell and reveals those that contain mines

for (let i = 0; i < gridSize; i++)
Loops through each row
for (let j = 0; j < gridSize; j++)
Loops through each column
if (grid[i][j].mine)
Checks if the cell contains a mine
grid[i][j].revealed = true;
Marks the mine as revealed so it will be drawn in red on the screen

mousePressed()

mousePressed() is where all interaction happens: restart detection, flag toggling, mine placement, mine hitting, and revealing cells. It coordinates multiple game systems—game state, timer, mine placement, and win checking—making it the game's nerve center.

🔬 This code places mines only on the first click. What happens if you comment out 'firstClick = false'? Will mines be placed on every click?

  if (firstClick) {
    placeMines(i, j);
    firstClick = false;
  }
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 (25 lines)

🔧 Subcomponents:

conditional Restart button detection if (mouseY > mapSize && mouseX > mapSize - 80)

Detects clicks in the footer restart area

conditional Game state validation if (gameOver || gameWon) return;

Prevents clicks after the game has ended

conditional Grid bounds check if (mouseX > mapSize || mouseY > mapSize) return;

Ignores clicks outside the game grid

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

Starts the timer on the first click

calculation Grid index calculation let j = floor(mouseX / cellSize); let i = floor(mouseY / cellSize);

Converts pixel coordinates to grid indices

conditional Flag toggle if (mouseButton === RIGHT || keyIsDown(SHIFT))

Detects right-click or Shift+click to toggle flags

conditional Mine collision if (c.mine)

Ends the game when a mine is clicked

// Restart button
Comment explaining the next section
if (mouseY > mapSize && mouseX > mapSize - 80)
Checks if the mouse click is in the footer (Y > 300) and on the right side (X > 220)—the restart button area
initGrid();
Resets the entire game when restart is clicked
return;
Exits the function early so no other code runs
if (gameOver || gameWon) return;
If the game has ended, ignore all subsequent clicks until restart is clicked
if (mouseX > mapSize || mouseY > mapSize) return;
If the click is outside the grid (in the footer), ignore it
if (!started)
Checks if the game hasn't started yet (first click)
started = true;
Marks the game as started
startTime = millis();
Records the current time in milliseconds so the timer can calculate elapsed time
let j = floor(mouseX / cellSize);
Divides the mouse's pixel X-coordinate by the cell size (30) and rounds down to get the column index (0-9)
let i = floor(mouseY / cellSize);
Divides the mouse's pixel Y-coordinate by the cell size (30) and rounds down to get the row index (0-9)
let c = grid[i][j];
Gets the cell object at the clicked position
if (mouseButton === RIGHT || keyIsDown(SHIFT))
Checks if the user right-clicked OR held Shift while clicking (two ways to flag on systems without right-click)
if (!c.revealed) c.flagged = !c.flagged;
Toggles the flagged property—if it was true, make it false; if false, make it true. Only allows flagging unrevealed cells
if (c.flagged) return;
If the cell is flagged, don't reveal it even if left-clicked
// First click safety
Comment explaining the safety zone logic
if (firstClick)
On the very first click of the game
placeMines(i, j);
Places all mines, avoiding the clicked cell and its 3x3 neighborhood
firstClick = false;
Marks that the first click has occurred so mines won't be placed again
if (c.mine)
Checks if the clicked cell contains a mine
c.revealed = true;
Marks the mine as revealed so it displays in red
revealAllMines();
Reveals all other mines on the board
gameOver = true;
Sets the game state to over, preventing further clicks and triggering the 'Game Over' message
reveal(i, j);
Reveals the clicked cell and recursively reveals neighbors if it's empty (flood-fill)
checkWin();
Checks if all safe cells are now revealed, triggering victory if so

checkWin()

checkWin() is simple but critical: after every reveal, it counts how many safe cells have been exposed and compares that to the total number of safe cells. When they match, victory is triggered.

🔬 This checks if the number of revealed safe cells equals the total number of safe cells. What if you changed === to >? Could the player win before revealing all safe cells?

  if (safe === gridSize * gridSize - mineCount) {
    gameWon = true;
  }
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:

for-loop Safe cell counter for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++)

Counts how many safe cells have been revealed

conditional Victory detection if (safe === gridSize * gridSize - mineCount)

Checks if the number of revealed safe cells equals the total safe cells

let safe = 0;
Initializes a counter to 0
for (let i = 0; i < gridSize; i++)
Loops through each row
for (let j = 0; j < gridSize; j++)
Loops through each column
if (grid[i][j].revealed && !grid[i][j].mine) safe++;
If the cell is revealed AND is not a mine, increment the counter
if (safe === gridSize * gridSize - mineCount)
Checks if the number of revealed safe cells equals the total safe cells (100 - 13 = 87 in standard mode)
gameWon = true;
Sets the game state to won, triggering the 'You Win!' message and pausing the timer

📦 Key Variables

grid array

A 2D array (array of arrays) storing all 100 cell objects, each with properties like mine, count, revealed, and flagged

let grid = [];
gridSize number

The width and height of the grid in cells (10x10 = 100 total cells)

const gridSize = 10;
mapSize number

The pixel width and height of the game canvas (300x300), used to calculate cell size and check click bounds

const mapSize = 300;
footerHeight number

The height in pixels of the footer area containing the timer and restart button (30 pixels)

const footerHeight = 30;
cellSize number

The width and height of each cell in pixels, calculated as mapSize / gridSize (30 pixels for 300/10)

const cellSize = mapSize / gridSize;
mineCount number

The total number of mines placed on the board, calculated as 1/8 of the total cells plus 1 (13 mines for 10x10)

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

Tracks whether the player has hit a mine and lost (true) or is still playing (false)

let gameOver = false;
gameWon boolean

Tracks whether the player has revealed all safe cells and won (true) or is still playing (false)

let gameWon = false;
started boolean

Tracks whether the player has made the first click and the game is in progress (true) or waiting to start (false)

let started = false;
startTime number

Stores the millisecond timestamp when the first click occurs, used to calculate elapsed time

let startTime = 0;
seconds number

Displays the elapsed seconds since the game started, recalculated every frame

let seconds = 0;
firstClick boolean

Tracks whether the first click of the game has occurred (true before first click, false after), used to trigger mine placement

let firstClick = true;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG mousePressed() and drawCells()

Flagged cells can display both 'F' and a number/mine color, creating visual overlap. If a flagged mine is revealed, both the 'F' and red mine color appear simultaneously.

💡 In drawCells(), draw the flag INSTEAD of the background color when flagged, not in addition to it. Change the flagged block to draw its own background: if (c.flagged) { fill(200); square(x, y, cellSize); fill(0); text('F', ...); }

PERFORMANCE drawCells() and checkWin()

Every frame, drawCells() loops through all 100 cells to render them, and checkWin() loops through all 100 cells to count revealed safe cells on every reveal. This is inefficient.

💡 Add a 'revealedCount' variable that increments when reveal() is called, eliminating the need to loop. checkWin() can then just check if revealedCount === gridSize * gridSize - mineCount.

FEATURE mousePressed()

The restart button is a text label in the footer but has no visual button styling, making it unclear that it's clickable

💡 Draw a visible button rectangle in drawFooter() before drawing the text. Use: fill(200); rect(mapSize - 80, mapSize + 5, 70, 20); to create a button background.

BUG placeMines()

The safety zone check uses abs(i - avoidI) <= 1 && abs(j - avoidJ) <= 1, which allows mines to be placed immediately adjacent (distance 1) to the first click. The comment says '3x3 area' but the actual safe zone is correct—this is misleading documentation.

💡 The code is correct (3x3 includes the clicked cell and 8 surrounding cells), but the placement loop could theoretically infinite-loop on very small grids if not enough safe spaces exist. Add a maximum iteration count to placeMines() to prevent hangs: let attempts = 0; while (placed < mineCount && attempts < 1000) { ... attempts++; }

STYLE drawCells() and drawFooter()

Magic numbers like 0.6 (text scaling), 16 (footer text size), and 48 (message text size) are hardcoded throughout the code, making it hard to maintain consistent styling

💡 Define constants at the top: const CELL_TEXT_SIZE_RATIO = 0.6; const FOOTER_TEXT_SIZE = 16; const MESSAGE_TEXT_SIZE = 48; Then use these throughout.

🔄 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 --> placemines[placeMines] placemines --> calculatenumbers[calculateNumbers] calculatenumbers --> draw[draw loop] draw --> backgroundclear[background-clear] backgroundclear --> drawgrid[drawGrid] drawgrid --> lineloop[line-loop] lineloop --> drawcells[drawCells] drawcells --> cellloop[cell-loop] cellloop --> revealedrender[revealed-render] revealedrender --> minevssafe[mine-vs-safe] minevssafe --> numberdisplay[number-display] numberdisplay --> flagrender[flag-render] flagrender --> drawfooter[drawFooter] drawfooter --> timerdisplay[timer-display] timerdisplay --> drawmessage[drawMessage] drawmessage --> mousepressed[mousePressed] mousepressed --> restartcheck[restart-check] restartcheck --> gameStateCheck[game-state-check] gameStateCheck --> boundscheck[bounds-check] boundscheck --> timerstart[timer-start] timerstart --> flagaction[flag-action] flagaction --> minehit[mine-hit] minehit --> reveal[reveal] reveal --> floodfill[flood-fill] floodfill --> statecheck[state-check] statecheck --> wincounting[win-counting] wincounting --> wincondition[win-condition] wincondition --> winmessage[win-message] winmessage --> losemessage[lose-message] losemessage --> draw click setup href "#fn-setup" click initgrid href "#fn-initgrid" click placemines href "#fn-placemines" click calculatenumbers href "#fn-calculatenumbers" click draw href "#fn-draw" click backgroundclear href "#sub-background-clear" click drawgrid href "#fn-drawgrid" click lineloop href "#sub-line-loop" click drawcells href "#fn-drawcells" click cellloop href "#sub-cell-loop" click revealedrender href "#sub-revealed-render" click minevssafe href "#sub-mine-vs-safe" click numberdisplay href "#sub-number-display" click flagrender href "#sub-flag-render" click drawfooter href "#fn-drawfooter" click timerdisplay href "#sub-timer-display" click drawmessage href "#fn-drawmessage" click mousepressed href "#fn-mousepressed" click restartcheck href "#sub-restart-check" click gameStateCheck href "#sub-game-state-check" click boundscheck href "#sub-bounds-check" click timerstart href "#sub-timer-start" click flagaction href "#sub-flag-action" click minehit href "#sub-mine-hit" click reveal href "#fn-reveal" click floodfill href "#sub-flood-fill" click statecheck href "#sub-state-check" click wincounting href "#sub-win-counting" click wincondition href "#sub-win-condition" click winmessage href "#sub-win-message" click losemessage href "#sub-lose-message"

❓ Frequently Asked Questions

What visual elements are present in the Minesweeper sketch?

The Minesweeper sketch features a grid representing cells, with lines demarcating the boundaries, and visual indicators for revealed cells, mine counts, and game status messages.

How can users interact with the Minesweeper sketch?

Users can click on the grid cells to reveal them, flag potential mines, and aim to clear the grid without triggering any mines.

What creative coding techniques does the Minesweeper sketch showcase?

The sketch demonstrates grid management, random placement of elements (mines), and interactive game logic, allowing for a dynamic and engaging user experience.

Preview

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