Overview
Cup Stack Jam is a colour-sorting puzzle. A honeycomb pile of stacked plastic cups sits above two tray slots, each fed by its own queue of coloured trays. Tapping a cup stack merges it with every touching stack of the same colour and feeds the whole group into a matching tray. Cups with nowhere to go park on the pegs — and the pegs are the only thing standing between a careless tap order and a jam.
The whole game is one deterministic, physics-free state machine. Nothing falls, nothing collides, and a level plays out identically every time — which is what lets a headless solver prove every shipped level winnable before it is written to disk.
500 solver-verified levels
Every shipped level was played to a win by a headless solver before it was saved. Not parsed — solved.
Manual Level Editor
A two-panel window: the campaign on the left, a clickable pile and tray queues on the right. Paint colours, cup counts, cut-outs, locks and mystery stacks.
Auto Level Generator
Batch-generates levels from min/max ranges interpolated by a difficulty curve, and refuses to write one it cannot solve.
Four boosters
Shuffle, Extra Peg and Unlock fire on one press; Pull Forward arms and lets you choose a tray waiting deeper in a queue.
Data-driven levels
One JSON file per level, readable in a text editor. Palette, timing and camera live in shared assets, so retuning the game does not mean regenerating it.
LiveOps backend included
Menu, level selector, shop, ads, IAP, minigames, daily rewards and battle pass all come from HyperCasualLiveOpsKit, documented separately.
Getting Started
Requirements
- Unity 6000.0.62f1 or newer
- Universal Render Pipeline (URP) 17.0.4
- Input System package 1.14.2 (the project ships configured for it)
- Linear colour space, portrait orientation
Installation
- Import the package into a Unity 6 URP project.
- Import TextMesh Pro's essential resources if the project has
never had them:
Window > TextMeshPro > Import TMP Essential Resources. Every label in the menu uses TMP, and without them the menu installer fails with aNullReferenceExceptionthat does not mention TextMesh Pro at all. - Let Package Manager finish resolving. The kit needs
com.unity.ugui,com.unity.purchasing,com.unity.mobile.notificationsand the built-incom.unity.modules.screencapture. - Open
Assets/CupStackJam/Scenes/CupStackLevel.unity. - Press Play.
Quick Test
With the gameplay scene open, press Play and tap a cup stack in the pile. Every touching stack of the same colour swells, merges into the one you tapped, and starts feeding cups into a matching tray. Deliver every cup and the level is won.
To play the campaign rather than a single level, make
Assets/HyperCasualLiveOpsKit/StarterMenu/Scenes/HLK_MainMenu.unity build
scene 0 and press Play from there.
Folder Structure
The two folders are separate products. CupStackJam holds this game's
mechanic and content; HyperCasualLiveOpsKit holds the menu, economy, ads,
IAP and minigames, and is documented in its own Documentation.html.
Gameplay
Core Mechanics
- The pile is a honeycomb grid of cup stacks. Each cell holds a number of cups of one colour, and the number is printed on the top cup.
- Tapping a stack flood-fills every touching stack of the same colour into one merged group, which then feeds cups out one at a time. Where you tap decides how big the merge is — that is the whole puzzle.
- Two tray slots sit at the far end of the board, each fed by its own queue of coloured trays. A tray holds dimple rows × dimple columns cups.
- A cup flies to a tray of its colour if one has room. If not, it parks on a peg. Pegs drain automatically the moment a tray of that colour arrives.
- A full tray seals, sparkles, flies away, and the next tray in that queue slides forward.
Palette Colours
Colours live in Settings/CupStackJamPalette.asset. Each entry carries an id
character, a display name, four colours (cup body, lit cap, tray top and tray dimple)
and a default tray capacity. The id character is what the level files use, so adding a
colour can never renumber the levels already on disk.
Win & Lose Conditions
- Win: every cup in the level has been delivered into a tray.
- Lose: the board deadlocks — cups are still owed but nothing can move, because every remaining cup's tray is unreachable and the pegs are full.
Because a level is only ever lost to a jam, the peg count is the game's tightest difficulty lever — and the reason the Extra Peg booster exists. Across the shipped campaign the pile grows while the peg grid shrinks.
Balance
A level is balanced when, for every colour, the cups in the pile exactly equal the total capacity of the trays that accept them. The economy is zero-slack by design: a cup with nowhere left to go is a loss, not a rounding error. An unbalanced level is unwinnable by construction, and no amount of searching discovers that — which is why the editor checks balance before it runs the solver.
Scripts Reference
Core Scripts
| Script | Responsibility |
|---|---|
CupStackSimulation | The whole game as data. No MonoBehaviour, no physics: taps, merges, flights, trays, pegs, win and lose. Advanced by Tick(dt), so a level plays out identically headless and on screen. |
CupStackGameDirector | Draws the simulation. Owns every pooled cup, tray, dimple, peg and badge, plus the gather, burst, sparkle and landing effects. |
CupStackJamGame | The gameplay component: loads the campaign level, routes input, and exposes the four booster effects. |
CupStackJamGameManager | Binds the campaign to the board: which level loads, what happens on win or lose, and every gameplay sound cue. |
CupStackJamLevelManager | Which level is selected and how far the player has got. Reads and writes the same keys the menu's level selector uses. |
CupStackJamLevelData | The runtime level: the pile grids, the tray queues and the variation feature lists. |
CupStackJamJsonLevel | The on-disk mirror of a level. One JSON file per level. |
CupStackJamLevelBuilder | Turns a level plus the shared palette and style into the runtime board definition the director consumes. |
CupStackJamCampaign | How many levels ship, where they live, and which palette and style they are played with. |
CupStackJamSolver | Headless playthrough simulator. Drives the real simulation, so what it proves is what the player gets. |
CupStackJamLevelGenerator | The generation engine the Auto Level Generator window is built on. |
Utility Scripts
| Script | Responsibility |
|---|---|
CupStackBoardLayout | Turns the level's grid numbers into cell and peg positions plus a neighbour table. Neighbours are found by distance, not index arithmetic, so any row pattern works. |
CupStackLevelDefinition | The runtime board description: palette, pile, trays, pegs and every timing and shading dial. |
CupStackJamStyle | Presentation shared by every level — timing, geometry, shading and the two camera reserves. |
CupStackJamPalette | The colour set every level draws from. |
CupStackJamBox / CupStackJamCrate | A tray as something a booster can point at, and the lock badge on a queued tray. |
CupStackJamCameraRig | Frames the board inside the band the HUD and booster reserves leave, and applies shake presets. |
CupStackBurstView / CupStackSparkleView / CupStackRingView | The spark burst on a merge, the sparkle scatter on a completed tray, and the impact ring on a landing. |
CupStackJamPowerUpAdapter | Connects the four boosters to the reusable power-up framework. |
CupStackJamLiveOpsAdapter | The only place the game talks to the backend: rewards, analytics and the leaderboard. |
Editor Scripts
| Script | Responsibility |
|---|---|
CupStackJamLevelEditorWindow | The Manual Level Editor. |
CupStackJamAutoLevelGeneratorWindow | The Auto Level Generator. |
CupStackJamWelcomePopup | The first-run greeting. |
CupStackJamLevelIO | Loads, saves, renumbers and validates the level set. Shared by both windows. |
Level Editor
Opening the Level Editor
Menu path: Tools > Cup Stack Jam > Manual Level Editor
Layout
Two panels. The left lists every level in the campaign, with buttons to add, duplicate, delete and reorder. The right shows the level you have selected: the pile grid on top, the two tray queues below it, and the per-level properties beneath those.
Editing
- Click a pile cell to paint it with the currently selected tool — a colour, a cup count, a cut-out, or a mystery marker.
- Click a queue position to set that tray's colour, or to lock it behind a countdown.
- Undo works with Ctrl+Z and with the Undo button in the toolbar. Both go through Unity's own undo stack, so an edit here behaves like every other edit in the editor.
Validation
- Check Balance reports, per colour, how many cups the pile supplies against how much tray capacity wants them. It runs first because an unbalanced level is unwinnable by construction and the solver would only report "gave up".
- Validate plays the level headlessly and tells you whether it can be won, how many taps it took, how close the pegs came to filling, and how many turns offered a real choice.
- Validate All does the same for the whole campaign and lists every level that fails. File existence is never validation.
Automatic Level Generator
Opening the Generator
Menu path: Tools > Cup Stack Jam > Auto Level Generator
The generator is a serialized asset
Every setting below lives in
Assets/CupStackJam/Settings/CupStackJamLevelGenerator.asset, and the
generator reads that. Editing the default values in
CupStackJamLevelGenerator.cs changes nothing: the C# defaults only ever
apply to an asset that does not exist yet. Change the asset, regenerate, then read
the level files back to see what you actually got.
Settings
| Setting | What it does |
|---|---|
| Difficulty curve exponent | Shapes the ramp across the campaign. Below 1 front-loads it so early levels differ from each other; above 1 holds the game easy and then climbs. |
| Colours | How many palette colours a level may use, as a min/max range interpolated by the curve. |
| Board columns / rows | The pile's widest row, and its row count. Leave rows at 0 to derive them from the width. |
| Min / max stack depth | The band each cell's cup count is drawn from. |
| Tray rows / columns | The dimple grid on a tray. Rows × columns is its capacity. |
| Buffer slots | How many pegs the level ships with — the scarce resource. |
| Family thresholds | The level each variation family is permitted from. A threshold is a floor, not a guarantee: whether one lands depends on the board rolled and whether the solver accepts it. |
| Family shares | What fraction of the board each family occupies once permitted. |
| Pile shapes | Which outlines the generator may cut: full, diamond, V, split columns, U, scattered. |
| Boosters | How many of each booster a level grants. |
| Max attempts | Seeds tried per stage before the ladder loosens. Higher costs time and buys fewer loosened levels. |
Seeds
Generation is deterministic. The same seed and the same settings produce a byte-identical campaign, and each level's seed is derived from its own number — so level 250 is the same board whether you generated 250 levels or 500, and re-rolling one level does not shift every level after it.
How a level is built
Backwards, from the trays. The generator chooses the trays first and then fills the pile to match, so the zero-slack balance holds by construction rather than by luck. Rolling a board first and hoping makes an unwinnable level the common case.
The three-stage ladder
No level is ever written without being solved. When one cannot be, the generator degrades in a defined order and names the stage in its report:
| Stage | What it tries |
|---|---|
| 1 | The level as specified. |
| 2 — LOOSENED | Variation features reduced to 40%. |
| 3 — LAST-RESORT | A plain board with no variation features at all. |
| FAILED | Reported and skipped. Numbering stays contiguous. |
The run report lists the loosened and last-resort counts, the duplicate rate, each family's measured first occurrence, and the difficulty trend per fifty-level band. A report that only said "500 written" would hide every one of those.
Boosters
Boosters are catalogue-driven. Each one is a PowerUpDefinition asset in
Assets/CupStackJam/PowerUps/ that owns its inventory, persistence,
tutorial, arming, sound, haptics and unlock level; the game supplies only what the
effect does to the board.
| Booster | Effect | Targeted? |
|---|---|---|
| Shuffle | Re-orders the trays still queued behind both slots. Trays already in a slot are left alone, and a lock travels with its tray. | No |
| Extra Peg | Adds one peg to the buffer for the rest of the level. The peg grid is rebuilt so the new peg has somewhere to be. | No |
| Unlock | Opens every locked tray in both queues at once, without waiting for its countdown. | No |
| Pull Forward | Pulls one queued tray straight to the front of its queue. While armed, every tray it can reach pulses. | Yes |
Refusals cost nothing
Every effect returns whether it actually changed something, and the framework consumes a charge only when it did. Shuffling a board with nothing queued, or pressing Unlock with nothing locked, refuses and spends no charge. Reports success and changes nothing is the worst failure a booster can have.
Pull Forward deliberately refuses a locked tray rather than quietly opening it — that is Unlock's job, and doing two boosters' work in one would make both harder to reason about.
Adding a fifth booster
- Add an
IPowerUpEffectHandlertoCupStackJamPowerUpAdapterand register it inAwake(and unregister inOnDestroy). - Author a
PowerUpDefinitionasset for it. - Raise
StarterMenuConfig > Authored Gameplay UI > Booster Slot Capacityand re-run the gameplay UI factory rebuild. The bar's socket count is authored, not dynamic — skip the rebuild and the fifth booster is catalogued, tested and unreachable.
Level Variation
What makes level 90 different from level 9 is not more cups — it is that the content changes. Four families ship, and every one of them is introduced inside the first ten levels, because a player who stops before then would never find out the game has it.
| Family | What it is | Counter-booster |
|---|---|---|
| Cut-outs | Holes in the pile. They change the shape of the board and therefore which stacks touch which. | — |
| Locked trays | A tray in a queue that will not come forward until a number of trays have completed. It shows its countdown, and it blocks its queue rather than being skipped. | Unlock |
| Mystery stacks | A cup stack whose colour is concealed behind a ?. It cannot be tapped, and a merge stops at it. Clearing a neighbouring stack reveals it, and the reveal lerps to the real colour. | — |
| Wildcard stacks | A pale stack that any tray with room will take. It routes to the tray that most needs it, not simply the first one open. | — |
The peg count is a fifth axis and is present from level 1: it is the level's scarce resource, and the campaign tightens it as the pile grows.
Reading the real first occurrence
The generator's thresholds are floors. To find out when a family actually first appears,
read Artifacts/Levels/campaign-report.txt, which measures the level files on
disk. Configured and measured are routinely different by a level or two.
Customization Guide
Re-skinning the colours
Open Settings/CupStackJamPalette.asset. Each entry has four colours: the cup
body, the lit cap, the tray top and the tray dimple. They are separate fields rather than
shades of one because the reference art shifts hue differently per colour — one
shared darkening factor cannot produce all of them.
Retuning the feel
Open Settings/CupStackJamStyle.asset. Everything about timing, geometry,
shading and the camera lives there, shared by every level — so you can retune the
whole game without regenerating a single level file.
Framing the board
The style asset carries two reserves: the fraction of screen height the HUD owns at the top, and the fraction the booster shelf and banner own at the bottom. The camera fits the board into the band those leave. They are measurements taken from a played build, not preferences — if you move the HUD, re-measure them.
Replacing the art
Cups, caps, trays, dimples, pegs and hex cells are ordinary prefabs in
Prefabs/, each backed by an FBX in Meshes/. Swap the mesh and
the game re-measures itself around it: cup height, radius and nesting step are all read
off the mesh rather than typed in.
Level Format
One JSON file per level, at
Assets/CupStackJam/Resources/Levels/Level_N.json, numbered from 1 with no
gaps. The loader walks upward until a file is missing, so a gap silently truncates the
campaign — reordering in the editor renumbers the files on disk rather than
keeping a separate order list.
Fields
| Field | Meaning |
|---|---|
levelNumber / levelName | Campaign position and display name. The position is re-assigned from the file index on load, so a hand-edited number cannot fight its own filename. |
boardColors | Pile rows, top first. One character per cell: a palette id, . for a hole, * for a wildcard. Rows may differ in length — shorter rows are centred, which produces the honeycomb stagger. |
boardStacks | Cup count per cell, same shape. 1–9 then A–Z for 10–35. |
defaultStackDepth | Cups in a stack whose count character is missing. |
trayBoxes | The two queues. Row 0 is the left queue, row 1 the right; each character is one tray's colour, front of the queue first. |
trayCapacities | Per-tray capacity override. Empty means "use the level default". |
trayDimpleRows / trayDimpleColumns | The dimple grid on a tray. Their product is the default capacity. |
bufferSlotCount / pegCapacity | How many pegs, and how many cups each holds. |
obstacles | Cut-out cells, as row/column pairs. Also stamped into boardColors as . so there is one read path for the board. |
crates | Locked trays: row is the queue, col the position, boxesRequired the completions needed. |
mysteryBoxes / wildcards | Concealed and wildcard cells, as row/column pairs. |
accessMode | 0 = any cell tappable (the shipped rule), 1 = edge cells only, 2 = the front row only. |
completeBoxesThatRunOutOfTiles | When on, a tray whose colour has run out completes anyway instead of deadlocking. A safety net for hand-authored levels; solver-verified ones do not need it. |
| booster counts and unlock levels | Four pairs. The count is how many uses this level grants; the unlock level overrides the catalogue's, or -1 to leave it alone. |
difficultyBand / generatorSeed | Provenance written by the generator. Nothing in the game reads them. |
Example
{
"levelNumber": 2,
"levelName": "Level 2",
"boardColors": [ "GGG", "BBGG", "GGB", "BBBG", "GGB" ],
"boardStacks": [ "233", "2223", "322", "2232", "223" ],
"defaultStackDepth": 2,
"trayRows": 2,
"trayColumns": 2,
"trayBoxes": [ "GB", "GB" ],
"trayCapacities": [ "", "" ],
"trayDimpleRows": 3,
"trayDimpleColumns": 4,
"bufferSlotCount": 12,
"pegCapacity": 4,
"accessMode": 0,
"shuffleCount": 3,
"extraSlotCount": 2
}The board above is illustrative. Every shipped level is balanced — each colour's cup total equals the capacity of the trays that take it — and has been solved.
Support
Common Issues
| Symptom | Cause and fix |
|---|---|
| Everything renders pink | The project is not using URP, or the render pipeline asset is not assigned. Set it in Project Settings > Graphics. |
| "The campaign cannot start" | CupStackJamCampaign.asset is missing from Resources/CupStackJam/, or its level count is 0. Open the Manual Level Editor and press Save All. |
| A level says it is not balanced | Some colour has more or fewer cups in the pile than its trays can hold. The report names the colour and the difference. |
| Taps do nothing on device | The gameplay scene needs an EventSystem. The kit's gameplay UI installer authors one; re-run it from Tools > HLK > Gameplay UI. |
| A booster button is greyed out | It is either locked (the button shows the level it opens on) or empty (it shows 0). Both are set per level. |
| The generator reports "LAST-RESORT" | That level could not be made winnable with its variation features and a plain board was written instead. Widen the tray or peg ranges, or lower the family shares. |
| A change to the generator did nothing | You edited the C# defaults. The generator reads its asset; edit Settings/CupStackJamLevelGenerator.asset. |
Tips
- Keep the opening level as level 1. It is the board the mechanic was derived from and the fixture the tests compare against.
- A locked tray's countdown must cost more than its queue position, or it opens before the tray reaches the front and the lock is decoration.
- Every mystery stack needs at least one ordinary neighbour, or nothing can ever reveal it.
- Re-run Validate All after any bulk edit. File existence is not validation.
Contact Us
If you get stuck or have any issues, feel free to reach out to us at satisvizion@gmail.com. We are happy to help!