Algebra
Algebra on grids is about combining shapes with simple rules to form new grids. Inspired by Boolean algebra and constructive solid geometry, this chapter shows how to merge Quadrilles by overlaying their cells according to a chosen operation—such as union (OR), intersection (AND), or difference (NOT). Such merging is a core mechanic in many tile-matching puzzle games—from classic Tetris to modern block-combining titles—where shapes interact on the same board to form new patterns or meet specific goals.
Merging two non-overlapping Quadrilles of different dimensions requires defining:
- The area spanned by the inputs—the smallest rectangle that contains both
q1
andq2
. This rectangle becomes the resulting grid on which the merge takes place. In the sketch below, this span is highlighted inmagenta
.
(to move theq2
quadrille drag mouse or pressa
,s
,w
,z
keys) - The cell operator — a function that determines the value of each cell within the spanned area. For example,
Quadrille.or
fills a cell if either input contains a value at that position. When both inputs have non-null values in the same cell, the value from the first operand takes precedence.
(to move theq2
quadrille drag mouse or pressa
,s
,w
,z
keys)
This chapter teaches
- How area span and cell operator together define a merge
- Boolean operators on grids:
or
(union),and
(intersection),not
(difference),xor
(symmetric difference) - How to sketch a tile-matching puzzle game that builds on these primitives
Tile-Matching Demo: Fill the Board
A quick demonstration of merging logic in action. Randomly shaped pieces appear under your cursor as a live preview—rotate them to fit and click to stick them to the grid. The goal: completely fill the board without overlapping existing cells. The HUD tracks how many cells remain and how many pieces you’ve attempted.
(move mouse to position the preview; click to place; r
: rotate piece, n
: skip / new piece)
code
Quadrille.cellLength = 20;
const cols = 20;
const rows = 20;
let grid; // Main board
let piece; // Active piece to be placed
let tries = 0;
function setup() {
createCanvas(cols * Quadrille.cellLength, rows * Quadrille.cellLength);
grid = createQuadrille(cols, rows); // Empty grid
newPiece(); // Generate initial piece
}
function draw() {
background(0);
drawQuadrille(grid, { outlineWeight: 0.5 }); // Draw current grid
// Show remaining unfilled cells in top-left corner
const remaining = grid.size - grid.order;
fill(255);
noStroke();
textSize(16);
text(`missed filled cells: ${remaining} · tries: ${tries}`, 10, 20);
const row = grid.mouseRow;
const col = grid.mouseCol;
// Offset preview to center the piece under the mouse
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
drawQuadrille(piece, { row: offsetRow, col: offsetCol }); // Preview piece under cursor
}
function mousePressed() {
// Clone the piece and replace its values with a fixed color (for consistency)
const placed = piece.clone();
placed.replace(color(160));
// AND: Check if placement overlaps any filled cells in the grid
const blocked = Quadrille.and(grid, placed);
// OR: Merge piece onto the grid at the current mouse position
const row = grid.mouseRow;
const col = grid.mouseCol;
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
const merged = Quadrille.or(grid, placed, offsetRow, offsetCol);
// Valid placement:
// - No overlap (blocked.order === 0)
// - Merge result still fits the grid dimensions
if (blocked.order === 0 && merged.size === grid.size) {
newPiece(); // Generate a new random piece
grid = merged; // Update grid with placed piece
}
}
function keyPressed() {
key === 'r' && piece.rotate(); // Rotate piece clockwise
key === 'n' && newPiece(); // Manually request a new piece
}
function newPiece() {
const w = int(random(2, 5));
const h = int(random(2, 5));
const value = color(random(255), random(255), random(255), 160);
// Minimum order = w + h ensures a reasonable number of filled cells
// and promotes the chance that each row and column is represented.
// This does not guarantee full coverage, but increases structural density.
const order = w + h;
// Create a new random quadrille-shaped piece with given density
piece = createQuadrille(w, h, order, value);
tries++; // Count every new piece
}
Game Mechanics
Gameplay combines mouse and keyboard controls.
Mouse Click — Placing a piece
Clicking the mouse tries to place the current piece on the board.
- Overlap check —
blocked = Quadrille.and(grid, placed)
returns the intersection between the board and the piece. Ifblocked.order === 0
, there is no overlap. - Fit check —
merged = Quadrille.or(grid, placed, offsetRow, offsetCol)
returns the union of the board and the piece at the given offset. Ifmerged.size === grid.size
, the piece fits entirely inside the board.
Only if both checks pass is the piece added to the board and newPiece()
is called to generate the next one.
mousePressed
function mousePressed() {
const placed = piece.clone().replace(color(160)); // Fixed color for placed pieces
const blocked = Quadrille.and(grid, placed); // Overlap check (AND)
const row = grid.mouseRow;
const col = grid.mouseCol;
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
const merged = Quadrille.or(grid, placed, offsetRow, offsetCol); // Union (OR)
if (blocked.order === 0 && merged.size === grid.size) {
grid = merged;
newPiece();
}
}
Keyboard — Rotating or skipping a piece
r
key — Rotates the current piece clockwise.n
key — Skips the current piece and calls thenewPiece()
function to generate another.
keyPressed
function keyPressed() {
key === 'r' && piece.rotate(); // Rotate piece clockwise
key === 'n' && newPiece(); // Skip to a new random piece
}
Generating New Pieces
The newPiece()
function creates a random rectangle (w × h
), assigns it a random semi-transparent color, sets a density (order = w + h
), and builds the piece with createQuadrille(w, h, order, value)
.
newPiece
function newPiece() {
const w = int(random(2, 5));
const h = int(random(2, 5));
const value = color(random(255), random(255), random(255), 160);
const order = w + h; // Minimum filled cells for balanced density
piece = createQuadrille(w, h, order, value);
tries++;
}
🧩 Try this
- Tweak the
newPiece()
function to generate only connected polyominoes (e.g., regenerate until all filled cells are 4-connected). - Scale difficulty by setting the polyomino degree (number of filled cells) as a game option.
- Implement a bag system — hold one piece and swap it in with
h
. - Add a simple end condition (e.g., stop when
remaining <= threshold
) and display a score based on bothtries
andremaining
. - Implement a tile-matching puzzle game (hint: see Further Reading for inspiration).
Counting Missed Cells
Basic algebra on counts: unfilled = total − filled. We render that as a simple HUD.
code
const remaining = grid.size - grid.order;
text(`missed filled cells: ${remaining} · tries: ${tries}`, 10, 20);
References
- Boolean algebra — Set-like reasoning on truth values; union/intersection maps neatly to
or
/and
. - Constructive solid geometry — Build complex shapes from simple ones with boolean ops.
Further Reading
- Tetris — The archetypal sticking game.
- Tetris is Hard, Even to Approximate — Demaine, Hohenberger & Liben-Nowell (2002). Proves the offline version of Tetris is NP-complete to optimize, and even hard to approximate for objectives such as maximizing cleared rows.
- Puyo Puyo — A match-four falling-block puzzle game focused on chain reactions.
- Columns — A match-three gem game played in vertical columns.
- Dr. Mario — Clear viruses by matching colored capsule halves.
- Lumines — Combines block matching with rhythm-based timing.
- Panel de Pon / Puzzle League — Swap tiles to match colors in a grid, often chaining combos.
- Super Puzzle Fighter II Turbo — Match colored gems and use crash gems to clear them.
- Magical Drop — Grab and release colored balls to form lines.
- Meteos — Match blocks to launch them upward off the playfield.
- Bejeweled — Match-three gameplay with endless possible board states.