Build a Horse Race Simulator using C++, SFML & a Skip List

Lesson 5 of 7

The Race Loop: Moving Horses and Updating the Skip List

Every tick, each horse's distance changes by a random amount, and every time that happens, its position in the skip list needs to change too. This is the moment the two structures actually connect.

Why you can't just edit the key in place

It's tempting to just reach into the skip list and change a node's key field directly. That silently breaks the list: a node's position in forward[] chains was decided by where its old key belonged, changing the key without re-splicing leaves the node sitting in the wrong spot, sorted for a value it no longer holds. The list looks fine, walking it in order would now visit horses out of their real rank.

Erase, update, reinsert

void updateHorsePosition(SkipList& track, Horse& horse) {
    track.erase(horse.raceKey); // remove the old (now stale) position

    float increment = 40.f + static_cast<float>(rand() % 60); // random speed this tick, 40-99 px/sec worth
    horse.distance += increment * (1.f / 60.f); // scaled by a fixed 60fps tick, matching setFramerateLimit(60)

    horse.raceKey = horse.distance + horse.id * 0.0001f; // recompute the key, tiebreaker included
    track.insert(horse.id, horse.raceKey); // reinsert at the correct new position
}

erase then insert is two O(log n) operations instead of one, but that's still dramatically cheaper than a sorted array's O(n) shift, and it's the same "remove, then add back correctly sorted" pattern any ordered structure needs when a stored value changes. rand() % 60 gives each horse a different random burst of speed every tick, so the race isn't a predictable straight line, some horses will pull ahead, fall back, and trade the lead.

Updating the sprite

const float PIXELS_PER_UNIT = 6.f;

void updateHorseSprite(Horse& horse) {
    float x = 50.f + horse.distance * PIXELS_PER_UNIT;
    horse.shape.setPosition(x, horse.shape.getPosition().y);
}

The skip list tracks rank, it has no idea what a pixel is. Converting distance into an x-coordinate is a separate, simple concern: multiply by a scale factor and keep the y-coordinate (the lane) exactly where it started.

Wiring it into the game loop

SkipList track;
std::vector<Horse> horses = createHorses(LANE_COUNT);
for (auto& horse : horses) {
    track.insert(horse.id, horse.raceKey);
}

// inside the game loop, once per frame:
for (auto& horse : horses) {
    updateHorsePosition(track, horse);
    updateHorseSprite(horse);
}

for (auto& horse : horses) {
    window.draw(horse.shape);
}

Every horse is seeded into track once, up front, at its starting raceKey (all effectively 0, tiebroken by id). From then on, every frame runs the same three steps for every horse: update its position in the skip list, update where it's drawn, then draw it, keeping the skip list and the screen in sync on every single tick.

WARNING

updateHorsePosition must call track.erase(horse.raceKey) before changing horse.distance. Erasing after the key has already changed would search for a key that no longer matches anything in the list, silently leaving the stale node behind forever.

📝 Race Loop Quiz

Passing score: 70%
  1. 1.Why not just reach into a skip list node and change its key field directly?

  2. 2.updateHorsePosition must erase the horse's old raceKey from the skip list before changing horse.distance.

  3. 3.Converting horse.distance into an x pixel coordinate is done by multiplying it by a ____ factor.