Skip to content

Game mechanics

This page details the core gameplay mechanics implemented on the server side: vision (Look), inventory, incantation / elevation, broadcast and ejection. The algorithms live in server/srcs/algorithms/; incantation orchestration in server/srcs/core/IncantationHandler.cpp.

Vision — Look

VisionAlgo::getTiles builds the player's vision cone, for depths 0..level, with 2*depth+1 tiles per row, oriented by Direction (server/srcs/algorithms/VisionAlgo.cpp:6-24):

for (int depth = 0; depth <= p.level; depth++) {
    for (int offset = -depth; offset <= depth; offset++) {
        int x = p.x, y = p.y;
        if (p.direction == Direction::North) { x = p.x + offset; y = p.y - depth; }
        if (p.direction == Direction::South) { x = p.x - offset; y = p.y + depth; }
        if (p.direction == Direction::East)  { x = p.x + depth;  y = p.y + offset; }
        if (p.direction == Direction::West)  { x = p.x - depth;  y = p.y - offset; }
        tiles.push_back(&world.getTile(x, y));
    }
}

Why a widening cone

Row 0 contains only the current tile. Each subsequent row advances one step in the gaze direction and widens by two tiles. The player's level sets the range: a level-N player sees (N+1)² tiles. Since world.getTile is toroidal, the edges wrap around.

formatLook serialises tiles in brackets, separated by , (VisionAlgo.cpp:56-69). Each tile first lists one player token per present player, then each resource repeated by its quantity, all space-separated (VisionAlgo.cpp:34-54):

[current tile, tile1, tile2, ...]

Example: [player, , linemate, food food, ...]. See AI ↔ Server protocol for the exact grammar.

Inventory

CmdInventory::execute returns the list of resources in the player's inventory (server/srcs/commands/CmdInventory.cpp:9-19):

[food N, linemate N, deraumere N, sibur N, mendiane N, phiras N, thystame N]

food multiplied by 126

Food is reported × 126 (CmdInventory.cpp:12), not in raw units. It is a conversion into remaining ticks, inconsistent with the GUI pin event. See Known limitations.

Incantation / elevation

An incantation raises a group of players to the next level if the conditions are met on the tile.

Requirements table

IncantationAlgo::TABLE indexes requirements by level-1 (server/srcs/algorithms/IncantationAlgo.cpp:5-13). Each row gives the number of same-level players required and the number of stones to gather on the tile.

Elevation Players linemate deraumere sibur mendiane phiras thystame
1 → 2 1 1 0 0 0 0 0
2 → 3 2 1 1 1 0 0 0
3 → 4 2 2 0 1 0 2 0
4 → 5 4 1 1 2 0 1 0
5 → 6 4 1 2 1 3 0 0
6 → 7 6 1 2 3 0 1 0
7 → 8 6 2 2 2 2 2 1

Double validation

IncantationAlgo::check verifies there are enough players at the exact level on the tile, then that each stone is present in sufficient quantity (IncantationAlgo.cpp:15-38). This check runs twice: at the start and at the end (re-validation), to prevent cheating during the 300-tick wait. execute consumes the stones and increments each participant's level (IncantationAlgo.cpp:40-57).

Flow — IncantationHandler

Startup is driven by IncantationHandler::start (server/srcs/core/IncantationHandler.cpp:14-189). CmdIncantation::execute is itself a no-op: all logic is in the handler.

  1. Gather on the tile the players that are same-level and not busy (isBusy() == false) — IncantationHandler.cpp:34-48.
  2. check; on failure, reply ko and abort.
  3. Success: isIncanting = true for all, the initiator becomes busy, each participant receives Elevation underway, broadcast pic to GUIs (IncantationHandler.cpp:57-73).
  4. Schedule completion after 300 ticks (cmd->getDelay()).
  5. At deadline: re-resolve players by id, verify none moved, died or changed tile, then re-validate check (IncantationHandler.cpp:126-144).
  6. Success: execute consumes the stones and level++; each participant receives Current level: N; broadcast pie 1, bct, plv (IncantationHandler.cpp:146-173).
  7. Failure at re-validation: isIncanting = false, ko to all, broadcast pie 0 (IncantationHandler.cpp:100-124).
flowchart TD
    A["Incantation dequeued"] --> B["Gather players<br/>same level, not busy"]
    B --> C{"check() ?"}
    C -->|no| K1["ko"]
    C -->|yes| D["isIncanting = true<br/>Elevation underway<br/>broadcast pic"]
    D --> E["Wait 300 ticks"]
    E --> F{"Players intact<br/>+ re-check() ?"}
    F -->|no| K2["ko + pie 0"]
    F -->|yes| G["execute: -stones, level++"]
    G --> H["Current level: N<br/>broadcast pie 1 / bct / plv"]
    H --> I{"≥ 6 players level 8 ?"}
    I -->|yes| W["seg + stop"]
    I -->|no| Z["end"]

Win condition

After a successful elevation, if the team counts ≥ 6 level-8 players, the server broadcasts seg <team> and schedules shutdown (_stopWhenFlushed = true, IncantationHandler.cpp:161-176).

bool gameWon = countLevel8Players(initiator->teamName) >= 6;
...
if (gameWon) protocol.sendSeg(winningTeam);
...
if (gameWon) stopWhenFlushed = true;

Broadcast

When a player emits Broadcast <text>, every other player receives the message together with a sound direction K (0..8) indicating where the sound comes from relative to its orientation (0 = same tile).

BroadcastAlgo::computeK computes the shortest toroidal vector from the emitter to the receiver, the angle atan2(dy, dx), shifts it by the receiver's orientation, then rounds onto the 8 sectors (server/srcs/algorithms/BroadcastAlgo.cpp:14-57):

if (emitter.x == receiver.x && emitter.y == receiver.y)
    return 0;                       // same tile
...
double alpha = std::atan2(dy, dx);  // receiver -> emitter vector
// theta from receiver direction (N=-pi/2, E=0, S=pi/2, W=pi)
double rel_alpha = alpha - theta;
int index = (int)std::round(-rel_alpha / (M_PI / 4));
int k = 1 + index;                  // wrapped into 1..8

CmdBroadcast::execute iterates over all tiles and computes K for each other player (server/srcs/commands/CmdBroadcast.cpp:15-29). The server then sends each receiver message K, <text> and broadcasts pbc to the GUIs (server/srcs/core/Server.cpp:344-364).

K reference frame

K = 1 corresponds to a sound coming from the front; indices increase counter-clockwise. K = 0 is reserved for the "emitter on the same tile" case. The toroidal projection guarantees the sound is always heard via the shortest path.

Ejection — Eject

Eject pushes all other players present on the tile one cell in the ejector's gaze direction (toroidal move), and succeeds if it pushed at least one player (server/srcs/commands/CmdEject.cpp:21-53):

int dx = 0, dy = 0;
if (player.direction == Direction::North) dy = -1;
if (player.direction == Direction::South) dy = 1;
if (player.direction == Direction::East)  dx = 1;
if (player.direction == Direction::West)  dx = -1;
...
for (Player* p : this->_ejectedPlayers) {
    currentTile.removePlayer(p);
    p->x = ((p->x + dx) % w + w) % w;
    p->y = ((p->y + dy) % h + h) % h;
    world.getTile(p->x, p->y).addPlayer(p);
}
_success = !this->_ejectedPlayers.empty();

On the server side (server/srcs/core/Server.cpp:268-309), for each victim an eject: K is sent (K computed by ejectDirectionToK from the push direction and the victim's direction), then all eggs on the tile are destroyed (edi + delete), and pex (ejector) plus ppo (moved victims) are broadcast.

Eggs destroyed by ejection

Any egg present on the tile at ejection time is removed, reducing the connection slots of the affected team accordingly. See Players, teams & eggs and Known limitations.

See also