Don’t Repeat Yourself. It’s one of those principles everyone knows, everyone agrees with, and everyone violates constantly.
Our vehcology codebase had the same 20-line CTRNN setup code duplicated across seven different vehicle implementations. Not similar code. Identical code. Copy-pasted with slightly different variable names.
Every time we fixed a bug in one vehicle’s CTRNN setup, we had to fix it in six other places. Every time we added a feature, we’d implement it in one vehicle and then realize we needed to propagate it everywhere else.
This is the kind of redundancy that kills productivity. Time to DRY it up.
The Duplication
Here’s what CartPole’s vehicle constructor looked like:
export function Vehicle(bp, opts = {}) {
const { ctrnn, partIDToNID: PToN } = PRToCTRNN(bp.partsRegistry)
const { noise, updateCycles, dt } = PMS(bp.data.parts.brain)
ctrnn.setNoise(noise)
// ... vehicle-specific setup
}
Here’s MountainCar:
export function Vehicle(bp, opts) {
const { ctrnn, partIDToNID: PToN } = PRToCTRNN(bp.partsRegistry)
const { noise, updateCycles, dt } = PMS(bp.data.parts.brain)
ctrnn.setNoise(noise)
// ... vehicle-specific setup
}
Here’s LunarLander:
export function Vehicle(bp, opts = {}) {
const { ctrnn, partIDToNID: PToN } = PRToCTRNN(bp.partsRegistry)
const { noise, updateCycles, dt } = PMS(bp.data.parts.brain)
ctrnn.setNoise(noise)
// ... vehicle-specific setup
}
Seeing the pattern? The setup was identical. The only difference was what came after.
And it wasn’t just CTRNN setup. The update loop pattern was duplicated too:
function update(opts) {
// Set sensor inputs
for (const [name, value] of Object.entries(state)) {
ctrnn.setNeuronByName(name, value)
}
// Update CTRNN multiple times
for (let i = 0; i < updateCycles; i++) {
ctrnn.update(dt)
}
// Get motor outputs
const outputs = ctrnn.getOutputs()
// ... use outputs to control vehicle
}
This exact loop appeared in five vehicles. Same iteration count. Same neuron updates. Same output extraction. Copy-pasted.
The Extraction
The fix was to pull common code into helper functions:
// 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, sensorState, updateCycles, dt) {
// Set sensor inputs
for (const [name, value] of Object.entries(sensorState)) {
ctrnn.setNeuronByName(name, value)
}
// Update CTRNN
for (let i = 0; i < updateCycles; i++) {
ctrnn.update(dt)
}
return ctrnn.getOutputs()
}
Now vehicles could use these helpers:
export function Vehicle(bp, opts = {}) {
const { ctrnn, partIDToNID: PToN, updateCycles, dt } = setupCTRNN(bp)
// Vehicle-specific setup...
function update(opts) {
// Get sensor state (vehicle-specific)
const sensorState = getSensorState()
// Update CTRNN (shared logic)
const outputs = updateCTRNN(ctrnn, sensorState, updateCycles, dt)
// Use outputs (vehicle-specific)
applyOutputsToVehicle(outputs)
}
}
The duplication vanished. CTRNN setup logic existed in exactly one place. Updates existed in exactly one place.
The Benefits
The immediate benefit was maintainability. Need to change how CTRNN noise works? Update one function. Need to add logging to the update loop? Update one function.
The secondary benefit was consistency. Before, we’d update one vehicle’s CTRNN handling and forget to update the others. Now it was impossible to get out of sync - they all used the same implementation.
The tertiary benefit was readability. New contributors could look at a vehicle and immediately see “this uses standard CTRNN setup” instead of having to parse 20 lines of initialization code.
The Gotcha
Not all duplication should be eliminated.
Slimeball had similar-looking CTRNN setup, but with important differences. It used two CTRNN networks (one per agent), different noise parameters, and custom update logic for competitive play.
My first refactoring attempt tried to force Slimeball into the standard helpers. It didn’t fit. I added parameters, special cases, and conditional logic to make the helpers work for both the standard vehicles and Slimeball.
The result was worse than duplication. The helpers became complicated, fragile, and hard to understand. Every change risked breaking both use cases.
The lesson: duplication is better than the wrong abstraction.
I reverted the Slimeball changes. It kept its custom CTRNN setup. The standard vehicles used the standard helpers. Both were clear and maintainable.
The Rule
DRY is valuable when:
- The duplicated code is truly identical
- Changes need to stay synchronized
- The abstraction is simpler than the duplication
- The code will be maintained going forward
DRY is harmful when:
- The similarity is superficial
- The use cases will diverge over time
- The abstraction adds complexity
- You’re abstracting to avoid typing
We had true duplication in standard vehicles. The CTRNN setup was identical, would stay synchronized, and the helpers were simpler than duplication. Perfect candidate for DRY.
Slimeball had superficial similarity that would diverge. Forcing it into the standard pattern added complexity. Better to leave it alone.
The Outcome
After refactoring:
- Eliminated ~400 lines of duplicated code
- Reduced CTRNN setup from 20 lines to 1 line per vehicle
- Created reusable helpers tested once and used everywhere
- Preserved custom logic where it made sense
The codebase got smaller, clearer, and easier to maintain. Bug fixes propagated automatically. New features required updating one helper instead of seven vehicles.
But Slimeball stayed weird. And that’s okay.
The Lesson
DRY is a tool, not a religion. Duplication is a code smell that suggests refactoring might help. But the goal isn’t zero duplication - it’s maintainable code.
Sometimes duplication is the most maintainable option. When code looks similar but serves different purposes, forcing it into shared abstractions creates coupling. Future changes have to navigate the shared code, breaking things that shouldn’t be related.
The test is simple: if you change the duplicated code, do you want all copies to change together? If yes, DRY it. If no, leave it duplicated.
Our CTRNN setup needed to stay synchronized. DRY helped.
Slimeball’s CTRNN setup needed to diverge. Duplication helped.
Know the difference.