-
Notifications
You must be signed in to change notification settings - Fork 1
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.
./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 100The 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.
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
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".
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.
Each tick, in order:
-
respawn_resources()— iftick - _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). -
send_message_queue.send_messages(tick)— flush all timed messages whose deadline has passed. - For each connected Player:
-
drain_food()— consume one food perFOOD_DRAIN_TICKS(126) ticks. If food reaches zero, the player is queued for death. -
step_player_action()— if the current action'saction_done_athas passed, mark it done (and, for incantation, callincantation_end); then dequeue and start the next command.
-
- Kill all queued-for-death players via
kill_player()(sends"dead\n", removes them from the map and client list, notifies the GUI withpdi).
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.
- Server sends
WELCOME\n. - Client replies with
GRAPHIC\n→ becomes a GUI, or a team name → becomes a player. -
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. -
GUI:
create_gui()attaches the GUI to_gui_subjectand immediately callsgui_start(), which pushes the full game snapshot (map size, all tile contents, team names, all live players with position/level/inventory, time unit).
Client (abstract base)
├── NetworkClient (unidentified connection, pre-handshake)
├── Player
└── Gui
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.
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.
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.
| 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.
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.
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".
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, setsaction_done_at = tick + 300for all of them, sends"Elevation underway\n"to each, notifies GUI withpic. - On failure: sends
ko\nto 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 withpie(result=1) andplvper participant. - On failure: sends
ko\nto 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.
Receives commands from the GUI client and receives passive event pushes via the Observer pattern.
| 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.
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 |
The server holds two Subject instances:
-
_gui_subject— allGuiclients subscribe. Every game event that must be reflected in the GUI calls_gui_subject.Notify(lambda)where the lambda casts each observer toGui*and calls the appropriate passive method. -
_player_subject— passed to eachPlayerat 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 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_clientsmap). -
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.
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.
When a player sends Fork:
- A new
Eggobject is created at the player's current position withparent_player_idset. - The egg is added to the team's
eggslist andspots_leftis incremented. - The GUI receives
pfk(fork started) andenw(egg created at position).
When a player connects to a team that has eggs:
- The newest egg is popped from the team list,
spots_leftis decremented. - The player spawns at the egg's tile.
- The GUI receives
pnw(new player) andebo(egg hatched).
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.