DEV Community

Macklee Gitonga
Macklee Gitonga

Posted on AI-assisted

From DNF to Sub-2:00 — How I Used IBM Bob to Fix My AI Racing Car

I spent 1.5 days getting TORCS installed, named my AI driver Max Verstoppin',
and watched him immediately drive full speed into a wall. This is the story of how
IBM Bob and I turned him into Max Verstappen.


The Challenge

The IBM Bob TORCS University Challenge
gives you a base Python script — snakeoil3_gym.py — that controls an AI car in
TORCS, an open-source racing simulator.
The goal: use IBM Bob as your crew, improve the script, and post the fastest clean
lap on the Corkscrew track from a standing start with zero damage.

Simple brief. Brutal execution.


Alpha — The Baseline (DNF, 1,863 Damage)

The baseline drive_example() function looked like this:

target_speed = 300

R['steer'] = S['angle'] * 15 / PI
R['steer'] -= S['trackPos'] * .10

if S['speedX'] < target_speed - (R['steer'] * 50):
    R['accel'] += .01
else:
    R['accel'] -= .01
Enter fullscreen mode Exit fullscreen mode

No brakes. No corner awareness. A steering multiplier of 15 that caused the wheel
to thrash violently. A target speed of 300 km/h regardless of what's ahead.

Max hit 237 km/h, missed the first corner entirely, and sat pinned against the
barrier wall until the server hit the 100,000 step limit.

Laps: 0. Damage: 1,863. Top speed: 237 km/h.


Bringing in IBM Bob

I pointed IBM Bob at the script and asked it to analyse what was wrong. It broke
down drive_example() and flagged three core issues immediately:

  1. No braking system — the brake key in the action dictionary was never touched
  2. Steering multiplier too aggressive — * 15 caused severe oscillation
  3. Blind target speed — 300 km/h with zero awareness of what's ahead

Once I understood the problems, the approach was straightforward: fix one or two
things at a time, run a lap, read the telemetry, feed it back to Bob, identify the
next bottleneck. Eight iterations. Alpha through Iota.


Beta — First Clean Lap (02:24.17, 0 Damage)

The first set of changes:

  • Added proportional braking using the forward track sensors to detect corners
  • Reduced steering multiplier from 15 to 10
  • Used track[8], track[9], track[10] (the forward-facing sensors) to calculate a corner-aware target speed instead of a hardcoded 300
forward_dist = min(track[8], track[9], track[10])
target_speed = max(60, min(220, forward_dist * 1.1))

speed_excess = S['speedX'] - target_speed
if speed_excess > 20:
    R['brake'] = min(1.0, (speed_excess - 20) / 80.0)
    R['accel'] = 0.0
Enter fullscreen mode Exit fullscreen mode

Max finished his first ever clean lap. 02:24.17. Zero damage.

He was slow — the 1.1 multiplier meant a 100m straight-line sensor reading only
set a target of 110 km/h — but he was clean. Foundation laid.


Gamma → Epsilon — Extracting More Pace (02:15 → 02:06)

Over the next three runs, the multiplier was progressively tuned upward:

Run Multiplier Lap Time Top Speed
Gamma × 2.0 02:15.40 217 km/h
Epsilon × 2.5 02:06.71 225 km/h

Each step brought more straight-line speed while the braking system kept damage
at zero. The brake ramp divisor was also tightened to make the car scrub speed
faster when approaching corners at higher speeds.


Zeta — The Key Architectural Insight (02:04.90)

This was the most important change of the whole project.

The sensor logic was using min() across a five-sensor forward cone to set target
speed. The problem: when Max drifted toward the track edge, the diagonal sensors
in that cone were pointing at the boundary wall and returning short readings —
sometimes 20–30m — on a completely open straight. The car thought a corner was
coming and backed off the throttle for no reason.

The fix was to split the sensor reading into two separate signals:

# MAX of the cone → straight-line pace (one clear sensor is enough)
straight_dist = max(track[7], track[8], track[9], track[10], track[11])

# MIN of the tightest 3 → corner guard (only fires when road ahead closes in)
corner_dist = min(track[8], track[9], track[10])

target_speed = max(50, min(280, straight_dist * 2.8))
if corner_dist < 55:
    corner_cap = max(50, corner_dist * 2.8)
    target_speed = min(target_speed, corner_cap)
Enter fullscreen mode Exit fullscreen mode

As long as any forward sensor sees clear road, Max pushes. Corner braking only
triggers when the road directly ahead is genuinely closing in.

02:04.90. 228 km/h. 0 damage.


Eta — A Lesson in Calibration (02:11.21 — Regression)

Not every run went forward. Lowering the corner detection threshold to < 45m
(from < 60m) delayed braking too long into the final tight turn. Max ran wide
onto the grass, lost grip, and the lap time went up by 6 seconds despite zero
damage.

The off-track excursion didn't register as damage in TORCS — grass just kills
speed. A good reminder that the metrics don't always tell the full story.


Theta → Iota — Breaking Sub-2:00 (02:02 → 01:58.15)

Theta split the difference: threshold < 55m, multiplier 2.8. New PB at
02:02.42.

Iota fixed the last remaining issue — Max was loitering along the track edge
after corner exits, not punching the throttle onto the straight. Two causes:

  1. When off-centre, diagonal sensors still saw the wall and suppressed straight_dist. Fix: fall back to dead-ahead sensor only when |trackPos| > 0.5
  2. The apex throttle boost required |steer| < 0.1 — too tight. The shallow correction Max held while returning to centre never qualified. Relaxed to 0.2.
  3. Centering gain doubled when near the edge (0.15 → 0.30) so he snapped back to the racing line faster.
track_pos = S.get('trackPos', 0)
if abs(track_pos) > 0.5:
    straight_dist = track[9]  # dead-ahead only — ignore lying diagonal sensors
else:
    straight_dist = max(track[7], track[8], track[9], track[10], track[11])

centering_gain = 0.3 if abs(track_pos) > 0.5 else 0.15
R['steer'] -= track_pos * centering_gain
Enter fullscreen mode Exit fullscreen mode

01:58.15. 235 km/h. 0 damage. Sub-2:00.


Final Progression

Run Lap Time Top Speed Damage Key Change
Alpha DNF 237 km/h 1,863 Baseline — blind throttle, no brakes
Beta 02:24.17 196 km/h 0 Braking, corner-aware speed
Gamma 02:15.40 217 km/h 0 dist × 2.0 multiplier
Epsilon 02:06.71 225 km/h 0 dist × 2.5, tighter brake ramp
Zeta 02:04.90 228 km/h 0 Dual max/min sensor logic
Eta 02:11.21 228 km/h 0 ⚠️ Regression — threshold < 45 too aggressive
Theta 02:02.42 229 km/h 0 Threshold < 55, multiplier 2.8
Iota 01:58.15 235 km/h 0 Edge sensor fix, adaptive centering

What I Learned

The most useful thing IBM Bob did wasn't writing code — it was helping me
understand what the code was actually doing before I changed anything. Once I
could see exactly why Max was crashing (no brakes, blind speed) or slowing down
(sensor reading the wall on a straight), each fix became obvious.

The iterative loop — run, observe, diagnose, fix — is also just good engineering.
Every regression (Eta) taught something. Every metric mattered: lap time, damage,
and top speed together told a more complete story than any one number alone.

I also completed the IBM Granite Models for Software Development course on
IBM SkillsBuild during this project and earned the official badge, which gave me
solid context for how these models reason about code — useful when you're having
technical back-and-forth with Bob about sensor arrays and braking logic.


Links



Enter fullscreen mode Exit fullscreen mode

Top comments (0)