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.idvalues (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_pinningis a contract with the Kotlin side; rename both ends together or not at all. - The TV-detection method channel name
dev.johnkneev.librebash/tv_detectionis a contract betweenTvDetectionServiceandMainActivity.kt; itsisTelevisionmethod name must change on both ends together or not at all. - The window-control method channel name
dev.johnkneev.librebash/windowis a contract betweenWindowControlServiceandlinux/runner/my_application.cc; itssetFullscreenmethod name and bool argument must change on both ends together or not at all. - The hotspot method channel name
dev.johnkneev.librebash/hotspotis a contract betweenHotspotService/WifiJoinServiceandMainActivity.kt; its method names (startHotspot,stopHotspot,joinNetwork,leaveNetwork), thestartHotspotrequest 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 betweenWirelessDisplayServiceandMainActivity.kt; the method names (show,hide), theshowarg (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 betweenJoinTicket.displayUri, the room server's static gate inws_session_link.dart, andWebClientConfig— all three must agree on which segment carries the code. - Package names under
packages/feeddart publockfiles, imports, and (eventually) persisted session records that name their game. tool/build-timestamp.jsonkeysBUILD_TIMESTAMPandBUILD_NUMBERare read bylib/core/build_info.dartvia--dart-define-from-file; build.sh, CI, and the Dart code must agree.BUILD_NUMBERis the git commit count — it exists because fastforge's internalflutter buildrestampsversion.jsonwith pubspec's+0, sopackage_info_plusalone 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;WindowControlServiceapplies it); absent means a windowed launch.inspiration_enabled— bool, the global inspiration default; absent means on.log_level— the minimum persisted severity, aLogLevel.name(debug/info/warning/error); absent or unknown meansinfo.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 encodedJoinTicket, the peer id its welcome dealt, theLinkRole.nameit 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 ofGameMetadata.ids, the manual card order behind the "My order" sort; absent means registry order.picker_sort— String, thePickerSort.namethe picker sorts by; absent or unknown meansshuffle.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— theThemeMode.namefor the light/dark pick (system/light/dark); absent or unknown meanssystem.turn_timer_level— the globalTurnTimerLevel.name(off/quick/standard/extended— the wire values are contracts too); absent or unknown means off.tv_mode— theTvModePreference.namefor the TV-mode pick (auto/off/on); absent or unknown meansauto.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.nameper-game timer override; absent means inherit the global.round_option_<gameId>_<optionKey>— the stored scalar for one declaredRoundOption(int, String, or bool, matching the option'sserialize); 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 persistengine,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 reservedselfid (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 finalizedGameCallRecords, oldest first, capped at 500. The record's field names are wire contracts too — the same JSON crosses the room's link incall_recordmessages:v(GameCallRecord.formatVersion),id,game,mode,room,start,end,playMs,status(aCallStatus.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:
LinkEnvelopefields (v,type,gameId,payload) andLinkEnvelope.protocolVersion.LinkProtocoltype strings (link.hello,link.welcome,link.refused,link.peers,link.started,link.returned,link.ended,link.ping,link.pong,game.relay), thelink.startedpayload shape ({gameId, seats, call}—callis the per-call id drafts namespace by; absent on pre-call-id builds), thelink.welcomegamepayload's same optionalcallfield, thelink.hellopayload's optionalpeerIdclaim (a returning client re-asserting the id its last welcome dealt —peer-*ids only), the version-announcement fields (compat/build/gameson hello, welcome, and roster entries —linkCompatVersionplus eachMiniGame.remoteCompatVersionare the values they carry), the render announcement fields (render—'web'on the host-served client, absent for native — andfallbackon hello and roster entries), the rosteroutbench flag (host-written, present only when true), the seat-credential fields (resumeKeyonlink.welcome,resumeonlink.hello's claim),link.pausedand the welcome'spausedseed,link.wrap_upand the welcome'swrapUpseed ({ms, total}— a relative remainder plus the armed length, never a wall-clock deadline),link.evictedand itsEvictReasonvalues (seat_taken,kicked), theRefuseReasonvalues (bad_token,malformed,lobby_closed,seat_held,name_taken), and the defaulthostPeerId(host).- The
call_recordlink-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 andformatVersionare the contract. JoinTicketfields (v,ip,port,token,ssid,psk,compat,build) andJoinTicket.ticketVersion.- The host-served page contract — the app's embedded fallback and
the browser shell share it: the static routes (
/bundle,/wssocket,/install,/app,/app.json), thelibrebash_roomcookie, the URL params (t,s,k,name,role,embedded), theLibreBashJavaScript channel name and its{m:…}message methods (haptic,speak,stop,turnPing,getVoices), and the page'swindow.librebashDeliverreply 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 typelobbyServiceType(_librebash._tcp), the TXT attributelocked('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'sroomTicket,<callId>comes fromActiveGame.callId, and<scope>is the game's own per-obligation discriminator (draw:r1,guess:r1:d2, …). Keys are composed byRemoteGameViewState; 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) orpoolDeckKeyfingerprints — 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_poetryfor "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.