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
- 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.
- Call
cy_session_next. OnCY_EVENT_HELLO, read match-wide facts or configure your tracker, then callcy_session_sendfor ready. - On
CY_EVENT_STATE, read observations, choose controls and callcy_session_sendexactly once for the act. Calls merge fields within that turn; sending clears pending controls for the next turn. - 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.