Skip to content

World & resources

The Zappy world — the planet Trantor — is a rectangular grid of tiles, toroidal (the edges join up), on which players, eggs and resources live. This page describes the structure of the world, the resource enumeration, and the generation algorithm (densities, top-up, periodic respawn).

The World class

World holds the grid and the teams. The grid is a flat vector of tiles, stored row by row (row-major), of size width × height. The teams are stored in a std::map<std::string, Team>.

// world/World.hpp:33-37 (excerpt)
int _width;
int _height;
std::vector<Tile> _tiles;
std::map<std::string, Team> _teams;
// world/World.cpp:13-25 (excerpt)
World::World(const int width, const int height,
    const std::vector<std::string> &teamNames)
    : _width(width), _height(height)
{
    for (int i = 0; i < _width * _height; i++)
        _tiles.emplace_back();
    for (const std::string &name : teamNames) {
        Team team;
        team._name = name;
        _teams[name] = team;
    }
}

Toroidal access: getTile

This is the centrepiece of the world. getTile(x, y) brings any coordinate — even negative or out of bounds — back inside the grid via a "clean" modulo:

// world/World.cpp:47-52
Tile& World::getTile(int x, int y)
{
    x = ((x % this->_width) + this->_width) % this->_width;
    y = ((y % this->_height) + this->_height) % this->_height;
    return this->_tiles[y * this->_width + x];
}

The toroidal concept

The formula ((x % w) + w) % w guarantees a result in [0, w[ even for a negative x (C's % can return a negative remainder). Concretely, going off the right edge brings you back to the left, going off the top brings you back to the bottom: the map behaves like the surface of a torus (a donut). A player moving straight ahead always eventually returns to its starting point; there is neither edge nor corner. The whole engine (movement, vision, broadcast, ejection) relies on this access to cross the borders transparently.

flowchart LR
    subgraph grid["Grid width × height"]
        direction LR
        l["left edge (x=0)"]
        r["right edge (x=w-1)"]
    end
    r -. "x = w → x = 0" .-> l
    l -. "x = -1 → x = w-1" .-> r

The Tile class

A tile holds three things: its resources, the players present, and the eggs laid on it.

// world/Tile.hpp:39-42 (excerpt)
std::map<Resource, int> _resources;
std::vector<Player*>    _players;   // non-owning
std::vector<Egg*>       _eggs;      // non-owning

The pointers to Player and Egg are non-owning: the tile merely references objects whose lifetime is managed elsewhere (the AI clients for players, the teams for eggs). removeResource fails if the requested quantity exceeds the stock present — you cannot pick up what does not exist.

The Resource enumeration

There are seven resources: food (Food) and six "stones" required for incantations.

// world/Tile.hpp:22
enum class Resource { Food, Linemate, Deraumere, Sibur, Mendiane, Phiras, Thystame };
Resource Role
Food Food — extends the player's survival.
Linemate, Deraumere, Sibur, Mendiane, Phiras, Thystame Stones — consumed during elevation incantations.

Resource generation: RessourceSpawner

The subject sets a density per resource: the target quantity present on the map is density × width × height. The most abundant is food; the rarest, thystame.

Resource Density
Food 0.5
Linemate 0.3
Deraumere 0.15
Sibur 0.1
Mendiane 0.1
Phiras 0.08
Thystame 0.05
// world/RessourceSpawner.hpp:31-38
inline static const std::map<Resource, float> _densityMap = {
    {Resource::Food, 0.5f},
    {Resource::Linemate, 0.3f},
    {Resource::Deraumere, 0.15f},
    {Resource::Sibur, 0.1f},
    {Resource::Mendiane, 0.1f},
    {Resource::Phiras, 0.08f},
    {Resource::Thystame, 0.05f}};

The spawn algorithm

ResourceSpawner::spawn(world) tops up the map rather than regenerating everything: for each resource, it counts what already exists and only adds the shortfall.

// world/RessourceSpawner.cpp:11-55 (excerpt)
for (auto const& [type, density] : _densityMap) {
    int totalResources = world.getTotalResources(type);
    int missingResources =
        static_cast<int>(density * mapArea - totalResources);

    if (totalResources == 0) {              // guarantee at least 1
        int x = rand() % world.getWidth();
        int y = rand() % world.getHeight();
        world.getTile(x, y).addResource(type, 1);
        changedTiles.push_back({x, y});
        missingResources--;
    }
    if (missingResources <= 0)
        continue;
    // shuffled tiles (mt19937), drop the missing units
    std::shuffle(positions.begin(), positions.end(),
                 std::mt19937(std::random_device{}()));
    for (int i = 0; i < missingResources; i++) {
        int index = positions[i % mapArea];
        ...
        world.getTile(x, y).addResource(type, 1);
        changedTiles.push_back({x, y});
    }
}

Steps:

  1. Target: target = density × (w × h).
  2. Shortfall: missing = target − existing.
  3. Minimum guarantee: if a resource is entirely absent (existing == 0), at least one is placed on a random tile, then the shortfall is decremented.
  4. Distribution: the set of tile indices is shuffled with a std::mt19937 and one unit is dropped per tile until the shortfall is filled.

spawn returns the list of modified tiles, which allows the targeted GUI to be notified (see below).

Top-up, not reset

Because the spawn tops up towards the target rather than resetting everything, resources already picked up by players gradually reappear, but those still on the ground are not duplicated. The map continuously tends towards the desired density.

Periodic respawn

The first spawn happens when the server is constructed (Server.cpp:70). Afterwards, the server reschedules a regeneration every 20 time units, which reprograms itself indefinitely and broadcasts the modified tiles to the GUI via the bct event (tile content).

// core/Server.cpp:651-669
void Server::scheduleResourceRespawn()
{
    _scheduler->schedule([this]() {
        std::vector<std::pair<int, int>> changedTiles =
            _world->spawnResources();

        std::set<std::pair<int, int>> uniqueTiles(
            changedTiles.begin(), changedTiles.end());

        broadcastToGUIs([&](GUIProtocol &protocol) {
            for (const auto &[x, y] : uniqueTiles)
                protocol.sendBct(x, y, _world->getTile(x, y));
        });

        scheduleResourceRespawn();      // reprograms itself
    }, 20);
}

Self-rescheduling

Rather than a recurring event, the server uses a one-shot event that reschedules itself at the end of its execution. This is the idiomatic pattern with a deadline-based Scheduler: each respawn queues the next one, which naturally follows frequency changes (-f, sst) without extra logic. See Game loop & time.

Duplicate coordinates are deduplicated (std::set) before sending, so only one bct is emitted per tile actually modified. For the exact format of bct and the other GUI events, see Protocol.