simulation-shared

Shared simulation vocabulary reused across environment, evaluation, worker, and browser-adjacent helpers.

This boundary exists so the example can share observation semantics and deterministic spawn logic without letting every runtime invent its own near- duplicate types. The payoff is consistency: when a policy sees a gap, or when a helper estimates urgency, those meanings stay aligned across the whole example.

This is the common language layer for the control problem. It does not own full simulation stepping, scoring, or rendering. It owns the smaller contracts those larger boundaries must agree on: difficulty profiles, pipe geometry, observation features, temporal memory, and action decoding.

That shared vocabulary is what keeps the demo honest across runtimes. The environment can advance the world, the evaluation layer can score policies, the worker can stream playback, and browser helpers can inspect decisions without silently redefining what "next gap" or "urgent correction" means.

What This Folder Is Trying To Teach

Read this chapter if you want to answer three practical questions:

  1. Which geometric signals does the policy actually see?
  2. How does the example add short-horizon memory without requiring recurrent networks?
  3. How do difficulty, spawn, observation, and control helpers stay reusable across Node training and browser playback?

Shared Vocabulary Map

flowchart LR
    Difficulty["difficulty utils\ncurriculum profile"] --> Spawn["spawn utils\nnext pipe cadence and gap"]
    Spawn --> World["environment + worker playback\nconcrete world state"]
    World --> Features["observation/\nfeature synthesis"]
    Features --> Memory["memory utils\nstack recent frames and actions"]
    Features --> Vector["observation/\ncanonical policy vectors"]
    Vector --> Control["control utils\nresolve flap decision"]
    Memory --> Control

    Browser["browser inspection helpers"] -.-> Features
    Evaluation["evaluation/"] -.-> Features
    Worker["flappy-evolution-worker/"] -.-> Memory

    classDef boundary fill:#001522,stroke:#0fb5ff,color:#9fdcff,stroke-width:2px;
    classDef runtime fill:#03111f,stroke:#00e5ff,color:#d8f6ff,stroke-width:2px;
    classDef highlight fill:#2a1029,stroke:#ff4a8d,color:#ffd7e8,stroke-width:3px;

    class Difficulty,Spawn,Features,Memory,Vector,Control boundary;
    class World,Browser,Evaluation,Worker runtime;
    class Features highlight;

The key teaching point is that the policy does not read pixels. It reads a curated state representation: gap geometry, velocity, urgency, and a short action-conditioned memory trail. That makes the control problem easier to inspect and keeps training, evaluation, and playback aligned around the same semantics.

Choose Your Route

Example sketch:

const difficultyProfile = resolveAdaptiveDifficultyProfile(pipesPassed, 1);
const features = resolveObservationFeatures({
  birdYPx,
  velocityYPxPerFrame,
  pipes,
  visibleWorldWidthPx,
  difficultyProfile,
  activeSpawnIntervalFrames: difficultyProfile.pipeSpawnIntervalFrames,
});
const networkInput = resolveTemporalObservationVector(
  features,
  observationMemoryState,
);
const didFlap = resolveFlapDecision(network.activate(networkInput));

If you want background reading, Wikipedia contributors on feature engineering, curriculum learning, and frame stacking are useful bridges, but this folder is where those ideas become concrete contracts for the Flappy Bird example.

simulation-shared/simulation-shared.types.ts

SharedDifficultyProfile

Shared runtime difficulty profile used by browser and environment simulators.

By projecting difficulty into one plain object, the example can keep curriculum logic independent from rendering, evaluation, and worker runtime concerns.

SharedObservationFeatures

Structured observation features for network input.

Educational note: These features make the policy input interpretable. The example does not feed raw pixels into NEAT; it feeds geometric signals such as distance to the next pipe, corridor clearance, and urgency of recovering to the gap center.

SharedObservationInput

Input shape for observation-feature synthesis.

This object is the raw world snapshot from which normalized features are derived. It intentionally separates world geometry from the later feature projection step.

SharedObservationMemoryState

Mutable temporal memory attached to one policy-controlled bird.

The memory stores recent core observation frames and recent action history, allowing feedforward policies to consume short-term context without adding recurrent connections.

If you want background reading, the Wikipedia article on "frame stacking" captures the basic idea of giving a feed-forward policy a short motion trail instead of full recurrent state.

SharedPipeLike

Common pipe shape consumed by observation helpers.

This is the narrowest useful pipe contract for feature synthesis: horizontal position plus the vertical gap geometry seen by the bird.

SharedRngLike

Minimal deterministic random contract used by shared spawn helpers.

The shared layer keeps its RNG contract intentionally small so the same spawn helpers can work with both Node-side and browser-side deterministic sources.

simulation-shared/simulation-shared.constants.ts

Default curriculum scale used when callers do not provide one.

A value of 1 means full adaptive difficulty behavior is enabled.

FLAPPY_SHARED_DEFAULT_DIFFICULTY_SCALE

Default curriculum scale used when callers do not provide one.

A value of 1 means full adaptive difficulty behavior is enabled.

FLAPPY_SHARED_DEFAULT_NORMALIZATION_EPSILON

Small positive epsilon used to guard divisions in normalized timing features.

The epsilon avoids unstable divide-by-zero behavior when distances or speeds collapse toward zero during normalization.

simulation-shared/simulation-shared.math.utils.ts

Clamps a numeric value to the inclusive [min, max] interval.

clamp

clamp(
  value: number,
  min: number,
  max: number,
): number

Internal clamp primitive.

Parameters:

Returns: Clamped value.

clamp01

clamp01(
  value: number,
): number

Clamps a numeric value to the inclusive [0, 1] interval.

Parameters:

Returns: Value clamped between 0 and 1.

clampValue

clampValue(
  value: number,
  min: number,
  max: number,
): number

Clamps a numeric value to the inclusive [min, max] interval.

Parameters:

Returns: Clamped value.

interpolateValue

interpolateValue(
  startValue: number,
  endValue: number,
  progress: number,
): number

Linear interpolation helper.

Parameters:

Returns: Interpolated value.

simulation-shared/simulation-shared.difficulty.utils.ts

resolveAdaptiveDifficultyProfile

resolveAdaptiveDifficultyProfile(
  pipesPassed: number,
  difficultyScale: number,
): SharedDifficultyProfile

Resolves adaptive difficulty profile from passed-pipe progress.

Educational note: Difficulty is ramped as a smooth profile rather than as a sequence of hard level jumps. That keeps the task readable for humans and less noisy for evolution.

The idea is closely related to curriculum learning: easier versions of the task dominate early, then the example interpolates toward the harder target settings as progress increases.

Parameters:

Returns: Active difficulty profile.

simulation-shared/simulation-shared.spawn.utils.ts

resolveNextSpawnGapCenterY

resolveNextSpawnGapCenterY(
  previousGapCenterYPx: number,
  rng: SharedRngLike,
  maximumGapCenterYPx: number,
): number

Resolves next gap center with bounded per-pipe delta.

Educational note: Consecutive gaps are deliberately constrained to avoid unfair zig-zag jumps. The environment should still be challenging, but it should not demand an impossible vertical correction from one pipe to the next.

Parameters:

Returns: Next gap center y-position.

resolveNextSpawnGapSize

resolveNextSpawnGapSize(
  previousSpawnGapPx: number | undefined,
  difficultyProfile: SharedDifficultyProfile,
  rng: SharedRngLike,
): number

Resolves next spawn gap size using progressive shrink and jitter.

The gap starts wider than the current hardest target, then shrinks toward the active difficulty profile with a small amount of deterministic jitter so runs do not feel mechanically repetitive.

Parameters:

Returns: Next spawn gap size.

resolveNextSpawnIntervalFrames

resolveNextSpawnIntervalFrames(
  previousSpawnIntervalFrames: number | undefined,
  difficultyProfile: SharedDifficultyProfile,
): number

Resolves next spawn interval using progressive shrink.

This mirrors the gap-size logic: early pipes are spaced more generously, then spacing contracts toward the current difficulty target as the episode settles into its harder rhythm.

Parameters:

Returns: Next spawn interval in frames.

sampleGapCenterY

sampleGapCenterY(
  rng: SharedRngLike,
  maximumGapCenterYPx: number,
): number

Samples a random gap center y-position.

The sampled center is bounded so the resulting pipe gap always remains inside the visible play area.

Parameters:

Returns: Sampled y-position.

simulation-shared/simulation-shared.observation.utils.ts

Shared observation compatibility façade.

The observation implementation now lives under simulation-shared/observation/ so feature synthesis and vector projection can evolve behind a focused module boundary. This file stays as the stable import path for existing callers.

That split keeps the high-level import path simple while allowing the observation subsystem to grow into its own documented folder.

resolveCoreObservationVectorFromFeatures

resolveCoreObservationVectorFromFeatures(
  features: SharedObservationFeatures,
): number[]

Resolves the compact core vector used for temporal stacking.

The core intentionally keeps directly observed kinematic and geometric channels while dropping derived one-step predictors that become redundant once short-term temporal memory is available.

This is the representation used when the example wants a short history of raw observation slices. The idea is similar to frame stacking in reinforcement learning: a feed-forward policy can recover some sense of motion by looking at several recent compact frames at once.

The Wikipedia article on "frame stacking" is a useful conceptual reference.

Parameters:

Returns: Core per-frame vector.

Example:

const coreFrame = resolveCoreObservationVectorFromFeatures(features);
observationMemoryState.previousCoreFrames.push(coreFrame);

resolveObservationFeatures

resolveObservationFeatures(
  input: SharedObservationInput,
): SharedObservationFeatures

Builds the shared normalized observation feature set consumed by policies.

Educational note: This helper stays focused on semantic feature assembly only. Projection into the canonical network vectors now lives in the neighboring vector module so observation policy and network-shape concerns can evolve independently.

The features deliberately mix three kinds of signal:

  1. Current state, such as bird height and vertical velocity.
  2. Near-term geometry, such as gap bounds and upcoming-pipe distances.
  3. Simple forward-looking control hints, such as urgency and one-flap reachability.

This is a compact example of feature engineering for control. Instead of asking NEAT to rediscover basic geometry from raw sensory input, the example hands the network semantically meaningful signals and lets evolution focus on policy search.

For broader context, the Wikipedia article on "feature engineering" is a good companion reference.

Parameters:

Returns: Structured observation features.

Example:

const features = resolveObservationFeatures({
  birdYPx: 120,
  velocityYPxPerFrame: 2,
  pipes,
  visibleWorldWidthPx: 640,
  difficultyProfile,
  activeSpawnIntervalFrames: 90,
});

if (features.normalizedEntryUrgency > 0.8) {
  // The bird is misaligned and running out of time to recover.
}

resolveObservationVectorFromFeatures

resolveObservationVectorFromFeatures(
  features: SharedObservationFeatures,
): number[]

Converts observation features to the canonical 12-value network input vector.

Educational note: This module owns the network-shape projection so feature semantics can change independently from how the policy input is ordered.

The 12-value vector is the compact feed-forward policy input used by the main evaluation and training flow. Its ordering is stable on purpose: once a network topology has evolved against one input layout, silent channel reshuffles would invalidate learned behavior.

Parameters:

Returns: Ordered feature vector.

Example:

const features = resolveObservationFeatures(input);
const networkInput = resolveObservationVectorFromFeatures(features);

resolveUpcomingPipes

resolveUpcomingPipes(
  pipes: SharedPipeLike[],
  birdCenterXPx: number,
  birdRadiusPx: number,
  pipeWidthPx: number,
): [SharedPipeLike | undefined, SharedPipeLike | undefined]

Resolves the next two upcoming pipes in front of the bird.

The observation pipeline only cares about the immediate near future, because Flappy Bird decisions are dominated by the next gap and the transition after it. Looking further ahead adds noise faster than it adds useful control signal.

Parameters:

Returns: Tuple of first and second upcoming pipes.

Example:

const [nextPipe, secondPipe] = resolveUpcomingPipes(pipes);

simulation-shared/simulation-shared.control.utils.ts

Resolves flap/no-flap decision from network outputs.

Educational note: The shared control layer accepts both two-output competitive policies (no flap vs flap) and simpler single-output thresholded policies. That flexibility makes the helper reusable across experiments without forcing every caller to reshape its outputs first.

resolveFlapDecision

resolveFlapDecision(
  rawOutputs: unknown,
  flapThreshold: number,
): boolean

Resolves flap/no-flap decision from network outputs.

Educational note: The shared control layer accepts both two-output competitive policies (no flap vs flap) and simpler single-output thresholded policies. That flexibility makes the helper reusable across experiments without forcing every caller to reshape its outputs first.

Parameters:

Returns: True when flap should trigger.

simulation-shared/simulation-shared.memory.utils.ts

commitSharedObservationMemoryStep

commitSharedObservationMemoryStep(
  observationMemoryState: SharedObservationMemoryState,
  features: SharedObservationFeatures,
  didFlap: boolean,
): void

Commits one observation-action step into temporal memory.

The memory update happens after the decision is made so the next step can see both the recent observation context and the action history that produced the current trajectory.

Parameters:

Returns: Nothing.

createSharedObservationMemoryState

createSharedObservationMemoryState(): SharedObservationMemoryState

Creates an empty temporal observation memory state.

Returns: Fresh mutable memory buffers for one bird/controller.

Example:

const memoryState = createSharedObservationMemoryState();

resolvePreviousCoreFramesWithPadding

resolvePreviousCoreFramesWithPadding(
  observationMemoryState: SharedObservationMemoryState,
): number[][]

Resolves previous core frames (newest-first) with deterministic zero padding.

Zero padding keeps the policy input width stable during the first few frames of an episode before enough history has accumulated.

Parameters:

Returns: Previous core frame list with fixed target length.

resolveTemporalObservationVector

resolveTemporalObservationVector(
  features: SharedObservationFeatures,
  observationMemoryState: SharedObservationMemoryState,
): number[]

Builds the temporal policy input vector (stacked observation + action memory).

Educational note: This helper turns an interpretable feature object into the exact flat vector a feed-forward network consumes. That is why the output layout is documented so explicitly: changing the order would change the meaning of every trained weight in the policy.

Output layout:

  1. current core observation frame
  2. previous core frames (newest to oldest) with zero padding
  3. last-action channel
  4. recent flap-rate channel over a fixed window

Parameters:

Returns: Ordered temporal input vector for policy activation.

resolveZeroCoreObservationFrame

resolveZeroCoreObservationFrame(): number[]

Builds a zero-valued core frame with canonical length.

Returns: Zero core frame.

simulation-shared/simulation-shared.statistics.utils.ts

compareNumbersAscending

compareNumbersAscending(
  leftValue: number,
  rightValue: number,
): number

Compares two numeric values in ascending order.

Parameters:

Returns: Comparator delta for Array.prototype.toSorted.

computeMean

computeMean(
  values: readonly number[],
): number

Computes arithmetic mean for numeric samples.

Parameters:

Returns: Arithmetic mean.

computePercentile

computePercentile(
  values: readonly number[],
  percentile: number,
): number

Computes percentile value via linear interpolation between nearest ranks.

Percentiles are useful in the trainer because they reveal whether strong performance is broad across the population or concentrated in a single outlier.

Parameters:

Returns: Percentile value, or Number.NaN when values is empty.

computePopulationStandardDeviation

computePopulationStandardDeviation(
  values: readonly number[],
  meanValue: number,
): number

Computes population standard deviation.

This uses population variance rather than sample variance because the trainer is summarizing the whole evolved population for that generation, not estimating a larger hidden distribution from a subsample.

Parameters:

Returns: Population standard deviation.

simulation-shared/simulation-shared.errors.ts

Prefix used when formatting unexpected shared-simulation errors.

A stable prefix makes logs easier to scan when multiple Flappy subsystems are emitting diagnostics.

FLAPPY_SHARED_SIMULATION_ERROR_PREFIX

Prefix used when formatting unexpected shared-simulation errors.

A stable prefix makes logs easier to scan when multiple Flappy subsystems are emitting diagnostics.

formatSharedSimulationErrorMessage

formatSharedSimulationErrorMessage(
  error: unknown,
): string

Formats unknown shared-simulation errors for stable logs.

Shared utilities are used from several runtime contexts, so this helper keeps the error surface human-readable even when the thrown value is not an Error instance.

Parameters:

Returns: Readable error message.

Generated from source JSDoc • GitHub