File a bug
Source code
The Custom JavaScript constraints panel lets you define constraints using JavaScript. It has two tabs:
To create a custom constraint:
The JavaScript you write is only executed when you add the constraint, not during loading or solving. The efficiency of your code does not affect solver performance.
console.log inside your functions to print
values to the browser console for debugging.
For worked examples, see the Recipes section on the main help page.
Write a function that takes two cell values a and
b and returns true if they form a valid
pair.
constraintCheck = (a, b) => a <= b
The function is called for each pair of cells determined by the
Chain handling setting. If the function returns
false (or any falsy value) for any pair, the constraint
is violated.
When more than two cells are selected, Chain handling determines which pairs of cells are checked.
| Mode | Pairs checked for cells [A, B, C] |
|---|---|
| Consecutive pairs | [A,B] and [B,C] |
| All pairs | [A,B], [A,C], [B,A], [B,C], [C,A], [C,B] |
Consecutive pairs checks each cell against the next cell in the selection. This is useful for ordered line constraints. For example, a slow thermometer where each value must be greater than or equal to the previous:
constraintCheck = (a, b) => a <= b
All pairs checks every ordered pair of cells in both directions. This is useful when the constraint applies between any two cells regardless of position. For example, requiring that no two digits on a line are consecutive:
constraintCheck = (a, b) => Math.abs(a - b) > 1
A state machine constraint processes the selected cells one at a time, in selection order, by maintaining a state that is updated at each step. After all cells are processed, the final state determines whether the constraint is satisfied.
startState, transition, and
accept in a single text area instead of separate fields.
The behavior is identical.
startState is the initial state before any cell values
are processed. It can be any JSON-serializable value — a number,
a string, an object, or null.
// A numeric accumulator.
startState = 0;
// null - e.g. as a sentinel for "first cell not yet seen".
startState = null;
// An object to track multiple values.
startState = { sum: 0, count: 0 };
States must be JSON-serializable (no functions, no circular
references).
Mutating a state object inside transition
or accept has no side effects — the solver creates
a fresh copy for each call.
To start from multiple initial states at once, set
startState to an array. Each element becomes an
independent starting path:
startState = ["even", "odd"];
transition(state, value) takes the current state and the
next cell's value. It returns the next state.
startState = 0;
function transition(state, value) {
return state + value;
}
If a transition is invalid (the constraint cannot be satisfied from
this state with this value), return undefined (or return
nothing) to signal that this path is dead. The solver will prune it.
// Only allow the running total to stay under 20.
function transition(state, value) {
const next = state + value;
if (next < 20) return next;
// Returning nothing kills this path.
}
NUM_CELLS is available as a variable containing the
number of selected cells.
When the state is an object, you can destructure it directly in the function parameters. For example, tracking the running minimum and maximum:
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;
}
accept(state) is called on the final state after all
cell values have been processed. Return true if the
state represents a valid outcome.
function accept(state) {
return state === 10;
}
A state machine is satisfied when at least one path through all the cells ends in an accepted state.
transition can return an array of states
instead of a single state. Each element becomes an independent path
that is explored separately. The constraint is satisfied if
any of these paths reaches an accepted final state.
Returning an empty array [] is equivalent to returning
undefined — the path is dead.
For example, to require that the cells sum to either 20 or 30, branch on the first value into two paths that each track a remaining target:
startState = null;
function transition(state, value) {
// On the first cell, branch into two targets.
if (state === null) {
return [20 - value, 30 - value];
}
// On subsequent cells, subtract from the remaining target.
return state - value;
}
function accept(state) {
return state === 0;
}
The optional maxDepth parameter limits how many cells
deep the solver will expand the state machine. States beyond this
depth are not created. It defaults to Infinity (no
limit).
maxDepth = 5;
This is useful when the state machine only needs to inspect the first
few cells of a line, or when the number of reachable states grows
too large. If the solver hits its internal state limit, consider
setting maxDepth to a lower value.
When you add a state machine constraint, ISS builds an internal
NFA (Nondeterministic Finite Automaton) by running your
transition function on every reachable state with every
possible cell value. Each distinct result becomes a state in
the NFA. Two return values map to the same state if they produce
identical JSON when serialized — object key order does not
matter, but {values: [1, 2]} and
{values: [2, 1]} are different states.
There is a hard limit of 4096 states. The builder will return an error if your state machine exceeds this limit. This happens when you create your constraint, not during solving. If you encounter this error:
maxDepth to bound
expansion depth. Generally maxDepth must be at least
the number of cells that the constraint applies to. Any less, and
you may get incorrect results.
Even under the limit, fewer states often means faster solving. ISS automatically removes dead-end states and merges logically equivalent states, so it is not always obvious how changes to your code will affect the final state count.
maxDepth to can sometimes reduce the
size, at the cost of making it less general.