Algebra
Algebra on grids is about merging 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), difference (NOT), or symmetric difference (XOR). These operators are the backbone of many tile-matching puzzle games, from Tetris to modern block-combining titles, where local merges produce global patterns and 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 build a tile-matching puzzle on top of these primitives
Block-Merge Puzzle: Fill the Board
A quick demo of merging in action: random pieces preview under your cursor; press r to rotate, ←/→ to shift horizontally, and click to stick them to the grid. The goal is to fill the board without overlapping filled cells. The HUD shows remaining empty cells and your attempt count—rotations and shifts don’t increase tries.
(move mouse to place; click to stick; r: rotate; n: new/skip; ↑/↓/←/→: shift)
code
Quadrille.cellLength = 20;
const cols = 20;
const rows = 20;
let game; // Main board
let piece; // Active piece to be placed
let tries = 0;
function setup() {
createCanvas(cols * Quadrille.cellLength, rows * Quadrille.cellLength);
game = createQuadrille(cols, rows); // Empty grid
newPiece(); // Generate initial piece
}
function draw() {
background(0);
drawQuadrille(game, { outlineWeight: 0.5 });
// Show remaining unfilled cells in top-left corner
const remaining = game.size - game.order;
fill(255);
noStroke();
textSize(16);
text(`missed filled cells: ${remaining} · tries: ${tries}`, 10, 20);
const row = game.mouseRow;
const col = game.mouseCol;
// Offset preview to center the piece under the mouse
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
// Preview piece under cursor
drawQuadrille(piece, { row: offsetRow, col: offsetCol });
}
function mousePressed() {
// Clone the piece and replace its values with a fixed color
const placed = piece.clone();
placed.replace(color(160));
// AND: Check if placement overlaps any filled cells in the grid
const blocked = Quadrille.and(game, placed);
// OR: Merge piece onto the grid at the current mouse position
const row = game.mouseRow;
const col = game.mouseCol;
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
const merged = Quadrille.or(game, placed, offsetRow, offsetCol);
// Valid placement:
// - No overlap (blocked.order === 0)
// - Merge result still fits the grid dimensions
if (blocked.order === 0 && merged.size === game.size) {
newPiece(); // Generate a new random piece
game = merged; // Update grid with placed piece
}
}
function keyPressed() {
key === 'r' && piece.rotate(); // Rotate piece clockwise
key === 'ArrowLeft' && piece.shift(0, -1); // Shift piece cells left
key === 'ArrowRight' && piece.shift(0, 1); // Shift piece cells right
key === 'ArrowUp' && piece.shift(-1, 0); // Shift piece cells up
key === 'ArrowDown' && piece.shift(1, 0); // Shift piece cells down
key === 'n' && newPiece(); // Request a new piece
return false;
}
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 Placement
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() {
// Clone the piece and replace its values with a fixed color
const placed = piece.clone();
placed.replace(color(160));
// AND: Check if placement overlaps any filled cells in the grid
const blocked = Quadrille.and(game, placed);
// OR: Merge piece onto the grid at the current mouse position
const row = game.mouseRow;
const col = game.mouseCol;
const offsetRow = row - int(piece.height / 2);
const offsetCol = col - int(piece.width / 2);
const merged = Quadrille.or(game, placed, offsetRow, offsetCol);
// Valid placement:
// - No overlap (blocked.order === 0)
// - Merge result still fits the grid dimensions
if (blocked.order === 0 && merged.size === game.size) {
newPiece(); // Generate a new random piece
game = merged; // Update grid with placed piece
}
}
Keyboard controls
r
key — Rotates the current piece clockwise.ArrowRight
key — Shifts the piece right.ArrowLeft
key — Shifts the piece left.ArrowUp
key — Shifts the piece up.ArrowDown
key — Shifts the piece down.n
key — Skips the current piece and generates a new one.
keyPressed
function keyPressed() {
key === 'r' && piece.rotate(); // Rotate piece clockwise
key === 'ArrowLeft' && piece.shift(0, -1); // Shift piece cells left
key === 'ArrowRight' && piece.shift(0, 1); // Shift piece cells right
key === 'ArrowUp' && piece.shift(-1, 0); // Shift piece cells up
key === 'ArrowDown' && piece.shift(1, 0); // Shift piece cells down
key === 'n' && newPiece(); // Request a new piece
return false;
}
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
- Use NOT to carve holes from a solid shape.
- Show a ghost preview by coloring the AND result (overlap) in red under the cursor so invalid placements stand out before clicking.
- 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.
HUD (excerpt)
const remaining = game.size - game.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 — Building complex shapes from simple ones via boolean ops.
- Tetris — The archetypal sticking game.
Further Reading
- Tetris complexity — Demaine, E. D., Hohenberger, S., & Liben-Nowell, D. (2003). Tetris is Hard, Even to Approximate. NP-hardness and approximation barriers for offline Tetris.
- Match-three hardness — Walsh, T. (2014). Candy Crush is NP-hard. General NP-hardness of match-three puzzles.
- Polyominoes — Golomb, S. W. (1994). Polyominoes: Puzzles, Patterns, Problems, and Packings. Princeton University Press. Classic combinatorics and tilings reference.
- Polygon/shape boolean ops — Greiner, G., & Hormann, K. (1998). Efficient Clipping of Arbitrary Polygons. ACM Transactions on Graphics. Influential algorithm for 2D shape set operations.
- Exact cover & tilings — Knuth, D. E. (2000). Dancing Links. Algorithm X; widely applied to packing and tiling problems.
Classic puzzle games
- Puyo Puyo — Match-four falling blocks with chain reactions.
- Columns — A match-three gem game played in vertical columns.
- Dr. Mario — Clear viruses by matching capsule halves.
- Lumines — Block matching blended with rhythm-based timing.
- Panel de Pon / Puzzle League — Swap tiles to form matches and chain combos.
- Super Puzzle Fighter II Turbo — Match colored gems, using crash gems to clear them.
- Magical Drop — Grab and release colored balls to make lines.
- Meteos — Match blocks to launch them upward off the field.
- Bejeweled — Iconic match-three with endless possible board states.