catan

catan.git
git clone git://git.lenczewski.org/catan.git
Log | Files | Refs

commit 8a92974912bed41810cb87ca9d5b5648f2f6f06a
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Fri,  3 May 2024 18:21:43 +0000

Initial commit

Diffstat:
A.editorconfig | 16++++++++++++++++
APROTO.md | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagent/agent.c | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagent/agent.h | 32++++++++++++++++++++++++++++++++
Abuild.sh | 21+++++++++++++++++++++
Acatan/.catan.h.swp | 0
Acatan/catan.c | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acatan/catan.h | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclean.sh | 5+++++
Arules.pdf | 0
Aserver/server.c | 288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserver/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 */