Skip to content

Board Generation

Board generation is the process of filling a board with objectives from your goal set. It balances randomness with constraints to produce fair, varied, and interesting boards every time.

At a high level, generation works like this:

  1. Prepare the pool: all enabled objectives are gathered and filtered by weighting

  2. Place forced objectives: any objectives with designated positions are placed first

  3. Fill remaining spots: open positions are filled one at a time, respecting all active constraints

  4. Apply modifiers: post-generation effects like shinies and blanks are applied

If placement gets stuck, the system has multiple recovery strategies (from backtracking to constraint relaxation) so a board is almost always produced.


Every board is generated from a seed. The same seed with the same goal set and settings will always produce the exact same board. Generation is fully deterministic, so boards are reproducible and shareable. Two players with the same seed and goal set will always get identical boards.


When generation starts, all non-disabled objectives from the goal set are loaded into a single pool. Each objective appears in the pool exactly once, regardless of its individual limit.

Each objective can have a weighting value between 1 and 100 (defaulting to 100). This controls how likely the objective is to make it into the generation pool for any given board.

  • A weighting of 100 means the objective is always included
  • A weighting of 50 means roughly a 50% chance of being included
  • A weighting of 1 means roughly a 1% chance, the lowest possible without disabling the objective entirely

Weighting is rolled once at the start of generation. Either an objective makes it into the pool or it doesn’t. Once in the pool, all objectives have an equal chance of being selected.

If weighting excludes too many objectives and the pool becomes too small for the board size, excluded objectives are pulled back in to fill the gap. A warning is issued when this happens.


Individual limits control how many times a specific objective can appear on a single board.

Most objectives have a limit of 1, meaning they can only appear once. Some objectives have higher limits (2, 3, etc.), allowing them to appear multiple times. After each placement, the limit decreases by 1. When it hits 0, the objective is removed from the pool entirely.

Ranges represent target amounts within goal text (like “Collect {{X}} gold” where X could be 100, 500, or 1000). They’re always positive quantities that the player is working toward. See Using Ranges for guidance on what makes a good range.

The objective is chosen first, and then a value is picked from its available ranges. If the same objective appears multiple times (via a higher individual limit), a different range value is used each time.

Tags are unique identifiers that enforce mutual exclusivity. Only one objective with any given tag can appear on a board. This is useful for preventing objectives that would conflict or overlap with each other.

Objectives can specify a list of board positions where they must be placed. These are 1-indexed positions on the board (position 1 is the first cell).

An objective with forced positions will only ever appear in one of its designated spots. If none of its positions are available or constraints prevent placement, the objective is skipped entirely and won’t appear in a non-designated spot as a fallback.

Forced objectives are always placed before regular objectives in the generation process.


Categories are the primary tool for controlling board composition and balance. There are two types, and they solve different problems.

Board Categories

Scope: The entire board · Purpose: Control overall composition

Board categories set limits on how much of the total board can be occupied by objectives of a given type. They’re defined as percentages and converted to absolute counts based on board size.

For example, a board category limit of 25% on a 5×5 board (25 cells) means a maximum of 7 objectives of that type (rounded up from 6.25).

Line Categories

Scope: Individual lines (rows, columns, diagonals) · Purpose: Prevent clustering

Line categories set limits on how many objectives of a given type can appear in any single line. They work the same way as board categories but are checked independently for every row and column (plus diagonals on square grids).

Additionally, the same objective can never appear twice in the same line, even if its individual limit allows multiple copies on the board. This prevents a single row or column from containing the same goal twice with different range values.

Board and line categories solve fundamentally different problems, even when they use the same category names.

You could limit the board to 25% combat goals, but without line limits you could end up with all of them clustered in the same row, making one line trivially easy and others devoid of combat.


Progression controls the difficulty distribution across the board, ensuring that objectives are placed in appropriate positions relative to where a player is likely to encounter them during gameplay.

Every objective can be tagged with one or more progression zones:

ZoneRangeTypical Position
Early0–40%Bottom of the board / start of the list
Mid20–60%Lower-middle area
Late40–80%Upper-middle area
Endgame60–100%Top of the board / end of the list

The zones intentionally overlap. The Mid zone overlaps with both Early and Late, and Late overlaps with Endgame. This flexibility prevents zones from being too rigid while still maintaining a general flow from easier objectives at the start to harder ones at the end.

Each position on the board gets a progress percentage based on its physical location:

  • Grid boards (Ascend): Counted from the bottom row (0%) to the top row (100%)
  • Pyramid boards (Summit): Bottom row is 0%, peak is 100%
  • Triple/Rush: Sequential from start (0%) to end (100%)

An objective can only be placed at a position if at least one of its progression zones includes that position’s progress percentage.

Some positions are exclusive and can only accept objectives from a single progression zone. These are typically the very bottom rows (early-only) and very top rows (endgame-only). The size of these exclusive zones scales with the board size so that smaller boards aren’t dominated by locked-off areas.


Objectives with forced positions are placed first. The system identifies all positions that are targeted by at least one forced objective, shuffles them randomly, and then attempts to place a valid forced candidate at each one.

Even forced placements must respect all active constraints (categories, tags, progression, etc.). If no valid forced candidate can fill a position, it’s left open for the regular placement phase.

Any forced objectives that weren’t placed during this phase are excluded from the regular phase and won’t appear in non-designated spots.

Remaining empty positions are filled one at a time. For each position, the pool is filtered down to only objectives that satisfy all active constraints:

  1. Tag check: remove any objectives whose tag is already on the board

  2. Progression check: remove objectives whose zones don’t match the position

  3. Board category check: remove objectives whose board categories have no remaining quota

  4. Individual limit check: remove objectives that have already hit their placement limit

  5. Range check: remove objectives that have no unused range values left

  6. Line category check: remove objectives that would exceed any line’s category limit

From the filtered pool, an objective is randomly selected and placed. Its constraints are updated (category counts decremented, individual limit reduced, etc.) and the process moves to the next position.

Positions are never filled in a fixed order like left-to-right or top-to-bottom. Instead, each position is randomly picked from the remaining unfilled spots. This prevents goals from categories that exhaust quickly from always clustering in the same area of the board.

When progression is active, unfilled positions are split into two groups: exclusive (early-only or endgame-only) and overlap (everything else). Exclusive positions are filled first, with random selection within each group. This ensures the most constrained spots are filled while there are still plenty of suitable candidates in the pool.


Simple random placement works for most goal sets, but some have tighter constraints like strict category limits or lots of cap-1 line restrictions. The generator has several extra tools to handle these, and they kick in automatically when needed. Most boards never use them.

If the first attempt at greedy placement fails, retries use heuristics to pick smarter. The same candidates are available, but they’re scored and the pick is biased toward better-scoring ones:

  • Board category pressure: favour goals whose board categories still have plenty of room
  • Line category pressure: favour goals that won’t strain already-tight lines
  • Progression scarcity: favour goals with fewer valid zones so they get placed before their options dry up

Randomness still comes first. Rather than always picking the single “best” option, it picks randomly from a top tier of well-scoring candidates. Heuristics nudge the generator away from bad picks, but don’t dictate the outcome.

If greedy can’t find a valid objective for a position, the generator switches to backtracking. This is a more thorough solver that tracks which objectives are valid at each position and updates that tracking incrementally as it places and undoes objectives.

The solver starts from the greedy frontier, keeping the objectives that greedy already placed successfully and only filling the remaining empty positions. This preserves the randomness from the greedy phase while giving the solver a smaller problem to work with.

The solver works by:

  1. Using MRV (Minimum Remaining Values) to decide which position to fill next, always picking the one with the fewest valid candidates. This tackles the tightest spots first while there are still plenty of options left.
  2. Auto-placing singletons: if any position has exactly one valid candidate, it’s placed immediately without spending search budget.
  3. Checking feasibility: after each placement, the solver checks the remaining board is still solvable. If the supply of available objectives drops below the number of empty positions, or if any progression zone runs out of candidates for its exclusive positions, the branch is abandoned immediately.
  4. Picking a random candidate at the selected position, then moving to the next tightest position.
  5. If a pick leads to a dead end, undoing it and trying a different candidate.

If the frontier-seeded attempt fails (the greedy placements may have caused the dead end), the solver automatically retries from a clean slate before counting against restart phases.

Each phase has a budget that scales with the number of open positions. If one phase runs out, a fresh phase starts with a new seed to try a different direction. If a phase exhausts its budget but barely made any progress, remaining restart phases are skipped since a different search direction won’t help.

Before generation starts, the system checks your goal set’s constraints. If they’re restrictive enough that random candidate picks would keep running into dead ends, backtracking switches to constrained mode. This typically means lots of cap-1 line categories covering most of your objectives, or many tight board categories.

Constrained mode activates when either of these is true:

  • Line-restrictive: most line categories cap at 1 per line, and most objectives carry line categories
  • Board-restrictive: most board categories are tight (allowing very few objectives each), and most objectives carry board categories

Instead of picking candidates randomly, constrained mode scores them using heuristics that check how much category budget remains and whether placing a candidate would leave enough supply for other positions that still need it. It then picks randomly from a top tier of the best-scoring candidates, with the tier narrowing across restart phases.

When forced-position objectives exist, constrained mode also checks after each placement that every unplaced forced objective still has at least one viable spot. If a placement would strand a forced objective, it gets skipped immediately.

Normal goal sets are unaffected. Constrained mode only kicks in when the constraint profile calls for it.

If backtracking runs out of budget without finding a valid board, the generator falls back to constraint relaxation. Rather than giving up, it:

  • Finds the candidate that breaks the fewest constraints
  • Places it anyway, ignoring those constraints
  • Finishes the board and flags which constraints were relaxed

A relaxed board is still a valid output and won’t get thrown away for another attempt. The warnings let you know which constraints were too tight so you can tweak them.

Generation has two layers of escalation. Greedy gets smarter across generation attempts, and backtracking gets smarter across restart phases within each attempt. Most boards finish on greedy attempt 1.

Retries only happen on hard failures (tag conflicts or a pool too small to fill the board). If constraints are just tight, constraint relaxation finishes the board without retrying.

Greedy escalates across up to 3 attempts:

AttemptCandidate SelectionBoard Diversity
1stRandomMaximum, every board is unique
2ndRandom from top ~50% of scored candidatesHigh, still varied but steered away from poor picks
3rdRandom from top ~10% of scored candidatesLower, different seeds may give more similar boards

Backtracking runs up to 2 phases per attempt. Each phase uses a different random seed. Normally candidates are random. In constrained mode, they’re scored and the pick is biased toward candidates that preserve future constraint supply:

PhaseNormal BoardsConstrained Boards
1stRandomRandom from top ~50% of scored candidates
2nd (new seed)RandomRandom from top ~25% of scored candidates

Each attempt gets its own backtracking phases, so there’s plenty of room to find valid boards before falling back to constraint relaxation.


Several game modifiers can alter generation behaviour by disabling specific constraints, forcing range values, or adding post-generation effects like shinies and blanks.

Wiki last updated for app version 0.27.X