Overview  ›  Roadmap

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.

Phase 1: ~1 week Phase 2: ~3–4 weeks Phase 3: ~4–8 weeks (optional)

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.

Phase 1 first, no exceptions

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 — Stabilise ~1 week unblocks the rest ↓ PHASE 2 — Close the print loop ~3–4 weeks finishes the original product brief ↓ PHASE 3 — Simulator ~4–8 weeks adds design validation (optional)

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 cost to object shape on save
    Problem: 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 a normaliseCost() helper. If shape is array of numbers, map first scalar to the game's default resource (look up from game_parts where part_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 type from subtype on save
    Problem: AI returns "type": "unit__support" — a subtype slug stuffed into the type field. Schema wants type: "unit" + subtype: "unit__support" separately.
    Files: ajax-cards-autogenerate.php; the game_cards table 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 both card_json and the column values.
    Acceptance: every new row has either type only (root card) or both type + subtype populated; 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.php
    Problem: Lines 37–43 load cardtypes-default-schema.json (the JSON-Schema validator) and assign it to $_game_defaults['card-types'], overwriting the real catalog populated by inc-game-defaults.php.
    Files: index-decks.php.
    Approach: Delete the entire local-load block. The catalog is already loaded by include("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.php
    Problem: A live custom_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 flags vs behaviours duality
    Problem: Cards carry both a behaviours array (e.g. ["Permanent"]) and a flags map (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 (renderCardBlock derives flags from behaviours at display time).
    Approach: Pick behaviours as truth. Build flagsFromBehaviours($behaviours, $behLib) helper. Renderer reads behaviours, computes flags at render. Stop writing flags in the JSON envelope.
    Acceptance: new cards have no flags key; UI still shows the same flag labels as before.
  • 1.6 — Tidy _old / _backup sibling files
    Problem: The repo has index_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–5
    Approach: 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 in logfile.log that 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 spec
    What: 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 existing updateGameMeta().
    Acceptance: spec is captured, persisted, and read back on subsequent visits.
  • 2.2 — Card-back template
    What: 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), reuse inc-card-renderer.php as-is — a back is just another template.
    Storage: add game_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 engine
    What: 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.
    Acceptance: given 9 portrait PNGs and an A4 spec, returns a 3×3 sheet PNG with bleed and crops. Verified by eye, ideally printed once on paper to check alignment.
  • 2.4 — Multi-page PDF export
    What: 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 repeated addImage() + final writeImages($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 manifest
    What: 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 endpoint ajax-export-manifest.php or 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 as index-layout.php (include inc-before-content.php and inc-after-content.php, vendor scripts so the preloader dismisses).
    Nav: add "Next: Print & Export →" button at the bottom of index-layout.php; add "Print & Export" outline button to the bottom-of-page nav strip on index-decks.php too.
    Acceptance: a user can walk Step 5 → Step 6 → Download PDF without touching the URL bar.
  • 2.7 — Print one for real
    What: 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".
Don't start Phase 3 until Phase 2 ships

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):
    1. 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.
    2. 3.1b — Define the ending schema. Save / load via game_meta['ending'] using the shape from Appendix A.1. Each entry: {label, when, scope, terminates_at, resolution}. when is a JSON Logic predicate; resolution.metric (when scoring) is a JSON Logic numeric expression.
    3. 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.
    4. 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.
    v1 scope: ship the six recipes from Appendix A.9 v1 list. Defer v1.5 (cooperative 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.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: migrate game_parts (where part_type='resource') and game_meta['life_points'] into a unified game_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-readable
    Read 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_zones blob.

    What: Replace today's descriptive game_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 new description field instead of free text); the simulator's setup() uses the new setup.start_filled_from.
    v1 scope: the 10 recipes from C.5, the four scopes / five visibilities / five orderings from C.2, and the three auto_refill.when_empty policies 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 (the apply() 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's setup() can build a complete initial Game state from game_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 trigger to card effects
    What: Each effect on a card gains a trigger field (on_play default, also at_start_of_turn, on_destroy, at_end_of_turn).
    Files: card generator prompt (ajax-cards-autogenerate.php); effects.json may need a default trigger per effect.
    Acceptance: every effect on a new card has a trigger.
  • 3.5 — Define the action vocabulary
    What: 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 in game_meta['enabled_actions'].
    Acceptance: 5–7 canonical actions exist; a deck-builder game enables a known subset.
  • 3.6 — Phase definitions
    What: 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 spec
    What: 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 object
    What: 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): Game returns a fully-populated initial Game.
  • 3.9 — apply() reducer
    What: 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's allowed_actions, filters by precondition (legal targets, sufficient resources), returns the list.
    Files: same engine file as apply().
    Acceptance: at any point in any game, returns at least one action (even if just pass / end_phase).
  • 3.11 — Random bot + game runner
    What: A bot interface BotController::choose($view, $legal): $action with a uniform-random implementation. A runner loop that calls apply until $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 + reports
    What: 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?), a php-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 history log 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

  1. Start at Phase 1 step 1.1. Don't skip ahead; don't parallelise across phases.
  2. 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.
  3. 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.
  4. 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).
A loose definition of "done"

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.