Cup Stack Jam

A Cup-Sorting Puzzle — Unity Template

Version 1.0 | Unity 6+

Important: Template / Starting Point

This asset is designed as a template and starting point for your own game development. It provides the core systems, architecture and tools needed to build a cup-sorting puzzle. You are expected to customize, extend and modify it to create your unique game. This is not a complete, ready-to-publish game — it is a foundation for developers to build upon.

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

  1. Import the package into a Unity 6 URP project.
  2. 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 a NullReferenceException that does not mention TextMesh Pro at all.
  3. Let Package Manager finish resolving. The kit needs com.unity.ugui, com.unity.purchasing, com.unity.mobile.notifications and the built-in com.unity.modules.screencapture.
  4. Open Assets/CupStackJam/Scenes/CupStackLevel.unity.
  5. 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

Assets/CupStackJam/ Documentation/ this manual Editor/ the three buyer-facing tools and their shared engine CupStackJamLevelEditorWindow.cs Tools > Cup Stack Jam > Manual Level Editor CupStackJamAutoLevelGeneratorWindow.cs Tools > Cup Stack Jam > Auto Level Generator CupStackJamWelcomePopup.cs Tools > Cup Stack Jam > Welcome CupStackJamLevelIO.cs reads and writes the level files Levels/ the two hand-authored reference level assets Materials/ cup, cap, tray, dimple, peg and board materials Meshes/ cup, cap, hex cell, peg, tray and dimple PowerUps/ the four booster definitions Prefabs/ cup, cap, outline shell, hex cell, peg, tray, dimple, panel Resources/ CupStackJam/ CupStackJamCampaign.asset - the shipped campaign Levels/ Level_1.json ... Level_500.json Scenes/ CupStackLevel.unity Scripts/ runtime C# Settings/ palette, style and the level generator Shaders/ the outlined cup shader Textures/ the neutral reflection probe Assets/HyperCasualLiveOpsKit/ the reusable backend, with its own manual

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

ScriptResponsibility
CupStackSimulationThe 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.
CupStackGameDirectorDraws the simulation. Owns every pooled cup, tray, dimple, peg and badge, plus the gather, burst, sparkle and landing effects.
CupStackJamGameThe gameplay component: loads the campaign level, routes input, and exposes the four booster effects.
CupStackJamGameManagerBinds the campaign to the board: which level loads, what happens on win or lose, and every gameplay sound cue.
CupStackJamLevelManagerWhich level is selected and how far the player has got. Reads and writes the same keys the menu's level selector uses.
CupStackJamLevelDataThe runtime level: the pile grids, the tray queues and the variation feature lists.
CupStackJamJsonLevelThe on-disk mirror of a level. One JSON file per level.
CupStackJamLevelBuilderTurns a level plus the shared palette and style into the runtime board definition the director consumes.
CupStackJamCampaignHow many levels ship, where they live, and which palette and style they are played with.
CupStackJamSolverHeadless playthrough simulator. Drives the real simulation, so what it proves is what the player gets.
CupStackJamLevelGeneratorThe generation engine the Auto Level Generator window is built on.

Utility Scripts

ScriptResponsibility
CupStackBoardLayoutTurns 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.
CupStackLevelDefinitionThe runtime board description: palette, pile, trays, pegs and every timing and shading dial.
CupStackJamStylePresentation shared by every level — timing, geometry, shading and the two camera reserves.
CupStackJamPaletteThe colour set every level draws from.
CupStackJamBox / CupStackJamCrateA tray as something a booster can point at, and the lock badge on a queued tray.
CupStackJamCameraRigFrames the board inside the band the HUD and booster reserves leave, and applies shake presets.
CupStackBurstView / CupStackSparkleView / CupStackRingViewThe spark burst on a merge, the sparkle scatter on a completed tray, and the impact ring on a landing.
CupStackJamPowerUpAdapterConnects the four boosters to the reusable power-up framework.
CupStackJamLiveOpsAdapterThe only place the game talks to the backend: rewards, analytics and the leaderboard.

Editor Scripts

ScriptResponsibility
CupStackJamLevelEditorWindowThe Manual Level Editor.
CupStackJamAutoLevelGeneratorWindowThe Auto Level Generator.
CupStackJamWelcomePopupThe first-run greeting.
CupStackJamLevelIOLoads, 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

SettingWhat it does
Difficulty curve exponentShapes 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.
ColoursHow many palette colours a level may use, as a min/max range interpolated by the curve.
Board columns / rowsThe pile's widest row, and its row count. Leave rows at 0 to derive them from the width.
Min / max stack depthThe band each cell's cup count is drawn from.
Tray rows / columnsThe dimple grid on a tray. Rows × columns is its capacity.
Buffer slotsHow many pegs the level ships with — the scarce resource.
Family thresholdsThe 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 sharesWhat fraction of the board each family occupies once permitted.
Pile shapesWhich outlines the generator may cut: full, diamond, V, split columns, U, scattered.
BoostersHow many of each booster a level grants.
Max attemptsSeeds 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:

StageWhat it tries
1The level as specified.
2 — LOOSENEDVariation features reduced to 40%.
3 — LAST-RESORTA plain board with no variation features at all.
FAILEDReported 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.

BoosterEffectTargeted?
ShuffleRe-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 PegAdds 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
UnlockOpens every locked tray in both queues at once, without waiting for its countdown.No
Pull ForwardPulls 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

  1. Add an IPowerUpEffectHandler to CupStackJamPowerUpAdapter and register it in Awake (and unregister in OnDestroy).
  2. Author a PowerUpDefinition asset for it.
  3. Raise StarterMenuConfig > Authored Gameplay UI > Booster Slot Capacity and 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.

FamilyWhat it isCounter-booster
Cut-outsHoles in the pile. They change the shape of the board and therefore which stacks touch which.
Locked traysA 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 stacksA 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 stacksA 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

FieldMeaning
levelNumber / levelNameCampaign 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.
boardColorsPile 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.
boardStacksCup count per cell, same shape. 19 then AZ for 10–35.
defaultStackDepthCups in a stack whose count character is missing.
trayBoxesThe two queues. Row 0 is the left queue, row 1 the right; each character is one tray's colour, front of the queue first.
trayCapacitiesPer-tray capacity override. Empty means "use the level default".
trayDimpleRows / trayDimpleColumnsThe dimple grid on a tray. Their product is the default capacity.
bufferSlotCount / pegCapacityHow many pegs, and how many cups each holds.
obstaclesCut-out cells, as row/column pairs. Also stamped into boardColors as . so there is one read path for the board.
cratesLocked trays: row is the queue, col the position, boxesRequired the completions needed.
mysteryBoxes / wildcardsConcealed and wildcard cells, as row/column pairs.
accessMode0 = any cell tappable (the shipped rule), 1 = edge cells only, 2 = the front row only.
completeBoxesThatRunOutOfTilesWhen 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 levelsFour pairs. The count is how many uses this level grants; the unlock level overrides the catalogue's, or -1 to leave it alone.
difficultyBand / generatorSeedProvenance 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

SymptomCause and fix
Everything renders pinkThe 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 balancedSome 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 deviceThe 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 outIt 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 nothingYou 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!