Skip to content
Alrik Neihouser edited this page Jun 27, 2026 · 2 revisions

Server

The server is written in C++ and runs as a single process, single thread. It uses poll() for socket multiplexing so it never blocks waiting for one client.

Usage

./zappy_server -p port -x width -y height -n name1 name2 ... -c clientsNb -f freq
Flag Meaning
-p Port number
-x Map width
-y Map height
-n One or more team names (space-separated, collected until the next - flag)
-c Max clients per team
-f Frequency — how many ticks per second (default 100)

All flags are required. Passing -p 0, -x 0, -c 0, an empty team list, or a duplicate/reserved (GRAPHIC) team name exits with code 84.

Example:

./zappy_server -p 4242 -x 10 -y 10 -n TeamA TeamB -c 7 -f 100

High-level architecture

The server owns a game world (map + teams + clients) and runs one loop forever:

while running:
    wait up to (next_tick - now) ms for socket activity  ← poll()
    for each elapsed tick:
        advance_game()                                    ← food, actions, respawn

Three distinct concerns are kept separate:

  • Networking (NetworkServer, Polling.cpp) — raw TCP: accept, read, dispatch, write.
  • Game logic (game_loop.cpp) — food drain, action execution, resource respawn, win condition.
  • Protocol (Player, Gui) — turns raw command strings into typed actions and sends formatted responses.

Detailed architecture

main.cpp
└── Server
    ├── run()                  main loop — game ticks + poll_clients
    │
    ├── Polling.cpp            poll() + accept + per-client read/dispatch
    ├── server_helper.cpp      socket setup, client lifecycle, player/gui factory
    ├── client_helpers.cpp     move_player, parse_resource
    └── game_loop.cpp          advance_game, respawn_resources, drain_food, game_tick

Argument parsing (main.cpp)

handle_arguments() calls parse_server_args(), which scans argv with a dispatch table keyed on flag strings. The -n flag is handled separately: it greedily collects all following tokens that do not start with -. After parsing, validate_server_args() checks all fields are positive, team names are non-empty, non-duplicate, and not "GRAPHIC".

Tick clock (game_loop.cpp : Server::run)

A single std::chrono::steady_clock wall clock drives the tick counter. next_tick is advanced by exactly time_unit milliseconds per tick, where time_unit = 1000 / freq. The poll() call is given timeout = max(0, next_tick - now), so it never over-sleeps. If a slow frame causes now > next_tick, the loop fires multiple advance_game() calls back-to-back to catch up.

Game advancement (game_loop.cpp : Server::advance_game)

Each tick, in order:

  1. respawn_resources() — if tick - _last_respawn_tick >= RESPAWN_TICKS (20), count existing resources, add only the missing quantity one unit at a time at random tiles, then notify the GUI only for tiles that actually changed (bct).
  2. send_message_queue.send_messages(tick) — flush all timed messages whose deadline has passed.
  3. For each connected Player:
    • drain_food() — consume one food per FOOD_DRAIN_TICKS (126) ticks. If food reaches zero, the player is queued for death.
    • step_player_action() — if the current action's action_done_at has passed, mark it done (and, for incantation, call incantation_end); then dequeue and start the next command.
  4. Kill all queued-for-death players via kill_player() (sends "dead\n", removes them from the map and client list, notifies the GUI with pdi).

Socket multiplexing (Polling.cpp)

All file descriptors — the listening socket and every client socket — are kept in std::vector<pollfd> _fds. poll() runs with the full list. When POLLIN fires on the server fd, a new client is accepted. When it fires on a client fd, the client's buffer is read and every complete \n-terminated command is dispatched.

Connection handshake (server_helper.cpp : accept_new_client)

  1. Server sends WELCOME\n.
  2. Client replies with GRAPHIC\n → becomes a GUI, or a team name → becomes a player.
  3. Player: server validates the team name and checks spots_left > 0. If an egg for that team exists, the player spawns at the egg's tile (consuming the egg); otherwise at a random tile. Sends <slots_remaining>\n<width> <height>\n.
  4. GUI: create_gui() attaches the GUI to _gui_subject and immediately calls gui_start(), which pushes the full game snapshot (map size, all tile contents, team names, all live players with position/level/inventory, time unit).

Class hierarchy

Client  (abstract base)
├── NetworkClient   (unidentified connection, pre-handshake)
├── Player
└── Gui

Client (include/Client/Client.hpp)

Abstract base for all connections. Holds the socket fd (control_fd) and an incomplete-command buffer. Implements the Observer interface so any Client can be attached to a Subject and receive Update(message) notifications.

Player (include/Client/Player.hpp)

Represents an AI client. State:

Field Type Description
position array<int,2> X, Y on the map
orientation orientation_t enum NORTH / EAST / SOUTH / WEST
level int Current elevation (1–8)
team_name string Team this player belongs to
inventory Inventory 7 resource slots; starts with 10 food
busy bool Executing a timed command
action_done_at long long Tick at which the current command resolves
in_incantation bool Frozen while a ritual is in progress
next_food_at long long Next tick at which one food unit drains

Every player gets a unique auto-incrementing player_id.

Command timing

All player commands are timed in ticks (not wall time). When a command is dequeued, action_done_at = tick + delay:

Command Tick cost
Forward, Right, Left, Look, Broadcast, Eject, Take, Set 7
Inventory 1
Fork 42
Incantation 300
Connect_nbr 0 (immediate)

At freq = 100, one tick = 10 ms, so Forward takes 70 ms and Incantation takes 3 s — matching the spec's action_cost / f formula.

Player commands

Command Action
Forward Move one tile in facing direction (map wraps)
Right / Left Rotate 90° clockwise / counter-clockwise
Look Returns a vision grid — rows of 1, 3, 5, … (2·level+1) tiles ahead
Inventory Returns current resource counts
Take <res> / Set <res> Pick up / drop a resource on the current tile
Eject Push all other players on the tile one step in facing direction
Broadcast <text> Send a message to all players with a torus-correct direction hint
Connect_nbr Returns open slots in player's team
Fork Lay an egg (adds a slot, creates an Egg, notifies GUI with pfk + enw)
Incantation Start an elevation ritual

Unknown commands return ko\n.

Look grid

The look response is [tile0, tile1, ..., tileN]. Tile 0 is the player's own tile. The grid grows one row per player level: a level-L player sees rows of width 1, 3, 5, …, 2L+1 tiles ahead. Each tile lists player tokens for occupants and resource names. The view is relative to the player's facing direction and wraps around the torus correctly.

Broadcast direction

Broadcasts compute the shortest-path vector from sender to receiver on the torus (using the W/2, H/2 boundary to pick the right wrap direction), then rotate it into the receiver's local frame. The result k is in the range 0–8: 0 = same tile, 1 = straight ahead, 2–8 = clockwise sectors. Receivers get "message k, text\n".

Incantation (Incantation.cpp)

Two-phase:

Phase 1 (incantation_start, called when the command is dequeued):

  • Counts same-level players on the tile.
  • Calls check_requirements(level, players, tile.inventory) — verifies the right player count and stone quantities.
  • On success: marks all same-level players on the tile as in_incantation = true, sets action_done_at = tick + 300 for all of them, sends "Elevation underway\n" to each, notifies GUI with pic.
  • On failure: sends ko\n to the initiating player, no cooldown.

Phase 2 (incantation_end, called when action_done_at fires):

  • Re-checks requirements (players may have died or moved during the 300-tick wait).
  • On success: consumes the required stones from the tile, bumps all surviving participants to level + 1, sends "Current level: K\n" to each, notifies GUI with pie (result=1) and plv per participant.
  • On failure: sends ko\n to all participants.

Win condition: immediately after any level-up, the server scans every team. If any team has ≥ 6 live players at level 8, running = false and the loop exits.

Gui (include/Client/Gui.hpp)

Receives commands from the GUI client and receives passive event pushes via the Observer pattern.

GUI query commands (client → server)

Command Response Description
msz msz X Y Map dimensions
bct X Y bct X Y q0..q6 Tile resource counts
mct all tiles as bct lines Full map dump
tna tna N per team Team names
ppo #n ppo #n X Y O Player position + orientation
plv #n plv #n L Player level
pin #n pin #n X Y q0..q6 Player inventory
sgt sgt T Current time unit
sst T sst T Set server time unit

Unknown GUI commands return suc\n.

Passive server → GUI events

Pushed via _gui_subject.Notify(lambda) whenever the corresponding action happens:

Message Trigger
pnw #n X Y O L N New player connects
ppo #n X Y O Player moves or turns
plv #n L Player levels up
pin #n X Y q0..q6 Player inventory changes (take, set, food drain)
pex #n Player ejects others
pbc #n M Player broadcasts
pic X Y L #n... Incantation starts
pie X Y R Incantation ends (R=1 success, R=0 failure)
pfk #n Player starts fork
pdr #n i Player drops resource
pgt #n i Player picks up resource
pdi #n Player dies
enw #e #n X Y Egg laid
ebo #e Egg hatches (player connected from egg)
edi #e Egg dies
seg N Game over, team N wins

Observer pattern

The server holds two Subject instances:

  • _gui_subject — all Gui clients subscribe. Every game event that must be reflected in the GUI calls _gui_subject.Notify(lambda) where the lambda casts each observer to Gui* and calls the appropriate passive method.
  • _player_subject — passed to each Player at construction; currently unused for broadcasts but available for player-facing push events.

When an action happens (e.g., a player picks up a resource), the action handler calls:

server._gui_subject.Notify([player, resource, &server](Client* c) {
    auto gui = static_cast<Gui*>(c);
    gui->pgt(player, idx(resource));
    gui->pin(server, {std::to_string(player->getId())});
    gui->bct(server, {std::to_string(player->getX()), std::to_string(player->getY())});
});

Map and Tiles

_map is a vector<vector<Tiles>> indexed as _map[y][x].

Each Tiles holds:

  • Inventory inventory — how many of each resource are on this tile.
  • vector<weak_ptr<Player>> players — who is currently standing here (weak pointers to avoid cycles with the _clients map).
  • shared_ptr<Egg> egg — at most one egg per tile (nullable, legacy field; eggs are now stored in team lists).

The map is a torus: all movement and direction calculations use modulo arithmetic.


Resource system

Quantities are computed as floor(width * height * density). At startup and every RESPAWN_TICKS (20) ticks, the server counts how many of each resource already exist on the floor, then drops only the deficit one unit at a time at uniformly-random tiles. This guarantees the map never goes below the target density and avoids the uneven distribution of a simple random-scatter.

After each respawn, the GUI is notified only for tiles whose resource counts actually changed.


Egg and fork system

When a player sends Fork:

  1. A new Egg object is created at the player's current position with parent_player_id set.
  2. The egg is added to the team's eggs list and spots_left is incremented.
  3. The GUI receives pfk (fork started) and enw (egg created at position).

When a player connects to a team that has eggs:

  1. The newest egg is popped from the team list, spots_left is decremented.
  2. The player spawns at the egg's tile.
  3. The GUI receives pnw (new player) and ebo (egg hatched).

Send queue (SendMessageQueue)

Not all responses are sent immediately. The queue stores (fd, message, deadline_tick) tuples. send_messages(tick) flushes all entries whose deadline ≤ current tick. This lets the server simulate async response timing so an AI's pipelined commands are answered in the correct timed order, even if the server processes them all in the same tick.