BEHIND NIGHTY
Behind Nighty: notes on how it was madeNighty 幕后:做这个 App 时的思考与调优记录
Nighty is a box of slow, dim pixel games for the last ten minutes before sleep, built because every game I tried was designed to keep me up.
Nighty 是一盒节奏慢、画面暗的像素小游戏,给睡前最后十分钟玩。做它,是因为我试过的游戏都在想办法让我别睡。
I made the product calls and designed the workflow; AI agents wrote most of the code inside it. This page is about that split: where I put the human checkpoints, what the agents got wrong, and the rules that came out of it.
产品上的决定和工作流程由我来定,大部分代码是 AI agent 在这套流程里写的。这页讲的就是这个分工:人工检查点放在哪里,agent 在哪些地方出过错,以及从中定下的规矩。
- What: an iOS side project with 6 retro games, in English, Spanish and Chinese, submitted to the App Store.
- 是什么:业余做的 iOS App,6 款复古小游戏,英 / 西 / 中三语,已提交 App Store。
- My role: product decisions, plus the agent workflow they ran through (spec → plan → subagent → review → device check), which I later open-sourced as a kit.
- 我的角色:做产品决策,并搭了 agent 干活的流程(规格 → 计划 → 子代理 → 审查 → 真机验收),这套流程后来整理开源了。
- Key lesson: every per-task review passed; only the whole-branch review caught the crash between two tasks.
- 最大的教训:逐任务审查全部通过,只有全分支审查抓到了两个任务之间的崩溃。
374 commits · 559 tests · 19 specs · 22 plans · EN / ES / 简体中文374 个 commit · 559 个测试 · 19 份规格 · 22 份计划 · 英 / 西 / 简体中文
Status: submitted to the App Store.状态:已提交 App Store 审核。
Website官网Play Brick Breaker in the browser在网页里试玩打砖块Case: the maze generator案例:迷宫算法
How I organize the AI我怎么组织 AI 干活
I own the two ends of every round: what gets built, and whether it is good enough to ship. The usual loop: I notice a problem and give a direction; the AI proposes options; I pick one or push back, and settle the approach and the parameters; agents implement and review; I accept it on the phone.
每一轮的两头在我手里:做什么,以及做到什么程度可以上线。通常的节奏是:我发现问题、给出方向;AI 拿出几种方案;我判断、提别的想法,定下方案和参数;agent 实现并审查;最后我在真机上验收。
All of it ran in Claude Code, following a brainstorm → spec → plan → subagent pipeline. I used my own workflow kit, which drew on the Superpowers pipeline, with my own pieces on top: the model split below, the CLAUDE.md constraints, the rules in 04 and the phone gate. Each feature starts as a conversation that ends in a written spec, then a step-by-step plan. Each task in the plan goes to a fresh subagent that has no memory of earlier tasks: Haiku when the plan already contains the code to write, Sonnet when the task needs judgment or Haiku gets stuck. A Sonnet reviewer checks every task against the spec, and for code quality, before the next task starts. At the end, Opus reviews the whole branch. Cheap models do the volume; the strongest model goes where a miss costs the most.
全程在 Claude Code 里完成,按头脑风暴 → 规格 → 计划 → 子代理这条线做。用的是我自己的工作流 kit,它参考了 Superpowers 的流水线,并加了我自己的部分:下面说的模型分工、CLAUDE.md 约束、04 里的规矩和真机闸门。每个功能先聊清楚、写成规格,再拆成逐步的计划。计划里每个任务交给一个全新的子代理,它不记得之前的任务:计划里已经写好代码的用 Haiku,需要判断或者 Haiku 卡住了用 Sonnet。每个任务做完,由 Sonnet 审查者对照规格检查、再看代码质量,通过了才做下一个。最后由 Opus 审查整个分支。量大的活交给便宜模型,最强的模型放在漏掉代价最大的地方。
After Nighty, I folded what it taught me back into that kit and open-sourced it as the Claude Code workflow kit (v2): risk-tiered specs, plan-first, TDD, a verify gate enforced by hooks, and a lesson-capture loop. Some of it is newer than Nighty: the hook-enforced gate came in v2, and the rules in 04 were added by hand at the time. The kit's lesson-capture skill writes that habit down: a failure becomes a rule once it has happened more than once and fits in one sentence.
Nighty 做完后,我把这次的经验补回这套 kit,优化后开源成了 Claude Code workflow kit(v2):按风险分级的规格、先写计划、TDD、用 hooks 强制执行的验证闸门,以及从失误里总结规矩的闭环。其中有些是 Nighty 之后才有的:hooks 强制执行的验证闸门是 v2 加的,04 里的规矩当时是手动加进来的。kit 里的 lesson-capture skill 把这个习惯写成了流程:一个失误重复出现、并且能用一句话说清,才变成规矩。
Decisions I made我做的判断
01Throwing away the first look推翻第一版外观
The first version looked like a monochrome LCD handheld. It worked, but I didn't want to play it myself, and an app I wouldn't open was not going to win anyone else over, so I asked for a redesign. Round one compared three directions (night light, quiet ink, cartridge) and I picked cartridge. Round two still showed single-color blocks once you were inside a game, so I rejected it. Round three gave every game its own small world and art style, and that became the app. My only change request on round three: English and Spanish game titles must fit on one line.
第一版是单色液晶掌机的样子。能用,但我自己都不想玩,我自己都不想打开的 App 也打动不了别人,于是要求重做。第一轮比了三个方向(夜灯、静墨、卡带),我选了卡带;第二轮进游戏后还是单色方块,我否掉了;第三轮给每款游戏一个自己的小世界和画风,就是现在的 App。第三轮我只提了一处返修:英文、西语的游戏名必须单行。
02Measuring the maze before fixing it迷宫先量化再改
The mazes felt like one corridor with stubs. Tuning by feel would have left me unable to tell whether a change helped, so I asked for numbers first: on the old generator, "always walk toward the exit" was within a few percent of the shortest path. We built a browser test bench, I tuned the parameters there by playing, and a golden test checks the Swift version against it cell for cell.
迷宫玩起来像一条走廊加几截短岔路。凭感觉改,就没法判断一次改动到底有没有用,所以我先要数据:旧算法下「一直朝出口走」只比最短路多走几个百分点。然后做了网页试验台,我在上面边玩边调参,最后用对照测试核对 Swift 版本与试验台逐格一致。
03A ball that never reached the middle打砖块:球永远打不到中间
On the phone, the Brick Breaker ball sometimes circled forever without touching the bricks in the middle. The cause: the ball only ever moved at 45°, and on a grid with reflecting walls that allows closed loops around a centered pattern. It was reproduced with a deterministic probe that prints every frame as text: 4,000 steps, zero hits, locked into a loop from step 38. The fix gives the ball variable slopes depending on where it hits the paddle. Because the probe is deterministic, the bug reproduces on every run, so the same setup became a regression test: across 8 seeds, the ball now hits the pattern and clears the level within a bounded number of steps.
真机上打砖块的球有时会一直绕圈,碰不到中间的砖。原因是球只会走 45°,在会反弹的网格里,这能形成绕开中间图案的闭合轨道。复现用的是确定性探针:每一帧打印成文字,跑 4000 步零击中,第 38 步起进入闭环。修复是让球根据打在挡板上的位置有不同斜率。探针是确定性的,每次运行都能复现,所以同一套设置直接变成了回归测试:8 个种子下,球都能在有限步数内打中图案并过关。
04Rules that came out of mistakes从失误里来的规矩
- First level first. For any multi-level game, level one is built as a tunable prototype and accepted on a real device before more levels are produced. For endless difficulty curves, check at least three points (start, middle, near the cap), because level one only tells you where the curve starts.
- 第一关先行。多关卡游戏先把第一关做成可调参数的原型,真机验收通过才铺开后续关卡。程序化难度曲线至少看三个点(起始、中段、接近上限),因为第一关只能告诉你起点。
- A regression test must fail before the fix. In one round, three tests passed without actually exercising the bug they were written for. Since then, every bug-fix task includes reverting the fix and watching the test go red.
- 回归测试必须先确认会红。有一轮里三次出现测试是绿的、但根本没测到要测的 bug。从那以后,每个修 bug 的任务都要先把修复回退,看着测试变红。
- The phone is the gate. A round only merges after I've gone through its checklist on a real phone, because green tests had already missed real bugs.
- 真机是闸门。每一轮都要我在真机上逐条过完验收清单才合并,因为测试全绿已经漏过真实的 bug。
05Product calls产品取舍
- The bedtime reminder never forces you out. Tapping "OK, time to sleep" fades the screen slowly.
- 睡前提醒不强制退出,点「好,休息了」后屏幕慢慢变暗。
- Brightness can go below the system minimum, because the app is meant for a dark room.
- 亮度可以压到比系统最低档还暗,因为它就是在黑屋子里用的。
- Version 1.0 leaves out the China mainland storefront instead of waiting weeks for an ICP filing.
- 1.0 先不上中国大陆,不为等 ICP 备案拖几周。
06Being talked out of an idea被说服放弃一个想法
I suggested labeling the new serve button "S". The AI argued against it: the other two buttons are A and B, so a letter would read as a third action button; serving only works while the ball is waiting, so a mistap costs nothing and the button doesn't need a distinct look to prevent mistakes. I agreed. It became a round ▶ button the same size as A and B, with a small caption underneath.
我提议把新加的发球键标成「S」。AI 不同意:另外两个键叫 A 和 B,再加一个字母会被读成第三个动作键;发球只在球待发时有效,误按没有代价,所以不需要靠外形区分来防误触。我被说服了,最后是一个和 A/B 同样大小的圆形 ▶ 键,下面带一行小字说明。
What I learned building with AI agents和 AI agent 一起做东西学到的
- Checking the parts doesn't check the whole. In Brick Breaker, every task passed its own review, but the whole-branch review found a crash between two of them: when the ball hit a corner while the paddle was against the wall, the paddle bounce pointed it into the wall and nothing clamped it again, so it moved to column −1 and the next frame crashed.
- 每块都对,合起来不一定对。打砖块的每个任务单独审查都通过了,但全分支审查发现了两个任务之间的崩溃:球打到角落、挡板又贴着墙时,挡板反弹把球指向墙里,之后没有再夹一次边界,球跑到第 −1 列,下一帧直接崩溃。
- An agent can satisfy the check without testing the problem. The three regression tests in 04 were green and still missed their bugs. The subagents reported it themselves, which is why every review asks for evidence, not just a passing run.
- agent 可能只是让检查通过,并没有测到问题。04 里那三个回归测试都是绿的,却都没测到 bug。这是子代理自己如实报出来的,也是为什么每次审查都要看证据,而不只看测试跑没跑过。
- Durable constraints keep sessions consistent. A
CLAUDE.mdfile in the repo pins the ground rules (the game logic package may not import UI frameworks, colors come only from the theme, randomness is injected through a seed), so every new session and every fresh subagent starts from the same constraints. - 写死的约束让每次会话保持一致。仓库里的
CLAUDE.md写着基本规矩(游戏逻辑包不准引入 UI 框架、颜色只能取自主题、随机数通过种子注入),每次新会话、每个全新的子代理都从同样的约束开始。 - Know when to listen. Splitting the work doesn't mean the AI only executes. In 06 it had a better argument than I did, and I changed my mind.
- 知道什么时候该听 AI 的。分工不等于 AI 只管执行。在 06 里它的理由比我的好,我就改了主意。
- The real device is the last gate. Tests check what I thought to specify; the phone shows how it feels. The one-corridor mazes and the ball stuck in a 45° loop were both found by playing on the phone, not by tests.
- 真机是最后一道关。测试只能检查我想到要写的东西,手机上才知道玩起来什么感觉。迷宫像一条走廊、球卡在 45° 闭环里,这两个问题都是在手机上玩出来的,不是测试发现的。
Engineering notes工程结构
- Two layers: a pure Swift package for game logic (no SwiftUI or UIKit) and a thin SwiftUI app. Every game implements one protocol: it takes input events and produces a frame.
- 两层:纯 Swift 的游戏逻辑包(不准引入 SwiftUI / UIKit),加一层薄薄的 SwiftUI App。每款游戏实现同一个协议:吃输入事件,吐出一帧画面。
- Randomness comes in through a seed, so the logic is deterministic and testable. The maze golden test and the Brick Breaker probe both rely on this.
- 随机数通过种子注入,逻辑是确定性的、可以测试。迷宫对照测试和打砖块探针都靠这一点。
- Art direction was decided in HTML/JS drafts; the Swift renderer ports those drafts function by function, and a tool renders both and compares them pixel by pixel.
- 画风在 HTML/JS 设计稿里定,Swift 渲染逐函数移植设计稿,再用工具把两边都渲染出来逐像素比对。
- Store screenshots, the website images and the app icon are generated by scripts, in three languages.
- 商店截图、官网图片和 App 图标都由脚本生成,三种语言。
Together, the seeded logic, the maze golden test, the frame-by-frame probe and the pixel-diff tool work as an eval suite: every claim about behavior can be reproduced and checked.
种子注入的逻辑、迷宫对照测试、逐帧探针和逐像素比对工具合在一起,就是一套评估体系:每一条关于行为的说法都能复现、能核对。
public protocol PixelGame {
static var info: GameInfo { get }
init(seed: UInt64)
mutating func handle(_ input: GameInput)
mutating func tick(_ deltaTime: TimeInterval)
var frame: PixelFrame { get }
var state: GamePlayState { get }
}
What I'd validate after launch上线后想验证的
The app collects no data by design, so these can't come from in-app analytics.
App 刻意不收集任何数据,所以这些问题没法靠 App 内的统计来回答。
- Does the reminder work? When it appears, do people actually stop? I'd find out by asking a few testers, not from logs.
- 提醒有没有用?提醒弹出后,大家是不是真的停下了。这要靠问几位测试用户,而不是看日志。
- Do sessions stay short? A bedtime app that people play longer and longer has failed. App Store Connect's opt-in analytics shows sessions and retention without the app collecting anything itself.
- 单次使用是不是一直很短?如果大家越玩越久,这个睡前 App 就失败了。App Store Connect 自带的统计(用户自愿共享)能看到使用次数和留存,App 本身不用收集任何东西。
Archived drafts原稿存档
These are the pages I used to make decisions at the time, unedited apart from a note at the top. They are in Chinese.
这些是当时用来做决定的页面,除了顶部加一条说明,没有改动。
| Cartridge redesign, round 2卡带改版 · 第二轮 | Rejected: games still looked monochrome否掉:进游戏仍是单色 |
| Cartridge redesign, round 3卡带改版 · 第三轮 | Approved: an art style per game定稿:每款一个画风 |
| Push Boxes level-clear screen推箱子过关画面 | Picked C, the full-moon badge选 C 满月徽章 |
| Sound audition, round 1音色试听 · 第一轮 | Harp and soft xylophone竖琴 + 轻木琴 |
| Sound audition, round 2音色试听 · 第二轮 | Buttons told apart by pitch按键靠音高区分 |
| Icon, opening scene, settings图标、开场、设置页 | Icon E + opening ④图标 E + 开场④ |
| Website redesign官网改版 | Option A, the opening story方案 A 开场故事 |
| Maze test bench (EN / 中文)迷宫试验台(中 / EN) | Parameters tuned here参数在这里调定 |