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

Lesson 4 of 7

SFML Setup: The Track and the Horses

With the skip list working, it's time to give it something to sort: a window, a track, and a row of horses. This lesson follows the same SFML setup as the Breakout course (install libsfml-dev/brew install sfml, compile with -lsfml-graphics -lsfml-window -lsfml-system), if you've built that course already, the window boilerplate below will look familiar.

Opening the window and drawing lanes

#include <SFML/Graphics.hpp>
#include <vector>

const int LANE_COUNT = 6;
const float LANE_HEIGHT = 80.f;
const float TRACK_WIDTH = 900.f;

int main() {
    sf::RenderWindow window(sf::VideoMode(1000, LANE_COUNT * LANE_HEIGHT + 40), "Horse Race");
    window.setFramerateLimit(60);

    std::vector<sf::RectangleShape> lanes;
    for (int i = 0; i < LANE_COUNT; i++) {
        sf::RectangleShape lane(sf::Vector2f(TRACK_WIDTH, LANE_HEIGHT - 4.f));
        lane.setPosition(50.f, i * LANE_HEIGHT + 20.f);
        lane.setFillColor(i % 2 == 0 ? sf::Color(60, 60, 60) : sf::Color(45, 45, 45));
        lanes.push_back(lane);
    }

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) window.close();
        }

        window.clear(sf::Color::Black);
        for (auto& lane : lanes) window.draw(lane);
        window.display();
    }

    return 0;
}

Alternating lane colors (i % 2 == 0) is purely cosmetic, it makes it easy to tell one horse's lane from its neighbor's at a glance once horses are moving.

The Horse struct

struct Horse {
    int id;
    sf::CircleShape shape;
    float distance = 0.f;   // how far this horse has traveled
    float raceKey = 0.f;    // the value actually stored in the skip list

    Horse(int horseId, int lane, sf::Color color) : id(horseId) {
        shape.setRadius(12.f);
        shape.setFillColor(color);
        shape.setPosition(50.f, lane * LANE_HEIGHT + 20.f + (LANE_HEIGHT - 4.f) / 2.f - 12.f);
    }
};

distance is the honest, human-meaningful value, how far along the track this horse actually is. raceKey is what the skip list will be sorted by, and the next lesson gives it a small twist: two horses can legitimately end up at the exact same distance on the same frame, and erase()'s exact-key lookup from the last lesson can't tell those apart. Rather than teach the skip list to break ties, it's simpler to make ties impossible in the first place.

std::vector<Horse> createHorses(int count) {
    std::vector<Horse> horses;
    std::vector<sf::Color> colors = {
        sf::Color::Red, sf::Color::Blue, sf::Color::Green,
        sf::Color::Yellow, sf::Color::Magenta, sf::Color::Cyan
    };

    for (int i = 0; i < count; i++) {
        Horse horse(i, i, colors[i % colors.size()]);
        horse.raceKey = horse.id * 0.0001f; // tiny, unique tiebreaker baked in from the start
        horses.push_back(horse);
    }
    return horses;
}

Every horse's raceKey starts as distance + id * 0.0001f, an offset so small it never changes which horse is actually in the lead, but large enough that no two horses can ever produce an identical float. The skip list only ever sees unique keys, no tie-handling logic required anywhere in it.

TIP

Keep horses in a std::vector<Horse>, indexed by id, alongside the skip list. The skip list is what determines rank order, the vector is what lets you look up "horse 3"'s sprite directly by index to move or draw it, each structure does the job the other one is bad at.

📝 Track & Horses Setup Quiz

Passing score: 70%
  1. 1.Why give each horse's raceKey a tiny offset like id * 0.0001f?

  2. 2.distance and raceKey are meant to be the same value, tracked twice for no reason.

  3. 3.Alongside the skip list, horses are also kept in a std::vector<Horse> indexed by id, so a specific horse can be looked up directly to move or ____ it.