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

Lesson 6 of 7

Live Leaderboard & the Finish Line

The race is moving, every horse's position in the skip list stays correct frame to frame. Now use that to show the standings live, and to actually end the race.

Rendering the leaderboard

void drawLeaderboard(sf::RenderWindow& window, SkipList& track, sf::Font& font) {
    std::vector<int> order = track.leaderboard(); // horse IDs, 1st place first

    for (std::size_t rank = 0; rank < order.size(); rank++) {
        sf::Text entry;
        entry.setFont(font);
        entry.setCharacterSize(16);
        entry.setFillColor(sf::Color::White);
        entry.setPosition(970.f, 20.f + rank * 22.f);
        entry.setString(std::to_string(rank + 1) + ". Horse " + std::to_string(order[rank] + 1));
        window.draw(entry);
    }
}

track.leaderboard() does all the real work, it's the level-0 traversal from two lessons ago, called once per frame. Nothing here re-sorts anything, the list was already kept sorted the entire time updateHorsePosition was running, this function's only job is turning that order into text on screen.

Detecting the finish line

const float FINISH_LINE = 800.f;

int checkForWinner(std::vector<Horse>& horses) {
    for (auto& horse : horses) {
        if (horse.distance >= FINISH_LINE) {
            return horse.id;
        }
    }
    return -1; // nobody has finished yet
}

This loops horses directly rather than the skip list, since the question here is "has any horse crossed FINISH_LINE", not "who's currently ahead", a plain scan is simpler and just as fast for a handful of horses. Once a winner is found, stop calling updateHorsePosition for every horse (freeze the race) and display the result.

bool raceOver = false;
int winnerId = -1;

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

    winnerId = checkForWinner(horses);
    if (winnerId != -1) {
        raceOver = true;
    }
}

drawLeaderboard(window, track, font);

if (raceOver) {
    sf::Text winnerText;
    winnerText.setFont(font);
    winnerText.setCharacterSize(28);
    winnerText.setFillColor(sf::Color::Yellow);
    winnerText.setPosition(300.f, 10.f);
    winnerText.setString("Horse " + std::to_string(winnerId + 1) + " wins!");
    window.draw(winnerText);
}

Guarding the movement update with if (!raceOver) freezes every horse exactly where it was the instant someone crossed the line, while drawLeaderboard and the window's own clear/display calls keep running either way, so the final standings and the winner banner both stay visible instead of the screen going blank.

TIP

Because checkForWinner scans in id order, not skip list order, two horses crossing on the exact same frame would always report the lower id as the winner. Detecting a genuine photo finish (comparing exact distances, not just who's checked first) is a good stretch goal for the final project.

📝 Leaderboard & Finish Line Quiz

Passing score: 70%
  1. 1.Why does drawLeaderboard not need to sort anything itself?

  2. 2.checkForWinner scans the horses vector directly rather than querying the skip list.

  3. 3.Guarding the per-frame movement update with if (!raceOver) freezes every horse in place once a ____ has been found.