commit 8a92974912bed41810cb87ca9d5b5648f2f6f06a
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Fri, 3 May 2024 18:21:43 +0000
Initial commit
Diffstat:
A | .editorconfig | | | 16 | ++++++++++++++++ |
A | PROTO.md | | | 371 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | agent/agent.c | | | 120 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | agent/agent.h | | | 32 | ++++++++++++++++++++++++++++++++ |
A | build.sh | | | 21 | +++++++++++++++++++++ |
A | catan/.catan.h.swp | | | 0 | |
A | catan/catan.c | | | 413 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | catan/catan.h | | | 254 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | clean.sh | | | 5 | +++++ |
A | rules.pdf | | | 0 | |
A | server/server.c | | | 288 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | server/server.h | | | 78 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
12 files changed, 1598 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+guidelines = 80 120 160
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+
+[*.{md,txt}]
+indent_style = space
+indent_size = 2
+
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
diff --git a/PROTO.md b/PROTO.md
@@ -0,0 +1,371 @@
+Catan
+-------------------------------------------------------------------------------
+
+This protocol is based on the [Catan 5th ed. base game rules](catan-5ed-rules).
+
+A rough description of the game, taken from the above rules, is as follows:
+
+- Catan plays on a hexagonal grid, with each hex (tile) having an associated
+ resource and counter. The board's tiles and counter values are generated
+ randomly, and players get assigned 2 placed settlements and 2 placed roads,
+ and a set of resource cards as their starting hand. In addition to resources,
+ there are one or more desert tiles (on one of which the robber is initially
+ placed) which produce nothing. At the edge of the board are "coasts", and
+ at coast-tile intersections are placed harbours.
+
+- On each player's turn, two dice are first rolled and each tile with the
+ matching counter value produces its associated resource for all players with
+ a settlement or city on one of the vertices of said tile. The current player
+ then enters their combined build/trade phase.
+
+- Should a 7 be rolled, all players with more than 7 cards in hand discard
+ half of their hand (rounded down), and the current player moves the robber
+ to a new tile. After moving the robber, the current player chooses a target
+ player to steal from (from the set of players with settlements or cities
+ on the robber tile's vertices), and moves one random resource card from the
+ target player's hand into their own.
+
+- During a player's turn, they may spend resources to build structures (roads
+ or settlements), upgrade a settlement into a city, buy a development card,
+ play a development card bought at least 1 turn ago, or offer a trade to the
+ bank or another player.
+
+- Trades allow for a player to trade `n` of one resource for `m` of a different
+ type of resource, either in a `4:1` ratio (in the case a default bank trade),
+ a `3:1` ratio (in the case of a default harbour trade), a `2:1` ratio (in the
+ case of a specialty harbour trade), or an `n:m` ratio (in the case of a
+ player trade).
+
+Catan: Protocol
+===============================================================================
+
+This protocol is intended for use over a network, and is assumed to use a
+byte-oriented stream transport protocol (such as TCP). The protocol message
+description below is written in a C-like language, with the caveat that
+anonymous unions describe conditional parts of a message (with the condition
+noted by a comment), and that the `<stdint.h>` header for fixed-size types is
+assumed to be included.
+
+```c
+// NOTE: https://www.redblobgames.com/grids/hexagons/#coordinates-axial
+struct tilepos {
+ int8_t q, r;
+};
+
+// NOTE: https://www.redblobgames.com/grids/parts/#hexagon-relationships
+enum edge : uint8_t {
+ W = 0,
+ NW = 1,
+ NE = 2,
+};
+
+struct edgepos {
+ int8_t q, r;
+ enum edge e;
+};
+
+// NOTE: https://www.redblobgames.com/grids/parts/#hexagon-relationships
+enum vert : uint8_t {
+ N = 0,
+ S = 1,
+};
+
+struct vertpos {
+ int8_t q, r;
+ enum vert v;
+};
+
+enum resource : uint8_t {
+ ANY = 0,
+ BRICK = 1,
+ GRAIN = 2,
+ LUMBER = 3,
+ ORE = 4,
+ WOOL = 5,
+};
+
+struct trade_offer {
+ enum resource resource;
+ uint8_t count;
+};
+
+enum tile_type : uint8_t {
+ TILE_EMPTY = 0,
+ TILE_HILLS = BRICK,
+ TILE_FIELDS = GRAIN,
+ TILE_FOREST = LUMBER,
+ TILE_MOUNTAINS = ORE,
+ TILE_PASTURE = WOOL,
+ TILE_DESERT = 6,
+};
+
+struct tile {
+ enum tile_type type;
+ uint8_t counter;
+};
+
+enum buildable : uint8_t {
+ ROAD = 0,
+ SETTLEMENT = 1,
+ CITY = 2,
+ DEVELOPMENT_CARD = 3,
+};
+
+enum devcard : uint8_t {
+ KNIGHT = 0,
+ ROAD_BUILDING = 1,
+ YEAR_OF_PLENTY = 2,
+ MONOPOLY = 3,
+ VICTORY_POINT = 4,
+};
+
+enum msg_type : uint8_t {
+ MSG_BEGIN = 0, // gives game setup info
+ MSG_SETUP = 1, // sends initial board and agent states
+ MSG_KICK = 2, // removes a misbehaving agent from the game
+ MSG_END = 3, // gives the winner of the game
+
+ MSG_TURN = 10, // marks beginning of given agent's turn
+ MSG_ROLL = 11, // request a roll
+ MSG_ROLL_RESULT = 12, // public result of rolling a dice
+ MSG_PLAY = 13, // plays a development card
+ MSG_BUILD = 14, // builds a structure or development card
+ MSG_BUILD_RESULT = 15, // private result of building a structure or devcard
+ MSG_TRADE = 16, // requests a trade with a given agent
+ MSG_TRADE_ACCEPT = 17, // accepts a pending trade offer
+ MSG_TRADE_REJECT = 18, // rejects a pending trade offer
+ MSG_PASS = 19, // marks end of agent's turn
+
+ MSG_ROB = 20, // moves the robber to a new tile, and robs an agent
+ MSG_ROB_RESULT = 21, // public result of robbing a given agent
+};
+
+struct msg_begin_tag {
+ uint32_t timeout_secs;
+ uint32_t thread_limit;
+ uint32_t memory_limit; // in MiB
+ uint8_t board_radius; // excluding central tile
+ uint8_t player_id;
+
+ uint32_t cell_count;
+ uint32_t harbour_count;
+ uint32_t player_count;
+};
+
+struct msg_setup_tag {
+ struct {
+ struct tilepos pos;
+ struct tile tile;
+ } cells[begin.cell_count];
+
+ struct {
+ struct vertpos vert1;
+ struct vertpos vert2;
+ enum resource resource;
+ uint8_t cost;
+ } harbours[begin.harbour_count];
+
+ struct {
+ uint8_t id;
+ struct vertpos house1;
+ struct edgepos road1;
+ struct vertpos house2;
+ struct edgepos road2;
+ enum resource starting_hand[3];
+ } players[begin.player_count];
+};
+
+struct msg_kick_tag {
+ uint8_t kick_id;
+};
+
+struct msg_end_tag {
+ uint8_t winner_id;
+};
+
+struct msg_turn_tag {
+ uint8_t player_id;
+};
+
+struct msg_roll_tag {};
+
+struct msg_roll_result {
+ uint8_t roll;
+};
+
+struct msg_play_tag {
+ uint8_t player_id;
+ enum devcard card;
+};
+
+struct msg_build_tag {
+ uint8_t player_id;
+ enum buildable type;
+ union {
+ struct edgepos edgepos; // if type == ROAD
+ struct vertpos vertpos; // if type == SETTLEMENT || type == CITY
+ };
+};
+
+struct msg_build_result_tag {
+ enum buildable type;
+ union {
+ enum devcard card; // if type == DEVELOPMENT_CARD
+ };
+};
+
+struct msg_trade_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+ struct trade_offer offer;
+ struct trade_offer ask;
+};
+
+struct msg_trade_accept_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+};
+
+struct msg_trade_reject_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+};
+
+struct msg_pass_tag {};
+
+struct msg_rob_tag {
+ struct tilepos tilepos;
+ uint8_t target_id;
+};
+
+struct msg_rob_result_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+ enum resource card;
+};
+
+union msg_tag {
+ struct msg_begin_tag begin;
+ struct msg_setup_tag setup;
+ struct msg_kick_tag kick;
+ struct msg_end_tag end;
+
+ struct msg_turn_tag turn;
+ struct msg_roll_tag roll;
+ struct msg_roll_result_tag roll_result;
+ struct msg_play_tag play;
+ struct msg_build_tag build;
+ struct msg_build_result_tag build_result;
+ struct msg_trade_tag trade;
+ struct msg_trade_accept_tag trade_accept;
+ struct msg_trade_reject_tag trade_reject;
+ struct msg_pass_tag pass;
+
+ struct msg_rob_tag rob;
+ struct msg_rob_result_tag rob_result;
+};
+
+struct msg {
+ enum msg_type type;
+ union msg_tag tag;
+};
+```
+
+Catan: Protocol: Server Flow
+===============================================================================
+
+The server finite state machine is roughly described below in pseudocode:
+
+```text
+INIT:
+ generate board and agent state
+ GOTO ACCEPT
+
+ACCEPT:
+ start all agent processes
+ accept all agent connections
+ GOTO BEGIN
+
+BEGIN:
+ send `MSG_BEGIN` to all agents, assigning player ids from 0 to N
+ set `player_id` to id of last agent in agent turn order
+ GOTO TURN
+
+TURN:
+ set `player_id` to next agent in agent turn order
+ send `MSG_TURN` to all agents
+
+ mark `agents[player_id]` as not having played a development card this turn
+ mark `agents[player_id]` as not having rolled this turn
+ while `agents[player_id]` has not timed out
+ if `agents[player_id]` has not rolled this turn
+ set `expected_messages` to { MSG_ROLL, MSG_PLAY }
+ else
+ set `expected_messages` to { MSG_PASS, MSG_PLAY, MSG_BUILD, MSG_TRADE }
+
+ receive a message `msg` from `agents[player_id]`
+ if `msg.type` not in `expected_messages`
+ set `kick_id` to `player_id`
+ GOTO KICK
+
+ if `msg.type == MSG_PASS`
+ GOTO PASS
+ else if `msg.type == MSG_ROLL`
+ mark `agents[player_id]` as having rolled this turn
+ generate a random dice roll between 2 and 12
+ update hand state for all agents
+ send `MSG_ROLL_RESULT` to all agents
+ if dice rolled a 7
+ receive a message `msg` from `agents[player_id]`
+ if `msg.type != MSG_ROB`
+ set `kick_id` to `player_id`
+ GOTO KICK
+ else
+ send `msg` to all other agents
+ move the robber to the tile at `msg.tilepos`
+ select a random resource from the hand of `agents[msg.target_id]`
+ send `MSG_ROB_RESULT` to `agents[player_id]` and `agents[msg.target_id]`
+ else if `msg.type == MSG_PLAY`
+ if `agents[player_id]` has already played a development card this turn
+ set `kick_id` to `player_id`
+ GOTO KICK
+ else
+ mark `agents[player_id]` as having played a development card this turn
+ play `msg.devcard`
+ send `msg` to all other agents
+ else if `msg.type == MSG_BUILD`
+ build structure or buy development card
+ send `msg` to all other agents
+ send `MSG_BUILD_RESULT` to `agents[player_id]`
+ else if `msg.type == MSG_TRADE`
+ send `msg` to all other agents
+ receive a message `response` from `agents[msg.target_id]`
+ if `response.type` not in { MSG_TRADE_ACCEPT, MSG_TRADE_REJECT }
+ set `kick_id` to `msg.target_id`
+ GOTO KICK
+ else
+ send `response` to all other agents
+ update hands of `agents[msg.player_id]` and `agents[msg.target_id]`
+
+PASS:
+ if `agents[player_id]` has reached 10 victory points
+ GOTO END
+ else
+ GOTO TURN
+
+KICK:
+ send `MSG_KICK` for `agents[kick_id]` to all other agents
+ close `agents[kick_id]` connection
+ GOTO TURN
+
+END:
+ send `MSG_END` to all agents
+ GOTO DEINIT
+
+DEINIT:
+ close all agent connections
+```
+
+Catan: References
+===============================================================================
+[catan-5ed-rules]: https://www.catan.com/sites/default/files/2021-06/catan_base_rules_2020_200707.pdf
diff --git a/agent/agent.c b/agent/agent.c
@@ -0,0 +1,120 @@
+#include "agent.h"
+
+struct opts opts = {
+ .verbose = false,
+};
+
+static char const *optstr = "hv";
+#define POSITIONAL_ARGS 2
+
+static void
+usage(char **argv)
+{
+ fprintf(stderr, "Usage: %s [-hv] <addr> <port>\n", argv[0]);
+ fprintf(stderr, "\t-h :\n\t\tdisplays this help message\n");
+ fprintf(stderr, "\t-v :\n\t\tenabled verbose logging\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\thost :\n\t\tthe server address to connect to\n");
+ fprintf(stderr, "\tport :\n\t\tthe server port to connect to\n");
+}
+
+#include <getopt.h>
+
+static bool
+parse_args(int argc, char **argv)
+{
+ int opt;
+ while ((opt = getopt(argc, argv, optstr)) != -1) {
+ switch (opt) {
+ case 'v':
+ opts.verbose = true;
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ if (argc < optind + POSITIONAL_ARGS)
+ return false;
+
+ opts.addr = argv[optind++];
+ opts.port = argv[optind++];
+
+ return true;
+}
+
+static void
+kill_handler(int signum)
+{
+ fprintf(stderr, "Signal %d received\n", signum);
+ exit(EXIT_SUCCESS);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct sigaction sigkill = {
+ .sa_handler = kill_handler,
+ };
+
+ sigaction(SIGKILL, &sigkill, NULL);
+
+ if (!parse_args(argc, argv)) {
+ usage(argv);
+ exit(EXIT_FAILURE);
+ }
+
+ fprintf(stderr, "====== Args ======\n");
+ fprintf(stderr, " verbose: %d\n", opts.verbose);
+ fprintf(stderr, " server: %s:%s\n", opts.addr, opts.port);
+
+ struct addrinfo hints = {
+ .ai_flags = AI_NUMERICHOST | AI_NUMERICSERV,
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo(opts.addr, opts.port, &hints, &addrinfo))) {
+ fprintf(stderr, "Failed to get server address info: %s\n", gai_strerror(res));
+ exit(EXIT_FAILURE);
+ }
+
+ int fd;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ fd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
+ if (fd == -1)
+ continue;
+
+ if (connect(fd, ptr->ai_addr, ptr->ai_addrlen) == -1) {
+ perror("connect");
+ close(fd);
+ continue;
+ }
+
+ break;
+ }
+
+ if (ptr) {
+ char host[NI_MAXHOST], serv[NI_MAXSERV];
+ getnameinfo(ptr->ai_addr, ptr->ai_addrlen,
+ host, ARRLEN(host), serv, ARRLEN(serv),
+ AI_NUMERICHOST | AI_NUMERICSERV);
+
+ fprintf(stderr, "Connected to server: %s:%s\n", host, serv);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) {
+ fprintf(stderr, "Failed to connect to server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ while (true) {
+ sleep(1);
+ }
+
+ exit(EXIT_SUCCESS);
+}
diff --git a/agent/agent.h b/agent/agent.h
@@ -0,0 +1,32 @@
+#ifndef AGENT_H
+#define AGENT_H
+
+#define _XOPEN_SOURCE 700
+#define _GNU_SOURCE
+#define _DEFAULT_SOURCE
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "catan.h"
+
+#include <signal.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define GAME_BOARD_SIZE 3
+
+struct opts {
+ bool verbose;
+ char *addr;
+ char *port;
+};
+
+#endif /* AGENT_H */
diff --git a/build.sh b/build.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+CC="${CC:-clang}"
+AR="${AR:-ar}"
+RANLIB="${RANLIB:-ranlib}"
+
+WARNINGS="-Wall -Wextra -Wpedantic -Werror -Wno-fixed-enum-extension -Wno-gnu-empty-struct"
+
+CFLAGS="-std=c2x -Og -g"
+CPPFLAGS="-Icatan"
+
+set -ex
+
+mkdir -p bin obj
+
+$CC -o obj/catan.o -c catan/catan.c $WARNINGS $CFLAGS $CPPFLAGS
+$AR -rcs bin/libcatan.a obj/catan.o
+$RANLIB bin/libcatan.a
+
+$CC -static -o bin/server server/server.c bin/libcatan.a $WARNINGS $CFLAGS $CPPFLAGS
+$CC -static -o bin/agent agent/agent.c bin/libcatan.a $WARNINGS $CFLAGS $CPPFLAGS
diff --git a/catan/.catan.h.swp b/catan/.catan.h.swp
Binary files differ.
diff --git a/catan/catan.c b/catan/catan.c
@@ -0,0 +1,413 @@
+#include "catan.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+static inline bool
+try_read_u8(int sockfd, uint8_t *out)
+{
+ return recv(sockfd, out, 1, 0) == 1;
+}
+
+static inline bool
+try_read_u32(int sockfd, uint32_t *out)
+{
+ uint32_t buf;
+ if (recv(sockfd, &buf, sizeof buf, 0) != sizeof buf) return false;
+ *out = ntohl(buf);
+ return true;
+}
+
+static inline bool
+try_read_s8(int sockfd, int8_t *out)
+{
+ return recv(sockfd, out, 1, 0) == 1;
+}
+
+static inline bool
+try_read_tilepos(int sockfd, struct tilepos *out)
+{
+ return try_read_s8(sockfd, &out->q)
+ && try_read_s8(sockfd, &out->r);
+}
+
+static inline bool
+try_read_edgepos(int sockfd, struct edgepos *out)
+{
+ return try_read_s8(sockfd, &out->q)
+ && try_read_s8(sockfd, &out->r)
+ && try_read_u8(sockfd, &out->e);
+}
+
+static inline bool
+try_read_vertpos(int sockfd, struct vertpos *out)
+{
+ return try_read_s8(sockfd, &out->q)
+ && try_read_s8(sockfd, &out->r)
+ && try_read_u8(sockfd, &out->v);
+}
+
+static inline bool
+try_read_trade_offer(int sockfd, struct trade_offer *out)
+{
+ return try_read_u8(sockfd, &out->resource)
+ && try_read_u8(sockfd, &out->count);
+}
+
+static inline bool
+try_read_cell_infos(int sockfd, struct cell_info *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_read_tilepos(sockfd, &buf[i].pos)) return false;
+ if (!try_read_u8(sockfd, &buf[i].tile.type)) return false;
+ if (!try_read_u8(sockfd, &buf[i].tile.counter)) return false;
+ }
+
+ return true;
+}
+
+static inline bool
+try_read_harbour_infos(int sockfd, struct harbour_info *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_read_vertpos(sockfd, &buf[i].vert1)) return false;
+ if (!try_read_vertpos(sockfd, &buf[i].vert2)) return false;
+ if (!try_read_u8(sockfd, &buf[i].resource)) return false;
+ if (!try_read_u8(sockfd, &buf[i].cost)) return false;
+ }
+
+ return true;
+}
+
+static inline bool
+try_read_player_infos(int sockfd, struct player_info *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_read_u8(sockfd, &buf[i].id)) return false;
+ if (!try_read_vertpos(sockfd, &buf[i].settlement1)) return false;
+ if (!try_read_edgepos(sockfd, &buf[i].road1)) return false;
+ if (!try_read_vertpos(sockfd, &buf[i].settlement2)) return false;
+ if (!try_read_edgepos(sockfd, &buf[i].road2)) return false;
+ if (!try_read_u8(sockfd, &buf[i].starting_hand[0])) return false;
+ if (!try_read_u8(sockfd, &buf[i].starting_hand[1])) return false;
+ if (!try_read_u8(sockfd, &buf[i].starting_hand[2])) return false;
+ }
+
+ return true;
+}
+
+bool
+try_recv_msg(int sockfd, struct msg *out)
+{
+ if (!try_read_u8(sockfd, &out->type)) return false;
+
+ switch (out->type) {
+ case MSG_BEGIN: {
+ if (!try_read_u32(sockfd, &out->tag.begin.timeout_secs)) return false;
+ if (!try_read_u32(sockfd, &out->tag.begin.thread_limit)) return false;
+ if (!try_read_u32(sockfd, &out->tag.begin.memory_limit)) return false;
+ if (!try_read_u8(sockfd, &out->tag.begin.board_radius)) return false;
+ if (!try_read_u8(sockfd, &out->tag.begin.player_id)) return false;
+
+ if (!try_read_u32(sockfd, &out->tag.begin.cell_count)) return false;
+ if (!try_read_u32(sockfd, &out->tag.begin.harbour_count)) return false;
+ if (!try_read_u32(sockfd, &out->tag.begin.player_count)) return false;
+
+ return true;
+ } break;
+
+ case MSG_SETUP: {
+ assert(out->tag.setup.cell_count);
+ assert(out->tag.setup.harbour_count);
+ assert(out->tag.setup.player_count);
+ assert(out->tag.setup.cells);
+ assert(out->tag.setup.harbours);
+ assert(out->tag.setup.players);
+
+ if (!try_read_cell_infos(sockfd, out->tag.setup.cells, out->tag.setup.cell_count)) return false;
+ if (!try_read_harbour_infos(sockfd, out->tag.setup.harbours, out->tag.setup.harbour_count)) return false;
+ if (!try_read_player_infos(sockfd, out->tag.setup.players, out->tag.setup.player_count)) return false;
+
+ return true;
+ } break;
+
+ case MSG_KICK:
+ return try_read_u8(sockfd, &out->tag.kick.kick_id);
+
+ case MSG_END:
+ return try_read_u8(sockfd, &out->tag.end.winner_id);
+
+ case MSG_TURN:
+ return try_read_u8(sockfd, &out->tag.turn.player_id);
+
+ case MSG_ROLL:
+ return true;
+
+ case MSG_ROLL_RESULT:
+ return try_read_u8(sockfd, &out->tag.roll_result.roll);
+
+ case MSG_PLAY:
+ return try_read_u8(sockfd, &out->tag.play.player_id)
+ && try_read_u8(sockfd, &out->tag.play.card);
+
+ case MSG_BUILD: {
+ if (!try_read_u8(sockfd, &out->tag.build.player_id)) return false;
+ if (!try_read_u8(sockfd, &out->tag.build.type)) return false;
+
+ switch (out->tag.build.type) {
+ case ROAD:
+ return try_read_edgepos(sockfd, &out->tag.build.edgepos);
+
+ case SETTLEMENT:
+ case CITY:
+ return try_read_vertpos(sockfd, &out->tag.build.vertpos);
+
+ default:
+ return true;
+ }
+ } break;
+
+ case MSG_BUILD_RESULT: {
+ if (!try_read_u8(sockfd, &out->tag.build_result.type)) return false;
+
+ switch (out->tag.build_result.type) {
+ case DEVELOPMENT_CARD:
+ return try_read_u8(sockfd, &out->tag.build_result.card);
+
+ default:
+ return true;
+ }
+ } break;
+
+ case MSG_TRADE:
+ return try_read_u8(sockfd, &out->tag.trade.player_id)
+ && try_read_u8(sockfd, &out->tag.trade.target_id)
+ && try_read_trade_offer(sockfd, &out->tag.trade.offer)
+ && try_read_trade_offer(sockfd, &out->tag.trade.ask);
+
+ case MSG_TRADE_ACCEPT:
+ return try_read_u8(sockfd, &out->tag.trade_accept.player_id)
+ && try_read_u8(sockfd, &out->tag.trade_accept.target_id);
+
+ case MSG_TRADE_REJECT:
+ return try_read_u8(sockfd, &out->tag.trade_reject.player_id)
+ && try_read_u8(sockfd, &out->tag.trade_reject.target_id);
+
+ case MSG_PASS:
+ return true;
+
+ case MSG_ROB:
+ return try_read_tilepos(sockfd, &out->tag.rob.tilepos)
+ && try_read_u8(sockfd, &out->tag.rob.target_id);
+
+ case MSG_ROB_RESULT:
+ return try_read_u8(sockfd, &out->tag.rob_result.player_id)
+ && try_read_u8(sockfd, &out->tag.rob_result.target_id)
+ && try_read_u8(sockfd, &out->tag.rob_result.resource);
+
+ default:
+ return false;
+ }
+}
+
+static inline bool
+try_write_u8(int sockfd, uint8_t val)
+{
+ return send(sockfd, &val, 1, 0) == 1;
+}
+
+static inline bool
+try_write_u32(int sockfd, uint32_t val)
+{
+ uint32_t buf = htonl(val);
+ return send(sockfd, &buf, sizeof buf, 0) == sizeof buf;
+}
+
+static inline bool
+try_write_s8(int sockfd, int8_t val)
+{
+ return send(sockfd, &val, 1, 0) == 1;
+}
+
+static inline bool
+try_write_tilepos(int sockfd, struct tilepos val)
+{
+ return try_write_s8(sockfd, val.q)
+ && try_write_s8(sockfd, val.r);
+}
+
+static inline bool
+try_write_edgepos(int sockfd, struct edgepos val)
+{
+ return try_write_s8(sockfd, val.q)
+ && try_write_s8(sockfd, val.r)
+ && try_write_u8(sockfd, val.e);
+}
+
+static inline bool
+try_write_vertpos(int sockfd, struct vertpos val)
+{
+ return try_write_s8(sockfd, val.q)
+ && try_write_s8(sockfd, val.r)
+ && try_write_u8(sockfd, val.v);
+}
+
+static inline bool
+try_write_trade_offer(int sockfd, struct trade_offer val)
+{
+ return try_write_u8(sockfd, val.resource)
+ && try_write_u8(sockfd, val.count);
+}
+
+static inline bool
+try_write_cell_infos(int sockfd, struct cell_info const *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_write_tilepos(sockfd, buf[i].pos)) return false;
+ if (!try_write_u8(sockfd, buf[i].tile.type)) return false;
+ if (!try_write_u8(sockfd, buf[i].tile.counter)) return false;
+ }
+
+ return true;
+}
+
+static inline bool
+try_write_harbour_infos(int sockfd, struct harbour_info const *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_write_vertpos(sockfd, buf[i].vert1)) return false;
+ if (!try_write_vertpos(sockfd, buf[i].vert2)) return false;
+ if (!try_write_u8(sockfd, buf[i].resource)) return false;
+ if (!try_write_u8(sockfd, buf[i].cost)) return false;
+ }
+
+ return true;
+}
+
+static inline bool
+try_write_player_infos(int sockfd, struct player_info const *buf, uint32_t count)
+{
+ for (uint32_t i = 0; i < count; i++) {
+ if (!try_write_u8(sockfd, buf[i].id)) return false;
+ if (!try_write_vertpos(sockfd, buf[i].settlement1)) return false;
+ if (!try_write_edgepos(sockfd, buf[i].road1)) return false;
+ if (!try_write_vertpos(sockfd, buf[i].settlement2)) return false;
+ if (!try_write_edgepos(sockfd, buf[i].road2)) return false;
+ if (!try_write_u8(sockfd, buf[i].starting_hand[0])) return false;
+ if (!try_write_u8(sockfd, buf[i].starting_hand[1])) return false;
+ if (!try_write_u8(sockfd, buf[i].starting_hand[2])) return false;
+ }
+
+ return true;
+}
+
+bool
+try_send_msg(int sockfd, struct msg const *msg)
+{
+ if (!try_write_u8(sockfd, msg->type)) return false;
+
+ switch (msg->type) {
+ case MSG_BEGIN: {
+ if (!try_write_u32(sockfd, msg->tag.begin.timeout_secs)) return false;
+ if (!try_write_u32(sockfd, msg->tag.begin.thread_limit)) return false;
+ if (!try_write_u32(sockfd, msg->tag.begin.memory_limit)) return false;
+ if (!try_write_u8(sockfd, msg->tag.begin.board_radius)) return false;
+ if (!try_write_u8(sockfd, msg->tag.begin.player_id)) return false;
+
+ if (!try_write_u32(sockfd, msg->tag.begin.cell_count)) return false;
+ if (!try_write_u32(sockfd, msg->tag.begin.harbour_count)) return false;
+ if (!try_write_u32(sockfd, msg->tag.begin.player_count)) return false;
+
+ return true;
+ } break;
+
+ case MSG_SETUP: {
+ if (!try_write_cell_infos(sockfd, msg->tag.setup.cells, msg->tag.setup.cell_count)) return false;
+ if (!try_write_harbour_infos(sockfd, msg->tag.setup.harbours, msg->tag.setup.harbour_count)) return false;
+ if (!try_write_player_infos(sockfd, msg->tag.setup.players, msg->tag.setup.player_count)) return false;
+
+ return true;
+ } break;
+
+ case MSG_KICK:
+ return try_write_u8(sockfd, msg->tag.kick.kick_id);
+
+ case MSG_END:
+ return try_write_u8(sockfd, msg->tag.end.winner_id);
+
+ case MSG_TURN:
+ return try_write_u8(sockfd, msg->tag.turn.player_id);
+
+ case MSG_ROLL:
+ return true;
+
+ case MSG_ROLL_RESULT:
+ return try_write_u8(sockfd, msg->tag.roll_result.roll);
+
+ case MSG_PLAY:
+ return try_write_u8(sockfd, msg->tag.play.player_id)
+ && try_write_u8(sockfd, msg->tag.play.card);
+
+ case MSG_BUILD: {
+ if (!try_write_u8(sockfd, msg->tag.build.player_id)) return false;
+ if (!try_write_u8(sockfd, msg->tag.build.type)) return false;
+
+ switch (msg->tag.build.type) {
+ case ROAD:
+ return try_write_edgepos(sockfd, msg->tag.build.edgepos);
+
+ case SETTLEMENT:
+ case CITY:
+ return try_write_vertpos(sockfd, msg->tag.build.vertpos);
+
+ default:
+ return true;
+ }
+ } break;
+
+ case MSG_BUILD_RESULT: {
+ if (!try_write_u8(sockfd, msg->tag.build_result.type)) return false;
+
+ switch (msg->tag.build_result.type) {
+ case DEVELOPMENT_CARD:
+ return try_write_u8(sockfd, msg->tag.build_result.card);
+
+ default:
+ return true;
+ }
+ } break;
+
+ case MSG_TRADE:
+ return try_write_u8(sockfd, msg->tag.trade.player_id)
+ && try_write_u8(sockfd, msg->tag.trade.target_id)
+ && try_write_trade_offer(sockfd, msg->tag.trade.offer)
+ && try_write_trade_offer(sockfd, msg->tag.trade.ask);
+
+ case MSG_TRADE_ACCEPT:
+ return try_write_u8(sockfd, msg->tag.trade_accept.player_id)
+ && try_write_u8(sockfd, msg->tag.trade_accept.target_id);
+
+ case MSG_TRADE_REJECT:
+ return try_write_u8(sockfd, msg->tag.trade_reject.player_id)
+ && try_write_u8(sockfd, msg->tag.trade_reject.target_id);
+
+ case MSG_PASS:
+ return true;
+
+ case MSG_ROB:
+ return try_write_tilepos(sockfd, msg->tag.rob.tilepos)
+ && try_write_u8(sockfd, msg->tag.rob.target_id);
+
+ case MSG_ROB_RESULT:
+ return try_write_u8(sockfd, msg->tag.rob_result.player_id)
+ && try_write_u8(sockfd, msg->tag.rob_result.target_id)
+ && try_write_u8(sockfd, msg->tag.rob_result.resource);
+
+ default:
+ return false;
+ }
+}
diff --git a/catan/catan.h b/catan/catan.h
@@ -0,0 +1,254 @@
+#ifndef CATAN_H
+#define CATAN_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+// NOTE: https://www.redblobgames.com/grids/hexagons/#coordinates-axial
+struct tilepos {
+ int8_t q, r;
+};
+
+// NOTE: https://www.redblobgames.com/grids/parts/#hexagon-relationships
+enum edge : uint8_t {
+ W = 0,
+ NW = 1,
+ NE = 2,
+};
+
+struct edgepos {
+ int8_t q, r;
+ enum edge e;
+};
+
+// NOTE: https://www.redblobgames.com/grids/parts/#hexagon-relationships
+enum vert : uint8_t {
+ N = 0,
+ S = 1,
+};
+
+struct vertpos {
+ int8_t q, r;
+ enum vert v;
+};
+
+enum resource : uint8_t {
+ ANY = 0,
+ BRICK = 1,
+ GRAIN = 2,
+ LUMBER = 3,
+ ORE = 4,
+ WOOL = 5,
+};
+
+struct trade_offer {
+ enum resource resource;
+ uint8_t count;
+};
+
+enum tile_type : uint8_t {
+ TILE_EMPTY = 0,
+ TILE_HILLS = BRICK,
+ TILE_FIELDS = GRAIN,
+ TILE_FOREST = LUMBER,
+ TILE_MOUNTAINS = ORE,
+ TILE_PASTURE = WOOL,
+ TILE_DESERT = 6,
+};
+
+struct tile {
+ enum tile_type type;
+ uint8_t counter;
+};
+
+enum buildable : uint8_t {
+ ROAD = 0,
+ SETTLEMENT = 1,
+ CITY = 2,
+ DEVELOPMENT_CARD = 3,
+};
+
+enum devcard : uint8_t {
+ KNIGHT = 0,
+ ROAD_BUILDING = 1,
+ YEAR_OF_PLENTY = 2,
+ MONOPOLY = 3,
+ VICTORY_POINT = 4,
+};
+
+enum msg_type : uint8_t {
+ MSG_BEGIN = 0, // gives game setup info
+ MSG_SETUP = 1, // gives initial board and agent states
+ MSG_KICK = 2, // removes a misbehaving agent from the game
+ MSG_END = 3, // gives the winner of the game
+
+ MSG_TURN = 10, // marks beginning of given agent's turn
+ MSG_ROLL = 11, // request a roll
+ MSG_ROLL_RESULT = 12, // public result of rolling a dice
+ MSG_PLAY = 13, // plays a development card
+ MSG_BUILD = 14, // builds a structure or development card
+ MSG_BUILD_RESULT = 15, // private result of building a structure or devcard
+ MSG_TRADE = 16, // requests a trade with a given agent
+ MSG_TRADE_ACCEPT = 17, // accepts a pending trade offer
+ MSG_TRADE_REJECT = 18, // rejects a pending trade offer
+ MSG_PASS = 19, // marks end of agent's turn
+
+ MSG_ROB = 20, // moves the robber to a new tile, and robs an agent
+ MSG_ROB_RESULT = 21, // public result of robbing a given agent
+};
+
+struct msg_begin_tag {
+ uint32_t timeout_secs;
+ uint32_t thread_limit;
+ uint32_t memory_limit; // in MiB
+ uint8_t board_radius; // excluding central tile
+ uint8_t player_id;
+
+ uint32_t cell_count;
+ uint32_t harbour_count;
+ uint32_t player_count;
+};
+
+struct cell_info {
+ struct tilepos pos;
+ struct tile tile;
+};
+
+struct harbour_info {
+ struct vertpos vert1;
+ struct vertpos vert2;
+ enum resource resource;
+ uint8_t cost;
+};
+
+struct player_info {
+ uint8_t id;
+ struct vertpos settlement1;
+ struct edgepos road1;
+ struct vertpos settlement2;
+ struct edgepos road2;
+ enum resource starting_hand[3];
+};
+
+struct msg_setup_tag {
+ // NOTE: these counts are expected to be provided before receiving MSG_SETUP
+ uint32_t cell_count;
+ uint32_t harbour_count;
+ uint32_t player_count;
+
+ // NOTE: these points are expected to be initialised before receiveing MSG_SETUP
+ struct cell_info *cells;
+ struct harbour_info *harbours;
+ struct player_info *players;
+};
+
+struct msg_kick_tag {
+ uint8_t kick_id;
+};
+
+struct msg_end_tag {
+ uint8_t winner_id;
+};
+
+struct msg_turn_tag {
+ uint8_t player_id;
+};
+
+struct msg_roll_tag {};
+
+struct msg_roll_result_tag {
+ uint8_t roll;
+};
+
+struct msg_play_tag {
+ uint8_t player_id;
+ enum devcard card;
+};
+
+struct msg_build_tag {
+ uint8_t player_id;
+ enum buildable type;
+ union {
+ struct edgepos edgepos; // if type == ROAD
+ struct vertpos vertpos; // if type == SETTLEMENT || type == CITY
+ };
+};
+
+struct msg_build_result_tag {
+ enum buildable type;
+ union {
+ enum devcard card; // if type == DEVELOPMENT_CARD
+ };
+};
+
+struct msg_trade_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+ struct trade_offer offer;
+ struct trade_offer ask;
+};
+
+struct msg_trade_accept_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+};
+
+struct msg_trade_reject_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+};
+
+struct msg_pass_tag {};
+
+struct msg_rob_tag {
+ struct tilepos tilepos;
+ uint8_t target_id;
+};
+
+struct msg_rob_result_tag {
+ uint8_t player_id;
+ uint8_t target_id;
+ enum resource resource;
+};
+
+union msg_tag {
+ struct msg_begin_tag begin;
+ struct msg_setup_tag setup;
+ struct msg_kick_tag kick;
+ struct msg_end_tag end;
+
+ struct msg_turn_tag turn;
+ struct msg_roll_tag roll;
+ struct msg_roll_result_tag roll_result;
+ struct msg_play_tag play;
+ struct msg_build_tag build;
+ struct msg_build_result_tag build_result;
+ struct msg_trade_tag trade;
+ struct msg_trade_accept_tag trade_accept;
+ struct msg_trade_reject_tag trade_reject;
+ struct msg_pass_tag pass;
+
+ struct msg_rob_tag rob;
+ struct msg_rob_result_tag rob_result;
+};
+
+struct msg {
+ enum msg_type type;
+ union msg_tag tag;
+};
+
+extern bool
+try_recv_msg(int sockfd, struct msg *out);
+
+extern bool
+try_send_msg(int sockfd, struct msg const *msg);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* CATAN_H */
diff --git a/clean.sh b/clean.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+rm -rf bin obj
diff --git a/rules.pdf b/rules.pdf
Binary files differ.
diff --git a/server/server.c b/server/server.c
@@ -0,0 +1,288 @@
+#include "server.h"
+
+struct opts opts = {
+ .verbose = false,
+ .game_seed = 0,
+ .agent_threads = 4,
+ .agent_mem_mib = 1024,
+ .agent_timeout = 300,
+ .agents = {0},
+};
+
+static char const *optstr = "hvg:t:m:s:";
+
+static void
+usage(char **argv)
+{
+ fprintf(stderr, "Usage: %s [-hv] [-g <game-seed>] [-t <agent-thread-limit] [-m <agent-mem-limit-mib>] [-s <agent-timeout>] <uid:agent>...\n", argv[0]);
+ fprintf(stderr, "\t-h :\n\t\tdisplay this help message\n");
+ fprintf(stderr, "\t-v :\n\t\tenable verbose logging\n");
+ fprintf(stderr, "\t-g :\n\t\tset the seed for the generated game board\n");
+ fprintf(stderr, "\t-t :\n\t\tset the per-agent thread limit (default: 4)\n");
+ fprintf(stderr, "\t-m :\n\t\tset the per-agent memory limit in MiB (default: 1024 MiB)\n");
+ fprintf(stderr, "\t-s :\n\t\tset the per-agent timeout (default 300 secs)\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\tagent :\n\t\tadds the given agent, with the given uid (between %d and %d times)\n", GAME_AGENTS_MIN, GAME_AGENTS_MAX);
+ fprintf(stderr, "\n");
+ fprintf(stderr, "\tNOTE: agent programs must expect to receive the following commandline arguments:\n");
+ fprintf(stderr, "\t\t1. host :\n\t\t\tsocket host to connect to\n");
+ fprintf(stderr, "\t\t2. port :\n\t\t\tsocket port to connect to\n");
+}
+
+#include <getopt.h>
+
+static bool
+parse_args(int argc, char **argv)
+{
+ int opt;
+ while ((opt = getopt(argc, argv, optstr)) != -1) {
+ switch (opt) {
+ case 'v':
+ opts.verbose = true;
+ break;
+
+ case 'g':
+ if (!(opts.game_seed = strtoull(optarg, NULL, 10)))
+ return false;
+ break;
+
+ case 't':
+ if (!(opts.agent_threads = strtoull(optarg, NULL, 10)))
+ return false;
+ break;
+
+ case 'm':
+ if (!(opts.agent_mem_mib = strtoull(optarg, NULL, 10)))
+ return false;
+ break;
+
+ case 's':
+ if (!(opts.agent_timeout = strtoull(optarg, NULL, 10)))
+ return false;
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ opts.agents.ptr = argv + optind;
+ opts.agents.len = argc - optind;
+
+ if (opts.agents.len < GAME_AGENTS_MIN || opts.agents.len > GAME_AGENTS_MAX)
+ return false;
+
+ return true;
+}
+
+int
+main(int argc, char **argv)
+{
+ if (!parse_args(argc, argv)) {
+ usage(argv);
+ exit(EXIT_FAILURE);
+ };
+
+ fprintf(stderr, "====== Args ======\n");
+ fprintf(stderr, " verbose: %d\n", opts.verbose);
+ fprintf(stderr, " game seed: %" PRIu32 "\n", opts.game_seed);
+ fprintf(stderr, " agent threads: %" PRIu32 "\n", opts.agent_threads);
+ fprintf(stderr, " agent mem MiB: %" PRIu32 "\n", opts.agent_mem_mib);
+ fprintf(stderr, " agent timeout: %" PRIu32 " secs\n", opts.agent_timeout);
+ fprintf(stderr, " agents: %" PRIuMAX "\n", opts.agents.len);
+ for (size_t i = 0; i < opts.agents.len; i++)
+ fprintf(stderr, "\tagent %zu: %s\n", i, opts.agents.ptr[i]);
+
+ struct server_state server;
+ if (!server_init(&server)) {
+ fprintf(stderr, "Failed to initialise server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ struct agent_state agents[opts.agents.len];
+ for (size_t i = 0; i < opts.agents.len; i++) {
+ char *agent = opts.agents.ptr[i];
+
+ char *saveptr;
+ char *raw_uid = strtok_r(agent, ":", &saveptr);
+ char *cmd = strtok_r(NULL, "\n", &saveptr);
+
+ if (!cmd) {
+ fprintf(stderr, "Bad agent string: %s\n", agent);
+ exit(EXIT_FAILURE);
+ }
+
+ uid_t uid = strtoul(raw_uid, NULL, 10);
+ if (!uid) {
+ fprintf(stderr, "Bad uid given: %s\n", raw_uid);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!server_start_agent(&server, &agents[i], i, cmd, uid)) {
+ fprintf(stderr, "Failed to start agent %zu, '%s' as uid %d\n", i, cmd, uid);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ server_run(&server, agents, ARRLEN(agents));
+
+ for (size_t i = 0; i < opts.agents.len; i++) {
+ server_wait_agent(&agents[i]);
+ }
+
+ exit(EXIT_SUCCESS);
+}
+
+bool
+server_init(struct server_state *server)
+{
+ struct addrinfo hints = {
+ .ai_flags = AI_PASSIVE | AI_NUMERICSERV,
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo(NULL, "0", &hints, &addrinfo))) {
+ fprintf(stderr, "Failed to get address info for server socket: %s\n", gai_strerror(res));
+ return false;
+ }
+
+ int fd;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ fd = socket(ptr->ai_family, ptr->ai_socktype | SOCK_CLOEXEC, ptr->ai_protocol);
+ if (fd == -1)
+ continue;
+
+ if (bind(fd, ptr->ai_addr, ptr->ai_addrlen) == -1) {
+ perror("bind");
+ close(fd);
+ continue;
+ }
+
+ if (listen(fd, GAME_AGENTS_MAX) == -1) {
+ perror("listen");
+ close(fd);
+ continue;
+ }
+
+ break;
+ }
+
+ if (ptr) {
+ server->sockfd = fd;
+
+ struct sockaddr_storage addr;
+ socklen_t addrlen = sizeof addr;
+ getsockname(fd, (struct sockaddr *) &addr, &addrlen);
+
+ getnameinfo((struct sockaddr *) &addr, addrlen,
+ server->addr, ARRLEN(server->addr),
+ server->port, ARRLEN(server->port),
+ AI_NUMERICHOST | AI_NUMERICSERV);
+
+ fprintf(stderr, "Hosting server on %s:%s\n", server->addr, server->port);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) {
+ fprintf(stderr, "Failed to bind server socket\n");
+ return false;
+ }
+
+ // TODO: initialise board
+
+ return true;
+}
+
+bool
+server_start_agent(struct server_state *server, struct agent_state *agent,
+ uint8_t id, char *cmd, uid_t uid)
+{
+ pid_t pid = fork();
+ if (pid == -1) {
+ perror("fork");
+ return false;
+ } else if (pid == 0) /* child */ {
+ struct rlimit nproc_rlimit = {
+ .rlim_cur = opts.agent_threads,
+ .rlim_max = opts.agent_threads,
+ };
+
+ prlimit(getpid(), RLIMIT_NPROC, &nproc_rlimit, NULL);
+
+ struct rlimit data_rlimit = {
+ .rlim_cur = opts.agent_mem_mib * MiB,
+ .rlim_max = opts.agent_mem_mib * MiB,
+ };
+
+ prlimit(getpid(), RLIMIT_DATA, &data_rlimit, NULL);
+
+ setuid(uid);
+
+ char *args[] = {
+ cmd,
+ server->addr,
+ server->port,
+ NULL,
+ };
+
+ char *envp[] = { NULL, };
+
+ execvpe(cmd, args, envp);
+
+ perror("execv");
+ exit(EXIT_FAILURE);
+ }
+
+ /* parent */
+ agent->id = id;
+ agent->pid = pid;
+
+ struct pollfd acceptfd = {
+ .fd = server->sockfd,
+ .events = POLLIN,
+ };
+
+ int res = poll(&acceptfd, 1, AGENT_ACCEPT_TIMEOUT_MS);
+ if (res <= 0) {
+ fprintf(stderr, "Failed to accept agent: %s, timeout\n", cmd);
+ return false;
+ }
+
+ agent->sockfd = accept4(server->sockfd, NULL, NULL, SOCK_CLOEXEC);
+ if (agent->sockfd < 0) {
+ fprintf(stderr, "Failed to accept agent: %s\n", cmd);
+ return false;
+ }
+
+ return true;
+}
+
+void
+server_wait_agent(struct agent_state *agent)
+{
+ kill(agent->pid, SIGKILL);
+
+ int stats;
+ if (waitpid(agent->pid, &stats, WNOHANG) == -1) {
+ kill(agent->pid, SIGTERM);
+
+ waitpid(agent->pid, &stats, 0);
+ }
+}
+
+void
+server_run(struct server_state *server, struct agent_state *agents, size_t len)
+{
+ (void) server;
+ (void) agents;
+ (void) len;
+
+ // TODO: send begin message
+ // TODO: send setup message
+ // TODO: start turns
+ // TODO: play until an agent wins
+ // TODO: send end message
+}
diff --git a/server/server.h b/server/server.h
@@ -0,0 +1,78 @@
+#ifndef SERVER_H
+#define SERVER_H
+
+#define _XOPEN_SOURCE 700
+#define _GNU_SOURCE
+#define _DEFAULT_SOURCE
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "catan.h"
+
+#include <signal.h>
+
+#include <sys/wait.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#include <poll.h>
+
+#define KiB 1024ULL
+#define MiB (1024 * KiB)
+#define GiB (1024 * MiB)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define GAME_BOARD_SIZE 3
+
+#define GAME_AGENTS_MIN 2
+#define GAME_AGENTS_MAX 4
+
+#define AGENT_ACCEPT_TIMEOUT_MS 1000
+
+struct opts {
+ bool verbose;
+ uint32_t game_seed;
+ uint32_t agent_threads;
+ uint32_t agent_mem_mib;
+ uint32_t agent_timeout;
+
+ struct {
+ char **ptr;
+ size_t len;
+ } agents;
+};
+
+struct server_state {
+ int sockfd;
+
+ char addr[NI_MAXHOST], port[NI_MAXSERV];
+};
+
+struct agent_state {
+ uint8_t id;
+ pid_t pid;
+ int sockfd;
+
+};
+
+bool
+server_init(struct server_state *server);
+
+bool
+server_start_agent(struct server_state *server, struct agent_state *agent,
+ uint8_t id, char *cmd, uid_t uid);
+
+void
+server_wait_agent(struct agent_state *agent);
+
+void
+server_run(struct server_state *server, struct agent_state *agents, size_t len);
+
+#endif /* SERVER_H */