Layered Boards

Grid-based games like Tic Tac Toe can also be implemented using multiple layers, where each player interacts with their own Quadrille. This strategy cleanly separates game state per player, improving clarity and modularity—especially in multiplayer, strongly typed, or bitboard-based setups.

Instead of placing both players’ moves on a single grid, each player fills their own layer, overlaid during display. Win detection is simpler and more general: shared structural patterns are matched independently on each layer, with no need for player-specific values. This approach also supports dynamic sizes, varied value types, and scales naturally to more complex rules.

ℹ️

This chapter teaches:

  • How to use multiple Quadrille layers for per-player state
  • How to detect wins by comparing each layer against shared patterns
  • How to generate win patterns declaratively using predicate functions
  • How to work with dynamic board sizes using createQuadrille(n, n, predicate, value)

Layered 3×3 Tic Tac Toe

The sketch below implements standard 3×3 Tic Tac Toe using two separate Quadrilles—player1 and player2—and overlays them on a single canvas. Players alternate turns, and each move is recorded in the corresponding layer. The game checks whether any of the shared win patterns are matched in either layer.

code
let patterns = [];
let n;
let player1;
let player2;
let resetButton;
let winner;
Quadrille.cellLength = 100;
let x, o;

async function setup() {
  // Load image and video assets
  x = await loadImage('images/v.jpg');
  o = await createVideo('clips/c2.webm');
  o.volume(0);      // mute video
  o.hide();         // hide default video element
  // Create canvas and UI
  createCanvas(3 * Quadrille.cellLength, 3 * Quadrille.cellLength);
  textAlign(CENTER, CENTER);
  resetButton = createButton('reset game');
  resetButton.position(10, height + 10);
  resetButton.mousePressed(resetGame);
  // Disable right-click context menu
  document.oncontextmenu = () => false;
  resetGame();
}

function draw() {
  background(0);
  drawQuadrille(player1);
  drawQuadrille(player2);
  // Display winner message if game is over
  if (winner) {
    textSize(32);
    fill('white');
    text(`${winner} wins!`, width / 2, height / 2);
    noLoop();
  }
}

function mousePressed() {
  // Ignore clicks if game is over
  if (winner) return;
  // Determine current and opponent players
  const current = player1.order <= player2.order ? player1 : player2;
  const opponent = current === player1 ? player2 : player1;
  // Determine clicked cell coordinates
  const row = current.mouseRow;
  const col = current.mouseCol;
  // Check bounds and ensure both layers are empty
  if (current.isValid(row, col) && current.isEmpty(row, col) && opponent.isEmpty(row, col)) {
    current.fill(row, col, current === player1 ? x : o);
    current === player2 && o.loop(); // ensure video starts when first used
    // Check for win or restart on draw
    winner = checkWinner();
    if (!winner && player1.order + player2.order === n * n) resetGame();
  }
}

// Initialize both player layers, reset state and patterns
function resetGame() {
  o.pause();
  n = int(random(4, 10));
  Quadrille.cellLength = width / n;
  player1 = createQuadrille(n, n);
  player2 = createQuadrille(n, n);
  winner = undefined;
  patterns = [];
  const qDiag1 = createQuadrille(n, n, ({ row, col }) => row === col, '⨂');
  const qDiag2 = qDiag1.clone().reflect();
  const qRow = createQuadrille(n, 1, ({ row }) => row === 0, '⨂');
  const qCol = qRow.clone().transpose();
  patterns.push(qDiag1, qDiag2, qRow, qCol);
  loop();
}

// Check win patterns for each layer
function checkWinner() {
  return patterns.some(p => player1.search(p, false).length > 0) && 'Player 1' ||
    patterns.some(p => player2.search(p, false).length > 0) && 'Player 2';
}
ℹ️

🎬 Media-aware game logic
This version demonstrates how to manage media elements in a grid context. The createVideo asset for Player 2 is:

  • muted (o.volume(0))
  • hidden from the default DOM (o.hide())
  • paused initially in resetGame()
  • looped only when the player makes a valid move

This pattern ensures that video playback is tightly controlled and only activated during gameplay.

🧩 Try this:

Compare and contrast this layered implementation with the one in Aggregated States:

  • What are the main differences in game logic and rendering?
  • How do the strategies affect win detection, value types, and state separation? → Run both examples and write a short note highlighting benefits and tradeoffs of each approach.

Turn-Based Interaction

mousePressed
function mousePressed() {
  // Ignore clicks if game is over
  if (winner) return;
  // Determine current and opponent players
  const current = player1.order <= player2.order ? player1 : player2;
  const opponent = current === player1 ? player2 : player1;
  // Determine clicked cell coordinates
  const row = current.mouseRow;
  const col = current.mouseCol;
  // Check bounds and ensure both layers are empty
  if (current.isValid(row, col) && current.isEmpty(row, col) && opponent.isEmpty(row, col)) {
    current.fill(row, col, current === player1 ? x : o);
    current === player2 && o.loop(); // ensure video starts when first used
    // Check for win or restart on draw
    winner = checkWinner();
    if (!winner && player1.order + player2.order === 9) resetGame();
  }
}

Key ideas:

  • Each player fills only their layer.
  • A move is valid if both layers are empty at the selected cell.
  • Turn order is derived from the sum of orders.
  • After each move, the game checks if that player’s grid matches a win pattern.

🧩 Try this:

  • Implement fog of war: draw only the active player’s layer to hide the opponent’s moves.
  • Add replay support: store and navigate previous states to enable undo or full timeline playback.
  • Enable simultaneous turns: let both players submit their moves before revealing them.

Win Pattern Definition

Patterns are shared between players. Only one set is needed, since we check each pattern against each layer independently. They are defined using a base Quadrille and symmetry operations:

setup (excerpt)
const diag = createQuadrille([
  ['⨂'],
  [null, '⨂'],
  [null, null, '⨂']
]);
const horiz = createQuadrille(['⨂', '⨂', '⨂']);
patterns.push(diag, diag.clone().reflect(), horiz, horiz.clone().transpose());

You can see all these patterns visualized below.

Detecting a Winner

After each move, we use search(pattern, false) on both player layers:

checkWinner
function checkWinner() {
  return patterns.some(p => player1.search(p, false).length > 0) && 'Player 1' ||
         patterns.some(p => player2.search(p, false).length > 0) && 'Player 2';
}

The false flag in search disables value matching: only non-null cell positions are compared. Any player using consistent non-null values (like images or emojis) will match the patterns.

Pattern Generation with Predicates

A major benefit of Quadrille’s functional design is support for predicates—functions that describe which cells to fill based on their row, col coordinates. This enables clear, rule-based definitions of win conditions, ideal for larger or dynamic boards, and follows a declarative programming style where you describe what to fill rather than how to do it.

The sketch below shows a connect-n version of the game, where n is randomly chosen at each reset. All win patterns are defined declaratively using simple predicates, such as rows, diagonals, or custom shapes.

Inside resetGame():

resetGame (excerpt)
function resetGame() {
  o.pause();
  n = int(random(4, 10));
  Quadrille.cellLength = width / n;
  player1 = createQuadrille(n, n);
  player2 = createQuadrille(n, n);
  winner = undefined;
  patterns = [];
  const qDiag1 = createQuadrille(n, n, ({ row, col }) => row === col, '⨂');
  const qDiag2 = qDiag1.clone().reflect();
  const qRow = createQuadrille(n, 1, ({ row }) => row === 0, '⨂');
  const qCol = qRow.clone().transpose();
  patterns.push(qDiag1, qDiag2, qRow, qCol);
  loop();
}

These patterns are visualized below, updating live as n changes via the slider:

What are Predicates?

A predicate is a function that returns true or false for each { row, col } cell. It determines whether to assign a value at that location:

({ row }) => row === 0        // Top row
({ row, col }) => row === col // Main diagonal

This approach replaces manual grid input with concise, reusable logic.

🧩 Try this:

  • Define qCol directly using a predicate, instead of transposing qRow.
  • Create an anti-diagonal pattern with ({ row, col }) => row + col === n - 1.
  • Create a T-shaped or L-shaped pattern using compound predicate logic.
  • Create other variants, such as:
    • A “connect-l” version of the game, where 3 ≤ l ≤ n, and patterns must match exactly l consecutive cells.
    • A gravity-based version where pieces fall to the lowest empty cell in a column.
    • Asymmetric win conditions: assign different patterns or connect-lengths to each player.
    • A highlight mode that visually marks the matching pattern when a player wins.
    • A block-based version where players place compound shapes (like tetrominoes) instead of single cells.

References

  • Tic Tac Toe — Classic example for exploring grid logic and symmetry-based pattern matching.
  • Predicate creation in Quadrille — Generate flexible, rule-based patterns like lines, diagonals, or compound shapes.
  • Bitboard-based setups — Efficient representations for grid states, often used for variable-length matches and AI logic.

Further Reading

  • Declarative programming — Programming paradigm focused on describing what to compute, rather than how, especially useful for rule-based logic and pattern generation.
  • Pattern matching (computer science) — Background on searching structured data, including visual and symbolic patterns.
  • Symmetry operations on grids — Useful for reflecting and rotating patterns like L or T shapes.
  • Connect Four — A classic gravity-based, connect-l game, useful for implementing the falling mechanics variant.
  • Game balance — Introduces asymmetry in games and how different win conditions per player can still result in fair play.
  • Encapsulation and abstraction — Highlights the benefits of separating state logic across layers.