Skip to content

Drop Deck (Game 001) — implementation PRD #12

Description

@DanielBonaker

Drop Deck — PRD (Game 001)

Field Value
Slug 001-drop-deck
Date 2026-05-05
Source spec docs/superpowers/specs/2026-05-04-drop-deck-design.md
Source bestiary docs/franchise/bestiary.jsx + docs/franchise/franchise-design.md
Status Ready for /to-issues (post grill-me, post deep-module sketch)

Problem Statement

I'm building 100g — a 100-day challenge to ship a different mini-game every day at https://danielbonaker.github.io/100g/. Before any games can ship, I need a vertical slice that proves the engine and shared services contract works end-to-end. Drop Deck is that vertical slice: it's the first daily game, and it must exercise every cross-game system (engine, persistence, economy, achievements, input, audio, the shared shape bestiary) so that games 002 through 100 build on a tested foundation rather than re-inventing infrastructure each time.

The design has been specified and grilled. What I need now is implementation: cross-game prerequisites, then the Drop Deck game itself, all under TDD per CLAUDE.md's iron law (no production code without a failing test first). The implementation must respect the project's DEEP MODULES contract — every public surface should be small, every behavior should be deep and testable in isolation.

Solution

Drop Deck is a mobile-first roguelike-deckbuilder block-drop puzzle.

The player drops creature-shaped blocks (sourced from the cross-100g bestiary, 167 hand-named shapes graded across 9 size tiers from Keim to Absolut) onto an 8-column-by-16-row board. There is no rotation — neither gesture nor button — so block geometry is fully determined by the bestiary entry, and the player's only placement choice is the column. Each block also carries one of five effects (Standard, Ghost, Melt, Impact, Rain), giving five-fold variety on the same shape. Blocks come from the player's deck; between rounds a shop offers boosters by tier, deck removals, and run-permanent passives. Runs are endless and harden round-by-round through a garbage-row cadence; runs end only on top-out.

At run-end the game emits three achievement signals on mixed axes (clear-100-rows, reach-round-10, end-with-20-blocks-in-deck) and a cross-game currency yield with per-component caps capped at 300 per run.

The implementation produces three layers: cross-game prerequisites (engine, shared shape bestiary, persistence/economy/achievements/input/audio services), Drop Deck's pure-logic domain layer, and a Pixi-based rendering / controller integration layer.

User Stories

A. Core gameplay loop — placement, commit, top-out

  1. As a mobile player, I want to position a block by drag-holding anywhere on the board and tapping to commit, so that I can play with one thumb without complex gestures.
  2. As a player, I want my block to never require rotation — neither by gesture nor by button — so that the controls stay simple and the catalog is the only source of orientation variety.
  3. As a player, I want a ghost-preview of where my block will land after the effect resolves, so that I can plan my move before committing rather than guessing.
  4. As a player, I want to release my finger without committing, so that I can re-aim before tapping.
  5. As a player, I want a dedicated 44×44 minimum hold-swap button outside the board area, so that I never accidentally swap when dragging.
  6. As a player, I want one hold swap per active block (Tetris-classic rule), so that hold is a meaningful choice rather than a free do-over.
  7. As a player, I want to see the next two upcoming blocks (or three with the Foresight passive), so that I can plan two turns ahead.
  8. As a player, I want the board to be 8 columns by 16 rows in mobile-portrait viewport, so that the play area fits a 375×667 phone screen.
  9. As a player, I want top-out to be the only failure mode — endless runs, no win state — so that the experience is roguelike chase-the-best-run.
  10. As a player, I want both top-out trigger paths (a new block can't fit in the spawn cells, AND a garbage shift would push a cell above row 0) to produce the same end-of-run experience, so that the game feels consistent.

B. The five effects

  1. As a player, when I commit a Standard block, I want it to land at first cell-collision rigidly, so that placement is predictable.
  2. As a player, when I commit a Ghost block, I want it to find the deepest row where the rigid block fits in empty cells (passing through above-stack obstructions), so that I can wedge under overhangs that Standard can't reach.
  3. As a player, I want Ghost to remain a rigid block (not disassemble), so that the block's shape continues to matter.
  4. As a player, when I commit a Melt block, I want it to disintegrate and water-fill the flood-fill-reachable cavity below my drop column, so that I can fill wide horizontal gaps without precise placement.
  5. As a player, I want Melt's water to respect walls — sealed cavities are unreachable, buckets hold water, tunnels fill horizontally — so that the effect feels like real water finding its level.
  6. As a player, I want Melt's overflow (when more cells than the cavity can hold) to stack above the cavity using the same outward-from-drop-column rule, so that the water rises predictably.
  7. As a player, when I commit an Impact block, I want all cells in the block's bounding-box-plus-one-cell aura to be cleared, so that I can break stuck stacks.
  8. As a player, I want Impact's aura to scale with block size (a 1×1 clears a 3×3 area, a 2×2 clears 4×4, a 4×4 clears 6×6), so that bigger blocks make bigger explosions.
  9. As a player, when I commit a Rain block, I want each cell to fall to its own column's lowest empty independently, so that I can distribute cells across multiple columns simultaneously.
  10. As a player, I want Rain's per-column cells to stack from the lowest empty upward when the block has multiple cells in the same column, so that the per-column rule is consistent.

C. Bestiary — the cross-100g shape source of truth

  1. As a player, I want each block to be a creature shape with a German name from the cross-100g bestiary (Schwert, Drache, Phönix, Banshee, Vollkommen, etc.), so that the game has thematic identity and shared IP.
  2. As a player, I want diagonally-connected (king-adjacency) shapes to be allowed, so that the bestiary's full 167 shape pool is usable and the puzzle has more variety than classical polyominoes.
  3. As a player, I want shapes never to rotate or mirror at runtime — every appearance of a creature is its canonical bestiary orientation — so that I learn each creature's footprint through repeated play.
  4. As the project owner, I want Drop Deck to consume the cross-100g bestiary as read-only data, so that future games sharing creature geometry stay consistent.
  5. As the project owner, I want the bestiary geometry extracted to TypeScript from its current JSX form, so that all games (and the bestiary viewer) import strongly-typed shape data from a single source.

D. Shop and economy

  1. As a player, I want a shop visit between every round (skippable), so that I can shape my deck deliberately.
  2. As a player, I want gold from three sources during a round — per-row trickle, round-clear lump-sum, end-of-round interest — so that there are multiple income streams.
  3. As a player, I want to buy Small ($2), Medium ($5), or Large ($10) boosters that offer 3 random blocks of which I pick 1 (or skip), so that booster purchases are tier-graded gambles.
  4. As a player, I want booster offers to weight effects by tier (Standard most common in Small, Impact rarest), so that bigger boosters offer more variety and bigger creatures.
  5. As a player, I want booster tier to map to bestiary creature size (Small → Keim/Bund/Funke, Medium → Gestalt/Wesen, Large → Titan/Apex/Archon/Absolut), so that bigger boosters feel like bigger creatures.
  6. As a player, I want to remove an unwanted block from my deck for a fixed cost (3 random offered), so that I can prune.
  7. As a player, I want a 50% chance of seeing a Passive offer in the shop, so that passives feel earned rather than guaranteed.
  8. As a player, I want passives to be run-permanent and capped at one of each per run, so that they feel meaningful but don't stack to break the game.
  9. As a player, I want 9 passives across 3 categories (Currency-Boost, Deck-Convenience, Difficulty-Reducer), so that there's variety in how I customize.
  10. As a player, I want NO effect-amplifier passives, so that effects stand on their own without complex upgrade trees.
  11. As a player, I want my deck size capped at 5 minimum and 20 maximum, so that the deck is meaningful in size but not unboundedly large.

E. Achievements and cross-game progression

  1. As a player, I want three achievements on mixed axes (Centurion = clear 100 rows in one run, Long Haul = reach round 10, Full Hand = end with a 20-block deck), so that I can pursue different play styles each run.
  2. As a player, I want a cross-game currency yield at run-end, formula min(rows_cleared/5, 200) + min(highest_round - 1, 100) capped at 300, so that long careful runs are rewarded without infinite scaling.
  3. As a player, I want achievement unlocks to fire once per run only, so that completing a threshold doesn't spam events.
  4. As a player, I want my best-run stats (highest round, total rows ever) saved forever, so that I can chase records across sessions.

F. Persistence and mobile resilience

  1. As a mobile player, I want my run to auto-save after every block I commit (with a small debounce to coalesce rapid commits), so that an interrupted phone session — incoming call, OS sleep, tab kill — doesn't lose mid-round progress.
  2. As a player, I want to pick up exactly where I left off, including the upcoming-blocks queue and shuffled deck order, so that resume is deterministic.
  3. As a player, I want cross-game currency and unlocked achievements to persist across runs and sessions, so that progress carries forward.

G. Mobile-first interaction

  1. As a mobile player, I want all interactive UI elements at minimum 44×44 pixels, so that touch targets are reachable.
  2. As a mobile player, I want audio to start muted and require a tap to enable, so that the game respects browser autoplay rules and doesn't blast sound on accidental opens.
  3. As a mobile player, I want no rotation gestures on the board, so that the game doesn't conflict with my system swipe gestures.
  4. As a player, I want the active block clamped so its full bounding box fits within the board's 8 columns, so that I can never aim a block off-screen.

H. Cross-game architecture (developer-as-platform-owner)

  1. As the project owner, I want every game to implement a single Game lifecycle contract (init / update / render / teardown plus a manifest), so that the platform shell can mount games uniformly.
  2. As the project owner, I want each cross-game service (persistence, economy, achievements, input, audio) to be a deep module with a small interface, so that games consume them through stable contracts.
  3. As the project owner, I want Drop Deck's domain layer to contain zero Pixi imports and zero DOM access, so that game logic is fully unit-testable in pure node.
  4. As the project owner, I want every domain function in Drop Deck implemented test-first per CLAUDE.md TDD invariant, so that the code is reliable from day one.
  5. As the project owner, I want no franchise-trademark names anywhere in code, paths, commits, docs, UI strings, or assets (per Anti-IP rules), so that the project stays legally clean.
  6. As the project owner, I want no new runtime dependencies introduced by Drop Deck (Pixi is already in package.json), so that bundle size stays controlled.
  7. As the project owner, I want TypeScript strict mode with noUncheckedIndexedAccess, so that off-by-one and nullish access bugs are caught at compile time.
  8. As the project owner, I want the engine layer's Game contract to be the FIRST module shipped, so that subsequent games (002, 003, ...) can be onboarded without renegotiating the contract.
  9. As the project owner, I want Drop Deck to be the proving ground for the cross-game services — if a service interface is wrong here, fix it before shipping any other game.

Implementation Decisions

Architecture

  • Three-layer split. Layer A: cross-game prerequisites (the Game lifecycle contract; the shared shape bestiary; persistence, economy, achievements, input, audio services). Layer B: Drop Deck's pure domain (board, effects, deck, garbage cadence, shop, economy, round, run-state reducer). Layer C: Drop Deck's integration (Pixi rendering, input controller, catalog, manifest).
  • Layer A is a hard prerequisite. Drop Deck cannot ship until the Game contract and all five services are in place. The bestiary TypeScript extraction is also a Layer A prerequisite.
  • Pure domain. Layer B contains zero Pixi imports and zero DOM access. All game logic is exercised through plain reducers over typed structures.
  • DEEP MODULES contract. Every service and every domain module exports a small interface with deep behavior. A change is "wide and shallow" if it adds many surface methods; the response is to refuse such changes and propose collapsing them into one deeper method first.

The five effects (final algorithms)

  • Standard. Block lands at the targeted column at first cell-collision (rigid, identical to classical block-drop placement).
  • Ghost. Block lands at the deepest row where the entire rigid block fits in empty cells, ignoring above-stack obstructions. May NOT reach the floor when columns disagree on stack heights — that's the cost of staying rigid.
  • Melt. Block disintegrates on landing. Cell count N is poured from the drop column top. Flood-fill from drop column top through empty cells (existing block cells = walls) determines the reachable cavity. Cavity fills bottom-up: at each row, cells alternate outward from the drop column (left-bias on tie). When a row of the reachable region is full, overflow stacks at the next row above using the same outward rule. Sealed (encapsulated) cavities are unreachable — water cannot pass through walls. If N exceeds the reachable region's capacity, overflow rises above the cavity's natural top using the same rule.
  • Impact. Block lands as Standard. Then all cells in the block's bounding box, expanded by 1 cell in every direction (the aura), are cleared. The aura scales with block size: a 1×1 clears a 3×3, a 2×2 clears a 4×4, a 1×4 clears a 3×6, a 4×4 clears a 6×6. Cleared cells trigger row-clear checks normally. Catalog curation prevents oversized Impact blocks from existing in the catalog (no Impact-on-4×4-brick).
  • Rain. Block disassembles on landing. Each cell falls to its own column's lowest empty cell, independently. When a block has multiple cells in the same column, they stack from the lowest empty upward.

Effect resolution order on a single landing

  1. Position block per its effect rule (Standard, Ghost, Melt, Rain).
  2. Apply Impact aura clear (Impact only).
  3. Run row-clear check.
  4. Increment garbage-cadence counter.
  5. Check top-out for next-spawn.

Shape catalog

  • Drop Deck draws ~28–30 creatures from the cross-100g bestiary's 167-shape pool.
  • 3×3 bounding box hard cap; 9-cell maximum (was previously 4×4 / 16 cells in pre-grill spec).
  • King-graph connectivity allowed (cells may be diagonally connected).
  • Bestiary German names are used directly in UI; block IDs follow <tier>-<name-slug>.
  • Total catalog: ~28–30 shapes × 5 effects = ~140–150 unique block definitions.
  • Booster-tier mapping: Small ($2) draws from Keim+Bund+Funke (1–3 cells); Medium ($5) draws from Gestalt+Wesen (4–5 cells); Large ($10) draws from Titan+Apex+Archon+Absolut (6–9 cells).

Persistence

  • Save the active run snapshot after every committed block, with a 50ms debounce to coalesce rapid commits within the same window.
  • Save best-run stats and cross-game currency at run-end.
  • Save achievement unlocks at unlock event.
  • All persistence goes through the cross-game persistence service (IndexedDB; no server, per ADR 0001).
  • RNG state is included in the run snapshot so resumes are deterministic.

Currency yield formula

  • Run-end yield = min(floor(cleared_rows_this_run / 5), 200) + min(highest_round_reached - 1, 100).
  • Hard cap: 300 per run.
  • Yielded to the cross-game economy service.

Achievements

  • Three IDs on mixed axes: rows-100 (Centurion: clear 100 rows in one run), round-10 (Long Haul: reach round 10), deck-20 (Full Hand: end a run with a 20-block deck).
  • Each unlocks at most once per run.
  • Yielded to the cross-game achievements service.

Passives (9 total, no effect-amplifier category)

  • Currency-Boost (3): Penny Pincher, Compound Interest, Round Bonus+.
  • Deck-Convenience (3): Spare Pocket, Foresight, Warm Start.
  • Difficulty-Reducer (3): Slow Pollution, Foundation, Skipper's Bonus.
  • Max one of each per run.

State machine

  • Three top-level states: InRound, InShop, RunEnded.
  • Triggers: Round-Cleared (rows cleared meets target), Top-Out (spawn collision OR garbage shift past row 0), Shop-Exit (player taps Continue).
  • Both top-out trigger paths produce identical RunEnded state — same final stats, same achievements, same currency yield. Worth a focused parity test.

Service interfaces (cross-game)

  • Persistence: save(key, value, opts), load(key), delete(key); opts include debounce window. Schema versioning + migration runner are deep behaviors hidden behind this surface.
  • Economy: addYield(gameId, amount), getBalance(), subscribe(callback). Cross-game balance accumulation; persisted via the persistence service.
  • Achievements: unlock(gameId, achievementId), getUnlocked(gameId), subscribe(callback). Dedup logic; persisted via the persistence service.
  • Input: subscribe(callback) emitting normalised drag-start / drag-move / drag-end / tap events. Pointer event normalisation across mouse / touch / pen.
  • Audio: play(soundId), setMuted(boolean), enable(). Lazy WebAudio init; autoplay-safe gating that requires a user gesture for first sound.

Tech invariants honored (per CLAUDE.md)

  • TypeScript strict + noUncheckedIndexedAccess.
  • Vite build with base: "/100g/" for GitHub Pages.
  • Pixi for rendering plus a thin custom ECS for render-side animated entities only.
  • Domain layer is plain reducers over typed structures, no ECS.
  • Vitest with happy-dom for DOM-adjacent tests, node for pure tests.
  • typescript-eslint + Prettier + Husky/lint-staged.
  • pnpm package manager.
  • Daily auto-deploy to GitHub Pages on every merge to main.

Testing Decisions

What makes a good test (project-wide)

A good test exercises external behavior, never implementation details. It calls the deep module's public surface, asserts on the return value or observable side effects, and would still pass if the internals were rewritten. Tests live next to source. Pure tests run in node; DOM-adjacent tests run in happy-dom.

TDD coverage

  • Layer A and Layer B are TDD non-negotiable per CLAUDE.md's iron law (no production code without a failing test first). Every public function in domain modules and every service is implemented test-first.
  • Layer C is mixed. Catalog, controller, and manifest are TDD. Render layer (Pixi-based views) is NOT unit-tested — visual regression is human-reviewed during night-shift QA. A single integration smoke test mounts the game, simulates a 5-row clear, and asserts the Round-Cleared transition + correct gold payout.

Critical test specifications

  • Top-out parity test. Run the spawn-block top-out path and the garbage-shift top-out path against equivalent setups and assert the resulting RunEnded states are identical (same stats, same achievements, same currency yield). This is the spec author's flagged concern — both triggers must behave identically from the player's point of view.
  • Persistence round-trip. Save after every commit; close the simulated session; load; assert RunState plus deck shuffle order plus RNG state plus achievement set are bit-identical. Confirm 50ms debounce coalesces rapid commits to a single write.
  • Effects suites (one per effect).
    • Standard: lands at first collision; tests at floor, on top of stacks, against walls.
    • Ghost: finds deepest fitting row; reaches under overhangs; identical to Standard when no overhangs exist; may not reach the floor on uneven columns.
    • Melt: flood-fill respects walls; bucket holds water; tunnel fills outward; sealed cavity unreachable; overflow stacks above using same outward rule.
    • Impact: aura clears bbox+1 area; tested with 1×1 (3×3 result), 2×2 (4×4 result), 1×4 (3×6 result), 3×3 (5×5 result).
    • Rain: per-column independence; multi-cell-per-column stacking; uneven columns produce uneven landing rows.
  • King-adjacency-aware connectivity. The board's "is this cell part of this block" check correctly accepts diagonal-only connections per the bestiary's king-graph definition.
  • Shop weighted RNG. Statistical test with seeded RNG and tolerance bounds: booster offer generation respects tier × effect-rarity weights; remove-3-of-deck samples uniformly.
  • Currency formula. min(rows/5, 200) + min(round-1, 100) capped at 300, including all edge cases (zero rows, exactly 1000 rows, exactly round 100, far past caps).
  • Achievement unlock-once-per-run. Crossing a threshold a second time within the same run does not re-emit.
  • State machine transitions. InRound ↔ InShop ↔ RunEnded transitions on correct triggers; idempotent shop-exit.
  • Mobile viewport smoke. Manual pass on 375×667 viewport before merging: drag works on touch, no scroll-conflict, hit-targets ≥44×44.

What is NOT tested

  • Pixi visual rendering (no snapshot tests; visual regression is night-shift QA).
  • Audio playback timing (audio service has its own light tests; game-level audio is fire-and-forget).
  • The cross-100g shape franchise's fusion economy (out of scope; that's a different game's concern).

Out of Scope

The following were considered and explicitly cut, either in the original spec or during the 2026-05-05 grill-me session:

  • Slide effect. Cut during grill-me; replaced with Melt as a richer 5th-effect-slot. Melt + Rain together cover what Slide attempted.
  • Effect-amplifier passives (Sliding Strong, Big Bang, Phase Walker). Cut to keep the passive list manageable; effects stand on their own.
  • Per-block rotation of any kind. No gesture, no button, no tap-rotate. Bestiary canonical orientations are the only orientations.
  • Meta-progression between runs. No persistent block-pool unlocks, no cross-run upgrades. The cross-game currency is the only inter-run progression and it lives in the platform shell.
  • Hand-pick from drawn cards. The deck is a strict queue. Roguelike-card-pick feel is delivered via Hold-slot and Shop, not a hand of N.
  • Procedural shape mutation (Cell+ / Cell−). Shapes are fixed at catalog time.
  • Per-cell effects. A block has one effect total. No mixed-effect cells within a single block.
  • Wildcard effect. Cut — would only make sense paired with a Combo or Color-match subsystem, which is also cut.
  • Combos / colour-match / chain bonuses. Row-clear is the only score-event.
  • Consumables. No mid-round usable items.
  • Multiple difficulty modes. Round-target curve provides the difficulty arc; no Easy/Normal/Hard.
  • Online features. No leaderboards, no shareable runs, no telemetry. Per ADR 0001.
  • Win state. Endless run; top-out is the only way out.
  • Bestiary fusion economy. Drop Deck consumes shape geometry from the bestiary but does NOT implement the franchise's fusion mechanic — that's the cross-100g creature game's concern.
  • Server-backed anything. All state IndexedDB; no fetch calls. Per ADR 0001.

Further Notes

Calibration items deferred to playtesting

These are tuning concerns, not architecture, and should be revisited after the first playable build is shipped:

  • Round-target curve (currently linear 5 + 2(round-1); might want quadratic).
  • Garbage cadence number (currently every 8 drops).
  • Booster prices ($2 / $5 / $10).
  • Booster rarity percentages (placeholders in the spec).
  • Remove-cost ($3) and fairness with extreme deck sizes.
  • Foundation passive pre-fill details and interaction with garbage-row injection.
  • Starting-deck composition (currently 7 Standard small-tier blocks; specific bestiary picks TBD).
  • Concrete catalog curation (which ~28–30 of the 167 bestiary creatures, especially how many diagonal-only-connected shapes survive curation).

Decomposition strategy for /to-issues

This PRD should decompose into independently-grabbable vertical slices. Suggested grain:

  • Engine Game contract + ECS skeleton.
  • Bestiary TypeScript extraction (with schema validation tests).
  • Persistence service.
  • Economy service.
  • Achievements service.
  • Input service.
  • Audio service.
  • Drop Deck domain/board.ts (placement, row-clear, garbage, top-out).
  • Drop Deck domain/effects.ts (5 effect strategies behind one resolve surface).
  • Drop Deck domain/deck.ts (shuffle, draw, hold-swap-lock, min/max).
  • Drop Deck domain/garbage.ts (cadence with passive integration).
  • Drop Deck domain/shop.ts (offer generation, transactions).
  • Drop Deck domain/economy.ts (gold + cross-game yield).
  • Drop Deck domain/round.ts (round-target curve, transitions).
  • Drop Deck domain/runState.ts (state machine, achievement triggers).
  • Drop Deck catalog (curated subset of bestiary × 5 effects).
  • Drop Deck input controller (drag-then-tap commit, hold-swap).
  • Drop Deck render layer (Pixi views; integration smoke test only).
  • Drop Deck manifest + index (Game contract conformance).
  • Top-out parity test (cross-cuts board + runState).
  • Mobile viewport smoke (cross-cuts everything; 375×667 manual pass).

Each slice should produce a passing test suite plus minimal implementation, mergeable independently to main. The night-shift workflow (subagent-driven-development per CLAUDE.md) picks up issues with the night-shift label after /triage clears them.

Prerequisite ordering

Cross-game services and the bestiary extraction MUST land before any Drop Deck domain module that depends on them. Suggested order: engine Game contract → bestiary extraction → persistence service → economy/achievements (depend on persistence) → input/audio → Drop Deck domain in parallel where possible → catalog → controller → render → manifest. If a Drop Deck domain slice doesn't touch a service, it can run in parallel with that service's implementation.

Source of truth

The design spec at docs/superpowers/specs/2026-05-04-drop-deck-design.md is the authoritative specification — including specific algorithms, data shapes, and module layout. This PRD describes WHAT to build and WHY; the spec describes HOW. If the two ever conflict, the spec wins and the PRD should be updated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationgameA daily mini-gamepriority:p1Highest

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions