Aggregated States

Grid-based games like Tic Tac Toe offer an ideal context to explore how a single grid—or aggregate layer—can represent the game state, with all moves recorded in one shared Quadrille using distinct player values like 'X' and 'O'.

Instead of separating game state per player, this approach simplifies move handling and turn logic by using a single grid. Win detection, however, requires managing player-specific pattern sets and exact value matching. Both strategies support dynamic sizes, diverse value types, and integrate smoothly with more advanced mechanics.

ℹ️

This chapter teaches:

  • How to use a single quadrille to manage all game state
  • How to define and reuse winning patterns for different players
  • How to use quadrille search for strict pattern matching
  • Basic turn logic using order, isEmpty, and fill

Aggregated Layer Tic Tac Toe

The sketch below creates a standard 3×3 Tic Tac Toe game using only one Quadrille. Each player alternates placing x or o on the board, and the game checks whether a win pattern has been matched.

code
let patterns1 = [];
let patterns2 = [];
let game;
let winner;
let resetButton;
const x = 'X', o = 'O';

function setup() {
  // Create canvas and UI
  createCanvas(300, 300);
  textAlign(CENTER, CENTER);
  resetButton = createButton('reset game');
  resetButton.position(10, height + 10);
  resetButton.mousePressed(resetGame);
  // Initialize board and win patterns
  resetGame();
  const diag1 = createQuadrille([
    [x],
    [null, x],
    [null, null, x]
  ]);
  const horiz1 = createQuadrille([x, x, x]);
  patterns1.push(diag1,
                 diag1.clone().reflect(),
                 horiz1,
                 horiz1.clone().transpose());
  const horiz2 = horiz1.clone().replace(x, o);
  const diag2 = diag1.clone().replace(x, o);
  patterns2.push(diag2,
                 diag2.clone().reflect(),
                 horiz2,
                 horiz2.clone().transpose());
}

function draw() {
  background('DarkSeaGreen');
  drawQuadrille(game);
  // Show winner message if game is over
  if (winner) {
    textSize(32);
    fill('yellow');
    text(`${winner} wins!`, width / 2, height / 2);
    noLoop();
  }
}

function mousePressed() {
  // Ignore clicks if game is over
  if (winner) return;
  const row = game.mouseRow;
  const col = game.mouseCol;
  // Place current marker if cell is valid and empty
  if (game.isValid(row, col) && game.isEmpty(row, col)) {
    const current = game.order % 2 === 0 ? x : o;
    game.fill(row, col, current);
    // Check for win or restart on draw
    winner = checkWinner();
    if (!winner && game.order === 9) resetGame();
  }
}

// Reset board and state, pause video
function resetGame() {
  game = createQuadrille(3, 3);
  winner = undefined;
  loop();
}

// Search for winning pattern in board
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';
}

Turn-Based Interaction

mousePressed
function mousePressed() {
  // Ignore clicks if game is over
  if (winner) return;
  const row = game.mouseRow;
  const col = game.mouseCol;
  // Place current marker if cell is valid and empty
  if (game.isValid(row, col) && game.isEmpty(row, col)) {
    const current = game.order % 2 === 0 ? x : o;
    game.fill(row, col, current);
    // Check for win or restart on draw
    winner = checkWinner();
    if (!winner && game.order === 9) resetGame();
  }
}

Each time the player clicks on a valid, empty cell:

The game alternates turns based on the parity of game order.

Defining and Detecting Win Patterns

To determine a winner, the game compares the current state of the board to a set of predefined pattern quadrilles. Each player has their own array of patterns representing the valid win conditions: horizontal, vertical, and diagonal lines of three.

Defining Patterns

The patterns are created once at the start using createQuadrille() for diagonals and rows, then transformed with .reflect() and .transpose() to cover all directions. Player 2’s patterns are cloned from Player 1’s using .replace(x, o).

setup (excerpt)
const diag1 = createQuadrille([
  [x],
  [null, x],
  [null, null, x]
]);
const horiz1 = createQuadrille([x, x, x]);
patterns1.push(diag1,
               diag1.clone().reflect(),
               horiz1,
               horiz1.clone().transpose());
const horiz2 = horiz1.clone().replace(x, o);
const diag2 = diag1.clone().replace(x, o);
patterns2.push(diag2,
               diag2.clone().reflect(),
               horiz2,
               horiz2.clone().transpose());

You can see all these patterns (patterns1 and patterns2) visualized below.

ℹ️

Detecting Wins

To detect a win, the game searches for a match between the current board and any of the stored patterns using search(pattern, true) and Array.prototype.some():

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';
}

This returns the winning player as soon as a match is found. The .some() method exits early for efficiency and reads cleanly.

ℹ️

💡 Without some, you’d need to use a for…of loop with early returns:

function checkWinner() {
  for (let p of patterns1) {
    if (game.search(p, true).length > 0) {
      return 'Player 1';
    }
  }
  for (let p of patterns2) {
    if (game.search(p, true).length > 0) {
      return 'Player 2';
    }
  }
  return undefined;
}

🧩 Try this:

  • Replace diagonal patterns with an “L” shape and update detection logic.
  • Introduce patterns that require gaps or alternating cells.

Working with Different Value Types

The aggregated layer strategy is not limited to strings like 'X' and 'O'. In fact, each cell in a Quadrille can store any valid JavaScript value, and the logic for win detection using search still applies as long as values match precisely.

This section shows how the exact same game logic can work with strings, numbers, or even images—demonstrating the flexibility and power of Quadrille’s unified data model.

Strings and Numbers

In this variant, Player 1 uses the string 'X', while Player 2 uses the number 125. Despite the difference in types, the pattern matching mechanism remains the same.

Winning patterns:

Images as Values

You can also use images as player symbols. When loaded and reused consistently, image references behave just like strings or numbers when passed to search and replace.

Winning patterns:

ℹ️
🖼️ For image-based values to match, the exact image object must be used across both game state and win patterns. Avoid reloading or duplicating image instances.

🧩 Try this:

  • Use emojis ('🍎', '🥦') as symbols.
  • Try using objects with .toString() or .display methods, preparing for cell objects in later chapters.
  • Explore mismatched value types to see how search behaves.

3×3 Pattern Search in Larger Boards

This variant keeps the same 3×3 winning patterns as before but plays the game on an n×n board, where n is randomly chosen between 4 and 9. The logic and pattern definitions remain unchanged—thanks to Quadrille’s ability to search for patterns anywhere within a larger board.

This highlights the local nature of pattern matching: the patterns themselves do not need to scale with the board size. Instead, search(pattern, true) scans the entire board for a matching subgrid of the same size as the pattern.

ℹ️
💡 Win detection is position-independent. As long as the board contains a region that matches the 3×3 pattern, search will find it—even on a larger grid.

New logic for supporting variable board size

Only two small additions are needed to extend the board dynamically:

sketch excerpt
let n;

function resetGame() {
  n = int(random(4, 10)); // Choose board size at random
  Quadrille.cellLength = width / n; // Scale cells to fit canvas
  game = createQuadrille(n, n);
  // ...
}

function mousePressed() {
  // ...
  if (!winner && game.order === n * n) resetGame();
}

The variable n determines the size of the grid, and the game automatically resets after n × n moves if no winner is found. The cell size is adjusted to fit the canvas dynamically using Quadrille.cellLength.

🧩 Try this:

  • The higher n is, the harder it becomes to align three in a row—players have more space to spread out.
  • Mix dynamic board sizes with alternative 3×3 patterns for varied gameplay challenges.

References

Further Reading