Timelines

Grid-based games like Tic Tac Toe can be extended with a timeline of board states, enabling undo, redo, and replay features. After every move, the board is cloned with .clone() and stored in a timeline array. This creates a sequence of game states that can be navigated step-by-step or restarted entirely.

This example uses an aggregate layer: a single shared grid holds the current game state, while the timeline wraps that grid in a history of snapshots. The same concept also applies to layered setups: simply maintain a separate timeline per player layer. Either way, timelines represent how the state changes—not where it’s stored.

ℹ️

This chapter teaches:

  • How to clone and store grid states to build a timeline
  • How to render the current layer using a time index
  • How to implement replay, undo, and temporal navigation
  • The difference between aggregate-based and layer-based timelines


code
let patterns1 = [];
let patterns2 = [];
let game;
let winner;
let resetButton;
let undoButton;
let redoButton;
let timeline = [];
let timelineIndex = 0;
const x = 'X', o = 'O';

function setup() {
  // Create canvas and buttons
  createCanvas(300, 300);
  textAlign(CENTER, CENTER);
  resetButton = createButton('reset game');
  resetButton.position(10, height + 10);
  resetButton.mousePressed(resetGame);
  undoButton = createButton('undo');
  undoButton.position(100, height + 10);
  undoButton.mousePressed(undoMove);
  redoButton = createButton('redo');
  redoButton.position(160, height + 10);
  redoButton.mousePressed(redoMove);
  // Initialize board and win patterns
  resetGame();
  // Create win patterns for Player 1 (x)
  const diag1 = createQuadrille([
    [x],
    [null, x],
    [null, null, x]
  ]); // createQuadrille builds a grid from a jagged array
  const horiz1 = createQuadrille([x, x, x]);
  patterns1.push(
    diag1,
    diag1.clone().reflect(),
    horiz1,
    horiz1.clone().transpose()
  );
  // Create win patterns for Player 2 (o) by replacing x with o
  const horiz2 = horiz1.clone().replace(x, o); // replace modifies matching values
  const diag2 = diag1.clone().replace(x, o);
  patterns2.push(
    diag2,
    diag2.clone().reflect(),
    horiz2,
    horiz2.clone().transpose()
  );
}

function draw() {
  background('DarkSeaGreen');
  drawQuadrille(timeline[timelineIndex], { origin: CORNER });
  // Display winner message if game is over
  if (winner) {
    undoButton.elt.disabled = true;
    redoButton.elt.disabled = true;
    textSize(32);
    fill('black');
    text(`${winner} wins!`, width / 2, height / 2);
    noLoop();
  }
  // Enable or disable undo/redo buttons based on timeline position
  undoButton.elt.disabled = timelineIndex === 0;
  redoButton.elt.disabled = timelineIndex === timeline.length - 1;
}

function mousePressed() {
  // Skip input if game is over
  if (winner) return;
  const row = game.mouseRow;
  const col = game.mouseCol;
  // Place marker if cell is valid and empty
  if (game.isValid(row, col) && game.isEmpty(row, col)) {
    const current = game.order % 2 === 0 ? x : o;
    // Truncate timeline if branching from a past state
    timeline.length = timelineIndex + 1;
    game.fill(row, col, current);              // fill writes to the board
    timeline.push(game.clone());               // clone stores a snapshot of the current state
    timelineIndex++;
    // Check for win or reset if board is full
    winner = checkWinner();
    if (!winner && game.order === 9) resetGame();
    undoButton.elt.disabled = false;
    redoButton.elt.disabled = true;
  }
}

// Start new game and timeline
function resetGame() {
  game = createQuadrille(3, 3);      // createQuadrille initializes empty grid
  timeline = [game.clone()];         // clone captures initial state
  timelineIndex = 0;
  winner = undefined;
  undoButton.elt.disabled = false;
  redoButton.elt.disabled = true;
  loop();
}

// Step backward in timeline
function undoMove() {
  if (timelineIndex > 0 && !winner) {
    timelineIndex--;
    game = timeline[timelineIndex].clone(); // restore previous state
    loop();
  }
}

// Step forward in timeline
function redoMove() {
  if (timelineIndex < timeline.length - 1 && !winner) {
    timelineIndex++;
    game = timeline[timelineIndex].clone(); // restore next state
    loop();
  }
}

// Check for win condition using search
function checkWinner() {
  return patterns1.some(p => game.search(p, true).length > 0) && 'Player 1' ||
         patterns2.some(p => game.search(p, true).length > 0) && 'Player 2';
  // search returns matching locations of a pattern in the board
}

Timeline-Based Interaction

Each move updates the current grid and appends a clone to the timeline. The player can press undo to step back or redo to move forward. The game disables further input once a winner is detected.

mousePressed
function mousePressed() {
  if (winner) return;
  const row = game.mouseRow;
  const col = game.mouseCol;
  if (game.isValid(row, col) && game.isEmpty(row, col)) {
    const current = game.order % 2 === 0 ? x : o;
    timeline.length = timelineIndex + 1; // truncate timeline
    game.fill(row, col, current);        // place move
    timeline.push(game.clone());         // save snapshot
    timelineIndex++;
    winner = checkWinner();              // check win
    if (!winner && game.order === 9) resetGame(); // restart on draw
  }
}

Undo and Redo

Undo and redo functions simply step through the timeline and restore the corresponding board state using .clone().

undo and redo
function undoMove() {
  if (timelineIndex > 0 && !winner) {
    timelineIndex--;
    game = timeline[timelineIndex].clone();
  }
}

function redoMove() {
  if (timelineIndex < timeline.length - 1 && !winner) {
    timelineIndex++;
    game = timeline[timelineIndex].clone();
  }
}

Undo is disabled at the beginning of the timeline. Redo is disabled at the most recent state. Once a player wins, both buttons are locked, and no further navigation is allowed.

Initializing the Timeline

The timeline is initialized with an empty board. At every reset, this initial state is cloned and stored as the first snapshot.

resetGame
function resetGame() {
  game = createQuadrille(3, 3);
  timeline = [game.clone()];
  timelineIndex = 0;
  winner = undefined;
  loop();
}

This ensures that the timeline always begins with a blank board and grows with each move.

Pattern Matching and Game Over

As in the aggregate strategy, win detection uses predefined patterns for each player and matches them using search.

checkWinner
function checkWinner() {
  return patterns1.some(p => game.search(p, true).length > 0) && 'Player 1' ||
         patterns2.some(p => game.search(p, true).length > 0) && 'Player 2';
}

If a player wins, the result is displayed, the game loop stops, and timeline navigation becomes read-only.

Displaying the Current State

The current grid displayed is always timeline[timelineIndex]. When the user moves back or forward in time, this value is updated and drawn each frame.

function draw() {
  background('DarkSeaGreen');
  drawQuadrille(timeline[timelineIndex], { origin: CORNER });
  if (winner) {
    textSize(32);
    fill('black');
    text(`${winner} wins!`, width / 2, height / 2);
    noLoop();
  }
}

🧩 Try This

🧩 Try this:

  • Implement a timeline for a different game, such as Snake, Connect Four, or a puzzle game.
  • Add a replay button that steps through the timeline automatically.
  • Show a mini-map of all timeline states using drawQuadrille at smaller sizes.
  • Add a “branch” mechanic: after undoing a move, place a new one to fork into an alternate future.

Further Reading

  • Time-travel debugging (Wikipedia) — An overview of reverse or “time‑travel” debugging, how tools let you step backward through execution and inspect program states (Wikipedia).

  • “McFly: Time‑Travel Debugging for the Web” (academic paper) — Presents a practical system for time‑travel debugging web apps, keeping JavaScript and visual state synchronized during forward/backward execution (arXiv).

  • Red Blob Games blog — While not about timelines per se, Amit Patel’s interactive tutorials demonstrate state-driven diagrams and snapshots in educational web graphics. Great for inspiration on visualizing state changes over time (redblobgames.com).

  • General Board Game Concepts (arXiv) — An academic look at how game states and transitions are formalized in systems like Ludii, with implications for tracking game-state histories and branching logic (arXiv).

  • Bitboard representation in board games (Wikipedia) — Describes a compact state encoding used in games like Chess and Connect Four that supports fast state checks—a useful idea for efficient history and pattern detection (Wikipedia).