Skip to content

Sprint 4 Design Pack — GWP-246

Three things every cartridge wants to draw eventually: a threat-level bar (how hot is this contract?), a progress bar (how far through this scan?), and a bordered box (panel chrome for any sub-region of the 80×25 grid). ADR-0005 §“Display Helpers (stdlib)” already lists all three with full signatures, range checks, and error tags — so this isn’t a design exercise; it’s a “ship what’s specified” sprint. Today, every cart hand-rolls its own version. icebreaker_cart.c and blackledger_cart.c already draw bordered boxes, with subtle inconsistencies in corner-glyph choice, title-padding behavior, and where the title sits inside the top edge. By the time we author 14 launch carts, those inconsistencies become 14 slightly-different box styles — exactly the kind of drift a stdlib exists to prevent. Implementing these three primitives in one PR, binding them to Lisp, migrating one in-tree cart as a sample, and citing them in the cart-authoring docs gives every future cart author one obvious right answer per drawing primitive.

Player-facing Semantics with Worked Example

Section titled “Player-facing Semantics with Worked Example”

Renders level filled blocks (, U+25A0 — already in the KN-86 Code Page) followed by (max-level - level) empty blocks (, U+25A1) starting at (col, row) in the 80×25 grid. Used for difficulty/heat/danger indicators where the meaningful range is small (1–16). Choosing block glyphs rather than [##.....] style: tighter, more terminal-like, fits the device aesthetic; still 1 cell per “tick” so a 5-of-8 threat reads as 5 filled + 3 empty = 8 cells wide.

Continuous percentage rendered as a width-character bar at (col, row). Internally: leading [, width-2 interior cells, trailing ]. Interior fill is integer-rounded — pct * (width-2) / 100 filled cells of (U+2588), the rest · (U+00B7) or space (TBD — see edge cases). For pct = 100, the entire interior is filled. For widths < 4, the brackets eat the bar; behavior here is up to the engineer (clamp / no-op / error) — recommended: enforce width >= 4 before drawing interior, otherwise raise :invalid-width. ADR-0005 already says width: 1–32 chars so the clamp is in the spec; tighten the lower bound to 4 in the implementation.

Outline-only box of width w × height h, top-left at (col, row), using CP437 box-drawing glyphs from font.c’s table:

┌── title ───┐ <- row r0 (top edge with optional centered title)
│ │ <- rows r0+1 .. r0+h-2 (interior is left untouched — author draws into it)
└────────────┘ <- row r0+h-1 (bottom edge)

Title is centered within (col+1, col+w-2) (the interior of the top edge). If len(title) + 2 > w-2, truncate with single-character ellipsis (, U+2026 — confirm presence in KN-86 Code Page; if absent, fall back to ..). Title is rendered with one space of padding on each side: ── title ──.

Worked example — icebreaker_cart.c migration:

Today (one of the box-drawing call sites):

text_putc(state, 0, 5, '+');
for (uint8_t c = 1; c < 39; c++) text_putc(state, c, 5, '-');
text_putc(state, 39, 5, '+');
text_puts(state, 2, 5, " ICE LATTICE ");
/* ... and the same dance for the other three sides */

After:

(draw-bordered-box 0 5 40 12 "ICE LATTICE")

One line, byte-identical output, identical to every other cart’s panel chrome.

  • C implementations land in src/nosh_stdlib.c (additive):
    • void stdlib_draw_threat_bar(uint8_t level, uint8_t max_level, uint8_t col, uint8_t row)
    • void stdlib_draw_progress_bar(uint8_t pct, uint8_t col, uint8_t row, uint8_t width)
    • void stdlib_draw_bordered_box(uint8_t col, uint8_t row, uint8_t w, uint8_t h, const char *title)
  • nosh_stdlib.h declares all three with the canonical signatures (additive).
  • CP437 box-drawing glyphs sourced from font.c’s existing table — corners ┌┐└┘, horizontals ─, verticals │. Do NOT add or modify glyphs; if font.c is missing any of these, file a sub-task and pause.
  • Title centering within (col+1, col+w-2). Truncation with when too long; fallback .. if is not in the code page.
  • Range validation matches ADR-0005:
    • draw-threat-bar: raises :out-of-range if max_level > 16 or col + max_level > 80.
    • draw-progress-bar: :invalid-percentage if pct > 100; :invalid-width if width > 32 or col + width > 80. Add: tighten lower bound to width >= 4 (raise :invalid-width) — leaves room for [..].
    • draw-bordered-box: :out-of-range if col + w > 80 or row + h > 25; :invalid-size if w < 3 or h < 2.
  • Lisp bindings registered in nosh_lisp_bridge.c as draw-threat-bar, draw-progress-bar, draw-bordered-box (kebab-case; conventional).
  • Test tests/test_nosh_stdlib_helpers.c validates each helper against a mocked text buffer:
    • draw-threat-bar: 5/8, 0/8, 8/8, off-screen edge case (raises error).
    • draw-progress-bar: 0%, 50%, 100%, width=4 minimum, width=33 raises, pct=101 raises.
    • draw-bordered-box: small (3×2) box, large box with long title (truncates), title-fits-exactly, off-screen raises.
  • Cart author docs cite the helpers. Update docs/software/cartridges/authoring/ui-patterns.md with a “Stdlib Display Helpers” subsection enumerating the three with one short example each.
  • Sample migration: carts/icebreaker_cart.c migrates one existing box-drawing call to use stdlib_draw_bordered_box. Pure C call (this cart is C-authored); demonstrates parity. Do NOT migrate other call sites or other carts in this PR — scope guard.
  1. Title-bar width arithmetic. When title is exactly w-4 chars long (room for one space of padding either side, no ellipsis), it fits. When title is w-3 chars, padding-left becomes 1 and padding-right becomes 0 — does the function force symmetric padding (truncate one char) or accept asymmetry? Recommendation: accept asymmetry up to a 1-char delta (matches conventional terminal UI); document the rule.
  2. Bordered box at exact grid edges. A draw-bordered-box 0 0 80 25 call sits flush with the cart’s content area boundary — but Row 0 is firmware territory and Row 24 is firmware territory. The helper must not clamp to “rows 1–23”; that’s the cart’s responsibility. The helper draws what’s asked. (Same posture as every other text-write primitive — caller decides where on the canvas.) Document this in ui-patterns.md: bordered-box does not enforce the row-authority split.
  3. Progress bar fill character. Spec choice between · (middle dot, U+00B7) and (space) for unfilled interior. Recommendation: middle dot — gives the empty portion visible texture so the bar is readable even at 0%. Confirm the dot is in the KN-86 Code Page (check font.c).
  4. level > max_level for threat-bar. ADR-0005 doesn’t explicitly tag this. Two options: clamp silently to level = max_level, or raise :invalid-level. Recommendation: clamp silently — pragmatic for cart authors who compute level from gameplay state and may briefly overshoot during animations. Document the clamp in the contract row.

Cross-references (cart specs that consume)

Section titled “Cross-references (cart specs that consume)”

These are the launch-cart specs that already plan to use threat/progress/box semantics — engineers shipping this PR should glance at them so they understand the consumer side:

  • Files (additive only):
    • kn86-emulator/src/nosh_stdlib.c — three new functions.
    • kn86-emulator/src/nosh_stdlib.h — three new declarations.
    • kn86-emulator/src/nosh_lisp_bridge.c — three bindings (additive).
    • kn86-emulator/tests/test_nosh_stdlib_helpers.c — NEW test file. Add to CMakeLists.txt.
    • kn86-emulator/carts/icebreaker_cart.c — migrate ONE box-drawing call (sample only).
    • docs/software/cartridges/authoring/ui-patterns.md — new subsection.
  • Do NOT touch: font.c (glyph table), display.c. If a needed glyph is missing from font.c, file a sub-task and pause.
  • TDD via test-driven-development skill: test first (red on each helper), implement, refactor.
  • Expected size: ~120 LOC stdlib + ~80 LOC bridge bindings + ~250 LOC test + ~10 LOC migration + ~40 lines doc. Single PR. ~2-day engineer-dispatch.
  • Validation: cd kn86-emulator/build && ctest -R stdlib_helpers passes; full ctest stays green; emulator boots and icebreaker_cart renders identically to before migration (visual diff acceptable).
  • Spec hygiene: ADR-0005 is the contract; no spec values invented. Glyph choices reference the KN-86 Code Page in docs/software/api-reference/grammars/character-set.md.
  • Progress-bar unfilled interior glyph: middle dot · (recommended) or space ? Affects readability at low percentages.
  • level > max_level for threat-bar: silent clamp (recommended) or raise :invalid-level?
  • Title asymmetric padding tolerance: accept up to 1-char asymmetry (recommended) or always truncate to symmetric? Standard terminal UI accepts asymmetry; calling out for confirmation.