2a6dc5a304
Add the WalletOrderRequest / WalletOrderResponse FlatBuffers messages and the wallet.order Connect op: the gateway decodes the order request, forwards it to the backend POST /wallet/order and returns the created order id plus the provider launch URL the client opens. Regenerate the committed Go and TS FlatBuffers code.
914 lines
32 KiB
Plaintext
914 lines
32 KiB
Plaintext
// FlatBuffers payloads for the client <-> gateway edge transport.
|
|
//
|
|
// Every request and response that rides inside the Connect envelope
|
|
// (gateway/proto/edge) `payload` field, and every push Event payload, is one of
|
|
// these tables. They are the binary wire contract shared with the UI, which
|
|
// generates TypeScript from this same schema. A single namespace keeps
|
|
// nested tables (GameView inside MoveResult / MatchResult) free of
|
|
// cross-namespace imports. Keep this schema and the backend JSON DTOs in lockstep
|
|
// — the gateway transcodes one to the other.
|
|
//
|
|
// Generate Go with `make fbs` (flatc, version-pinned). The committed output lives
|
|
// in fbs/scrabblefb/.
|
|
namespace scrabblefb;
|
|
|
|
// --- shared building blocks ---
|
|
|
|
// TileRecord is one tile in a decoded move record (history, move result, hint): its
|
|
// board coordinate, the concrete letter ("?" when read from a hand for a blank) and
|
|
// whether it came from a blank. Inbound tiles to place use PlayTile (alphabet indices).
|
|
table TileRecord {
|
|
row:int;
|
|
col:int;
|
|
letter:string;
|
|
blank:bool;
|
|
}
|
|
|
|
// PlayTile is one inbound tile to place, addressed by its alphabet index rather than a
|
|
// concrete letter. For a blank, letter carries the designated letter's index
|
|
// and blank is true. The board coordinate is its target square.
|
|
table PlayTile {
|
|
row:int;
|
|
col:int;
|
|
letter:ubyte;
|
|
blank:bool;
|
|
}
|
|
|
|
// AlphabetEntry is one letter of a variant's alphabet, sent for display only:
|
|
// index is the engine alphabet-index byte the wire uses for this letter, letter is the
|
|
// concrete character and value is its tile score. The client caches the table per variant.
|
|
table AlphabetEntry {
|
|
index:ubyte;
|
|
letter:string;
|
|
value:int;
|
|
}
|
|
|
|
// SeatView is one seat's public standing in a game. display_name is resolved by the
|
|
// backend from the account store (added trailing — backward-compatible).
|
|
table SeatView {
|
|
seat:int;
|
|
account_id:string;
|
|
score:int;
|
|
hints_used:int;
|
|
is_winner:bool;
|
|
display_name:string;
|
|
}
|
|
|
|
// GameView is the shared (non-private) game summary.
|
|
table GameView {
|
|
id:string;
|
|
variant:string;
|
|
dict_version:string;
|
|
status:string;
|
|
players:int;
|
|
to_move:int;
|
|
turn_timeout_secs:int;
|
|
multiple_words_per_turn:bool;
|
|
move_count:int;
|
|
end_reason:string;
|
|
seats:[SeatView];
|
|
// last_activity_unix is the lobby sort key: the current turn's start for an active
|
|
// game, the finish time for a finished one.
|
|
last_activity_unix:long;
|
|
// vs_ai marks an honest-AI game: the opponent is shown as 🤖 and chat/nudge/add-friend
|
|
// are disabled in the client. A robot-filled random game keeps vs_ai=false.
|
|
vs_ai:bool;
|
|
// unread_chat is a per-viewer flag: the requesting player has at least one unread chat
|
|
// entry (message or nudge) in this game. It drives the lobby and in-game unread badge.
|
|
unread_chat:bool;
|
|
// unread_messages is a per-viewer flag: at least one of the unread entries is a real chat
|
|
// message (not just a nudge). With unread_chat it colours the badge — both set is the regular
|
|
// badge, unread_chat alone is the softer nudge-only badge.
|
|
unread_messages:bool;
|
|
}
|
|
|
|
// MoveRecord is one decoded move (a committed play, or a hint preview).
|
|
table MoveRecord {
|
|
player:int;
|
|
action:string;
|
|
dir:string;
|
|
main_row:int;
|
|
main_col:int;
|
|
tiles:[TileRecord];
|
|
words:[string];
|
|
count:int;
|
|
score:int;
|
|
total:int;
|
|
}
|
|
|
|
// --- auth (unauthenticated) ---
|
|
|
|
// TelegramLoginRequest carries the platform launch data; the gateway validates
|
|
// its HMAC before forwarding the extracted identity to the backend. browser_tz is
|
|
// the client's detected UTC offset ("±HH:MM"), seeded into a brand-new account's
|
|
// time zone so the robot's sleep window and the turn-timeout away window are
|
|
// anchored to the player's real zone from first contact (first contact only).
|
|
table TelegramLoginRequest {
|
|
init_data:string;
|
|
browser_tz:string;
|
|
// Client-reported device family (ios/android/web). Telegram's initData does not sign it, so
|
|
// the server records it best-effort and the payments gate never relies on it.
|
|
subtype:string;
|
|
}
|
|
|
|
// VKLoginRequest carries a VK Mini App launch. params is the raw query string of the
|
|
// signed vk_* launch parameters plus the sign, which the gateway verifies in-process
|
|
// (HMAC-SHA256 over the sorted vk_* params under the app secret, base64url) before
|
|
// forwarding the extracted vk_user_id to the backend. display_name is the player's
|
|
// name read client-side via VKWebAppGetUserInfo — VK omits it from the signed params,
|
|
// so it is an untrusted, cosmetic seed for a brand-new account's display name.
|
|
// browser_tz is the client's detected UTC offset ("±HH:MM"), seeded into a brand-new
|
|
// account's time zone (see TelegramLoginRequest.browser_tz; first contact only).
|
|
table VKLoginRequest {
|
|
params:string;
|
|
browser_tz:string;
|
|
display_name:string;
|
|
}
|
|
|
|
// GuestLoginRequest bootstraps an ephemeral guest session. locale is an optional
|
|
// preferred-language hint; browser_tz is the detected UTC offset seeded into the
|
|
// guest account's time zone (see TelegramLoginRequest.browser_tz).
|
|
table GuestLoginRequest {
|
|
locale:string;
|
|
browser_tz:string;
|
|
// Client-reported device family (ios/android/web) of this direct session; best-effort
|
|
// (a direct session has no external signer).
|
|
subtype:string;
|
|
}
|
|
|
|
// EmailRequestRequest asks the backend to send a login confirm-code to email. It
|
|
// also provisions the account on first contact, so browser_tz (the detected UTC
|
|
// offset) is seeded into its time zone here, not at the later login step; language
|
|
// (the client's UI language) seeds the new account's language and localises the email.
|
|
table EmailRequestRequest {
|
|
email:string;
|
|
browser_tz:string;
|
|
language:string;
|
|
// Set when the request originates from an installed PWA (standalone display mode): the
|
|
// backend then omits the one-tap confirm link from the login email so the code is typed in
|
|
// the same window, avoiding a link that opens in a separate browser (a different storage
|
|
// context) where the minted session could not reach the PWA.
|
|
pwa:bool;
|
|
}
|
|
|
|
// EmailLoginRequest logs in to the account owning email (provisioned at the
|
|
// request step), verifying the confirm-code.
|
|
table EmailLoginRequest {
|
|
email:string;
|
|
code:string;
|
|
// Client-reported device family (ios/android/web) of this direct session; best-effort.
|
|
subtype:string;
|
|
}
|
|
|
|
// EmailConfirmLinkRequest verifies a one-tap deeplink token from a confirmation
|
|
// email. The token, not a session, is the authorization.
|
|
table EmailConfirmLinkRequest {
|
|
token:string;
|
|
}
|
|
|
|
// EmailConfirmLinkResult is the outcome of a deeplink confirmation: purpose is
|
|
// "login" or "link"; for a login session carries the minted credential; for a link
|
|
// status is "confirmed" or "merge_required" (the app completes the merge).
|
|
table EmailConfirmLinkResult {
|
|
purpose:string;
|
|
status:string;
|
|
session:Session;
|
|
}
|
|
|
|
// Session is the minted credential returned by every auth operation.
|
|
table Session {
|
|
token:string;
|
|
user_id:string;
|
|
is_guest:bool;
|
|
display_name:string;
|
|
}
|
|
|
|
// Ack is a simple success acknowledgement (e.g. an email-code request).
|
|
table Ack {
|
|
ok:bool;
|
|
}
|
|
|
|
// --- advertising banner ---
|
|
|
|
// BannerCampaign is one campaign in the rotation feed: a GCD-reduced show weight
|
|
// (the client runs a smooth weighted round-robin over campaigns by this weight)
|
|
// and its messages in display order, each already resolved to the viewer's bot
|
|
// language (the stored en/ru pair is picked server-side). The override_* colours
|
|
// are an optional per-campaign palette: override_bg/fg/link paint the strip on
|
|
// every theme, and the *_dark trio further overrides the dark theme (the client
|
|
// resolves dark ← dark ?? all ?? token, light ← all ?? token). Empty means the
|
|
// campaign keeps the neutral theme tokens (all added trailing — backward-compatible).
|
|
table BannerCampaign {
|
|
weight:int;
|
|
messages:[string];
|
|
override_bg:string;
|
|
override_fg:string;
|
|
override_link:string;
|
|
override_bg_dark:string;
|
|
override_fg_dark:string;
|
|
override_link_dark:string;
|
|
}
|
|
|
|
// BannerInfo is the advertising-banner block of an eligible viewer's profile: the
|
|
// campaigns to rotate and the global display timings the client rotator reads.
|
|
// It is present (set on Profile.banner) only when the viewer is eligible to see a
|
|
// banner; absent otherwise. The transition between messages is fade_out_ms, then
|
|
// gap_ms, then fade_in_ms.
|
|
table BannerInfo {
|
|
campaigns:[BannerCampaign];
|
|
hold_ms:int;
|
|
edge_pause_ms:int;
|
|
scroll_px_per_sec:int;
|
|
fade_out_ms:int;
|
|
gap_ms:int;
|
|
fade_in_ms:int;
|
|
}
|
|
|
|
// --- profile (authenticated) ---
|
|
|
|
// Profile is the authenticated account's own profile view. away_start/away_end are
|
|
// the "HH:MM" daily away-window bounds. notifications_in_app_only (default true)
|
|
// suppresses out-of-app platform push, leaving only the in-app live stream. banner
|
|
// carries the advertising-banner block for an eligible viewer, absent otherwise
|
|
// (all added trailing — backward-compatible).
|
|
// DictVersion is one variant's current dictionary version, carried on the profile so an offline
|
|
// client learns it from an existing cold-start request (no extra call for a rarely-used feature).
|
|
table DictVersion {
|
|
variant:string;
|
|
version:string;
|
|
}
|
|
|
|
table Profile {
|
|
user_id:string;
|
|
display_name:string;
|
|
preferred_language:string;
|
|
time_zone:string;
|
|
hint_balance:int;
|
|
block_chat:bool;
|
|
block_friend_requests:bool;
|
|
is_guest:bool;
|
|
away_start:string;
|
|
away_end:string;
|
|
notifications_in_app_only:bool = true;
|
|
banner:BannerInfo;
|
|
// variant_preferences is the set of game variants the player allows themselves to be
|
|
// matched into (engine.Variant labels), Erudit-first; the New Game picker is gated by it.
|
|
variant_preferences:[string];
|
|
// email is the account's confirmed email address ("" when none); telegram_linked and
|
|
// vk_linked report whether a platform identity is attached. They drive the profile's
|
|
// link / unlink / change-email controls (all added trailing — backward-compatible).
|
|
email:string;
|
|
telegram_linked:bool;
|
|
vk_linked:bool;
|
|
// dict_versions carries each variant's current dictionary version so an offline client can
|
|
// preload the right dictionary and pin a new local game without a separate request (added
|
|
// trailing — backward-compatible).
|
|
dict_versions:[DictVersion];
|
|
}
|
|
|
|
// BlockStatus reports the caller's current manual block. The UI fetches it after any operation
|
|
// returns the "account_blocked" result code, then replaces every screen with the terminal blocked
|
|
// screen and stops all push/poll. permanent is false for a temporary block; until is an RFC3339
|
|
// UTC instant for a temporary block and empty for a permanent one; reason is the operator-set text
|
|
// resolved to the account's language, empty when none was cited.
|
|
table BlockStatus {
|
|
blocked:bool;
|
|
permanent:bool;
|
|
until:string;
|
|
reason:string;
|
|
}
|
|
|
|
// --- game (authenticated) ---
|
|
|
|
// SubmitPlayRequest places tiles on the player's turn; the backend infers the play's
|
|
// orientation from the tiles and the board. tiles are addressed by alphabet index.
|
|
table SubmitPlayRequest {
|
|
game_id:string;
|
|
tiles:[PlayTile];
|
|
}
|
|
|
|
// MoveResult is the outcome of a committed move: the move and the post-move game. rack and
|
|
// bag_len carry the actor's own post-move private state — their refilled rack (alphabet indices,
|
|
// a blank is the sentinel index 255) and the bag size after drawing — so the mover
|
|
// renders the next state straight from this response without a follow-up game.state (added
|
|
// trailing — backward-compatible).
|
|
table MoveResult {
|
|
move:MoveRecord;
|
|
game:GameView;
|
|
rack:[ubyte];
|
|
bag_len:int;
|
|
}
|
|
|
|
// StateRequest asks for the requesting player's view of a game. include_alphabet asks the
|
|
// backend to embed the variant's AlphabetEntry table in the reply; the client
|
|
// sets it only on a per-variant cache miss so the table is not resent on every poll.
|
|
table StateRequest {
|
|
game_id:string;
|
|
include_alphabet:bool = false;
|
|
}
|
|
|
|
// StateView is a player's view of a game: the shared summary plus their private rack, the
|
|
// bag size and their remaining hint budget. rack carries alphabet indices; a
|
|
// blank tile is the sentinel index 255. alphabet is present only when the request set
|
|
// include_alphabet (a display table the client caches per variant).
|
|
table StateView {
|
|
game:GameView;
|
|
seat:int;
|
|
rack:[ubyte];
|
|
bag_len:int;
|
|
hints_remaining:int;
|
|
alphabet:[AlphabetEntry];
|
|
// wallet_balance is the requesting player's global hint-wallet balance, sent apart from
|
|
// hints_remaining (which folds the wallet in with the per-game allowance) so the client can
|
|
// separate the two: the per-game allowance remaining is hints_remaining - wallet_balance, and
|
|
// the wallet is a single global figure the client keeps live across games (added trailing —
|
|
// backward-compatible).
|
|
wallet_balance:int;
|
|
// hint_unlock_left_seconds is, for a vs_ai game, how many seconds until the idle hint unlocks
|
|
// (the robot's last move plus the idle window, computed from the SERVER clock online / the device
|
|
// clock offline, capped at the window and floored at 0); 0 for a human's first move (no robot move
|
|
// yet) or a non-vs_ai game. The client anchors a MONOTONIC countdown (performance.now()) to it, so a
|
|
// client clock change cannot skew it — see lib/hints. Seconds granularity is enough; sent trailing
|
|
// (additive, backward-compatible).
|
|
hint_unlock_left_seconds:int;
|
|
}
|
|
|
|
// GameActionRequest carries just a game id (pass / resign / hint / history).
|
|
table GameActionRequest {
|
|
game_id:string;
|
|
}
|
|
|
|
// ExchangeRequest swaps the listed rack tiles back into the bag. tiles are alphabet
|
|
// indices; a blank is the sentinel index 255.
|
|
table ExchangeRequest {
|
|
game_id:string;
|
|
tiles:[ubyte];
|
|
}
|
|
|
|
// EvalRequest previews a tentative play without committing it. tiles are addressed by
|
|
// alphabet index; the backend infers the play's orientation from the tiles and the board.
|
|
table EvalRequest {
|
|
game_id:string;
|
|
tiles:[PlayTile];
|
|
}
|
|
|
|
// EvalResult is an unlimited move preview: legality, score, the words formed (main word
|
|
// first) and the orientation the backend inferred (dir is "H"/"V", empty when illegal).
|
|
table EvalResult {
|
|
legal:bool;
|
|
score:int;
|
|
words:[string];
|
|
dir:string;
|
|
}
|
|
|
|
// CheckWordRequest looks a word up in the game's pinned dictionary. word is a sequence of
|
|
// alphabet indices; the client constrains input to the variant's alphabet.
|
|
table CheckWordRequest {
|
|
game_id:string;
|
|
word:[ubyte];
|
|
}
|
|
|
|
// WordCheckResult is the dictionary lookup outcome.
|
|
table WordCheckResult {
|
|
word:string;
|
|
legal:bool;
|
|
}
|
|
|
|
// ComplaintRequest disputes a word-check result.
|
|
table ComplaintRequest {
|
|
game_id:string;
|
|
word:string;
|
|
note:string;
|
|
}
|
|
|
|
// HintResult is the top-ranked move plus the remaining hint budget. wallet_balance is the
|
|
// global hint-wallet balance after spending (see StateView.wallet_balance), so the client
|
|
// refreshes its live wallet and re-derives the per-game allowance (added trailing —
|
|
// backward-compatible).
|
|
table HintResult {
|
|
move:MoveRecord;
|
|
hints_remaining:int;
|
|
wallet_balance:int;
|
|
}
|
|
|
|
// DraftRequest saves the player's client-side composition for a game: a single
|
|
// JSON string of {rack_order, board_tiles} the client serializes itself, so the wire carries
|
|
// no tile array. The gateway forwards json verbatim to the backend, which owns its shape.
|
|
table DraftRequest {
|
|
game_id:string;
|
|
json:string;
|
|
}
|
|
|
|
// DraftView returns the player's stored composition as the same opaque JSON string (empty
|
|
// when none is stored). A draft get reuses GameActionRequest for its game id.
|
|
table DraftView {
|
|
json:string;
|
|
}
|
|
|
|
// History is a game's decoded move journal — the source for client board replay.
|
|
table History {
|
|
game_id:string;
|
|
moves:[MoveRecord];
|
|
}
|
|
|
|
// GameList is the caller's games (active and finished) for the lobby. at_game_limit is
|
|
// true when the caller has reached the simultaneous quick-game cap, so the lobby
|
|
// disables "New Game" and shows a notice.
|
|
table GameList {
|
|
games:[GameView];
|
|
at_game_limit:bool;
|
|
}
|
|
|
|
// --- lobby (authenticated) ---
|
|
|
|
// EnqueueRequest starts a quick game for a variant under a per-turn word rule.
|
|
// multiple_words_per_turn true is standard Scrabble; false is the single-word rule.
|
|
// vs_ai true starts an honest-AI game against a robot instead of human auto-match.
|
|
table EnqueueRequest {
|
|
variant:string;
|
|
multiple_words_per_turn:bool;
|
|
vs_ai:bool;
|
|
}
|
|
|
|
// MatchResult reports whether the caller has been paired into a game yet.
|
|
table MatchResult {
|
|
matched:bool;
|
|
game:GameView;
|
|
}
|
|
|
|
// --- chat (authenticated) ---
|
|
|
|
// ChatPostRequest posts a per-game chat message.
|
|
table ChatPostRequest {
|
|
game_id:string;
|
|
body:string;
|
|
}
|
|
|
|
// ChatMessage is one stored chat message or nudge.
|
|
table ChatMessage {
|
|
id:string;
|
|
game_id:string;
|
|
sender_id:string;
|
|
kind:string;
|
|
body:string;
|
|
created_at_unix:long;
|
|
}
|
|
|
|
// ChatList is a game's chat history.
|
|
table ChatList {
|
|
messages:[ChatMessage];
|
|
}
|
|
|
|
// --- feedback (authenticated) ---
|
|
|
|
// FeedbackSubmitRequest submits a feedback message with an optional single
|
|
// attachment. attachment is the raw file bytes (empty for none); attachment_name
|
|
// carries the original file name (its extension keys the allow-list); channel is
|
|
// the submitting platform (telegram/ios/android/web). The response is an Ack.
|
|
table FeedbackSubmitRequest {
|
|
body:string;
|
|
attachment:[ubyte];
|
|
attachment_name:string;
|
|
channel:string;
|
|
version:string;
|
|
browser_tz:string;
|
|
}
|
|
|
|
// FeedbackReply is the operator's answer shown back to the player.
|
|
table FeedbackReply {
|
|
body:string;
|
|
replied_at_unix:long;
|
|
}
|
|
|
|
// FeedbackState is the player's feedback screen state. blocked_reason is "" (can
|
|
// send), "pending" or "banned"; reply is null when there is none to show. Fetching
|
|
// it (feedback.get) delivers any pending reply, clearing the badge.
|
|
table FeedbackState {
|
|
can_send:bool;
|
|
blocked_reason:string;
|
|
reply:FeedbackReply;
|
|
}
|
|
|
|
// FeedbackUnread reports whether the player has an undelivered operator reply, for
|
|
// the lobby/Info badge (feedback.unread; no side effect).
|
|
table FeedbackUnread {
|
|
reply_unread:bool;
|
|
}
|
|
|
|
// --- account, statistics, friends, blocks, invitations, history ---
|
|
|
|
// AccountRef is a referenced account with its display name resolved — the shared
|
|
// shape for friends, blocked users and invitation participants.
|
|
table AccountRef {
|
|
account_id:string;
|
|
display_name:string;
|
|
}
|
|
|
|
// UpdateProfileRequest overwrites the full editable profile (the client sends the
|
|
// complete desired profile). away_start/away_end are "HH:MM" bounds.
|
|
// notifications_in_app_only (trailing — backward-compatible) toggles out-of-app
|
|
// platform push off when set.
|
|
table UpdateProfileRequest {
|
|
display_name:string;
|
|
preferred_language:string;
|
|
time_zone:string;
|
|
away_start:string;
|
|
away_end:string;
|
|
block_chat:bool;
|
|
block_friend_requests:bool;
|
|
notifications_in_app_only:bool = true;
|
|
variant_preferences:[string];
|
|
}
|
|
|
|
// --- account linking & merge (authenticated) ---
|
|
|
|
// LinkEmailRequest mails a confirm-code to email for a later link or merge. The
|
|
// code is always sent (no pre-send "taken" signal), so a probe cannot enumerate
|
|
// registered addresses.
|
|
table LinkEmailRequest {
|
|
email:string;
|
|
}
|
|
|
|
// LinkEmailConfirm carries the email and its confirm code, for both the confirm
|
|
// (preview) and the explicit merge step.
|
|
table LinkEmailConfirm {
|
|
email:string;
|
|
code:string;
|
|
}
|
|
|
|
// LinkTelegramRequest carries Telegram Login Widget data (a URL query string) for
|
|
// attaching a Telegram identity to the current account.
|
|
table LinkTelegramRequest {
|
|
data:string;
|
|
}
|
|
|
|
// LinkVKRequest carries a VK ID web authorization for attaching a VK identity to the
|
|
// current account. The fields are the PKCE code-exchange inputs the frontend obtains
|
|
// from the VK ID SDK (One Tap): the authorization code, the device id issued with it,
|
|
// and the PKCE code verifier. The gateway completes the confidential code exchange
|
|
// server-side (under the app's protected key) to obtain the trusted vk user id.
|
|
table LinkVKRequest {
|
|
code:string;
|
|
device_id:string;
|
|
code_verifier:string;
|
|
}
|
|
|
|
// LinkUnlinkRequest detaches a platform identity (kind = "telegram" | "vk") from the
|
|
// caller's account; email is never unlinked (it is changed).
|
|
table LinkUnlinkRequest {
|
|
kind:string;
|
|
}
|
|
|
|
// AccountDeleteConfirm carries the account-deletion step-up proof: a mailed code (email
|
|
// accounts) or the typed phrase (platform-only accounts).
|
|
table AccountDeleteConfirm {
|
|
code:string;
|
|
phrase:string;
|
|
}
|
|
|
|
// AccountDeleteRequestResult reports which deletion step-up the account uses: "email"
|
|
// (a code was mailed) or "phrase" (type the confirmation phrase).
|
|
table AccountDeleteRequestResult {
|
|
method:string;
|
|
}
|
|
|
|
// LinkResult is the unified result of a confirm or merge step. status is "linked"
|
|
// (bound to the caller), "merge_required" (the identity belongs to another account —
|
|
// the secondary_* fields summarise it for the irreversible confirmation), or
|
|
// "merged" (done). session is present only when the active account switched (a guest
|
|
// initiator whose durable counterpart won) — the client adopts it.
|
|
table LinkResult {
|
|
status:string;
|
|
secondary_user_id:string;
|
|
secondary_display_name:string;
|
|
secondary_games:int;
|
|
secondary_friends:int;
|
|
session:Session;
|
|
}
|
|
|
|
// BestMoveTile is one letter cell of a best-move word: its concrete letter (the designated
|
|
// letter for a blank), its tile point value (0 for a blank) and whether it is a blank. It
|
|
// lets the client render the word as game tiles without the variant's alphabet table.
|
|
table BestMoveTile {
|
|
letter:string;
|
|
value:int;
|
|
blank:bool;
|
|
}
|
|
|
|
// BestMoveView is an account's highest-scoring single play within one game variant: the
|
|
// variant label, the play's total score (matching max_word_points for that variant) and its
|
|
// main word as ordered tiles.
|
|
table BestMoveView {
|
|
variant:string;
|
|
score:int;
|
|
word:[BestMoveTile];
|
|
}
|
|
|
|
// StatsView is a durable account's lifetime statistics (games-played and win-rate
|
|
// are derived client-side). best_moves breaks the best move down per variant, carrying the
|
|
// word itself; it is empty for an account with no recorded play and lists only variants the
|
|
// account has played (added trailing — backward-compatible).
|
|
table StatsView {
|
|
wins:int;
|
|
losses:int;
|
|
draws:int;
|
|
max_game_points:int;
|
|
max_word_points:int;
|
|
best_moves:[BestMoveView];
|
|
// moves is the player's lifetime play count (tile placements); hints_used is their lifetime
|
|
// hint count. The screen shows the hint share = hints_used / moves (added trailing).
|
|
moves:int;
|
|
hints_used:int;
|
|
}
|
|
|
|
// TargetRequest names a single counterpart account (friend request/cancel/unfriend,
|
|
// block/unblock). game_id is set only by an in-game block of a disguised-robot opponent,
|
|
// so the backend can record the per-game robot block (the seat name the player saw)
|
|
// against that game; every other path leaves it empty (FlatBuffers-optional).
|
|
table TargetRequest {
|
|
account_id:string;
|
|
game_id:string;
|
|
}
|
|
|
|
// FriendRespondRequest accepts or declines a pending request from a requester.
|
|
table FriendRespondRequest {
|
|
requester_id:string;
|
|
accept:bool;
|
|
}
|
|
|
|
// FriendList is the caller's accepted friends.
|
|
table FriendList {
|
|
friends:[AccountRef];
|
|
}
|
|
|
|
// IncomingRequestList is the friend requests awaiting the caller's response.
|
|
table IncomingRequestList {
|
|
requests:[AccountRef];
|
|
}
|
|
|
|
// RobotFriendRef is one per-game friend request to a disguised-robot opponent: a
|
|
// per-game record (not a real account) carrying the game name the player saw and the
|
|
// game/seat it was sent in, so the in-game card can re-mark that seat as already
|
|
// requested. Its id is the robot_friend_requests row.
|
|
table RobotFriendRef {
|
|
id:string;
|
|
display_name:string;
|
|
game_id:string;
|
|
seat:int;
|
|
}
|
|
|
|
// OutgoingRequestList is the accounts the caller has already requested and cannot
|
|
// (re-)request: a live pending request or one the addressee declined. The game's
|
|
// "add to friends" item reads it to stay disabled across reloads. robots are the
|
|
// per-game disguised-robot requests (not real accounts), added trailing.
|
|
table OutgoingRequestList {
|
|
requests:[AccountRef];
|
|
robots:[RobotFriendRef];
|
|
}
|
|
|
|
// FriendCode is a freshly issued one-time add-a-friend code (returned once).
|
|
table FriendCode {
|
|
code:string;
|
|
expires_at_unix:long;
|
|
}
|
|
|
|
// RedeemCodeRequest redeems a friend code, befriending its issuer.
|
|
table RedeemCodeRequest {
|
|
code:string;
|
|
}
|
|
|
|
// RedeemResult reports the new friend gained by redeeming a code.
|
|
table RedeemResult {
|
|
friend:AccountRef;
|
|
}
|
|
|
|
// RobotBlockRef is one blocked disguised-robot opponent: a per-game record (not a real
|
|
// account) carrying the game name the player saw and the game/seat it was blocked in, so
|
|
// the blocked list shows it as a distinct personality and the in-game card can re-mark
|
|
// that seat. Its id is the robot_blocks row, used to unblock it.
|
|
table RobotBlockRef {
|
|
id:string;
|
|
display_name:string;
|
|
game_id:string;
|
|
seat:int;
|
|
}
|
|
|
|
// BlockList is the accounts the caller has blocked, plus the per-game disguised-robot
|
|
// blocks (robots) which are not real accounts.
|
|
table BlockList {
|
|
blocked:[AccountRef];
|
|
robots:[RobotBlockRef];
|
|
}
|
|
|
|
// InvitationInvitee is one invitee's seat and response, name resolved.
|
|
table InvitationInvitee {
|
|
account_id:string;
|
|
display_name:string;
|
|
seat:int;
|
|
response:string;
|
|
}
|
|
|
|
// Invitation is a friend-game invitation with its settings and invitees.
|
|
table Invitation {
|
|
id:string;
|
|
inviter:AccountRef;
|
|
invitees:[InvitationInvitee];
|
|
variant:string;
|
|
turn_timeout_secs:int;
|
|
hints_allowed:bool;
|
|
hints_per_player:int;
|
|
multiple_words_per_turn:bool;
|
|
dropout_tiles:string;
|
|
status:string;
|
|
game_id:string;
|
|
expires_at_unix:long;
|
|
}
|
|
|
|
// CreateInvitationRequest proposes a 2-4 player friend game to the named invitees.
|
|
table CreateInvitationRequest {
|
|
invitee_ids:[string];
|
|
variant:string;
|
|
turn_timeout_secs:int;
|
|
hints_allowed:bool;
|
|
hints_per_player:int;
|
|
dropout_tiles:string;
|
|
multiple_words_per_turn:bool;
|
|
}
|
|
|
|
// InvitationActionRequest accepts / declines / cancels an invitation by id.
|
|
table InvitationActionRequest {
|
|
invitation_id:string;
|
|
}
|
|
|
|
// InvitationList is the caller's open invitations.
|
|
table InvitationList {
|
|
invitations:[Invitation];
|
|
}
|
|
|
|
// GcgExport is a finished game's GCG transcript: a suggested filename and the text.
|
|
table GcgExport {
|
|
game_id:string;
|
|
filename:string;
|
|
content:string;
|
|
}
|
|
|
|
// ExportUrlRequest asks for a signed download URL of a finished game's export artifact.
|
|
// kind is "png" or "gcg". For the PNG the client passes its presentation context — the
|
|
// UI-localized non-play move labels (pass, exchange, resign, timeout, in that order),
|
|
// the device date locale and the device IANA time zone — which ride the signed URL so
|
|
// the server render matches what the player would have seen locally.
|
|
table ExportUrlRequest {
|
|
game_id:string;
|
|
kind:string;
|
|
date_locale:string;
|
|
action_labels:[string];
|
|
time_zone:string;
|
|
}
|
|
|
|
// ExportUrl is the minted download link: a relative, signed, short-lived path the client
|
|
// resolves against its own origin (the SPA and the gateway share it), plus the filename
|
|
// the download will carry.
|
|
table ExportUrl {
|
|
path:string;
|
|
filename:string;
|
|
}
|
|
|
|
// --- push event payloads ---
|
|
|
|
// YourTurnEvent signals that it is now the recipient's turn. The trailing fields enrich the
|
|
// out-of-app push: opponent_name is the player who just moved, last_action is their
|
|
// move kind ("play"/"pass"/"exchange"/...), last_word is the main word of a scoring play (empty
|
|
// otherwise), and score_line is the recipient-first running score (e.g. "120:95:80"). They are
|
|
// appended (FlatBuffers-optional), so an older reader that only needs game_id/deadline is unaffected.
|
|
// move_count is the post-move count (matching the opponent_moved GameView): the client uses it to
|
|
// tell whether its cached game already reflects the move, falling back to a refetch on a gap.
|
|
table YourTurnEvent {
|
|
game_id:string;
|
|
deadline_unix:long;
|
|
opponent_name:string;
|
|
last_action:string;
|
|
last_word:string;
|
|
score_line:string;
|
|
move_count:int;
|
|
}
|
|
|
|
// GameOverEvent signals that a game the recipient is seated in has finished, driving the
|
|
// out-of-app "game over" push. result is the outcome from the recipient's own
|
|
// perspective ("won"/"lost"/"draw"); score_line is the recipient-first final score. game is the
|
|
// final post-game summary (adjusted scores after rack penalties + the winner flag), so the client
|
|
// settles the finished game from the event without a refetch (added trailing).
|
|
table GameOverEvent {
|
|
game_id:string;
|
|
result:string;
|
|
score_line:string;
|
|
game:GameView;
|
|
}
|
|
|
|
// OpponentMovedEvent carries a move another seat just committed as a delta the client applies to
|
|
// its cached game without a refetch: move is the decoded play/pass/exchange (the same record
|
|
// game.history returns), game is the post-move summary (per-seat scores, to_move, move_count,
|
|
// status) and bag_len is the bag size after the draw.
|
|
table OpponentMovedEvent {
|
|
game_id:string;
|
|
move:MoveRecord;
|
|
game:GameView;
|
|
bag_len:int;
|
|
}
|
|
|
|
// NudgeEvent signals that a player nudged the recipient. sender_name is the nudger's seat
|
|
// display-name snapshot, so the toast and the out-of-app push can name them (added trailing —
|
|
// an older reader still reads just game_id + from_user_id, and falls back to the plain phrase).
|
|
table NudgeEvent {
|
|
game_id:string;
|
|
from_user_id:string;
|
|
sender_name:string;
|
|
}
|
|
|
|
// MatchFoundEvent signals that an auto-match pairing (or robot substitution) started a game the
|
|
// recipient is seated in. state is the recipient's full initial view of the new game (empty board,
|
|
// dealt rack), so the client navigates straight in from the event with no follow-up fetch (added
|
|
// trailing — an older reader still reads just game_id).
|
|
table MatchFoundEvent {
|
|
game_id:string;
|
|
state:StateView;
|
|
}
|
|
|
|
// NotificationEvent is a lightweight "something changed, re-poll" signal that
|
|
// drives the lobby badge (incoming friend requests, invitations). kind is a sub-
|
|
// discriminator ("friend_request", "friend_added", "friend_declined", "invitation",
|
|
// "game_started"); the client re-fetches its lobby counters (and, for a requester
|
|
// watching a game, its friend state) on any of them. To let the client update its lobby without a
|
|
// follow-up fetch, each event also carries the payload its kind changed: account for the
|
|
// friend_* kinds (the requester/friend), invitation for "invitation" (the new invitation) and
|
|
// state for "game_started" (the started game's initial view, like match_found). Only the field
|
|
// matching kind is set (all added trailing — backward-compatible).
|
|
table NotificationEvent {
|
|
kind:string;
|
|
account:AccountRef;
|
|
invitation:Invitation;
|
|
state:StateView;
|
|
}
|
|
|
|
// WalletSegment is one chip balance in the wallet view: the funding source
|
|
// ("vk"/"telegram"/"direct"), the chip count, and whether it is spendable in the current
|
|
// execution context (false for a frozen VK-iOS balance or an untrusted platform).
|
|
table WalletSegment {
|
|
source:string;
|
|
chips:int;
|
|
spendable:bool;
|
|
}
|
|
|
|
// Wallet is the user-facing wallet: the context-visible chip segments and the context-applicable
|
|
// benefits — the no-ads term end as unix milliseconds (0 = no active term) or the perpetual
|
|
// forever flag, and the available hint count.
|
|
table Wallet {
|
|
segments:[WalletSegment];
|
|
ads_forever:bool;
|
|
ads_paid_until_ms:long;
|
|
hints:int;
|
|
}
|
|
|
|
// WalletBuyRequest buys a chip-priced value with chips: the product to buy.
|
|
table WalletBuyRequest {
|
|
product_id:string;
|
|
}
|
|
|
|
// WalletOrderRequest opens a money order to fund a chip pack: the pack to buy.
|
|
table WalletOrderRequest {
|
|
product_id:string;
|
|
}
|
|
|
|
// WalletOrderResponse returns the created order id and the provider launch URL the client opens
|
|
// (the Robokassa hosted-payment page); chips are credited later, by the verified server callback.
|
|
table WalletOrderResponse {
|
|
order_id:string;
|
|
redirect_url:string;
|
|
}
|
|
|
|
// CatalogAtom is one atom line of a storefront product: the base value type it grants
|
|
// ("chips"/"hints"/"noads_days"/"tournament") and how many of it the product carries.
|
|
table CatalogAtom {
|
|
atom_type:string;
|
|
quantity:int;
|
|
}
|
|
|
|
// CatalogProduct is one storefront product projected for the caller's context. A "value" is
|
|
// bought with chips (chips is its uniform price, money fields zero); a "pack" funds chips with
|
|
// money (money_amount is its price in the context currency's minor units under money_currency,
|
|
// chips zero). atoms lists what the product grants.
|
|
table CatalogProduct {
|
|
kind:string;
|
|
product_id:string;
|
|
title:string;
|
|
chips:int;
|
|
money_amount:long;
|
|
money_currency:string;
|
|
atoms:[CatalogAtom];
|
|
}
|
|
|
|
// Catalog is the storefront: the products visible and purchasable in the caller's context — the
|
|
// chip-priced values and the chip packs priced in the context's payment method.
|
|
table Catalog {
|
|
products:[CatalogProduct];
|
|
}
|