KN-86 Deckline — Cartridge Grammar Specification
Design Intent
Section titled “Design Intent”The cartridge grammar is a C-based domain-specific framework — not a language you interpret, but a set of structures, macros, and conventions that make C read like the Deckline’s interaction model while compiling to native ARM code on the Pi Zero 2 W (and native x86/ARM64 code in the desktop emulator). No interpreter overhead. No garbage collector. Full speed.
The framework enforces the interaction paradigm at the authoring level: every cell type declares what happens when each key is pressed in its context. The cell definition IS the game design document.
Capability model context: In the capability model, cartridges do not own their main loop. The nOSh runtime (nOSh) is the orchestrator — it runs the mission board, accepts bids, initiates phases, and calls into cartridge handlers when a phase is active. The cartridge grammar provides the macros for registering capabilities (CAPABILITY_DECLARE), defining mission templates (TEMPLATE_DEFINE), implementing phase handlers (PHASE_HANDLER), and contributing Cipher domain vocabulary (CIPHER_DOMAIN). The cell definitions, event handlers, and standard library functions described below remain unchanged — what changes is the ownership model.
Architecture: Three Layers
Section titled “Architecture: Three Layers”┌─────────────────────────────────────────────────────────┐│ CARTRIDGE SOURCE (developer-authored) ││ #include "nosh_cart.h" ││ Uses: DECK_TITLE, CAPABILITY_DECLARE, CELL_DEFINE, ││ TEMPLATE_DEFINE, PHASE_HANDLER, CIPHER_DOMAIN, ││ ON_CAR, ON_EVAL, etc. │├─────────────────────────────────────────────────────────┤│ CARTRIDGE RUNTIME (nosh_runtime.c) — NOSH RUNTIME CODE ││ Input dispatch, cell pool, nav stack, quote/lambda ││ Phase handler dispatch, template parser │├─────────────────────────────────────────────────────────┤│ STANDARD LIBRARY (nosh_stdlib.c) ││ drill_into(), next_sibling(), trace_advance(), etc. │├─────────────────────────────────────────────────────────┤│ nOSh NOSH RUNTIME (ORCHESTRATOR) ││ Mission board, phase chain, economy, Cipher voice ││ Display, sound, input, save, link APIs │└─────────────────────────────────────────────────────────┘All layers are C. The cartridge compiles to native ARM (for the Pi Zero 2 W) or x86/ARM64 (for the desktop emulator) depending on the build target. Same source, both targets: make device or make emulator. The runtime (nosh_runtime.c) is nOSh runtime code — it bridges the nOSh runtime’s mission board and the cartridge’s event handlers.
Firmware vtable: Cartridge code calls nOSh runtime functions through a NoshAPI function table passed at init time. The nosh_cart.h macros hide this — nosh_text_puts(...) expands to _nosh_api->text_puts(...). This decouples nOSh runtime versions from cartridge binaries on both targets. On the desktop emulator (where cartridge code loads via dlopen), the vtable is populated with direct function pointers. See KN-86-Capability-Model-Spec.md § Cartridge Code Execution Model for the full vtable definition.
Layer 1: nosh_cart.h — The Authoring Surface
Section titled “Layer 1: nosh_cart.h — The Authoring Surface”This is the header a cartridge developer includes. It provides macros that create a declarative, readable authoring grammar while expanding to efficient C structures and function pointers.
Title Declaration
Section titled “Title Declaration”#include "nosh_cart.h"
DECK_TITLE("ICE_BREAKER", "ICE Breaker v1.3", "NETWORK INTRUSION")Expands to: a CartridgeHeader struct populated with the program name, display name, and module class, plus a registration call that nOSh invokes on cartridge load.
Cell Type Definition
Section titled “Cell Type Definition”CELL_TYPE(contract) { CELL_FIELDS { char name[24]; uint8_t threat; uint16_t payout; uint32_t network_seed; };
ON_CAR { drill_into_network(self, self->network_seed); } ON_CDR { next_sibling(); } ON_EVAL { accept_contract(self); } ON_INFO { show_contract_details(self); } ON_QUOTE { quote_cell(self); } ON_BACK { navigate_back(); }
ON_DISPLAY { nosh_text_puts(0, 0, "CONTRACT: "); nosh_text_puts(10, 0, self->name); nosh_text_puts(0, 1, "THREAT: "); draw_threat_bar(self->threat, 8, 1); nosh_text_puts(0, 2, "PAYOUT: "); nosh_text_printf(8, 2, "%d CR", self->payout); }}How the Macros Work
Section titled “How the Macros Work”// CELL_TYPE(name) expands to:typedef struct cell_##name cell_##name;struct cell_##name { CellBase base; // Common cell header (type, flags, id, nav links) // ... CELL_FIELDS content inserted here ...};static const CellHandlers cell_##name##_handlers;
// ON_CAR { ... } expands to:// A static function definition:static void cell_##name##_on_car(cell_##name *self)// ... with the braced body ...// And a slot assignment in the handlers struct:// .on_car = cell_##name##_on_car
// ON_DISPLAY { ... } expands to:// A render function called by the display manager when this cell has focusstatic void cell_##name##_render(cell_##name *self)Key Handler Slots
Section titled “Key Handler Slots”Every cell type can define handlers for any combination of the 14 function keys plus the numpad:
// Lisp Primitives (amber legends)ON_CAR { /* Enter/examine/drill in */ }ON_CDR { /* Next/traverse/advance */ }ON_CONS { /* Attach/combine/construct */ }ON_NIL { /* Discard/clear/cancel */ }ON_ATOM { /* Test if current cell is a leaf (no children) */ }ON_EQ { /* Compare two quoted elements, report match/delta */ }
// Action Verbs (white legends)ON_EVAL { /* Execute/confirm/commit */ }ON_QUOTE { /* Bookmark/defer/hold */ }ON_LAMBDA { /* Custom lambda behavior (rare — firmware handles recording) */ }ON_APPLY { /* Deploy/use tool against target */ }
// System (gray legends)ON_BACK { /* Return to parent */ }ON_INFO { /* Inspect/scan/details */ }ON_LINK { /* Initiate link action */ }ON_SYS { /* System menu. Tap = menu. Hold 2s = abort/cancel/hard disconnect */ }
// NumpadON_NUMPAD(digit) { /* Handle numeric input */ }
// RenderingON_DISPLAY { /* Draw this cell's screen representation */ }
// LifecycleON_ENTER { /* Called when cursor enters this cell */ }ON_EXIT { /* Called when cursor leaves this cell */ }Unimplemented handlers default to sensible behavior:
ON_CARwith no handler: attemptdrill_into(self)— if self has children, navigate to first child; if not, beep (error sound)ON_CDRwith no handler:next_sibling()— move to next element in current listON_BACKwith no handler:navigate_back()— pop navigation stackON_ATOMwith no handler: returnsis_leaf(self)— true if cell has no children, false otherwiseON_EQwith no handler: compares current cell with first quoted element — reports match or deltaON_QUOTEwith no handler:quote_cell(self)— standard firmware quote behaviorON_SYSwith no handler: SYS tap = system menu, SYS hold 2s = abort/cancel (replaces old ESC)- All others with no handler:
nosh_sfx_error()— key not active in this context
This means a minimal cell type with NO handlers still supports basic list navigation. CAR/CDR/BACK just work. The developer only writes handlers for keys that do something domain-specific.
LAMBDA Conditional Behavior
Section titled “LAMBDA Conditional Behavior”When ATOM or EQ is pressed during a LAMBDA recording, it inserts a conditional branch point. If the test fails during playback, the LAMBDA skips to the next step or aborts. This allows recorded sequences to adapt to cell state without manual intervention — for example, a LAMBDA can test is_leaf(self) and branch accordingly, or compare two cells and proceed conditionally.
Cell Instance Creation
Section titled “Cell Instance Creation”// Static cell (defined at compile time, typical for templates)CELL_INSTANCE(contract, meridian_extract) { .name = "MERIDIAN EXTRACT", .threat = 4, .payout = 800, .network_seed = 0xA7F3,};
// Dynamic cell (created at runtime from procedural generation)cell_contract *c = spawn_cell(contract);strncpy(c->name, generated_name, 24);c->threat = lfsr_range(2, 6);c->payout = c->threat * lfsr_range(100, 200);c->network_seed = lfsr_next();List Construction
Section titled “List Construction”// Build a contract list from procedural generationCellList *board = list_create();for (int i = 0; i < contract_count; i++) { cell_contract *c = spawn_cell(contract); generate_contract(c, deck_state->reputation); list_push(board, (CellBase *)c);}
// Set as the current navigable listset_root(board);// Operator now sees the first contract; CDR scrolls through themMission Definition
Section titled “Mission Definition”MISSION_DEFINE("MERIDIAN EXTRACT") { .class = MISSION_NETWORK | MISSION_CRYPTO, .threat_range = {2, 5}, .phases = 2,
PHASE(1, "NETWORK INTRUSION") { .requires = MODULE_ICE_BREAKER, .generator = generate_intrusion_network, .objective = OBJECTIVE_EXTRACT, }
PHASE(2, "DECRYPT PAYLOAD") { .requires = MODULE_SIGNAL, .generator = generate_cipher_puzzle, .objective = OBJECTIVE_DECRYPT, }
.payout_formula = mission_payout_standard,}Layer 2: nosh_runtime.c — The Dispatcher
Section titled “Layer 2: nosh_runtime.c — The Dispatcher”Framework code written once, linked into every cartridge. The cartridge developer never touches this.
Core Loop
Section titled “Core Loop”void nosh_runtime_tick(void) { // 1. Poll input queue KeyEvent ev; while (input_dequeue(&ev)) { // 2. Resolve current cell CellBase *current = nav_stack_peek(); if (!current) continue;
// 3. Dispatch to handler const CellHandlers *handlers = current->handlers; switch (ev.key) { case KEY_CAR: CALL_HANDLER(handlers->on_car, current); break; case KEY_CDR: CALL_HANDLER(handlers->on_cdr, current); break; case KEY_CONS: CALL_HANDLER(handlers->on_cons, current); break; case KEY_NIL: CALL_HANDLER(handlers->on_nil, current); break; case KEY_ATOM: CALL_HANDLER(handlers->on_atom, current); break; case KEY_EQ: CALL_HANDLER(handlers->on_eq, current); break; case KEY_EVAL: CALL_HANDLER(handlers->on_eval, current); break; case KEY_QUOTE: CALL_HANDLER(handlers->on_quote, current); break; case KEY_APPLY: CALL_HANDLER(handlers->on_apply, current); break; case KEY_BACK: CALL_HANDLER(handlers->on_back, current); break; case KEY_INFO: CALL_HANDLER(handlers->on_info, current); break; case KEY_LINK: CALL_HANDLER(handlers->on_link, current); break; case KEY_SYS: CALL_HANDLER(handlers->on_sys, current); break; default: if (ev.key >= KEY_NUM0 && ev.key <= KEY_NUM9) { CALL_NUMPAD(handlers->on_numpad, current, ev.key - KEY_NUM0); } break; } }
// 4. Render current cell CellBase *current = nav_stack_peek(); if (current && current->handlers->on_display) { current->handlers->on_display(current); }
// 5. Render status bar (runtime-level, always present) render_status_bar();}Navigation Stack
Section titled “Navigation Stack”#define NAV_STACK_DEPTH 32
static CellBase *nav_stack[NAV_STACK_DEPTH];static uint8_t nav_depth = 0;
void drill_into(CellBase *target) { if (nav_depth >= NAV_STACK_DEPTH) { nosh_sfx_error(); return; } CellBase *current = nav_stack_peek(); if (current && current->handlers->on_exit) { current->handlers->on_exit(current); } nav_stack[nav_depth++] = target; if (target->handlers->on_enter) { target->handlers->on_enter(target); }}
void navigate_back(void) { if (nav_depth <= 1) { nosh_sfx_error(); return; } // Can't back past root CellBase *leaving = nav_stack[--nav_depth]; if (leaving->handlers->on_exit) { leaving->handlers->on_exit(leaving); } CellBase *returning = nav_stack_peek(); if (returning->handlers->on_enter) { returning->handlers->on_enter(returning); }}Cell Pool
Section titled “Cell Pool”#define CELL_POOL_SIZE 4096 // Max cells in memory simultaneously
static uint8_t cell_pool_memory[CELL_POOL_SIZE * CELL_MAX_SIZE];static CellBase *free_list = NULL;
// Allocate a cell of a specific typevoid *spawn_cell_raw(uint16_t type_id, size_t size) { // Allocate from free list or pool CellBase *cell = free_list_pop(size); if (!cell) { /* pool exhausted — handle gracefully */ return NULL; } memset(cell, 0, size); cell->type_id = type_id; cell->handlers = get_handlers_for_type(type_id); cell->id = next_cell_id++; return cell;}
// The spawn_cell(typename) macro wraps this:#define spawn_cell(type) \ ((cell_##type *)spawn_cell_raw(CELL_TYPE_ID_##type, sizeof(cell_##type)))Layer 3: nosh_stdlib.c — Standard Library
Section titled “Layer 3: nosh_stdlib.c — Standard Library”Common operations that cartridge code calls inside handlers. These are the verbs of the framework.
Navigation Verbs
Section titled “Navigation Verbs”void drill_into(CellBase *target); // Push target onto nav stack, trigger ON_ENTERvoid navigate_back(void); // Pop nav stack, trigger ON_EXIT/ON_ENTERvoid next_sibling(void); // Move cursor to cdr of current cellvoid prev_sibling(void); // Move cursor backward in list (if doubly linked)void jump_to(CellBase *target); // Direct navigation (for QUOTE jumps)void set_root(CellList *list); // Set the root navigable listCellBase *current_cell(void); // Return the cell the cursor is onCell Manipulation
Section titled “Cell Manipulation”void *spawn_cell(type); // Allocate a new cell of given typevoid destroy_cell(CellBase *cell); // Return cell to free poolvoid link_cells(CellBase *a, CellBase *b); // Create a relation between two cellsvoid unlink_cell(CellBase *cell); // Remove from its listList Operations
Section titled “List Operations”CellList *list_create(void); // Create empty listvoid list_push(CellList *list, CellBase *cell); // Add to endvoid list_insert(CellList *list, int index, CellBase *cell);CellBase *list_get(CellList *list, int index);int list_length(CellList *list);void list_sort(CellList *list, CellCompare cmp); // Sort by comparison functionLFSR / Procedural Generation
Section titled “LFSR / Procedural Generation”void lfsr_seed(uint32_t seed); // Seed the LFSRuint32_t lfsr_next(void); // Next pseudo-random valueuint32_t lfsr_range(uint32_t min, uint32_t max); // Random in rangevoid lfsr_shuffle(void **array, int count); // Fisher-Yates shuffleDisplay Helpers
Section titled “Display Helpers”void draw_threat_bar(uint8_t level, uint8_t x, uint8_t y); // ████░░ style barvoid draw_progress_bar(uint8_t pct, uint8_t x, uint8_t y, uint8_t width);void draw_bordered_box(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const char *title);void draw_list_view(CellList *list, uint8_t start_row, uint8_t visible_rows, int selected);void draw_split_panes(CellList *left, CellList *right, int sel_l, int sel_r);Sound Helpers
Section titled “Sound Helpers”void sfx_select(void); // Short click for selectionvoid sfx_confirm(void); // Confirmation tonevoid sfx_error(void); // Error buzzvoid sfx_alert(void); // Warning pingvoid sfx_ambient_drone(uint16_t freq, uint8_t vol); // Set background dronevoid sfx_sting(uint16_t freq, uint8_t duration_ms); // Sharp one-shotDeck State Access
Section titled “Deck State Access”DeckState *deck_state(void); // Access universal deck statevoid credit_add(int32_t amount); // Modify credit balancevoid credit_deduct(int32_t amount);void rep_modify(int16_t delta); // Modify reputationbool has_capability(uint8_t module_bit); // Check cartridge history (up to bit 31)uint8_t knowledge_index(void); // The Vault bonusuint8_t logic_index(void); // Takezo bonusNote on coordinate types: All bitmap graphics API functions (nosh_gfx_pixel, nosh_gfx_line, nosh_gfx_rect, nosh_gfx_circle, nosh_gfx_blit) use uint16_t for both x-coordinates and y-coordinates to address the full 1024×600 framebuffer. Widths and heights are also uint16_t.
Save / Load
Section titled “Save / Load”bool cart_save(const void *data, uint32_t size); // Save cartridge-specific databool cart_load(void *data, uint32_t size); // Load cartridge-specific databool cart_save_exists(void);Complete Example: Minimal Cartridge
Section titled “Complete Example: Minimal Cartridge”A fully functional cartridge that creates a navigable list of items:
#include "nosh_cart.h"
DECK_TITLE("DEMO", "Demo v1.0", "DIAGNOSTIC")
// --- Cell Types ---
CELL_TYPE(item) { CELL_FIELDS { char label[24]; int value; };
ON_CAR { nosh_text_clear(); nosh_text_puts(0, 0, "EXAMINING:"); nosh_text_puts(0, 1, self->label); nosh_text_printf(0, 3, "VALUE: %d", self->value); sfx_select(); }
ON_EVAL { credit_add(self->value); nosh_text_printf(0, 5, "COLLECTED %d CR", self->value); sfx_confirm(); destroy_cell((CellBase *)self); navigate_back(); }
ON_INFO { nosh_text_printf(0, 7, "DETAIL: %s (%d)", self->label, self->value); }
ON_DISPLAY { // Default list item rendering nosh_text_puts(2, 0, self->label); nosh_text_printf(20, 0, "%4d CR", self->value); }}
// --- Initialization ---
CART_INIT { // Called once when cartridge loads CellList *items = list_create();
lfsr_seed(deck_state()->credit_balance ^ 0xDEAD);
for (int i = 0; i < 8; i++) { cell_item *it = spawn_cell(item); snprintf(it->label, 24, "DATA PACKET %02X", lfsr_next() & 0xFF); it->value = lfsr_range(50, 500); list_push(items, (CellBase *)it); }
set_root(items);
nosh_text_puts(0, 14, "CDR: next | CAR: examine | EVAL: collect");}That’s it. ~50 lines of code for a playable cartridge. The framework handles input dispatch, list navigation, BACK/CDR default behavior, rendering, QUOTE/LAMBDA integration, and save state. The developer defined things and what happens to them.
Build System
Section titled “Build System”Cartridge Project Structure
Section titled “Cartridge Project Structure”my-cartridge/├── Makefile # or CMakeLists.txt├── cart.c # Cartridge source (includes nosh_cart.h)├── generators.c # Procedural generation functions├── generators.h└── assets/ ├── label.txt # Glyph-to-verb reference text └── data/ # Lookup tables, text banks, etc.Makefile Targets
Section titled “Makefile Targets”# Build for desktop emulatormake emulator# Produces: build/cart_emu (executable, links against nosh-emulator)# Run with: ./build/cart_emu
# Build for device (Pi Zero 2 W)make device# Produces: build/cart.kn86 (cartridge image)# Copy to the device's SD card under ~/.nosh/cartridges/
# Build bothmake allCross-Compilation
Section titled “Cross-Compilation”The same cartridge source compiles for both targets because:
nosh_cart.h,nosh_runtime.c, andnosh_stdlib.ccontain no platform-specific code- The nOSh platform API (
nosh_platform.h) is implemented twice:platform_linux.c— Device (Pi Zero 2 W, SDL2 fullscreen, SDL_audio, USB HID input)platform_sdl2.c— Desktop emulator (SDL2 window, SDL_audio, keyboard input)
- The Makefile selects the right implementation based on the target:
ifeq ($(TARGET),device) CC = aarch64-linux-gnu-gcc CFLAGS += -DTARGET_PI_ZERO $(shell sdl2-config --cflags) LDFLAGS += $(shell sdl2-config --libs) NOSH_IMPL = nosh_linux.celse CC = cc CFLAGS += -DTARGET_EMULATOR $(shell sdl2-config --cflags) LDFLAGS += $(shell sdl2-config --libs) NOSH_IMPL = nosh_sdl.cendifWhat This Buys Over Raw C
Section titled “What This Buys Over Raw C”- Every cartridge follows the same structure: define cell types, declare key behaviors, wire into lists. A developer can’t accidentally build a cartridge that ignores the interaction model.
- Cell definitions are readable as design documents. You can scan the
ON_CAR/ON_EVAL/ON_INFOslots and understand the interaction without tracing control flow. - Default behaviors handle the 80% case. A cell with no explicit handlers still supports CAR/CDR/BACK navigation. The developer only writes the domain-specific parts.
- The framework IS the interaction model. If you author a cell, you’re thinking in the Deckline’s grammar. The macros make it difficult to NOT think this way.
What This Buys Over uLisp
Section titled “What This Buys Over uLisp”- Native compiled speed. No interpreter. No GC pauses. Procedural generation runs at full 150MHz.
- Dual-target compilation. Same source builds for device and emulator. Massive iteration speed advantage.
- Zero runtime overhead. The macros expand to plain C structs and function pointers. The dispatcher is a switch statement. This compiles to the fastest possible code.
- Debuggable. GDB, printf debugging, memory inspection — standard C tooling. No Lisp stack traces to decode.
The Tradeoff
Section titled “The Tradeoff”Community accessibility narrows. A Lisp SDK means game designers write configuration-like code. The cell grammar means they need a C toolchain and basic pointer fluency. For the first four titles — written by the device builder — this doesn’t matter. For a hypothetical future community — it narrows the pool. But anyone building cartridges for a hand-soldered cyberdeck already knows C.
uLisp remains a valid Phase 2 goal: a Lisp interpreter that sits on top of the cell grammar, translating Lisp expressions into spawn_cell() and handler registration calls. The grammar’s clean separation makes this layering possible without modifying the runtime.
Naming
Section titled “Naming”The framework should have a name. Proposals:
- DeckC — C for the Deck. Clear, functional.
- CellScript — Describes what it does (define cells and their scripts).
- nosh_cart — The practical name (it’s the header you include).
Recommendation: Call the header nosh_cart.h, call the framework “the cartridge grammar” in documentation, and don’t overthink branding for an internal tool.
The cartridge grammar is the Deckline’s interaction model expressed as C. It doesn’t interpret lists — it compiles them.