Contents
Other Help Pages
Help Custom Constraints

Recipes

Some constraint types aren't directly supported but can be constructed using combinations of available constraints. Many of these use the Custom JavaScript constraints panel — see the Custom Constraints help page for a full guide.

Clone

Description: Two regions of the same shape and size, which must have the same values in corresponding cells.

Use the Same Value Sets on every pair of corresponding cells to mark them as equal. Each pair must be a separate constraint.

Magic Square

Description: A square of cells, where the sum of the values in each row, column, and diagonal is the same.

Use a single Equal Sum constraint, with each row, column, and diagonal of the square as a segment.

Thermometer Variants

Description: A thermometer is a line where values are strictly increasing. Some variants modify this basic rule.

Most Thermometer variants can be created with pairwise constraints in the "Custom JavaScript constraints" panel, with an appropriate condition.

For an Odd/Even Thermometer, where the values must be all odd or all even, use the following condition:

a < b && (a % 2 == b % 2)

For a Slow Thermometer, where the values don't need to be strictly increasing, use the following condition:

a <= b

Nabner Line

Description: No two digits along the line may be be consecutive. ("Nabner" is "Renban" spelled backwards.)

Create a pairwise constraint in the "Custom JavaScript constraints" panel with "Chain handling" set to "All pairs" and use the following condition:

Math.abs(a - b) > 1

Not Renban

Description: Unlike Nabner, this is how to create a set where all the values taken together are not consecutive.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. The NFA tracks the minimum and maximum values seen, accepting if max - min != count - 1 (i.e., the values are not all consecutive). If you know all the values are distinct, then this is sufficient:

NUM_CELLS = 3;  // Number of cells in the set
startState = { min: 16, max: -1 };

function transition({ min, max }, value) {
  return {
    min: Math.min(min, value),
    max: Math.max(max, value),
  };
}

function accept({ min, max }) {
  return max - min !== NUM_CELLS - 1;
}

If the values may not be distinct, then you need to also need to check for duplicates. This can be done efficiently by using non-deterministic branches to check each digit:

NUM_CELLS = 3;  // Number of cells in the set
startState = { type: 'start' };

function transition({ type, min, max, value: trackedValue }, value) {
  if (type === 'start') {
    return [
      { type: 'rangeCheck', min: value, max: value },
      { type: 'duplicateCheck', value },
    ];
  }
  if (type === 'rangeCheck') {
    return [{
      type: 'rangeCheck',
      min: Math.min(min, value),
      max: Math.max(max, value),
    }];
  }
  if (type === 'duplicateCheck') {
    if (value === trackedValue) {
      return [{ type: 'hasDuplicate' }];
    }
    return [
      { type: 'duplicateCheck', value: trackedValue },
      { type: 'duplicateCheck', value }
    ];
  }
  if (type === 'hasDuplicate') return [{ type: 'hasDuplicate' }];
}

function accept({ type, min, max }) {
  if (type === 'rangeCheck') {
    return max - min !== NUM_CELLS - 1;
  }
  return type === 'hasDuplicate';
}

You can generalize this to a single state machine that works for any number of cells, but it becomes a larger and less efficient.

Arithmetic Progression

Description: Digits along the line must form an arithmetic progression. That is, the difference between all consecutive digits is same.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel:

startState = { lastVal: null, diff: null };

function transition({ lastVal, diff }, value) {
  if (lastVal === null) {
    return { lastVal: value, diff };
  }

  const nextDiff = value - lastVal;

  if (diff === null || diff === nextDiff) {
    return { lastVal: value, diff: nextDiff };
  }
}

function accept(state) {
  return true;
}

Successor Arrow

Description: If a digit N is placed in an arrow cell, the digit N + 1 must appear exactly N cells away in the arrow's direction.

Select the arrow cells and all the cells along the arrow's direction, then add a Regex constraint from "Line and Sets" panel. The regex pattern should alternate over each possible starting digit:

(12|2.3|3.{2}4|4.{3}5|5.{4}6|6.{5}7|7.{6}8|8.{7}9).*

Flatmates

Description: Generalize the "Dutch Flatmates" constraint to apply to an arbitrary center digit D, which must have either digit A above it or digit B below it (or both).

Use a Regex constraint from the "Line and Sets" panel, applied to every column (replace A, B and D):

.*(AD|DB).*

Note: This assumes that D must exist in the line. This might not be the case in non-standard grid. In that case, then you can modify the regex as follows:

[^D]*(AD|DB)?[^D]*

Odd/Even Lots

Description: A digit in a marked cell indicates the number of Odd/Even digits which appear along the attached line, including itself.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. Select the cells in the line so that the marked cell is first, the order of the rest don't matter. The following code implements Odd Lots, but change to the isEven function for Even Lots:

startState = { remaining: null };

function transition({ remaining }, value) {
  const isOdd = x => x % 2 === 1;
  const isEven = x => x % 2 === 0;

  // If we are starting, then initialize the count of remaining values.
  if (remaining === null) remaining = value;

  // Update remaining count if we see a matching value.
  if (isOdd(value)) remaining = remaining - 1;

  // Only return a new state if it is valid.
  if (remaining >= 0) return { remaining };
}

function accept({ remaining }) {
  return remaining === 0;
}

Sum Variants

Description: A set of cells where the sum must satisfy a custom condition. Use this when you need sum constraints beyond what the built-in Cage supports — such as parity, ranges, or multiple possible totals.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. Select the cells in the set. The transition function accumulates the sum, and the accept function defines your condition:

startState = 0;

function transition(state, value) {
  // Limit avoids infinite states (max sum in a 9-cell region)
  // Increase if you need to support larger regions
  if (state <= 45) return state + value;
}

function accept(state) {
  // Customize this condition (see examples below)
  return state < 10;
}

Examples of accept conditions:

// Sum within a range
return state >= 10 && state <= 20;

// One of several possible sums
return [15, 20, 25].includes(state);

// Even sum
return state % 2 === 0;

// Odd sum
return state % 2 === 1;

This allows duplicate values. If you need all values to be distinct, overlay an All Different constraint from the "Lines & Sets" panel on the same cells.

If you want the values to be restricted as well, then you can filter out unwanted values in the transition function.

In some cases, it can be easier to count down from a target sum, especially if the target sum needs to be determined. In this case, start with the remainingSum as null to indicate that it is not yet known. For example, to simulate a Sandwich constraint where the first cell is the clue cell:

startState = { remainingSum: null, inside: false };

function transition({ remainingSum, inside }, value) {
  if (remainingSum === null) {
    return { remainingSum: value, inside };
  }
  if (value === 1 || value === 9) {
    return { remainingSum, inside: !inside };
  }
  if (inside) {
    remainingSum -= value;
  }
  if (remainingSum >= 0) {
    return { remainingSum, inside };
  }
}

function accept({ remainingSum }) {
  return remainingSum === 0;
}

Likewise for XSum:

startState = { remainingCount: null, remainingSum: null };

function transition({ remainingCount, remainingSum, done }, value) {
  if (done === true) return { done };
  if (remainingSum === null) {
    return { remainingCount, remainingSum: value };
  }
  remainingSum -= value;
  if (remainingCount === null) {
    remainingCount = value;
  }
  remainingCount--;
  if (remainingCount === 0 && remainingSum === 0) {
    return { done: true };
  }
  if (remainingCount > 0 && remainingSum > 0) {
    return { remainingCount, remainingSum };
  }
}

function accept({ done }) {
  return done === true;
}

General Skyscraper

Description: Generalize the skyscraper constraint to apply along any line of cells (for example, a diagonal). From the start of the line, there is a target indicate how many "buildings" are visible, where taller buildings block the view of shorter ones behind them.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. Change TARGET to set the target number of visible buildings:

TARGET = 5;  // Change to target number of visible buildings
startState = { max: 0, remaining: TARGET };

function transition({ max, remaining }, value) {
  return {
    max: Math.max(max, value),
    remaining: remaining - (value > max),
  }
}

function accept({ remaining }) {
  return remaining === 0;
}

If the first cell is the clue cell, remaining can be set to null and initialized on the first transition.

Ambiguous Arrow

Description: The greatest cell value must equal the sum of the remaining cells. When applied to a 2x2 square, this is also known as the Quad Sum constraint.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. This checks that the total sum is twice the maximum value:

startState = { max: 0, sum: 0 };

function transition({ max, sum }, value) {
  if (sum > 18) return;

  return {
    max: Math.max(max, value),
    sum: sum + value
  }
}

function accept({ max, sum }) {
  return sum === 2*max;
}

Generalized Entropic Line

Description: An entropic line requires that every consecutive group of N cells (where N is the number of sets) contains one digit from each set. The standard entropic line uses low (1–3), mid (4–6), and high (7–9), but this recipe generalizes it to any custom partitioning of the digits.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. Customize the sets variable to define your own partitioning:

sets = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

startState = { window: [] };

function transition({ window }, value) {
  const index = sets.findIndex(s => s.includes(value));
  if (index < 0) return;
  const nextWindow = [...window, index].slice(-sets.length);
  if (new Set(nextWindow).size < nextWindow.length) return;
  return { window: nextWindow };
}

function accept(state) {
  return true;
}

Index Line

Description: On an index line, the digit in the Nth cell indicates the position along the line where the digit N appears. For example, 3 2 1 4 is valid: the 1st digit is 3, so digit 1 appears at position 3; and the 3rd digit is 1, so digit 3 appears at position 1.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. The state tracks the current position and a futures map of unsatisfied digits. if the current position already has an expectation, it is checked and cleared. If the current digit refers to a future position, that expectation is recorded. The line is valid when all expectations have been satisfied:

startState = { position: 0, futures: {} };

function transition({ position, futures }, value) {
  position++;

  if (value < position || futures[position] !== undefined) {
    if (futures[position] !== value) return;
    delete futures[position];
  }
  if (value > position) {
    if (futures[value] !== undefined) return;
    futures[value] = position;
  }

  return { position, futures };
}

function accept({ futures }) {
  return Object.keys(futures).length === 0;
}

Magnet Sum

Description: In a magnet clue cell, let the clue value be M. Moving in the indicated direction, sum digits until the first digit greater than M is encountered. The sum of those seen digits must equal M.

Implement this by configuring a state machine in the "Custom JavaScript constraints" panel. Select the clue cell first, followed by all the cells in the indicated direction:

startState = { first: null, sum: 0, done: false };

function transition({ first, sum, done }, value) {
  if (done) {
    return { first, sum, done };
  }
  if (first === null) {
    return { first: value, sum, done };
  }
  if (value > first) {
    return sum !== first ? [] :
      { first: 0, sum: 0, done: true };
  }
  sum += value;
  return sum > first ? [] : { first, sum, done };
}

function accept({ first, sum }) {
  return first === sum;
}