Documentation
¶
Overview ¶
Package kit is the shellcade guest SDK: the authoring surface for wasm games targeting shellcade ABI v2. It is implemented purely from the ABI contract — it imports no shellcade private code — and mirrors the native sdk package's value types so a native game ports mechanically.
A game implements Game + Handler and calls Run(game) from main(), plus the eight //go:export trampolines (`shellcade-kit new` scaffolds exactly this).
Index ¶
- Constants
- Variables
- func ANSIRows(f *Frame) []string
- func Main(g Game)
- func SeedEpoch(seed int64) time.Time
- func TextRows(f *Frame) []string
- type Account
- type AccountStore
- type Action
- type Aggregation
- type Attr
- type Base
- type Cadence
- type Cell
- type Character
- type Color
- type ConfigKeySpec
- type ConfigStore
- type ConfigType
- type ControlDecl
- type Direction
- type Frame
- func (f *Frame) Clear()
- func (f *Frame) Fill(r0, c0, r1, c1 int, cell Cell)
- func (f *Frame) Set(row, col int, cell Cell)
- func (f *Frame) SetGrapheme(row, col int, cluster string, st Style) int
- func (f *Frame) SetGraphemeWide(row, col int, cluster string, st Style) int
- func (f *Frame) SetRune(row, col int, r rune, st Style)
- func (f *Frame) SetWide(row, col int, r rune, st Style) int
- func (f *Frame) Text(row, col int, s string, st Style) int
- func (f *Frame) TextRight(row, end int, s string, st Style)
- type Game
- type GameMeta
- type Handler
- type Input
- type InputContext
- type InputKind
- type KVStore
- type Key
- type Kind
- type LeaderboardSpec
- type Lifecycle
- type MergeRule
- type MetricFormat
- type Mode
- type Player
- type PlayerResult
- type Result
- type Room
- type RoomConfig
- type ScoreKeeper
- func (sk *ScoreKeeper) FlushAll(r Room, status Status)
- func (sk *ScoreKeeper) FlushLeave(r Room, p Player, status Status)
- func (sk *ScoreKeeper) PersistBest(r Room, p Player, key string, value int)
- func (sk *ScoreKeeper) PersistWallet(r Room, p Player, balanceKey string, balance int, peakKey string, peak int)
- func (sk *ScoreKeeper) Record(r Room, p Player, metric int)
- type Services
- type Status
- type Style
Constants ¶
const ( Rows = 24 Cols = 80 )
const ABIVersion uint32 = 2
ABIVersion is the ABI major version this SDK targets.
const CtxFeatCharacter = wire.CtxFeatCharacter
CtxFeatCharacter opts the game into per-member character sections: the host appends each member's resolved Character to every member-bearing roster encoding, and the SDK populates Player.Character. Without the declaration Player.Character is always the zero value. Declare it in GameMeta.CtxFeatures.
const CtxFeatRosterEpoch = wire.CtxFeatRosterEpoch
CtxFeatRosterEpoch opts the game into the ctx roster-epoch encoding: the host sends the full member list only when the roster changes (with an epoch), and a 6-byte unchanged marker otherwise — the large-room callback path. Declare it in GameMeta.CtxFeatures.
Variables ¶
var ( White = RGB(0xff, 0xff, 0xff) Red = RGB(0xff, 0x55, 0x55) Green = RGB(0x55, 0xff, 0x55) Yellow = RGB(0xff, 0xff, 0x55) Cyan = RGB(0x55, 0xff, 0xff) DimGray = Gray(0x6c) )
Standard palette (matches the platform canvas constants).
Functions ¶
func ANSIRows ¶
ANSIRows renders each frame row as a truecolor ANSI string (SGR emitted on style change, grapheme clusters burst unbroken, reset at row end). It is the single encoder behind the dev runner's terminal output and the smoke runner's shot files — one renderer so both surfaces emit identical bytes.
func Main ¶
func Main(g Game)
Main, built natively (not wasm), is the instant inner-loop dev runner: `go run .` in a game directory plays the game in this terminal with normal Go tooling (debugger, prints, sub-second rebuilds) and zero wasm involved. The wasm artifact is verified separately by `devkit check` — including the determinism check that guarantees the two backends behave identically.
Flags: -seed N · -heartbeat 50ms · -config k=v (repeatable, v may be @file) · -handle name · -seats N. Esc or Ctrl-C leaves; Ctrl-T switches the active hot-seat. The input path tolerates escape sequences split across reads, paste bursts, and terminal resizes (SIGWINCH re-letterboxes); a terminal smaller than 80x24 shows a "too small" notice and resumes when grown back.
Types ¶
type AccountStore ¶
AccountStore yields an Account for a Player.
type Action ¶
type Action uint8
Action is a resolved, semantic input action.
func Resolve ¶
func Resolve(in Input, ctx InputContext) Action
Resolve maps an Input to a semantic Action for the given context. It is a local reimplementation of the platform's canonical control vocabulary:
Up=↑/k Down=↓/j Left=←/h Right=→/l Confirm=Enter/Space Back=Esc/q/Ctrl-C
type Aggregation ¶
type Aggregation uint8
Leaderboard enums (values match the native sdk's uint8 enums).
const ( BestResult Aggregation = iota SumResults )
type Cadence ¶ added in v2.11.0
type Cadence int
Cadence controls when ScoreKeeper.Record auto-posts a player's metric.
type Cell ¶
Cell is a single drawable position. Rune is the base code point; Cp2/Cp3 carry the extra code points of a grapheme cluster (VS16, skin-tone modifier, keycap U+20E3, ZWJ pieces), 0 = unused. Single-code-point authoring leaves Cp2/Cp3 zero by zero value, so SetRune/Set/Text/SetWide are unchanged.
func CharacterCell ¶ added in v2.9.0
CharacterCell returns the one ready-made cell of a member's character tile: the glyph styled with the resolved ink and background (see ABI.md §4.1 — every admitted glyph is width 1, so games place a character with zero width logic). The zero Character (the game's meta does not declare CtxFeatCharacter) yields a blank cell.
type Character ¶ added in v2.9.0
type Character struct {
Glyph string
InkR uint8
InkG uint8
InkB uint8
BgR uint8
BgG uint8
BgB uint8
Fallback uint8
}
Character is a player's resolved arcade character (mirrors wire.Character): a single width-1 glyph with ink/background colors and an ASCII fallback codepoint. The zero value means "no character" — what every member carries unless the game declares CtxFeatCharacter in GameMeta.CtxFeatures.
type Color ¶
type Color struct {
// contains filtered or unexported fields
}
Color is an optional truecolor value; the zero value is the terminal default.
type ConfigKeySpec ¶
type ConfigKeySpec struct {
Key string // the ConfigStore key the game reads
Title string // short admin-facing label
Description string // one or two sentences for the admin screen
Type ConfigType // how the value is edited/validated
Default string // value the game uses when unset ("" = not declared)
Schema string // JSON Schema document (ConfigJSON only; "" = none)
}
ConfigKeySpec declares one admin-settable config key the game reads via Services.Config. Declaring specs is optional; they exist so the platform's admin tools can render a real get/edit surface for the game's keys.
type ConfigStore ¶
ConfigStore is the slug-bound, read-only per-game config surface.
type ConfigType ¶
type ConfigType uint8
ConfigType tells the platform's admin surface how to render and validate a declared config value (values match the wire type codes).
const ( ConfigText ConfigType = iota // single-line string ConfigNumber // decimal number ConfigBool // true/false ConfigJSON // JSON document (multiline / rich form) )
type ControlDecl ¶ added in v2.10.0
ControlDecl declares one extra control: the exact Input it sends (a printable rune or a named key) and a short display label of at most 16 runes. Build with RuneControl / KeyControl.
func KeyControl ¶ added in v2.10.0
func KeyControl(k Key, label string) ControlDecl
KeyControl declares a named-key control, e.g. KeyControl(KeyBackspace, "UNDO").
func RuneControl ¶ added in v2.10.0
func RuneControl(r rune, label string) ControlDecl
RuneControl declares a printable-rune control, e.g. RuneControl('r', "RESIGN").
type Direction ¶
type Direction uint8
Leaderboard enums (values match the native sdk's uint8 enums).
type Frame ¶
Frame is the fixed 24x80 grid a game composes and sends.
func NewFrame ¶
func NewFrame() *Frame
NewFrame returns a grid filled with blank cells. Frames are handled by POINTER throughout the SDK: a Frame is ~46KB and pass-by-value explodes into thousands of wasm locals (pathological compile time and artifact size).
func (*Frame) Clear ¶
func (f *Frame) Clear()
Clear resets every cell to a blank (space, default colors), so one Frame can be reused across renders — the allocation-free steady state the SDK recommends (a fresh NewFrame per render is ~46KB of churn).
func (*Frame) SetGrapheme ¶
SetGrapheme writes a grapheme cluster of up to three code points into one cell: the base goes to Rune, the second to Cp2, the third to Cp3 (covering VS16 emoji, skin-tone-modified emoji, and keycaps like base + U+20E3). It REFUSES a cluster that decodes to more than three code points (e.g. a family ZWJ emoji) or to zero code points: it draws nothing and returns col unchanged — refusing rather than truncating to a different, valid-looking glyph, mirroring SetWide's drop-on-overflow philosophy. On success it returns col+1. The SDK never measures display width; width-1 is the author's contract here.
func (*Frame) SetGraphemeWide ¶
SetGraphemeWide is the width-2 companion of SetGrapheme, mirroring SetWide: the cluster occupies (row, col) and a continuation cell at (row, col+1) marked Cont=true. It refuses an over-/zero-length cluster (draws nothing, returns col unchanged) and refuses at the right edge (col == Cols-1) exactly like SetWide, dropping rather than drawing a half-glyph. On success returns col+2.
func (*Frame) SetWide ¶
SetWide writes a double-width rune: the glyph occupies (row, col) and its continuation cell (row, col+1), which is marked Cont=true so the renderer skips it (the wide glyph already covers both columns). CJK, many emoji, and box-drawing pairs need this.
Edge handling follows Set's drop-on-overflow philosophy: a wide glyph has no room when col is out of bounds OR the continuation cell would fall off the right edge (col == Cols-1). In that case the whole write is REFUSED (nothing is drawn) — a half-glyph would desync every column to its right. Returns the next free column (col+2), or col unchanged when the write was refused.
type Game ¶
type Game interface {
Meta() GameMeta
NewRoom(cfg RoomConfig, svc Services) Handler
}
Game is the module entry: static metadata plus the room behavior factory.
type GameMeta ¶
type GameMeta struct {
Slug string
Name string
ShortDescription string
MinPlayers int
MaxPlayers int
Tags []string
QuickModeLabel string
SoloModeLabel string
PrivateInviteLine string
Leaderboard *LeaderboardSpec
// Config optionally declares the game's admin-settable config keys.
// Nil/empty means the game declares no config surface (the platform's
// generic editor still works). Declarations are validated at meta encode
// time — an invalid spec list is an authoring bug and panics there.
Config []ConfigKeySpec
// CtxFeatures optionally opts the game into negotiated callback
// encodings (the CtxFeat* bits; zero = none, today's behavior).
// Undefined bits are an authoring bug and panic at meta encode time.
CtxFeatures uint32
// HeartbeatMS optionally declares the game's preferred wake cadence in
// milliseconds. 0 = no declaration (platform default). The host clamps
// to its envelope and an admin config override always wins; out-of-range
// declarations are an authoring bug and panic at meta encode time.
HeartbeatMS int
// Lifecycle optionally declares the room's end-of-life shape. The zero
// value (LifecycleResumable) is today's behavior: hibernate on abandon,
// player-driven resume. LifecycleEphemeral ends and disposes the room
// after the abandon grace (no snapshot, no Resume entry) — right for
// casual social rooms. LifecycleResident declares one long-lived room
// per slug; it takes effect only when the platform grants it (an
// ungranted declaration behaves as resumable). Undefined values and
// resident-with-MinPlayers>1 are authoring bugs and panic at meta
// encode time.
Lifecycle Lifecycle
// Controls optionally declares the game's extra controls: inputs beyond
// the canonical vocabulary (a raw rune like 'r', or a named key like
// KeyBackspace), each with a short display label. Front ends on devices
// without the corresponding physical key (touch) surface each
// declaration as a tappable affordance that sends exactly the declared
// input — presentation metadata only; declarations change no input
// interpretation. Nil/empty means no declarations, and a game fully
// served by the canonical vocabulary needs none. Invalid declarations
// are an authoring bug and panic at meta encode time.
Controls []ControlDecl
}
GameMeta is the static game metadata (mirrors native sdk.GameMeta).
type Handler ¶
type Handler interface {
OnStart(r Room)
OnJoin(r Room, p Player)
OnLeave(r Room, p Player)
OnInput(r Room, p Player, in Input)
OnWake(r Room)
OnClose(r Room)
}
Handler is the game's per-room behavior — the lean wasm surface (OnWake is the host heartbeat; there are no ticks, timers, or frame callbacks).
type InputContext ¶
type InputContext uint8
InputContext selects how an Input is interpreted (mirrors native sdk).
const ( CtxCommand CtxText )
type KVStore ¶
type KVStore interface {
Get(ctx context.Context, key string) ([]byte, bool, error)
Set(ctx context.Context, key string, value []byte, rule MergeRule) error
Delete(ctx context.Context, key string) error
}
KVStore is the durable per-user KV, already namespaced to this game and one account (the HOST derives both — a guest can never name another namespace). Signatures mirror the native sdk so KV patterns port unchanged; the context is accepted and ignored (the host bounds the call).
type LeaderboardSpec ¶
type LeaderboardSpec struct {
MetricLabel string
Direction Direction
Aggregation Aggregation
Format MetricFormat
}
LeaderboardSpec declares how a game's board behaves.
type Lifecycle ¶
type Lifecycle uint8
Lifecycle is the room end-of-life declaration.
const ( LifecycleResumable Lifecycle = Lifecycle(wire.LifecycleResumable) LifecycleEphemeral Lifecycle = Lifecycle(wire.LifecycleEphemeral) LifecycleResident Lifecycle = Lifecycle(wire.LifecycleResident) )
type MergeRule ¶
type MergeRule string
MergeRule governs per-user KV reconciliation on account merge.
type MetricFormat ¶
type MetricFormat uint8
Leaderboard enums (values match the native sdk's uint8 enums).
const ( Integer MetricFormat = iota Decimal Duration )
type Player ¶
Player is a value-comparable membership token (mirrors native sdk.Player).
func (Player) DisplayName ¶
DisplayName is the handle with a "(guest)" marker for guests.
type PlayerResult ¶
PlayerResult is one player's outcome in a settled room.
type Room ¶
type Room interface {
// Local reads.
Members() []Player
Has(p Player) bool
Count() int
Config() RoomConfig
Rand() *rand.Rand
Now() time.Time
Settled() bool
// Effects (host calls).
Send(p Player, f *Frame)
Identical(f *Frame)
SetInputContext(ctx InputContext)
End(res Result)
Post(res Result)
Log(msg string)
Services() Services
}
Room is the authoring surface: local reads answered from the cached CallContext (zero host calls), effects via host functions. A Room handle is valid only inside the callback that received it.
type RoomConfig ¶
RoomConfig mirrors the native sdk.RoomConfig.
type ScoreKeeper ¶ added in v2.11.0
type ScoreKeeper struct {
// contains filtered or unexported fields
}
ScoreKeeper tracks each player's current leaderboard metric and standardises posting it three ways: live (Record), on disconnect (FlushLeave), and — for continuous "never-ending" games — periodically (FlushAll).
It holds NO goroutines or timers: periodic flushing is driven by the game's own OnWake heartbeat, so behaviour stays deterministic under hibernation and replay. A single keeper is held on the room and is safe for the room actor.
The board itself is fed only by the Post calls this makes; PersistBest / PersistWallet write per-account KV for session *resume*, which is separate from the leaderboard.
func NewScoreKeeper ¶ added in v2.11.0
func NewScoreKeeper(c Cadence) *ScoreKeeper
NewScoreKeeper returns a ScoreKeeper with the given auto-post cadence.
func (*ScoreKeeper) FlushAll ¶ added in v2.11.0
func (sk *ScoreKeeper) FlushAll(r Room, status Status)
FlushAll posts every tracked player's current metric with the given status, in deterministic AccountID order. Continuous games call this from OnWake on a throttled interval so an abandoned, still-ticking world keeps recording.
func (*ScoreKeeper) FlushLeave ¶ added in v2.11.0
func (sk *ScoreKeeper) FlushLeave(r Room, p Player, status Status)
FlushLeave posts the player's current tracked metric with the given status (normally StatusDNF) and stops tracking them. Call it from OnLeave so a mid-game disconnect still records the player's progress. Calling it for an untracked player is a no-op.
IMPORTANT: the platform's leaderboard reader ranks DNF rows the same as finished ones. For a lower-is-better board, pass a fair full-run metric (e.g. par-extrapolated), never a raw partial, or a half-played run will top the board.
func (*ScoreKeeper) PersistBest ¶ added in v2.11.0
func (sk *ScoreKeeper) PersistBest(r Room, p Player, key string, value int)
PersistBest writes a monotonic high-water value to the player's per-account KV (MergeMax) for session resume. The leaderboard board is fed by Record/FlushLeave/FlushAll; this only preserves state across reconnects.
func (*ScoreKeeper) PersistWallet ¶ added in v2.11.0
func (sk *ScoreKeeper) PersistWallet(r Room, p Player, balanceKey string, balance int, peakKey string, peak int)
PersistWallet writes a carryable balance (MergeSum) and a high-water peak (MergeMax) for casino-style games, replacing the duplicated persistWallet helpers. MergeMax/MergeSum make a KV-outage-era write unable to clobber the durable value at merge time.
type Services ¶
type Services struct {
Accounts AccountStore
Config ConfigStore
}
Services is the ABI v1 service bundle (no chat, no spectate).