Aquarium Puzzle Engine Manual

Version: 1.0.0

Built on Unity 6000.2. Nothing in the code requires anything past plain C# 8/9, so it may well work on older LTS releases too, but that hasn’t been tested and isn’t guaranteed.

Aquarium Puzzle Engine is a self-contained Unity framework for Aquarium logic puzzles. It includes random puzzle generation, solver-based validation, authored puzzle assets, level collections, runtime play logic, and Editor authoring tools. It does not include game menus, UI prefabs, animations, economies, saves, ads, or a visual skin.

Don’t want to write C#? Skip ahead to the No-Code Integration sample (Package Manager → this package → Samples), then follow No-Code Setup for a full menu + gameplay setup wired entirely through the Inspector.

Contents

  1. Install and open the demo
  2. Core concepts
  3. Generate and play a puzzle
  4. Background generation
  5. Cancellation and restart
  6. Mobile performance
  7. Author levels and collections
  8. Content validation
  9. Progression: thematic order and economy config
  10. Troubleshooting and limitations
  11. Support

Install and open the demo

  1. Add the package to a Unity project through the Package Manager or as an embedded package.
  2. In the Package Manager, open this package’s Samples tab and import Demo (or use
    Tools > Aquarium > Welcome & Setup, which has the same Import button). This copies the sample
    into Assets/Samples/Aquarium Puzzle Engine/.../Demo/ — it isn’t part of the package until imported.
  3. Open the imported DemoScene.unity (in the Demo sample’s Scenes/ folder).
  4. Enter Play Mode.

DemoScene.unity is intentionally implemented with IMGUI to show raw core logic without any dependencies: generate a board, solve it by clicking the grid, validate it with the solver, or load an authored level from a collection.

For a production-ready starting point, open UGUIDemoScene.unity from the same Demo sample. It provides a clean, standard Canvas-based UI implemented in Runtime/Scripts/View/ that demonstrates how to render the grid, cells, and clues using Unity UI (UnityEngine.UI). You can use this as a template to build your own visual presentation.

UGUI demo controls

UGUIDemoScene comes pre-wired with three optional standard uGUI controls on AquariumPuzzleUGUIDemo:

ControlComponent fieldBehaviour
Board size selectorBoard Size DropdownOffers 6×6, 10×10, and 15×15. Choose a size, then press Generate.
Authored level selectorLevel DropdownOffers a random puzzle followed by levels from the bundled demo registry; the project registry is a fallback.
Region color settingRegion Colors ToggleShows or hides the region color fill without changing puzzle data or region borders.

The bundled demo levels show up with no setup needed. If you also want your own project-level collections in that selector, run Tools > Aquarium > Project Setup > Sync Project Level Collection Registry after adding or removing them, then restart the scene.

Core concepts

TypeResponsibility
AquariumPuzzleAssetScriptableObject for an authored board, clues, regions, author solution, visual data, and difficulty metadata.
AquariumPuzzlePlayFieldPure interactive model. It applies water gravity, crosses, mistake checks, and completion checks. Render this model with any UI system.
AquariumPuzzleSolverValidates raw board data, determines solvability, counts solutions, and returns the first solution.
AquariumRandomPuzzleGeneratorBuilds a unique random board for any size from 5×5 up to 30×30. Can optionally target an AquariumPuzzleDifficulty (best effort).
AquariumRandomPuzzleRuntimeSessionSerializable generated-board data used to create a play field.
AquariumLevelCollectionOrdered list of authored puzzle assets.
AquariumLevelCollectionRegistryCollection lookup by stable ID or display name.

Puzzle data flow

Your UI owns rendering, input feedback, animation, and screen navigation. The engine owns puzzle rules and data only.

Generate and play a puzzle

Generate a board, then load the resulting session into AquariumPuzzlePlayField.

using System;
using Aquarium.Puzzle;

var random = new Random(12345);
bool generated = AquariumRandomPuzzleGenerator.TryGenerate(
    width: 10,
    height: 10,
    attempts: 60,
    random,
    out AquariumRandomPuzzleRuntimeSession session);

if (!generated)
{
    return;
}

var field = new AquariumPuzzlePlayField(session);
field.Toggle(3, 4, AquariumPuzzlePlayField.PaintTool.Water);
field.Toggle(1, 0, AquariumPuzzlePlayField.PaintTool.Cross);

int mistakes = field.CheckMistakes();
bool solved = field.IsSolved;

Use a fixed seed to reproduce a board during debugging. Two generator calls that receive new System.Random instances with the same seed produce equivalent board data. SessionId is intentionally unique and must not be used when comparing deterministic results.

Background generation

AquariumPuzzleSolver.ValidateAndSolve accepts raw arrays and does not access UnityEngine.Object. AquariumRandomPuzzleGenerator.TryGenerate also works on data values and can run in a background task.

Do not create, read, mutate, or destroy Unity objects from that task. This includes GameObject, MonoBehaviour, ScriptableObject, Texture, UnityEngine.Random, UI controls, and scene state. Capture only value inputs on the main thread, generate in the background, then load the completed session into the play field and update UI on the main thread.

using System;
using System.Threading.Tasks;
using Aquarium.Puzzle;
using UnityEngine;

public sealed class PuzzleGenerationController : MonoBehaviour
{
    private int requestVersion;
    private AquariumPuzzlePlayField field;

    public async void Generate(int width, int height, int seed)
    {
        int version = ++requestVersion;

        AquariumRandomPuzzleRuntimeSession session = await Task.Run(() =>
        {
            var random = new Random(seed);
            AquariumRandomPuzzleGenerator.TryGenerate(
                width,
                height,
                attempts: 60,
                random,
                out AquariumRandomPuzzleRuntimeSession generatedSession,
                timeBudgetMs: 1500);
            return generatedSession;
        });

        // This code resumes on Unity's main synchronization context.
        if (this == null || version != requestVersion || session == null)
        {
            return;
        }

        field = new AquariumPuzzlePlayField(session);
        RefreshBoardUI(field);
    }

    private void RefreshBoardUI(AquariumPuzzlePlayField playField)
    {
        // Create or update Unity UI here, on the main thread only.
    }
}

Do not share one System.Random instance between concurrent generation tasks. Give each request its own instance with its own seed.

Cancellation and restart

The current generator does not accept a CancellationToken, so it cannot stop in the middle of an individual generation attempt. Use two complementary controls:

  • Set timeBudgetMs to bound the time an individual background request can spend searching.
  • Increment a request version whenever the player changes board size, starts a new puzzle, or leaves the screen. Ignore a result whose version is no longer current.

The requestVersion check in the previous example implements the second rule. Calling Generate again does not forcibly abort the old task; it makes its eventual result harmless. This is the recommended behaviour for the current API.

For an explicit restart button:

public void RestartGeneration(int width, int height)
{
    int seed = Environment.TickCount;
    Generate(width, height, seed);
}

private void OnDisable()
{
    requestVersion++;
}

OnDisable invalidates a completed result after its owning screen has gone away. It does not cancel CPU work already in progress; keep timeBudgetMs conservative for the target device.

Mobile performance

Generation is CPU work. It is safe to use on mobile when it runs outside the main thread and has a clear time budget.

Board sizeRecommended useStarting attemptsSuggested background time budget
5×5 – 6×6First-session play, frequent refreshes, low-end devices40–60250–500 ms
7×7 – 10×10Standard gameplay60–120750–1500 ms
11×11 – 15×15Advanced mode or pre-generated content120–1801500–3000 ms
16×16 – 30×30Loading-screen or pre-generation only; ship curated authored levels when possible180+3000–8000 ms

These are starting values, not performance guarantees. Profile representative Android and iOS hardware and adjust according to your loading UX.

Recommended mobile integration:

  1. Pre-generate the next board while the player is solving the current board or viewing a result screen.
  2. Show an indeterminate loading state while a requested board is being generated; do not block input or the Unity main thread.
  3. Reuse the generated AquariumRandomPuzzleRuntimeSession instead of generating again when the player reopens the same board.
  4. Treat 15×15 generation as a background or loading-screen operation. For predictable pacing, ship curated AquariumPuzzleAsset levels.
  5. Use a time budget and the version-guarded restart pattern described above.
  6. Profile with Development Build on physical devices. Editor timings do not represent mobile hardware.

Author levels and collections

Create an authored level through Assets > Create > Aquarium > Puzzle Asset, or open the Aquarium workbench via Tools > Aquarium > Puzzle Workbench.

The workbench offers four distinct tools (modes) for creating puzzles:

  1. Manual (✏): Draw regions and paint the intended water mask by hand.
    • Region Palette: Add a new empty region with + Region. Remove a region by clicking its X Button (subsequent region IDs shift down automatically to maintain continuous numbering).
  2. Generate from Texture (🖼): Create a puzzle automatically from a solution texture (required) and an optional region texture.
    • Solution texture only: pixel luminance decides water vs. no-water (invert with Invert Luminance, tune the cutoff with Water Threshold). Regions are then built automatically around the resulting water shape, using the same algorithm as Generate from Water.
    • Both textures: the solution texture still drives the water mask, and the region texture is sampled directly — each distinct pixel color becomes one region.
    • This mode writes AuthorWaterMask/RegionIds (and RegionColors, when a region texture is supplied) only. It does not read pixel colors into FinalPaintColors. For a full-color reveal, repaint the level in Final State mode afterward, or set FinalPaintMask/FinalPaintColors yourself through the authoring API — auto-capturing sprite colors during generation is a planned improvement, not implemented yet.
  3. Generate from Water (💧): Draw the intended water mask directly on the grid, then click “Generate Regions From Water” to let the engine build valid regions around your water placement.
  4. Final State (🎨): Paint a final pixel-art solution that is revealed only when the player completes the puzzle. This is currently the only way to set FinalPaintMask/FinalPaintColors — painting here is manual, per-cell, and independent of whatever texture (if any) you used to generate the puzzle.

In all modes, the workbench provides a live preview grid where you can test solving your puzzle. The inspector at the bottom displays real-time validation from the built-in solver (checking for multiple solutions, disconnected regions, and incorrect water levels).

How the final-state reveal reaches the player

Painting a final state in the workbench only stores data (AquariumPuzzleAsset.FinalPaintMask/FinalPaintColors). Showing it to the player at runtime takes one more step:

  • AquariumPuzzlePlayField.Load(AquariumPuzzleAsset) copies that data in and sets HasFinalPicture to true if any cell was painted. Random boards never have final-paint data, so HasFinalPicture stays false for sessions loaded through Load(AquariumRandomPuzzleRuntimeSession) or the raw Load(...) overloads.
  • AquariumPuzzlePlayField.TryGetFinalPaintColor(x, y, out color) returns the painted color for a cell once IsSolved is true. Call it after checking IsSolved, and fall back to your normal “solved” look (a filled aquarium) when it returns false — the cell may not be painted, or the board may have no final picture at all.
  • AquariumPuzzleBoardView (the bundled UGUI view, used by both demo scenes and the No-Code Integration sample) handles this for you. Its Reveal Final Paint On Solve toggle is on by default: once the bound playfield is solved, RefreshAll() switches a painted cell to its final color and hides the water fill, mistake hatch, and cross for that cell. Every sample already calls RefreshAll() right after a solving move, so the reveal needs no extra wiring.
  • The IMGUI demo (Samples~/Demo/Scenes/DemoScene.unity) draws the same reveal directly in its own DrawCell method, since it doesn’t use AquariumPuzzleBoardView.
  • Building your own renderer? Call TryGetFinalPaintColor yourself once IsSolved. The engine doesn’t repaint anything outside the bundled view.

The reveal is an instant color swap. There’s no built-in transition (fade, water drain, or otherwise) — build one on top of TryGetFinalPaintColor if you want it; presentation is your call.

Group finished puzzle assets in an Aquarium > Level Collection, then add those collections to a Level Collection Registry. Each collection has a CollectionMode:

  • ByTheme — thematic, ordered progression.
  • BySize — all levels share one board size.
  • ByDifficulty — all levels share one AquariumPuzzleDifficulty.

For BySize/ByDifficulty, the Inspector shows an “Add all matching levels” button that scans the project for levels matching the collection’s declared size or difficulty.

A BySize collection sets its size one of two ways, picked from the Size Category dropdown in its Inspector:

  • Linked to a category — pick a category from an AquariumGameConfig (your own assigned config, or the project config from Resources). CollectionWidth/CollectionHeight then resolve live from that category. Rename or resize the category in the config, and every collection linked to it updates automatically.
  • Manual ("(Manual — not linked to a category)") — a plain width/height on the collection itself, unrelated to any config. This is also the automatic fallback if a linked category goes missing (config reassigned, category renamed or removed); the Inspector shows a warning instead of silently reporting a 0x0 size.

Need the full category data at runtime, not just the width/height? Call AquariumLevelCollection.TryResolveSizeCategory(out category) — it returns the underlying AquariumPuzzleCategoryDefinition, including LaunchCost/WinReward.

At runtime, access authored collections through AquariumLevelCollectionRegistryProvider.Registry. Run Tools > Aquarium > Project Setup > Sync Project Level Collection Registry after adding or removing project collections. The provider only loads assets; it never creates or saves project data during a read.

Content validation

Open Tools > Aquarium > Content Validator and press Run Validation to scan every puzzle asset and collection in the project for:

  1. Orphaned levels — AquariumPuzzleAsset assets that are not referenced by any collection.
  2. Levels whose Difficulty does not match the CollectionDifficulty of the ByDifficulty collection that contains them.
  3. Levels whose Width/Height don’t match the CollectionWidth/CollectionHeight of the BySize collection that contains them.
  4. ByTheme collections that are not registered in any AquariumThematicCollectionsOrder asset.

The window reports how many levels, collections, and order assets it checked, plus a list of issues with the offending asset. Run it before every content update or build — none of these problems block compilation, so they can otherwise ship silently.

Progression: thematic order and economy config

Two optional ScriptableObjects add level progression and a light economy layer on top of the core engine. Skip this section if you only need generation, the solver, and the play field.

AquariumThematicCollectionsOrder

Create one via Assets > Create > Aquarium > Thematic Collections Order, then list your ByTheme collections in ThematicCollections, in the order you want players to encounter them. It only considers collections with CollectionMode == ByTheme; ByDifficulty collections are ignored.

MemberUse it to
TryGetLevelAt(int orderedLevelIndex, out level)Resolve a flat, cross-collection level index (e.g. “level 42 of the campaign”) to the actual asset.
CountLevelsInOrder()Get the total number of levels across all registered ByTheme collections, for progress bars or level-select grids.
TryGetNextUncompletedByDifficulty(difficulty, isCompleted, out level, out orderedIndex)Find the next level of a given AquariumPuzzleDifficulty your save data hasn’t marked complete yet.
TryGetNextUncompletedBySize(width, height, isCompleted, out level, out orderedIndex)Same idea, filtered by board size instead of difficulty; internally tries Easy, then Medium, then Hard for that size.
TryGetLevelById(levelId, out level, out orderedIndex)Look up a level by its AquariumPuzzleAsset.LevelId (e.g. to resume a level referenced from a save file). Results are cached; the cache clears automatically via OnValidate when you edit the asset in the Editor.

isCompleted is a callback you provide. This package doesn’t persist save data itself — wire the callback to your own save system to decide which levels count as completed.

AquariumGameConfig

One config for the whole game’s puzzle rules, not just random generation. It has two parts:

  • Categories (AquariumPuzzleCategoryDefinition[]) — named board-size presets (Id, DisplayName, Width, Height), each with an optional LaunchCostCurrencyId/LaunchCost and RewardCurrencyId/WinReward. These apply to any level of that size — authored or random. Look one up by id with TryGetCategory(id, out category), or by exact board size with TryGetCategoryForSize(width, height, out category).
  • Random Generation — GenerationAttempts (used by AquariumRandomPuzzleGenerator) and MilestoneRewards (MilestoneReward[]: a CategoryId, a CompletedLevelsRequired count, and a reward — RewardCurrencyId/RewardAmount, or a free-form RewardId — to grant once a player finishes that many levels in a category).

For an authored level’s win reward, call config.TryResolveWinReward(level, out currencyId, out amount): it returns the level’s own AquariumPuzzleAsset.CustomCompletionReward if HasCustomCompletionReward is set, otherwise the reward of the category matching that level’s Width/Height, if one exists.

Nothing here spends or grants currency automatically — that’s your economy/save system’s job. The point is one place for size-based rules and rewards, read by authored levels, random generation, and your own game code alike, instead of scattering the same numbers across separate assets.

Troubleshooting and limitations

SituationResolution
Generation returns falseRetry with a larger attempts value or time budget. Do not assume a session is present when the method returns false.
Solver result is AbortedRaise maxNodeVisits only after profiling, or reject the board and generate another candidate. An aborted solver result is not a validation result.
A stale board replaces a newer selectionUse one request version per screen and ignore results whose version is no longer current.
UI freezes during generationMove TryGenerate into a background task. Update the play field and all Unity UI only after the task completes on the main thread.
Need a finished mobile game UIBuild it on top of AquariumPuzzlePlayField; UI prefabs, tweening, menus, sound, progression, and saves are intentionally outside this package.

Support

For setup questions, bug reports, or feature requests, contact me at lifgaldur@gmail.com.

For a useful support request, include the package version, Unity version, render pipeline, target platform, reproduction steps, Console output, and a minimal project when possible.