Contents
Other Help Pages
Help

Overview

The Custom JavaScript constraints panel lets you define constraints using JavaScript. It has two tabs:

  • Pairwise — constrains relationships between pairs of cell values.
  • State machine — processes cell values sequentially through a finite-state machine.

To create a custom constraint:

  • Select cells on the grid by dragging or Shift+clicking.
  • Open the panel and write your constraint logic.
  • Optionally set a Name to label the constraint in the list.

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.

You can use 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.

Pairwise Constraints

The Check Function

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.

Chain Handling

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

State Machine Constraints

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.

Toggle Unified input to write startState, transition, and accept in a single text area instead of separate fields. The behavior is identical.

Start State

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 Function

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 Function

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.

Branching

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;
}

Max Depth

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.

Reducing the State Count

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.

The 4096 State Limit

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:

  • Ensure the state space is finite. If any of values in your state grow without bound (like a running total), the builder can't finish building the NFA.
  • Normalize states. Sort collections where order doesn't matter so that equivalent states produce the same JSON.
  • Carry only what you need. A boolean to flag that a threshold has been exceeded may be sufficient instead of a running total.
  • Reject invalid paths early. If you can determine that a state cannot lead to an accepted result, then immediately prune it.
  • Use 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.

Optimization

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.

  • Avoid over-pruning. Aggressively pruning can prevent states from being merged. If you are under the state limit, you usually don't need to prune since the builder can determine that automatically.
  • Rethink the structure of the problem. Just finding a different state representation is often not enough, since the builder can merge states that are logically equivalent. Explore different ways the problem can be solved. Sometimes even generalizing can help, as the overall structure maybe simpler.
  • Consider splitting into multiple constraints. If you are combining two independent pieces of logic, that can quadratically increase the state count. However, combining them can allow for more powerful propagation during solving and is easier to manage in the UI.
  • Take advantage of non-determinism. If your problem can be solved by branching into multiple paths, it can reduce the absolute number of states required. Branches are just as efficient in the solver.
  • maxDepth to can sometimes reduce the size, at the cost of making it less general.