Phase 5 — build the Flame game in 3 gated sub-phases (core loop → systems+components → UI+content), then self-evaluate against the contract.
Scanned 8/31/2026
Install to Claude Code
npx -y skills add tjdrhs90/flutter-flame-harness --skill flame-harness-generator --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Flame Harness Generator?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tjdrhs90-flame-harness-generator)More formats (shields.io, HTML) on the badges page.
---
name: flame-harness-generator
description: Phase 5 — build the Flame game in 3 gated sub-phases (core loop → systems+components → UI+content), then self-evaluate against the contract.
argument-hint: ""
allowed-tools: [Agent, Read, Write, Edit, Bash, Glob, Grep]
---
# flame-harness-generator
Phase 5 of the flutter-flame-harness pipeline. Builds the Flame game in three gated
sub-phases (5a → 5b → 5c). Each sub-phase ends with a HARD GATE (`flutter analyze` zero
issues + `flutter test` all pass) before the next sub-phase begins. On round N > 1, reads
the previous evaluator feedback and fixes only the listed failures.
All file schemas (`config.md`, `state.md`, `handoff/round-N-gen.md`, `feedback/round-N-qa.md`)
and the phase transition table are defined in `docs/harness-protocol.md` — that document is
the single source of truth (§2 for `state.md` schema; §4 for handoff layout; §5 for feedback
layout; §7 for the `generator → evaluator` transition). Do not redefine schemas here.
---
## Round Handling and Feedback Intake
### Determine current round
Read `docs/harness/state.md`. Extract `current_round` (integer, ≥ 1).
### Round 1 — build from scratch
When `current_round` is 1:
1. Read `docs/harness/config.md` (extract `app_slug`, `app_name`, `bundle_id`,
`default_language`, `skip_admob`).
2. Read the latest PRD (`docs/harness/plans/*-prd.md`, sort descending, take first).
3. Read the latest design doc (`docs/harness/plans/*-design.md`, sort descending, take first).
4. Read `docs/harness/contract.md`. Confirm `## Status: AGREED` is present; abort with
`flame-harness-generator: contract not AGREED — run flame-harness-contract first` if missing.
5. Read `checkpoint` from `state.md` (see `docs/harness-protocol.md` §2). **Re-entry:** if a prior run
crashed/paused mid-phase, skip every sub-phase `≤ checkpoint` (its HARD GATE already passed and its
output exists) and resume at the next one — e.g. `checkpoint: 5a` → start at 5b. If `checkpoint` is
`""`, start at 5a. This makes the generator safe to re-run without redoing (or colliding with)
completed work.
### Round N > 1 — fix only listed failures (feedback intake)
When `current_round` is greater than 1:
1. Read `docs/harness/feedback/round-<N-1>-qa.md` (per `docs/harness-protocol.md` §5 layout).
2. Parse the `## Failed Criteria` section to extract each failing criterion and its prescribed fix.
3. Do NOT redesign or refactor areas that were not listed as failures. Make only the minimum
changes required to satisfy the listed fixes.
4. If `docs/harness/feedback/round-<N-1>-qa.md` does not exist, abort with:
`flame-harness-generator: feedback file for round <N-1> not found — cannot determine fixes`.
5. Apply each fix, then run the HARD GATE for the affected sub-phase before writing the handoff.
---
## Sub-phase 5a — Scaffold + Core Loop
### 5a.1 Create the Flutter project (flutter create)
Run `flutter create`. The Dart **package name** must be snake_case (lowercase + underscores, no
hyphens) — convert `app_slug` (e.g. `swing-line` → `swing_line`). This package name is separate from
the bundle id:
```bash
GAME=<projects-dir>/<app_slug>
if [ -d "$GAME/lib" ]; then
echo "project already exists — reusing (skipping flutter create)"
else
flutter create --org com.<company> --project-name <app_slug_snake_case> "$GAME"
fi
```
**Idempotency:** `flutter create` fails if the target already exists, which would break a resume or a
re-run of round 1. The guard above makes 5a.1 safe to re-enter — it reuses an existing scaffold instead
of hard-failing. (If the user passed `--clean`, `rm -rf "$GAME"` first, then create fresh.)
**Bundle id — set it explicitly and IDENTICALLY on both platforms (do not trust the value
`flutter create` derives from the project name).** `flutter create` builds the bundle id from
`--org` + project-name, which can leave underscores / case differences and can diverge between iOS
and Android. Force both to the canonical `config.bundle_id` (`com.<company>.<id>`, lowercase
`[a-z0-9]` only — see `docs/harness-protocol.md`):
- iOS: set `PRODUCT_BUNDLE_IDENTIFIER` = `<bundle_id>` in **all three** build configs
(Debug/Release/Profile) in `ios/Runner.xcodeproj/project.pbxproj`.
- Android: set **both** `applicationId` and `namespace` = `<bundle_id>` in `android/app/build.gradle.kts`.
- Verify iOS `PRODUCT_BUNDLE_IDENTIFIER` == Android `applicationId` == `config.bundle_id`,
**byte-for-byte** (same case, no `_`, no `-`). The AdMob app and store records use this exact id.
**Important (per `docs/harness-protocol.md`):** After `flutter create` succeeds, move the
`docs/harness/` directory into the game project so all artifacts share one repository:
```bash
mv <projects-dir>/flutter-flame-harness/docs/harness \
<projects-dir>/<app_slug>/docs/harness
```
All subsequent reads/writes of harness files use the new path inside the game project.
### 5a.2 Remove default template files
Delete the generated counter demo to start from a clean slate:
```bash
rm <projects-dir>/<app_slug>/lib/main.dart
rm <projects-dir>/<app_slug>/test/widget_test.dart
```
These files will be replaced by the game implementation below.
### 5a.3 Configure pubspec.yaml
Set the following fields in `pubspec.yaml`:
- `name`: `<app_slug>` (from `config.md`)
- `description`: `<app_name>` (from `config.md`)
- `publish_to: none`
- `version: 1.0.0+1` — explicit semver `MAJOR.MINOR.PATCH+BUILD` (App Store marketing version =
`1.0.0`, not `1.0`; the build phase bumps the `+BUILD` on each upload).
Add to `dependencies` (use the latest compatible versions; minimum versions shown):
```yaml
dependencies:
flutter:
sdk: flutter
flame: ^1.37.0
flame_audio: ^2.0.0
google_mobile_ads: ^5.0.0
shared_preferences: ^2.0.0
flutter_secure_storage: ^9.0.0 # durable save: iOS Keychain (survives reinstall/device)
play_services_block_store: ^0.8.0 # durable save: Android Block Store (survives reinstall/device)
```
Add to `dev_dependencies` (`image` is used by `tool/gen_icon.dart` + `tool/strip_bg.dart`):
```yaml
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter # core-loop integration test (5c.8)
flutter_lints: ^4.0.0
image: ^4.0.0
flutter_launcher_icons: ^0.14.0
flutter_native_splash: ^2.4.0
```
Add asset directories to `flutter:`:
```yaml
flutter:
assets:
- assets/images/
- assets/audio/
- assets/icons/
```
Run `flutter pub get` and confirm it exits 0.
### 5a.4 Create the lib/ directory structure
Create the following directories (and `assets/` directories):
```
lib/
game/
components/
systems/
data/
screens/
ui/
l10n/
assets/
images/
audio/
icons/
```
This structure matches the PRD's `lib/` layout.
### 5a.5 game_config.dart — all tuning constants
Create `lib/game/game_config.dart`. This file must centralize every tuning constant that
affects gameplay (speeds, spawn rates, scores, timings, physics). No magic numbers are
permitted in any other game file — all values must reference a constant in `GameConfig`.
Template:
```dart
// lib/game/game_config.dart
// All tuning constants for <app_name>. Edit here to tweak gameplay — do not
// hard-code numbers elsewhere.
abstract class GameConfig {
// Screen / world
static const double worldWidth = 360.0;
static const double worldHeight = 640.0;
// Player
static const double playerSpeed = 200.0;
static const double playerJumpForce = 500.0;
// Scoring
static const int scorePerEnemy = 10;
static const int scorePerDistance = 1;
// Difficulty
static const double initialSpawnInterval = 2.0; // seconds
static const double minSpawnInterval = 0.4;
static const double difficultyRampRate = 0.02; // per second
// Audio
static const double bgmVolume = 0.6;
static const double sfxVolume = 0.9;
// AdMob (non-credential tuning only — IDs come from config.md)
static const int adShowIntervalSeconds = 120;
}
```
Fill all values from the PRD's game-mechanics section. Add more constants as needed; remove
any that the specific game does not use.
### 5a.6 GameState enum
Create `lib/game/game_state.dart`:
```dart
// lib/game/game_state.dart
enum GameState {
menu,
playing,
paused,
gameOver,
// Add states as the PRD requires (e.g. levelComplete, shop)
}
```
### 5a.7 FlameGame subclass
Create `lib/game/<app_slug>_game.dart`. This is the root `FlameGame` (or `FlameGame` with
`HasCollisionDetection` when the PRD requires collision detection). Do **not** add the removed
`HasTappables` mixin — it was dropped in Flame 1.7 and will cause a compile error. Tap input is
handled via `TapCallbacks` on individual components (see §5a.8). It must:
- Declare `GameState _state = GameState.menu;` and a getter `GameState get state`.
- Expose `void startGame()`, `void pauseGame()`, `void resumeGame()`, `void gameOver()` methods
that transition `_state` and show/hide Flutter overlays by name.
- Override `onLoad()` to load assets and add the background component (call `super.onLoad()` first).
- Register all overlay names needed by screens (e.g. `'menu'`, `'hud'`, `'pause'`, `'gameOver'`).
- **Performance (see `docs/game-gotchas.md`):** if components need to query peers each frame,
compute the active lists **once** in the game root's `update(dt)` and have components read that
cache — never call `world.children.whereType<X>()` per-component per-frame. Reuse
`static final Paint` objects; don't recreate `Paint`/shaders/blur every frame.
### 5a.8 Input handling
Wire up the input method specified in the PRD:
- Tap/click → `TapCallbacks` mixin on the player component (preferred for mobile).
- Drag → `DragCallbacks` mixin.
- Keyboard → `KeyboardEvents` mixin on the game class.
### 5a.9 main.dart entry point
Create `lib/main.dart`:
```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flame/game.dart';
import 'game/<app_slug>_game.dart';
// import overlay screens here in 5c
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Lock to config.md `orientation` — portrait OR landscape, never both.
// (Native lock is also set in §5c.10 so there is no rotate-on-launch flash.)
await SystemChrome.setPreferredOrientations(
// portrait → [portraitUp, portraitDown]; landscape → [landscapeLeft, landscapeRight]
<DeviceOrientation>[/* fill from config.orientation */],
);
runApp(GameWidget(game: <AppSlugGame>()));
}
```
Replace `<AppSlugGame>` with the actual class name.
**Startup order & lifecycle (required — see the Lifecycle patterns in `docs/game-gotchas.md`).**
`main()` must run in this order: `WidgetsFlutterBinding.ensureInitialized()` →
`await SharedPreferences.getInstance()` (so synchronous prefs reads later work) → orientation →
(if ads) ATT-after-first-frame → UMP consent → `MobileAds.initialize()` → `runApp(...)`.
Host the `GameWidget` in a `StatefulWidget` that implements `WidgetsBindingObserver`:
`didChangeAppLifecycleState` on `paused`/`inactive` → `game.pauseEngine()` + BGM pause; on
`resumed` → resume. Clean up (stop audio, dispose pools, cancel timers) on the game's
`onDetach()`/`onRemove()`. Gate stale input on resume with a `_canInput` flag.
### 5a.10 Minimal passing test
Create `test/game_config_test.dart`:
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:<app_slug>/game/game_config.dart';
void main() {
test('GameConfig constants are positive', () {
expect(GameConfig.worldWidth, greaterThan(0));
expect(GameConfig.worldHeight, greaterThan(0));
expect(GameConfig.bgmVolume, inInclusiveRange(0.0, 1.0));
expect(GameConfig.sfxVolume, inInclusiveRange(0.0, 1.0));
});
}
```
### 5a.11 CI workflow
Copy `templates/ci.yml.template` → `.github/workflows/ci.yml` (GitHub Actions running
`flutter analyze --no-fatal-infos` + `flutter test` on push/PR), mirroring the shipped games' CI.
### 5a.12 Initialize version control
The generated game is its own git repository — initialize it and commit as the build progresses, so
the work has atomic history from the start (and the user can diff/revert per sub-phase).
`flutter create` already wrote a complete, standard Flutter `.gitignore` (it ignores `build/`,
`.dart_tool/`, IDE files, Pods, etc.). **Do not overwrite it** — append the harness's
credential/secret excludes on top, by appending `templates/gitignore.template` (covers `secrets/`,
`*.jks`/`*.p8`/`*.p12`, `key.properties`, `play-store-key.json`, `**/google-services.json`,
`ios/Runner/Runner.entitlements`, `ios/fastlane/certs/`). These guarantee the signing/API key files
the build phase copies into the game are never committed.
```bash
cd <projects-dir>/<app_slug>
git init -q
cat <plugin-templates>/gitignore.template >> .gitignore # harness credential/secret excludes
git add -A && git commit -q -m "chore: scaffold <app_name> (flame-harness 5a)"
```
`<plugin-templates>` is this plugin's `templates/` directory. **`pubspec.lock` is committed** (this is
an app — reproducible builds), so never add it to `.gitignore`; `git add -A` includes it by design.
Use **Conventional Commits** and **never add AI-authorship trailers** (no `Co-Authored-By`, no
"Generated with…" line). Make one atomic commit at each sub-phase gate (5a/5b/5c) and one per
discrete fix on round N > 1.
### 5a HARD GATE
After completing steps 5a.1–5a.12, run both commands and confirm both exit 0 before
proceeding to Sub-phase 5b:
```bash
cd <projects-dir>/<app_slug>
flutter analyze
flutter test
```
`flutter analyze` must report **0 issues**. `flutter test` must report **0 failures**. When green,
commit: `git add -A && git commit -q -m "feat: core loop scaffold passes analyze+test (5a)"`, then
write `checkpoint: 5a` to `state.md` (atomically) so a later re-entry skips 5a.
**If either command fails, fix all reported issues before proceeding. Do not start 5b until
this gate is green.**
---
## Sub-phase 5b — Systems and Components
Sub-phase 5b implements all game entities, systems, and data catalogs as specified in the PRD.
Start only after the 5a HARD GATE passes.
### 5b.1 Player component
Create `lib/game/components/player_component.dart`. The player component must:
- Extend `SpriteAnimationComponent` (or `SpriteComponent` for static sprites).
- Implement input callbacks from 5a.8.
- Use constants from `GameConfig` for speed, jump force, and any other tunable values.
- Include a hitbox for collision detection if the PRD requires it.
### 5b.2 Enemy / obstacle components
For each enemy or obstacle type named in the PRD, create a separate file under
`lib/game/components/`. Each component:
- Extends `SpriteAnimationComponent` or `SpriteComponent`.
- Uses `GameConfig` constants for speed and behaviour parameters.
- Calls a callback on collision.
### 5b.3 Spawning system
Create `lib/game/systems/spawn_system.dart`. This system:
- Reads spawn intervals and difficulty parameters from `GameConfig`.
- Implements the difficulty ramp defined in the PRD (increase spawn rate over time).
- Is registered on the game class with `add(SpawnSystem())`.
- Draws entity definitions from the data catalog (see 5b.6).
### 5b.4 Collision system
Create `lib/game/systems/collision_system.dart` (or integrate via `CollisionCallbacks` on
each component if simpler). Handle:
- Player vs enemy/obstacle: trigger game over or reduce health as the PRD specifies.
- Player vs collectible: increment score and play SFX.
### 5b.5 Scoring system
Create `lib/game/systems/score_system.dart`. This system:
- Maintains the current score as an `int`.
- Exposes `void addScore(int points)` and `int get score`.
- Fires a callback or `ValueNotifier` update when score changes so the HUD can update.
- Persists the high score via `shared_preferences` (key: `highScore`).
### 5b.6 Audio system
**Audio assets (default = code-synthesized, so the game always ships with sound).** Copy
`templates/build_audio.dart.template` → `tool/build_audio.dart`, tune its notes/tempo to the game's
mood, and `dart run tool/build_audio.dart` to write `assets/audio/*.wav` (22 kHz mono 16-bit,
iOS-safe). If the design asset plan sourced real audio instead, use it (convert to WAV per
`docs/game-gotchas.md`). Either way the game must reference only audio files that exist.
Create `lib/game/systems/audio_system.dart`. Follow the **Audio** patterns in
`docs/game-gotchas.md` (cite it; do not restate). Concretely this system:
- Pre-warms an `AudioPool` for each **frequent** SFX in `onLoad()` (1–3 players); `playSfx(name)`
calls `pool.start()`. Rare one-offs may use `FlameAudio.play()`.
- Throttles repeated SFX (~70 ms per key) to avoid burst stutter.
- Wraps pool creation and every play/BGM call in **try/catch** with `debugPrint` — a missing or
bad audio asset must never crash; playback is skipped silently.
- BGM: `FlameAudio.bgm.play(...)` only in the `playing` state; `FlameAudio.bgm.stop()` on game-over,
on app-background, and in `onRemove()`/`onDetach()`.
- Applies per-channel volume caps from `GameConfig` (e.g. `bgmVolume`, `sfxVolume` are safe caps;
user slider is a fraction of the cap), and respects mute toggles in `shared_preferences`
(`bgmEnabled`, `sfxEnabled`).
### 5b.6a Haptics system
Create `lib/systems/haptics.dart` — a pure-Dart haptics helper per the **Haptics** patterns in
`docs/game-gotchas.md`. Gameplay never calls `HapticFeedback.*` directly. The helper:
- No-ops unless `!kIsWeb && (Platform.isIOS || Platform.isAndroid)`.
- Enforces a global throttle (~60 ms minimum gap) so bursts don't machine-gun the motor.
- Has a persisted `Haptics.enabled` toggle, and wraps every call in try/catch.
- Exposes intent methods (e.g. `light()`, `medium()`, `heavy()`) over `HapticFeedback`.
(Optional, for haptic-heavy games: a native iOS `UIImpactFeedbackGenerator` MethodChannel — see
`docs/game-gotchas.md`. Baseline Dart is sufficient otherwise.)
### 5b.7 Difficulty system
Create `lib/game/systems/difficulty_system.dart`. This system:
- Tracks elapsed play time.
- Updates spawn interval and enemy speed using `GameConfig.difficultyRampRate` and
`GameConfig.minSpawnInterval`.
### 5b.8 Data catalogs
Create data files under `lib/game/data/` for each data-driven element the PRD defines
(enemies, levels, waves, collectibles, shop items). Each catalog is a Dart list of plain
data objects. No enemy/level/wave values may be hardcoded in spawning or component logic —
all values must come from the catalog.
### 5b.9 Tests for systems
Scaffold real system tests from `templates/test_system.dart.template` (copy to
`test/systems_test.dart` and adapt to this game's systems). Write **at least 3** focused unit tests
covering the game's actual systems, e.g.:
- `ScoreSystem.addScore` increments correctly (and `reset` zeroes it).
- `DifficultySystem` increases spawn rate over time but never below `minSpawnInterval`.
- Data catalog entries are non-empty and all numeric fields are positive.
These are the minimum; add one per non-trivial system the PRD defines (matches the shipped games,
which carry 8–21 unit tests). Contract gate R11 requires ≥3 system unit tests.
### 5b HARD GATE
After completing steps 5b.1–5b.9, run:
```bash
cd <projects-dir>/<app_slug>
flutter analyze
flutter test
```
`flutter analyze` must report **0 issues**. `flutter test` must report **0 failures**. When green,
commit: `git add -A && git commit -q -m "feat: game systems and components (5b)"`, then write
`checkpoint: 5b` to `state.md`.
**If either command fails, fix all reported issues before proceeding. Do not start 5c until
this gate is green.**
---
## Sub-phase 5c — UI, Content, and Polish
Sub-phase 5c wires up all Flutter screens and overlays, adds l10n, applies design tokens,
and completes `shared_preferences` persistence. Start only after the 5b HARD GATE passes.
### 5c.1 Design tokens
Create `lib/ui/design_tokens.dart` from the design doc's `## Design tokens` section, using
the exact file template specified in the design doc. Every colour, spacing, and radius value
must come from this file — no raw hex or numeric literals in UI code.
### 5c.2 Flutter screens and overlays
For each screen the PRD requires, create a file under `lib/screens/`. Each screen is a
`StatefulWidget` or `StatelessWidget` that receives the game instance and calls its state
transition methods. Required screens (add or remove based on PRD):
| File | Overlay name | Trigger |
|---|---|---|
| `lib/screens/main_menu_screen.dart` | `'menu'` | game starts |
| `lib/screens/hud_screen.dart` | `'hud'` | game playing |
| `lib/screens/pause_screen.dart` | `'pause'` | pause button tapped |
| `lib/screens/game_over_screen.dart` | `'gameOver'` | player dies |
| `lib/screens/settings_screen.dart` | `'settings'` | settings button |
If the PRD defines a shop, add `lib/screens/shop_screen.dart` with overlay name `'shop'`.
Register all overlay builder functions in `main.dart`'s `GameWidget.overlayBuilderMap`.
**Accessibility baseline (gate R10 — see `docs/game-gotchas.md` → Accessibility & safety):** every
tappable control on these menu/overlay screens must (1) be at least **48×48 dp**, and (2) carry a
`Semantics(label: …)` (or a `Tooltip`/labelled widget) — especially icon-only buttons (pause,
settings, restart) — so the menus are screen-reader navigable. Read the OS **Reduce Motion** setting
via `MediaQuery.of(context).disableAnimations` and expose it (e.g. through the settings/prefs service)
so gameplay effects can damp screen-shake/particles/flashing; **never flash faster than 3×/second**.
### 5c.3 HUD widget
Create `lib/ui/hud.dart` (a Flutter widget overlay, not a Flame component). The HUD must
display the current score, health or lives (if the PRD defines them), and a pause button.
All sizes and colours use `DesignTokens` constants.
### 5c.4 KO/EN localisation
Create ARB files:
- `lib/l10n/app_<default_language>.arb` — primary-language strings (`default_language`); add `app_en.arb` as a secondary locale when `default_language` ≠ `en`.
- `lib/l10n/app_en.arb` — English strings.
Every user-visible string in every screen and overlay must have an ARB entry. No string
literals in widget code — use `AppLocalizations.of(context).<key>`.
Add to `pubspec.yaml`:
```yaml
flutter:
generate: true
dependencies:
flutter_localizations:
sdk: flutter
intl: ^0.19.0
```
Verify l10n is complete:
```bash
cd <projects-dir>/<app_slug>
flutter gen-l10n
```
### 5c.5 Persistence — durable, device-transfer-surviving (default ON)
Persist high score, BGM/SFX enabled flags, and any economy totals through a single
`PreferencesService` class. No raw `SharedPreferences` calls outside that class.
Persistence is **durable by default** — saves survive app reinstall *and* moving to a new device,
not just `shared_preferences` (which iOS drops on delete). Mirror the shipped games' proven pattern:
1. Copy `templates/save_repository.dart.template` → `lib/data/save_repository.dart`, replacing
`__SAVE_KEY__` with `<app_slug>_save_v1`. It stores one JSON blob across three tiers —
**iOS Keychain** (`flutter_secure_storage`, `first_unlock`), **Android Block Store**
(`play_services_block_store`), and a `shared_preferences` mirror — reading durable-first, writing
all tiers, every call in try/catch so a backend failure degrades instead of crashing.
2. `PreferencesService` is backed by `SaveRepository`: load `readMap()` once at startup into an
in-memory map; each setter mutates the map and writes `SaveRepository.write(jsonEncode(map))`.
All getters/setters go through it — no raw `SharedPreferences` and no direct `SaveRepository`
calls elsewhere.
This is **not** real-time cloud sync — it's last-write-wins restore on a fresh install/device.
See `docs/game-gotchas.md` (Persistence). Gated **R9**.
### 5c.6 AdMob wiring (if skip_admob is false in config.md)
If `skip_admob` is `false`:
- Initialise `MobileAds.instance.initialize()` in `main.dart` before `runApp`.
- Read AdMob app ID and ad unit IDs from `config.md` — never hardcode in Dart source.
- Implement a banner ad widget in `lib/ui/banner_ad_widget.dart`.
- Show the banner in the HUD overlay at the bottom.
### 5c.7 Anti-stub verification
Before the final HARD GATE, run:
```bash
grep -rn "TODO\|stub\|placeholder\|스텁\|미구현" \
<projects-dir>/<app_slug>/lib/ --include="*.dart"
```
If this command returns any output, fix or remove every match. The contract requires zero
stubs in game logic (per `docs/harness-protocol.md` §3, Hard Gate 3).
### 5c.8 Full tests
Scaffold the two higher-level tests (contract gate R11 requires **≥1 widget test + ≥1 integration
test**), then expand coverage:
- **Widget test** — copy `templates/widget_test.dart.template` → `test/menu_widget_test.dart`; adapt
to the real main-menu screen. Assert it renders and its Play control (Semantics-labelled, ≥48dp per
R10) is tappable and triggers game start.
- **Integration test** — copy `templates/integration_test.dart.template` →
`integration_test/app_test.dart`; adapt to drive the core loop (boot → play → game-over) and assert
no exception. (Requires the `integration_test` dev dep added in 5a.3.)
- Game-over screen shows the correct score.
- Localisation: all required ARB keys are present in every configured `app_<locale>.arb` (`default_language`, plus `app_en.arb` when `default_language` ≠ `en`).
- `PreferencesService` read/write round-trip (use a mock `SharedPreferences`).
### 5c.9 App branding — icon · splash · display name
A shipped game must not have the default Flutter icon, default splash, or a slug-ish name. Follow
the **Build/platform** + **Store rejections** patterns in `docs/game-gotchas.md`.
1. **Icon + splash art.** Default: copy `templates/gen_icon.dart.template` → `tool/gen_icon.dart`,
fill its color constants from `design_tokens` (Background / Primary / Accent RGB) and `kGlyph`
from the first letter of `app_name`, add `image: ^4.0.0` to `dev_dependencies`, then
`dart run tool/gen_icon.dart` → writes `assets/icons/icon.png` (1024×1024, **opaque/no-alpha**, iOS),
**`assets/icons/icon-fg.png`** (Android adaptive **foreground**, transparent, motif inside the safe
zone), **`assets/icons/icon-mono.png`** (Android 13+ **themed/monochrome** layer, white on
transparent), `assets/images/splash.png`, **`assets/store/play_icon.png` (512×512 Play hi-res icon)**,
and **`assets/store/feature_graphic.png` (1024×500 Play feature graphic)**. The screenshot phase
places the last two into the Android listing. If the design asset plan chose AI-generated art, use it
at `assets/icons/icon.png` (flattened **opaque**) **and** produce a padded `icon-fg.png` + a
`icon-mono.png` — do not reuse the full-bleed art as the adaptive foreground (see below).
2. **Config + run the tools.** Add the `flutter_launcher_icons` block to `pubspec.yaml` with **all**
Android adaptive layers, not just `image_path` — a full-bleed `image_path` reused as the Android
icon is cropped/"zoomed" by the launcher mask (108dp canvas, only the center ~66dp is safe;
`docs/game-gotchas.md` → Build/platform). Then `flutter_native_splash` (`color:` = design
Background, `image: assets/images/splash.png`):
```yaml
flutter_launcher_icons:
android: true
ios: true
remove_alpha_ios: true
image_path: assets/icons/icon.png # iOS + legacy Android
adaptive_icon_background: "<design Background hex>" # solid color, opaque
adaptive_icon_foreground: assets/icons/icon-fg.png # safe-zone padded
adaptive_icon_monochrome: assets/icons/icon-mono.png # Android 13+ themed
min_sdk_android: 21
```
```bash
dart run flutter_launcher_icons
dart run flutter_native_splash:create
```
Verify Android adaptive layers were emitted: `ls android/app/src/main/res/mipmap-anydpi-v26/`
should contain `ic_launcher.xml` referencing `<foreground>` (and `<monochrome>`).
3. **Localized display name.** Set `app_name` as the home-screen name: iOS `CFBundleDisplayName`
(base `Info.plist` + per-locale `<locale>.lproj/InfoPlist.strings`, register in the Xcode project);
Android `android:label="@string/app_name"` + `values/strings.xml` (+ `values-<locale>/strings.xml`)
for `default_language` and English. Not "Runner" / not the slug.
### 5c.10 Native platform config
Apply per `docs/game-gotchas.md` (Build/platform + Store rejections):
1. **Orientation lock (native — remove the unused orientation).** Read `config.orientation`.
- iOS `Info.plist` `UISupportedInterfaceOrientations` = only the chosen set (portrait →
`UIInterfaceOrientationPortrait`; landscape → `…LandscapeLeft` + `…LandscapeRight`), and **remove**
`UISupportedInterfaceOrientations~ipad`.
- Android: on the main `<activity>` set `android:screenOrientation="portrait"` (or
`"sensorLandscape"` for landscape).
This removes the other orientation entirely, so the app opens directly in its orientation with no
rotate-on-launch flash (`setPreferredOrientations` in `main.dart` alone is not enough).
2. **Remove iPadOS.** Set `TARGETED_DEVICE_FAMILY = 1` (iPhone-only) in `ios/Runner.xcodeproj`
(both build configs).
3. **Export compliance.** Add `ITSAppUsesNonExemptEncryption = false` to `ios/Runner/Info.plist`
(skips the per-upload export-compliance prompt).
4. **Root back-button (Android).** Wrap the root/menu screen in `PopScope(canPop: false, ...)`: if an
overlay is open, close it; otherwise show a Flutter `SnackBar` ("뒤로 한 번 더 누르면 종료" /
"Press back again to exit", localized) and only exit on a second back within ~2 s. (In-game, back
= pause — see game-gotchas.)
### 5c.11 Game assets (visuals)
Default: **code-drawn visuals** — render players/enemies/UI with `CustomPainter` / Flame shapes using
the `design_tokens` palette (zero external image files, always renders). Only if the design asset plan
chose **sprite art** (AI-generated or a free/CC0 pack): obtain the images, then run
`tool/strip_bg.dart` (copy from `templates/strip_bg.dart.template`) to flood-fill the background to
alpha, and place the cleaned PNGs under `assets/images/`. **Every asset path referenced in code or
declared in `pubspec.yaml` must exist on disk** — no dangling references (a missing asset is a
runtime crash / blank). The harness never depends on un-sourced art: if nothing was sourced, the game
ships fully code-drawn.
### 5c.12 Project README + LICENSE
A shipped game has a `README.md` and a `LICENSE` (all the reference games do). Create both at the game
project root:
- **`README.md`** — from `templates/README.md.template`, filling: `<app_name>` (title + one-line pitch
from the PRD), a short gameplay summary, the tech stack (Flame + the actual `pubspec.yaml` deps:
audio, ads, l10n, durable save), and run/test/build commands. Add one asset-credits line stating the
default sourcing (audio = code-synthesized, visuals = code-drawn) — adjust if sprites were sourced.
Author it in the project's `default_language` (add an English section when it isn't `en`).
- **`LICENSE`** — from `templates/LICENSE.template` (MIT), with `Copyright (c) <year> <company>` taken
from `config.developer` (`company`, current year). If the user prefers a different license, honour it.
Commit them with the 5c gate commit below.
### 5c HARD GATE
After completing steps 5c.1–5c.11, run:
```bash
cd <projects-dir>/<app_slug>
flutter analyze
flutter test
```
`flutter analyze` must report **0 issues**. `flutter test` must report **0 failures**. Also confirm
branding (5c.9): a **custom** icon + splash were generated (not the default Flutter art), the icon is
**opaque (no alpha)**, and the native display name equals `app_name` (not "Runner"/slug). And native
config (5c.10): orientation locked natively to `config.orientation` (unused one removed), iPhone-only
(`TARGETED_DEVICE_FAMILY = 1`), `ITSAppUsesNonExemptEncryption = false`, root back-button SnackBar.
And assets/CI (5b.6 / 5c.11 / 5a.11): the game ships with **synthesized (or sourced) audio** and
**code-drawn (or cleaned-sprite) visuals**, **no missing-asset references**, and a
`.github/workflows/ci.yml` is present.
**This is the final gate. Do not write the handoff until both commands pass. If either fails,
fix all reported issues and re-run both commands.** When green, commit:
`git add -A && git commit -q -m "feat: UI, content, branding and native config (5c)"`, then write
`checkpoint: 5c` to `state.md` before writing the handoff.
---
## Self-evaluation and Handoff
After the 5c HARD GATE passes, write the generator handoff and update pipeline state.
### Write handoff/round-N-gen.md
Create `docs/harness/handoff/round-<N>-gen.md` inside the game project, following the layout
defined in `docs/harness-protocol.md` §4:
```markdown
# Generator Handoff — Round <N>
## What Was Built / Fixed
<!-- Bullet list of features implemented (round 1) or bugs fixed (round N>1). -->
## Contract Self-Assessment
| Criterion | Status | Notes |
|---|---|---|
| flutter analyze zero errors | DONE / PARTIAL / FAIL | … |
| flutter test zero failures | DONE / PARTIAL / FAIL | … |
| No TODO/stub in game logic | DONE / PARTIAL / FAIL | … |
| game_config.dart for all tuning | DONE / PARTIAL / FAIL | … |
| Content defined as data | DONE / PARTIAL / FAIL | … |
| l10n complete (all configured locales) | DONE / PARTIAL / FAIL | … |
| Core loop end-to-end | DONE / PARTIAL / FAIL | … |
| Zero crashes on simulator | DONE / PARTIAL / FAIL | … |
| <game-specific criterion> | DONE / PARTIAL / FAIL | … |
## Test Results
\`\`\`
<last 20 lines of flutter analyze output>
\`\`\`
\`\`\`
<last 20 lines of flutter test output>
\`\`\`
## Environment Detection
- Flutter version: <output of `flutter --version`>
- Dart version: <from above>
- Device/emulator used for smoke test: <name>
## Known Issues
<!-- List any known issues or deferred items. State "none" if clean. -->
```
Fill every section with actual output and real assessments. Do not leave placeholder text.
### Update state.md
Update `docs/harness/state.md` per `docs/harness-protocol.md` §2 and the `generator → evaluator`
transition in §7. Per §7 rule 2, a successful phase completion sets `status: running` in the
same atomic write:
```yaml
status: running
current_phase: generator
next_role: evaluator
updated_at: "<ISO-8601 UTC now>"
```
Leave `current_round`, `created_at`, `resume_attempts`, and all other keys unchanged. Use
`Edit` for a targeted update.
> **Note:** `current_round` is incremented by the evaluator when it returns FAIL; the generator reads it but does not write it.
### Append to pipeline-log.md
Append one row to `docs/harness/pipeline-log.md` per `docs/harness-protocol.md` §6:
```
| <ISO-8601 UTC now> | handoff | generator | round <N> built; analyze 0; test 0; next: evaluator |
```
---
## Error handling
- If `contract.md` is missing or does not contain `## Status: AGREED`, abort immediately.
- If any HARD GATE fails (non-zero `flutter analyze` issues or failing tests), stop at that
sub-phase, fix the failures, and re-run the gate. Do not advance to the next sub-phase.
- If the feedback file for round N-1 is missing on a round > 1 run, abort with a clear message
and set `state.md` to `status: paused`, `pause_reason: manual_action`.
- If `flutter create` fails, abort immediately and do not proceed to 5a.3.
- If `flutter pub get` fails, check `pubspec.yaml` for version conflicts and resolve before
continuing.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!