Aggregated States
Grid-based games like Tic Tac Toe highlight how a quadrille can serve as a general model for game state. Here, all moves are recorded in a single Quadrille—an aggregate layer for the entire game—where the players are distinguished only by their values, x (='X')
and o (='O')
.
This approach keeps turn logic straightforward: one grid, one shared history. The trade-off is that win detection must manage separate pattern sets for each player and match them exactly. Later chapters (e.g. Layered Boards) will explore alternatives where each player has their own quadrille.
Aggregated Layer Tic Tac Toe
The classic 3×3 Tic Tac Toe is a perfect testbed for the aggregate approach. All moves are stored in a single Quadrille, with x (='X')
and o (='O')
alternating turns. Each click fills a cell if it is empty, the game checks for a winning pattern, and the board resets automatically on a draw.
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; resume draw loop
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
Player turns alternate automatically based on the parity of game.order
. On each click, the code checks whether the chosen cell is valid and empty. If so, it places x
or o
, updates the board, and tests for a win. If the board fills without a winner, the game resets for a new round.
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();
}
}
Key points:
mouseRow
andmouseCol
give the coordinates of the clicked cell.isValid
ensures the click is inside the board.isEmpty
ensures the cell isn’t already taken.order
counts how many moves have been made so far — its parity determines the current player.fill(row, col, value)
places the chosen marker.
Try this
- Change the starting player.
- Add a key to skip a turn by incrementing
game.order
.
Defining and Detecting Win Patterns
In a single, shared quadrille, player identity comes from values alone. We build two sets of patterns, one using x (='X')
and the other using o (='O')
, and detect a win by matching the board against the appropriate set.
Defining Patterns
From a diagonal and a horizontal seed, all win shapes follow through symmetry operations. Player-2 inherits the same shapes, obtained by value substitution.
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 orientations from the two base seeds. - Use
replace()
to adapt Player-1’s patterns to Player-2. - Patterns for larger boards can be expressed declaratively with predicate functions.
Detecting Wins
Win detection reduces to testing patterns with search(pattern, true)
, which enforces value equality on a shared quadrille.
The check is wrapped in Array.prototype.some()
, ensuring a winner is reported immediately on the first match.
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';
}
💡 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
- Define winning patterns in the shape of an L with three cells.
- Experiment with patterns that include gaps (e.g.,
x·x
).
Working with Different Value Types
Quadrille cells act as typed containers: they can store any valid JavaScript value, from primitives (strings, numbers, booleans) to complex objects (images, functions). In an aggregated layer, win detection with search
remains valid as long as values match exactly, regardless of their type.
This means the same game logic can be applied with strings, numbers, images, or custom objects—showcasing the generality of Quadrille’s data model.
Strings and Numbers
In this variant, Player 1 uses the string 'X'
, while Player 2 uses the number 125
. Even though the types differ, win detection works identically.
Winning patterns:
Images as Values
Player symbols need not be text or numbers—they can also be images. When loaded once and reused consistently, image references behave exactly like other values when passed to search
or replace
.
Winning patterns:
Reloading or creating duplicate instances will break equality checks.
Try this
- Replace
'X'
and'O'
with images (e.g., icons or small sprites) as player symbols. - Use booleans (
true
andfalse
) instead of strings, and confirm thatsearch
still detects wins correctly.
3×3 Pattern Search in Larger Boards
The same 3×3 winning patterns can be reused on an n×n board, where n
is chosen at random between 4 and 9. Neither the logic nor the pattern definitions need to change—Quadrille’s search
scans for the 3×3 subgrid anywhere on the larger board.
This illustrates the local nature of pattern matching: patterns remain fixed in size, while the board can grow arbitrarily.
The call search(pattern, true)
checks every possible subgrid and succeeds if any region matches the pattern exactly.
search
will find it—even when the board is larger.Logic for variable board size
Only two changes are required to support a dynamic board:
- Resize the cells so the board always fits within the canvas.
- Reset the game after all
n × n
cells have been filled.
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
controls both the grid dimensions and the draw condition. If no winner is found after n × n
moves, the board resets automatically. Cell size is recomputed dynamically from Quadrille.cellLength
so the grid always fits.
Try this
- Increase
n
to make the game harder: as the board grows, three-in-a-row becomes less likely. - Keep the 3×3 patterns fixed but let
n
vary—observe how pattern search adapts without changes in definition.
References
- Tic Tac Toe — Classic grid game used here to illustrate win patterns and search logic.
- Tic Tac Toe Challenge — The Coding Train — Beginner-friendly p5.js tutorial demonstrating win detection with arrays and conditionals.
Further Reading
- A Complexity Dichotomy for Permutation Pattern Matching on Grid Classes (2020) — Research paper on the computational complexity of detecting patterns across grid classes.
- Rabin–Karp Algorithm — Classic string-search method whose hashing ideas extend conceptually to 2D grids.
- Knuth, D. E. (1998). The Art of Computer Programming, Volume 3: Sorting and Searching. Addison-Wesley. — Standard reference on search algorithms, with foundations relevant to grid and subgrid pattern detection.
- Gusfield, D. (1997). Algorithms on Strings, Trees and Sequences. Cambridge University Press. — Comprehensive treatment of string and pattern matching, with ideas generalizable to multidimensional cases.
- Schaeffer, J. (2002). The Games Computers (and People) Play. In Advances in Computers (Vol. 55). Academic Press. — Survey of algorithms and strategies for solving simple games such as Tic Tac Toe, Connect Four, and others.
- Allis, L. V. (1994). Searching for Solutions in Games and Artificial Intelligence. PhD thesis, University of Limburg. — Foundational work on exhaustive search and pattern-based reasoning in small board games.