Director Feedback Loops
The RunDirector accumulates runtime gameplay signals (deaths, chunk clears, low-health exits) and feeds them through flag-gated policies that adjust per-marker pressure. This lets pacing respond to the player instead of following a fixed curve.
Shipped in Phase 8 of the generation refactor with both policies OFF by default so the accumulator can run harmlessly during a bake window before the behavior turns on.
Event Entry Points
runDirector.recordDeath({ marker, cause, at }); // player died
runDirector.recordChunkCleared({ marker, seconds, at }); // cleared without dying
runDirector.recordLowHealthExit({ marker, healthFraction, at }); // reserved
Current wiring:
- Deaths —
DeathServiceexposes anonPlayerDiedlistener registry (mirrors the existing safe-zone callback pattern).ChunkService.onInit()registers a callback that resolves the death position to a trail marker vialifecycleManager.getChunkIndexAtPosition()and callsrunDirector.recordDeath(). - Chunk cleared / low-health exit — not yet wired. The director APIs are available; hook them from whatever system owns the "chunk completed without dying" notion.
Feedback State
src/apps/chunks/server/director/feedback-state.ts accumulates raw events and produces a FeedbackSignal vector:
interface FeedbackSignal {
recentDeaths: number; // deaths in the last N markers (N=3)
lastDeathMarker: number | undefined;
timeSinceLastDeath: number; // defaults to math.huge
streak: number; // consecutive clears without death
avgRecentClearSeconds: number | undefined;
fastClearStreak: number; // clears faster than FAST_CLEAR_RATIO × avg
}
Constants: RECENT_WINDOW = 3, FAST_CLEAR_RATIO = 0.7.
Call runDirector.resetFeedback() on fresh run start.
Policies
Each policy is a FeedbackPolicy { name, description, apply(signal, marker) → multiplier }. They compose by multiplication in composeFeedbackAdjustment so e.g. a death-ease and a streak-bump partially cancel rather than one silencing the other.
| Policy | Flag | Effect |
|---|---|---|
DeathAwareEasingPolicy | FeedbackDeathEase | Eases the 3 markers after a death. Returns 0.85 / 0.90 / 0.95 for distances 1 / 2 / 3. Max 15% ease. |
StreakPressurePolicy | FeedbackStreakPressure | Adds a small pressure bump after a 4+ marker clean streak. Returns 1.0 + step × 0.025 capped at 1.10. Max 10% bump. |
When a flag is off its policy returns 1.0 (abstains), so getFeedbackPressureMultiplier() always returns safe values whether or not the feedback system is enabled.
Consuming the Multiplier
const base = desiredPressureForMarker(marker);
const adjusted = base * runDirector.getFeedbackPressureMultiplier(marker);
Safe to call unconditionally — returns 1.0 when every feedback flag is off (the default during bake).
Rollout Strategy
- Ship with flags OFF (current state). Accumulator collects data, no pressure change.
- Flip flags ON at ship-time multipliers — modest effect, validate nothing regresses.
- Tune multipliers in
feedback-policies.tsif playtest indicates the effect is too mild or too aggressive.
Rollback at any step: set FeedbackDeathEase(false) and FeedbackStreakPressure(false) — the accumulator keeps running, the multiplier returns 1.0, and the game reverts to fixed pacing.
Key Files
| File | Purpose |
|---|---|
apps/chunks/server/director/feedback-state.ts | Event accumulator + FeedbackSignal vector. |
apps/chunks/server/director/feedback-policies.ts | DeathAwareEasingPolicy, StreakPressurePolicy, composeFeedbackAdjustment. |
apps/chunks/server/director/index.ts | Barrel export. |
apps/chunks/server/services/run-director.ts | Event API + getFeedbackPressureMultiplier. |
apps/chunks/server/services/chunk-service.ts | Registers the onPlayerDied listener. |
apps/global/server/services/death-service.ts | onPlayerDied callback registry. |
shared/feature-flags.ts | FeedbackDeathEase, FeedbackStreakPressure. |
Adding New Signals / Policies
- Extend
FeedbackStatewith the new event method and add the derived field toFeedbackSignal. - Wire the event source (follow the DeathService pattern: callback registry + listener in ChunkService).
- Write a new
FeedbackPolicyinfeedback-policies.ts, flag-gate it, and add it to theALL_POLICIESarray. - Ship with the flag off, bake, then flip.