commit 259e55f5690be7df58b9d7ff3f830488b82b53a6
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Sun, 13 Aug 2023 23:27:34 +0000
Initial commit
Diffstat:
36 files changed, 3585 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,34 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+# visual studio editorconfig plugin specific settings
+guidelines = 80, 120, 160
+
+[*.{c,cpp,h,hpp,tpp}]
+indent_style = tab
+indent_size = 8
+
+[*.sh]
+indent_style = tab
+indent_size = 8
+
+[*.py]
+indent_style = space
+indent_size = 4
+
+[*.java]
+indent_style = tab
+indent_size = 8
+
+[*.{css,html,js,jsx,ts,tsx}]
+indent_style = space
+indent_size = 2
+
+[*.{conf,json,md,txt}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+hex-server
+hex-server.tar
+output.txt
+obj/
+
+**/.*.swp
diff --git a/Makefile b/Makefile
@@ -0,0 +1,91 @@
+.PHONY: all build clean cleanall dist extra install uninstall
+
+PREFIX ?= /usr/local
+
+CC ?= cc
+AR ?= ar
+RANLIB ?= ranlib
+TAR ?= tar
+
+SRC := src
+INC := include
+OBJ := obj
+
+WARN := -Wall -Wextra -Wpedantic -Werror
+
+CFLAGS := -std=c11 $(WARN) -Og -g
+CPPFLAGS := -I$(INC)
+LDFLAGS :=
+
+HEX_SERVER_TARGET := hex-server
+
+HEX_SERVER_SOURCES := $(SRC)/hex.c \
+ $(SRC)/server.c \
+ $(SRC)/board.c \
+ $(SRC)/proto.c \
+ $(SRC)/utils.c
+
+HEX_SERVER_OBJECTS := $(HEX_SERVER_SOURCES:$(SRC)/%.c=$(OBJ)/%.o)
+HEX_SERVER_OBJDEPS := $(HEX_SERVER_OBJECTS:%.o=%.d)
+HEX_SERVER_FLAGS :=
+
+HEX_AGENT_SOURCES := $(wildcard agents/*)
+
+ARCHIVE_TARGET := hex-server.tar
+
+ARCHIVE_SOURCES := .editorconfig \
+ .gitignore \
+ Makefile \
+ README.md \
+ schedule.txt \
+ tournament-host.py \
+ agents/ \
+ include/ \
+ src/
+
+HEX_AGENT_USERS := $(shell seq 1 16)
+
+all: build extra
+
+build: $(HEX_SERVER_TARGET)
+
+clean:
+ rm -rf $(HEX_SERVER_TARGET) $(OBJ)
+
+cleanall: clean | $(HEX_AGENT_SOURCES)
+ @for d in $(HEX_AGENT_SOURCES); do make -C $$d clean; done
+
+dist: cleanall
+ $(TAR) cf $(ARCHIVE_TARGET) $(ARCHIVE_SOURCES)
+
+extra: | $(HEX_AGENT_SOURCES)
+ @for d in $(HEX_AGENT_SOURCES); do make -C $$d; done
+
+install: build
+ for i in $(HEX_AGENT_USERS); do \
+ if [ ! $$(id hex-agent-$$i -u 2>/dev/null) ]; then \
+ useradd -M -N -e '' hex-agent-$$i; \
+ fi; \
+ done
+ mkdir -p $(DESTDIR)$(PREFIX)/bin
+ install -m 0755 $(HEX_SERVER_TARGET) $(DESTDIR)$(PREFIX)/bin/$(HEX_SERVER_TARGET)
+
+uninstall:
+ for i in $(HEX_AGENT_USERS); do \
+ if [ $$(id hex-agent-$$i -u 2>/dev/null) ]; then \
+ userdel hex-agent-$$i; \
+ fi; \
+ done
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(HEX_SERVER_TARGET)
+
+$(HEX_SERVER_TARGET): $(HEX_SERVER_OBJECTS)
+ $(CC) -o $@ $^ $(LDFLAGS) $(HEX_SERVER_FLAGS)
+
+-include $(HEX_SERVER_OBJDEPS)
+
+$(OBJ)/%.o: $(SRC)/%.c | $(OBJ)
+ @mkdir -p $(dir $@)
+ $(CC) -MMD -o $@ -c $< $(CFLAGS) $(CPPFLAGS)
+
+$(OBJ):
+ mkdir $@
diff --git a/README.md b/README.md
@@ -0,0 +1,205 @@
+hex-server: A Simple 'Hex' Server
+==============================================================================
+A simple 'Hex' game server, for running 1 round of a tournament between 2
+player agents. Supports the PIE rule. Includes example agents written in C,
+C++, Java, and Python3.
+
+To run tournaments, there exists a helper script written for Python3.11 (see
+`tournament-host.py`), which takes a comma-delimited agent-pair tournament
+schedule, runs the tournament, and collects the resulting statistics in the
+given output file.
+
+NOTE: currently, the Java agent (written for Java 17) requires ~16 threads to
+load a jar and execute, thanks to the JVM requirements. since the default
+thread limit is 4, this agent cannot be used without increasing the limit to
+16
+
+hex-server: Building
+------------------------------------------------------------------------------
+To build the server, run the following shell commands in the project's root
+directory:
+```sh
+$ make build # default, builds only the hex server
+$ make extra # optional, builds all included agents
+$ make all # optional, builds the hex server and all included agents
+```
+
+To clean all built artefacts, run the following shell commands:
+```sh
+$ make clean # cleans only the hex server binary
+$ make cleanall # cleans the hex server binary and all agent artefacts
+```
+
+hex-server: Usage
+------------------------------------------------------------------------------
+The server can be invoked using the following shell command:
+```sh
+$ sudo hex-server -a <agent-1> -ua <uid> -b <agent-2> -ub <uid> \
+ [-d 11] [-s 300] [-t 4] [-m 1024] [-v]
+```
+
+NOTE: The server MUST be ran as root (i.e. as a privileged process), or by a
+user with the Linux CAP_SETUID capability. This is due to the use of setuid()
+to set the user id for the agents, and thus maintain process limits which work
+based on the (effective) user id of a given process. Note that despite the
+server running as root, the user agents run as the given users (via -ua/-ub).
+
+Server Options:
++-----+-----------------------------------------------+-----------+-----------+
+| Opt | Description | Optional | Default |
++-----+-----------------------------------------------+-----------+-----------+
+| -a | The first agent (black) | Required | N/A |
+| -ua | The uid to set for the first agent (black) | Required | N/A |
+| -b | The second agent (white) | Required | N/A |
+| -ub | The uid to set for the second agent (white) | Required | N/A |
+| -d | Board dimensions | Optional | 11 |
+| -s | Per-Agent game timer (seconds) | Optional | 300 |
+| -t | Per-Agent thread hard-limit | Optional | 4 |
+| -m | Per-Agent memory hard-limit (MiB) | Optional | 1024 |
+| -v | Verbose output | Optional | N/A |
++-----+-----------------------------------------------+-----------+-----------+
+
+Each agent will be invoked using the following shell command:
+```sh
+<agent-string> <server-host> <server-port>
+```
+
+For maximum flexibility and ease of use, it is recommended to write a wrapper
+shell script to be passed as the `agent-string`, to allow for a more
+specialised run command structure (e.g. using a compiled agent with
+differently named options, or having an interpreted agent and having to pass
+the agent source to the interpreter).
+
+An example of such a wrapper script is as follows:
+```sh
+#!/bin/sh
+
+exec /usr/bin/env python3 $(dirname $0)/my_agent.py $@ # forward all args
+```
+
+Writing such a wrapper shell script also means that agent-specific commandline
+options (e.g. a verbose logging mode, an optimised "release" mode or
+unoptimised "debug" mode, or passing the server host and port via named
+commandline arguments instead of as positional ones) can be implemented,
+without any special handling from the server.
+
+For examples of both compiled and interpreted agents, as well as for an
+example of the wrapper scripts used to invoke the agents, please see the
+`run.sh` wrapper scripts in the example agent directories (under `agents/`).
+
+NOTE: the wrapper script, if used, must be made executable. This can be done
+using the following shell command (replacing `/tmp/my_agent/` with your
+specific agent's directory):
+```sh
+$ chmod +x /tmp/my_agent/my_wrapper_script.sh
+```
+
+hex-server: Protocol
+------------------------------------------------------------------------------
+The server uses a simple binary protocol for all messages between the server
+and individual agents, and will communicate between itself and an agent using
+a socket.
+
+Server Flow
+1) Create processes for both agents (setting process limits)
+2) accept() both agents (within a timeout)
+3) send() a MSG_START to both agents
+4) recv() a MSG_MOVE (or MSG_SWAP on round 1 as white only)
+5) Make said move and test the board for a winner
+ a) If there is a winner, goto 7)
+ b) Otherwise, goto 4)
+6) send() the received message to the other agent, goto 4)
+7) send() a MSG_END to both agents
+
+NOTE: if at any point in this flow an agent sends a malformed message, plays
+an invalid move (e.g. attempts to move out-of-bounds, swaps except as player 2
+on the first turn, moves onto a spot with an existing piece), or causes the
+socket connection to close, the game is over and the other agent wins by
+default.
+
+Agent Flow
+1) connect() to the server given by the commandline args (host/port)
+2) recv() a MSG_START from the server
+ a) If playing as black (agent 1), goto 3)
+ b) If playing as white (agent 2), goto 4)
+3) send() a MSG_MOVE (or MSG_SWAP on round 1 as white only)
+4) recv() a MSG_MOVE, MSG_SWAP, or MSG_END
+ a) If received MSG_MOVE or MSG_SWAP, update internal state, goto 3)
+ b) If received MSG_END, goto 5)
+5) close() connection to server
+
+hex-server: Protocol Wire Format
+------------------------------------------------------------------------------
+The wire format consists of a fixed 32-byte packet, with a simple
+(type:u32,params:u32[]) packet structure.
+
+The wire format if oriented around 32-bit unsigned words for simplicity, and
+values for the packet type and all parameters, will all be of this type.
+
+NOTE: for implementations, this boils down into a single recv() or send() of
+32 bytes, followed by parsing the received packet based on the `type`,
+extracting all the required parameters. Agents should also make sure to follow
+this exact packet structure when sending messages, as otherwise they will be
+seen by the server as having sent malformed messages and the server will
+consider this a forfeit. Please see the example agents under `agents/` for
+specific example implementations.
+
+hex-server: Protocol Messages
+------------------------------------------------------------------------------
+Below is a listing of the messages in the protocol, their IDs and parameters,
+and the relationships between the server and 2 agents.
+
+Protocol Messages:
++-------+-----------+---------------------------------------------------------+
+| ID | Name | Params |
++-------+-----------+---------------------------------------------------------+
+| 0 | MSG_START | player:u32, board_size:u32, game_secs:u32 |
+| | | thread_limit:u32, mem_limit_mib:u32 |
++-------+-----------+---------------------------------------------------------+
+| 1 | MSG_MOVE | board_x:u32, board_y:u32 |
++-------+-----------+---------------------------------------------------------+
+| 2 | MSG_SWAP | N/A |
++-------+-----------+---------------------------------------------------------+
+| 3 | MSG_END | winner:u32 |
++-------+-----------+---------------------------------------------------------+
+
+An example of this protocol defined in a C-like language is as follows:
+```c
+enum player_type : u32 {
+ PLAYER_BLACK = 0,
+ PLAYER_WHITE = 1,
+};
+
+enum msg_type : u32 {
+ MSG_START = 0,
+ MSG_MOVE = 1,
+ MSG_SWAP = 2,
+ MSG_END = 3,
+};
+
+union msg_data {
+ struct {
+ enum player_type player;
+ u32 board_size;
+ u32 game_secs;
+ u32 thread_limit;
+ u32 mem_limit_mib; // NOTE: in units of MiB
+ } start;
+
+ struct {
+ u32 board_x;
+ u32 board_y;
+ } move;
+
+/* struct { } swap; */ // NOTE: swap has no parameters
+
+ struct {
+ enum player_type winner;
+ } end;
+};
+
+struct msg {
+ enum msg_type type;
+ union msg_data data;
+};
+```
diff --git a/agents/example_c_agent/.gitignore b/agents/example_c_agent/.gitignore
@@ -0,0 +1,2 @@
+agent
+obj/
diff --git a/agents/example_c_agent/Makefile b/agents/example_c_agent/Makefile
@@ -0,0 +1,38 @@
+.PHONY: all build clean
+
+CC ?= cc
+
+SRC := src
+INC := include
+DEPINC := ../../include
+OBJ := obj
+
+WARN := -Wall -Wextra -Wpedantic -Werror
+
+CFLAGS := -std=c11 $(WARN) -Og -g
+CPPFLAGS := -I$(INC) -I$(DEPINC)
+LDFLAGS :=
+
+TARGET := agent
+SOURCES := $(SRC)/agent.c
+OBJECTS := $(SOURCES:$(SRC)/%.c=$(OBJ)/%.o)
+OBJDEPS := $(OBJECTS:%.o=%.d)
+
+all: build
+
+build: $(TARGET)
+
+clean:
+ rm -rf $(TARGET) $(OBJ)
+
+$(TARGET): $(OBJECTS)
+ $(CC) -o $@ $^ $(LDFLAGS)
+
+-include $(OBJDEPS)
+
+$(OBJ)/%.o: $(SRC)/%.c | $(OBJ)
+ @mkdir -p $(dir $@)
+ $(CC) -MMD -o $@ -c $< $(CFLAGS) $(CPPFLAGS)
+
+$(OBJ):
+ mkdir -p $@
diff --git a/agents/example_c_agent/include/agent.h b/agents/example_c_agent/include/agent.h
@@ -0,0 +1,27 @@
+#ifndef AGENT_H
+#define AGENT_H
+
+#ifdef _XOPEN_SOURCE
+#undef _XOPEN_SOURCE
+#endif
+
+#define _XOPEN_SOURCE 700
+
+#include "hex/types.h"
+#include "hex/proto.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#endif /* AGENT_H */
diff --git a/agents/example_c_agent/run.sh b/agents/example_c_agent/run.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec $(dirname $0)/agent $@
diff --git a/agents/example_c_agent/src/agent.c b/agents/example_c_agent/src/agent.c
@@ -0,0 +1,444 @@
+#include "agent.h"
+
+int
+net_init(char *restrict host, char *restrict port);
+
+bool
+net_recv_msg(int sock, struct hex_msg *out, enum hex_msg_type expected[], size_t len);
+
+bool
+net_send_msg(int sock, struct hex_msg *msg);
+
+enum board_cell {
+ CELL_BLACK = HEX_PLAYER_BLACK,
+ CELL_WHITE = HEX_PLAYER_WHITE,
+ CELL_EMPTY,
+};
+
+struct move {
+ u32 x, y;
+};
+
+void
+move_swap(struct move *restrict lhs, struct move *restrict rhs);
+
+struct board {
+ u32 size;
+
+ enum board_cell *cells;
+
+ size_t moves_len;
+ struct move *moves;
+};
+
+bool
+board_init(struct board *self, u32 size);
+
+bool
+board_play(struct board *self, enum hex_player player, u32 x, u32 y);
+
+void
+board_swap(struct board *self);
+
+bool
+board_next(struct board *self, u32 *out_x, u32 *out_y);
+
+enum game_state {
+ GAME_START,
+ GAME_RECV,
+ GAME_SEND,
+ GAME_END,
+};
+
+int
+main(int argc, char **argv)
+{
+ srandom(getpid());
+
+ if (argc < 3) {
+ fprintf(stderr, "Not enough args: %s <host> <port>\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ char *host = argv[1], *port = argv[2];
+
+ int sockfd = net_init(host, port);
+ if (sockfd == -1) {
+ fprintf(stderr, "Failed to initialise network\n");
+ exit(EXIT_FAILURE);
+ }
+
+ enum game_state game_state = GAME_START;
+ struct board board;
+
+ enum hex_player player, opponent, winner;
+
+ u32 game_secs, thread_limit, mem_limit_mib; // currently unused
+ (void) game_secs; (void) thread_limit; (void) mem_limit_mib;
+
+ bool game_over = false, first_round = true;
+ while (!game_over) {
+ switch (game_state) {
+ case GAME_START: {
+ enum hex_msg_type expected_msg_types[] = {
+ HEX_MSG_START,
+ };
+
+ struct hex_msg msg;
+ if (!net_recv_msg(sockfd, &msg, expected_msg_types, ARRLEN(expected_msg_types))) {
+ fprintf(stderr, "Failed to receive message from hex server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ // unpack all parameters
+ player = msg.data.start.player;
+ game_secs = msg.data.start.game_secs;
+ thread_limit = msg.data.start.thread_limit;
+ mem_limit_mib = msg.data.start.mem_limit_mib;
+
+ u32 board_size = msg.data.start.board_size;
+
+ if (!board_init(&board, board_size)) {
+ fprintf(stderr, "Failed to allocate game board of size %" PRIu32 "x%" PRIu32 "\n",
+ board_size, board_size);
+ exit(EXIT_FAILURE);
+ }
+
+ switch (player) {
+ case HEX_PLAYER_BLACK:
+ opponent = HEX_PLAYER_WHITE;
+ game_state = GAME_SEND;
+ break;
+
+ case HEX_PLAYER_WHITE:
+ opponent = HEX_PLAYER_BLACK;
+ game_state = GAME_RECV;
+ break;
+ }
+
+ printf("[%s] Starting game: %" PRIu32 "x%" PRIu32 ", %" PRIu32 " secs\n",
+ hexplayerstr(player), board_size, board_size, game_secs);
+ } break;
+
+ case GAME_RECV: {
+ enum hex_msg_type expected_msg_types[] = {
+ HEX_MSG_MOVE,
+ HEX_MSG_SWAP,
+ HEX_MSG_END,
+ };
+
+ struct hex_msg msg;
+ if (!net_recv_msg(sockfd, &msg, expected_msg_types, ARRLEN(expected_msg_types))) {
+ fprintf(stderr, "Failed to receive message from hex server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ switch (msg.type) {
+ case HEX_MSG_MOVE:
+ board_play(&board, opponent, msg.data.move.board_x, msg.data.move.board_y);
+
+ if (first_round && random() % 2) {
+ board_swap(&board);
+
+ msg.type = HEX_MSG_SWAP;
+ if (!net_send_msg(sockfd, &msg)) {
+ fprintf(stderr, "Failed to send swap message to hex server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ game_state = GAME_RECV;
+ } else {
+ game_state = GAME_SEND;
+ }
+ break;
+
+ case HEX_MSG_SWAP:
+ board_swap(&board);
+ game_state = GAME_SEND;
+ break;
+
+ case HEX_MSG_END:
+ winner = msg.data.end.winner;
+ game_state = GAME_END;
+ break;
+ }
+
+ first_round = false;
+ } break;
+
+ case GAME_SEND: {
+ struct hex_msg msg = {
+ .type = HEX_MSG_MOVE,
+ };
+
+ if (!board_next(&board, &msg.data.move.board_x, &msg.data.move.board_y)) {
+ fprintf(stderr, "Failed to generate next board move\n");
+ exit(EXIT_FAILURE);
+ }
+
+ board_play(&board, player, msg.data.move.board_x, msg.data.move.board_y);
+
+ if (!net_send_msg(sockfd, &msg)) {
+ fprintf(stderr, "Failed to send message to hex server\n");
+ exit(EXIT_FAILURE);
+ }
+
+ game_state = GAME_RECV;
+ first_round = false;
+ } break;
+
+ case GAME_END: {
+ printf("[%s] Player %s has won the game\n",
+ hexplayerstr(player), hexplayerstr(winner));
+ game_over = true;
+ } break;
+
+ default:
+ fprintf(stderr, "Unknown game state: %d\n", game_state);
+ exit(EXIT_FAILURE);
+ break;
+ }
+ }
+
+ exit(EXIT_SUCCESS);
+}
+
+int
+net_init(char *restrict host, char *restrict port)
+{
+ assert(host);
+ assert(port);
+
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo(host, port, &hints, &addrinfo))) {
+ fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(res));
+ return -1;
+ }
+
+ int sockfd;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
+ if (sockfd == -1) continue;
+ if (connect(sockfd, ptr->ai_addr, ptr->ai_addrlen) != -1) break;
+ close(sockfd);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) {
+ fprintf(stderr, "Failed to connect to %s:%s\n", host, port);
+ return -1;
+ }
+
+ return sockfd;
+}
+
+static inline size_t
+net_recv_all(int sock, u8 *buf, size_t len)
+{
+ assert(buf);
+
+ size_t nbytes_received = 0;
+
+ do {
+ ssize_t res = recv(sock, buf + nbytes_received, len - nbytes_received, 0);
+ if (res <= 0) break; // error or socket shutdown
+ nbytes_received += res;
+ } while (nbytes_received < len);
+
+ return nbytes_received;
+}
+
+bool
+net_recv_msg(int sock, struct hex_msg *out, enum hex_msg_type expected[], size_t len)
+{
+ assert(out);
+ assert(expected);
+
+ u8 buf[HEX_MSG_SZ];
+ if (!(net_recv_all(sock, buf, HEX_MSG_SZ) == HEX_MSG_SZ)) return false;
+
+ struct hex_msg msg;
+ if (!hex_msg_try_deserialise(buf, &msg)) return false;
+
+ for (size_t i = 0; i < len; i++) {
+ if (msg.type == expected[i]) {
+ *out = msg;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static inline size_t
+net_send_all(int sock, u8 *buf, size_t len)
+{
+ assert(buf);
+
+ size_t nbytes_sent = 0;
+
+ do {
+ ssize_t res = send(sock, buf + nbytes_sent, len - nbytes_sent, 0);
+ if (res <= 0) break; // error or socket shutdown
+ nbytes_sent += res;
+ } while (nbytes_sent < len);
+
+ return nbytes_sent;
+}
+
+bool
+net_send_msg(int sock, struct hex_msg *msg)
+{
+ assert(msg);
+
+ u8 buf[HEX_MSG_SZ];
+ if (!hex_msg_try_serialise(msg, buf)) return false;
+
+ return net_send_all(sock, buf, HEX_MSG_SZ) == HEX_MSG_SZ;
+}
+
+void
+move_swap(struct move *restrict lhs, struct move *restrict rhs)
+{
+ assert(lhs);
+ assert(rhs);
+
+ struct move tmp = *lhs;
+ *lhs = *rhs;
+ *rhs = tmp;
+}
+
+static void
+shuffle_moves(struct move *arr, size_t len)
+{
+ for (size_t i = 0; i < len - 2; i++) {
+ size_t j = (i + random()) % len;
+ move_swap(&arr[i], &arr[j]);
+ }
+}
+
+bool
+board_init(struct board *self, u32 size)
+{
+ assert(self);
+
+ self->size = size;
+
+ if (!(self->cells = malloc(size * size * sizeof *self->cells)))
+ return false;
+
+ self->moves_len = size * size;
+ if (!(self->moves = malloc(size * size * sizeof *self->moves))) {
+ free(self->cells);
+ return false;
+ }
+
+ for (size_t j = 0; j < size; j++) {
+ for (size_t i = 0; i < size; i++) {
+ size_t idx = j * size + i;
+
+ self->cells[idx] = CELL_EMPTY;
+
+ self->moves[idx].x = i;
+ self->moves[idx].y = j;
+ }
+ }
+
+ shuffle_moves(self->moves, self->moves_len);
+
+ return true;
+}
+
+bool
+board_play(struct board *self, enum hex_player player, u32 x, u32 y)
+{
+ assert(self);
+
+ enum board_cell *cell = &self->cells[y * self->size + x];
+ if (*cell != CELL_EMPTY) return false;
+
+ switch (player) {
+ case HEX_PLAYER_BLACK:
+ *cell = CELL_BLACK;
+ break;
+
+ case HEX_PLAYER_WHITE:
+ *cell = CELL_WHITE;
+ break;
+
+ default:
+ return false;
+ }
+
+ for (size_t i = 0; i < self->moves_len; i++) {
+ if (self->moves[i].x == x && self->moves[i].y == y) {
+ move_swap(&self->moves[i], &self->moves[--self->moves_len]);
+ break;
+ }
+ }
+
+ return true;
+}
+
+void
+board_swap(struct board *self)
+{
+ assert(self);
+
+ self->moves_len = 0;
+
+ for (size_t j = 0; j < self->size; j++) {
+ for (size_t i = 0; i < self->size; i++) {
+ enum board_cell *cell = &self->cells[j * self->size + i];
+
+ switch (*cell) {
+ case CELL_BLACK:
+ *cell = CELL_WHITE;
+ break;
+
+ case CELL_WHITE:
+ *cell = CELL_BLACK;
+ break;
+
+ default: {
+ struct move *move = &self->moves[self->moves_len++];
+ move->x = i;
+ move->y = j;
+ } break;
+ }
+ }
+ }
+
+ shuffle_moves(self->moves, self->moves_len);
+}
+
+bool
+board_next(struct board *self, u32 *out_x, u32 *out_y)
+{
+ assert(self);
+ assert(out_x);
+ assert(out_y);
+
+ if (self->moves_len == 0) return false;
+
+ struct move move = self->moves[--self->moves_len];
+ *out_x = move.x;
+ *out_y = move.y;
+
+ return true;
+}
+
+extern inline b32
+hex_msg_try_serialise(struct hex_msg const *msg, u8 out[static HEX_MSG_SZ]);
+
+extern inline b32
+hex_msg_try_deserialise(u8 buf[static HEX_MSG_SZ], struct hex_msg *out);
+
+extern inline char const *
+hexplayerstr(enum hex_player val);
diff --git a/agents/example_cpp_agent/.gitignore b/agents/example_cpp_agent/.gitignore
@@ -0,0 +1,2 @@
+agent
+obj/
diff --git a/agents/example_cpp_agent/Makefile b/agents/example_cpp_agent/Makefile
@@ -0,0 +1,38 @@
+.PHONY: all build clean
+
+CCX ?= c++
+
+SRC := src
+INC := include
+DEPINC := ../../include
+OBJ := obj
+
+WARN := -Wall -Wextra -Wpedantic -Werror
+
+CFLAGS := -std=c++14 $(WARN) -Og -g
+CPPFLAGS := -I$(INC) -I$(DEPINC)
+LDFLAGS :=
+
+TARGET := agent
+SOURCES := $(SRC)/agent.cpp
+OBJECTS := $(SOURCES:$(SRC)/%.cpp=$(OBJ)/%.o)
+OBJDEPS := $(OBJECTS:%.o=%.d)
+
+all: build
+
+build: $(TARGET)
+
+clean:
+ rm -rf $(TARGET) $(OBJ)
+
+$(TARGET): $(OBJECTS)
+ $(CCX) -o $@ $^ $(LDFLAGS)
+
+-include $(OBJDEPS)
+
+$(OBJ)/%.o: $(SRC)/%.cpp | $(OBJ)
+ @mkdir -p $(dir $@)
+ $(CCX) -MMD -o $@ -c $< $(CFLAGS) $(CPPFLAGS)
+
+$(OBJ):
+ mkdir -p $@
diff --git a/agents/example_cpp_agent/include/agent.hpp b/agents/example_cpp_agent/include/agent.hpp
@@ -0,0 +1,33 @@
+#ifndef AGENT_HPP
+#define AGENT_HPP
+
+#ifdef _XOPEN_SOURCE
+#undef _XOPEN_SOURCE
+#endif
+
+#define _XOPEN_SOURCE 700
+
+#include "hex/types.h"
+#include "hex/proto.h"
+
+#include <cassert>
+#include <cerrno>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <array>
+#include <iostream>
+#include <memory>
+#include <random>
+#include <vector>
+
+#endif /* AGENT_HPP */
diff --git a/agents/example_cpp_agent/run.sh b/agents/example_cpp_agent/run.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec $(dirname $0)/agent $@
diff --git a/agents/example_cpp_agent/src/agent.cpp b/agents/example_cpp_agent/src/agent.cpp
@@ -0,0 +1,333 @@
+#include "agent.hpp"
+
+enum class Cell {
+ BLACK = HEX_PLAYER_BLACK,
+ WHITE = HEX_PLAYER_WHITE,
+ EMPTY,
+};
+
+struct Move {
+ u32 x, y;
+
+ bool operator==(const Move &rhs) {
+ return this->x == rhs.x && this->y == rhs.y;
+ }
+};
+
+void swap(Move &lhs, Move &rhs) {
+ std::swap(lhs.x, rhs.x);
+ std::swap(lhs.y, rhs.y);
+}
+
+class Board {
+ u32 size;
+ std::vector<Cell> cells;
+ std::vector<Move> moves;
+
+public:
+ template <class URBG>
+ Board(u32 size, URBG &&rng) : size(size) {
+ cells.reserve(size * size);
+ moves.reserve(size * size);
+
+ for (u32 j = 0; j < this->size; j++) {
+ for (u32 i = 0; i < this->size; i++) {
+ this->cells.push_back(Cell::EMPTY);
+
+ Move move{i, j};
+ this->moves.push_back(move);
+ }
+ }
+
+ std::shuffle(this->moves.begin(), this->moves.end(), rng);
+ }
+
+ bool play(enum hex_player player, u32 x, u32 y) {
+ Cell &cell = this->cells.at(y * this->size + x);
+ if (cell != Cell::EMPTY) return false;
+
+ switch (player) {
+ case HEX_PLAYER_BLACK:
+ cell = Cell::BLACK;
+ break;
+
+ case HEX_PLAYER_WHITE:
+ cell = Cell::WHITE;
+ break;
+ }
+
+ Move move{x, y};
+ auto it = std::find(this->moves.begin(), this->moves.end(), move);
+ if (it != std::end(this->moves)) {
+ ::swap(*it, this->moves.back());
+ this->moves.pop_back();
+ }
+
+ return true;
+ }
+
+ template <class URBG>
+ void swap(URBG &&rng) {
+ this->moves.clear();
+
+ for (u32 j = 0; j < this->size; j++) {
+ for (u32 i = 0; i < this->size; i++) {
+ Cell &cell = this->cells.at(j * this->size + i);
+
+ switch (cell) {
+ case Cell::BLACK: cell = Cell::WHITE; break;
+ case Cell::WHITE: cell = Cell::BLACK; break;
+ case Cell::EMPTY:
+ Move move{i, j};
+ this->moves.push_back(move);
+ break;
+ }
+ }
+ }
+
+ std::shuffle(this->moves.begin(), this->moves.end(), rng);
+ }
+
+ bool next(Move &out) {
+ if (this->moves.empty()) return false;
+
+ out = this->moves.back();
+ this->moves.pop_back();
+
+ return true;
+ }
+};
+
+class Net {
+ int sockfd;
+public:
+ Net() : sockfd(-1) {}
+
+ bool init(char *host, char *port) {
+ struct addrinfo hints, *addrinfo, *ptr;
+
+ memset(&hints, 0, sizeof hints);
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ int res;
+ if ((res = getaddrinfo(host, port, &hints, &addrinfo))) {
+ std::cerr << "getaddrinfo: " << gai_strerror(res) << std::endl;
+ return false;
+ }
+
+ int sockfd;
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
+ if (sockfd == -1) continue;
+ if (connect(sockfd, ptr->ai_addr, ptr->ai_addrlen) != -1) break;
+ close(sockfd);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) {
+ std::cerr << "Failed to connect to " << host << ":" << port << std::endl;
+ return false;
+ }
+
+ this->sockfd = sockfd;
+
+ return true;
+ }
+
+ bool recv_msg(struct hex_msg &out, const std::vector<enum hex_msg_type> &expected) {
+ u8 buf[HEX_MSG_SZ];
+
+ size_t nbytes_recv = 0, len = HEX_MSG_SZ;
+
+ do {
+ ssize_t curr = recv(this->sockfd, buf + nbytes_recv, len - nbytes_recv, 0);
+ if (curr <= 0) return false; // error or socket shutdown
+ nbytes_recv += curr;
+ } while (nbytes_recv < len);
+
+ struct hex_msg msg;
+ if (!hex_msg_try_deserialise(buf, &msg)) return false;
+
+ if (std::find(expected.begin(), expected.end(), msg.type) != std::end(expected)) {
+ out = msg;
+ return true;
+ }
+
+ return false;
+ }
+
+ bool send_msg(const struct hex_msg &msg) {
+ u8 buf[HEX_MSG_SZ];
+ if (!hex_msg_try_serialise(&msg, buf)) return false;
+
+ size_t nbytes_sent = 0, len = HEX_MSG_SZ;
+
+ do {
+ ssize_t curr = send(this->sockfd, buf + nbytes_sent, len - nbytes_sent, 0);
+ if (curr <= 0) return false; // error or socket shutdown
+ nbytes_sent += curr;
+ } while (nbytes_sent < len);
+
+ return true;
+ }
+};
+
+enum class State {
+ START,
+ RECV,
+ SEND,
+ END,
+};
+
+std::ostream &operator<<(std::ostream &os, const State &self) {
+ return os << static_cast<std::underlying_type<State>::type>(self);
+}
+
+int
+main(int argc, char *argv[])
+{
+ std::minstd_rand rand;
+ rand.seed(getpid());
+
+ if (argc < 3) {
+ std::cerr << "Not enough args: " << argv[0] << " <host> <port>" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ char *host = argv[1], *port = argv[2];
+
+ Net net;
+ if (!net.init(host, port)) {
+ std::cerr << "Failed to initialise network" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ State state = State::START;
+ std::unique_ptr<Board> board;
+
+ enum hex_player player, opponent, winner;
+
+ // game parameters (unused)
+ u32 game_secs, thread_limit, mem_limit_mib;
+ (void) game_secs; (void) thread_limit; (void) mem_limit_mib;
+
+ bool game_over = false, first_round = true;
+ while (!game_over) {
+ switch (state) {
+ case State::START: {
+ std::vector<enum hex_msg_type> expected_msg_types = {HEX_MSG_START};
+
+ struct hex_msg msg;
+ if (!net.recv_msg(msg, expected_msg_types)) {
+ std::cerr << "Failed to receive message from hex server" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ player = static_cast<enum hex_player>(msg.data.start.player);
+ game_secs = msg.data.start.game_secs;
+ thread_limit = msg.data.start.thread_limit;
+ mem_limit_mib = msg.data.start.mem_limit_mib;
+
+ u32 board_size = msg.data.start.board_size;
+
+ board = std::make_unique<Board>(board_size, rand);
+
+ switch (player) {
+ case HEX_PLAYER_BLACK:
+ opponent = HEX_PLAYER_WHITE;
+ state = State::SEND;
+ break;
+
+ case HEX_PLAYER_WHITE:
+ opponent = HEX_PLAYER_BLACK;
+ state = State::RECV;
+ break;
+ }
+
+ std::cout << "[" << hexplayerstr(player) << "] Starting game: "
+ << board_size << "x" << board_size << ", "
+ << game_secs << "secs" << std::endl;
+ } break;
+
+ case State::RECV: {
+ std::vector<enum hex_msg_type> expected_msg_types = {HEX_MSG_MOVE, HEX_MSG_SWAP, HEX_MSG_END};
+
+ struct hex_msg msg;
+ if (!net.recv_msg(msg, expected_msg_types)) {
+ std::cerr << "Failed to receive message from hex server" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ switch (msg.type) {
+ case HEX_MSG_MOVE:
+ board->play(opponent, msg.data.move.board_x, msg.data.move.board_y);
+
+ if (first_round && rand() % 2) {
+ board->swap(rand);
+
+ msg.type = HEX_MSG_SWAP;
+ if (!net.send_msg(msg)) {
+ std::cerr << "Failed to send message to hex server" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ state = State::RECV;
+ } else {
+ state = State::SEND;
+ }
+ break;
+
+ case HEX_MSG_SWAP:
+ board->swap(rand);
+ state = State::SEND;
+ break;
+
+ case HEX_MSG_END:
+ winner = static_cast<enum hex_player>(msg.data.end.winner);
+ state = State::END;
+ break;
+ }
+
+ first_round = false;
+ } break;
+
+ case State::SEND: {
+ struct hex_msg msg;
+ msg.type = HEX_MSG_MOVE;
+
+ Move move;
+ if (!board->next(move)) {
+ std::cerr << "Failed to generate next board move" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ board->play(player, move.x, move.y);
+
+ msg.data.move.board_x = move.x;
+ msg.data.move.board_y = move.y;
+
+ if (!net.send_msg(msg)) {
+ std::cerr << "Failed to send message to hex server" << std::endl;
+ exit(EXIT_FAILURE);
+ }
+
+ state = State::RECV;
+ first_round = false;
+ } break;
+
+ case State::END: {
+ std::cout << "[" << hexplayerstr(player) << "] Player " << hexplayerstr(winner) << " has won the game" << std::endl;
+ game_over = true;
+ } break;
+
+ default:
+ std::cerr << "Unknown game state: " << state << std::endl;
+ exit(EXIT_FAILURE);
+ break;
+ }
+ }
+
+ exit(EXIT_SUCCESS);
+}
diff --git a/agents/example_java_agent/.gitignore b/agents/example_java_agent/.gitignore
@@ -0,0 +1,2 @@
+agent.jar
+obj/
diff --git a/agents/example_java_agent/MANIFEST.txt b/agents/example_java_agent/MANIFEST.txt
@@ -0,0 +1,3 @@
+Main-Class: Agent
+Class-Path: obj/
+
diff --git a/agents/example_java_agent/Makefile b/agents/example_java_agent/Makefile
@@ -0,0 +1,36 @@
+.PHONY: all build clean deps
+
+JAVAC ?= javac
+JAR ?= jar
+
+SRC := src
+OBJ := obj
+
+JLINT := -deprecation -Xlint:unchecked
+JTARGET := -source 17 -target 17
+JCFLAGS := -d $(OBJ) -cp $(SRC) -g $(JLINT) $(JTARGET)
+
+TARGET := agent.jar
+MANIFEST := MANIFEST.txt
+SOURCES := $(SRC)/Agent.java
+OBJECTS := $(SOURCES:$(SRC)/%.java=$(OBJ)/%.class)
+
+all: build
+
+build: $(TARGET)
+
+clean:
+ rm -rf $(TARGET) $(OBJ)
+
+deps: $(MANIFEST)
+
+$(TARGET): deps $(OBJECTS)
+ $(JAR) -cfmv $@ $(MANIFEST) $(wordlist 2,$(words $^),$^)
+
+$(OBJ)/%.class: $(SRC)/%.java
+ @mkdir -p $(dir $@)
+ $(JAVAC) $(JCFLAGS) $<
+
+$(OBJ):
+ mkdir -p $@
+
diff --git a/agents/example_java_agent/run.sh b/agents/example_java_agent/run.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# we need to limit the number of threads that the Java runtime creates by
+# default to fit into the default thread limit (4 threads). the minimum hit
+# with the current options is 10 threads, but during loading of the jar
+# file we can hit up to 16 threads
+JVM_OPTS="
+ -XX:CICompilerCount=2
+ -XX:+UnlockExperimentalVMOptions
+ -XX:+UseSerialGC
+ -XX:+ReduceSignalUsage
+ -XX:+DisableAttachMechanism
+"
+
+exec java ${JVM_OPTS} -jar $(dirname $0)/agent.jar $@
diff --git a/agents/example_java_agent/src/Agent.java b/agents/example_java_agent/src/Agent.java
@@ -0,0 +1,315 @@
+import java.io.*;
+import java.net.*;
+import java.nio.*;
+import java.util.*;
+
+import hex.*;
+
+public class Agent {
+ private static Net net;
+ private static State state;
+ private static Board board;
+ private static HexPlayer player, opponent, winner;
+ private static int gameSecs, threadLimit, memLimitMib; // unused
+
+ public static void main(String[] args) {
+ if (args.length < 2) {
+ System.err.println("Not enough args: <host> <port>");
+ return;
+ }
+
+ String host = args[0];
+ int port = Integer.parseInt(args[1]);
+
+ try {
+ net = new Net(host, port);
+ } catch (IOException ex) {
+ System.err.println("Failed to initialise network");
+ return;
+ }
+
+ state = State.START;
+
+ boolean gameOver = false, firstRound = true;
+ while (!gameOver) {
+ switch (state) {
+ case START -> {
+ Optional<NetMessage> msg = net.recvMsg();
+
+ if (!msg.isPresent()) {
+ System.err.println("Failed to receive message from hex server");
+ return;
+ }
+
+ if (msg.get() instanceof StartMessage start) {
+ player = start.player();
+
+ gameSecs = start.gameSecs();
+ threadLimit = start.threadLimit();
+ memLimitMib = start.memLimitMib();
+
+ board = new Board(start.boardSize());
+
+ switch (player) {
+ case BLACK -> { opponent = HexPlayer.WHITE; state = State.SEND; }
+ case WHITE -> { opponent = HexPlayer.BLACK; state = State.RECV; }
+ }
+ } else {
+ System.err.println("Invalid message received from server");
+ return;
+ }
+ }
+
+ case RECV -> {
+ Optional<NetMessage> msg = net.recvMsg();
+
+ if (!msg.isPresent()) {
+ System.err.println("Failed to receive message from hex server");
+ return;
+ }
+
+ if (msg.get() instanceof MoveMessage move) {
+ board.play(opponent, move.boardX(), move.boardY());
+
+ if (firstRound && new Random().nextBoolean()) {
+ board.swap();
+
+ if (!net.sendMsg(new SwapMessage())) {
+ System.err.println("Failed to send message to hex server");
+ return;
+ }
+
+ state = state.RECV;
+ } else {
+ state = state.SEND;
+ }
+ } else if (msg.get() instanceof SwapMessage swap) {
+ board.swap();
+ state = State.SEND;
+ } else if (msg.get() instanceof EndMessage end) {
+ winner = end.winner();
+ state = State.END;
+ } else {
+ System.err.println("Invalid message received from server");
+ return;
+ }
+
+ firstRound = false;
+ }
+
+ case SEND -> {
+ Optional<Move> nextMove = board.next();
+
+ if (!nextMove.isPresent()) {
+ System.err.println("Failed to generate next board move");
+ return;
+ }
+
+ Move move = nextMove.get();
+
+ board.play(player, move.x(), move.y());
+
+ if (!net.sendMsg(new MoveMessage(move.x(), move.y()))) {
+ System.err.println("Failed to send message to hex server");
+ return;
+ }
+
+ state = State.RECV;
+ firstRound = false;
+ }
+
+ case END -> {
+ gameOver = true;
+ }
+ }
+ }
+
+ return;
+ }
+}
+
+enum State {
+ START, RECV, SEND, END,
+}
+
+record Move(int x, int y) {}
+
+enum Cell {
+ BLACK(HexPlayer.BLACK.value),
+ WHITE(HexPlayer.WHITE.value),
+ EMPTY(2);
+
+ public final int value;
+
+ private Cell(int value) {
+ this.value = value;
+ }
+}
+
+class Board {
+ private final int size;
+ private final ArrayList<Cell> cells;
+ private final ArrayList<Move> moves;
+
+ public Board(int size) {
+ this.size = size;
+ this.cells = new ArrayList<>(size * size);
+ this.moves = new ArrayList<>(size * size);
+
+ for (int j = 0; j < size; j++) {
+ for (int i = 0; i < size; i++) {
+ this.cells.add(Cell.EMPTY);
+ this.moves.add(new Move(i, j));
+ }
+ }
+
+ Collections.shuffle(this.moves);
+ }
+
+ public boolean play(HexPlayer player, int x, int y) {
+ int idx = y * this.size + x;
+
+ Cell cell = this.cells.get(idx);
+ if (cell != Cell.EMPTY) return false;
+
+ switch (player) {
+ case BLACK -> this.cells.set(idx, Cell.BLACK);
+ case WHITE -> this.cells.set(idx, Cell.WHITE);
+ }
+
+ this.moves.remove(new Move(x, y));
+
+ return true;
+ }
+
+ public void swap() {
+ this.moves.clear();
+
+ for (int j = 0; j < this.size; j++) {
+ for (int i = 0; i < this.size; i++) {
+ int idx = j * this.size + i;
+
+ Cell cell = this.cells.get(idx);
+
+ switch (cell) {
+ case BLACK -> this.cells.set(idx, Cell.WHITE);
+ case WHITE -> this.cells.set(idx, Cell.BLACK);
+ case EMPTY -> this.moves.add(new Move(i, j));
+ }
+ }
+ }
+
+ Collections.shuffle(this.moves);
+ }
+
+ public Optional<Move> next() {
+ if (this.moves.isEmpty()) return Optional.empty();
+
+ return Optional.of(this.moves.remove(this.moves.size() - 1));
+ }
+}
+
+sealed interface NetMessage {}
+
+record StartMessage(HexPlayer player, int boardSize, int gameSecs, int threadLimit, int memLimitMib) implements NetMessage {}
+record MoveMessage(int boardX, int boardY) implements NetMessage {}
+record SwapMessage() implements NetMessage {}
+record EndMessage(HexPlayer winner) implements NetMessage {}
+
+class Net {
+ private final Socket sock;
+ private final OutputStream out;
+ private final InputStream in;
+
+ public static final int MESSAGE_SIZE = 32;
+
+ public Net(String host, int port) throws IOException {
+ this.sock = new Socket(host, port);
+
+ this.out = this.sock.getOutputStream();
+ this.in = this.sock.getInputStream();
+ }
+
+ public Optional<NetMessage> recvMsg() {
+ byte[] buf = new byte[MESSAGE_SIZE];
+
+ int nbytes_recv = 0;
+
+ do {
+ int curr;
+ try {
+ curr = this.in.read(buf, nbytes_recv, buf.length - nbytes_recv);
+ } catch (IOException ex) {
+ return Optional.empty();
+ }
+
+ if (curr <= 0) return Optional.empty();
+ nbytes_recv += curr;
+ } while (nbytes_recv < buf.length);
+
+ return deserialiseMsg(ByteBuffer.wrap(buf));
+ }
+
+ public boolean sendMsg(NetMessage msg) {
+ byte[] buf = new byte[MESSAGE_SIZE];
+
+ serialiseMsg(msg, ByteBuffer.wrap(buf));
+
+ try {
+ this.out.write(buf);
+ this.out.flush();
+ } catch (IOException ex) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static void serialiseMsg(NetMessage msg, ByteBuffer buf) {
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ if (msg instanceof StartMessage start) {
+ buf.putInt(HexMessageType.START.value);
+ buf.putInt(start.player().value);
+ buf.putInt(start.boardSize());
+ buf.putInt(start.gameSecs());
+ buf.putInt(start.threadLimit());
+ buf.putInt(start.memLimitMib());
+ } else if (msg instanceof MoveMessage move) {
+ buf.putInt(HexMessageType.MOVE.value);
+ buf.putInt(move.boardX());
+ buf.putInt(move.boardY());
+ } else if (msg instanceof SwapMessage swap) {
+ buf.putInt(HexMessageType.SWAP.value);
+ } else if (msg instanceof EndMessage end) {
+ buf.putInt(HexMessageType.END.value);
+ buf.putInt(end.winner().value);
+ }
+ }
+
+ private static Optional<NetMessage> deserialiseMsg(ByteBuffer buf) {
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ int type = buf.getInt();
+
+ if (type == HexMessageType.START.value) {
+ HexPlayer player = HexPlayer.fromRaw(buf.getInt());
+ int boardSize = buf.getInt();
+ int gameSecs = buf.getInt();
+ int threadLimit = buf.getInt();
+ int memLimitMib = buf.getInt();
+ return Optional.of(new StartMessage(player, boardSize, gameSecs, threadLimit, memLimitMib));
+ } else if (type == HexMessageType.MOVE.value) {
+ int boardX = buf.getInt();
+ int boardY = buf.getInt();
+ return Optional.of(new MoveMessage(boardX, boardY));
+ } else if (type == HexMessageType.SWAP.value) {
+ return Optional.of(new SwapMessage());
+ } else if (type == HexMessageType.END.value) {
+ HexPlayer winner = HexPlayer.fromRaw(buf.getInt());
+ return Optional.of(new EndMessage(winner));
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/agents/example_java_agent/src/hex/HexMessageType.java b/agents/example_java_agent/src/hex/HexMessageType.java
@@ -0,0 +1,14 @@
+package hex;
+
+public enum HexMessageType {
+ START(0),
+ MOVE(1),
+ SWAP(2),
+ END(3);
+
+ public final int value;
+
+ private HexMessageType(int value) {
+ this.value = value;
+ }
+}
diff --git a/agents/example_java_agent/src/hex/HexPlayer.java b/agents/example_java_agent/src/hex/HexPlayer.java
@@ -0,0 +1,20 @@
+package hex;
+
+public enum HexPlayer {
+ BLACK(0),
+ WHITE(1);
+
+ public final int value;
+
+ private HexPlayer(int value) {
+ this.value = value;
+ }
+
+ public static HexPlayer fromRaw(int raw) {
+ switch (raw) {
+ case 0 -> { return HexPlayer.BLACK; }
+ case 1 -> { return HexPlayer.WHITE; }
+ default -> { return null; }
+ }
+ }
+}
diff --git a/agents/example_python3_agent/.gitignore b/agents/example_python3_agent/.gitignore
@@ -0,0 +1 @@
+**/__pycache__
diff --git a/agents/example_python3_agent/Makefile b/agents/example_python3_agent/Makefile
@@ -0,0 +1,7 @@
+.PHONY: all build clean
+
+all: build
+
+build:
+
+clean:
diff --git a/agents/example_python3_agent/agent.py b/agents/example_python3_agent/agent.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+
+from __future__ import annotations
+
+import enum
+import random
+import socket
+import struct
+import sys
+
+
+class PlayerType(enum.Enum):
+ PLAYER_BLACK = 0
+ PLAYER_WHITE = 1
+
+ def __str__(self) -> str:
+ match self:
+ case self.PLAYER_BLACK: return 'black'
+ case self.PLAYER_WHITE: return 'white'
+
+
+class MsgType(enum.Enum):
+ MSG_START = 0
+ MSG_MOVE = 1
+ MSG_SWAP = 2
+ MSG_END = 3
+
+
+class MsgStartData:
+ def __init__(self, player: int, board_size: int, game_secs: int, thread_limit: int, mem_limit_mib: int):
+ self.player = PlayerType(player)
+ self.board_size = board_size
+ self.game_secs = game_secs
+ self.thread_limit = thread_limit
+ self.mem_limit_mib = mem_limit_mib
+
+ def as_tuple(self) -> tuple[PlayerType, int, int, int, int]:
+ return self.player, self.board_size, self.game_secs, self.thread_limit, self.mem_limit_mib
+
+
+class MsgMoveData:
+ def __init__(self, board_x: int, board_y: int):
+ self.board_x = board_x
+ self.board_y = board_y
+
+ def __repr__(self) -> str:
+ return f'move: ({self.board_x}, {self.board_y})'
+
+ def as_tuple(self) -> tuple[int, int]:
+ return self.board_x, self.board_y
+
+
+class MsgSwapData:
+ def __init__(self):
+ pass
+
+ def __repr__(self) -> str:
+ return f'swap'
+
+
+class MsgEndData:
+ def __init__(self, winner: int):
+ self.winner = PlayerType(winner)
+
+ def __repr__(self) -> str:
+ return f'end: {self.winner}'
+
+ def as_tuple(self) -> tuple[PlayerType]:
+ return self.winner,
+
+
+MsgData = MsgStartData | MsgMoveData | MsgSwapData | MsgEndData
+
+
+class Msg:
+ def __init__(self, typ: MsgType, dat: MsgData):
+ self.typ = typ
+ self.dat = dat
+
+ def __repr__(self) -> str:
+ return f'msg: {self.typ}, {self.dat}'
+
+ @classmethod
+ def size(cls) -> int:
+ return 32
+
+ def serialise_into(self, buffer: memoryview) -> int:
+ assert Msg.size() <= len(buffer)
+
+ match self.typ:
+ case MsgType.MSG_START: # this message type is never sent by the client
+ pass
+
+ case MsgType.MSG_MOVE:
+ struct.pack_into('!III', buffer, 0,
+ self.typ.value,
+ self.dat.board_x,
+ self.dat.board_y)
+
+ case MsgType.MSG_SWAP:
+ struct.pack_into('!I', buffer, 0, self.typ.value)
+
+ case MsgType.MSG_END: # this message type is never sent by the client
+ pass
+
+ return Msg.size()
+
+ @classmethod
+ def deserialise_from(cls, buffer: memoryview) -> Msg:
+ assert cls.size() <= len(buffer)
+
+ raw_typ, = struct.unpack_from('!I', buffer, 0)
+ typ = MsgType(raw_typ)
+
+ match typ:
+ case MsgType.MSG_START: dat = MsgStartData(*struct.unpack_from('!IIIII', buffer, 4))
+ case MsgType.MSG_MOVE: dat = MsgMoveData(*struct.unpack_from('!II', buffer, 4))
+ case MsgType.MSG_SWAP: dat = MsgSwapData()
+ case MsgType.MSG_END: dat = MsgEndData(*struct.unpack_from('!I', buffer, 4))
+
+ return Msg(typ, dat)
+
+
+def recv_msg(sock: socket.socket, *, expected_msg_types: list[MsgType]) -> Msg:
+ buffer = bytearray(Msg.size())
+
+ def recv_all_bytes(sock: socket.socket, buf: memoryview, sz: int) -> int:
+ total = 0
+ while total < sz:
+ curr = sock.recv_into(buf[total:], sz - total)
+ if curr == 0: return total
+ total += curr
+
+ return total
+
+ recv_all_bytes(sock, memoryview(buffer), len(buffer))
+
+ return Msg.deserialise_from(buffer)
+
+
+def send_msg(sock: socket.socket, msg: Msg) -> None:
+ buffer = bytearray(Msg.size())
+
+ def send_all_bytes(sock: socket.socket, buf: memoryview, sz: int) -> int:
+ total = 0
+ while total < sz:
+ curr = sock.send(buf[total:], sz - total)
+ if curr == 0: return total
+ total += curr
+
+ return total
+
+ msg.serialise_into(buffer)
+
+ send_all_bytes(sock, memoryview(buffer), len(buffer))
+
+
+class Board:
+ class Cell(enum.Enum):
+ BLACK = PlayerType.PLAYER_BLACK.value
+ WHITE = PlayerType.PLAYER_WHITE.value
+ EMPTY = enum.auto()
+
+ def __init__(self, board_size: int):
+ self.board_size = board_size
+ self.board = [self.Cell.EMPTY for _ in range(board_size * board_size)]
+ self.remaining_moves = [(i, j) for i in range(board_size) for j in range(board_size)]
+ random.shuffle(self.remaining_moves)
+
+ def swap(self) -> None:
+ self.remaining_moves = []
+ for j in range(self.board_size):
+ for i in range(self.board_size):
+ match self.board[j * self.board_size + i]:
+ case self.Cell.BLACK:
+ self.board[j * self.board_size + i] = self.Cell.WHITE
+
+ case self.Cell.WHITE:
+ self.board[j * self.board_size + i] = self.Cell.BLACK
+
+ case self.Cell.EMPTY:
+ self.remaining_moves.append((i, j))
+
+ random.shuffle(self.remaining_moves)
+
+ def play(self, player: PlayerType, px: int, py: int) -> bool:
+ old = self.board[py * self.board_size + px]
+
+ if old != self.Cell.EMPTY:
+ return False
+
+ new = None
+ match player:
+ case PlayerType.PLAYER_BLACK: new = self.Cell.BLACK
+ case PlayerType.PLAYER_WHITE: new = self.Cell.WHITE
+
+ self.board[py * self.board_size + px] = new
+
+ for idx, t in enumerate(self.remaining_moves):
+ if t[0] == px and t[1] == py:
+ self.remaining_moves.pop(idx)
+ break
+
+ return True
+
+ def get_next_move(self) -> tuple[int, int]:
+ return self.remaining_moves.pop(0)
+
+
+class GameState(enum.Enum):
+ START = enum.auto()
+ RECV = enum.auto()
+ SEND = enum.auto()
+ END = enum.auto()
+
+
+def main() -> None:
+ if len(sys.argv) < 3:
+ print(f'Usage: {sys.argv[0]} <host> <port>', file=sys.stderr)
+ quit(1)
+
+ host, port, *args = sys.argv[1:]
+ with socket.create_connection((host, port)) as sock:
+ state = GameState.START
+
+ player = None
+ game_secs = None # unused
+ thread_limit = None # unused
+ mem_limit_mib = None # unused
+
+ board = None
+ other_player = None
+ winner = None
+
+ first_round = True
+ game_is_over = False
+ while not game_is_over:
+ match state:
+ case GameState.START:
+ msg = recv_msg(sock, expected_msg_types=[MsgType.MSG_START])
+ player, board_size, game_secs, thread_limit, mem_limit_mib = msg.dat.as_tuple()
+
+ board = Board(board_size)
+
+ print(f'[{player}] Started game: {board_size}x{board_size}, {game_secs} secs, {thread_limit} threads, {mem_limit_mib} MiB')
+
+ if player == PlayerType.PLAYER_BLACK:
+ other_player = PlayerType.PLAYER_WHITE
+ state = GameState.SEND
+
+ elif player == PlayerType.PLAYER_WHITE:
+ other_player = PlayerType.PLAYER_BLACK
+ state = GameState.RECV
+
+ case GameState.RECV:
+ msg = recv_msg(sock, expected_msg_types=[MsgType.MSG_MOVE, MsgType.MSG_SWAP, MsgType.MSG_END])
+
+ if msg.typ == MsgType.MSG_MOVE:
+ board_x, board_y = msg.dat.as_tuple()
+ board.play(other_player, board_x, board_y)
+
+ if first_round and random.choice([True, False]):
+ board.swap()
+
+ msg = Msg(MsgType.MSG_SWAP, MsgSwapData())
+ send_msg(sock, msg)
+
+ state = GameState.RECV
+
+ else:
+ state = GameState.SEND
+
+ elif msg.typ == MsgType.MSG_SWAP:
+ board.swap()
+
+ state = GameState.SEND
+
+ elif msg.typ == MsgType.MSG_END:
+ winner, = msg.dat.as_tuple()
+
+ state = GameState.END
+
+ first_round = False
+
+ case GameState.SEND:
+ board_x, board_y = board.get_next_move()
+ board.play(player, board_x, board_y)
+
+ msg = Msg(MsgType.MSG_MOVE, MsgMoveData(board_x, board_y))
+
+ send_msg(sock, msg)
+
+ state = GameState.RECV
+
+ first_round = False
+
+ case GameState.END:
+ print(f'[{player}] Player {winner} has won the game')
+ break
+
+ case _:
+ print(f'[{player}] Unknown state encountered: {state}')
+ break
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/agents/example_python3_agent/run.sh b/agents/example_python3_agent/run.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec /usr/bin/env python3 $(dirname $0)/agent.py $@
diff --git a/include/hex.h b/include/hex.h
@@ -0,0 +1,248 @@
+#ifndef HEX_H
+#define HEX_H
+
+#ifdef _XOPEN_SOURCE
+#undef _XOPEN_SOURCE
+#endif /* _XOPEN_SOURCE */
+
+#define _XOPEN_SOURCE 700
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif /* _GNU_SOURCE */
+
+#ifndef _DEFAULT_SOURCE
+#define _DEFAULT_SOURCE
+#endif /* _DEFAULT_SOURCE */
+
+#ifdef __cplusplus
+ #include <cassert>
+ #include <cerrno>
+ #include <cinttypes>
+ #include <climits>
+ #include <cstdarg>
+ #include <cstdbool>
+ #include <cstdint>
+ #include <cstdio>
+ #include <cstdlib>
+ #include <cstring>
+#else
+ #include <assert.h>
+ #include <errno.h>
+ #include <inttypes.h>
+ #include <limits.h>
+ #include <stdarg.h>
+ #include <stdbool.h>
+ #include <stdint.h>
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
+#endif /* __cplusplus */
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "hex/types.h"
+#include "hex/proto.h"
+
+/* timeout for accept()-ing an agent connection before assuming a forfeit
+ */
+#define HEX_AGENT_ACCEPT_TIMEOUT_MS (1 * 1000)
+
+#define HEX_AGENT_LOGFILE_TEMPLATE "/tmp/hex-agent.XXXXXX"
+#define HEX_AGENT_LOGFILE_MODE (0666)
+
+extern struct args {
+ char *agent_1;
+ uid_t agent_1_uid;
+ char *agent_2;
+ uid_t agent_2_uid;
+ u32 board_dimensions;
+ u32 game_secs;
+ u32 thread_limit;
+ u32 mem_limit_mib;
+ b32 verbose;
+} args;
+
+enum hex_error {
+ HEX_ERROR_OK,
+ HEX_ERROR_GAME_OVER,
+ HEX_ERROR_TIMEOUT,
+ HEX_ERROR_BAD_MOVE,
+ HEX_ERROR_BAD_MSG,
+ HEX_ERROR_DISCONNECT,
+ HEX_ERROR_SERVER,
+};
+
+inline char const *
+hexerrorstr(enum hex_error val)
+{
+ switch (val) {
+ case HEX_ERROR_OK: return "OK";
+ case HEX_ERROR_GAME_OVER: return "GAME_OVER";
+ case HEX_ERROR_TIMEOUT: return "TIMEOUT";
+ case HEX_ERROR_BAD_MOVE: return "BAD_MOVE";
+ case HEX_ERROR_BAD_MSG: return "BAD_MSG";
+ case HEX_ERROR_DISCONNECT: return "DISCONNECT";
+ case HEX_ERROR_SERVER: return "SERVER";
+ default: return "UNKNOWN";
+ }
+}
+
+struct statistics {
+ char *agent_1;
+ b32 agent_1_won;
+ u32 agent_1_rounds;
+ f32 agent_1_secs;
+ enum hex_error agent_1_err;
+ char *agent_2;
+ b32 agent_2_won;
+ u32 agent_2_rounds;
+ f32 agent_2_secs;
+ enum hex_error agent_2_err;
+};
+
+struct agent_state {
+ /* which player are we, and what agent do we run */
+ enum hex_player player;
+ char *agent;
+ uid_t agent_uid;
+ char logfile[PATH_MAX];
+
+ /* how much time this agent has left to execute before it times out */
+ struct timespec timer;
+
+ /* socket for communicating with agent */
+ int sockfd;
+ struct sockaddr_storage sock_addr;
+ socklen_t sock_addrlen;
+};
+
+enum cell_state {
+ CELL_EMPTY,
+ CELL_BLACK,
+ CELL_WHITE,
+};
+
+struct board_segment {
+ s16 parent_relptr; /* pointer to root of rooted tree */
+ u8 rank; /* disambiguation between identical segments */
+ u8 cell; /* the owner of the current cell */
+};
+
+static inline s16
+board_segment_abs2rel(struct board_segment *base, struct board_segment *absptr) {
+ return RELPTR_ABS2REL(s16, base, absptr);
+}
+
+static inline struct board_segment *
+board_segment_rel2abs(struct board_segment *base, s16 relptr) {
+ return RELPTR_REL2ABS(struct board_segment *, s16, base, relptr);
+}
+
+extern struct board_segment *
+board_segment_root(struct board_segment *self);
+
+extern void
+board_segment_merge(struct board_segment *restrict self, struct board_segment *restrict elem);
+
+extern b32
+board_segment_joined(struct board_segment *self, struct board_segment *elem);
+
+struct board_state {
+ u32 size;
+
+ /* track connections between board "segments" (groups of cells owned
+ * by one player), and the edges for each player
+ */
+ struct board_segment black_source, black_sink, white_source, white_sink;
+ struct board_segment segments[];
+};
+
+extern struct board_state *
+board_alloc(size_t size);
+
+extern void
+board_free(struct board_state *self);
+
+extern void
+board_print(struct board_state *self);
+
+extern b32
+board_play(struct board_state *self, enum hex_player player, s32 x, s32 y);
+
+extern void
+board_swap(struct board_state *self);
+
+extern b32
+board_completed(struct board_state *self, enum hex_player *winner);
+
+struct server_state {
+ struct agent_state black_agent, white_agent;
+ struct board_state *board;
+
+ /* socket for accepting agent connections */
+ int servfd;
+ struct sockaddr_storage serv_addr;
+ socklen_t serv_addrlen;
+ char serv_host[NI_MAXHOST], serv_port[NI_MAXSERV];
+};
+
+extern bool
+server_init(struct server_state *state);
+
+extern void
+server_free(struct server_state *state);
+
+extern bool
+server_spawn_agent(struct server_state *state, struct agent_state *agent_state);
+
+extern void
+server_wait_all_agents(struct server_state *state);
+
+extern void
+server_run(struct server_state *state, struct statistics *statistics);
+
+inline void
+errlog(char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ vfprintf(stderr, fmt, va);
+ va_end(va);
+}
+
+inline void
+dbglog(char *fmt, ...)
+{
+ va_list va;
+
+ va_start(va, fmt);
+ if (args.verbose) vfprintf(stderr, fmt, va);
+ va_end(va);
+}
+
+inline void
+difftimespec(struct timespec *restrict lhs, struct timespec *restrict rhs, struct timespec *restrict out)
+{
+ if (lhs->tv_sec <= rhs->tv_sec && lhs->tv_nsec < rhs->tv_nsec) {
+ out->tv_sec = 0;
+ out->tv_nsec = 0;
+ } else {
+ out->tv_sec = lhs->tv_sec - rhs->tv_sec - (lhs->tv_nsec < rhs->tv_nsec);
+ out->tv_nsec = lhs->tv_nsec - rhs->tv_nsec + (lhs->tv_nsec < rhs->tv_nsec) * NANOSECS;
+ }
+}
+
+#endif /* HEX_H */
diff --git a/include/hex/proto.h b/include/hex/proto.h
@@ -0,0 +1,174 @@
+#ifndef HEX_PROTO_H
+#define HEX_PROTO_H
+
+#include "hex/types.h"
+
+#ifdef __cplusplus
+ #include <cassert>
+ #include <cstring>
+#else
+ #include <assert.h>
+ #include <string.h>
+#endif /* __cplusplus */
+
+#include <arpa/inet.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+enum hex_player {
+ HEX_PLAYER_BLACK = 0,
+ HEX_PLAYER_WHITE = 1,
+};
+
+inline enum hex_player
+hexopponent(enum hex_player player)
+{
+ switch (player) {
+ case HEX_PLAYER_BLACK: return HEX_PLAYER_WHITE;
+ case HEX_PLAYER_WHITE: return HEX_PLAYER_BLACK;
+ }
+}
+
+inline char const *
+hexplayerstr(enum hex_player val)
+{
+ switch (val) {
+ case HEX_PLAYER_BLACK: return "black";
+ case HEX_PLAYER_WHITE: return "white";
+ default: return "(err)";
+ }
+}
+
+enum hex_msg_type {
+ HEX_MSG_START = 0,
+ HEX_MSG_MOVE = 1,
+ HEX_MSG_SWAP = 2,
+ HEX_MSG_END = 3,
+};
+
+struct hex_msg_start {
+ u32 player;
+ u32 board_size;
+ u32 game_secs;
+ u32 thread_limit;
+ u32 mem_limit_mib;
+};
+
+struct hex_msg_move {
+ u32 board_x;
+ u32 board_y;
+};
+
+struct hex_msg_end {
+ u32 winner;
+};
+
+union hex_msg_data {
+ struct hex_msg_start start;
+ struct hex_msg_move move;
+ struct hex_msg_end end;
+};
+
+struct hex_msg {
+ u32 type;
+ union hex_msg_data data;
+};
+
+#define HEX_MSG_SZ 32
+
+inline b32
+#ifdef __cplusplus
+hex_msg_try_serialise(struct hex_msg const *msg, u8 (&out)[HEX_MSG_SZ])
+#else
+hex_msg_try_serialise(struct hex_msg const *msg, u8 out[static HEX_MSG_SZ])
+#endif
+{
+ assert(msg);
+ assert(out);
+
+ u32 *bufp = (u32 *) out;
+
+ *bufp++ = htonl(msg->type);
+
+ switch (msg->type) {
+ case HEX_MSG_START:
+ *bufp++ = htonl(msg->data.start.player);
+ *bufp++ = htonl(msg->data.start.board_size);
+ *bufp++ = htonl(msg->data.start.game_secs);
+ *bufp++ = htonl(msg->data.start.thread_limit);
+ *bufp++ = htonl(msg->data.start.mem_limit_mib);
+ break;
+
+ case HEX_MSG_MOVE:
+ *bufp++ = htonl(msg->data.move.board_x);
+ *bufp++ = htonl(msg->data.move.board_y);
+ break;
+
+ case HEX_MSG_SWAP:
+ break;
+
+ case HEX_MSG_END:
+ *bufp++ = htonl(msg->data.end.winner);
+ break;
+ }
+
+ /* zero out remaining all message bytes */
+ u8 *remaining = (u8 *) bufp;
+ assert(remaining < out + HEX_MSG_SZ);
+ memset(remaining, 0, (out + HEX_MSG_SZ) - remaining);
+
+ return true;
+}
+
+inline b32
+#ifdef __cplusplus
+hex_msg_try_deserialise(u8 (&buf)[HEX_MSG_SZ], struct hex_msg *out)
+#else
+hex_msg_try_deserialise(u8 buf[static HEX_MSG_SZ], struct hex_msg *out)
+#endif
+{
+ assert(buf);
+ assert(out);
+
+ u32 *bufp = (u32 *) buf;
+
+ struct hex_msg msg;
+ msg.type = ntohl(*bufp++);
+
+ switch (msg.type) {
+ case HEX_MSG_START:
+ msg.data.start.player = ntohl(*bufp++);
+ msg.data.start.board_size = ntohl(*bufp++);
+ msg.data.start.game_secs = ntohl(*bufp++);
+ msg.data.start.thread_limit = ntohl(*bufp++);
+ msg.data.start.mem_limit_mib = ntohl(*bufp++);
+ break;
+
+ case HEX_MSG_MOVE:
+ msg.data.move.board_x = ntohl(*bufp++);
+ msg.data.move.board_y = ntohl(*bufp++);
+ break;
+
+ case HEX_MSG_SWAP:
+ break;
+
+ case HEX_MSG_END:
+ msg.data.end.winner = ntohl(*bufp++);
+ break;
+
+ default:
+ return false;
+ }
+
+ *out = msg;
+
+ return true;
+}
+
+#ifdef __cplusplus
+};
+#endif /* __cplusplus */
+
+#endif /* HEX_PROTO_H */
diff --git a/include/hex/types.h b/include/hex/types.h
@@ -0,0 +1,64 @@
+#ifndef HEX_TYPES_H
+#define HEX_TYPES_H
+
+#ifdef __cplusplus
+ #include <cinttypes>
+ #include <cstdbool>
+ #include <cstddef>
+ #include <cstdint>
+#else
+ #include <inttypes.h>
+ #include <stdbool.h>
+ #include <stddef.h>
+ #include <stdint.h>
+#endif /* __cplusplus */
+
+typedef int32_t b32;
+
+typedef unsigned char c8;
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+typedef float f32;
+typedef double f64;
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define MIN(a, b) ((a) > (b) ? (a) : (b))
+#define MAX(a, b) ((a) < (b) ? (b) : (a))
+
+#define RELPTR_NULL (0)
+
+#define _RELPTR_MASK(ty_relptr) ((ty_relptr)1 << ((sizeof(ty_relptr) * 8) - 1))
+#define _RELPTR_ENC(ty_relptr, ptroff) \
+ ((ty_relptr)((ptroff) ^ _RELPTR_MASK(ty_relptr)))
+#define _RELPTR_DEC(ty_relptr, relptr) \
+ ((ty_relptr)((relptr) ^ _RELPTR_MASK(ty_relptr)))
+
+#define RELPTR_ABS2REL(ty_relptr, base, absptr) \
+ ((absptr) \
+ ? _RELPTR_ENC(ty_relptr, (u8 *) absptr - (u8 *) base) \
+ : RELPTR_NULL)
+
+#define RELPTR_REL2ABS(ty_absptr, ty_relptr, base, relptr) \
+ ((relptr) \
+ ? ((ty_absptr)((u8 *) base + _RELPTR_DEC(ty_relptr, relptr))) \
+ : NULL)
+
+#define NANOSECS (1000000000ULL)
+
+#define TIMESPEC_TO_NANOS(sec, nsec) (((u64) (sec) * NANOSECS) + (nsec))
+
+#define KiB (1024ULL)
+#define MiB (1024ULL * KiB)
+#define GiB (1024ULL * MiB)
+
+#endif /* HEX_TYPES_H */
diff --git a/schedule.txt b/schedule.txt
@@ -0,0 +1,11 @@
+# schedule file consisting of comma-separated agent pairs
+
+agents/example_python3_agent/agent.py,agents/example_python3_agent/run.sh
+
+agents/example_c_agent/run.sh,agents/example_python3_agent/run.sh
+
+agents/example_cpp_agent/run.sh,agents/example_python3_agent/run.sh
+
+# java agent requires at least 16 threads, even with the minimal JVM options,
+# which means that --threads 16 must be passed to tournament-host.py
+#agents/example_java_agent/run.sh,agents/example_python3_agent/run.sh
diff --git a/src/board.c b/src/board.c
@@ -0,0 +1,248 @@
+#include "hex.h"
+
+extern inline s16
+board_segment_abs2rel(struct board_segment *base, struct board_segment *absptr);
+
+extern inline struct board_segment *
+board_segment_rel2abs(struct board_segment *base, s16 relptr);
+
+struct board_segment *
+board_segment_root(struct board_segment *self)
+{
+ assert(self);
+
+ struct board_segment *parent, *grandparent;
+ while ((parent = board_segment_rel2abs(self, self->parent_relptr))) {
+ grandparent = board_segment_rel2abs(parent, parent->parent_relptr);
+ if (!grandparent) return parent;
+
+ /* compress the path from self->parent->grandparent to
+ * self->grandparent for faster future traversal
+ */
+ self->parent_relptr = board_segment_abs2rel(self, grandparent);
+
+ self = grandparent;
+ }
+
+ return self;
+}
+
+void
+board_segment_merge(struct board_segment *restrict self, struct board_segment *restrict elem)
+{
+ assert(self);
+ assert(elem);
+
+ struct board_segment *self_root = board_segment_root(self);
+ struct board_segment *elem_root = board_segment_root(elem);
+
+ if (self_root == elem_root) return; /* NOTE: already merged */
+
+ if (self_root->rank < elem_root->rank) {
+ self_root->parent_relptr = board_segment_abs2rel(self_root, elem_root);
+ } else if (self_root->rank > elem_root->rank) {
+ elem_root->parent_relptr = board_segment_abs2rel(elem_root, self_root);
+ } else {
+ self_root->parent_relptr = board_segment_abs2rel(self_root, elem_root);
+ elem_root->rank++;
+ }
+}
+
+b32
+board_segment_joined(struct board_segment *self, struct board_segment *elem)
+{
+ return board_segment_root(self) == board_segment_root(elem);
+}
+
+#define NEIGHBOURS_COUNT 6
+
+static s8 neighbour_dx[NEIGHBOURS_COUNT] = { -1, -1, 0, 0, +1, +1, };
+static s8 neighbour_dy[NEIGHBOURS_COUNT] = { 0, +1, -1, +1, -1, 0, };
+
+static inline struct board_segment *
+board_get_segment(struct board_state *self, s32 x, s32 y)
+{
+ assert(self);
+
+ if (-1 < x && x < (s32) self->size && -1 < y && y < (s32) self->size)
+ return &self->segments[y * self->size + x];
+
+ return NULL;
+}
+
+static size_t
+board_neighbours(struct board_state *self, s32 x, s32 y, struct board_segment *buf[static NEIGHBOURS_COUNT])
+{
+ assert(self);
+ assert(buf);
+
+ size_t count = 0;
+
+ for (size_t i = 0; i < NEIGHBOURS_COUNT; i++) {
+ s32 px = x + neighbour_dx[i], py = y + neighbour_dy[i];
+
+ struct board_segment *seg;
+ if ((seg = board_get_segment(self, px, py)))
+ buf[count++] = seg;
+ }
+
+ return count;
+}
+
+struct board_state *
+board_alloc(size_t size)
+{
+ struct board_state *board;
+ size_t segments = size * size;
+ size_t sz = sizeof *board + segments * sizeof *board->segments;
+
+ if (!(board = malloc(sz))) return NULL;
+
+ memset(board, 0, sz);
+
+ board->size = size;
+
+ return board;
+}
+
+void
+board_free(struct board_state *self)
+{
+ assert(self);
+
+ free(self);
+}
+
+void
+board_print(struct board_state *self)
+{
+ assert(self);
+
+ for (size_t y = 0; y < self->size; y++) {
+ for (size_t k = 0; k < y; k++)
+ dbglog(" ");
+
+ struct board_segment *seg;
+ for (size_t x = 0; x < self->size; x++) {
+ seg = board_get_segment(self, x, y);
+
+ switch (seg->cell) {
+ case CELL_EMPTY: dbglog(". "); break;
+ case CELL_BLACK: dbglog("B "); break;
+ case CELL_WHITE: dbglog("W "); break;
+ }
+ }
+
+ dbglog("\n");
+ }
+}
+
+b32
+board_play(struct board_state *self, enum hex_player player, s32 x, s32 y)
+{
+ assert(self);
+
+ struct board_segment *seg = board_get_segment(self, x, y);
+ if (!seg) {
+ dbglog("[server] Agent %u played invalid move: (%" PRIi32 ", %" PRIi32 "); out of bounds\n",
+ player, x, y);
+
+ return false;
+ }
+
+ if (seg->cell != CELL_EMPTY) {
+ char *cell_strs[] = {
+ [CELL_EMPTY] = "empty", [CELL_BLACK] = "black", [CELL_WHITE] = "white",
+ };
+
+ dbglog("[server] Agent %u played invalid move: (%" PRIi32 ", %" PRIi32 "); previously occupied by %s\n",
+ player, x, y, cell_strs[seg->cell]);
+
+ return false;
+ }
+
+ switch (player) {
+ case HEX_PLAYER_BLACK: seg->cell = CELL_BLACK; break;
+ case HEX_PLAYER_WHITE: seg->cell = CELL_WHITE; break;
+ }
+
+ /* NOTE: handle case where player plays a cell connected to their
+ * relevant edged (source/sink)
+ */
+ if (player == HEX_PLAYER_BLACK) {
+ if (x == 0) {
+ board_segment_merge(seg, &self->black_source);
+ } else if (x == (s32) self->size - 1) {
+ board_segment_merge(seg, &self->black_sink);
+ }
+ } else if (player == HEX_PLAYER_WHITE) {
+ if (y == 0) {
+ board_segment_merge(seg, &self->white_source);
+ } else if (y == (s32) self->size - 1) {
+ board_segment_merge(seg, &self->white_sink);
+ }
+ }
+
+ /* NOTE: merge the played cell with all of its neighbours played by
+ * the same player
+ */
+ struct board_segment *neighbours[NEIGHBOURS_COUNT];
+ size_t neighbours_count = board_neighbours(self, x, y, neighbours);
+
+ for (size_t i = 0; i < neighbours_count; i++) {
+ if (neighbours[i]->cell == seg->cell)
+ board_segment_merge(seg, neighbours[i]);
+ }
+
+ return true;
+}
+
+void
+board_swap(struct board_state *self)
+{
+ assert(self);
+
+ for (size_t i = 0; i < self->size * self->size; i++) {
+ struct board_segment *seg = &self->segments[i];
+
+ s32 x = i % self->size;
+ s32 y = i / self->size;
+
+ switch (seg->cell) {
+ case CELL_EMPTY:
+ break; /* skip empty cells */
+
+ default: {
+ u8 old_cell_value = seg->cell;
+
+ seg->cell = CELL_EMPTY;
+ seg->parent_relptr = RELPTR_NULL;
+ seg->rank = 0;
+
+ switch (old_cell_value) {
+ case CELL_BLACK: board_play(self, HEX_PLAYER_WHITE, x, y); break;
+ case CELL_WHITE: board_play(self, HEX_PLAYER_BLACK, x, y); break;
+ }
+ } break;
+ }
+ }
+}
+
+b32
+board_completed(struct board_state *self, enum hex_player *winner)
+{
+ assert(self);
+ assert(winner);
+
+ if (board_segment_joined(&self->black_source, &self->black_sink)) {
+ *winner = HEX_PLAYER_BLACK;
+ return true;
+ }
+
+ if (board_segment_joined(&self->white_source, &self->white_sink)) {
+ *winner = HEX_PLAYER_WHITE;
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/hex.c b/src/hex.c
@@ -0,0 +1,203 @@
+#include "hex.h"
+
+struct args args = {
+ .agent_1 = NULL,
+ .agent_1_uid = 0,
+ .agent_2 = NULL,
+ .agent_2_uid = 0,
+ .board_dimensions = 11,
+ .game_secs = 300,
+ .thread_limit = 4,
+ .mem_limit_mib = 1024,
+ .verbose = false,
+};
+
+static void
+parse_args(s32 argc, char **argv);
+
+static void
+usage(char **argv)
+{
+ fprintf(stderr, "Usage: %s -a <agent-1> -ua <uid> -b <agent-2> -ub <uid> [-d 11] [-s 300] [-t 4] [-m 1024] [-v] [-h]\n", argv[0]);
+ fprintf(stderr, "\t-a: The command to execute for the first agent (black)\n");
+ fprintf(stderr, "\t-ua: The user id to set for the first agent (black)\n");
+ fprintf(stderr, "\t-b: The command to execute for the second agent (white)\n");
+ fprintf(stderr, "\t-ub: The user id to set for the second agent (white)\n");
+ fprintf(stderr, "\t-d: The dimensions for the game board (default: 11)\n");
+ fprintf(stderr, "\t-s: The per-agent game timer, in seconds (default: 300 seconds)\n");
+ fprintf(stderr, "\t-t: The per-agent thread hard-limit (default: 4 threads)\n");
+ fprintf(stderr, "\t-m: The per-agent memory hard-limit, in MiB (default: 1024 MiB)\n");
+ fprintf(stderr, "\t-v: Enables verbose logging on the server\n");
+ fprintf(stderr, "\t-h: Prints this help information\n");
+}
+
+s32
+main(s32 argc, char **argv)
+{
+ parse_args(argc, argv);
+
+ if (!args.agent_1 || !args.agent_2) {
+ errlog("Must provide execution targets for both agent-1 and agent-2\n");
+ usage(argv);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!args.agent_1_uid || !args.agent_2_uid) {
+ errlog("Must provide (non-root) user ids for both agent-1 and agent-2\n");
+ usage(argv);
+ exit(EXIT_FAILURE);
+ }
+
+ struct board_state *board = board_alloc(args.board_dimensions);
+ if (!board) {
+ errlog("Failed to allocate board of size %" PRIu32 "\n", args.board_dimensions);
+ exit(EXIT_FAILURE);
+ }
+
+ struct server_state state = {
+ .black_agent = {
+ .player = HEX_PLAYER_BLACK,
+ .agent = args.agent_1,
+ .agent_uid = args.agent_1_uid,
+ .logfile = HEX_AGENT_LOGFILE_TEMPLATE,
+ .timer = { .tv_sec = args.game_secs, .tv_nsec = 0, },
+ .sock_addrlen = sizeof(struct sockaddr_storage),
+ },
+ .white_agent = {
+ .player = HEX_PLAYER_WHITE,
+ .agent = args.agent_2,
+ .agent_uid = args.agent_2_uid,
+ .logfile = HEX_AGENT_LOGFILE_TEMPLATE,
+ .timer = { .tv_sec = args.game_secs, .tv_nsec = 0, },
+ .sock_addrlen = sizeof(struct sockaddr_storage),
+ },
+ .board = board,
+ };
+
+ if (!server_init(&state)) {
+ errlog("Failed to initialise server state\n");
+ exit(EXIT_FAILURE);
+ }
+
+ if (!server_spawn_agent(&state, &state.black_agent)) {
+ errlog("Failed to spawn black user agent: %s\n", state.black_agent.agent);
+ exit(EXIT_FAILURE);
+ }
+
+ if (!server_spawn_agent(&state, &state.white_agent)) {
+ errlog("Failed to spawn white user agent: %s\n", state.white_agent.agent);
+ exit(EXIT_FAILURE);
+ }
+
+ struct statistics stats;
+ server_run(&state, &stats);
+
+ server_wait_all_agents(&state);
+
+ server_free(&state);
+
+ board_free(board);
+
+ fprintf(stdout, "agent_1,agent_1_won,agent_1_rounds,agent_1_secs,agent_1_err,agent_1_logfile,agent_2,agent_2_won,agent_2_rounds,agent_2_secs,agent_2_err,agent_2_logfile,\n");
+ fprintf(stdout,
+ "%s,%i,%u,%f,%s,%s,%s,%i,%u,%f,%s,%s,\n",
+ stats.agent_1, stats.agent_1_won, stats.agent_1_rounds, stats.agent_1_secs, hexerrorstr(stats.agent_1_err), state.black_agent.logfile,
+ stats.agent_2, stats.agent_2_won, stats.agent_2_rounds, stats.agent_2_secs, hexerrorstr(stats.agent_2_err), state.white_agent.logfile);
+
+ return 0;
+}
+
+static u32
+try_parse_u32(char *src, s32 base, u32 *out)
+{
+ char *endptr = NULL;
+ u32 result = strtoul(src, &endptr, base);
+ if (*endptr || errno)
+ return false;
+
+ *out = result;
+
+ return true;
+}
+
+static void
+parse_args(s32 argc, char **argv)
+{
+ for (s32 i = 1; i < argc; i++) {
+ char *arg = argv[i];
+
+ if (arg[0] != '-') continue;
+
+ switch (arg[1]) {
+ case 'a':
+ args.agent_1 = argv[++i];
+ break;
+
+ case 'b':
+ args.agent_2 = argv[++i];
+ break;
+
+ case 'u':
+ if (arg[2] == 'a' && !try_parse_u32(argv[++i], 10, &args.agent_1_uid)) {
+ errlog("-ua takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ } else if (arg[2] == 'b' && !try_parse_u32(argv[++i], 10, &args.agent_2_uid)) {
+ errlog("-ub takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ } else if (arg[2] != 'a' && arg[2] != 'b') {
+ goto unknown_arg;
+ }
+ break;
+
+ case 'd': {
+ if (!try_parse_u32(argv[++i], 10, &args.board_dimensions)) {
+ errlog("-d takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ }
+ } break;
+
+ case 's': {
+ if (!try_parse_u32(argv[++i], 10, &args.game_secs)) {
+ errlog("-s takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ }
+ } break;
+
+ case 't': {
+ if (!try_parse_u32(argv[++i], 10, &args.thread_limit)) {
+ errlog("-t takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ }
+ } break;
+
+ case 'm': {
+ if (!try_parse_u32(argv[++i], 10, &args.mem_limit_mib)) {
+ errlog("-m takes a positive, unsigned integer argument, was given: '%s'\n",
+ argv[i]);
+ exit(EXIT_FAILURE);
+ }
+ } break;
+
+ case 'v':
+ args.verbose = true;
+ break;
+
+ case 'h':
+ usage(argv);
+ exit(EXIT_SUCCESS);
+ break;
+
+ default: {
+unknown_arg:
+ errlog("[server] Unknown argument: %s\n", &arg[1]);
+ usage(argv);
+ exit(EXIT_FAILURE);
+ } break;
+ }
+ }
+}
diff --git a/src/proto.c b/src/proto.c
@@ -0,0 +1,7 @@
+#include "hex.h"
+
+extern inline b32
+hex_msg_try_serialise(struct hex_msg const *msg, u8 out[static HEX_MSG_SZ]);
+
+extern inline b32
+hex_msg_try_deserialise(u8 buf[static HEX_MSG_SZ], struct hex_msg *out);
diff --git a/src/server.c b/src/server.c
@@ -0,0 +1,465 @@
+#include "hex.h"
+
+bool
+server_init(struct server_state *state)
+{
+ assert(state);
+
+ struct addrinfo hints = {
+ .ai_family = AF_UNSPEC,
+ .ai_socktype = SOCK_STREAM,
+ .ai_flags = AI_PASSIVE,
+ }, *addrinfo, *ptr;
+
+ int res;
+ if ((res = getaddrinfo("localhost", "0", &hints, &addrinfo))) {
+ errlog("[server] Failed to get address information: %s\n", gai_strerror(res));
+ goto error_without_socket;
+ }
+
+ for (ptr = addrinfo; ptr; ptr = ptr->ai_next) {
+ state->servfd = socket(ptr->ai_family, ptr->ai_socktype | SOCK_CLOEXEC, ptr->ai_protocol);
+ if (state->servfd == -1) continue;
+ if (bind(state->servfd, ptr->ai_addr, ptr->ai_addrlen) != -1) break;
+ close(state->servfd);
+ }
+
+ freeaddrinfo(addrinfo);
+
+ if (!ptr) {
+ errlog("[server] Failed to bind server socket\n");
+ goto error_without_socket;
+ }
+
+ state->serv_addrlen = sizeof state->serv_addr;
+ if (getsockname(state->servfd, (struct sockaddr *) &state->serv_addr, &state->serv_addrlen)) {
+ errlog("[server] Failed to get server socket addr\n");
+ goto error;
+ }
+
+ if ((res = getnameinfo((struct sockaddr *) &state->serv_addr, state->serv_addrlen,
+ state->serv_host, sizeof state->serv_host,
+ state->serv_port, sizeof state->serv_port,
+ NI_NUMERICHOST | NI_NUMERICSERV)) != 0) {
+ errlog("[server] Failed to get bound socket addr host and port\n");
+ goto error;
+ }
+
+ listen(state->servfd, 2);
+
+ dbglog("[server] Server socket is listening on %s:%s\n", state->serv_host, state->serv_port);
+
+ return true;
+
+error:
+ close(state->servfd);
+
+error_without_socket:
+ return false;
+}
+
+void
+server_free(struct server_state *state)
+{
+ assert(state);
+
+ close(state->servfd);
+}
+
+bool
+server_spawn_agent(struct server_state *state, struct agent_state *agent_state)
+{
+ assert(state);
+ assert(agent_state);
+
+ int fd;
+ if ((fd = mkstemp(agent_state->logfile)) != -1) {
+ fchmod(fd, HEX_AGENT_LOGFILE_MODE);
+
+ dbglog("[server] Created logfile '%s' for agent: '%s'\n",
+ agent_state->logfile, agent_state->agent);
+ } else {
+ dbglog("[server] Failed to create logfile '%s' for agent: '%s'\n",
+ agent_state->logfile, agent_state->agent);
+
+ strcpy(agent_state->logfile, "/dev/null");
+ }
+
+ pid_t child_pid = fork();
+
+ if (child_pid == 0) { /* child process, exec() agent */
+ pid_t pid = getpid();
+
+ dbglog("[server] Child process '%" PRIi32 "', setting uid\n", pid);
+
+ if (setuid(agent_state->agent_uid) == -1) {
+ perror("setuid");
+ exit(EXIT_FAILURE); /* fork()-d process can die without issue */
+ }
+
+ dbglog("[server] Child process '%" PRIi32 "', setting resource limits\n", pid);
+
+ struct rlimit limit;
+
+ limit.rlim_cur = limit.rlim_max = args.thread_limit;
+ prlimit(pid, RLIMIT_NPROC, &limit, NULL);
+
+ limit.rlim_cur = limit.rlim_max = args.mem_limit_mib * 1024 * 1024;
+ prlimit(pid, RLIMIT_DATA, &limit, NULL);
+
+ char *args[] = {
+ agent_state->agent,
+ state->serv_host,
+ state->serv_port,
+ NULL,
+ };
+
+ char *env[] = {
+ NULL,
+ };
+
+ dbglog("[server] Child process '%" PRIi32 "', exec()-ing agent: '%s'\n",
+ pid, agent_state->agent);
+
+ freopen("/dev/null", "rb", stdin);
+ freopen(agent_state->logfile, "wb", stdout);
+ freopen(agent_state->logfile, "wb", stderr);
+
+ if (execve(agent_state->agent, args, env)) {
+ perror("execve");
+ exit(EXIT_FAILURE); /* fork()-d process can die without issue */
+ }
+ } else if (child_pid == -1) { /* parent process, fork() error */
+ perror("fork");
+ errlog("[server] Failed to fork() to agent process: '%s'\n", agent_state->agent);
+ goto error;
+ }
+
+ /* parent process, fork() success */
+
+ /* accept() the agent socket */
+ struct pollfd pollfds[] = {
+ { .fd = state->servfd, .events = POLLIN, },
+ };
+
+ int ready = poll(pollfds, 1, HEX_AGENT_ACCEPT_TIMEOUT_MS);
+
+ if (ready == -1) {
+ perror("poll");
+ goto error;
+ } else if (ready == 0) {
+ errlog("[server] %s (%s) timed out during accept() period, assuming forfeit\n",
+ hexplayerstr(agent_state->player), agent_state->agent);
+ goto error;
+ }
+
+ int sockflags = SOCK_CLOEXEC;
+ agent_state->sockfd = accept4(state->servfd,
+ (struct sockaddr *) &agent_state->sock_addr,
+ &agent_state->sock_addrlen,
+ sockflags);
+
+ if (agent_state->sockfd == -1) {
+ perror("accept4");
+ goto error;
+ }
+
+ return true;
+
+error:
+ kill(0, SIGKILL);
+
+ int wpid, wstatus;
+ while ((wpid = wait(&wstatus)) > 0); /* wait for all children to die */
+
+ return false;
+}
+
+void
+server_wait_all_agents(struct server_state *state)
+{
+ assert(state);
+
+ int wpid, wstatus;
+ while ((wpid = wait(&wstatus)) > 0) {
+ dbglog("[server] Child process '%" PRIi32 "' returned code: %d\n",
+ wpid, WEXITSTATUS(wstatus));
+ }
+}
+
+static enum hex_error
+send_msg(struct agent_state *agent, struct hex_msg *msg, b32 force);
+
+static enum hex_error
+recv_msg(struct agent_state *agent, struct hex_msg *out, enum hex_msg_type *expected, size_t len);
+
+static enum hex_error
+play_round(struct server_state *state, size_t turn, enum hex_player *winner);
+
+void
+server_run(struct server_state *state, struct statistics *statistics)
+{
+ assert(state);
+
+ enum hex_error err;
+
+ enum hex_player winner;
+
+ /* setup common statistics */
+ statistics->agent_1 = state->black_agent.agent;
+ statistics->agent_2 = state->white_agent.agent;
+
+ /* send a start message to both agents, including all game parameters
+ */
+ struct hex_msg msg;
+ msg.type = HEX_MSG_START;
+ msg.data.start.board_size = args.board_dimensions;
+ msg.data.start.game_secs = args.game_secs;
+ msg.data.start.thread_limit = args.thread_limit;
+ msg.data.start.mem_limit_mib = args.mem_limit_mib;
+
+ msg.data.start.player = HEX_PLAYER_BLACK;
+ if ((err = send_msg(&state->black_agent, &msg, true))) goto forfeit_black;
+
+ msg.data.start.player = HEX_PLAYER_WHITE;
+ if ((err = send_msg(&state->white_agent, &msg, true))) goto forfeit_white;
+
+ size_t round = 0;
+ while ((err = play_round(state, round++, &winner)) == HEX_ERROR_OK);
+
+ msg.type = HEX_MSG_END;
+ msg.data.end.winner = winner;
+
+ send_msg(&state->black_agent, &msg, true);
+ send_msg(&state->white_agent, &msg, true);
+
+ /* calculate game statistics
+ */
+ statistics->agent_1_won = state->black_agent.player == winner;
+ statistics->agent_2_won = state->white_agent.player == winner;
+
+ statistics->agent_1_rounds = (round + 1) / 2;
+ statistics->agent_2_rounds = round / 2;
+
+ statistics->agent_1_secs = state->black_agent.timer.tv_sec
+ + state->black_agent.timer.tv_nsec / (f32) NANOSECS;
+ statistics->agent_2_secs = state->white_agent.timer.tv_sec
+ + state->white_agent.timer.tv_nsec / (f32) NANOSECS;
+
+ if (winner == HEX_PLAYER_BLACK) {
+ statistics->agent_1_err = HEX_ERROR_OK;
+ statistics->agent_2_err = err;
+ } else {
+ statistics->agent_1_err = err;
+ statistics->agent_2_err = HEX_ERROR_OK;
+ }
+
+ return;
+
+forfeit_black:
+ statistics->agent_1_won = false;
+ statistics->agent_2_won = true;
+
+ statistics->agent_1_rounds = statistics->agent_2_rounds = 0;
+
+ statistics->agent_1_secs = state->black_agent.timer.tv_sec
+ + state->black_agent.timer.tv_nsec / (f32) NANOSECS;
+ statistics->agent_2_secs = state->white_agent.timer.tv_sec
+ + state->white_agent.timer.tv_nsec / (f32) NANOSECS;
+
+ return;
+
+forfeit_white:
+ statistics->agent_1_won = true;
+ statistics->agent_2_won = false;
+
+ statistics->agent_1_rounds = statistics->agent_2_rounds = 0;
+
+ statistics->agent_1_secs = state->black_agent.timer.tv_sec
+ + state->black_agent.timer.tv_nsec / (f32) NANOSECS;
+ statistics->agent_2_secs = state->white_agent.timer.tv_sec
+ + state->white_agent.timer.tv_nsec / (f32) NANOSECS;
+
+ return;
+}
+
+static enum hex_error
+send_msg(struct agent_state *agent, struct hex_msg *msg, b32 force)
+{
+ assert(agent);
+ assert(msg);
+
+ size_t nbytes_sent = 0;
+
+ u8 buf[HEX_MSG_SZ];
+ if (!hex_msg_try_serialise(msg, buf)) return HEX_ERROR_BAD_MSG;
+
+ struct pollfd pollfd = { .fd = agent->sockfd, .events = POLLOUT, };
+
+ struct timespec start, end, diff, temp;
+ if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) {
+ perror("clock_gettime");
+ return HEX_ERROR_SERVER;
+ }
+
+ int res;
+ while (nbytes_sent < ARRLEN(buf) && (res = ppoll(&pollfd, 1, force ? NULL : &agent->timer, NULL)) > 0) {
+ ssize_t curr = send(pollfd.fd, buf + nbytes_sent, ARRLEN(buf) - nbytes_sent, 0);
+
+ if (curr <= 0) /* connection closed or error */
+ return HEX_ERROR_DISCONNECT;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &end) < 0) {
+ perror("clock_gettime");
+ return HEX_ERROR_SERVER;
+ }
+
+ difftimespec(&end, &start, &diff);
+ difftimespec(&agent->timer, &diff, &temp);
+
+ start = end;
+ agent->timer = temp;
+
+ nbytes_sent += curr;
+ }
+
+ if (res == 0) { /* timeout */
+ dbglog("[server] Timeout when sending message to %s\n",
+ hexplayerstr(agent->player));
+ return HEX_ERROR_TIMEOUT;
+ }
+
+ if (res == -1) {
+ perror("ppoll");
+ return HEX_ERROR_SERVER;
+ }
+
+ return HEX_ERROR_OK;
+}
+
+static enum hex_error
+recv_msg(struct agent_state *agent, struct hex_msg *out, enum hex_msg_type *expected, size_t len)
+{
+ assert(agent);
+ assert(out);
+ assert(expected);
+
+ size_t nbytes_received = 0;
+
+ u8 buf[HEX_MSG_SZ];
+
+ struct pollfd pollfd = { .fd = agent->sockfd, .events = POLLIN, };
+
+ struct timespec start, end, diff, temp;
+ if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) {
+ perror("clock_gettime");
+ return HEX_ERROR_SERVER;
+ }
+
+ int res;
+ while (nbytes_received < ARRLEN(buf) && (res = ppoll(&pollfd, 1, &agent->timer, NULL)) > 0) {
+ ssize_t curr = recv(pollfd.fd, buf + nbytes_received, ARRLEN(buf) - nbytes_received, 0);
+
+ if (curr <= 0) /* connection closed or error */
+ return HEX_ERROR_DISCONNECT;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &end) < 0) {
+ perror("clock_gettime");
+ return HEX_ERROR_SERVER;
+ }
+
+ difftimespec(&end, &start, &diff);
+ difftimespec(&agent->timer, &diff, &temp);
+
+ start = end;
+ agent->timer = temp;
+
+ nbytes_received += curr;
+ }
+
+ if (res == 0) { /* timeout */
+ dbglog("[server] Timeout while receiving message from %s\n",
+ hexplayerstr(agent->player));
+ return HEX_ERROR_TIMEOUT;
+ }
+
+ if (res == -1) {
+ perror("ppoll");
+ return HEX_ERROR_SERVER;
+ }
+
+ if (!hex_msg_try_deserialise(buf, out)) return HEX_ERROR_BAD_MSG;
+
+ for (size_t i = 0; i < len; i++) {
+ if (out->type == expected[i]) return HEX_ERROR_OK;
+ }
+
+ return HEX_ERROR_BAD_MSG;
+}
+
+static enum hex_error
+play_round(struct server_state *state, size_t turn, enum hex_player *winner)
+{
+ assert(state);
+ assert(winner);
+
+ enum hex_error err;
+
+ struct agent_state *agents[] = {
+ [HEX_PLAYER_BLACK] = &state->black_agent,
+ [HEX_PLAYER_WHITE] = &state->white_agent,
+ };
+
+ struct agent_state *player = agents[turn % 2];
+ struct agent_state *opponent = agents[(turn + 1) % 2];
+
+ dbglog("[server] round %zu, to-play: %s, opponent: %s\n",
+ turn, hexplayerstr(player->player), hexplayerstr(opponent->player));
+
+ /* on the first turn for white (i.e. turn 1 when 0-addressed), white
+ * can respond with either a MSG_MOVE, or a MSG_SWAP, but for all
+ * other turns (for both black and white), only a MSG_MOVE can be
+ * played, thus implementing the swap rule.
+ */
+ enum hex_msg_type expected_msg_types[] = { HEX_MSG_MOVE, HEX_MSG_SWAP, };
+ size_t expected_msg_types_len = (turn == 1) ? 2 : 1;
+
+ struct hex_msg msg;
+
+ if ((err = recv_msg(player, &msg, expected_msg_types, expected_msg_types_len))) {
+ *winner = opponent->player;
+ return err;
+ }
+
+ switch (msg.type) {
+ case HEX_MSG_MOVE:
+ dbglog("[server] %s made move (%u,%u)\n",
+ hexplayerstr(player->player), msg.data.move.board_x, msg.data.move.board_y);
+
+ if (!board_play(state->board, player->player, msg.data.move.board_x, msg.data.move.board_y)) {
+ *winner = opponent->player;
+ return HEX_ERROR_BAD_MOVE;
+ }
+
+ if (board_completed(state->board, winner)) {
+ board_print(state->board);
+ return HEX_ERROR_GAME_OVER;
+ }
+ break;
+
+ case HEX_MSG_SWAP:
+ dbglog("[server] %s swapped board\n", hexplayerstr(player->player));
+
+ board_swap(state->board);
+ break;
+ }
+
+ if ((err = send_msg(opponent, &msg, false))) {
+ *winner = player->player;
+ return err;
+ }
+
+ board_print(state->board);
+
+ return HEX_ERROR_OK;
+}
diff --git a/src/utils.c b/src/utils.c
@@ -0,0 +1,16 @@
+#include "hex.h"
+
+extern inline char const *
+hexerrorstr(enum hex_error val);
+
+extern inline void
+errlog(char *fmt, ...);
+
+extern inline void
+dbglog(char *fmt, ...);
+
+extern inline void
+difftimespec(struct timespec *restrict lhs, struct timespec *restrict rhs, struct timespec *restrict out);
+
+extern inline char const *
+hexplayerstr(enum hex_player val);
diff --git a/test-server.sh b/test-server.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+AGENT1="$1"
+AGENT2="$2"
+shift 2
+
+./hex-server -a $AGENT1 -ua 1001 -b $AGENT2 -ub 1002 -t 16 $@
diff --git a/tournament-host.py b/tournament-host.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+
+import argparse
+import asyncio
+import csv
+import itertools
+import os
+import pwd
+import re
+import sys
+import time
+
+
+HEX_SERVER_PROGRAM = os.path.abspath('hex-server')
+
+HEX_AGENT_USERS = [
+ ent.pw_name for ent in pwd.getpwall() if re.match('hex-agent-\d+$', ent.pw_name)
+]
+
+HEX_AGENT_UIDS = [
+ str(pwd.getpwnam(user).pw_uid) for user in HEX_AGENT_USERS
+]
+
+
+arg_parser = argparse.ArgumentParser(prog='tournament-host',
+ description='Helper script for hosting Hex tournaments.')
+
+arg_parser.add_argument('schedule_file',
+ type=argparse.FileType('r'),
+ help='schedule file of agent-pairs to define the tournament schedule')
+arg_parser.add_argument('output_file',
+ type=argparse.FileType('w'),
+ help='output file to write final tournament statistics to')
+arg_parser.add_argument('--concurrent-matches',
+ default=1,
+ choices=[1,2,3,4,5,6,7,8],
+ type=int,
+ help='number of matches that can occur concurrently')
+arg_parser.add_argument('-d', '--dimension',
+ default=11,
+ type=int,
+ help='size of the hex board')
+arg_parser.add_argument('-s', '--seconds',
+ default=300,
+ type=int,
+ help='per-agent game timer length (seconds)')
+arg_parser.add_argument('-t', '--threads',
+ default=4,
+ type=int,
+ help='per-agent thread hard-limit')
+arg_parser.add_argument('-m', '--memory',
+ default=1024,
+ type=int,
+ help='per-agent memory hard-limit (MiB)')
+arg_parser.add_argument('-v', '--verbose',
+ action='store_true',
+ help='enabling verbose logging for the server')
+
+
+def log(string):
+ print(f'[tournament-host] {string}')
+
+
+async def game(sem, args, agent_pair, uid_pool):
+ '''
+ Plays a single game using the hex-server program, between the given pair
+ of user agents and taking 2 uids from the current pool.
+ '''
+
+ await sem.acquire() # wait until we can play another (potentially concurrent) game
+
+ agent1, agent2 = agent_pair
+ agent1_uid = uid_pool.pop(0)
+ agent2_uid = uid_pool.pop(0)
+
+ log(f'Starting game between {agent1} (uid: {agent1_uid}) and {agent2} (uid: {agent2_uid}) ...')
+
+ proc = await asyncio.create_subprocess_exec(
+ HEX_SERVER_PROGRAM,
+ '-a', agent1, '-ua', agent1_uid,
+ '-b', agent2, '-ub', agent2_uid,
+ '-d', str(args.dimension),
+ '-s', str(args.seconds),
+ '-t', str(args.threads),
+ '-m', str(args.memory),
+ '-v' if args.verbose else '',
+ stdout=asyncio.subprocess.PIPE)
+
+ stdout, _ = await proc.communicate()
+ output = stdout.decode()
+ csv_rows = [
+ [e.strip() for e in row.split(',') if len(e)] for row in output.split('\n') if len(row)
+ ]
+
+ uid_pool.append(agent1_uid)
+ uid_pool.append(agent2_uid)
+
+ sem.release()
+
+ return dict(zip(*csv_rows))
+
+
+async def tournament(args, schedule):
+ '''
+ Play an entire tournament, using the given parsed args and the given
+ agent-pair schedule.
+ '''
+
+ sem = asyncio.BoundedSemaphore(args.concurrent_matches) # limits concurrent matches
+ uid_pool = [uid for uid in HEX_AGENT_UIDS]
+
+ log(f'Starting tournament with {args.concurrent_matches} concurrent games...')
+
+ start = time.time()
+
+ tasks = [asyncio.create_task(game(sem, args, pair, uid_pool)) for pair in schedule]
+ results = await asyncio.gather(*tasks)
+
+ end = time.time()
+
+ log(f'Finished tournament in {end - start:.03} seconds')
+
+ return results
+
+
+def main():
+ if not os.path.exists(HEX_SERVER_PROGRAM):
+ print(f'Failed to find server executable: {HEX_SERVER_PROGRAM}. Ensure it exists (run Make?) before attempting a tournament', file=sys.stderr)
+ quit(1)
+
+ args = arg_parser.parse_args()
+
+ schedule = []
+ with args.schedule_file as f:
+ for raw_line in f.read().strip().split('\n'):
+ line = raw_line.strip()
+ if len(line) == 0: continue # skip empty lines
+ if line[0] == '#': continue # skip commented lines
+ elif ',' in line: schedule.append(line.split(',')[:2])
+
+ results = asyncio.run(tournament(args, schedule))
+
+ fields = [
+ 'agent_1', 'agent_1_won', 'agent_1_rounds', 'agent_1_secs', 'agent_1_err', 'agent_1_logfile',
+ 'agent_2', 'agent_2_won', 'agent_2_rounds', 'agent_2_secs', 'agent_2_err', 'agent_2_logfile',
+ ]
+
+ fields_hdr = ','.join(fields)
+
+ with args.output_file as f:
+ f.write(f'game,{fields_hdr},\n')
+ for i, res in enumerate(results):
+ fields_row = ','.join(map(lambda f: str(res[f]), fields))
+ f.write(f'{i},{fields_row},\n')
+
+
+if __name__ == '__main__':
+ main()
+