numberlink.generatorΒΆ
Procedural level generator for the numberlink environment.
This module implements two procedural generators and a set of internal helpers used to create valid NumberLink puzzles.
The primary entry point is generate_level(). The function generate_level() dispatches to
_gen_hamiltonian_partition() or _gen_random_walk() depending on the provided configuration. All generator
functions return a numberlink.levels.Level representing the puzzle grid and optional bridge positions.
The implementation organizes helpers for path creation, rewiring, partitioning, validation, and rendering. See individual function docstrings for algorithmic notes and usage details.
Module ContentsΒΆ
FunctionsΒΆ
Generate a puzzle level using the configured generator settings. |
|
Deterministic generator that assembles a valid, fully filled puzzle. |
|
Return whether two coordinates are orthogonally adjacent. |
|
Return whether the supplied solution paths cover every cell in the grid. |
|
Precompute orthogonal neighbours for every cell in the grid. |
|
Generate a Hamiltonian path using randomized backtracking. |
|
Backtracking helper used by |
|
Randomize which letter is assigned to each solution path. |
|
Compute grid dimensions after applying a rotation. |
|
Transform a coordinate by a rotation of the grid. |
|
Randomly rotate and optionally mirror a generated level. |
|
Apply post processing steps that introduce additional visual variety. |
|
Construct a deterministic Hamiltonian path and partition it into segments. |
|
Create a Hamiltonian path that covers the entire grid using a serpentine sweep. |
|
Apply random 2-opt style loop reversals to introduce variety while preserving coverage. |
|
Identify the earliest valid segment start index for each position in the Hamiltonian path. |
|
Compute viable segment endpoints for dynamic partitioning of a Hamiltonian path. |
|
Recursively construct segment bounds for a Hamiltonian path partition. |
|
Split a Hamiltonian path into contiguous segments while avoiding adjacent endpoints. |
|
Compute a 2D numpy array that maps each grid cell to its segment index. |
|
Validate that every segment is a simple path and meets NumberLink constraints. |
|
Render segment endpoints onto the puzzle grid. |
|
Compute the shortest grid distance between two coordinates using the configured movement rules. |
|
Sample a start cell uniformly among cells not in |
|
Generate a puzzle by placing endpoints using per-color random walks. |
DataΒΆ
APIΒΆ
- numberlink.generator.generate_level(cfg: numberlink.config.GeneratorConfig, *, variant: numberlink.config.VariantConfig | None = None) numberlink.levels.Level[source]ΒΆ
Generate a puzzle level using the configured generator settings.
Dispatch to either
_gen_hamiltonian_partition()or_gen_random_walk()depending on the value ofcfg.mode. The returned value is anumberlink.levels.Levelinstance suitable for immediate use by the environment or saving to disk.- Parameters:
cfg (GeneratorConfig) β Generator configuration object. See
numberlink.config.GeneratorConfigfor options and defaults.variant (VariantConfig or None) β Variant configuration object. If
None, defaults toVariantConfig()with standard settings. Themust_fillandallow_diagonalsettings are taken from this parameter.
- Returns:
Generated puzzle level as a
numberlink.levels.Level.- Return type:
- Raises:
ValueError β If
cfg.modeis not one of'hamiltonian'or'random_walk'.
- numberlink.generator._gen_hamiltonian_partition(cfg: numberlink.config.GeneratorConfig, *, variant: numberlink.config.VariantConfig, allow_random_fallback: bool = True) numberlink.levels.Level[source]ΒΆ
Deterministic generator that assembles a valid, fully filled puzzle.
The generator builds a Hamiltonian path that covers every cell and partitions that path into segments that become colored paths for the puzzle. The high level steps are:
Build a Hamiltonian backbone using a serpentine sweep implemented by
_build_initial_path().Apply randomized 2 opt style rewiring to the backbone using
_apply_loop_moves()to introduce variety while preserving full coverage.Partition the Hamiltonian path into contiguous segments using
_partition_path_into_segments()so that each segment meets the minimum node length.Validate that the partitioned segments are simple paths and meet rules using
_validate_segments()and_compute_segment_map().Convert segment endpoints into a puzzle grid with
_segments_to_level()and return the resultingnumberlink.levels.Level.
- Parameters:
cfg (GeneratorConfig) β Generator configuration object. See
numberlink.config.GeneratorConfigfor details.- Returns:
Generated puzzle level as a
numberlink.levels.Level.- Return type:
- Raises:
ValueError β If the grid dimensions or color count are not positive, or if bridge placement is requested in the configuration. If the generator fails to produce a valid partition within a bounded number of attempts it falls back to
_gen_random_walk()and does not raise here.
- numberlink.generator._are_adjacent(a: Coord, b: Coord) bool[source]ΒΆ
Return whether two coordinates are orthogonally adjacent.
Adjacency is measured by the Manhattan distance and requires a distance of exactly
1.- Parameters:
a (Coord) β First coordinate as a
(row, column)pair.b (Coord) β Second coordinate as a
(row, column)pair.
- Returns:
Truewhen the coordinates are adjacent, otherwiseFalse.- Return type:
- numberlink.generator._paths_cover_grid(paths: list[list[Coord]], height: int, width: int) bool[source]ΒΆ
Return whether the supplied solution paths cover every cell in the grid.
- numberlink.generator._build_neighbor_map(height: int, width: int) dict[Coord, list[Coord]][source]ΒΆ
Precompute orthogonal neighbours for every cell in the grid.
- numberlink.generator._random_hamiltonian_path(height: int, width: int, rng: numpy.random.Generator, *, attempts: int = 128) list[Coord] | None[source]ΒΆ
Generate a Hamiltonian path using randomized backtracking.
The search performs Warnsdorff style tie breaking by preferring candidates with the fewest onward moves to improve success probability. When all attempts fail the function returns
Noneand callers may fall back to deterministic patterns such as_build_initial_path().- Parameters:
height (int) β Grid row count.
width (int) β Grid column count.
rng (numpy.random.Generator) β Random number generator used for stochastic decisions.
attempts (int, optional) β Number of random start attempts to try.
- Returns:
Hamiltonian path covering every cell as a list of coordinates, or
Noneif no path is found.- Return type:
list[Coord] or None
- numberlink.generator._hamiltonian_backtrack(path: list[Coord], visited: set[Coord], neighbours: dict[Coord, list[Coord]], total_cells: int, max_steps: int, steps: list[int], rng: numpy.random.Generator) bool[source]ΒΆ
Backtracking helper used by
_random_hamiltonian_path().The helper advances the recursive search and respects
max_stepsto avoid excessively long runs. It implements Warnsdorff style tie breaking by ordering candidate moves according to the number of onward moves.- Parameters:
path (list[Coord]) β Current path under construction.
visited (set[Coord]) β Set of visited coordinates.
neighbours (dict[Coord, list[Coord]]) β Mapping from a coordinate to its orthogonal neighbours.
total_cells (int) β Total number of cells to cover.
max_steps (int) β Maximum allowed search steps to limit runtime.
steps (list[int]) β Mutable single element list used as a counter for steps.
rng (numpy.random.Generator) β Random number generator used for tie breaking.
- Returns:
Truewhen a Hamiltonian path covering all cells is found.- Return type:
- numberlink.generator._shuffle_letter_assignments(level: numberlink.levels.Level, rng: numpy.random.Generator) numberlink.levels.Level[source]ΒΆ
Randomize which letter is assigned to each solution path.
The function returns a new
numberlink.levels.Levelwith grid characters remapped and with the solution paths reordered to match the new assignment. Whenlevel.solutionisNoneor contains fewer than two paths the inputlevelis returned unchanged.- Parameters:
level (Level) β Level to remap.
rng (numpy.random.Generator) β Random number generator used to shuffle assignments.
- Returns:
New
numberlink.levels.Levelwith shuffled letter assignments.- Return type:
- numberlink.generator._rotated_dims(height: int, width: int, rotation: int) Coord[source]ΒΆ
Compute grid dimensions after applying a rotation.
Determine the resulting
(height, width)pair when a rectangular grid with the suppliedheightandwidthis rotated byrotationsteps of 90 degrees clockwise. Rotation values are integers in the range0..3where each increment represents a 90 degree clockwise rotation. When the rotation is odd the dimensions are swapped.This helper is used by
_apply_random_orientation()to allocate the transformed grid matrix before mapping characters and coordinates. It does not validate the numeric range ofrotationand will return a swapped pair for odd values and the original pair for even values.- Parameters:
- Returns:
Tuple
(new_height, new_width)after rotation.- Return type:
See also
_transform_coord()for per-coordinate transformation under the same rotation convention.
- numberlink.generator._transform_coord(r: int, c: int, height: int, width: int, rotation: int) Coord[source]ΒΆ
Transform a coordinate by a rotation of the grid.
Map an input coordinate
(r, c)from the original grid to the corresponding coordinate in the grid rotated byrotationsteps of 90 degrees clockwise. Rotation values follow the same convention used by_rotated_dims()where0represents no rotation and1represents a 90 degree clockwise rotation.The mapping rules are:
rotation == 0:(r, c) -> (r, c)(identity)rotation == 1:(r, c) -> (c, height - 1 - r)rotation == 2:(r, c) -> (height - 1 - r, width - 1 - c)rotation == 3:(r, c) -> (width - 1 - c, r)
This function is used by
_apply_random_orientation()when copying grid characters and when transforming solution paths and bridge coordinates.- Parameters:
- Returns:
Transformed coordinate
(new_row, new_col)in the rotated grid.- Return type:
- Raises:
ValueError β When
rotationis not one of0,1,2, or3.
See also
_rotated_dims()for computing the dimensions of the rotated grid used when allocating the destination matrix.
- numberlink.generator._apply_random_orientation(level: numberlink.levels.Level, rng: numpy.random.Generator) numberlink.levels.Level[source]ΒΆ
Randomly rotate and optionally mirror a generated level.
Apply a random rotation and an independent horizontal flip when selected. The returned
numberlink.levels.Leveluses transformed coordinates for thenumberlink.levels.Level.solutionattribute and for thenumberlink.levels.Level.bridgesattribute when present.- Parameters:
level (
numberlink.levels.Level) β Level to transform.rng (numpy.random.Generator) β Random number generator used to select rotation and flip.
- Returns:
Transformed level with updated
grid,bridges, andsolution.- Return type:
- Raises:
ValueError β If an unsupported rotation value is passed to
_transform_coord().
- numberlink.generator._enhance_variability(level: numberlink.levels.Level, rng: numpy.random.Generator) numberlink.levels.Level[source]ΒΆ
Apply post processing steps that introduce additional visual variety.
Compose
_apply_random_orientation()and_shuffle_letter_assignments()to produce a visually varied puzzle instance. The returned object preserves the semantics ofnumberlink.levels.Level.- Parameters:
level (
numberlink.levels.Level) β Base level to vary.rng (numpy.random.Generator) β Random number generator used for stochastic steps.
- Returns:
Varied level with normalized types for attributes.
- Return type:
- numberlink.generator._build_serpentine_segments(height: int, width: int, n_colors: int, min_nodes: int, rng: numpy.random.Generator) list[list[Coord]] | None[source]ΒΆ
Construct a deterministic Hamiltonian path and partition it into segments.
Use
_build_initial_path()to create a serpentine Hamiltonian path and then partition it inton_colorscontiguous segments. Each segment meets themin_nodesrequirement when partitioning succeeds.- Parameters:
height (int) β Grid row count.
width (int) β Grid column count.
n_colors (int) β Number of segments to produce.
min_nodes (int) β Minimum allowed nodes in each segment.
rng (numpy.random.Generator) β Random number generator used to preserve call signature.
- Returns:
List of segments when partitioning succeeds or
Nonewhen it fails.- Return type:
- numberlink.generator._build_initial_path(height: int, width: int, rng: numpy.random.Generator) list[Coord][source]ΒΆ
Create a Hamiltonian path that covers the entire grid using a serpentine sweep.
Construct a full coverage path by sweeping rows or columns in alternating directions. The initial sweep axis and an optional reversal are selected using the provided random number generator.
- Parameters:
height (int) β Number of rows in the grid.
width (int) β Number of columns in the grid.
rng (numpy.random.Generator) β Numpy random number generator used to choose orientation and reversal. See
numpy.random.
- Returns:
List of coordinates that covers every cell exactly once.
- Return type:
list[Coord]
- Raises:
ValueError β If
heightorwidthis not positive.
- numberlink.generator._apply_loop_moves(path: list[Coord], rng: numpy.random.Generator, attempts: int = 128) None[source]ΒΆ
Apply random 2-opt style loop reversals to introduce variety while preserving coverage.
Modify
pathin place by performing loop reversals that preserve path contiguity. A reversal is applied only when the candidate move keeps the path contiguous according to_are_adjacent(). Increaseattemptsfor more mixing or reduce it for speed.- Parameters:
path (list[Coord]) β Hamiltonian path represented as a list of
(row, column)coordinates.rng (numpy.random.Generator) β Numpy random number generator used to select indices. See
numpy.random.attempts (int, optional) β Number of random move trials to perform. Defaults to
128.
- Returns:
None. The inputpathis modified in place.- Return type:
None
- numberlink.generator._compute_conflict_min_starts(path: list[Coord]) list[int][source]ΒΆ
Identify the earliest valid segment start index for each position in the Hamiltonian path.
Compute constraints that prevent partitioned segments from producing adjacent endpoints that belong to different segments. For each path index
ithe returned list contains the minimum allowed start index for a segment that ends ati.
- numberlink.generator._partition_choices(pos: int, segments_left: int, total_cells: int, min_len: int, min_start: list[int], rng: numpy.random.Generator, memo: dict[Coord, tuple[int, ...] | None]) tuple[int, ...] | None[source]ΒΆ
Compute viable segment endpoints for dynamic partitioning of a Hamiltonian path.
This recursive helper for
_partition_path_into_segments()uses memoization to avoid repeated work. It enforces the minimum segment length and the adjacency constraints produced by_compute_conflict_min_starts().- Parameters:
pos (int) β Current path index where a new segment would begin.
segments_left (int) β Number of segments remaining to place.
total_cells (int) β Total number of cells in the Hamiltonian path.
min_len (int) β Minimum allowed length for a segment.
min_start (list[int]) β Constraint list returned by
_compute_conflict_min_starts().rng (numpy.random.Generator) β Random number generator used to break ties when the caller needs a random choice.
memo (dict[Coord, tuple[int, ...] | None]) β Memoization mapping keyed by tuples
(pos, segments_left).
- Returns:
Tuple of viable end indices for a segment starting at
posorNonewhen no viable choices exist.- Return type:
- numberlink.generator._build_partition(pos: int, segments_left: int, total_cells: int, min_len: int, min_start: list[int], rng: numpy.random.Generator, memo: dict[Coord, tuple[int, ...] | None], segments_bounds: list[Coord]) bool[source]ΒΆ
Recursively construct segment bounds for a Hamiltonian path partition.
Attempt to place
segments_leftcontiguous segments starting atpos. For the current position query_partition_choices()to obtain viable end indices and choose one uniformly usingrng. Append chosen bounds tosegments_bounds. The recursion succeeds when the chosen bounds covertotal_cells.- Parameters:
pos (int) β Current start index for the next segment.
segments_left (int) β Number of segments remaining to place.
total_cells (int) β Total number of cells in the path.
min_len (int) β Minimum allowed length of each segment.
min_start (list[int]) β Adjacency constraint list returned by
_compute_conflict_min_starts().rng (numpy.random.Generator) β Random number generator used for uniform tie breaking.
memo (dict[Coord, tuple[int, ...] | None]) β Memoization mapping shared with
_partition_choices().segments_bounds (list[Coord]) β Output list that will receive chosen
(start, end)index tuples.
- Returns:
Trueif a complete partition coveringtotal_cellswas constructed, otherwiseFalse.- Return type:
- numberlink.generator._partition_path_into_segments(path: list[Coord], n_segments: int, min_len: int, rng: numpy.random.Generator) list[list[Coord]] | None[source]ΒΆ
Split a Hamiltonian path into contiguous segments while avoiding adjacent endpoints.
Attempt to partition
pathinton_segmentscontiguous subpaths. Each subpath has at leastmin_lennodes. The endpoints of different segments are not adjacent on the grid. The partitioning uses_partition_choices()and the adjacency constraints from_compute_conflict_min_starts().- Parameters:
path (list[Coord]) β Hamiltonian path that covers every grid cell.
n_segments (int) β Desired number of segments.
min_len (int) β Minimum allowed length of each segment.
rng (numpy.random.Generator) β Random number generator used to break ties when multiple partitions are possible.
- Returns:
List of segments when partitioning succeeds or
Nonewhen no valid partition exists.- Return type:
- numberlink.generator._compute_segment_map(segments: list[list[Coord]], height: int, width: int) numpy.typing.NDArray[numpy.int16][source]ΒΆ
Compute a 2D numpy array that maps each grid cell to its segment index.
The returned array uses
-1to indicate unassigned cells. RaiseValueErrorwhen a segment contains a coordinate that is outside the grid bounds or when two segments assign the same cell.- Parameters:
- Returns:
Numpy array with shape
(height, width)mapping each cell to its segment index.- Return type:
- Raises:
ValueError β If a segment cell is outside grid bounds or if segments overlap on the same cell.
- numberlink.generator._validate_segments(segments: list[list[Coord]], seg_map: numpy.typing.NDArray[numpy.int16], min_nodes: int) None[source]ΒΆ
Validate that every segment is a simple path and meets NumberLink constraints.
Perform the following checks. Each segment length is at least
max(3, min_nodes). Each segment is contiguous as defined by_are_adjacent(). Segment endpoints are not adjacent to each other. Usingseg_mapensure that each grid cell is assigned and that each segment has exactly two endpoints when measured on the grid.- Parameters:
segments (list[list[Coord]]) β Partitioned segments to validate.
seg_map (numpy.ndarray) β Grid mapping produced by
_compute_segment_map().min_nodes (int) β Minimum nodes a single segment must contain. Effective minimum is
max(3, min_nodes).
- Returns:
Nonewhen validation succeeds.- Return type:
None
- Raises:
ValueError β When any validation rule fails.
- numberlink.generator._segments_to_level(segments: list[list[Coord]], height: int, width: int) list[str][source]ΒΆ
Render segment endpoints onto the puzzle grid.
Assign an uppercase letter to each segment starting at
'A'. Place only the endpoints of each segment on the returned grid. Return a list of strings where each string represents a row and the dot character represents an empty cell.
- numberlink.generator._shortest_dist(a: Coord, b: Coord, allow_diagonal: bool = False) int[source]ΒΆ
Compute the shortest grid distance between two coordinates using the configured movement rules.
Return the Chebyshev distance when
allow_diagonalisTrue. Otherwise return the Manhattan distance.
- numberlink.generator._sample_start_cell(used_endpoints: set[Coord], h: int, w: int, rng: numpy.random.Generator) Coord[source]ΒΆ
Sample a start cell uniformly among cells not in
used_endpoints.- Parameters:
used_endpoints (set[Coord]) β Set of coordinates already used as endpoints.
h (int) β Grid row count.
w (int) β Grid column count.
rng (numpy.random.Generator) β Random number generator used for uniform sampling.
- Returns:
Sampled coordinate that is not in
used_endpoints.- Return type:
Coord
- numberlink.generator._gen_random_walk(cfg: numberlink.config.GeneratorConfig, *, variant: numberlink.config.VariantConfig) numberlink.levels.Level[source]ΒΆ
Generate a puzzle by placing endpoints using per-color random walks.
For each color the algorithm performs the following steps
Choose a random start cell that is not already used as an endpoint.
Perform a random walk that avoids cells occupied by previous walks.
End at a different cell when the walk satisfies the minimum length requirement.
Ensure the shortest path distance between endpoints meets the configured minimum.
Only endpoints are placed on the returned grid. Intermediate walk cells are marked as occupied to prevent other colors from using them. When
cfg.bridges_probabilityis greater than0.0the function may return a set of bridge coordinates in thenumberlink.levels.Levelresult.- Parameters:
cfg (
numberlink.config.GeneratorConfig) β Generator configuration object. Seenumberlink.config.GeneratorConfig.- Returns:
Generated puzzle level with endpoint letters and optional bridges.
- Return type:
- Raises:
ValueError β If grid dimensions or color count are invalid, if the configured minimum path length is too large for the board, or if the generator cannot place valid endpoints after the configured number of attempts.