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.