Nov 19, 2025
5 min read

Refactoring Evolution: Building a Consistent Vehicle API

Seven different vehicles, seven different APIs - time to unify the chaos and extract the common patterns.

The vehcology project evolves neural networks to control simulated vehicles. CartPole, MountainCar, LunarLander, Slimeball - each one a different physics challenge. Each one evolved with a different codebase structure.

After six months of development, we had seven vehicle implementations. They all did roughly the same thing: take a neural network blueprint, create a simulation, run physics updates, calculate fitness. But they did it seven different ways.

Adding a new vehicle meant studying an existing one and guessing which patterns to copy. Fixing a bug meant checking if the same issue existed in six other places. It was getting unmaintainable.

Time to refactor.

The Audit

I started by reading every vehicle implementation and documenting the patterns. What emerged was simultaneously encouraging and horrifying.

The good news: there were clear common patterns. Every vehicle had a constructor, an update method, a reset method, a CTRNN neural controller, and a physics simulation. The abstractions existed.

The bad news: they were implemented inconsistently everywhere.

Constructor signatures:

// CartPole
Vehicle(bp, opts = {})
// destructures: state, dt, saveFlag, randomSeed

// MountainCar
Vehicle(bp, opts)
// destructures: state, dt

// LunarLander
Vehicle(bp, opts = {})
// destructures: screenshotFlag

// Slimeball
Vehicle(bp, opts = {})
// destructures: randomSeed, dt

Same concept, different implementations. Some had defaults, some didn’t. Some extracted state, some didn’t. No consistency.

Update methods:

// CartPole, MountainCar
update(opts = { dt: 0.1 })

// LunarLander, Monopede
update(opts = { dt: 0.1, lastTimeStep: false })

// Slimeball
update(opts = { timeSteps = 1, humanPlay = false, twoPlayer = false })

The Slimeball one was particularly wild because it was a legacy competitive game with completely different semantics. Same method name, totally different meaning.

Return interfaces:

// CartPole, MountainCar, LunarLander, Monopede
return { update, reset, ctrnn, PToN, sim }

// Slimeball
return { reset, update, ctrnn, PToN, inputs }

Close! But Slimeball had inputs instead of sim. Why? Because Slimeball’s architecture was fundamentally different. It exposed agent inputs rather than the simulation object.

The pattern was clear: we’d started with good intentions, but each vehicle had diverged based on its specific needs.

The Common Ground

Despite the inconsistencies, there were shared patterns worth extracting:

CTRNN Setup (duplicated in 5 vehicles):

const { ctrnn, partIDToNID: PToN } = PRToCTRNN(bp.partsRegistry)
const { noise, updateCycles, dt } = PMS(bp.data.parts.brain)
ctrnn.setNoise(noise)

Update Loop (duplicated in 4 vehicles):

for (const [name, value] of Object.entries(state)) {
    ctrnn.setNeuronByName(name, value)
}
for (let i = 0; i < updateCycles; i++) {
    ctrnn.update(dt)
}
outputs = ctrnn.getOutputs()

Action Mapping (duplicated in 3 vehicles):

const action = outputs[0] > 0.5 ? 1 : outputs[0] < -0.5 ? -1 : 0

These weren’t just similar - they were identical. Copy-pasted across files. Classic DRY violation.

The Refactoring Plan

The insight was that we didn’t need to make all vehicles identical. We needed to extract the common parts and allow the unique parts to vary.

Three layers:

  1. Shared helpers for CTRNN setup, update loops, and action mapping
  2. Base patterns for simple physics vs complex physics vehicles
  3. Vehicle-specific logic for unique behavior

The goal wasn’t to create rigid inheritance hierarchies. JavaScript doesn’t do that well anyway. The goal was to eliminate duplication and establish conventions.

Phase 1: Extract Helpers

Created vehicleHelpers.js:

export function setupCTRNN(bp) {
    const { ctrnn, partIDToNID } = PRToCTRNN(bp.partsRegistry)
    const { noise, updateCycles, dt } = PMS(bp.data.parts.brain)
    ctrnn.setNoise(noise)
    return { ctrnn, partIDToNID, updateCycles, dt }
}

export function updateCTRNN(ctrnn, state, updateCycles, dt) {
    for (const [name, value] of Object.entries(state)) {
        ctrnn.setNeuronByName(name, value)
    }
    for (let i = 0; i < updateCycles; i++) {
        ctrnn.update(dt)
    }
    return ctrnn.getOutputs()
}

export function thresholdAction(output, high = 0.5, low = -0.5) {
    return output > high ? 1 : output < low ? -1 : 0
}

Now vehicles could use these instead of reimplementing them.

Phase 2: Standardize Interfaces

Established a common contract:

// Standard constructor
function Vehicle(blueprint, options = {}) {
    // Extract common options
    const {
        randomSeed = 0,
        dt = 0.1,
        saveFlag = false,
        ...vehicleSpecificOpts
    } = options

    // Setup CTRNN (using helper)
    // Setup physics
    // Return standard interface
}

// Standard return interface
return {
    update(opts),  // Run one physics step
    reset(state),  // Reset to initial state
    ctrnn,         // Neural controller
    PToN,          // Part-to-neuron mapping
    sim            // Physics simulation
}

Vehicles could extend this pattern with their own options, but the base structure was consistent.

Phase 3: Convert Vehicles

We didn’t convert everything at once. We started with the simplest vehicle (CartPole), validated the pattern, then moved to the others.

CartPole before:

export function Vehicle(bp, opts = {}) {
    const { state: _state = {...}, dt, saveFlag = false, randomSeed = 0 } = opts

    const { ctrnn, partIDToNID: PToN } = PRToCTRNN(bp.partsRegistry)
    const { noise, updateCycles, dt: dt_ } = PMS(bp.data.parts.brain)
    ctrnn.setNoise(noise)

    // ... 30 more lines of setup and logic
}

CartPole after:

export function Vehicle(bp, opts = {}) {
    const { randomSeed = 0, dt = 0.1, saveFlag = false } = opts

    const { ctrnn, partIDToNID: PToN, updateCycles, dt: ctrnDt } =
        setupCTRNN(bp)

    // Vehicle-specific logic only
}

Shorter, clearer, easier to understand.

The Outcome

After three weeks of refactoring:

  • Eliminated ~400 lines of duplicated code
  • Standardized constructor signatures across all modern vehicles
  • Created reusable helpers for CTRNN setup and updates
  • Documented the vehicle API contract
  • Made it trivial to add new vehicles

The real win was cognitive load. Before, understanding a vehicle meant reading the entire implementation. After, you could scan the helpers being used and immediately understand the structure.

Adding a new vehicle went from “study an existing one and hope you got it right” to “use the helpers, implement your physics, done.”

The Lesson

Good abstractions emerge from actual use, not upfront design. We didn’t know what to abstract until we had seven implementations to compare.

The refactoring worked because we:

  1. Documented actual patterns before touching code
  2. Extracted helpers for truly duplicated logic
  3. Standardized interfaces without forcing uniformity
  4. Converted incrementally, validating each step
  5. Left unique behavior alone

Slimeball still has its weird inputs interface because that’s what it needs. But now it’s the intentional exception, not one of seven incompatible designs.

The vehicles aren’t identical. But they’re consistent where it matters.

References