Skip to main content

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 — DeathService exposes an onPlayerDied listener registry (mirrors the existing safe-zone callback pattern). ChunkService.onInit() registers a callback that resolves the death position to a trail marker via lifecycleManager.getChunkIndexAtPosition() and calls runDirector.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.

PolicyFlagEffect
DeathAwareEasingPolicyFeedbackDeathEaseEases the 3 markers after a death. Returns 0.85 / 0.90 / 0.95 for distances 1 / 2 / 3. Max 15% ease.
StreakPressurePolicyFeedbackStreakPressureAdds 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​

  1. Ship with flags OFF (current state). Accumulator collects data, no pressure change.
  2. Flip flags ON at ship-time multipliers — modest effect, validate nothing regresses.
  3. Tune multipliers in feedback-policies.ts if 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​

FilePurpose
apps/chunks/server/director/feedback-state.tsEvent accumulator + FeedbackSignal vector.
apps/chunks/server/director/feedback-policies.tsDeathAwareEasingPolicy, StreakPressurePolicy, composeFeedbackAdjustment.
apps/chunks/server/director/index.tsBarrel export.
apps/chunks/server/services/run-director.tsEvent API + getFeedbackPressureMultiplier.
apps/chunks/server/services/chunk-service.tsRegisters the onPlayerDied listener.
apps/global/server/services/death-service.tsonPlayerDied callback registry.
shared/feature-flags.tsFeedbackDeathEase, FeedbackStreakPressure.

Adding New Signals / Policies​

  1. Extend FeedbackState with the new event method and add the derived field to FeedbackSignal.
  2. Wire the event source (follow the DeathService pattern: callback registry + listener in ChunkService).
  3. Write a new FeedbackPolicy in feedback-policies.ts, flag-gate it, and add it to the ALL_POLICIES array.
  4. Ship with the flag off, bake, then flip.