# 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](../../sdk/core/include/clunkyard.h), the generated
[C reference](c.md) and [API name table](names.md) describe the callable
surface. This guide explains how to use that surface safely. The header
and layouts come from [the API model](../../protocol/v2/api.js) and its
generated [JSON description](../../protocol/v2/api.json); 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:

```sh
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:

```sh
.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](../local-trials.md) 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.

```c
#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](../../sdk/wasm/abi4/clunkyard.h)
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](wasm.md) 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.
