game

package
v2.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 16 Imported by: 0

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

View Source
const (
	Rows = 24
	Cols = 80
)
View Source
const ABIVersion uint32 = 2

ABIVersion is the ABI major version this SDK targets.

View Source
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.

View Source
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

View Source
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

func ANSIRows(f *Frame) []string

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.

func SeedEpoch

func SeedEpoch(seed int64) time.Time

SeedEpoch derives a fixed virtual-clock start from a run seed, so the same seed always begins at the same instant. The year-2000 base keeps the value human-readable in logs while staying well clear of the zero time. Shared by the dev runner's -seed mode and the smoke runner.

func TextRows

func TextRows(f *Frame) []string

TextRows renders each frame row as plain text: full grapheme clusters, no escape sequences, trailing blanks trimmed — the greppable twin of ANSIRows.

Types

type Account

type Account interface {
	ID() string
	Handle() string
	Kind() Kind
	Store() KVStore
}

Account is a live, account-scoped handle for a Player.

type AccountStore

type AccountStore interface{ For(p Player) Account }

AccountStore yields an Account for a Player.

type Action

type Action uint8

Action is a resolved, semantic input action.

const (
	ActNone Action = iota
	ActUp
	ActDown
	ActLeft
	ActRight
	ActConfirm
	ActBack
)

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 Attr

type Attr uint8

Attr is a bitset of text attributes.

const (
	AttrBold Attr = 1 << iota
	AttrDim
	AttrUnderline
	AttrReverse
)

type Base

type Base struct{}

Base supplies no-op defaults so a game overrides only what it needs.

func (Base) OnClose

func (Base) OnClose(Room)

func (Base) OnInput

func (Base) OnInput(Room, Player, Input)

func (Base) OnJoin

func (Base) OnJoin(Room, Player)

func (Base) OnLeave

func (Base) OnLeave(Room, Player)

func (Base) OnStart

func (Base) OnStart(Room)

func (Base) OnWake

func (Base) OnWake(Room)

type Cadence added in v2.11.0

type Cadence int

Cadence controls when ScoreKeeper.Record auto-posts a player's metric.

const (
	// OnImprove posts only when the new metric beats the last posted value —
	// the right choice for monotonic high-water boards (peak credits, best
	// survival time, kill count).
	OnImprove Cadence = iota
	// OnChange posts whenever the metric changes from the last posted value.
	OnChange
)

type Cell

type Cell struct {
	Rune rune
	Cp2  rune
	Cp3  rune
	FG   Color
	BG   Color
	Attr Attr
	Cont bool
}

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

func CharacterCell(c Character) Cell

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.

func Gray

func Gray(v uint8) Color

Gray is a convenience for an even gray.

func RGB

func RGB(r, g, b uint8) Color

RGB constructs a truecolor value.

func (Color) IsSet

func (c Color) IsSet() bool

IsSet reports whether the color is set (vs terminal default).

func (Color) RGBVals

func (c Color) RGBVals() (uint8, uint8, uint8)

RGBVals returns the color components.

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

type ConfigStore interface {
	Get(ctx context.Context, key string) ([]byte, bool, error)
}

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

type ControlDecl struct {
	Input Input
	Label string
}

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).

const (
	HigherBetter Direction = iota
	LowerBetter
)

type Frame

type Frame struct {
	Cells [Rows][Cols]Cell
}

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) Fill

func (f *Frame) Fill(r0, c0, r1, c1 int, cell Cell)

Fill paints a rectangle (inclusive bounds) with the given cell.

func (*Frame) Set

func (f *Frame) Set(row, col int, cell Cell)

Set writes one cell; out-of-bounds writes are clamped (dropped).

func (*Frame) SetGrapheme

func (f *Frame) SetGrapheme(row, col int, cluster string, st Style) int

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

func (f *Frame) SetGraphemeWide(row, col int, cluster string, st Style) int

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) SetRune

func (f *Frame) SetRune(row, col int, r rune, st Style)

SetRune writes one styled rune.

func (*Frame) SetWide

func (f *Frame) SetWide(row, col int, r rune, st Style) int

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.

func (*Frame) Text

func (f *Frame) Text(row, col int, s string, st Style) int

Text writes a string left-to-right, clamped to the row. Returns the next col.

func (*Frame) TextRight

func (f *Frame) TextRight(row, end int, s string, st Style)

TextRight writes a string so it ends at col `end` (inclusive).

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 Input

type Input struct {
	Kind InputKind
	Rune rune
	Key  Key
}

Input is the SDK-neutral input event.

type InputContext

type InputContext uint8

InputContext selects how an Input is interpreted (mirrors native sdk).

const (
	CtxNav InputContext = iota
	CtxCommand
	CtxText
)

type InputKind

type InputKind uint8

InputKind distinguishes a printable rune from a named key.

const (
	InputRune InputKind = iota
	InputKey
)

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 Key

type Key uint8

Key is a named (non-printable) key. Values match the native sdk.

const (
	KeyNone Key = iota
	KeyEnter
	KeyBackspace
	KeyEsc
	KeyTab
	KeyUp
	KeyDown
	KeyLeft
	KeyRight
	KeyCtrlC
)

type Kind

type Kind uint8

Kind distinguishes a keyless guest from a member account.

const (
	KindGuest Kind = iota
	KindMember
)

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.

const (
	MergeKeepWinner MergeRule = "keep-winner"
	MergeKeepLoser  MergeRule = "keep-loser"
	MergeSum        MergeRule = "sum"
	MergeMax        MergeRule = "max"
)

type MetricFormat

type MetricFormat uint8

Leaderboard enums (values match the native sdk's uint8 enums).

const (
	Integer MetricFormat = iota
	Decimal
	Duration
)

type Mode

type Mode uint8

Mode is the matchmaking + timing classifier.

const (
	ModeQuick Mode = iota
	ModePrivate
	ModeSolo
)

type Player

type Player struct {
	AccountID string
	Handle    string
	Kind      Kind
	Conn      string
	Character Character
}

Player is a value-comparable membership token (mirrors native sdk.Player).

func (Player) DisplayName

func (p Player) DisplayName() string

DisplayName is the handle with a "(guest)" marker for guests.

func (Player) Guest

func (p Player) Guest() bool

Guest reports whether the player is a keyless guest.

type PlayerResult

type PlayerResult struct {
	Player Player
	Metric int
	Rank   int
	Status Status
}

PlayerResult is one player's outcome in a settled room.

type Result

type Result struct {
	Rankings []PlayerResult
}

Result is the room-level outcome.

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

type RoomConfig struct {
	Mode       Mode
	Capacity   int
	MinPlayers int
	Seed       int64
	SeedSet    bool
}

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.

func (*ScoreKeeper) Record added in v2.11.0

func (sk *ScoreKeeper) Record(r Room, p Player, metric int)

Record updates the player's current metric and posts it per the cadence. Live posts always carry StatusFinished.

type Services

type Services struct {
	Accounts AccountStore
	Config   ConfigStore
}

Services is the ABI v1 service bundle (no chat, no spectate).

type Status

type Status uint8

Status is a player's terminal outcome.

const (
	StatusFinished Status = iota
	StatusDNF
	StatusFlagged
)

type Style

type Style struct {
	FG   Color
	BG   Color
	Attr Attr
}

Style bundles the styling applied when writing text.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL