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.
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:
mouseRow
andmouseCol
determine the cell clicked.isValid
andisEmpty
confirm that it’s within bounds and unoccupied.fill(row, col, value)
updates the cell based on the current turn, inferred usinggame
order
.
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.
- Use
reflect()
andtranspose()
to generate all pattern orientations from minimal code. - Use
replace()
to convert one player’s pattern to the other’s. - For larger boards, patterns can be generated programmatically using predicate functions.
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:
🧩 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.
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
Tic Tac Toe — Classic grid game used throughout this chapter to illustrate win patterns and search logic.
Tic Tac Toe Challenge — The Coding Train — Beginner-friendly p5.js tutorial showing win detection using arrays and conditionals.
StackOverflow: Grid Pattern Matching — Discussion on matching subgrids based on structure and value configuration.
Further Reading
A Complexity Dichotomy for Permutation Pattern Matching on Grid Classes (2020) — Explores the complexity of pattern detection across various grid classes.
Rabin–Karp Algorithm — Classic string search algorithm with conceptual ties to 2D pattern matching.