Library Through a Peephole

I had ~290 stories written. The selector cycled through twelve. I'd built a library and shipped a peephole. Here's the sprint plan that took me to Steam, and the one prompt that exposed ten times more game than the player could see.

I've been on the rope-end of other people's deadlines for a long time. Magazine closes where the cover hasn't been approved and the printer is already calling. Brand campaigns that ship Tuesday whether legal signed off or not. A studio at 2am with the model on her third coffee and the light gone wrong. Pentest reports due Friday because someone's board meeting is Monday. Different rooms, same shape. You learn the rhythm. You learn whose panic is real and whose is theater. You learn what shipping looks like when somebody else has the rope around their neck.

Now it's my sprint.

This is the game I've been building. The deadline is mine. The rope is mine. And every one of those rooms โ€” the studio, the agency, the engagement โ€” is finally pointed at my own ship date.

The plan I'm about to lay out was a four-week plan when I wrote it. The clock has flexed since โ€” the Steam date pushed back slightly, polish revealed work that polish always reveals โ€” but the spine is the same. One AI collaborator. A pile of code, a pile of writing, and a clock.


The unlock that made the sprint real.

A couple of days ago I asked Claude a simple question:

"Why do my story branches feel hidden when I play, even though I know they're written?"

Claude went into the code and came back with the answer in fifteen minutes.

I had ~290 stories written. The selector that picked the next chapter cycled through twelve. Twelve. The other ninety-five percent of my writing was sitting in the codebase, never reached, because the routine progression was hardcoded to a list of three stories per age bracket and rotated through them on a modulo.

I'd written a library and built a peephole.

This is the lesson nobody warns you about when you're shipping with AI. The AI will help you make the content. It will not necessarily help you expose the content unless you ask the right question. And you don't always know what the right question is until you've played the thing yourself, alone, in silence, and felt that something is missing even though you remember writing it.

The fix isn't more content. The fix is the picker.


The prompt that found it.

The killer prompt looks pedestrian on the page. It is not.

"Why do my story branches feel hidden when I play, even though I know they're written?"

Three things make it work, and they are the three things I want you to steal:

  1. Symptom-first, not solution-first. I didn't say audit the chapter selector or check for off-by-one in the rotation logic. I described the feeling. Hidden. The model has to read the codebase to map the feeling onto the cause, which is exactly what you want. The moment you name the bug, you've narrowed the model's search to whatever you already suspected. Most of the time you suspect wrong.

  2. The contradiction is the prompt. Even though I know they're written is the engine. You're handing the model a fact โ€” the content exists โ€” and a feeling โ€” it doesn't surface. The gap between fact and feeling is the bug. Models are good at closing that gap if you give them both halves.

  3. No prescriptions, no guardrails, no scope. I didn't say don't touch the database or only look at the UI layer. The picker turned out to live in a service file two layers from where I would've started. If I'd scoped the question, the model would've stayed scoped, and I would still be polishing the wrong thing.

The general shape: describe the symptom, name the contradiction, ask why. Run that on any project where you suspect there's more under the hood than the user can see, and you will be amazed at what the AI digs up that you'd been stepping over for months.


The picker problem (it's not just games).

What I'm calling the picker is whatever decides what comes next in your product. In a game, it's the next chapter. In a SaaS app, it's what surfaces on the dashboard. In a content site, it's what fills the recommendation slot. In an editor, it's what the AI suggests.

Most products start with a hardcoded picker because that's the fastest path to a working prototype. Then you write more content, add more flows, build more features โ€” and the picker stays primitive. You ship a library through a peephole.

Symptoms:

  • Users say "I feel like there isn't much here" even though there's a lot.
  • Your team gets bored testing the same paths.
  • The metrics on your "long tail" content are zero, and you assume nobody wants it. Nobody's seeing it.
  • New content takes weeks to surface because somebody has to manually wire it into the picker.

The fix is what I started calling the Living Pool. Tag every piece of content with eligibility metadata โ€” what state it requires, what tone it carries, what feels right when. Then write a scorer that reads the world state and picks the right thing dynamically. Not era-gated. Not hardcoded. Lived.

The shape of the architecture, in pseudocode:

- Every content item gets:
  - Hard gates (must-be-true conditions)
  - Soft tags (emotional register, theme, location)
  - A baseline weight

- The picker:
  - Filters by hard gates
  - Drops anything in the recently-played window
  - Scores remaining items by state-relevance + variety + lifecycle echo
  - Weighted-random pick from the top N

- The world tracks:
  - Recently played IDs (anti-repeat)
  - Recent tones (anti-emotional-fatigue)
  - Last speaker (anti-monotony)

In Dart, the scorer ends up looking like this โ€” readable enough that you can argue with it instead of guessing what it's doing:

double scoreItem(ContentItem item, WorldState world) {
  if (!item.gates.every((g) => g.satisfiedBy(world))) return -1;
  if (world.recentlyPlayed.contains(item.id)) return -1;

  var score = item.baselineWeight;
  score += item.tags.relevanceTo(world.currentMood) * 2.0;
  score -= world.recentTones.fatiguePenaltyFor(item.tone);
  score += item.lifecycleEcho(world.phase);
  return score;
}

That's it. That's the architecture. It works for chapters, for events, for emails, for content recommendations, for AI prompts in your toolchain. The shape is the same.

The reason it matters is simple: I had ~290 stories of content sitting in the database. Once I unlocked the picker, the player got the whole library. That's not feature work. That's ten times my game's apparent size, free, just from fixing one service file.


The sprint, in four weeks.

Once you've identified your big unlock โ€” the thing that makes the rest worth polishing โ€” the sprint plan writes itself.

Week 1 โ€” The big unlock + critical narrative fixes.

Build the picker. Migrate the existing content into the new pool with conservative defaults. Fix the one or two narrative paths the user has flagged as broken.

Don't tag every piece of content yet. Just make the new picker work, and verify that the player can suddenly reach what they couldn't before.

The deliverable at end of Week 1 isn't polish. It's the unlock. The "huh, there's a lot more here" moment.

HACK LOVE BETRAY
COMING SOON

HACK LOVE BETRAY

Mobile-first arcade trench run through leverage, trace burn, and betrayal. The City moves first. You keep up or you get swallowed.

VIEW GAME FILE โ†’

Week 2 โ€” Tag the library + smart pacing.

Now that the picker is live, every piece of content needs metadata. This is the long pole. ~290 stories at thirty seconds of careful reading each is several focused sessions.

Use heuristics where possible โ€” Python passes that auto-derive tags from content. Verify with human eye. Manual pass for the things that can't be auto-derived: the emotional register, the lifecycle hint, the save this for the right moment weight.

End of Week 2: every piece of content is tagged. Pacing logic โ€” rotate emotional registers, prevent fatigue โ€” is in the scorer. The world breathes.

Week 3 โ€” Visual polish loop + minigame sweep.

First full playthrough end-to-end. Don't fix anything yet. Just log. Every cropped panel, every faceless scene, every palette violation, every animation that feels off. Collect.

Then days two through five: address the log. The polish foundations were laid in earlier sessions; this is mop-up and minigame-by-minigame review.

End of Week 3: clean playthrough. No visual gaps.

Week 4 โ€” Audio + final pacing + ship.

Audio pass. Map score and SFX to the motion vocabulary you've defined for the project. Inventory existing assets, fill gaps with placeholder if you have to.

"Too much at once" pass โ€” walk the game with somebody who'll tell you the truth and mark every compression point where the user gets numb.

Day five: final QA, build, ship.

No new scope in Week 4. Post-launch backlog only. This is the rule that keeps the sprint from collapsing into a death march.


The rules that make a one-month sprint possible.

I've watched a moodboard unravel in week three because nobody locked the look in week one. I've watched a security engagement blow its report date to scope creep. Sprints fail for predictable reasons. So here are the rules I'm running:

One item per session. Don't pile changes. Each focused stretch handles one thing. When it's done, it's done. Move on.

Static-check before declaring done. Whatever your equivalent is โ€” dart analyze, tsc --noEmit, the linter, the typechecker โ€” must pass cleanly. Zero errors is the bar. Warnings are negotiable. Errors are not.

The tester holds the trigger. Don't restart the build or the dev server on the user's behalf. They'll tell you when they want to see it. Trust the static checks until then.

Per-session progress note. Always. In a doc the next session can read. Cross-account continuity matters when you're hitting weekly limits and switching contexts mid-sprint.

The bar is fixed in Week 1. The visual direction, the design language, the architectural decisions โ€” those get locked early. You don't re-litigate them in Week 3 because you saw a competitor do something cool. The bar is fixed. Polish to it.

No new scope in Week 4. Anything that surfaces in the last week goes to the post-launch list. New ideas are the enemy of finished products.


The thing nobody tells you about AI-assisted sprints.

The AI doesn't ship the game. You ship the game.

The AI is a brilliant pair-programmer with infinite patience and zero opinions about your deadline. It will execute beautifully on whatever you ask it. It will not push back on bad scope decisions unless you train it to. It will not tell you the picker is hardcoded and that's why your game feels small until you ask the right question.

Every one of those rooms taught me the same thing: the work isn't in the typing. The work is in the direction. The work is in noticing that something feels off when you're playing alone in silence, and then having the discipline to not start polishing pixels โ€” to instead ask: why does this feel hollow when I know I've made a lot of it?

That question, asked at the right moment, was worth a thousand stories of content. Not because the AI couldn't have written more. Because the player would have never seen what was already there.


Where I am right now.

Deep in polish. Skill windows fully wired. Picker live, library exposed. Steam date moved a hair to the right because polish always reveals work that polish always reveals, and because some of what surfaced was good enough to be worth the extra days.

The architecture is decided. The punch list is written. Memory and handoff docs are in place so the work continues across context limits, across accounts, across whatever sleep schedule shipping demands of me.

If you're in a similar place โ€” content stacking up, a feature you keep almost-finishing, that gnawing sense that you've built more than the user can see โ€” the move is the same. Stop adding. Start exposing. Find the picker. Unlock the library. Then polish.

I'll write the post-mortem after launch. For now I have a sprint to run.

The clock is mine.


The Pizza Connection โ€” iPad & Steam. One lifetime. Six phases. No two runs the same.

The Pizza Connection ยท View the full pitch deck โ†’ ยท mdrn.app