hex

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

commit 259e55f5690be7df58b9d7ff3f830488b82b53a6
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Sun, 13 Aug 2023 23:27:34 +0000

Initial commit

Diffstat:
A.editorconfig | 34++++++++++++++++++++++++++++++++++
A.gitignore | 6++++++
AMakefile | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagents/example_c_agent/.gitignore | 2++
Aagents/example_c_agent/Makefile | 38++++++++++++++++++++++++++++++++++++++
Aagents/example_c_agent/include/agent.h | 27+++++++++++++++++++++++++++
Aagents/example_c_agent/run.sh | 3+++
Aagents/example_c_agent/src/agent.c | 444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagents/example_cpp_agent/.gitignore | 2++
Aagents/example_cpp_agent/Makefile | 38++++++++++++++++++++++++++++++++++++++
Aagents/example_cpp_agent/include/agent.hpp | 33+++++++++++++++++++++++++++++++++
Aagents/example_cpp_agent/run.sh | 3+++
Aagents/example_cpp_agent/src/agent.cpp | 333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagents/example_java_agent/.gitignore | 2++
Aagents/example_java_agent/MANIFEST.txt | 3+++
Aagents/example_java_agent/Makefile | 36++++++++++++++++++++++++++++++++++++
Aagents/example_java_agent/run.sh | 15+++++++++++++++
Aagents/example_java_agent/src/Agent.java | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagents/example_java_agent/src/hex/HexMessageType.java | 14++++++++++++++
Aagents/example_java_agent/src/hex/HexPlayer.java | 20++++++++++++++++++++
Aagents/example_python3_agent/.gitignore | 1+
Aagents/example_python3_agent/Makefile | 7+++++++
Aagents/example_python3_agent/agent.py | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aagents/example_python3_agent/run.sh | 3+++
Ainclude/hex.h | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/hex/proto.h | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/hex/types.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aschedule.txt | 11+++++++++++
Asrc/board.c | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/hex.c | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/proto.c | 7+++++++
Asrc/server.c | 465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/utils.c | 16++++++++++++++++
Atest-server.sh | 7+++++++
Atournament-host.py | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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() +