index-decks.php — Decks (Step 3)
The step where the designer decides how to organise the cards they're about to create. Not a gameplay setup. Not physical piles at the table. This is an authoring structure — how Step 4's card-design surface is chopped into manageable sections.
What this page is, conceptually
Two different concepts, both called “deck” in casual language:
- A gameplay pile — a location cards live in during play (draw pile, market, discard). Modelled in Step 2 as zones.
- A design deck — a bucket of cards the designer works on together while authoring. Modelled here, on Step 3.
The two can correspond 1:1 in simple games (one design deck → one draw pile) or many-to-one in richer ones. They deserve separate data.
Concrete example. Building a Star-Realms-like game, the designer might want their Step 4 authoring UI split as:
- Starter deck — scouts + vipers (10 cards, shared)
- Machine Cult deck — faction cards (~20)
- Trade Federation deck — faction cards (~20)
- Blob deck — faction cards (~20)
- Star Empire deck — faction cards (~20)
- Generic / outpost deck — non-faction utility cards
At play time, decks 2–6 all shuffle together into one gameplay “trade deck” zone. But while designing, the author wants them separated — each faction has distinct mechanics, tone, and balance concerns. Step 3's job is capturing that authoring layout; the play-time collapse to a single pile is a downstream concern.
The page models three decisions, all about the authoring view:
- How to chunk the card list — one big bucket? One per family? One per card type? Something custom? Offered as presets for the first-time flow; editable afterwards.
- Starter deck — optional. A pre-made opening hand / personal pile. The designer chooses whether one shared starter exists (all players use the same cards) or each player gets a unique one. This bucket carries a play-time meaning as well as an authoring one, but the field captured here is the authoring structure.
- Composition quota per bucket — for each deck, the designer specifies what proportion of cards should be each family, type, and subtype. This is an authoring target ("when I'm filling this deck, aim for 60 % Units"), not a gameplay balance constraint.
Nothing about card identity, flavour, or rules text lives here. That's Step 4 — and Step 3's structure drives how Step 4's UI is organised.
The user's journey on this page
The page renders one of two states depending on whether the game already has decks:
First visit — the preset wizard
- User arrives from Step 2 (or directly from My Games). No decks in DB yet.
-
A card labelled "Define your initial decks" opens. The
server has pre-computed up to three presets based on game-meta:
- One deck per family — only appears if families are enabled. Splits
totalCapevenly across families. - One deck per card type — only if >1 root type is ticked on Step 2. Sized by type ratio from
frequency_distribution.type_distribution. - Single core deck — always offered as fallback. All cards in one pile.
- One deck per family — only appears if families are enabled. Splits
- User picks one preset (radio). Optionally toggles "Add starter deck(s)" and chooses Shared vs Unique per player.
-
Save & Continue posts to
ajax-steps.php?method=save-decks-presets. On success, redirects toindex-cards.php.
Return visit — the existing-decks table
- Preset wizard is not shown (because decks already exist). Instead, an Existing Decks table lists them with Name, Cards, Parent, Description, and per-row Edit / Cards buttons.
- Clicking New Deck (top-right) reveals a hidden card form: name, description, soft/hard caps, and the collapsed Card Frequency Distribution editor with sliders.
-
Saving that form creates or updates a deck. (Save logic lives in
ajax-steps.php; not on this page.)
The page has two independent forms: #deckPresetForm
(the wizard) and #gameDecksForm (the per-deck editor).
Each posts a different method to ajax-steps.php. Worth
knowing because the UX implies a single flow, but the server handles
them as separate concerns.
What's really captured
Grouped by intent, not by field name:
Authoring layout — “How do I want my card list carved up?”
deck_setup[preset_key], deck_setup[preset_details][*]
The preset choice and a hidden payload with each resulting deck's name,
cardtype_key or family_key, and soft/hard
caps. The number of decks is derived from the preset —
the user doesn't explicitly say "I want 6 decks".
Starter deck — “Is there a pre-made opening pile?”
deck_setup[starter][enabled], deck_setup[starter][mode]
A toggle plus a choice of shared (all players use the same
starter composition) or unique (each player gets a separately-authored
starter). The size defaults to totalCap / 10 soft and
soft * 1.2 hard — reasonable guesses, not customisable.
The starter is a first-class authoring bucket just like the main decks.
Sizing — “How many cards should I author in this bucket?”
deck[soft_card_cap], deck[hard_card_cap]
Only captured explicitly when editing an individual deck. Soft cap is the target card count the designer plans to create, hard cap is the ceiling. Both default from the preset but can be adjusted after.
Composition quota — “What proportions am I aiming for in this bucket?”
deck[frequency_distribution][family_distribution][*],
[type_distribution][*],
[subtype_{root}_distribution][*]
Sliders grouped into sections, each summing to 100 %. Three layers:
- Family split — only if families are enabled.
- Root type ratio — % Unit vs % Event vs % Item, etc.
- Per-root subtype split — e.g. within Units: 40 % Striker / 30 % Frontliner / …
Subtype sub-distributions ship with an "Auto" toggle
(_auto: true meta flag) that hides the sliders and lets
the system pick defaults. Good intention; invisible to users who
don't know the sliders exist.
These numbers feed Step 4 as authoring quotas — when the designer (or AI) fills the bucket, the target is "roughly 60 % Units, 30 % Events…".
Hierarchy — “Is this a sub-section of a bigger deck?” disabled
deck[parent]
The parent_deck_id column exists in game_decks,
and the UI has a hidden field for it tagged "coming soon".
If shipped, this would let the designer say "Machine Cult,
Trade Federation, Blob, Star Empire, and Generic are all sub-buckets
of the Trade deck" — useful for authoring layout (a collapsible
tree in Step 4) and for expressing the many-to-one mapping into a
gameplay zone at play time.
What happens when they hit Save
First-time flow (preset wizard):
- JS serializes
#deckPresetForm(hidden inputs with preset details). - Appends
method=save-decks-presets+gameUniqueId. - Posts to
ajax-steps.php. -
The server: reads the preset, creates one row in
game_decksper deck described by the preset (with the hiddencardtype_key/family_keystored indeck_meta), plus starter decks if toggled. - Returns
{ success, gameUniqueId }. JS offers a "Continue" button toindex-cards.php.
Per-deck edit flow (#gameDecksForm):
- Separate form, different server method. Loads when user clicks New Deck or Edit.
- Writes a single row to
game_deckswith fulldeck_metaincluding frequency distribution.
ALT-clicking Save on the preset wizard opens the POST in a new tab with
is_debug=1 — same debug hatch as the other pages.
How decks feed the rest of the app
Decks are the contract between this step and Step 4's authoring UI — not between Step 3 and gameplay. Each deck writes:
- A row in
game_deckswithdeck_name,soft_card_cap,hard_card_cap,sort_order,deck_metaJSON. - A
deck_meta.frequency_distributionblock describing the proportional target for the designer / AI to hit when filling the deck.
Step 4 reads these and:
- Presents the card list grouped by deck (the authoring layout).
- Drives card-generation quotas: "fill this deck with up to 60 cards; the composition says 50 % Unit, 30 % Event, 20 % Item, so generate 30/18/12".
- Names deck batches when the AI is asked to generate a whole deck.
No link to gameplay zones. Whether a design deck ends up as a draw pile, a market deck, a trash, or is shuffled together with other decks into one play pile — none of that is expressed on Step 3. It's a real gap (see "What's rough") but it also helps explain the page's scope: this is about organisation for authoring, not setup for play.
$totalCap on this page is
game_briefing.card_volume_data.soft_card_cap — the
volume preset picked on Step 1. Change the game size there and the
presets here re-scale. Nice bit of cross-step coherence.
What works well, conceptually
- Three-preset model is on point. "By family / by type / single pile" matches how designers actually think. Most games fit one of these three patterns.
- Soft vs hard cap distinction is the right abstraction — designers target an ideal size but want a ceiling. Sticks to reality of playtest iteration.
- Starter deck is a first-class concept. Many deck-builders have them (Dominion's estates + coppers, Star Realms' scouts + vipers), and the shared/unique toggle covers both patterns.
- Frequency distribution with sliders is powerful. The `_auto` flag on subtype sub-distributions is a clever progressive-disclosure move — novices don't see the sliders, power users can override.
-
Distribution is data-driven from game-meta
(
buildFreshDistributionStructure). Families and enabled types drive what sliders exist, not a static template. - Cross-step coherence: changes to Step 1 (card volume) and Step 2 (enabled types, enabled families) flow through to this page's preset sizing. Real mechanical link, not just visual reference.
- Two views on one page (wizard for first visit vs table for return) is a nice fit — onboarding vs management without a hard mode toggle.
What's rough, conceptually
Presets are monolithic — no hybrid / mixed layouts
The three presets are "all by family", "all by type", or "one pile". The Star-Realms-like example from the model section (4 faction decks + 1 generic deck) fits none of them cleanly. A designer who wants hybrid layouts has to pick the closest preset, then edit afterwards by hand in the existing-decks table — adding the generic deck, resizing the faction decks, fixing their compositions. It works but the preset system doesn't express the real design intent.
No link to gameplay zones
Step 2 defines zones. Step 3 defines design decks. Nothing connects them. For a simple game, the 1:1 mapping (design deck X → draw pile) is obvious enough to guess. For anything richer (multiple design decks shuffled into one play pile, or design decks that never become a pile because they are the starter), the relationship is implicit and undocumented.
This is a real gap but one that belongs with Flow / setup
rather than with authoring. Design decks don't need a direct
start_zone field — they need a setup step (post-Step 3)
that says "at play time, combine these design decks into this gameplay
zone, shuffle it".
The preset language leaks deck-builder assumptions
"One per family" and "One per type" are useful authoring views for deck-builders and TCGs. But:
- A trick-taking designer just wants "one deck of 52 cards with rank + suit"
- A drafting designer wants to author packs, not decks — different authoring unit
- A legacy / campaign designer wants decks organised by scenario or chapter
The presets are authoring-layout patterns. Different mechanics have different natural layouts. Today's three presets are deck-builder/TCG-shaped.
Starter-deck sizing is hardcoded
$starterSoftCap = $totalCap / 10, $starterHardCap = $starterSoftCap * 1.2.
Reasonable defaults but not tunable. A Dominion-like game wants
~13 starter cards (10 copper + 3 estate) regardless of total volume;
a Star-Realms-like game wants exactly 10 (8 scouts + 2 vipers). The
1/10 ratio is a guess, not a design knob.
The `_auto` flag is invisible to the user
Subtype sub-distributions ship with _auto: true and the
sliders are hidden. Users don't know these sliders exist
until they stumble on the "Auto" toggle. This is a magic default
that's doing work nobody can see or question.
Sliders show percentages, not card counts
"50 % Units" on a 60-card deck means 30 Units to author. The UI doesn't show the 30. Designers think in cards, not percentages — the mental conversion is friction on every slider change.
No IA Suggestions — but the plumbing exists
Two data-method="deck-suggestion" links exist in the
source but are commented out. There's no
case 'deck-suggestion': in
ajax-ia-suggestions.php (we confirmed during an earlier
audit). Step 3 is the only wizard page with zero AI assistance,
despite being one of the hardest questions in game design —
"how should I organise and balance my ~80-card game?".
No delete or archive action in the existing-decks table
The DB has an is_archived column, the list query even
filters on it (WHERE is_archived = 0), but the UI has
no button to archive or delete a deck. Once created, decks are
effectively permanent via the UI.
"Parent Deck (coming soon)" — actually would help here
Parent/child decks aren't just UI nesting. In the Star Realms example, "Trade deck" could be the parent of "Machine Cult / Trade Federation / Blob / Star Empire / Generic" — expressing that all five are authoring sub-sections of a single conceptual pile. That also gives Step 4 a clear tree-of-decks to render and later Flow a natural way to say "the Trade deck (all children) becomes one gameplay zone". Less of a WIP placeholder than it looked at first — but still needs to actually ship.
Number of decks is implicit from the preset
The Star-Realms-like "4 faction decks + 1 generic deck" pattern needs 5 decks, which no preset produces directly. The user has to either pick a close preset and edit, or use "single" and manually add 5 decks via the existing-decks form. A first-class "how many decks and roughly what each represents?" question would land this.
Conceptual suggestions
1. Support hybrid preset layouts (Star Realms-style)
Today's presets are exclusive: all-by-family, all-by-type, or one pile. Add a hybrid / composable mode where the designer picks a pattern per section:
- "4 decks by family" + "1 generic deck" (Star Realms)
- "3 decks by type" + "1 starter per player" (TCG)
- "2 decks by family" + "1 shared utility deck" (drafted LCG)
The saved structure is still a flat list of decks — the preset just becomes a better wizard.
2. Ship parent/child decks (unlock authoring trees)
The parent_deck_id field already exists. Shipping it
gives:
- Step 4 an authoring tree: "Trade deck → Machine Cult / Trade Federation / Blob / Star Empire / Generic".
- A natural place to carry an eventual "at play time, all children merge into gameplay zone X" link — without adding that field to every deck.
- The Star Realms example a proper home.
3. Make presets mechanic-aware
Different mechanics have different natural authoring layouts. Load a per-mechanic preset catalog (parallel to the types split):
- Deck-builder: single core + (families) + starter
- TCG: per-colour / per-faction decks + starter per player
- Trick-taking: one 52-card deck
- Drafting: N packs as top-level authoring buckets
- Legacy / campaign: per-scenario decks, sealed reveal decks
assets/data/deck-presets/{mechanic}.json, same pattern
as types/{mechanic}.json.
4. Show card counts alongside percentages
Live label on every slider: "Units: 50 % → 30 cards to author". Designers think in cards, not percentages; translating in their head on every slider change is friction.
5. Expose the `_auto` flag visually
When a section is in auto mode, show a coach tip in place of the hidden sliders: "Auto-balanced — click to customise". Surfaces the fact that distribution exists and is doing work.
6. Add IA Suggestions at the deck level
The frontend plumbing exists (ia_resource_suggestions
handler, data-method="deck-suggestion" links commented
out). Add the server case: read briefing + Step 2 pieces, suggest a
deck layout ("for your Star-Realms-inspired game I'd
suggest 4 faction decks + 1 generic deck + 1 shared starter, sized
at…"). Single highest-value AI surface the wizard doesn't
yet have.
7. Allow "how many decks" as a first-class question
Before or alongside the preset, ask: "How many decks in your authoring layout?" with a stepper. Combined with suggestion #1 (hybrid presets), this covers the Star-Realms case natively.
8. Make starter-deck sizing configurable
Replace hardcoded totalCap / 10 with a user input (or
AI suggestion) defaulting to 10 %. Real starter decks are
size-sensitive (Dominion wants exactly 13 cards, Star Realms
exactly 10) and have nothing to do with a game's total card volume.
9. Document the design-deck vs gameplay-zone distinction
The two concepts are adjacent and confusing. A short explainer on this page ("these decks are authoring containers; how they become piles of cards during play is decided later") would save designers from assuming each deck equals a physical pile. Could be an info callout above the preset wizard.
Bugs & smells found during the read
- Line 630:
<?php custom_print_r($gameMeta); ?>— dumps the entire game_meta array on every page render, below the content. Flagrantly in production. - Line 136:
custom_print_r($dist);insidebuildDeckPresets()— dumps the distribution on every call. Happens during normal page rendering.
$_game_defaults['card-types']
Lines 37–43: the page reads assets/data/cardtypes-default-schema.json
(the JSON-Schema validator file!) and assigns it to
$_game_defaults['card-types'], overwriting the
properly-loaded catalog from inc-game-defaults.php.
loadCardTypeSchema() saves the day later by reading
from the global (which post-migration now comes from the split files),
but the local override is dead code leaving a landmine if anything
else on this page reads $_game_defaults['card-types']
between lines 43 and the first loadCardTypeSchema() call.
FILTER_SANITIZE_STRINGLine 27. Same PHP 8.1+ deprecation as on Step 1 / Step 2.
123Line 4. Same story as every other entry point.
$genres
Line 30 loads assets/data/genres.json into $genres.
Nothing on this page reads it. Copy-paste from Step 1.
TODOs
Checkbox state persists locally per page.
Bugs to fix
-
Remove
custom_print_rdebug dumpsLine 630 (page tail) and line 136 (insidebuildDeckPresets). Both render on every view. -
Delete the local override of
$_game_defaults['card-types']Lines 37–43 load the wrong file (the JSON-Schema validator). Either delete the block outright (the global is already populated) or fix to point at the correct source. -
Replace
FILTER_SANITIZE_STRINGLine 27. Whitelist regex orhtmlspecialcharson output. -
Remove the unused
$genresloadLine 30. Not used on this page.
Small UX / data fixes
-
Add delete/archive action on existing-decks tableThe
is_archivedcolumn exists and the query already filters on it. Just missing the button. -
Ship or cut "Parent Deck (coming soon)"Line 528. Third permanent WIP placeholder across the wizard.
-
Show card counts next to percentages on slidersLive "50% → 30 cards" label. Designers think in cards.
-
Expose the
_autoflag visually"Auto balanced — click to customise" note instead of hiding the whole section. -
Make starter-deck sizing configurableReplace hardcoded
totalCap / 10and* 1.2.
Bigger changes aligned with the goal
-
Support hybrid preset layouts (Star-Realms case)Let the wizard combine "by family" + "generic" + "starter" in one preset choice. Saved structure stays a flat list of decks.
-
Ship parent/child decksEnable the
parent_deck_idfield in the UI. Unlocks authoring trees in Step 4 and a natural later-binding to gameplay zones at play time. Retires one of the permanent WIP placeholders. -
Make presets mechanic-aware
assets/data/deck-presets/{mechanic}.json. Parallel to the per-mechanic types catalog. Non-deck-builder mechanics get authoring layouts that fit their shape. -
Add
deck-suggestionAI methodFrontend plumbing exists (commented out). Add the server case; propose a deck layout ("4 faction decks + 1 generic + 1 shared starter…") from briefing + Step 2 pieces. -
Allow "how many decks" as a free-form questionStepper or number input before the preset picker. Combined with hybrid presets, covers arbitrary layouts.
-
Document "design deck vs gameplay zone" in the UIA short info callout on the page. Avoids the mental conflation users (and analysis docs) fall into by default.
-
(Later) Define a setup step that maps design decks to gameplay zonesBelongs with Flow, not Step 3. Example: "the 4 faction design-decks + the generic design-deck all merge into the
trade_rowgameplay zone at play time". Captures the many-to-one relationship without bloating the deck entity.
Open questions for the product
- Is the preset catalog meant to be exhaustive (cover every sensible layout) or seed-only (user edits afterwards)? Today it's effectively seed-only but not labelled that way.
- Parent decks: ship or cut? If shipping, is it a pure authoring-layout concept (Step 4 tree nesting), or does it also carry semantics at play time (all children merge into one gameplay pile)?
- Should starter-deck sizing be driven by briefing data (complexity, play time) or by a first-class user input? Today's fixed 1/10 ratio probably doesn't serve either pattern.
- How should the page behave for a trick-taking or party game? The whole preset wizard assumes a composition can be sliced by family / type / subtype — which makes zero sense for a shared 52-card ranked deck.
- Does an authoring deck have to eventually map to a gameplay pile? Or are some decks purely authoring constructs (e.g., "Generic reference cards we draw names from") that never become physical piles? This affects whether the design-deck → gameplay-zone mapping is mandatory or optional at setup.