Monde & ressources¶
Le monde de Zappy — la planète Trantor — est une grille rectangulaire de tuiles, torique (les bords se rejoignent), sur laquelle vivent les joueurs, les œufs et les ressources. Cette page décrit la structure du monde, l'énumération des ressources, et l'algorithme de génération (densités, complétion, respawn périodique).
La classe World¶
World détient la grille et les équipes. La grille est un vecteur plat de tuiles, stocké ligne par ligne (row-major), de taille width × height. Les équipes sont stockées dans une std::map<std::string, Team>.
// world/World.hpp:33-37 (extrait)
int _width;
int _height;
std::vector<Tile> _tiles;
std::map<std::string, Team> _teams;
// world/World.cpp:13-25 (extrait)
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;
}
}
Accès torique : getTile¶
C'est le point central du monde. getTile(x, y) ramène toute coordonnée — même négative ou hors-bornes — à l'intérieur de la grille par un modulo « propre » :
// 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];
}
Le concept torique
La formule ((x % w) + w) % w garantit un résultat dans [0, w[ même pour un x négatif (le % du C peut renvoyer un reste négatif). Concrètement, sortir par la droite ramène à gauche, sortir par le haut ramène en bas : la carte se comporte comme la surface d'un tore (un donut). Un joueur qui avance tout droit finit toujours par revenir à son point de départ ; il n'existe ni bord ni coin. Tout le moteur (déplacement, vision, broadcast, éjection) s'appuie sur cet accès pour franchir les bordures de façon transparente.
flowchart LR
subgraph grid["Grille width × height"]
direction LR
l["bord gauche (x=0)"]
r["bord droit (x=w-1)"]
end
r -. "x = w → x = 0" .-> l
l -. "x = -1 → x = w-1" .-> r
La classe Tile¶
Une tuile contient trois choses : ses ressources, les joueurs présents et les œufs déposés.
// world/Tile.hpp:39-42 (extrait)
std::map<Resource, int> _resources;
std::vector<Player*> _players; // non-owning
std::vector<Egg*> _eggs; // non-owning
Les pointeurs vers Player et Egg sont non-propriétaires : la tuile ne fait que référencer des objets dont la durée de vie est gérée ailleurs (les clients IA pour les joueurs, les équipes pour les œufs). removeResource échoue si la quantité demandée dépasse le stock présent — on ne peut pas ramasser ce qui n'existe pas.
L'énumération Resource¶
Il existe sept ressources : la nourriture (Food) et six « pierres » nécessaires aux incantations.
// world/Tile.hpp:22
enum class Resource { Food, Linemate, Deraumere, Sibur, Mendiane, Phiras, Thystame };
| Ressource | Rôle |
|---|---|
Food |
Nourriture — prolonge la survie du joueur. |
Linemate, Deraumere, Sibur, Mendiane, Phiras, Thystame |
Pierres — consommées lors des incantations d'élévation. |
La génération des ressources : RessourceSpawner¶
Le sujet fixe une densité par ressource : la quantité cible présente sur la carte est densité × largeur × hauteur. La plus abondante est la nourriture ; la plus rare, le thystame.
| Ressource | Densité |
|---|---|
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}};
L'algorithme de spawn¶
ResourceSpawner::spawn(world) complète la carte plutôt que de tout régénérer : pour chaque ressource, il compte ce qui existe déjà et n'ajoute que le manque.
// world/RessourceSpawner.cpp:11-55 (extrait)
for (auto const& [type, density] : _densityMap) {
int totalResources = world.getTotalResources(type);
int missingResources =
static_cast<int>(density * mapArea - totalResources);
if (totalResources == 0) { // garantir au moins 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;
// tuiles mélangées (mt19937), on dépose les unités manquantes
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});
}
}
Étapes :
- Cible :
target = densité × (w × h). - Manque :
missing = target − existant. - Garantie minimale : si une ressource est totalement absente (
existant == 0), on en pose au moins une sur une tuile aléatoire, puis on décrémente le manque. - Distribution : on mélange l'ensemble des indices de tuiles avec un
std::mt19937et on dépose une unité par tuile jusqu'à combler le manque.
spawn renvoie la liste des tuiles modifiées, ce qui permet de notifier le GUI ciblé (voir ci-dessous).
Complétion, pas remise à zéro
Parce que le spawn complète vers la cible plutôt que de tout réinitialiser, les ressources déjà ramassées par les joueurs réapparaissent progressivement, mais celles encore au sol ne sont pas dupliquées. La carte tend en permanence vers la densité voulue.
Le respawn périodique¶
Le premier spawn a lieu à la construction du serveur (Server.cpp:70). Ensuite, le serveur replanifie une régénération toutes les 20 unités de temps, qui se reprogramme elle-même indéfiniment et diffuse au GUI les tuiles modifiées via l'événement bct (contenu d'une tuile).
// 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(); // se reprogramme
}, 20);
}
Auto-réplanification
Plutôt qu'un événement récurrent, le serveur utilise un événement à usage unique qui se replanifie à la fin de son exécution. C'est le motif idiomatique avec un Scheduler à échéances : chaque respawn met le suivant en file, ce qui suit naturellement les changements de fréquence (-f, sst) sans logique supplémentaire. Voir Boucle de jeu & temps.
Les doublons de coordonnées sont dédupliqués (std::set) avant l'envoi pour n'émettre qu'un seul bct par tuile réellement modifiée. Pour le format exact de bct et des autres événements GUI, voir Protocole.