Skip to content

Terminology

This is the canonical glossary for LibreBash: one vocabulary shared by the UI copy in lib/l10n/app_en.arb, code identifiers (classes, fields, providers, files, package names), and anything that will ever be persisted. User-facing terms win. When a concept gets a user-facing name, code names follow it — not the other way around.

Check this file before naming a new identifier, ARB key, package, or user-facing string. If you add or rename a user-facing concept, update this file in the same commit.

Canonical terms

Alphabetical by user-facing term, as written (so "the Bard" files under T). Insert new terms in sorted position.

User-facing term Code identifier Notes
abort the call MaroonController.endTurn ends the describer's turn early in either mode — always penalty-free; the lost chance at points is the cost
about tile AboutTile, buildTimeProvider icon + name + version + build date + license, in the home-screen about dialog
app share AppShareService, SharedAppBinary, RoomStaticContent, /install + /app + /app.json the host handing a joiner its own installed binary (ADR 0006): /install is the dependency-free download page (offline APK first, stores grouped by OS, "not needed to play" banner), /app streams the APK as a named download, /app.json reports {versionName, versionCode, sizeBytes, sha256} so a receiver can compare before pulling
asker DilemmasState.asker, askerIndex, nextAsker, DilemmasStrings.askerLabel/passToLabel the This or That? seat that reads the current card aloud — rotates once per deal, and the advance button names the next one ("Pass to Maya") so the handoff is built in. also the deck's exclusion: name-drop dilemmas never name the asker
ballot Ballot one voter's choices: a favorite plus per-entry reactions
Bard-O-Bot botAuthorId, MonkeysStrings.botName the Infinite Monkeys bot persona — an optional dealt player answering from the word pools, and the name of its inspire-me button
billboard Billboard, submitBillboardFor, billboardsFor one assembled Buildboard ad — a curator's pick of one dealt tagline plus one dealt sketch for a client, the unit the ballot votes on
bench LinkPeer.sittingOut, SessionLink.setPeerSittingOut, roster key out, hostToolsBenchTooltip/"deal in" the host's soft timeout from the toolbox — a benched peer stays connected and keeps its warm seat, but calls skip it until dealt back in. room-scoped on the link and keyed on the peer id, so it survives a reclaim; the benched client's own banner already reads "sitting out" — the same flag, seen from its side
blank SceneBlank, BlankType one typed fill-in inside a scene ("a noun", "a verb ending in -ing"); original is what Shakespeare wrote, shown under the fill at the reveal; identical {type\|original} tokens are one blank — asked once, substituted in every occurrence. noun splits into thing/emotion/concept/person so the ask matches the original word's register — tag narrowly when it does
blitz MaroonTurnMode.blitz the race-the-clock turn shape — the describer burns through a stack of cards until the timer or "abort the call"
Board-O-Bot botAuthorId, DictionairheadsStrings.botName the Dictionairheads bot persona — an app-written definition slipped into the vote
bot entry botAuthorId, botDefinition an app-written entry slipped into a vote: votable, scored for show, never eligible for a player callout
bot player SessionOptions.botPlays, GameMetadata.bot, BotPlayer, BotPlayer.scores a computer player a game can seat, declared on the metadata as a BotPlayer persona (name + emoji); personas carry the -O-Bot suffix so the seat reads as the CPU at a glance — Bottle-O-Bot, Board-O-Bot, Bard-O-Bot, Research-O-Bot, Troll-O-Bot. On the setup screen it moves in and out of the roster like a regular player: seated it gets a roster row with a remove affordance, unseated it waits as a quick-seat chip — but it's never a roster Player and never lands in player_history. The seat is the global bot_plays_enabled flag, carried between games like the roster; networked setup shows the same row beside the room's seats. scores marks a persona whose entry is a real ballot target (Board-O-Bot banks points), so a seated scoring bot counts toward minScoringPlayers; comparison-only ghosts (Research-O-Bot, Troll-O-Bot) leave it false
Bottle-O-Bot botAuthorId, PoetryStrings.botName the Desert Island Poetry bot persona — writes a full share of composed opening lines; the poem itself is already "passed in a bottle"
buzzed MaroonOutcome.buzzed a banned word slipped out — the card burns, the describer loses 50 points, and the turn ends on the spot in either mode; a foul, not a free skip
calibration AcquiredTasteState.scales/picks, calibrationFormat one typed line of an Acquired Taste post — "on the {scale} scale, it lands right on {pick}": the example the influencer would place at their secret rating on that scale. the only evidence the followers decode from
call WsHostSessionLink.startGame/returnToRoom, ActiveGame one game summoned inside a room — "the host calls Dictionairheads". repeatable: a room sees many calls over its life, each carrying {gameId, seats} on link.started and ending on link.returned. internal vocabulary only — user-facing copy says "game" ("the next game", "sitting this one out"), never "call"
call outcome CallOutcome, GameSession.onOutcome, sessionOutcomeProvider the terminal standing a call reports to the statistics harvest: scores keyed by player or team id plus the winners (a shared crown lists every tied leader). an empty outcome means "completed, nobody crowned" — the last reveal of an unscored run; a call abandoned before any standing reports nothing. replays inside one call legitimately report several
call record GameCallRecord, RecordedOutcome, CallSeat, CallStatsRecorder, GameStatsRepository, game_stats/game_stats_live keys, call_record wire kind the durable log of one call: seats snapshotted under seat keys, the terminal standings reached, the foreground playtime banked. parks under game_stats_live with a heartbeat while in flight — a crashed call's stale stamp reconciles to abandoned on next launch — and files under game_stats as completed or abandoned on finalize. the host broadcasts the completed record to the room in a call_record link message so every client keeps its own tally
catalog / registry GameRegistry, gameRegistryProvider, game_catalog.dart the app's list of compiled-in games; "registry" for the object, "catalog" for the feature-level wiring
client BuildboardClient, BuildboardState.clients, dealClients one of the round's two fake companies — the whole room pitches the same pair; dealt per round at 80% off the company-name generator (companyTemplates/authoredCompanies), 20% off the shared venue generator
close read closePoints, UnfocusedGroupStrings.closeTag a guess within the tolerance of the consumer's rating — worth 50 points
common settings game_scoring_*, game_timer_*, game_inspiration_*, game_typos_*, CommonSettingOverrides per-game overrides on the setup screen's "Common settings" tile (scoring, inspiration, typos, turn timer); unset means "inherit the global". was "game settings" — the rename reflects that the same controls apply to (almost) every game
confusable family NounEntry.families, confusableFamilies, ConfusionDeal, dealConfusion, minClusterSize a set of 6+ near-neighbor words grouped by a family tag in the shared noun pools — an Off by One round draws its majority and minority programs from one family and the leftover members stock the steal guess's decoy options; membership is metadata (families on NounEntry, NounGenerator.confusable gating eligibility), not a separate bank, so one word can sit in several families and every member carries a definition gloss the surfaces resolve locally
consumer UnfocusedState.consumer, UnfocusedPhase.consumerGate/consumerRate the Unfocused Group player secretly rating the survey question 1–10; the rest of the roster (or their own team) guesses the rating. the consumer banks the best read of the turn
controller command ControllerCommand, GamepadInput, GamepadCommandMapper, GamepadInputService, GamepadCloneFilter, ControllerInputBridge the normalized gamepad vocabulary a Linux pad produces — GamepadInput names the physical control, GamepadCommandMapper turns edges and held directions into ControllerCommands (press + repeat), and ControllerInputBridge invokes each as the same intent its key binding produces. GamepadCloneFilter sits before the mapper and mutes Steam Input's virtual mirror at the device level: pads that connect in the same burst form a cohort, a second pad pressing inside echoWindow (correlated by time, not button — remaps can't dodge it) pairs the two, the virtual-named pad stays canonical so Steam mappings apply, the dupe is quarantined, and an unpaired edge on either member dissolves the pair so a real second pad self-heals. Android gamepads never touch this path — they already arrive as Flutter key events
controller legend ControllerLegend, controllerHint* the docked strip under every screen in TV mode showing which pad button does what — Xbox-style glyphs for D-pad/A/B/X/Start with their actions, contextual: X and Start only show when the focused control/host link can answer them. controllerTraversalPolicy (focus_reveal_policy.dart) is the related seam: the app's FocusTraversalGroup policy that re-reveals a directional move's destination when the framework's keep-visible guard declines to scroll, and swallows StateError from candidates that aren't laid out yet
cover story DrawWhatNowState.guesses, DreamweaversState.guesses, ReviewOption.authorId a fake prompt/dream written by a non-artist seat during review — joins the real one on the ballot, and each fooled vote pays its author 50 (double when it's the dreamer's pick). Ballots render every option lowercased (RemoteBallotView.entryBody) so casing can't out the real one — the wire keeps raw text
creature card GuessCard, composeGuessCard one Doctor Maroon guess target: several NounEntry slots composed by a PhraseTemplate ("a walrus engaged to an emu"), dealt by composePhrase through the shared draw deck so neither recipes nor words repeat until their pools wrap
curated pool CuratedPool, dealCuratedOrElse, defaultCuratedShare, minCuratedPoolSize the hand-written list paired with every generator: a dealt phrase rolls the pool's share (10% by default, tunable per pool — Acquired Taste's scales run 0.85) on sessionRandomProvider, then either serves an authored item off the shared draw deck under the pool's deckKey or runs the generator. every generated thing/phrase gets one — see conventions.md
curator Billboard.curatorId the seat that paired and submitted a billboard — the only contributor barred from voting it; banks 100 per vote it receives
dealt hand BillboardHand, BuildboardState.hands a seat's share of the client's filed taglines and sketches after the production shuffle — the only pieces it may assemble into billboards; private to the seat until its boards stand
debrief MaroonPhase.recap, CardRecord the post-turn card rundown where identified creatures get their guesser(s) credited; "recap" is the code name, UI says "debrief". blitz turns always debrief the stack; a one-card turn only stops there when a guess needs crediting — everything else hands the phone straight over
decode AcquiredTasteState.guesses, AcquiredTastePhase.guesserGate/guesserRate a follower's secret guess at the influencer's dealt rating — exact decodes bank 100 uptoast, decodes within the tolerance bank 50
describer MaroonState.describer, describerId the Doctor Maroon player holding the device and talking; the rest of the room guesses. the only turn shape where the active player faces the room instead of writing secretly
device gate PassDeviceGate the "Pass the device to X / I am X" screen; "gate" is a code term, UI says "pass the device"
dilemma Dilemma, DilemmasState.current, dealDilemma, authoredDilemmas, dilemmaPool, dilemmaTemplates, dilemmasOptionDilemmas ("Dilemmas") one This or That? card — a would-you-rather pair of option fragments read "X or Y". deals two ways: the authored bank at the pool's 0.67 share (its voice carries the game) or a pair composed off DilemmaTemplates; the count round option sizes the session, endless by default
dish AcquiredTasteState.dish, ReviewPost.dish, dishTemplatesKey the silly plate under review in Acquired Taste — public to the whole room, composed off the foods pool by the draw deck
display path /d, /d/<code>, JoinTicket.displayUri, WebClientConfig's d segment, wireless_display channels the remote-typable TV URL the host serves — /d for an open room, /d/<code> for a locked one, the code riding the path so a remote never types ?t=, and emitted bare (the ?v= marker identifies player links; a /d path identifies itself). the room server resolves it to the web-client index and gates it on the segment exactly like t (cookie included); the locked-room gate's form posts back to /d so a TV stays an observer, and the shell auto-joins the page as LinkRole.observer — no name, no tap. JoinTicket.fromUri reads the segment back like t; ?role=observer stays the equivalent query form. the invite dialog's "TV display" face shows it beside a scan-me QR, and the wireless-display switch points a RoomDisplayPresentation WebView at the same URL — stamped ?projected=1 so the page knows it's the host's own view-only mirror
draw deck DrawDeck, DeckEntry, drawDeckProvider, DrawDeckRepository, draw_decks the app-wide shuffle-bag dealer: every named content pool deals from a shuffled order without repeating until the pool wraps, then reseeds — the "no repeats" feel that makes the content read as handcrafted. each pool's cycle is {seed, position, size}, persisted to disk so variety survives an app restart; a stored size that no longer matches the pool's real length (content edits happen) reseeds on next deal. opaque pools (template closures, scenes) carry an authored key constant; value pools fingerprint via poolDeckKey. mechanics rolls — bot guesses, hand shuffles, voice picks — stay on sessionRandomProvider
DLC — marketing shorthand for a game package shipped in an app update; never a code identifier or separate artifact
drawing document DrawingDocument, DrawOp (draw_core) the ordered op history behind a canvas — strokes and fills kept whole so games can filter which ops render; serializes to a compact wire form for projections
dreamer DreamweaversState.dreamers, currentDreamerId, isDreamer, dreamerId the Dreamweavers player a dream is woven for — dealt as a derangement (nobody weaves for themselves, every seat is dreamed-for once a round); seat-private while drawing, public through review so cover stories can aim, never files a fake reading, and always holds a ballot (their pick pays its target double — the payee doubles, not the dreamer's own voter reward); readers vote too unless their pick would be forced — a ballot thin enough to leave them only the real dream drops them to emoji-only, unawaited (awaitingVotes counts a seat's legal options)
Dubious Art SketchySourcesStrings.drawing* copy, sketchySourcesDraw* ARB keys the fictional art platform Sketchy Sources drawings are "posted" to — framing only; a drawing hop's UI copy speaks in its voice
embedded fallback FallbackWebScreen, /multiplayer/web, FallbackWebArgs, fallbackClientUri, webBridgeChannelName ('LibreBash'), WebHostBridge, BridgeSpeaker, ActiveLinkController.holdWebFallbackNetwork/releaseWebFallbackNetwork the host-served web client running inside the app's own WebView (ADR 0006) — the render path for a call this build can't draw natively. the page claims the native link's seat via ?s=/?k= URL params (the host evicts the old socket with seat_taken), the screen holds the wakelock/presence/Wi-Fi the dead link held, navigation locks to the room origin, and the LibreBash JS channel bridges haptics, turn pings, and TTS (BridgeSpeaker is a Speaker over it, paced by utteranceDone deliveries). no embeddable WebView → the screen shows the URL for an external browser. UI says "the web client"
ending SketchyEnding, sketchyOptionEnding, SketchySourcesState.ending/hopCount the round option choosing where a Sketchy Sources story stops — lands ends on the last natural hop (odd rosters on a drawing, even on a headline), circle adds one beat so the story's originator draws the final headline
exact read exactPoints, UnfocusedGroupStrings.exactTag a guess matching the consumer's rating exactly — worth 100 points
fandom AcquiredTasteState.teams, acquiredDeliberationTitle what Acquired Taste's teams mode calls a side — the fandom huddles openly ("stan chat") and locks one shared decode
fading ink FadingInkBoard, DrawingController.opCommittedAts, DrawingPainter.strokeAlphas, dreamInkPool (draw_core) the disappearing-ink drawing mode Dreamweavers plays on — the live stroke stays opaque while every committed op dissolves linearly to nothing over fadeDuration (5s by default), computed off the controller's per-op commit stamps against its own injectable clock; the submitted document keeps every op (fade is presentation only), and restored drafts stamp at epoch so they never flash fresh
favorite Ballot.favoriteId, favoritesReceived the single entry a voter stars — worth 100 points to its author
forbidden word NounEntry.forbidden, GuessWord.forbidden the clues a describer may not say — ranked most-obvious first, so a game bans the first few or the whole list (Maroon's ban slider picks 3–8 per word). GuessCard.bannedWords lays the card's own component words on top — the "Never say" box bans those outright (inflections like plurals are implied, not listed)
forced pick awaitingVotes/awaitingBallots vote-count gates, RemoteBallotView.reactOnly a ballot seat with fewer than two legal entries (the ballot minus its own authored entries — own-curated billboards in Buildboard) — it holds no vote obligation and never blocks the reveal; its ballot face drops the star/lock for emoji-only with a persistent hint, and each emoji files standalone via the react intent, riding the tally as a favorite-less ballot. the locked ballot's reaction sheet is authoritative — it replaces the seat's standalone picks wholesale, and reactions after the lock drop. a locked ballot always names an entry — abstaining is not a ballot
follower AcquiredTasteState.guessers, AcquiredTastePhase.guesserGate/guesserRate a solo-mode Acquired Taste guesser — everyone but the influencer, each locking a secret decode in seating order
game category GameCategory, GameMetadata.category the kind of play (writing, guessing, bluffing…); shown on the picker card, future catalog grouping
game metadata GameMetadata stable id + display fields + accentColor + capability flags
game package packages/<game_name>/ a workspace package implementing MiniGame. "plugin" is the mechanism, not a user-facing term
game picker HomeScreen, gameCatalog* the home screen grid of MiniGame cards — sorted via the picker's sort sheet and narrowed via its session-only filter sheet
game use stats GameUseStats, summarizeGameUse, gameUseStatsProvider one game's play tally, feeding the picker's played-based sorts: completed calls only (an abandoned one banked playtime, not a play — same rule as the statistics screen). playCount sums terminal standings so replays count, lastPlayedAtMs is the freshest finish stamp
group size PickerFilter.playerCount the picker's "we are this many" filter facet — a game matches when GameMetadata.playerCountIsValid seats it
guess tolerance UnfocusedState.tolerance, unfocusedOptionTolerance the slider round option bounding a close read — how far off a guess may be and still bank 50
headword DictionairheadsState.word the nonsense word a Dictionairheads round defines — dictionary term, UI calls it "today's headword"
host WsHostSessionLink, LinkProtocol.hostPeerId ('host'), SessionLink.isHost, HostLobbyScreen the device running the session's embedded server and owning its state (ADR 0005). the UI card says "Host"; on the wire the host peer id defaults to host and is the only id a client may sendTo
host tools host_toolbox.dart, HostToolboxSheet, openHostToolbox, rootNavigatorKey, appModalRouteDepthProvider, TextShareService, hostTools* the floating toolbox bubble + sheet the host gets on every screen while its room link lives — pause/resume, per-peer bench/kick/health/mode rows, the invite QR, URL sharing, the known-players editor, diagnostics. mounts in MaterialApp's builder above the Navigator, so it opens sheets through rootNavigatorKey and folds away while a modal route is up. in TV mode the bubble is unreachable by controller, so the sheet opens through openHostToolbox instead — called from a focusable banner/session "Host tools" button or the Start/F1 binding
hop SketchySourcesState.hop/hopCount/hopProgress, SketchySourcesPhase.drawing/writing one turn of a Sketchy Sources thread — every assigned seat filing one artifact at once, odd hops drawings and even hops headlines; "hop" is the code name, the UI counts posts ("Post 2 of 5")
huddle UnfocusedPhase.deliberation, deliberationTitle teams mode's open discussion — the consumer's side agrees one shared guess aloud; "deliberation" is the code name, the UI says "huddle up" (Acquired Taste's fandoms call it "stan chat")
influencer AcquiredTasteState.influencer, AcquiredTastePhase.influencerGate/influencerReview the Acquired Taste player dealt the secret 1–10 rating, who posts one calibration pick per scale; the post is public, the number is not. the influencer banks the best decode of the post — being cryptic but legible pays
inspiration SessionOptions.inspirationEnabled, supportsInspiration the settings toggle gating inspire-me buttons; independent of the bot player
inspire-me VoteEntry.assisted, adjustedPointsFor* the per-answer "phone-a-friend" button (Ask the Muse / the Focus Group / Bard-O-Bot): an app-suggested answer that joins the vote flagged assisted and banks half points
intent RemoteGameChannel.sendIntent, {kind:'intent', body} a client→host request to change game state — the only inbound traffic on a link channel. the host validates it against the live phase and seat before it reaches the controller; malformed, mistimed, or unseated intents are dropped, not errors
join code SessionCode, multiplayerLobbyCodeLabel, JoinTicket.token the 4-letter code the host lobby shows so friends can join by typing address:port + the code — it IS the session token, same value, typed not scanned
join ticket JoinTicket, HostLobbyScreen's QR, JoinScreen the payload a client scans to join. The QR encodes the URL form (joinUri — http://ip:port/?v=1&t=…&ssid=…&psk=…&compat=…&build=…) so any camera opens the host-served web client; displayUri emits the remote-typable TV form (/d or /d/<code> — the code rides the path, see display path) for the observer-facing flow; encode()'s canonical JSON {v, ip, port, token, ssid?, psk?, compat?, build?} remains the persisted contract and stays parseable via JoinTicket.parse. ticketVersion and all field/param names are wire contracts once shipped; compat/build announce the host's wire version and build label so a mismatched scanner can skip a doomed native handshake and open the web client instead. UI copy calls it "the code"
known player KnownPlayer, player_history, PlayerChipPicker a record of someone who has played — one shared pool across pass-and-play and rooms, ordered most recently used first. it powers the setup screen's quick-pick chips and roster pre-fill AND the chip row under both room name fields; the device's own room identity records under the reserved self id, adopting an existing entry's id on a name match
kick SessionLink.kickPeer, EvictReason.kicked, multiplayerErrorKicked, hostToolsKick* the host's hard eject from the toolbox — evicts the socket with kicked (distinct from seat_taken so auto-reconnect stands down and the remembered credential clears), drops the resumeKey, and retires the peer id for the room's life, since the claim handshake would otherwise let a rejoining hello re-seat into peer-id-keyed game state. a kicked peer's fresh join mints a brand-new seat; the host can't kick itself or an observer's display
leaked end leakedLineEnds, tail, lastWordsOf, poetryLeakedEndsLabel the last ≤N words of each of the previous ≤M lines, shown to the current Desert Island Poetry writer with the author's name; the leak_lines/leak_words round options size M and N (0–3 or all, default 2). on the wire the tail still ships under the lastWord key for compat
LibreBash LibreBash, librebash the app and the project. binary, package, and bundle names are all librebash; the app ID is dev.johnkneev.librebash
line / stroke DrawStroke, DrawingController (draw_core) one continuous pen-down-to-pen-up gesture; a held stroke auto-splits into a new line every 7 seconds so line-level mechanics can't be gamed. undo takes back only the newest line (no chaining), bounded by the round's undo budget
link StoryLink, storyLinksFor, SketchySourcesState.currentLinks one adjacent headline↔drawing pair on a Sketchy Sources story chain — the unit the ballot stars: a starred link pays both endpoint authors 100, a reaction pays both 10, and an endpoint with no author (the dealt seed or a mod-posted caption) simply pays its real side
link envelope LinkEnvelope, LinkProtocol the versioned wire frame the socket transport serializes: {v, type, gameId?, payload?} — link.* types (link.hello/welcome/refused/peers/pick/started/returned/ended/ping/pong) are transport-reserved, game.relay carries game payloads, everything else is game-owned. field names and protocolVersion are wire contracts
lobby HostLobbyScreen, WsHostSessionLink.acceptingJoins/closeLobby, RefuseReason.lobbyClosed the gathering phase of a room — joins land only while it's open. closeLobby() only locks the door; game calls announce themselves separately (link.started) and the room accepts joins between and during them
mingle OffByOnePhase.questions, QuestionPairing, derangedTargets the Off by One ask-around — the device passes in seating order and each holder asks one shuffled seat their dealt convention prompt; the pairings derange, so nobody asks themselves and every seat gets asked once
mini game MiniGame one playable game behind the plugin contract. UI copy may shorten it to "game"
motif GameMetadata.motifEmoji, GameBackdropScope, EmojiBackdrop, GameSticker a game's decorative emoji set (🐒🎭🪶💀 for Infinite Monkeys, …) — wallpapers the home/setup/session backdrops and corners the GameSticker on its picker card. decorative only, never persisted; an empty list means no backdrop
my order PickerSort.manual, picker_order, showPickerArrangeSheet the picker's arrangement sort — the user's own card order, dragged into place in the arrange sheet and persisted as a game-id list; games the list doesn't know sort after it in registry order. picking it in the sort sheet reveals the "Arrange games" editor
name-drop dilemma PlayerDilemma, authoredPlayerDilemmas, dealPlayerDilemma, playerDilemmaRate a This or That? dilemma that puts two seats inside the question ("have {p} plan your vacation or {p}?"). each side's {p} token — a content contract — fills with a different seat, never the asker, so it only deals at 3+ players at a 15% per-deal rate
nonsense word randomNonsenseWord, NonsenseWord, WordRole a generated pronounceable fake word + fake part-of-speech tag; syllable bounds are configurable
noun entry NounEntry one pool noun as structured data: word + authored article + optional plural, so articleFor/articleForPhrase and plural slots never guess
network-only GameMetadata.requiresNetwork a game that can only start over a Play Together room (isPassAndPlay: false + requiresNetwork: true + supportsRemote: true). the setup screen's roster swaps for a "Play Together required" panel routing to /multiplayer, start stays locked until a room is hosted, and the picker card wears the room badge
obliged peer SessionLink.setObligedPeers, LinkBridge.obligedPeerIds, FakeSessionLink.obligedPeerIds a seat the live call is currently blocked on — the peers whose silence the link is allowed to time out (see silence limit). the bridge republishes the set with every projection and clears it when the call ends; the default is the whole roster, and each game narrows it to whoever owes input — usually the same seats its wire awaiting list names. an unobliged peer is never timed out — idling through someone else's turn isn't an offense
observer LinkRole.observer, multiplayerObserverFallbackName ("a display"), multiplayerJoinPresentTitle ("Present this room") a joined device that renders only the public channel — the presenter role; holds no seat, sends no input, must never receive private payloads. joined via the join screen's "Present this room" switch, a TV browser opening the display path (/d), or an Android wireless-display projection — all three land the same web-client observer; its room banner keeps the join QR on-screen (a browser observer's lobby wears the same InviteGraphic above its roster) and its card taps no-op — picks are the players' job
order DrawWhatNowStrings.orderLabel, DrawWhatNowState.prompts what Draw What Now? calls a seat's secret drawing prompt ("a monkey in a cowboy hat") — private on the seat's phone until the reveal
orientation request OrientationRequest, OrientationRequests, orientationRequestsProvider (game_mechanics), PhoneOrientationLock (lib/) a subtree asking for a specific device orientation: the widget registers into the app's request stack for its mounted lifetime, newest wins, and the root lock — the only SystemChrome caller — applies it while live, falling back to portrait on handsets / free on tablets when the stack empties (no game requests one today — Buildboard's easel tried landscape in playtest and stayed portrait)
out seat OffByOneState.outIds, OffByOneOutMode, wasOut (wire) an Off by One seat dealt the minority program — nobody is told which side they're on, so the tag stays unsent until the reveal opens; the count is the wrong_programs round option's one/few modes
paint palette paintColorPool, pickPalette (draw_core) the shared 21-color pool drawing games deal per-seat palettes from — white excluded (the canvas is white); pickPalette samples it without replacement
pass-and-play PassDeviceGate, passDevice* one device handed around between turns. the gate hides secret state during the handoff
pause SessionLink.paused/pausedChanges/setPaused, LinkProtocol.pausedBroadcast, SessionPausedScope, PausableDeadline, hostPaused* the host's room-wide hold from the toolbox — one link.paused frame freezes every screen behind a "paused" card (host's card carries Resume; clients can't lift it) and the welcome echoes it so a mid-pause join lands held. SessionPausedScope feeds TurnTimer's freeze, PausableDeadline is the controller-side seam a hosted cutoff adopts (Draw What Now's phase deadline is the one today)
picker filter PickerFilter, pickerFiltersProvider, pickerFilter* the picker's card narrowing — group size, pass the phone, and game type, ANDed together. session-only by design (a keepAlive provider, never persisted): a stale pick can't hide the shelf next launch
picker sort PickerSort, pickerSortSettingProvider, pickerSort* the game picker's card order — shuffle (the default; see "shuffle"), manual ("my order"), alphabetical, recentlyAdded (reverse registry order), recentlyPlayed, mostPlayed. persisted under picker_sort; the played-based sorts read gameUseStatsProvider
player Player a person in the roster; identity is name + emoji + palette color, no accounts
play together multiplayer*, MultiplayerScreen, /multiplayer/*, ActiveLinkController the UI's name for local multiplayer — every player on their own phone over the shared local network. the link crosses routes through ActiveLinkController (the keepAlive handoff, same pattern as SessionSetupController) and the app shell owns closing it
player chip row PlayerChipRow the tap-a-player picker for "who guessed it" moments
peer LinkPeer, LinkRole, Player.peerId a device attached to a session's link; peerId binds a roster Player seat to one (null means the seat shares the local device). LinkPeer carries the peer's name, emoji avatar, colorIndex palette slot (the peer's own pick — unset falls back to a deterministic id fold), gamePick (the peer's mark on a remote-capable game while the room waits — link.pick client→host, rebroadcast on the roster, cleared when a call starts), renderMode (LinkRenderMode.web when the host-served bundle renders this peer — roster key render, the browser chip), fallbackCapable (roster key fallback — the peer can hand a mismatched call to the embedded fallback, so seatableFor still seats it), and sittingOut (the host's bench flag — WsHostSessionLink.setPeerSittingOut, roster key out, room-scoped: a benched peer keeps the room but takes no seat until dealt back in, and the flag survives a seat reclaim since it keys on the peer id, not the socket). LinkRole.observer peers get the public channel only — never private payloads
poem PoetryGameState.lines the assembled work shown at the reveal
poetry line PoemLine one authored line + its author, inside a poem
points scoreboard PlayerScoreboard the ranked points list for games that score without a vote round — VoteScoreboard is for ballots
post ReviewPost, acquiredTasteOptionPosts ("Posts"), AcquiredTastePhase.prompt one Acquired Taste turn's content — the dish under review plus the influencer's dealt scales; the typed calibrations join it once the review posts. the turn indicator counts posts ("Post 2 of 6")
program OffByOneState.dealtWords/majorityWord/minorityWord what Off by One calls a seat's dealt secret word — "the thing on your convention program". dealtWords never crosses the wire: each seat's own program rides only its private view for the whole live round
rating UnfocusedState.answer, ratingMin/ratingMax the consumer's secret 1–10 answer to the survey question; guesses score against it. Acquired Taste deals its rating (AcquiredTasteState.answer) instead of asking for it
reaction Ballot.reactions, reactionEmojis, voteReactions, castReactionFor one emoji per entry per voter — worth 10 points; every emoji scores positively. networked votes carry them on the locked ballot; a forced pick (or any seat) can also file them standalone through the react intent, where they collect on a per-seat voteReactions sheet the lock then replaces
Research-O-Bot UnfocusedGroupStrings.botName, UnfocusedState.botGuess the Unfocused Group bot persona — throws a random guess into every reveal for comparison; never scores and never counts toward the consumer's banked read
researcher UnfocusedState.guessers, UnfocusedPhase.guesserGate/guesserRate a solo-mode guesser — everyone but the consumer, each locking a secret guess in seating order
remote view MiniGame.buildRemoteSession, RemoteSession, RemoteGameChannel, GameView, RemoteGameScreen the UI a seated phone renders during a networked call: a projection of host state (public for everyone + private per seat) plus the intent path back. the host's own seat runs the same view in-process through its link bridge
reveal PoetryPhase.reveal the end-of-game readback step. generic code term for "show the hidden result"
remote draft RemoteDraftStore, RemoteGameViewState.draftScope/draftSnapshot/restoreDraft, remote_draft/ keys a remote seat's unfiled input persisted device-locally (debounced on edit, flushed on background/dispose) so a dropped link's rejoin resumes mid-obligation — drawing, writing, rating, ballot. keyed by room + call + seat + scope; never authoritative — a draft is only local input that still has to pass intent validation
roster players*, GameSetupScreen the list built on the setup screen; "roster" in docs/comments, "players" in UI copy — each game names its own cast via GameMetadata.playersTitle ("Shipwrecked poets", "Rescue agents")
round round* a full pass through the roster. don't use for a single player's turn
round option RoundOption, GameMetadata.roundOptions, SessionOptions.roundValues a gameplay knob a game declares on its metadata — count, choice, switch, or slider, rendered by the shared controls on the setup screen's "Round settings" tile; persisted as round_option_<gameId>_<key> and resolved into typed values at start
round picks roundVoice, roundSpeechRate the voice + rate a game resolved for the current round; rolled once from the session options where null means "random each round"
room RoomBanner, ActiveGame, SessionLink.activeGame/gameChanges, SessionLink.roomName, FoundLobby, LobbyBeacon, LobbyBrowser, RoomNames, RoomPresence, roomJoinFor, multiplayerRoom* the long-lived object a multiplayer session lives in — a bound link that persists across games and keeps accepting joins. the host calls a game into it (startGame → link.started, repeatable), everyone returns when it ends (returnToRoom → link.returned), and peers drop in/out between and during calls — a mid-call joiner waits unseated for the next one. both ends see it the same way: RoomBanner on HomeScreen (name, code, live roster, invite QR) — the exit button reads "End the room" for the host and "Leave the room" for a client, whose JoinTicket re-shares the invite. outwardly it's also the mDNS-advertised name joiners pick from a list — the host types one or RoomNames.generate() invents it ("The Salty Kraken", or "Chez Margie" — the first name slot is the host's when they're named); the name also rides the welcome + link.peers frames so the client's banner titles itself the same. a locked room asks for the session code (locked TXT flag); a hidden room skips mDNS entirely (multiplayer_hide_room, QR joins only). the join screen's code travels only to the picked room's endpoint — never probed at every discovered host
rules GameMetadata.rules, GameMetadata.rulesTitle the game's how-to-play text, shown from the setup screen's Rules button in a dialog titled in the game's voice ("The field manual", "Stage directions")
resume key WsClientSessionLink.resumeKey, link.hello's resume, link.welcome's resumeKey, RefuseReason.seatHeld, LinkProtocol.evicted/EvictReason.seatTaken, multiplayer_last_room_resume_key the seat credential a welcome deals next to the peer id (ADR 0006) — a reclaiming hello must quote both, so enumerable peer-N ids alone can't steal a seat's private channel. room-scoped on the host, never on the roster; a wrong or missing key on a held id refuses seat_held, and a correct claim on a live socket evicts the incumbent with link.evicted so it stands its reconnect down
scene MonkeyScene, SceneLine one Shakespeare excerpt in Infinite Monkeys: play + act/scene + speaker-attributed lines with {type|original} blank tokens
score / scoring score*, SessionOptions.scoringEnabled always optional — never required to finish a game. scoring-on sessions run a vote round
scoreboard VoteScoreboard the shared post-vote ranking: players + their scored entries with reactions flying through
screen pinning ScreenPinningService, screen_pinning channel Android startLockTask during sessions; no-op elsewhere. UI does not name it
seat ActiveGame.seatPeerIds, Player.peerId, ActiveGame.isSeated a peer's claim on a call — fixed when the game is called. networked rosters set Player.id == peerId so the wire and the controller speak one id; a seat closes while its peer is dropped mid-call and re-opens if the peer reclaims its id on reconnect (credentialed by the resumeKey — see resume key). setup only seats peers seatableFor the pick — natively compatible (canPlay) or fallback-capable (canPlayViaFallback, plays through the hosted web client) — an incompatible peer with no render path has its switch forced off and locked for that game, so a phone that can't show the call never holds a seat in it
seat driver SeatDriver, GameSession.seatDriver, SessionSetup.seatDriver, sessionSeatDriverProvider, LinkBridge.seatDriver, SeatDriverBar, debug_seat_driver, settingsSeatDriver* the debug-mode switch that runs a network-only call on one device — "drive every seat". setup mints each typed roster seat a loopback peerId and the driver hands the device to whichever seat owes input next (auto-advance off the bridge's obliged set, or a manual pin from the strip); the host channel serves the driven seat's private view and attributes its intents to it. inert unless debug_mode is also on
seat key CallSeat.key, remoteSeatPrefix (remote:), PlayerHistoryRepository.selfId the person identity a call record aggregates under: pass-and-play seats keep their player_history id, the local network seat resolves to self (or a name-matched known player's id — the same adoption rule the room screens use), and every other networked seat becomes remote:<lowercased name> — peer ids are per-call credentials and never persist. two same-named remotes suffix #2, #3 rather than merge into one row
seating order players (the List<Player>) the order GameSession.players is passed in; the roster order
session GameSession, session* one run of a mini game: players seated, game played, ended via onExit
session options SessionOptions, resolveSessionOptions the merged global + per-game settings baked into a session at start; changing settings mid-game never affects it
session link SessionLink, LocalSessionLink, GameSession.link the transport seam every session runs on (ADR 0005): peer join/leave events, a message stream, sendTo/broadcast. pass-and-play runs the LocalSessionLink loopback — sends deliver back to self — so a game written against the contract works on one device or many; the app shell injects and closes networked links. the socket implementation is WsHostSessionLink/WsClientSessionLink in packages/session_link
session token JoinTicket.token, SessionCode, RefuseReason.badToken, WsHostSessionLink.bind(token:) the per-session secret in the join ticket that gates the lobby — a 4-letter code from SessionCode's unambiguous alphabet (no I/O/U), shown on the host screen as "Session code: WXYZ" and typed by manual joiners; the same value rides the QR. ~234k codes is enough: every wrong guess costs a full socket handshake the host closes on sight
setup screen GameSetupScreen, /setup/<gameId> the one metadata-driven setup every game shares: roster + bot player + known-player picks + teams tile (team-capable games) + round settings + common settings + rules + start, before a session
Sharedit SketchySourcesStrings.writing* copy, sketchySourcesWrite*/sketchySourcesReveal* ARB keys the fictional text-only forum Sketchy Sources headlines are "posted" to — framing only; a writing hop's UI copy speaks in its voice ("Post to Sharedit") and its users are Shareditors
shuffle PickerSort.shuffle, pickerShuffleSeedProvider, pickerSortShuffle the picker's default sort — a seeded random deal of the card grid rolled once per app launch (pickerShuffleSeedProvider is keepAlive, so the order is stable for the session and re-deals next launch); the seed travels into applyPickerQuery so a rebuild never re-deals mid-view
silence limit defaultPeerSilenceLimit, WsClientSessionLink.isStale, SessionLink.setObligedPeers how long a socket may go quiet (no inbound frame of any kind — a live client pongs automatically) before the link may call it dead, AND the gate on when that verdict is allowed. silence past the limit means "can't respond" — frozen tab, suspended app, stalled radio — but a peer is only timed out while it's an obliged peer (the call is waiting on its input); idling through someone else's turn keeps the seat. server sockets carry no protocol pingInterval, so dart:io's missed-pong close can't preempt the gate. isStale is the client's read of its own clock — a resumed page treats a stale link as already lost and reclaims
sketch SketchEntry, submitSketchFor, sketchesBy one filed billboard painting in Buildboard — the wire-map DrawingDocument in the 4:3 buildboardCanvasSpace; authorship hidden until its client's reveal
skipped MaroonOutcome.skipped, MaroonState.skipLimit/skipsLeft the describer bails on the card and draws the next — never ends the turn. the per-turn budget is a round option (off, 1–3, unlimited); with a limit each skip costs the rescuer 25 points, unlimited is free
speaker Speaker, GameSession.speaker the read-aloud contract handed to every game; FlutterTtsSpeaker in the app, NoOpSpeaker in tests. the web client substitutes per surface and role — BridgeSpeaker inside the app's embedded fallback (the LibreBash channel lands utterances on the real engine), WebSpeechSpeaker on a browser player seat (the Web Speech API, with a watchdog so a dropped end event can't wedge the queue), RemoteHostSpeaker on a browser display (plays the WAV clips the host renders and ships, through WebClipPlayer's <audio> element; its own presses round-trip tts_request/tts_audio first and only fall back to Web Speech) — while a stable SwappableSpeaker lets the shell re-pick that engine without orphaning the live queue. DuckingSpeaker wraps either side's engine so a read dips the soundtrack. On a networked host DisplayRelaySpeaker wraps the session engine: with observer peers attached every speak ships a host-rendered speak frame to the displays instead of voicing locally (a lone projected mirror excepted — same audio channel), and its stop ships speak_stop so far-side queues drain too. the optional synthesize/acceptsClips capabilities are how a host renders the clips and a display announces it can play them. UI calls it "read aloud"
stan chat acquiredDeliberationTitle/acquiredDeliberationBody, AcquiredTastePhase.deliberation the Acquired Taste fandom's open huddle in teams mode — the fandom decodes the post together aloud while the influencer keeps a straight face
statistics StatsScreen, /stats, stats* ARB keys, GameStatsRepository, summarizeCallStats, StatsTotals the play-history rollups over every finalized call record — a person-free "Overall" section (sessions, games, players seen, playtime, per-game split — counted once per call, not per seat), per-player games/wins/win-rate/scores/streaks/playtime, per-game splits, head-to-head rivalries, and room leaderboards. a chart icon on the home screen opens it; UI copy just says "Statistics"
steal guess OffByOnePhase.steal, OffByOneState.stealPicks/guessOptions, offByOneStealPoints the out seats' five-option shot at naming the majority program — worth 50; the option list ships room-public but only out seats may pick (their own wrong program is never an option)
story StoryChain, StoryArtifact, SketchySourcesState.stories/revealOrder/storyIndex one Sketchy Sources chain — the dealt wire headline plus every hop's drawing-or-headline in order; "story" is the code name, the UI calls it a "thread" and walks them one at a time in a shuffled order
string scope <Name>Strings, <Name>StringsScope how localized text crosses the package boundary (see ARCHITECTURE.md)
study unfocusedOptionRounds ("Studies"), UnfocusedState.roundsTotal an Unfocused Group session's length — solo, one round is every player consuming once; in teams mode, every team taking a turn
survey question UnfocusedState.prompt, UnfocusedPhase.prompt the public silly question the consumer rates — shown to the whole room and speakable through the speaker; only the answer is secret. the turn indicator calls it a survey ("Survey 2 of 6")
tagline SloganEntry, submitSloganFor, slogansBy one filed slogan in Buildboard — written for the seat's assigned client, authorship hidden until the reveal; banks its author 50 per vote its billboard receives, except the author's own vote
team Team, Player.teamId, SessionOptions.teams, TeamsTile, TeamAvatar a named side in teams mode: id + editable name + emoji + palette color + memberIds; declared per game via GameMetadata.supportsTeams/minTeamSize, dealt and edited on the setup screen's Teams tile, resolved into SessionOptions.teams, and carried on each Player.teamId
teams mode game_teams_<gameId>, team_state, TeamScoreboard the optional split of the roster into competing teams — always a choice, never a requirement; the remembered split persists in team_state keyed to a roster signature (the same humans keep their teams across games, including games that don't use teams; any roster change means re-picking). team scoring is per-game: Unfocused banks one shared guess per side, Maroon banks the whole card to the describer's side (150 a rescue), Acquired Taste banks the fandom's one shared decode
the mods SketchySourcesStrings.modByline, a null StoryArtifact.authorId the house byline on authorless Sketchy Sources artifacts — every dealt seed headline is the mods', and a seat whose writing hop times out gets a caption "posted by the mods"
the menagerie identifiedRecords, menagerieLabel the rescued-creature gallery on the no-scoring results screen
timer TurnTimer optional per-turn countdown; games decide whether one exists
Troll-O-Bot AcquiredTasteStrings.botName, AcquiredTasteState.botGuess the Acquired Taste bot persona — the comment-section troll throwing a random dart into every reveal; never scores and never counts toward the influencer's banked read
TTS voice TtsVoice, TtsVoiceDisplay, TtsVoiceLabelService a raw engine voice map and its formatted picker label ("English (United States) – Samantha"). the map always carries name/locale; piper multi-speaker voices add engine ("piper"), model (the .onnx basename — basenames survive AppImage mount-path moves), and speaker (the numeric speaker id inside the model)
turn turn*, currentPlayerIndex one player's move; ordering follows seating order
turn timer level TurnTimerLevel, TurnTimerPaces off/quick/standard/extended; each game declares its own durations per level in GameMetadata.timerPaces
TV mode TvModePreference/TvModeSetting, tvModeActiveProvider, TvModeScope, TvDetectionService, tv_mode key, dev.johnkneev.librebash/tv_detection channel, FocusCenteringScroll, CompactRoomBar the couch-presenter posture — bigger text (stepping down to normal under tvTextScaleMinSurfaceHeight, ~720p-class), overscan margins, a pinned focus highlight, the docked controller legend, and host defaults that suit a television. the home picker scrolls by focus: FocusCenteringScroll parks the focused card mid-viewport and snaps to the top for first-screenful content so the banner/hero stay reachable, while CompactRoomBar docks under the chrome row once the full banner scrolls off. the setting (auto/off/on) resolves against the detected form factor: tvModeActive is true on a detected Android TV under auto, or anywhere under on — Linux has no leanback answer, so "On" is the way in there
typos maybeTypo, seasonedForBallot, typoPlain, SessionOptions.typosEnabled, GameMetadata.supportsTypos, curseWords, typos_enabled + game_typos_<gameId> keys optional seasoning that slips a plausible typo (~15% per entry, one or two edits: transposed letters, dropped doubles, keyboard-neighbor swaps, ie/ei swaps, dropped apostrophes, spacebar slips) into text the app files onto a shared ballot — the real prompt/dream in Draw What Now? and Dreamweavers, plus every -O-Bot/Muse/understudy entry in the bot-playing vote games — so clean spelling can't out them against what players typed. Only app-authored text is seasoned, never a player's own writing; the dealt prompt/dream stays canonical. Each slip annotates itself in place (~~typo~~ __canonical__), and typoPlain folds it to the ballot text voters saw — every surface reads through it, reveals and scoreboards included, because a restored correction would flag the entry as app-authored even once names are public. An output guard rejects any mutation that introduces a banned substring, and short/acronym/digit tokens are never touched. Games opt in via supportsTypos; the global default is on
uptoast AcquiredTasteState.scores, acquiredPointsLabel what Acquired Taste calls its points — decodes and banked reads both pay in uptoast. was "clout" — the rename ties the currency to toast (bread + a cheer) so it still reads as a social-media like
venue dealVenue, venueTemplates/authoredVenues/curatedVenues, venueTemplatesKey (game_content/venues.dart), AcquiredTasteState.venue, ReviewPost.venue the shared restaurant-name generator — Acquired Taste's review target ("tonight, at The Salty Turnip", read aloud inside a framing sentence so the two random names never smear together) and one in five Buildboard clients. the deck keys stayed acquired_taste_* when the generator moved — they're persisted contracts
verb entry VerbEntry one pool verb with its forms: base, -ing, past, and past participle (defaults to past)
vote entry VoteEntry one authored thing up for votes (poem line, definition…)
vote round VoteRound, VoteResult the post-game pass-and-play ballot: star a favorite, emoji the rest
vote subject VoteRound.subject what's being voted on, shown on every ballot (the nonsense word, the prompt)
web client lib/web_client/, WebClientShell, WebClientConfig, WebClientBundle, session_link_client.dart, linkRoomCookie, assets/web_client/ the browser build of the app the host serves from its own room port (ADR 0006) — compiled by flutter build web --target=lib/web_client/main.dart into bundled assets, gated on the room token (?t= authorizes + plants the cookie). a phone browser joins with zero install and plays the same buildRemoteSession views; always exactly the host's build since the host serves it. its hello announces render: 'web'; inside the app's embedded fallback it additionally speaks the LibreBash bridge (see embedded fallback). a visibilitychange resume closes a stale link (isStale) so the drop path's auto-rejoin reclaims the seat instead of repainting a queued frame backlog; a drop while visible rejoins without a tap. audio policy splits by role: a player seat keeps the filtered cues and no music ("phones small"), while an observer — the shared display — owns the room's soundtrack, unfiltered sound effects, and say-it speech (the host quiets its own session audio while one is attached, restoring on leave; music tracks the staged bundle omits resolve through RoomStaticContent's native-asset fallback). the playback itself is browser-native — WebMusicPlayer/WebSoundPlayer put plain <audio> elements behind the providers' dart.library.js_interop conditional instead of the SoLoud engine, whose WASM fetch+decode a TV browser's CPU can't keep fed; index.html accordingly carries neither the init_soloud.js glue nor an AudioContext-unlock shim. display speech rides the host's voice by default: RemoteHostSpeaker plays the WAV clips RemoteSpeechCue synthesizes into each speak frame and round-trips its own presses through tts_request/tts_audio, since most TV browsers have no usable TTS — a lone observer may flip the lobby's "own voice" switch (display_own_voice) to speak locally, but a second display locks the room to one voice. BrowserChrome gives a remote-typed browser the chrome it lacks — a night-mode toggle and a fullscreen button on the join and lobby screens — and the shell answers the host's remote version of the same controls: {kind: 'display', action} frames addressed to observers ride link.messages, night writing the shared theme pref and fullscreen asking the document (best-effort — browsers gesture-gate it). URL params: t (token), s/k (seat claim), name, role=observer, embedded (hides the install affordance), projected (the host's own Presentation — a view-only mirror on the host's audio channel, so its say-it buttons hide); the /d[/<code>] display path lands the same page as a name-free auto-joining observer — see display path
wireless display WirelessDisplayService, WirelessDisplayState (supported/attached/shown), wirelessDisplayService/wirelessDisplayState providers, RoomDisplayPresentation, dev.johnkneev.librebash/wireless_display{,_events} channels Android's second-screen projection — the OS-attached presentation display (a Miracast/Wi-Fi Display sink or wired secondary screen) shows the room's display path page in a Presentation holding a plain WebView, while the phone keeps its own UI. sink pairing stays in system settings — the service reports attached/shown, answers show(url)/hide, and openDisplaySettings fires WIRELESS_DISPLAY_SETTINGS then CAST_SETTINGS then plain Settings so a user hunting "how do I cast" lands in the right panel instead of a dead switch. hide is the full teardown — switch-off and ActiveLinkController.release alike: our Presentation dismisses and, because we were projecting onto it, the OS's Miracast session drops too (disconnectWifiDisplay, removeGroup fallback — best-effort, a session we never projected onto stays the user's own). The projection stamps ?projected=1 on the page (see display path) — it shares the phone's audio channel and has no input, so RemoteHostSpeaker(interactive: false) hides its say-it buttons while GameSession.selfProjecting lets the host dedupe "read on the display" against "say it" when the projection is the only observer. Miracast is a phone-side capability too — each snapshot's supported reflects DisplayManager.getWifiDisplayStatus() (FEATURE_STATE_ON/OFF = can source WFD — Samsung's Smart View stack answers through the same adapter; a Pixel has none; config_enableWifiDisplay only serves as the reflection-blocked fallback since Samsung leaves it off), and a Miracast-less build greys the row with the reason — and points at the other doors: the /d browser path, plus a friend's device casting since the client app's tile projects the host's /d URL the same way (the casted page is an ordinary observer — control stays with the host). no-ops to unsupported elsewhere; the invite dialog hides the switch off-Android
word pool safe*, silly* pools in game_content curated all-ages-safe vocabulary games compose into titles/prompts; every entry must be safe under every combination — see word_pools.dart
wrap up DilemmasPhase.wrapUp, DilemmasController.wrapUp/keepGoing, thisOrThatWrap* the This or That? end screen — the session's dilemma tally plus the way out or back into the deck. reaching it (the group's button, or the chosen count spent) reports a completed call — an empty outcome, nobody crowned
wrap it up SessionLink.setWrapUp/wrapUpEndsAt/wrapUpDuration/wrapUpChanges/wrapUpPhase, LinkProtocol.wrapUp (link.wrap_up), WrapUpClock, WrapUpChip, PhaseCutoff, wrapUpMusicSlot, hostToolsWrapUp*, wrapUpExpiredFlash the host's "wrap up this phase" countdown from the toolbox — a floating pill on every screen, and a takeover for the phase it was armed under: the live phase's cutoff claims it (wrapUpPhase, host-side only) and it becomes that phase's projected clock and host-side cutoff, so a longer pick extends a timed turn and a shorter one hurries it; cancelling hands the phase's banked remainder back, expiry resolves the phase exactly as its own timer would. the binding is by phase type — the countdown carries into the next window of the same kind (the next piece's guessing, the next story's reveal) but dies when the phase type changes, and an unclaimed arm dies at the next phase that opens rather than taking over a clock it was never aimed at. dormant cutoffs make untimed phases wrappable too (ballots resolve, host-paced reveals advance like the host's "next"); the standings arm a claim-only one so a countdown can't outlive its game. pausing the room banks the countdown's remainder, and a MusicRegion pinned while armed swaps the soundtrack for the dedicated wrap_up track. same idiom as This or That's wrap up phase, different scope — game-end screen versus room-level nudge

Planned terms (not implemented yet)

These names are reserved so future code lands on the right vocabulary on the first try. They describe design direction, not current behavior — see docs/PROJECT_PLAN.md for context.

Planned term Intended meaning Notes
local multiplayer simultaneous play across devices on the same LAN/hotspot here: LAN play + the Android local-only hotspot tier work via packages/session_link, /multiplayer/*, HotspotService, and WifiJoinService; Dictionairheads is the first networked game and the presenter/observer tier rides the same link. no Play Services
network provider the device whose link everyone shares — the hotspot owner or the LAN itself usually the host device; a desktop on a LAN is a provider-by-circumstance. the "Provide the Wi-Fi" lobby action makes the host the provider via HotspotService (Android startLocalOnlyHotspot); clients attach app-scoped through WifiJoinService (WifiNetworkSpecifier) when a scanned ticket carries ssid/psk
offline mode default posture, not a setting the app never requires connectivity; don't build a "go offline" toggle
presenter the shared room screen — a dedicated device (TV'd desktop, spare tablet) or the host's own screen additive — every client already renders the public channel, and SessionLink.presents names the stance for either form. ships two ways: the join screen's "Present this room" switch for a spare device (connects as LinkRole.observer — no seat, ever), or the host lobby's same-named switch (the host keeps player authority but presents is set, the banner keeps the join QR on-screen, and hostPlays starts false — the setup screen's "deal me in" persists that room-scoped choice across calls; a new room re-derives it). an unseated host renders the spectator view with phase controls plus read-aloud, and the host can push reads to an attached display via {kind: 'speak'} relay frames carrying host-rendered WAV clips. a browser-based presenter ships as the display path (/d) — the invite dialog's TV-display face — and Android projects the same page through wireless display
save / resume pausing a session to storage and returning later will introduce a persisted session schema — see below

Persisted names are contracts

Renaming a Dart identifier is free; renaming a stored string is a migration. LibreBash has no on-disk schema yet, so this list is short — but anything already embedded in a shipped artifact counts:

  • GameMetadata.id values (desert_island_poetry, dictionairheads, …). Future save/resume and stats will key off them; they must never change once a game ships.
  • The screen-pinning method channel name dev.johnkneev.librebash/screen_pinning is a contract with the Kotlin side; rename both ends together or not at all.
  • The TV-detection method channel name dev.johnkneev.librebash/tv_detection is a contract between TvDetectionService and MainActivity.kt; its isTelevision method name must change on both ends together or not at all.
  • The window-control method channel name dev.johnkneev.librebash/window is a contract between WindowControlService and linux/runner/my_application.cc; its setFullscreen method name and bool argument must change on both ends together or not at all.
  • The hotspot method channel name dev.johnkneev.librebash/hotspot is a contract between HotspotService/WifiJoinService and MainActivity.kt; its method names (startHotspot, stopHotspot, joinNetwork, leaveNetwork), the startHotspot request args (ssid, psk), and its answer keys (ssid, passphrase, address) must change on both ends together or not at all.
  • The wireless-display channels dev.johnkneev.librebash/wireless_display (methods) and …/wireless_display_events (state stream) are a contract between WirelessDisplayService and MainActivity.kt; the method names (show, hide), the show arg (url), and the event keys (attached, shown) must change on both ends together or not at all.
  • The display path /d (and its single code segment /d/<code>) is a URL contract between JoinTicket.displayUri, the room server's static gate in ws_session_link.dart, and WebClientConfig — all three must agree on which segment carries the code.
  • Package names under packages/ feed dart pub lockfiles, imports, and (eventually) persisted session records that name their game.
  • tool/build-timestamp.json keys BUILD_TIMESTAMP and BUILD_NUMBER are read by lib/core/build_info.dart via --dart-define-from-file; build.sh, CI, and the Dart code must agree. BUILD_NUMBER is the git commit count — it exists because fastforge's internal flutter build restamps version.json with pubspec's +0, so package_info_plus alone can't report the real number inside Linux packages.
  • SharedPreferences keys owned by SettingsRepository (lib/data/repositories/settings_repository.dart — never rename them; globals first, then per-game overrides, then TTS):
  • bot_plays_enabled — bool, the global bot-player seat; absent means on.
  • display_own_voice — bool, a browser display's "use this display's own voice" pick; absent means the display plays the host's rendered clips. Only honored while it is the room's lone observer.
  • fullscreen_on_launch — bool, the launch-fullscreen pick (Linux window control; WindowControlService applies it); absent means a windowed launch.
  • inspiration_enabled — bool, the global inspiration default; absent means on.
  • log_level — the minimum persisted severity, a LogLevel.name (debug/info/warning/error); absent or unknown means info.
  • multiplayer_hide_room — bool, the host's hide-room choice; absent means rooms advertise over mDNS.
  • multiplayer_last_room_ticket / multiplayer_last_room_peer_id / multiplayer_last_room_role / multiplayer_last_room_name / multiplayer_last_room_resume_key — the dropped client's reconnect memory: the encoded JoinTicket, the peer id its welcome dealt, the LinkRole.name it joined as, the room's display name for the home card, and the seat credential the reclaim quotes. Written on every client join; cleared on a deliberate leave, a host goodbye, or a re-dial that found the room gone.
  • multiplayer_player_color — int, the remembered avatar palette index; absent means a random seed at first join, then the deterministic id fold until a pick lands.
  • multiplayer_player_emoji — String, the remembered avatar emoji both name fields carry; absent means the name-initial fallback.
  • multiplayer_player_name — String, the remembered display name both the host lobby and join screens prefill from; absent means the fields start empty.
  • multiplayer_room_name — String, the host's hand-typed room name, prefilled into the next lobby so room-level stats keep grouping under it; absent means the lobby generates a fresh suggestion. Generated names are never written here — only typed ones.
  • picker_order — StringList of GameMetadata.ids, the manual card order behind the "My order" sort; absent means registry order.
  • picker_sort — String, the PickerSort.name the picker sorts by; absent or unknown means shuffle.
  • scoring_enabled — bool, the global scoring default; absent means on.
  • screen_pinning_enabled — bool; absent means the first-run pinning prompt has never been answered.
  • theme_mode — the ThemeMode.name for the light/dark pick (system/light/dark); absent or unknown means system.
  • turn_timer_level — the global TurnTimerLevel.name (off/quick/standard/extended — the wire values are contracts too); absent or unknown means off.
  • tv_mode — the TvModePreference.name for the TV-mode pick (auto/off/on); absent or unknown means auto.
  • game_inspiration_<gameId> — bool per-game inspiration override; absent means inherit the global.
  • game_scoring_<gameId> — bool per-game scoring override; absent means inherit the global.
  • game_teams_<gameId> — bool per-game teams-mode pick; absent means teams off (solo play).
  • game_timer_<gameId> — TurnTimerLevel.name per-game timer override; absent means inherit the global.
  • round_option_<gameId>_<optionKey> — the stored scalar for one declared RoundOption (int, String, or bool, matching the option's serialize); absent means the option's default. The <optionKey> constants live in each game package's *_options.dart — never rename a shipped key.
  • sfx_enabled — bool, the sound-effects master switch; absent means on.
  • sfx_volume — double, the sound-effects master volume 0.0–1.0; absent means full.
  • tts_speech_rate — double speech-rate multiplier (1.0 = normal); absent means "random each round" in 0.5–1.5 (piper narrows the roll to 0.8–1.2 — its speakers already vary in pace).
  • tts_voice — JSON map of the chosen engine voice ({"name": …, "locale": …}; piper voices also persist engine, model, speaker); absent or malformed means "random each round".
  • SharedPreferences keys owned by PlayerHistoryRepository (lib/data/repositories/player_history_repository.dart):
  • last_players — JSON list, the last session's roster in seating order; pre-fills the next setup screen.
  • player_history — JSON list of known players, most recently used first ([{id, name, emoji}]). shared across modes: pass-and-play sessions upsert the whole roster; room flows upsert the one local identity under the reserved self id (a name match adopts the existing entry's id, so "Maya" on the couch and "Maya" in a room are one entry).
  • team_state — JSON map, the remembered team split: {"signature": "id1,id2,…", "teams": [{id, name, emoji, colorIndex, memberIds}]}; the signature is the sorted ids of the human roster the split was picked for, and the split is only offered back while the roster still matches it.
  • SharedPreferences keys owned by GameStatsRepository (lib/data/repositories/game_stats_repository.dart):
  • game_stats — JSON list of finalized GameCallRecords, oldest first, capped at 500. The record's field names are wire contracts too — the same JSON crosses the room's link in call_record messages: v (GameCallRecord.formatVersion), id, game, mode, room, start, end, playMs, status (a CallStatus.name), seats (key/name/emoji/peer/team), outcomes (scores/win/winTeams).
  • game_stats_live — JSON list of {record, stamp}: the calls still in flight plus their heartbeat. A stamp gone quiet past two minutes reconciles to an abandoned record on next launch.
  • The multiplayer wire contracts — not on-disk, but baked into shipped builds on both ends of the socket, so they version rather than rename:
  • LinkEnvelope fields (v, type, gameId, payload) and LinkEnvelope.protocolVersion.
  • LinkProtocol type strings (link.hello, link.welcome, link.refused, link.peers, link.started, link.returned, link.ended, link.ping, link.pong, game.relay), the link.started payload shape ({gameId, seats, call} — call is the per-call id drafts namespace by; absent on pre-call-id builds), the link.welcome game payload's same optional call field, the link.hello payload's optional peerId claim (a returning client re-asserting the id its last welcome dealt — peer-* ids only), the version-announcement fields (compat/build/games on hello, welcome, and roster entries — linkCompatVersion plus each MiniGame.remoteCompatVersion are the values they carry), the render announcement fields (render — 'web' on the host-served client, absent for native — and fallback on hello and roster entries), the roster out bench flag (host-written, present only when true), the seat-credential fields (resumeKey on link.welcome, resume on link.hello's claim), link.paused and the welcome's paused seed, link.wrap_up and the welcome's wrapUp seed ({ms, total} — a relative remainder plus the armed length, never a wall-clock deadline), link.evicted and its EvictReason values (seat_taken, kicked), the RefuseReason values (bad_token, malformed, lobby_closed, seat_held, name_taken), and the default hostPeerId (host).
  • The call_record link-message kind — {kind: 'call_record', record: <GameCallRecord JSON>} broadcast by the host when a call finalizes. Peers that don't know the kind ignore it; the record's field names and formatVersion are the contract.
  • JoinTicket fields (v, ip, port, token, ssid, psk, compat, build) and JoinTicket.ticketVersion.
  • The host-served page contract — the app's embedded fallback and the browser shell share it: the static routes (/ bundle, /ws socket, /install, /app, /app.json), the librebash_room cookie, the URL params (t, s, k, name, role, embedded), the LibreBash JavaScript channel name and its {m:…} message methods (haptic, speak, stop, turnPing, getVoices), and the page's window.librebashDeliver reply hook ({id:…} answers requests, {event:'utteranceDone'} reports speech ends). A web bundle must agree with the serving app's side of all of these.
  • The mDNS advertisement (lib/core/lan_discovery.dart): the service type lobbyServiceType (_librebash._tcp), the TXT attribute locked ('1'/'0' — absent reads as locked), and the rule that the service instance name is the room's display name. The session code must never appear in the record.
  • SharedPreferences keys owned by RemoteDraftRepository (lib/data/repositories/remote_draft_repository.dart):
  • remote_draft/<room>/<gameId>/<callId>/<seatId>/<scope> — a remote seat's unfiled input (drawing board, text field, ballot) as JSON. <room> is a stable hash of the link's roomTicket, <callId> comes from ActiveGame.callId, and <scope> is the game's own per-obligation discriminator (draw:r1, guess:r1:d2, …). Keys are composed by RemoteGameViewState; drafts die on host-confirmed filing and a stale-call sweep clears leftovers per room+game.
  • SharedPreferences keys owned by DrawDeckRepository (lib/data/repositories/draw_deck_repository.dart):
  • draw_decks — JSON map from pool key to {seed, position, size}, one record per content pool that has dealt at least once; the seed orders the pool's shuffled cycle and the position marks the next deal inside it. Pool keys are contracts too: authored constants (e.g. unfocused_survey_templates, maroon_templates) or poolDeckKey fingerprints — either way, a renamed or reshaped key silently starts a fresh cycle.

When further persistence lands (saved sessions, more settings), add every stored key, table name, JSON field, and enum wire value here before shipping it. Names listed here are frozen; if one must change, ship a migration and keep the old value in the glossary marked legacy.

Legacy names

None yet — the project is new. When a persisted name is superseded, it moves here with a note about where the compat reader lives. Legacy names may only appear in migrations and compatibility code, never in new features.

Naming rules

  • ARB keys are camelCase named after where the string lives: <feature><Thing><Role> — poetryRevealTitle, mechanicsIAmPlayer.
  • Package names are snake_case and match the game's user-facing name where practical: desert_island_poetry for "Desert Island Poetry".
  • Feature folders under lib/features/ are named for the user-facing surface (home, multiplayer, session, settings, setup, stats), not the mechanism.
  • Don't reuse a glossary term for a different concept. If the natural name is taken, pick another word and add it to this file first.