Roadmap to a finished designer
Three phases, in order, that take DeckCraft from "polished card editor" to "shippable card-game design tool" to "design tool with playtesting built in". Each phase is broken into concrete steps with files, acceptance criteria, and checkboxes. Tick boxes as you complete; they persist in your browser via the same mechanism as the per-page TODO lists.
Why this document exists
The wizard works end to end today: brief → pieces → decks → cards → rendered card PNGs. That's a lot. But the original product brief was "a printable, customised board game in a box" — not "a tool that produces card images". Between today's state and that goal sit two chapters: the print pipeline (turn rendered cards into a printer-ready PDF) and, optionally, the simulator (validate the design works before you spend money printing it).
This roadmap orders the work so each phase delivers something shippable on its own, and earlier phases unblock later ones without forcing rewrites.
The known bugs and inconsistencies in Phase 1 are tiny, but starting Phase 2 or 3 on top of them means writing defensive code in three places to work around the same upstream problem. Spending a week stabilising buys back weeks across everything that follows.
The three phases
Phase 1 + Phase 2 = a designer that ships printable PDFs. That's the minimum to call this "finished". Phase 3 is an enrichment that moves DeckCraft from tool to design environment; valuable, but not required to declare victory.
Phase 1 — Stabilise
- Goal
- Kill all known bugs and inconsistencies so later phases sit on a clean base.
- Estimate
- ~1 week, full focus.
- Prerequisites
- None — this is the start.
- Deliverable
- Walking a new game through Steps 1–5 produces no warnings, no broken data shapes, no debug spam.
- What it unlocks
- Confidence to refactor and add features without hidden landmines.
Steps
-
1.1 — Normalise card
costto object shape on saveProblem: AI sometimes returns"cost": [2](array) instead of"cost": {"resource_id": 2}. The renderer handles it defensively but downstream code can't reason about the resource without normalisation.
Files:ajax-cards-autogenerate.php(around the GPT response handling, before INSERT/UPDATE).
Approach: Add anormaliseCost()helper. If shape is array of numbers, map first scalar to the game's default resource (look up fromgame_partswherepart_type='resource').
Acceptance: every card written from this point forward has cost as{resource_id: amount}; no array-shape costs in new rows. Optional one-off backfill script for existing rows. -
1.2 — Split
typefromsubtypeon saveProblem: AI returns"type": "unit__support"— a subtype slug stuffed into the type field. Schema wantstype: "unit"+subtype: "unit__support"separately.
Files:ajax-cards-autogenerate.php; thegame_cardstable already has both columns (card_type,card_subtype) — just populate them correctly.
Approach: If incoming type contains__, split: root = before, subtype = full. If no__, type = root, subtype = NULL. Update bothcard_jsonand the column values.
Acceptance: every new row has eithertypeonly (root card) or bothtype+subtypepopulated; the renderer's defensive__-splitting becomes dead code (leave it for backwards-compat, but it stops firing). -
1.3 — Fix wrong-file load in
index-decks.phpProblem: Lines 37–43 loadcardtypes-default-schema.json(the JSON-Schema validator) and assign it to$_game_defaults['card-types'], overwriting the real catalog populated byinc-game-defaults.php.
Files:index-decks.php.
Approach: Delete the entire local-load block. The catalog is already loaded byinclude("php-includes/inc-game-defaults.php")a few lines earlier.
Acceptance:$_game_defaults['card-types']contains the actual mechanic-scoped catalog; the decks page renders without the schema-vs-catalog confusion. -
1.4 — Remove debug dumps from
index-decks.phpProblem: A livecustom_print_r($gameMeta)call still renders on the page in production.
Files:index-decks.php(around line 630, just before the closing div).
Approach: Delete the call. If it's useful for debugging, gate it behind?debug=1.
Acceptance: page renders cleanly with no JSON dumps visible to the user. -
1.5 — Resolve
flagsvsbehavioursdualityProblem: Cards carry both abehavioursarray (e.g.["Permanent"]) and aflagsmap (e.g.{permanent: true}). The flags are derivable from behaviours but kept in sync in two places — classic source-of-truth bug waiting to happen.
Files:ajax-cards-autogenerate.php(stop emitting flags),php-includes/inc-functions-final.php(renderCardBlockderives flags from behaviours at display time).
Approach: Pickbehavioursas truth. BuildflagsFromBehaviours($behaviours, $behLib)helper. Renderer reads behaviours, computes flags at render. Stop writingflagsin the JSON envelope.
Acceptance: new cards have noflagskey; UI still shows the same flag labels as before. -
1.6 — Tidy
_old/_backupsibling filesProblem: The repo hasindex_old.php,index_old2.php,index-step2_old.php,index-decks_old.php,index-decks_backup_with_toggle.php,index-cards_old.php,ajax-cards-autogenerate_backup.php. They clutter searches and risk being edited by mistake.
Approach: Move all to an_archive/folder, or delete (git history preserves them). Pick one path, do it consistently.
Acceptance: root folder contains only live entry points. -
1.7 — Smoke test: walk a fresh game through Steps 1–5Approach: Create a brand-new game from
index-games.php. Fill the briefing. Add pieces. Create a deck. Generate 3 cards. Generate art for each. Render each. End the run on Step 5.
Watch for: any new console errors, any ajax failures, any visual glitches, anything inlogfile.logthat wasn't there before.
Acceptance: end-to-end run completes with no errors and matches the documented behaviour. Any surprises caught here become Phase 1.x sub-tasks before moving to Phase 2.
Phase 2 — Close the print loop
- Goal
- Take rendered card PNGs all the way to a printer-ready PDF.
- Estimate
- ~3–4 weeks.
- Prerequisites
- Phase 1 complete. Imagick available (already true on the server).
- Deliverable
- A new Step 6 (
index-print.php) where the user picks print spec, previews the imposed sheet, and downloads a multi-page PDF with bleed, crop marks, fronts and backs. - What it unlocks
- The original product goal: a designer that produces a real, physical, customised board game.
Steps
-
2.1 — Capture the print specWhat: Per-game settings the printer needs. Fields:
card_size(poker 63×88mm / bridge 56×88mm / mini-euro 45×68mm / custom),bleed_mm(default 3mm),copies_per_card(usually 1, can vary per-deck),language,box_format(tuck box / two-piece / none),paper_size(A4 / Letter / SRA3).
Files: add a section on Step 5 (index-layout.php) or kick this into the new Step 6 page from the start.
Storage:game_meta['print_spec']via existingupdateGameMeta().
Acceptance: spec is captured, persisted, and read back on subsequent visits. -
2.2 — Card-back templateWhat: A new template type for card backs. One back per deck (or one shared across the game). Different slot shape: deck name, deck-specific art, faction badge — no per-card data.
Files:assets/data/templates/card_back_v1.json(new), reuseinc-card-renderer.phpas-is — a back is just another template.
Storage: addgame_meta['card_layout']['back_template_by_deck']so each deck can pick a back. Default applies if not set.
Approach: The render-model builder needs a "back face" mode that produces a deck-keyed model instead of a card-keyed one (deck name, deck art). Same engine call, different model.
Acceptance: any deck can render a back face PNG. -
2.3 — Imposition engineWhat: Given N rendered card PNGs and a sheet spec (paper size, card size, bleed, gutters), compose a sheet PNG with cards laid out in a grid plus crop marks at the corners. This is where Imagick really earns its keep.
Files:php-includes/inc-print-imposition.php(new).
Functions:computeSheetLayout($paperMm, $cardMm, $bleedMm, $gutterMm)— returns rows, cols, x/y offsets.composeSheet($cardPaths[], $sheetSpec): Imagick— lays out the cards on a blank canvas at print DPI (usually 300).drawCropMarks($canvas, $layout)— small ticks at card corners.
-
2.4 — Multi-page PDF exportWhat: Combine front sheets + back sheets (back sheets must mirror horizontally so the back of card X aligns with its front when the page is flipped) into a single PDF.
Files:ajax-export-pdf.php(new), thin wrapper around the imposition engine.
Approach: Imagick supports multi-page PDF via repeatedaddImage()+ finalwriteImages($path, true). Pages alternate: page 1 = fronts of deck A, page 2 = backs of deck A (mirrored), page 3 = fronts of deck A continued, etc. Per-deck pagination so the printer can cut by deck.
Acceptance: downloaded PDF opens in Acrobat / Preview, has correct page count, fronts and backs align when paper is flipped horizontally. -
2.5 — Deck manifestWhat: A separate downloadable file (CSV or JSON) listing each card, its deck, and the copy count. Useful for the printer to verify quantities and for the user to keep a record.
Files: small endpointajax-export-manifest.phpor a new mode on the export endpoint.
Acceptance: CSV with columns (deck, card_name, card_type, copies) downloads correctly. -
2.6 — Step 6 page (
index-print.php)What: The user's home for this phase. Sections: print-spec form (if not captured earlier), "Generate sample sheet" preview button, "Generate full PDF" download button, "Download manifest" download button.
Files:index-print.php(new), follow the same chrome pattern asindex-layout.php(includeinc-before-content.phpandinc-after-content.php, vendor scripts so the preloader dismisses).
Nav: add "Next: Print & Export →" button at the bottom ofindex-layout.php; add "Print & Export" outline button to the bottom-of-page nav strip onindex-decks.phptoo.
Acceptance: a user can walk Step 5 → Step 6 → Download PDF without touching the URL bar. -
2.7 — Print one for realWhat: Send the generated PDF to a real print shop (or print on a colour laser at home). Verify: bleed survives, crops align, fronts and backs line up after flipping, deck distinguishability holds.
Acceptance: a stack of physical cards that look like cards. This is the moment the designer is "finished".
Phase 3 — Simulator (optional)
- Goal
- Add design validation: run a designed game N times with random or simple bots, surface dead cards, runaway loops, unreachable win conditions.
- Estimate
- ~4–8 weeks for a useful MVP.
- Prerequisites
- Phase 1 complete. Phase 2 nice-to-have but not required — the simulator works on the data, not the print artifact.
- Deliverable
- A "Run 1,000 simulations" button that produces a report of game length, win rates, card play frequency.
- What it unlocks
- The shift from "I designed a game" to "I designed a game I'm confident is playable".
A printable but unvalidated designer is more useful than a validated designer that can't print. The print loop closes the original brief. The simulator extends it.
Steps
-
3.1 — Structure win and loss conditions (evaluator-first)Implementation plan: Plan: Step 2 rebuild — section S.4 covers the UI integration. The evaluator (3.1a) is a prerequisite; the form work happens in the unified Step 2 rebuild.
Schema spec: Appendix A — Win conditions in the game-model doc. Sections A.0 (architectural principle: evaluator first), A.4 (operator vocabulary), and A.7 (worked examples in JSON Logic) are the load-bearing parts. The appendix is the spec; what's below is just the build order.
Sub-steps (do in order):-
3.1a — Install JSON Logic +
inc-game-expressions.php.composer require jwadhams/json-logic-php. Build a wrapper class (GameExpressions) that registers our custom operators (count_cards,count_players_where,for_each_player,lookup_player,objective_count) on top of standard JSON Logic. Pure function: takes (expression, game, context) and returns boolean / number. No DB, no HTTP. Unit-testable directly. -
3.1b — Define the
endingschema. Save / load viagame_meta['ending']using the shape from Appendix A.1. Each entry:{label, when, scope, terminates_at, resolution}.whenis a JSON Logic predicate;resolution.metric(when scoring) is a JSON Logic numeric expression. -
3.1c — Win Conditions page.
A new
index-win-conditions.php(or section on Step 2) with six recipe forms. Each form generates the JSON Logic expression and stores it. Show natural-language preview so the designer can sanity-check. - 3.1d — Step 1 win_shape dropdown. A single field on the briefing capturing the designer's intent in coarse form. Six options (combat, race_to_n, most_at_end, multi_source_score, weighted_score, last_standing). The Win Conditions page later turns this into structured ending data.
mission,boss_defeat) and v2 (team_victory,hidden_objective) until a designed game in those genres demands them.
Why first: the evaluator built here is the substrate for legality predicates (Phase 3.10), targeting filters, modifier conditions, and triggered effects. Building it once unlocks all of them.
Acceptance: every game has at least one structured trigger + resolution stored as JSON Logic;GameExpressions::evaluate()returns the right answer for sample expressions against a stub Game state; the natural-language preview on the form matches the schema produced. -
3.1a — Install JSON Logic +
-
3.2 — Unify trackers (resources + life + score + status)Implementation plan: Plan: Step 2 rebuild — section S.1. Foundation (F.0a–F.0d) ships first; this section is the first real feature on top of it.
Schema spec: Appendix B — Trackers in the game-model doc. It defines the two-dimensional model (type × role), the four value types (counter / boolean / enum / set), the six roles, the full schema shape, six recipe templates, and the migration plan from today's split storage.
What: One concept ("tracker") for all per-player and shared state with a value. Each tracker has{id, label, type, role, scope, visibility, starts_at | starts_value | starts_set, ...}.
Files: migrategame_parts(wherepart_type='resource') andgame_meta['life_points']into a unifiedgame_meta['trackers']store. Update Step 2 UI to replace the Resources + Life subsections with a single Trackers section using the recipe-based add form from Appendix B.6.
v1 scope: the four types and six recipes from Appendix B.6 + B.9. Defer derived trackers (B.7) to v1.5 unless Catan-style scoring is on the roadmap immediately. Defer per-team trackers and hidden visibility to v2.
Why it matters: the evaluator from step 3.1 already speaks{"var": "player.trackers.X"}. Without unified trackers, every win-condition recipe special-cases "is this in game_parts or game_meta?". Unifying first means the recipes don't need to know.
Acceptance: Step 2 has one section for "Trackers" instead of separate "Resources" and "Life". Existing games migrate cleanly. JSON Logic expressions like{"<=":[{"var":"player.trackers.authority"},0]}resolve correctly. -
3.3 — Promote zones to engine-readableRead this first: Appendix C — Zones in the game-model doc. It defines the dimensions (scope, visibility, ordering, constraints, auto-refill, reset, setup), the schema shape, 10 recipe templates covering most games, and the migration from today's free-text
game_zonesblob.
What: Replace today's descriptivegame_meta['game_zones']blob with the structured shape from Appendix C.3. Each zone gets{id, label, scope, visibility, ordering, constraints, auto_refill, reset, setup}.
Files: Step 2 zones editor (recipe picker + per-recipe form following Appendix C.5);game_meta['game_zones']shape; AI prompt context (read the newdescriptionfield instead of free text); the simulator'ssetup()uses the newsetup.start_filled_from.
v1 scope: the 10 recipes from C.5, the four scopes / five visibilities / five orderings from C.2, and the threeauto_refill.when_emptypolicies from C.6. Defer multi-zone selectors, hand-passing, per-card visibility / ownership in shared zones (all C.6 stretches) to v1.5.
Why it matters: every action handler in step 3.9 (theapply()reducer) is a state mutation against a zone — play_card = hand → in_play, buy = trade_row → discard, etc. With zones engine-readable, each handler is a 5-line mutation against a known shape; without, every handler reinvents that logic.
Acceptance: the simulator'ssetup()can build a complete initial Game state fromgame_meta['game_zones']+ the trackers from step 3.2.{"count_cards":["hand","player"]}in JSON Logic expressions returns the right number. Visibility honoured by the view-redaction layer later. -
3.4 — Add
triggerto card effectsWhat: Each effect on a card gains atriggerfield (on_playdefault, alsoat_start_of_turn,on_destroy,at_end_of_turn).
Files: card generator prompt (ajax-cards-autogenerate.php);effects.jsonmay need a default trigger per effect.
Acceptance: every effect on a new card has a trigger. -
3.5 — Define the action vocabularyWhat: A catalog of actions a player can take:
play_card,buy_from_market,attack_opponent,activate_ability,pass,end_phase. Each has preconditions, costs, and effects.
Files:assets/data/actions-default.json(new catalog); per-game enabled set ingame_meta['enabled_actions'].
Acceptance: 5–7 canonical actions exist; a deck-builder game enables a known subset. -
3.6 — Phase definitionsWhat: Each game has phases: a list of
{ id, label, automatic, allowed_actions, mandatory_actions, repeat }.
Files:game_meta['phases']; UI on Step 2 (or new Step 2.5) for phase editing; pre-canned phase templates per mechanic ("standard deck-builder turn", "trick-taking round").
Acceptance: every game has a phase list; default templates available per mechanic. -
3.7 — Setup specWhat: How a game starts:
{ starting_hand_size, initial_trackers, deck_to_zone_map, turn_order_rule }.
Files:game_meta['setup_spec']with a small UI on Step 2 or 3.
Acceptance: simulator can deterministically build the initial state from this spec + a random seed. -
3.8 — The Game objectWhat: The runtime state structure detailed in page-game-model.html#game-object: identity, config, players, shared_zones, shared_trackers, card_instances, flow, history, RNG.
Files:php-includes/inc-simulator-state.php(new). A PHP class or a documented array-shape, your choice.
Acceptance:setup($definitionId, $seed): Gamereturns a fully-populated initial Game. -
3.9 —
apply()reducerWhat:apply($game, $action): $game. Switch on$action['type']. Each branch validates, records to history, mutates trackers / zones / card locations, runs the action's effects, advances flow, checks end conditions.
Files:php-includes/inc-simulator-engine.php(new).
Acceptance: applying a sequence of legal actions to a fresh Game produces a valid end-state. -
3.10 —
legal_actions()What:legal_actions($game, $playerId): action[]. Walks the current phase'sallowed_actions, filters by precondition (legal targets, sufficient resources), returns the list.
Files: same engine file asapply().
Acceptance: at any point in any game, returns at least one action (even if justpass/end_phase). -
3.11 — Random bot + game runnerWhat: A bot interface
BotController::choose($view, $legal): $actionwith a uniform-random implementation. A runner loop that callsapplyuntil$game.status !== 'in_progress'.
Files:php-includes/inc-simulator-bot.php,php-includes/inc-simulator-runner.php.
Acceptance: one game runs from setup to a recorded winner without errors. -
3.12 — Harness + reportsWhat: Run N games, aggregate metrics: average game length (in turns), win rate by going first, per-card play frequency, never-played cards, infinite-loop detection. Output as an HTML report.
Files:index-simulator.php(new Step 7?), aphp-includes/inc-simulator-report.php.
Acceptance: "Run 1,000 sims" button produces a report with the metrics above. The report names dead cards explicitly so the designer can act on them. -
3.13 — Replay viewer (nice-to-have)What: Step through the recorded
historylog of any single sim run to see exactly what happened.
Files: a small page that reads a sim's log and replays the state forward, showing zones and trackers at each step.
Acceptance: any saved sim can be inspected turn-by-turn. Indispensable for debugging weird outcomes.
How to use this document
- Start at Phase 1 step 1.1. Don't skip ahead; don't parallelise across phases.
- Check the box when a step is genuinely done — i.e. acceptance criteria met, not just "code written". Boxes persist in your browser; if you refresh you keep your state.
- Treat each step's acceptance line as a contract. If a fix changes downstream code in unexpected ways, surface that as a follow-up task before moving on.
- Re-evaluate after each phase. Phase 1 is non-negotiable. Phase 2 is the recommendation. Phase 3 is optional and depends on whether design validation matters more to you than other features (real users, multi-game management, art-direction polish).
Phase 2 step 2.7 (print one for real) is the moment to call this designer finished. Everything before it is building toward that; everything after it is enrichment. Holding a stack of physical cards you designed in your own tool is the only signal that matters.