minigame-engine
AGENTS.md1 starsChanged 49 days agoMITView on GitHub
A self-written TypeScript 2D/2.5D game engine plus games built on it. Targets: WeChat (wx), Douyin (tt), TapTap (tap) mini-games, 233乐园 (wx package converted by the Tuanjie converter) and the web (dev/testing). Rendering is Canvas 2D. An AI agent writes all code, art, music and UI; the human only tests and gives feedback. So every feature must be inspectable from code: text dumps, lint reports, headless screenshots you can Read. - pnpm typecheck — TypeScript 7 (tsc --noEmit). Must be clean.…
- Reads credentials
- Installs packages
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository11 other files it gives its agents.
- audio-designSkill
- code-artSkill
- engine-extendSkill
- game-feelSkill
- input-and-i18nSkill
- make-a-gameSkill
- performanceSkill
- physicsSkill
- release-platformsSkill
- testing-and-shotsSkill
- ui-screensSkill
The file
We split this AGENTS.md into the rules it gives an agent
6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
Every CLI below takes --app <dir>.
- Very specific: line 42
If node_modules is missing: pnpm install --frozen-lockfile --prefer-offline.
- Distinctive: line 65
When you change an engine API, update the matching skill in the same change.
- Distinctive: line 150
Keep game rules pure (no rendering) so they can be unit-tested.
- Very specific: line 156
Every screen must pass pnpm shot --scene <s> --device "iphone-se,iphone-14,ipad" --lint with 0 errors.
- Very specific: line 181
If an edit tool turns CJK characters into ?, write them as \u escapes (see engine/scene/text.ts).
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md — project guide for coding agents
A self-written TypeScript 2D/2.5D game engine plus games built on it. Targets: WeChat (wx), Douyin (tt), TapTap (tap) mini-games, 233乐园 (wx package converted by the Tuanjie converter) and the web (dev/testing). Rendering is Canvas 2D. An AI agent writes all code, art, music and UI; the human only tests and gives feedback. So every feature must be inspectable from code: text dumps, lint reports, headless screenshots you can Read.
Commands (Windows / PowerShell, pnpm)
pnpm typecheck— TypeScript 7 (tsc --noEmit). Must be clean.pnpm test— Vitest (node env). Tests live next to code (*.test.ts) or intests/.pnpm check— typecheck + encoding lint + test.pnpm lint:encoding [--fix]— tracked text files must be UTF-8 without BOM, no U+FFFD, no mangled CJK (??) or mojibake; prints file:line:col, exits 1 on issues (--fixonly strips BOMs).- Every CLI below takes
--app <dir>; the default is package.json"engine": { "app": "game" }. Pass--app sandboxfor engine demo scenes. pnpm shot [--app <dir>] --scene <name> [--device "iphone-se,iphone-14,ipad"|all] [--tap <selector|x,y>] [--drag "<a>|<b>"] [--wait 0.5] [--seconds 1] [--seed 1] [--params '<json>'] [--dump] [--lint] [--bounds]— headless screenshot into.shots/;--tap/--drag/--waitrepeat and run in order. Read the PNG to look at it. Landscape apps (app.json"orientation": "landscape") useiphone-se-land, iphone-14-land, android-land, ipad-land; shot/bench/dev default to them and--device allpicks the profiles of the app's orientation.--dumpprints the node tree,--lintprints the UI lint report (exit 1 on errors),--boundswrites an extra-bounds.pngwith hit areas / text boxes / issues / keep-clear zones drawn on top.--out <file.png>with several devices writes<file>-<device>.png.--helplists everything.pnpm shot:browser— same options in real Chrome (falls back to Edge), plus--input engine|touch|mouse,--browser,--timeout; exits 1 on page console errors.pnpm dev [--port 5173]— dev server with live reload;/frameshows 3 phones side by side; LAN URLs for phones. In dev web buildswindow.__enginehastap/drag(target),point,dump(),go(scene),lint(),bounds(on),stats(),statsLine(),textures(),debug(opts); URL?scene=<name>&seed=<n>,&stats=1(stats panel) or&debug=1/&debug=all(debug overlay).pnpm build --target web|wx|tt|tap|233|all [--minify]— packages intodist/<target>/and prints a size table. Fails if wx/tt main package > 4 MB or game.js references node modules. 233 needs the converter in%TEMP%\minihost-converter(orMINIHOST_CONVERTER), otherwise it is skipped with instructions.pnpm audio [--only coin,menu] [--preview]— renders<app>/audio/index.ts(sfx params + songs) to mp3 + manifest in<app>/assets/audio/;--previewwrites waveform/spectrogram/piano-roll PNGs to.shots/audio/.pnpm art:preview [--only icons,shapes] [--svg file.svg]— contact sheets of the art toolkit in.shots/art-*.png.pnpm bench [--scene play] [--device iphone-14] [--seconds 10] [--taps none|random] [--tap-every 0.8] [--seed 1] [--params '<json>'] [--json]— headless frame-time / node / particle / texture-memory / heap benchmark. Numbers are relative (compare runs on the same machine), not device fps. Runtime:game.stats,formatGameStats(),textureStats(),showDebugOverlay(game, { stats, bounds, hits }), one-framedebugDraw.line/rect/circle/text. Bake art withbakeTexture(w, h, draw, { resolution: 'auto', key })instead of a fixed resolution.- If
node_modulesis missing:pnpm install --frozen-lockfile --prefer-offline. - GitHub needs the local proxy:
git -c http.proxy=http://127.0.0.1:7890 ...(npm registry works directly). GitHub CLI:gh(portable install in%LOCALAPPDATA%\Programs\gh\bin, on the user PATH).
Skills (read the matching one before starting a task)
Detailed how-to guides live in .agents/skills/<name>/SKILL.md (auto-discovered by Cursor and other agents).
This file stays the short index; each skill holds the API cheat-sheet, recipes, pitfalls and verification steps.
| Skill | Use it when |
make-a-game | starting or extending a game: app layout, rules model, scenes, art/audio, playtest, build (worked example: game/) |
ui-screens | building HUDs, menus, dialogs with ui.*; fixing UI lint errors |
code-art | drawing sprites/icons/backgrounds as code (SVG, shapes, pixel art), baking textures, review galleries |
audio-design | sound effects and music as code, pnpm audio, AudioManager |
game-feel | juice (shake/punch/springs/squash), aim gesture, node fixed-step, scene restart, sound shortcuts, ads helper |
physics | collisions and bodies: arcade PhysicsWorld vs rotating/stacking RigidWorld, contact events |
input-and-i18n | touch/keyboard/gamepad input, input actions, localization |
testing-and-shots | headless tests that play the game, screenshots, lint runs, CLI options |
performance | fps/frame time/memory, debug overlay, pnpm bench, texture memory and resolution |
release-platforms | building for wx/tt/tap/233/web, app.json, size limits, real-device checklist |
engine-extend | adding or changing an engine module (conventions, sandbox scene, tests, docs) |
When you change an engine API, update the matching skill in the same change.
Layout
engine/ runtime engine, imported as '@engine' (engine/index.ts). Bundled into builds:
no node:*, no @napi-rs/canvas, no DOM/window/wx access outside engine/platform/.
core/ math, emitter, rng (seeded), color, game (loop/scaling/input), app (defineApp/runApp),
geom (segment sweeps vs circle/capsule/rotated box, box frames, ballistics: ballisticAngle/Path)
gfx/ Ctx2D/Surface types, Texture, textures registry, bakeTexture, draw helpers,
tintTexture / duotoneTexture (cached recolours; Sprite.tint, ui.icon tint/duotone)
platform/ Platform interface (types.ts), current platform, adapters web / minigame (wx, tt, tap),
ads helper (configureAds / canShowAd / showRewardedAd, typed AppJsonConfig)
scene/ Node, selector, dump, Sprite, Box, Text, Scene/SceneManager
runtime/ tweens + easing, timers, juice (shake/punch/popIn/countTo...), springs (createSpring,
squashSpring, wobble, springProp), gestures (draggable, onSwipe, onAim...), joystick,
assets + atlas loading, LoadingScene, SaveStore/settings, pools, StateMachine,
fixedUpdate (Game or Node), events, keyboard/gamepad + input actions (input.ts), i18n (tr)
display/ Graphics (vector drawing), AnimatedSprite, NineSlice, TilingSprite, ParticleEmitter +
particlePresets/spawnParticles, MaskContainer, CacheContainer, Trail, ShadowBlob, Line, ArcProgress
world/ World (camera container with culling) + Camera2D (follow/deadzone/shake), ParallaxLayer,
TileMap (ASCII maps, autotile, chunk prerender), IsoMap/IsoObject (heights, picking),
DepthSortLayer, GroundObject (z + shadow), PerspectiveRoad, PhysicsWorld/ArcadeBody,
RigidWorld/RigidBody (rigid*.ts: rotating circles + convex polygons, stacking, sleeping,
contact events, sensors, raycast; joints: distance/rope, revolute with limits/motor/spring,
weld, mouse; presets createRigidRagdoll / createRigidChain; bindRigidNode, drawRigidWorld,
RigidDebugView),
findGridPath (A*), distance fields, PathFollower
ui/ flexbox layout, `ui.*` builder + buildUI(spec), widgets, modal/dialog/toast, themes,
inspectUI / lintUI / formatLint / drawUIBounds, followNode / pinToNode / nodeRect / convertPoint
(UI that tracks world objects)
art/ palettes + colour ramps, pixelSprite, SVG parser/renderer (svgTexture), 22 shapes, 46 icons
(registerIcons -> 'icon:<name>'), noise + tileable patterns, bake effects, creatures, TextureAtlas
audio/ synth + DSP, sfxPresets/renderSfx, text music notation + songs, WAV, analysis, AudioManager,
shortcuts that no-op without a manager (playSound / playSong / stopSong / setAudioMuted)
testing/ Node-only: HeadlessPlatform, createTestGame harness, devices ('@engine/testing')
sandbox/ engine showcase app = compatibility test pack; scenes/<module>.ts per module,
audio/index.ts (sound definitions), app.json (store metadata, appids, ad unit ids)
game/ 合成大西瓜 (Suika-style merge game), the default app for CLIs (package.json "engine.app") and
the worked example in the `make-a-game` skill
archer/ 布偶弓箭手 (Ragdoll Archers clone), landscape 1334x750: Verlet ragdoll, archery duel, upgrades menu,
armored enemies, local two-player versus / co-op (scenes/duo.ts, two onAim halves)
tools/ node CLIs: shot, build, dev, audio, art
tests/ cross-module testsCore conventions
- Design resolution 750x1334 portrait (1334x750 for landscape apps), scale mode 'expand':
game.viewis the visible size in design units,game.safethe safe rect. Lay out against these, never against raw screen pixels. - Node local space: origin at the content box top-left; (x, y) positions the anchor; anchor is the pivot.
Subclasses override
draw(ctx)anddescribe()(props shown in dumps / usable in selectors) andkind. Rendering is flat (setTransformper node, no save/restore):draw()/drawOver()must reset the canvas state they change (shadows, line dash, lineCap/lineJoin, composite op) or setisolate: true; Vitest warns about leaks. Render overrides move children withrenderChildrenWith/renderChildrenOffset, never a rawctx.save()(see theengine-extendskill). - Selectors:
Kind#id.tag[key=value][key*=sub], descendantA B, childA > B.describe()keys are attributes, e.g.Text[text=开始]. - Input:
node.onTap(fn); events bubble;game.on('pointerdown', ...)for stage-level input. Press-slide-release aiming:onAim(zone, { start, move, release }, { space, enabled }), not hand-tracked pointers. Keys / gamepad:createInputActions(scene, { jump: ['Space', 'PadA'] }), thenpressed('jump')/axis('left', 'right'); bind on-screen buttons withbindHold. Phones have no keyboard: always ship touch controls. - Per-frame: override
update(dt)or usenode.onUpdate(fn); global systems viagame.addSystem(). Gameplay simulation:fixedUpdate(this, 60, (step) => ...)with a node target (stops on destroy, pauses with the node); never hand-roll an accumulator. Restart a level withgame.scenes.restart({ transition: 'fade' }). - Lifetimes:
sequence()/parallel()belong to the scene of their first animated node (else the top scene), and modals / toasts to the top scene; both die with it. Pass{ owner: null }for app-level ones,{ owner: node }to bind to a node. - Frame rate / memory:
game.setFrameRate(30)on menus and pause screens, 60 in gameplay; free rebuildable textures ingame.on('memorywarning', ...)(engine caches are already dropped). - Physics:
PhysicsWorld/ArcadeBodyfor tile-map characters (platformer, top-down);RigidWorldfor anything that stacks, rolls or rotates (crates, balls, merge games). Game rules readworld.touches/ contact events; add/remove inside callbacks is deferred to the end of the step. See thephysicsskill. - Sounds from game code:
playSound('merge', { rate }),playSong('bgm', { fadeMs }), notgetAudioManager()?.. - Ads:
configureAds(appJson.ads)at boot, thencanShowAd()/await showRewardedAd(); never branch onplatform().namefor ads. - Text in several languages:
defineStrings({ en: {...}, 'zh-CN': {...} }),setLocale('auto')at boot,tr('key', { name }); rebuild screens inonLocaleChange(fn, scene); tests callresetI18n(). - Node subclasses must not reuse Node member names (
paused,data,visible,alpha, ...); thegame-feelskill lists them. - Deterministic: use
rng/new Rng(seed)from '@engine', neverMath.random()in engine or game logic. - Portable canvas subset only (see engine/gfx/types.ts): no Path2D (use
svgPath), ctx.filter, ctx.roundRect (useroundRectPath), letterSpacing, OffscreenCanvas, DOM. import typefor type-only imports (verbatimModuleSyntax). Avoid generic export names that could clash across barrels (export *conflicts are type errors); prefix with the module (drawArtShape,pcmFilter).- Keep game rules pure (no rendering) so they can be unit-tested.
UI: build with ui.*, verify with lint
- Screens:
mountScreen(scene, ui.column({...}, [...]))(lays out inside the safe area by default). Layout is flexbox (direction,justify,align,gap,padding,grow,%sizes); tokensxs..xxlfor spacing. - Every screen must pass
pnpm shot --scene <s> --device "iphone-se,iphone-14,ipad" --lintwith 0 errors. Rules cover overlaps, off-screen / outside safe area, truncation, contrast (min 3:1), tap size (min 88), font size (min 20). Tune per call withlintUI(root, game, { rules: { 'small-font': 'off' } }). - Plain nodes can declare a lint role by tag:
tags: ['lint-blocker']for a hand-made backdrop (content under it is skipped), alsolint-decor,lint-surface,lint-control. UI widgets set this themselves.lint-ignoreskips a whole subtree; everyWorldhas it (game-world content is not UI). - Game-world areas UI must not cover (enemy, player, aim zone): tag a node
lint-keep-clear(works inside a World) or passlintUI(root, game, { keepClear: [rect] }); covering UI is acovers-keep-clearerror in--lint. - HUD that sits on a world object:
mountScreen(parent, spec, { area: followNode(worldNode, { pad, clamp: 'safe' }) }), single nodespinToNode(label, worldNode); never hand-writefield.x + X * field.scale.
Art and audio are code
- Art: prefer SVG strings (
svgTexture), shapes, pixel grids (pixelSprite) and generated creatures, baked once at load into textures (use aTextureAtlasfor many small ones). Review withpnpm art:previewor a sandbox scene screenshot. Colours frompalettes/paletteRoles/colorRampkeep a game consistent. - Audio: sound effects are
SfxParams(start fromsfxPresets), music isdefineSongwith text notation. Render withpnpm audio; check the analysis table and--previewPNGs (loudness, clipping, key) instead of listening. Create the manager inboot()withcreateAudioManager(game, { library: { sfx, music } })(withoutlibrarynothing plays in tests/dev before files are rendered) and play withplaySound/playSong.
Pitfalls
- PowerShell 5:
Set-Content -Encoding utf8writes a BOM; write files with the file tools or[IO.File]::WriteAllText(path, text, (New-Object Text.UTF8Encoding $false)). - If an edit tool turns CJK characters into
?, write them as\uescapes (see engine/scene/text.ts). - PowerShell splits unquoted comma lists into arrays: quote them,
--device "iphone-se,iphone-14,ipad". PowerShell 5 also strips inner double quotes, so--params '{"a":1}'arrives as{a:1}(the shot tools accept it). - pnpm runs with
reporter: silent(pnpm-workspace.yaml) so scripts don't echo their command to stderr (which PowerShell shows as a red NativeCommandError). See install output withpnpm install --reporter=default. - Tests:
createTestGamerenders only the last frame of each step/advance/tap by default (render: 'every'to count per-frame draws,pixelRatio: 1for faster shots); each test game starts with fresh save storage, rng seed 1 and the default UI theme. Game tests that take no screenshots passrender: 'none'(lint and hit tests still work: layout runs on undrawn frames). One 30 s timeout lives in vitest.config.ts: no per-test timeouts. Balance tests over several seeds usesweepSeeds/formatSweepfrom '@engine/testing'. - New app folders must be covered by tsconfig/vitest includes (see the
make-a-gameskill). - Mini-game runtimes: no DOM,
performance.now()is in microseconds on wx/tt (adapters convert), fonts default to'sans-serif'. Only real devtools/phones prove a platform works; the headless and browser shots do not. - Dev-only code goes in an inline
if (process.env.NODE_ENV !== 'production') { ... }(no early return, no helper variable) so release builds drop it; review scenes go inAppDef.devScenes. Release builds fail when the audio manifest misses a library sound: runpnpm audio --app <app>.pnpm buildprints game.js size per folder. - Text:
TextStyle.cache('auto' default) draws static / stroked labels from shared bitmaps; change styles withsetStyle, not by mutating the style object. - Physics replays / golden values are engine-version specific (float details such as
Math.sqrtvshypotchange results); determinism holds within one version. - Scratch scripts run with tsx outside the repo need
TSX_TSCONFIG_PATH=<repo>/tsconfig.json, otherwise class fields get define semantics and benchmarks lie.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

