Numbers shuffle

This sketch is a classic sliding number puzzle (like the 15-puzzle) rendered entirely with p5.js shapes and text. Numbered tiles sit in a grid with one empty slot, and you click a tile next to the empty space to slide it into place, trying to restore the numbers to sequential order.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make it a 4x4 puzzle — Increasing gridSize creates a bigger, harder 15-puzzle with more tiles to arrange.
  2. Lighter shuffle for an easier warm-up — Fewer random moves during shuffleGrid() leaves the puzzle only slightly scrambled, making it easy to solve quickly.
  3. Recolor the tiles — Changing the fill color in drawGrid() gives all the numbered tiles a completely different look.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the classic sliding tile puzzle: a grid of numbered squares with one gap, where clicking a tile next to the gap slides it into place. It's a great sketch to study because it shows how to represent a game board as a 2D array, how to translate pixel mouse coordinates into grid cells, and how to detect a win condition by comparing the board to a target arrangement. It also demonstrates p5.js DOM integration by creating an HTML button alongside the canvas.

The code separates concerns cleanly: initializeGrid() and shuffleGrid() set up game state, drawGrid()/drawEmptySpace()/drawInfo()/drawSolvedOverlay() handle rendering, and mousePressed()/moveTile()/swapTiles()/checkSolved() handle interaction and logic. Studying it will teach you how to build a grid-based game from scratch, how push()/pop()/translate() simplify drawing repeated grid cells, and how a shuffle can be generated by replaying valid moves backward so the puzzle always stays solvable.

⚙️ How It Works

  1. When the sketch loads, setup() sizes the canvas to the window, calculates the board size and tile size from the smaller screen dimension, calls initializeGrid() to fill a 2D array with numbers 1 through 8 (plus an empty 0 slot), and creates a styled HTML 'Shuffle' button.
  2. Every frame, draw() clears the background and calls drawGrid() to render each numbered tile, drawEmptySpace() to shade the gap, and drawInfo() to show the move counter and status text.
  3. Clicking the Shuffle button runs shuffleGrid(), which calls makeRandomMove() 500 times - each call finds the tiles adjacent to the empty space and swaps one in randomly, guaranteeing the resulting shuffle is always solvable.
  4. Clicking a tile triggers mousePressed(), which converts the mouse's pixel position into grid row/column coordinates and calls moveTile() to check if that tile is next to the empty space.
  5. If the clicked tile is adjacent, swapTiles() exchanges it with the empty slot and moveTile() increments the move counter and calls checkSolved() to compare the grid against the winning 1-8-then-0 order.
  6. When checkSolved() returns true, isSolved becomes true and draw() calls drawSolvedOverlay() to paint a translucent green 'Solved!' message over the board.

🎓 Concepts You'll Learn

2D arraysMouse-to-grid coordinate mappingpush()/pop()/translate() transformsDOM integration (createButton)Win condition checkingRandomized valid-move shufflingwindowResized responsive layout

📝 Code Breakdown

setup()

setup() runs once at the start. Here it does double duty: computing responsive layout math AND creating an HTML button via p5's DOM functions, showing how canvas drawing and regular DOM elements can coexist in the same sketch.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Calculate sizes based on the smaller dimension to fit on screen
  let minDim = min(windowWidth, windowHeight);
  padding = minDim * 0.05; // 5% padding
  boardSize = minDim - 2 * padding;
  tileSize = boardSize / gridSize;

  initializeGrid();
  // Create a button to shuffle/reset the game
  shuffleButton = createButton('Shuffle');
  shuffleButton.position(padding, padding + boardSize + 20); // Position below the board
  shuffleButton.mousePressed(shuffleGrid); // Call shuffleGrid when button is pressed
  shuffleButton.style('font-size', '24px');
  shuffleButton.style('padding', '10px 20px');
  shuffleButton.style('background-color', '#4CAF50'); // Green button
  shuffleButton.style('color', 'white');
  shuffleButton.style('border', 'none');
  shuffleButton.style('border-radius', '5px');
  shuffleButton.style('cursor', 'pointer');
}
Line-by-line explanation (10 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
let minDim = min(windowWidth, windowHeight);
Finds the smaller of width/height so the square board always fits on screen, even on tall or wide windows.
padding = minDim * 0.05;
Reserves 5% of the smaller dimension as empty margin around the board.
boardSize = minDim - 2 * padding;
The board itself is the smaller dimension minus padding on both sides.
tileSize = boardSize / gridSize;
Divides the board evenly into gridSize columns/rows to get each tile's pixel size.
initializeGrid();
Fills the grid array with numbers in solved order and marks the puzzle as solved.
shuffleButton = createButton('Shuffle');
Creates a real HTML button element (not drawn on canvas) using p5's DOM API.
shuffleButton.position(padding, padding + boardSize + 20);
Places the button just below the board using the same padding math as the board layout.
shuffleButton.mousePressed(shuffleGrid);
Registers shuffleGrid() to run whenever the button is clicked.
shuffleButton.style('background-color', '#4CAF50');
Uses CSS-in-JS style calls to give the button a green background, part of p5's DOM styling API.

initializeGrid()

This function shows the standard pattern for building a 2D array in JavaScript: an outer loop creates rows, an inner loop fills each row's columns. Using 0 as a sentinel value for 'empty' avoids needing a separate data structure.

function initializeGrid() {
  grid = [];
  let num = 1;
  for (let i = 0; i < gridSize; i++) {
    grid[i] = [];
    for (let j = 0; j < gridSize; j++) {
      if (i === gridSize - 1 && j === gridSize - 1) {
        grid[i][j] = 0; // Empty space is represented by 0
        emptyX = j;
        emptyY = i;
      } else {
        grid[i][j] = num++;
      }
    }
  }
  isSolved = true; // Initially, the puzzle is solved
  moves = 0;
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Fill Grid Rows and Columns for (let i = 0; i < gridSize; i++) { grid[i] = []; for (let j = 0; j < gridSize; j++) {

Builds a 2D array row by row and column by column to represent the board.

conditional Place the Empty Slot if (i === gridSize - 1 && j === gridSize - 1) {

Reserves the very last cell (bottom-right) as the empty space, marked with 0.

grid = [];
Resets the grid to an empty array before rebuilding it.
let num = 1;
Tracks the next number to place in the grid, starting at 1.
for (let i = 0; i < gridSize; i++) {
Loops over each row of the grid.
grid[i] = [];
Creates an empty array for this row before filling in columns.
for (let j = 0; j < gridSize; j++) {
Loops over each column within the current row.
if (i === gridSize - 1 && j === gridSize - 1) {
Checks if this is the last cell in the grid (bottom-right corner).
grid[i][j] = 0; // Empty space is represented by 0
Marks the last cell as empty using the number 0 as a special code.
emptyX = j;
Remembers the empty space's column so other functions can find it quickly.
emptyY = i;
Remembers the empty space's row.
grid[i][j] = num++;
Places the next sequential number in this cell, then increments the counter.
isSolved = true; // Initially, the puzzle is solved
Since the grid was just filled in order, the puzzle starts in a solved state.

shuffleGrid()

Rather than randomly placing numbers (which can create unsolvable puzzles), this function shuffles by replaying legal moves backward from the solved state - a common trick that guarantees the scrambled puzzle can always be solved again.

🔬 This loop determines how scrambled the puzzle gets. What happens if you change 500 to just 5? Can you still easily solve it, or does it look barely shuffled at all?

  for (let i = 0; i < 500; i++) {
    makeRandomMove();
  }
function shuffleGrid() {
  shuffling = true; // Set shuffling state
  isSolved = false;
  moves = 0;

  // Perform a fixed number of random moves to shuffle the grid
  // A larger number of moves results in a more thoroughly shuffled puzzle
  for (let i = 0; i < 500; i++) {
    makeRandomMove();
  }
  shuffling = false; // Reset shuffling state
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Repeat Random Moves for (let i = 0; i < 500; i++) { makeRandomMove(); }

Replays 500 valid random slides to scramble the board while guaranteeing it stays solvable.

shuffling = true; // Set shuffling state
Flags the game as currently shuffling so drawInfo() can show a 'Shuffling...' message and mousePressed() can ignore clicks.
isSolved = false;
Clears the solved flag since the board is about to be scrambled.
moves = 0;
Resets the player's move counter for the new game.
for (let i = 0; i < 500; i++) {
Runs the shuffle loop 500 times - more iterations means a more thoroughly mixed board.
makeRandomMove();
Performs one random valid slide, moving the empty space to a random neighboring cell.
shuffling = false; // Reset shuffling state
Turns off the shuffling flag once all random moves are complete, re-enabling player clicks.

makeRandomMove()

This function demonstrates boundary checking in a grid: instead of trying a move and correcting errors, it first builds a list of only the moves that are actually legal, then picks one at random - a pattern useful in many grid-based games.

function makeRandomMove() {
  let possibleMoves = [];
  // Check adjacent tiles to the empty space
  if (emptyY > 0) possibleMoves.push({x: emptyX, y: emptyY - 1}); // Up
  if (emptyY < gridSize - 1) possibleMoves.push({x: emptyX, y: emptyY + 1}); // Down
  if (emptyX > 0) possibleMoves.push({x: emptyX - 1, y: emptyY}); // Left
  if (emptyX < gridSize - 1) possibleMoves.push({x: emptyX + 1, y: emptyY}); // Right

  if (possibleMoves.length > 0) {
    let move = random(possibleMoves); // Pick a random valid move
    swapTiles(move.x, move.y);
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Collect Valid Neighbor Moves if (emptyY > 0) possibleMoves.push({x: emptyX, y: emptyY - 1}); // Up

Only adds a direction to the list of possible moves if it stays inside the grid boundaries.

let possibleMoves = [];
Starts an empty list to collect the cells that could legally swap with the empty space.
if (emptyY > 0) possibleMoves.push({x: emptyX, y: emptyY - 1}); // Up
If there's a row above the empty space, add it as a possible move (an object storing its x,y).
if (emptyY < gridSize - 1) possibleMoves.push({x: emptyX, y: emptyY + 1}); // Down
If there's a row below, add that as an option too.
if (emptyX > 0) possibleMoves.push({x: emptyX - 1, y: emptyY}); // Left
Checks for a valid column to the left of the empty space.
if (emptyX < gridSize - 1) possibleMoves.push({x: emptyX + 1, y: emptyY}); // Right
Checks for a valid column to the right.
let move = random(possibleMoves); // Pick a random valid move
p5's random() function can pick a random item from an array, here choosing one of the valid neighbor moves.
swapTiles(move.x, move.y);
Performs the chosen move by swapping that tile with the empty space.

draw()

draw() is p5's main animation loop, running ~60 times per second. Here it acts more like a 'render current state' function since the puzzle only changes on clicks, but redrawing every frame keeps the code simple and ready to react instantly to game state changes.

function draw() {
  background(240); // Light grey background

  drawGrid();
  drawEmptySpace();
  drawInfo();

  if (isSolved) {
    drawSolvedOverlay(); // Show "Solved!" message
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Show Solved Overlay if (isSolved) { drawSolvedOverlay(); }

Only draws the green 'Solved!' overlay when the puzzle has actually been completed.

background(240); // Light grey background
Repaints the whole canvas each frame so nothing from the previous frame lingers.
drawGrid();
Renders all the numbered tiles in their current positions.
drawEmptySpace();
Shades in the empty gap so the board looks complete.
drawInfo();
Draws the move counter and status text below the board.
if (isSolved) {
Checks the global isSolved flag, which gets updated by moveTile()/checkSolved() whenever a tile is moved.
drawSolvedOverlay(); // Show "Solved!" message
Paints a translucent green overlay with a 'Solved!' message on top of the board.

drawGrid()

This function shows the classic pattern for drawing a grid: loop over rows and columns, multiply indices by a fixed cell size to get pixel coordinates, and use translate() so you don't have to add the board's offset to every single shape.

🔬 The number's size is set to tileSize * 0.4. What happens visually if you change 0.4 to 0.8? Does the number start overflowing the tile?

        fill(0); // Text color
        noStroke();
        textAlign(CENTER, CENTER);
        textSize(tileSize * 0.4); // Dynamic text size based on tile size
        text(grid[i][j], x + tileSize / 2, y + tileSize / 2);
function drawGrid() {
  push();
  translate(padding, padding); // Translate to top-left of the board
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      if (grid[i][j] !== 0) { // Don't draw the empty tile
        let x = j * tileSize;
        let y = i * tileSize;

        fill(200); // Tile background color
        stroke(100);
        strokeWeight(2);
        rect(x, y, tileSize, tileSize, 5); // Rounded rectangles for tiles

        fill(0); // Text color
        noStroke();
        textAlign(CENTER, CENTER);
        textSize(tileSize * 0.4); // Dynamic text size based on tile size
        text(grid[i][j], x + tileSize / 2, y + tileSize / 2);
      }
    }
  }
  pop();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

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

Visits every row/column combination in the grid array to draw each tile once.

conditional Skip the Empty Tile if (grid[i][j] !== 0) { // Don't draw the empty tile

Prevents drawing a numbered tile where the empty gap should be.

push();
Saves the current drawing style/transform state so changes inside this function don't leak into other functions.
translate(padding, padding); // Translate to top-left of the board
Shifts the coordinate origin so (0,0) becomes the board's top-left corner, simplifying tile position math.
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 within that row.
if (grid[i][j] !== 0) { // Don't draw the empty tile
Skips drawing a tile if this cell holds the special 'empty' value 0.
let x = j * tileSize;
Converts the column index into a pixel x-coordinate by multiplying by the tile's pixel size.
let y = i * tileSize;
Converts the row index into a pixel y-coordinate the same way.
rect(x, y, tileSize, tileSize, 5); // Rounded rectangles for tiles
Draws the tile's background square with slightly rounded corners (the 5 is the corner radius).
textSize(tileSize * 0.4); // Dynamic text size based on tile size
Scales the number's font size relative to the tile size so it always looks proportional, even if the window is resized.
text(grid[i][j], x + tileSize / 2, y + tileSize / 2);
Draws the tile's number centered in the middle of its square.

drawEmptySpace()

Separating the empty space's drawing from the numbered tiles keeps drawGrid() simpler and makes it easy to style the gap differently, such as making it fully transparent to look like a real hole in the board.

function drawEmptySpace() {
  push();
  translate(padding, padding);
  let x = emptyX * tileSize;
  let y = emptyY * tileSize;
  fill(180); // Empty space color
  noStroke();
  rect(x, y, tileSize, tileSize, 5); // Rounded rectangle for empty space
  pop();
}
Line-by-line explanation (5 lines)
translate(padding, padding);
Aligns coordinates with the board's top-left corner, same as in drawGrid().
let x = emptyX * tileSize;
Converts the empty space's column index into a pixel x-position.
let y = emptyY * tileSize;
Converts the empty space's row index into a pixel y-position.
fill(180); // Empty space color
Uses a slightly darker grey than the tiles to visually distinguish the gap.
rect(x, y, tileSize, tileSize, 5); // Rounded rectangle for empty space
Draws the empty slot as a plain rounded square with no number on it.

drawInfo()

This function demonstrates using boolean state variables (isSolved, shuffling) to drive which UI text and colors get displayed - a very common pattern in interactive sketches and games.

function drawInfo() {
  fill(0);
  noStroke();
  textSize(20);
  textAlign(LEFT, TOP);
  text(`Moves: ${moves}`, padding, padding + boardSize + 80); // Info below the button

  if (isSolved && !shuffling) {
    fill(0, 150, 0); // Green color for solved message
    text('Puzzle Solved!', padding, padding + boardSize + 110);
  } else if (shuffling) {
    fill(200, 0, 0); // Red color for shuffling message
    text('Shuffling...', padding, padding + boardSize + 110);
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Choose Status Message if (isSolved && !shuffling) { fill(0, 150, 0); text('Puzzle Solved!', padding, padding + boardSize + 110); } else if (shuffling) {

Picks which status text (solved, shuffling, or nothing) to show based on the current game state.

text(`Moves: ${moves}`, padding, padding + boardSize + 80); // Info below the button
Uses a JavaScript template literal to embed the moves variable directly inside the displayed string.
if (isSolved && !shuffling) {
Only shows the 'solved' message if the puzzle is solved AND we're not mid-shuffle (avoids a false positive right after shuffling).
fill(0, 150, 0); // Green color for solved message
Sets the text color to green to signal success.
} else if (shuffling) {
If instead we're currently shuffling, show a different message.
fill(200, 0, 0); // Red color for shuffling message
Uses red to indicate the board is temporarily locked while shuffling.

drawSolvedOverlay()

This function shows how RGBA color values with an alpha channel create semi-transparent overlays, a useful technique for win screens, dimming effects, or highlighting states without hiding what's underneath.

function drawSolvedOverlay() {
  push();
  fill(0, 200, 0, 150); // Semi-transparent green overlay
  noStroke();
  translate(padding, padding);
  rect(0, 0, boardSize, boardSize);

  fill(255); // White text
  textSize(tileSize * 0.6);
  textAlign(CENTER, CENTER);
  text('Solved!', boardSize / 2, boardSize / 2);
  pop();
}
Line-by-line explanation (4 lines)
fill(0, 200, 0, 150); // Semi-transparent green overlay
The fourth number (150) is the alpha/transparency value, letting the board show through the green overlay.
rect(0, 0, boardSize, boardSize);
Draws a rectangle covering the entire board area, now that we're translated to the board's top-left.
textSize(tileSize * 0.6);
Scales the 'Solved!' text relative to tile size so it looks proportionally large on any grid size.
text('Solved!', boardSize / 2, boardSize / 2);
Centers the celebratory text in the middle of the board.

mousePressed()

This function is the bridge between raw pixel input (mouseX, mouseY) and the abstract grid coordinates your game logic understands - a pattern you'll reuse in almost any grid-based game or tile editor.

🔬 This math converts pixel coordinates into grid cell indices. What do you think would happen if you removed the '- padding' part? Try it and click near the top-left of the screen to see the misalignment.

  let mouseGridX = floor((mouseX - padding) / tileSize);
  let mouseGridY = floor((mouseY - padding) / tileSize);
function mousePressed() {
  if (shuffling || isSolved) {
    return; // Don't allow moves while shuffling or if already solved
  }

  // Calculate grid coordinates from mouse/touch position
  let mouseGridX = floor((mouseX - padding) / tileSize);
  let mouseGridY = floor((mouseY - padding) / tileSize);

  // Check if click is within the board boundaries
  if (mouseGridX >= 0 && mouseGridX < gridSize &&
      mouseGridY >= 0 && mouseGridY < gridSize) {
    moveTile(mouseGridX, mouseGridY); // Attempt to move the clicked tile
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Ignore Clicks While Busy if (shuffling || isSolved) { return; }

Prevents the player from moving tiles while the board is being shuffled or after it's already solved.

conditional Check Click Is On The Board if (mouseGridX >= 0 && mouseGridX < gridSize && mouseGridY >= 0 && mouseGridY < gridSize) {

Ignores clicks that land outside the board (like on the button or empty page area).

if (shuffling || isSolved) {
A guard clause: exits immediately if the game isn't in a state where moves should be accepted.
let mouseGridX = floor((mouseX - padding) / tileSize);
Subtracts the board's offset from the mouse's x position, then divides by tile size and rounds down to get which column was clicked.
let mouseGridY = floor((mouseY - padding) / tileSize);
Does the same calculation for the y position to find the clicked row.
if (mouseGridX >= 0 && mouseGridX < gridSize &&
Makes sure the calculated column is actually within the grid's bounds (not off to the side).
moveTile(mouseGridX, mouseGridY); // Attempt to move the clicked tile
Passes the clicked cell's coordinates to moveTile(), which decides if the move is legal.

moveTile()

This function encapsulates the core puzzle rule - you can only slide a tile into the empty space if it's directly adjacent. Using abs() to measure distance is a common trick for adjacency checks in grid-based logic.

🔬 This formula only allows sliding tiles that are directly beside the empty space. What do you think would happen to gameplay if you changed both '=== 1' checks to '<= 1', allowing diagonal-distance-1 moves too?

  let isAdjacent = (abs(x - emptyX) === 1 && y === emptyY) || // Horizontal adjacency
                   (abs(y - emptyY) === 1 && x === emptyX);   // Vertical adjacency
function moveTile(x, y) {
  // Check if the clicked tile (x, y) is adjacent to the empty space (emptyX, emptyY)
  let isAdjacent = (abs(x - emptyX) === 1 && y === emptyY) || // Horizontal adjacency
                   (abs(y - emptyY) === 1 && x === emptyX);   // Vertical adjacency

  if (isAdjacent) {
    swapTiles(x, y);
    moves++; // Increment move counter
    isSolved = checkSolved(); // Check if the puzzle is now solved
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Adjacency Test let isAdjacent = (abs(x - emptyX) === 1 && y === emptyY) || (abs(y - emptyY) === 1 && x === emptyX);

Determines if the clicked tile is directly next to the empty space horizontally or vertically (not diagonally).

let isAdjacent = (abs(x - emptyX) === 1 && y === emptyY) || // Horizontal adjacency
True if the clicked tile is exactly one column away from the empty space while on the same row.
(abs(y - emptyY) === 1 && x === emptyX); // Vertical adjacency
True if the clicked tile is exactly one row away from the empty space while on the same column - either condition being true makes the move legal.
if (isAdjacent) {
Only proceeds with the swap if the clicked tile is truly next to the gap.
swapTiles(x, y);
Performs the actual tile/gap swap.
moves++; // Increment move counter
Counts this as one player move for the on-screen counter.
isSolved = checkSolved(); // Check if the puzzle is now solved
Re-checks the whole board after every move to see if the player just won.

swapTiles()

This is the classic three-line 'swap using a temporary variable' pattern, used here to exchange a numbered tile with the empty slot in a 2D array - a technique you'll use constantly whenever you need to swap two values.

function swapTiles(x, y) {
  let temp = grid[y][x];
  grid[y][x] = grid[emptyY][emptyX];
  grid[emptyY][emptyX] = temp;

  emptyX = x; // Update empty space coordinates
  emptyY = y;
}
Line-by-line explanation (5 lines)
let temp = grid[y][x];
Temporarily stores the value of the tile being clicked, since it's about to be overwritten.
grid[y][x] = grid[emptyY][emptyX];
Moves the empty value (0) into the clicked tile's position.
grid[emptyY][emptyX] = temp;
Places the clicked tile's original number into where the empty space used to be, completing the swap.
emptyX = x; // Update empty space coordinates
Updates the tracked empty column to the position that was just clicked.
emptyY = y;
Updates the tracked empty row the same way.

checkSolved()

This function demonstrates an 'early return' pattern: rather than checking every cell and combining results at the end, it exits the moment it finds proof the puzzle is unsolved, which is both efficient and easy to read.

function checkSolved() {
  let num = 1;
  for (let i = 0; i < gridSize; i++) {
    for (let j = 0; j < gridSize; j++) {
      if (i === gridSize - 1 && j === gridSize - 1) {
        if (grid[i][j] !== 0) return false; // Empty space must be at the end
      } else {
        if (grid[i][j] !== num++) return false; // Tiles must be in sequential order
      }
    }
  }
  return true; // All tiles are in order
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Scan Grid For Order for (let i = 0; i < gridSize; i++) { for (let j = 0; j < gridSize; j++) {

Walks through every cell in reading order (left-to-right, top-to-bottom) to compare it against the expected solved value.

conditional Fail Fast On Mismatch if (grid[i][j] !== num++) return false; // Tiles must be in sequential order

Immediately stops checking and returns false the moment any tile is out of order, avoiding unnecessary work.

let num = 1;
Tracks the expected number for each cell as we scan through the grid, starting at 1.
if (i === gridSize - 1 && j === gridSize - 1) {
Special-cases the very last cell, which should hold the empty value rather than a number.
if (grid[i][j] !== 0) return false; // Empty space must be at the end
If the last cell isn't empty, the puzzle definitely isn't solved, so exit early.
if (grid[i][j] !== num++) return false; // Tiles must be in sequential order
Compares the current cell to the expected number; if it doesn't match, the puzzle isn't solved. The num++ both checks and advances the counter in one expression.
return true; // All tiles are in order
If every cell passed its check, the function reaches this line and confirms the puzzle is solved.

windowResized()

windowResized() is a special p5.js callback that fires automatically whenever the browser window changes size, letting you rebuild responsive layouts. This function duplicates setup()'s layout math to keep everything proportional.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  let minDim = min(windowWidth, windowHeight);
  padding = minDim * 0.05;
  boardSize = minDim - 2 * padding;
  tileSize = boardSize / gridSize;

  // Reposition the shuffle button
  shuffleButton.position(padding, padding + boardSize + 20);
}
Line-by-line explanation (6 lines)
resizeCanvas(windowWidth, windowHeight);
p5's built-in function to change the canvas dimensions to match the new window size.
let minDim = min(windowWidth, windowHeight);
Recomputes the smaller screen dimension after resizing, same as in setup().
padding = minDim * 0.05;
Recalculates padding proportionally so the layout stays consistent at any size.
boardSize = minDim - 2 * padding;
Recalculates the board's pixel size for the new window dimensions.
tileSize = boardSize / gridSize;
Recalculates each tile's size so the grid still fits perfectly in the new board size.
shuffleButton.position(padding, padding + boardSize + 20);
Moves the HTML button to stay positioned just below the board after resizing.

📦 Key Variables

grid array

A 2D array (array of arrays) representing the puzzle board; each cell holds a tile number or 0 for the empty space.

let grid;
emptyX number

The column index of the current empty space in the grid.

let emptyX;
emptyY number

The row index of the current empty space in the grid.

let emptyY;
tileSize number

The pixel width/height of a single tile, calculated from boardSize and gridSize.

let tileSize;
boardSize number

The total pixel width/height of the puzzle board, based on the smaller screen dimension.

let boardSize;
padding number

The empty margin (in pixels) around the board, used to position the board and UI elements.

let padding;
gridSize number

How many rows/columns the puzzle has (3 for a classic 8-puzzle, 4 for a harder 15-puzzle).

let gridSize = 3;
shuffling boolean

Tracks whether the board is currently being shuffled, used to lock player input and show a status message.

let shuffling = false;
moves number

Counts how many valid moves the player has made since the last shuffle.

let moves = 0;
isSolved boolean

Tracks whether the puzzle is currently in its solved (winning) arrangement.

let isSolved = false;
shuffleButton object

Reference to the HTML button element created with createButton(), used to reposition and style it.

let shuffleButton;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG shuffleGrid()

There is a very small but non-zero chance that after 500 random moves the board lands back in the solved order, yet isSolved is left as false and no win overlay appears until the player makes another move.

💡 After the shuffle loop, call `isSolved = checkSolved();` to correctly reflect the actual state of the board.

PERFORMANCE checkSolved()

checkSolved() re-scans the entire grid from scratch after every single move, which is unnecessary work for larger grid sizes.

💡 Since only two tiles change per move, you could track solved-ness incrementally, or simply accept the cost since gridSize is small (3-4) and the full scan is cheap.

STYLE drawInfo()

Layout uses several magic numbers (80, 110, 20) to position text and the button below the board, making the spacing hard to adjust consistently.

💡 Define named constants like `const INFO_OFFSET = 80;` and `const STATUS_OFFSET = 110;` so the vertical spacing can be tuned in one place.

FEATURE overall sketch

There's no timer, best-score tracking, or win animation/sound beyond a static overlay, so replaying the game feels a bit static.

💡 Add a millis()-based timer displayed alongside moves, store a best move-count in localStorage, and trigger a short particle burst or sound effect when checkSolved() first returns true.

🔄 Code Flow

Code flow showing setup, initializegrid, shufflegrid, makerandommove, draw, drawgrid, drawemptyspace, drawinfo, drawsolvedoverlay, mousepressed, movetile, swaptiles, checksolved, windowresized

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

graph TD start[Start] --> setup[setup] setup --> initializegrid[initializegrid] initializegrid --> grid-nested-loop[grid-nested-loop] grid-nested-loop --> empty-slot-check[empty-slot-check] setup --> shufflegrid[shufflegrid] shufflegrid --> shuffle-loop[shuffle-loop] shuffle-loop --> adjacency-checks[adjacency-checks] shufflegrid --> draw[draw loop] draw --> drawgrid[drawgrid] drawgrid --> tile-nested-loop[tile-nested-loop] tile-nested-loop --> skip-empty-tile[skip-empty-tile] drawgrid --> drawemptyspace[drawemptyspace] draw --> drawinfo[drawinfo] drawinfo --> status-message-branch[status-message-branch] draw --> drawsolvedoverlay[drawsolvedoverlay] drawsolvedoverlay --> solved-overlay-check[solved-overlay-check] draw --> mousepressed[mousepressed] mousepressed --> boundary-check[boundary-check] boundary-check --> guard-clause[guard-clause] guard-clause --> movetile[movetile] movetile --> adjacency-formula[adjacency-formula] movetile --> swaptiles[swaptiles] draw --> checksolved[checksolved] checksolved --> solved-scan-loop[solved-scan-loop] solved-scan-loop --> early-return[early-return] setup --> windowresized[windowresized] click setup href "#fn-setup" click initializegrid href "#fn-initializegrid" click shufflegrid href "#fn-shufflegrid" click draw href "#fn-draw" click drawgrid href "#fn-drawgrid" click drawemptyspace href "#fn-drawemptyspace" click drawinfo href "#fn-drawinfo" click drawsolvedoverlay href "#fn-drawsolvedoverlay" click mousepressed href "#fn-mousepressed" click movetile href "#fn-movetile" click swaptiles href "#fn-swaptiles" click checksolved href "#fn-checksolved" click windowresized href "#fn-windowresized" click grid-nested-loop href "#sub-grid-nested-loop" click empty-slot-check href "#sub-empty-slot-check" click shuffle-loop href "#sub-shuffle-loop" click adjacency-checks href "#sub-adjacency-checks" click tile-nested-loop href "#sub-tile-nested-loop" click skip-empty-tile href "#sub-skip-empty-tile" click status-message-branch href "#sub-status-message-branch" click guard-clause href "#sub-guard-clause" click boundary-check href "#sub-boundary-check" click adjacency-formula href "#sub-adjacency-formula" click solved-scan-loop href "#sub-solved-scan-loop" click early-return href "#sub-early-return"

❓ Frequently Asked Questions

What visual experience does the 'Numbers Shuffle' sketch provide?

The 'Numbers Shuffle' sketch visually presents a grid of numbered tiles that users can manipulate to arrange them in sequential order, creating a dynamic puzzle effect.

How can users engage with the 'Numbers Shuffle' interactive puzzle?

Users can interact with the puzzle by clicking the 'Shuffle' button to mix the tiles, and then they can slide the tiles around to attempt to solve the puzzle.

What creative coding concepts are showcased in the 'Numbers Shuffle' sketch?

This sketch demonstrates concepts such as grid-based layout, user input handling, and dynamic state management to create an interactive puzzle experience.

Preview

Numbers shuffle - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Numbers shuffle - Code flow showing setup, initializegrid, shufflegrid, makerandommove, draw, drawgrid, drawemptyspace, drawinfo, drawsolvedoverlay, mousepressed, movetile, swaptiles, checksolved, windowresized
Code Flow Diagram