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:

  1. The area spanned by the inputs—the smallest rectangle that contains both q1 and q2. This rectangle becomes the resulting grid on which the merge takes place. In the sketch below, this span is highlighted in magenta.
    (to move the q2 quadrille drag mouse or press a, s, w, z keys)
  2. 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 the q2 quadrille drag mouse or press a, s, w, z keys)
ℹ️

This chapter teaches

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.

  1. Overlap checkblocked = Quadrille.and(grid, placed) returns the intersection between the board and the piece. If blocked.order === 0, there is no overlap.
  2. Fit checkmerged = Quadrille.or(grid, placed, offsetRow, offsetCol) returns the union of the board and the piece at the given offset. If merged.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 the newPiece() 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 both tries and remaining.
  • 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

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.