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:

  • DeathsDeathService 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.