Write a robot with the C API

Include clunkyard.h, open one robot, and let its session read the match. Your code chooses controls; the core handles protocol 2, observations, tracking, shot solutions and action encoding.

The installed header is /opt/clunkyard/sdk/include/clunkyard.h. Its source copy, the generated C reference and API name table describe the callable surface. This guide explains how to use that surface safely. The header and layouts come from the API model and its generated JSON description; change those inputs and run make api when maintaining a binding.

Build and run

With a native SDK and a C compiler installed, save the example below as bot.c and build it:

SDK=/opt/clunkyard/sdk
mkdir -p .clunkyard
cc -O2 -std=c17 -I"$SDK/include" bot.c \
  "$SDK/lib/libclunkyard.a" -lpthread -ldl -lm -o .clunkyard/bot

This is the Linux native build used by the arena's C profile. The core is linked into the executable; the runtime needs no compiler. Hosted builds supply these paths automatically. Source-built SDKs may use another prefix; keep the header and library from the same SDK installation.

Feed a recorded protocol stream to smoke-test the executable:

.clunkyard/bot < protocol/v2/fixtures/2d-classic.ndjson > replies.ndjson

Run that command from the repository root, or give an absolute fixture path. It checks a short input stream. Use local trials to play real matches and inspect decisions.

A complete robot

This robot searches until it sees a precise contact, then follows the core's shot solution. A failed tactic call is logged before the session sends the controls already set. An internal core fault produces the core's empty act.

#include "clunkyard.h"
#include <stdio.h>

#define CHECK(call) do { int32_t s_ = (call); if (s_ < 0) return s_; } while (0)

static void report(cy_robot *robot, int32_t status) {
    char text[1024];
    uint32_t length = 0;
    fprintf(stderr, "core status %d", status);
    int32_t result = cy_last_error(robot, text, sizeof text, &length);
    if (result == CY_OK) {
        fputs(": ", stderr);
        fwrite(text, 1, length, stderr);
    }
    fputc('\n', stderr);
}

static int32_t take_turn(cy_robot *robot) {
    cy_contact target = {0};
    int32_t found = cy_sensors_nearest(robot, &target);
    if (found < 0) return found;
    if (found == CY_NONE) {
        double heading = 0;
        CHECK(cy_me_heading(robot, &heading));
        CHECK(cy_drive_throttle(robot, 0.5));
        CHECK(cy_drive_turn(robot, 0.33));
        CHECK(cy_scanner_heading(robot, heading));
        CHECK(cy_scanner_arc(robot, 40));
        CHECK(cy_status_text(robot, "SEARCH", 6));
        return CY_OK;
    }

    double power = 0;
    CHECK(cy_gun_power_for_flight_time(robot, target.range, 1, &power));
    cy_solution aim = {0};
    int32_t solved = cy_gun_lead(robot, &target, power, &aim);
    if (solved < 0) return solved;
    if (solved == CY_NONE) {
        aim.heading = target.bearing;
        aim.pitch = target.elevation;
    }
    CHECK(cy_gun_aim_heading(robot, aim.heading));
    CHECK(cy_gun_aim_pitch(robot, aim.pitch));
    int32_t can_fire = 0;
    CHECK(cy_gun_can_fire(robot, power, 10, &can_fire));
    if (can_fire) {
        CHECK(cy_gun_fire(robot));
        CHECK(cy_gun_power(robot, power));
    }
    CHECK(cy_drive_throttle(robot, 0.5));
    CHECK(cy_scanner_heading(robot, target.bearing));
    CHECK(cy_scanner_arc(robot, 10));
    CHECK(cy_status_text(robot, "TRACK", 5));
    CHECK(cy_status_value(robot, target.range));
    return CY_OK;
}

int main(void) {
    cy_robot *robot = NULL;
    int32_t status = cy_open(CY_API_MAJOR, CY_API_MINOR,
                            "example-c", 9,
                            CY_API_VERSION, sizeof CY_API_VERSION - 1,
                            &robot);
    if (status != CY_OK) {
        fprintf(stderr, "cannot open core: %d\n", status);
        return 1;
    }
    int result = 0;
    for (;;) {
        int32_t event = CY_EVENT_NONE;
        status = cy_session_next(robot, &event);
        if (status == CY_E_INTERNAL) {
            report(robot, status);
            status = cy_session_send(robot);
            if (status == CY_OK) continue;
        }
        if (status < 0) {
            report(robot, status);
            result = 1;
            break;
        }
        if (event == CY_EVENT_END) break;
        if (event == CY_EVENT_STATE) {
            status = take_turn(robot);
            if (status < 0) report(robot, status);
        }
        status = cy_session_send(robot);
        if (status < 0) {
            report(robot, status);
            result = 1;
            break;
        }
    }
    cy_close(robot);
    return result;
}

Version negotiation

The core API version, currently 2.0.0, is separate from wire protocol 2 and WebAssembly ABI 4.

cy_version(&major, &minor, &patch) reports the loaded library's API version. cy_open and cy_open_memory take the API major and minimum minor required by the caller. A different major or a newer requested minor returns CY_E_VERSION. Patch is reported but is not an open argument. The binding name and binding-version strings identify the SDK in the ready reply; they do not select a wire protocol or an older implementation.

Exports involving versioned records have names such as cy_sensors_nearest_v1 and cy_contact_v1. The header's plain aliases select the current record version. Bindings that declare their own FFI layouts must pair the versioned function with its matching record. Use the installed header's aliases when compiling ordinary C source.

Handle and output ownership

cy_open creates an opaque cy_robot *; initialize your output pointer to NULL and use it only after CY_OK. Close it once with cy_close. cy_close(NULL) is allowed. A handle belongs to one thread at a time. The core starts no threads and calls no user callbacks.

Every output buffer and record belongs to the caller. A successful query copies its result into that memory; it gives you no pointer into core state. Copied records remain valid after the next tick or after closing the handle, but describe the observation from which they were obtained. Input text and records need stay valid only during their call.

Supply valid, suitably aligned memory for every pointer and enough storage for the capacity you declare. Native C cannot check whether an arbitrary address really points to a live allocation. Use the header's ordinary C layout: double, int32_t, uint32_t and nested structs. Boolean and enum slots are 32-bit integers. Do not add packing directives or replace these with C bool or platform-sized long. Reserved fields are part of the layout; zero-initialize records you construct.

Text and lists

Text is UTF-8 with an explicit byte length. Input lengths exclude a terminating NUL. Output lengths exclude it too: the core adds a NUL only when capacity exceeds the text's byte length. Use the returned length with fwrite or an equivalent length-aware operation. Character counts and strlen are unsuitable when text can contain an embedded NUL.

For text, capacity is bytes. For a list, capacity is records of its declared type. CY_SHORT writes the required length or count and leaves the text or record array untouched. Resize and retry the same query before advancing the session. For example, a cy_robot_id buffer needs the returned byte count, plus one if your own code wants a trailing NUL. cy_session_reply keeps a short-buffer reply queued until a successful copy takes it.

CY_NONE means an optional result is absent and leaves its value output untouched. Never read a value merely because status is non-negative: distinguish CY_OK, CY_NONE and CY_SHORT first.

Status and errors

Status Meaning and next step
CY_OK The call succeeded; its output is available.
CY_NONE An optional result is absent.
CY_SHORT Grow the text/list buffer using the returned required size.
CY_E_NOT_FINITE Replace a NaN or infinity argument.
CY_E_RANGE Correct an argument outside the operation's range.
CY_E_ARGUMENT Correct an invalid argument, enum or text encoding.
CY_E_PHASE Call in the appropriate session phase; avoid a second reply.
CY_E_VERSION Use a compatible header/library API pair.
CY_E_INTERNAL An internal core or I/O failure occurred; follow the session rules below.

Treat an unknown negative status as a generic error. cy_last_error copies the handle's most recent error message using the same text-buffer contract. Read it promptly. Stateless angle functions have no handle on which to keep an error message, so their status is the diagnostic available to the caller.

Session and stdio lifecycle

  1. Open the stdio session once, before printing to stdout. It reserves the protocol output descriptor and redirects descriptor 1 to stderr. This is process-wide; closing the handle does not restore stdout. Keep logs on stderr and let the session own stdin and protocol output.
  2. Call cy_session_next. On CY_EVENT_HELLO, read match-wide facts or configure your tracker, then call cy_session_send for ready.
  3. On CY_EVENT_STATE, read observations, choose controls and call cy_session_send exactly once for the act. Calls merge fields within that turn; sending clears pending controls for the next turn.
  4. On CY_EVENT_END, close the handle and exit. EOF ends input; unknown message types and blank lines are skipped by the session.

A panic on a core handle is caught at the native ABI. Its state answers with an empty act, tracking resets at the next state, and three core faults poison the handle so later states answer with empty acts. If cy_session_next returns CY_E_INTERNAL, attempt cy_session_send and continue only if it succeeds. A failed send is terminal. Negative statuses such as malformed input should be reported and handled explicitly; they are not missing observations.

cy_session_forfeit sends a deliberate forfeit and marks the session answered. Do not follow it with another send; the next event is end. cy_session_log writes a diagnostic line to stderr.

For tests or a runtime that owns its event loop, use cy_open_memory. cy_session_feed queues a protocol line; next consumes it and send queues a reply. Drain replies with cy_session_reply until CY_NONE. An empty input queue yields CY_EVENT_END, so enqueue the intended input before advancing. Memory sessions do not redirect process stdout and are the appropriate route for multiple independent test robots in one process.

Native C and WebAssembly C

The native c profile links libclunkyard.a and owns a session handle. The c-wasm profile includes the ABI 4 header and runs under the platform's Wasmtime host. Its imports omit the handle: one guest instance is one robot. It exports memory, clunkyard_start and clunkyard_turn; CY_ROBOT(start, turn) supplies the two functions. The host owns protocol I/O and bounds-checks guest-memory pointers. There is no WASI environment.

Use the WebAssembly reference for ABI 4 builds, imports and fuel limits. Its fixed guest layouts and host imports are a separate calling convention from a native shared-library FFI. Supplying a native header or linking the native archive into a guest does not build an ABI 4 robot.

Download this page as Markdown