Compare commits

...

3 Commits

Author SHA1 Message Date
Ilia Denisov 7a48327ab6 Stage 7 (wip): docs bake + stage renumber (insert UI Stage 8, shift +1)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Failing after 51m19s
- PLAN.md: new Stage 8 (UI social/account/history); Telegram->9, Admin->10, Linking->11, Polish->12; tracker + Stage 7 refinements; split the Stage 6 'wired in Stage 7' note between 7 and 8
- ARCHITECTURE: promote ui to current (slice scope, board-replay, codegen, theming, mock)
- FUNCTIONAL(+ru): client-app section with the Stage 7/8 split
- README + ui/README + CLAUDE.md: UI build/run/test, codegen, pnpm notes
- bumped Stage 8-11 refs (+1) across docs and code comments
2026-06-03 01:01:58 +02:00
Ilia Denisov 0284c9b83a Stage 7 (wip): tests + UI CI
- Vitest units: board replay, placement machine, premium parity, i18n key parity, FlatBuffers codec round-trips (19 tests)
- Playwright smoke (mock transport): guest -> lobby -> board -> place tile -> preview
- ui-test.yaml workflow: check/unit/build + bundle-size budget (67.5KB gzip < 100KB) + chromium e2e
- gateway transcode tests for games.list (seat display_name), pass, hint
- backend integration test for game.ListForAccount
2026-06-03 00:55:38 +02:00
Ilia Denisov 65689b903f Stage 7 (wip): wire remaining ops (backend REST, FBS, gateway transcode) + real UI transport
backend: REST handlers for pass/exchange/resign/hint/evaluate/check_word/complaint/history/chat-list/nudge + new game.ListForAccount (my games) + seat display_name resolution
pkg/fbs: GameActionRequest/ExchangeRequest/EvalRequest/EvalResult/CheckWordRequest/WordCheckResult/ComplaintRequest/HintResult/History/GameList/ChatList + SeatView.display_name; committed Go regenerated (flatc 23.5.26)
gateway: 11 new transcode ops + backendclient methods + FB encoders
ui: edge TS codegen (flatc --ts + protoc-gen-es, committed), FlatBuffers<->model codec, real connect-web transport (binary, bearer auth, Subscribe). prod bundle ~69KB gzip JS
2026-06-03 00:49:07 +02:00
85 changed files with 5937 additions and 95 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Tests · UI
# Hermetic UI checks: type-check, Vitest unit tests, production build with a
# bundle-size budget, and a Playwright smoke against the in-memory mock transport
# (no backend/gateway/Postgres). The committed src/gen/ codegen is built, not
# regenerated (the same model as the Go committed jet/fbs output).
on:
push:
paths:
- 'ui/**'
- '.gitea/workflows/ui-test.yaml'
pull_request:
paths:
- 'ui/**'
- '.gitea/workflows/ui-test.yaml'
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: ui
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install pnpm
run: npm install -g pnpm@11.0.9
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Type-check
run: pnpm run check
- name: Unit tests
run: pnpm run test:unit
- name: Build
run: pnpm run build
- name: Bundle-size budget
run: node scripts/bundle-size.mjs
- name: Install Playwright browser
run: pnpm exec playwright install --with-deps chromium
- name: E2E smoke (mock)
run: pnpm run test:e2e
+8
View File
@@ -121,4 +121,12 @@ go vet ./backend/...
gofmt -l . # must print nothing
go test -count=1 ./backend/...
go run ./backend/cmd/backend # /healthz, /readyz on :8080
cd ui && pnpm install && pnpm check && pnpm test:unit && pnpm build # the UI (Stage 7+)
pnpm start # UI mock mode: lobby -> game, no backend
```
The `ui` module is a Node project (pnpm), **not** in `go.work`; its CI is
`.gitea/workflows/ui-test.yaml`. Committed edge codegen under `ui/src/gen/`
(regenerate with `pnpm codegen`); pnpm build-script approval lives in
`ui/pnpm-workspace.yaml` (`allowBuilds: esbuild: true`).
+129 -19
View File
@@ -40,11 +40,12 @@ independent (see ARCHITECTURE §9.1).
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
| 5 | Robot opponent | **done** |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo |
| 8 | Telegram integration (bot side-service, deep-link, push) | todo |
| 9 | Admin & dictionary ops (complaint review, version reload) | todo |
| 10 | Account linking & merge | todo |
| 11 | Polish (observability, perf with evidence, deploy) | todo |
| 7 | UI playable slice (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | todo |
| 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | todo |
| 9 | Telegram integration (bot side-service, deep-link, push) | todo |
| 10 | Admin & dictionary ops (complaint review, version reload) | todo |
| 11 | Account linking & merge | todo |
| 12 | Polish (observability, perf with evidence, deploy) | todo |
Scaffolding is incremental: `go.work` lists only existing modules; each stage
adds the modules it needs.
@@ -70,7 +71,7 @@ platform identities.
Open details: Postgres version + DSN/`search_path` convention; jet vs
sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque
random length, TTL, revocation); account/identity table shape; whether the
admin bootstrap lands here or in Stage 9.
admin bootstrap lands here or in Stage 10.
### Stage 2 — Engine package
Scope: `backend/internal/engine` over scrabble-solver — versioned DAWG
@@ -120,25 +121,90 @@ available); Capacitor-ready structure.
Open details: detailed game-board UX (deferred by the owner to this stage);
client routing; offline/refresh behaviour; design system / theming.
### Stage 8 — Telegram integration
#### Suggested layouts (lobby + game screen)
User note:
> Detailed interview about UI/UX is **strongly** required.
> Too much to discuss.
```text
┌────────────────────┐
│ Display_Name =│- Profile
├────────────────────┤- Settings
│ Invitations │- About
│ - list │
├────────────────────┤
│ Active games │
│ - list │
├────────────────────┤
│ Finished games │
│ - list │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├────────────────────┤
│ ┌───┐ ┌───┐ ┌───┐│
│ New │ Stats Tourn│
│ └───┘ └───┘ └───┘│
└────────────────────┘
┌────────────────────┐
Lobby│◄ ==│- History
├────────────────────┤- Chat
│You Ann Kaya Rick│- Check word
│136 700 179 39│- Drop game
├────────────────────┤
│ │
│ │
│ │
│ c │
│ words │
│ o │
│ s │
│ s │
│ │
│ │
├──┬──┬──┬──┬──┬──┬──┤ ┌──┐
│A │Q │Z │* │N │I │W │◄│ │MakeMove/Reset
├──┴──┴──┴──┴──┴──┴──┤ └──┘
│ ┌───┐ ┌───┐ ┌───┐ │
│ Draw│ Skip│ Shfl│ │
│ └───┘ └───┘ └───┘ │
└────────────────────┘
```
### Stage 8 — UI: social, account & history surfaces
Scope: the UI surfaces deferred from Stage 7's playable slice, wiring the matching
backend/gateway operations as each screen needs them (the Stage 6 vertical-slice
pattern): friends (request/accept/decline/list), per-user blocks, friend-game
invitations (create 24 player, accept/decline, invitations list), profile **editing**
(`account.UpdateProfile` + the email confirm-code binding UI), the statistics screen,
and the history viewer with GCG export/download.
Open details: friends/invitations UX; stats presentation; history/GCG viewer + download
mechanics; any new validation the profile-editing forms need.
### Stage 9 — Telegram integration
Scope: bot side-service, deep-link invites, platform push (your-turn / nudge),
Mini App launch/auth; backend↔platform internal API.
Open details: bot framework/library; deep-link scheme; push message templates;
internal API contract; Mini App hosting/origin.
### Stage 9 — Admin & dictionary ops
### Stage 10 — Admin & dictionary ops
Scope: admin endpoints (users, games, complaint review queue, dictionary
versions + reload), complaint→dictionary update pipeline.
Open details: whether a server-rendered console is wanted or JSON-only; the
dictionary rebuild/deploy pipeline; complaint resolution workflow.
### Stage 10 — Account linking & merge
### Stage 11 — Account linking & merge
Scope: link-via-confirm; merge-into-A (stats sum, transfer games/friends,
dedupe). High blast-radius — focused regression tests.
Open details: conflict resolution (active games on both, duplicate friends,
display-name collisions); irreversibility/audit; confirm-flow per platform.
### Stage 11 — Polish
### Stage 12 — Polish
Scope: observability dashboards, evidence-based performance work, prod
build/deploy.
Open details: deployment target/host; dashboards; load expectations.
@@ -164,9 +230,9 @@ Open details: deployment target/host; dashboards; load expectations.
- HTTP surface: **service/store/cache layer only**. `/api/v1/{public,user,
internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via
`Server` group accessors); the session/account REST handlers land with the
gateway in **Stage 6**. Admin bootstrap deferred to **Stage 9**.
gateway in **Stage 6**. Admin bootstrap deferred to **Stage 10**.
- Telemetry: providers + request-timing middleware + otelsql; exporters
`none` (default) / `stdout`; OTLP + dashboards deferred to **Stage 11**.
`none` (default) / `stdout`; OTLP + dashboards deferred to **Stage 12**.
- Tests/CI: integration tests behind the `integration` build tag in
`backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk
off, serial), firing on push and PR. Backend now **hard-depends on Postgres
@@ -247,7 +313,7 @@ Open details: deployment target/host; dashboards; load expectations.
wins/losses; `max_word_points` = best single **move** score; ties draw,
resign/timeout is a loss, guests get no stats.
- **Complaint** (interview): full payload with `game_id`; word-check is scoped
to the game's pinned `(variant, dict_version)`. Stage 9 owns the resolution
to the game's pinned `(variant, dict_version)`. Stage 10 owns the resolution
lifecycle, so the `status` column carries no value CHECK yet.
- **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon`
pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES`
@@ -295,7 +361,7 @@ Open details: deployment target/host; dashboards; load expectations.
stored as a **SHA-256 hash**; a `Mailer` seam with an SMTP relay
(`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the
current account; an email already confirmed by another account → `ErrEmailTaken`
(**merge is Stage 10**); email-as-login is Stage 6 and reuses this mechanism.
(**merge is Stage 11**); email-as-login is Stage 6 and reuses this mechanism.
- **Multi-player drop-out** (interview; discharges the Stage 3 deferral): the
engine's `Resign` now drops a seat and the rest **play on** while ≥ 2 are
active, finishing (last-survivor wins) when one remains; `winner` excludes all
@@ -362,9 +428,11 @@ Open details: deployment target/host; dashboards; load expectations.
end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/
`auth.email.login`), `profile.get`, `game.submit_play`/`game.state`,
`lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin
passthrough. The remaining domain operations (friends, blocks, invitations,
hint, word-check, pass/exchange/resign, history/GCG, profile editing) reuse the
identical transcode pattern and are wired in **Stage 7** as the UI needs them.
passthrough. The remaining domain operations reuse the identical transcode
pattern and are wired as the UI needs them: the play-loop ops (pass/exchange/
resign, hint, evaluate, word-check/complaint, history, my-games list, chat
list/nudge) in **Stage 7**; the social/account ops (friends, blocks,
invitations, profile editing, stats, GCG export) in **Stage 8**.
- **Wire contracts in a new shared `scrabble/pkg` module** (interview): the
backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads
(`pkg/fbs`, one `scrabblefb` namespace) live here with **committed** generated
@@ -402,7 +470,7 @@ Open details: deployment target/host; dashboards; load expectations.
(the galaxy donor's crypto stack was dropped, per §3).
- **Admin = gateway validates Basic-Auth** (interview): the gateway checks
`GATEWAY_ADMIN_USER`/`_PASSWORD` and reverse-proxies to backend
`/api/v1/admin/*`; the backend admin surface is a single `ping` until Stage 9.
`/api/v1/admin/*`; the backend admin surface is a single `ping` until Stage 10.
- **Rate-limit = 2 dimensions, 3 classes** (interview): public per-IP (30/min,
burst 10), authenticated per-user (120/min, burst 40), admin per-IP (60/min,
burst 20), plus an email-code per-IP sub-limit (5/10 min); token bucket
@@ -416,6 +484,48 @@ Open details: deployment target/host; dashboards; load expectations.
(unit) — integration stays `./backend/...` (the only module with tagged tests).
The solver clone + `BACKEND_DICT_DIR` steps are unchanged.
- **Stage 7** (interview + implementation):
- **Scope = playable slice** (interview): the *whole* UI shell plus the core play
loop end-to-end; the social/account/history surfaces were split out into a new
**Stage 8** and the later stages shifted +1 (Telegram→9, Admin→10, Linking→11,
Polish→12). Stage 7 wires only the operations the slice needs (the Stage 6
"as the UI needs them" pattern): the new gateway/transcode + backend-REST ops
`games.list`, `game.{pass,exchange,resign,hint,evaluate,check_word,complaint,
history}`, `chat.{list,nudge}`. The only new domain code is `game.ListForAccount`
(the "my games" query) and seat **`display_name`** resolution (server DTO layer);
`SeatView` gained a trailing `display_name`. Friends/blocks/invitations,
profile-editing, stats and the history/GCG viewer are Stage 8.
- **Stack** (interview): plain **Svelte 5 (runes) + TypeScript + Vite**, no
SvelteKit; `@connectrpc/connect-web` + the `flatbuffers` runtime, with the edge
TS bindings generated from the **same** `edge.proto` (`protoc-gen-es`) and
`scrabble.fbs` (`flatc --ts`) and **committed** under `ui/src/gen/` (dev-time
codegen, like `cmd/jetgen` / `pkg/Makefile`; CI builds the committed output).
- **No board on the wire** (discovered): `StateView` carries no grid, so the client
**replays the decoded move journal** (`game.history`, newly wired) onto an empty
board; premium squares + tile values are a client-side map **ported from
`scrabble-solver/rules/rules.go`** with a Vitest parity test.
- **Board UX** (interview): full-width, borderless; tiles placed by **Pointer-Events
drag or tap** (no HTML5 DnD — it has no touch support); a contextual **MakeMove**
control (short tap → make/reset popup, ~1 s press-and-hold → commit); per-tile
recall by tapping a pending tile; a **two-state zoom** (15↔9 cells) on touch only
(auto-zoom-in on placement, double-tap / pinch manual); a blank-letter chooser.
All board/tiles/effects are **pure HTML5/CSS + Unicode** — no image/font/SVG asset.
- **Theming** (interview): own **CSS custom-property tokens**, light/dark via
`prefers-color-scheme`, **Telegram-themeParams-ready** (a runtime hook can override
the tokens; the SDK is wired in the Telegram stage). **Navigation** (interview):
dependency-free **hash router**; session token in memory + **IndexedDB**, re-resolved
on reload (reopen Subscribe, refetch the open game); stream reconnect on focus.
**i18n** en/ru is a hand-rolled typed catalog (compile-time key parity + a test).
- **Mock transport** (owner request): a build-flagged in-memory fake (`VITE_MOCK`,
`pnpm start`) drives lobby → active game → board with no backend, tree-shaken out
of production; it is the same fixture the Playwright smoke uses.
- **Tests/CI** (interview): **Vitest** units (board replay, placement machine,
premium parity, i18n parity, FlatBuffers codec) + a **Playwright** smoke against
the mock; a new **`ui-test.yaml`** workflow (type-check, unit, build with a
**bundle-size budget** — prod is ~67 KB gzip JS — and a chromium e2e). The Go
workflows already cover the new backend/gateway/pkg code; a `game.ListForAccount`
integration test and gateway transcode tests for the new ops were added.
## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
@@ -433,7 +543,7 @@ Open details: deployment target/host; dashboards; load expectations.
git submodule (the ~0.50.7 MB DAWGs are regenerated wholesale and bloat git
history); pin by tag/hash for a reproducible startup set. A submodule/LFS pull
is a **deploy-time** way to populate the directory, **not** the runtime
dynamic-reload mechanism (Stage 9) — keep the `BACKEND_DICT_DIR` directory as
dynamic-reload mechanism (Stage 10) — keep the `BACKEND_DICT_DIR` directory as
the runtime contract: a new `.dawg` appears in it and is loaded with
`dawg.Load`.
- **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a
+15 -2
View File
@@ -11,8 +11,9 @@ supports English Scrabble, Russian Scrabble and Эрудит.
admin surface behind Basic Auth. *(added in a later stage)*
- **`backend`** — internal-only service that owns every domain concern and
embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process.
- **`ui`** — pure-HTML5 client (plain Svelte + Vite), embeddable in platform
webviews and packageable to native via Capacitor. *(added in a later stage)*
- **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC
+ FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See [`ui/README.md`](ui/README.md).
- **`platform/*`** — per-platform side-services (e.g. the Telegram bot).
*(added in a later stage)*
@@ -67,3 +68,15 @@ Key environment: `BACKEND_HTTP_ADDR` (default `:8080`), `BACKEND_LOG_LEVEL`
(`debug|info|warn|error`, default `info`), `BACKEND_POSTGRES_DSN` (**required**).
The full configuration surface and the go-jet regeneration step live in
[`backend/README.md`](backend/README.md).
## Run the UI locally
```sh
cd ui && pnpm install
pnpm start # mock mode: lobby -> game with no backend, on http://localhost:5173
pnpm dev # against a running gateway (Vite proxies the RPC path to :8081)
```
`pnpm check` (type-check), `pnpm test:unit` (Vitest), `pnpm test:e2e` (Playwright
smoke vs the mock), `pnpm build` (static bundle). Details — including the committed
edge codegen (`pnpm codegen`) — are in [`ui/README.md`](ui/README.md).
+2 -2
View File
@@ -33,7 +33,7 @@ var (
// ErrInvalidEmail is returned for an unparseable email address.
ErrInvalidEmail = errors.New("account: invalid email address")
// ErrEmailTaken is returned when the email is already confirmed by another
// account; binding it would be a merge, which Stage 10 owns.
// account; binding it would be a merge, which Stage 11 owns.
ErrEmailTaken = errors.New("account: email already confirmed by another account")
// ErrAlreadyConfirmed is returned when the email is already confirmed by the
// requesting account.
@@ -52,7 +52,7 @@ var (
// Mailer and verifies it, binding a confirmed email identity to the requesting
// account. Only the SHA-256 hash of a code is stored (never the plaintext),
// matching the session model. Binding an email already confirmed by a different
// account is refused (ErrEmailTaken) — merging two accounts is Stage 10 — and
// account is refused (ErrEmailTaken) — merging two accounts is Stage 11 — and
// using an email as a login is Stage 6, which reuses this mechanism.
type EmailService struct {
store *Store
+7
View File
@@ -566,6 +566,13 @@ func (svc *Service) Participants(ctx context.Context, gameID uuid.UUID) ([]uuid.
return seats, g.ToMove, g.Status, nil
}
// ListForAccount returns every game the account is seated in, newest first, for the
// lobby's active/finished lists. The live position is not loaded — the summaries come
// straight from the durable rows.
func (svc *Service) ListForAccount(ctx context.Context, accountID uuid.UUID) ([]Game, error) {
return svc.store.ListGamesForAccount(ctx, accountID)
}
// History returns a game's full, dictionary-independent move journal.
func (svc *Service) History(ctx context.Context, gameID uuid.UUID) (HistoryView, error) {
g, err := svc.store.GetGame(ctx, gameID)
+44
View File
@@ -135,6 +135,50 @@ func (s *Store) GetGame(ctx context.Context, id uuid.UUID) (Game, error) {
return projectGame(grow, srows)
}
// ListGamesForAccount loads every game the account is seated in (active and
// finished), newest first, each joined with its ordered seats. It backs the lobby's
// "my games" lists.
func (s *Store) ListGamesForAccount(ctx context.Context, accountID uuid.UUID) ([]Game, error) {
gstmt := postgres.SELECT(table.Games.AllColumns).
FROM(table.Games.INNER_JOIN(table.GamePlayers, table.GamePlayers.GameID.EQ(table.Games.GameID))).
WHERE(table.GamePlayers.AccountID.EQ(postgres.UUID(accountID))).
ORDER_BY(table.Games.UpdatedAt.DESC())
var grows []model.Games
if err := gstmt.QueryContext(ctx, s.db, &grows); err != nil {
return nil, fmt.Errorf("game: list for account: %w", err)
}
if len(grows) == 0 {
return nil, nil
}
ids := make([]postgres.Expression, len(grows))
for i, g := range grows {
ids[i] = postgres.UUID(g.GameID)
}
sstmt := postgres.SELECT(table.GamePlayers.AllColumns).
FROM(table.GamePlayers).
WHERE(table.GamePlayers.GameID.IN(ids...)).
ORDER_BY(table.GamePlayers.GameID.ASC(), table.GamePlayers.Seat.ASC())
var srows []model.GamePlayers
if err := sstmt.QueryContext(ctx, s.db, &srows); err != nil {
return nil, fmt.Errorf("game: list seats for account: %w", err)
}
byGame := make(map[uuid.UUID][]model.GamePlayers, len(grows))
for _, r := range srows {
byGame[r.GameID] = append(byGame[r.GameID], r)
}
out := make([]Game, 0, len(grows))
for _, g := range grows {
pg, err := projectGame(g, byGame[g.GameID])
if err != nil {
return nil, err
}
out = append(out, pg)
}
return out, nil
}
// GetJournal loads the ordered, decoded move journal for a game.
func (s *Store) GetJournal(ctx context.Context, id uuid.UUID) ([]HistoryMove, error) {
stmt := postgres.SELECT(table.GameMoves.AllColumns).
+2 -2
View File
@@ -15,7 +15,7 @@ const (
StatusFinished = "finished"
)
// ComplaintStatus values; Stage 9 owns the resolution lifecycle, Stage 3 only
// ComplaintStatus values; Stage 10 owns the resolution lifecycle, Stage 3 only
// ever writes StatusComplaintOpen.
const StatusComplaintOpen = "open"
@@ -176,7 +176,7 @@ type RobotTurn struct {
Seed int64
}
// Complaint is a word-check complaint awaiting admin review (Stage 9).
// Complaint is a word-check complaint awaiting admin review (Stage 10).
type Complaint struct {
ID uuid.UUID
ComplainantID uuid.UUID
+57
View File
@@ -86,6 +86,63 @@ func readStats(t *testing.T, id uuid.UUID) (wins, losses, draws, maxGame, maxWor
return wins, losses, draws, maxGame, maxWord, true
}
// TestListForAccount checks the lobby "my games" query: it returns exactly the
// games the account is seated in (each with its seats), and nothing for an outsider.
func TestListForAccount(t *testing.T) {
ctx := context.Background()
svc := newGameService()
me, opp := provisionAccount(t), provisionAccount(t)
g1, err := svc.Create(ctx, game.CreateParams{
Variant: engine.VariantEnglish, Seats: []uuid.UUID{me, opp}, TurnTimeout: 24 * time.Hour, Seed: 1,
})
if err != nil {
t.Fatalf("create g1: %v", err)
}
g2, err := svc.Create(ctx, game.CreateParams{
Variant: engine.VariantEnglish, Seats: []uuid.UUID{opp, me}, TurnTimeout: 24 * time.Hour, Seed: 2,
})
if err != nil {
t.Fatalf("create g2: %v", err)
}
games, err := svc.ListForAccount(ctx, me)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(games) != 2 {
t.Fatalf("got %d games, want 2", len(games))
}
seen := map[uuid.UUID]bool{}
for _, g := range games {
seen[g.ID] = true
if len(g.Seats) != 2 {
t.Errorf("game %s has %d seats, want 2", g.ID, len(g.Seats))
}
seated := false
for _, s := range g.Seats {
if s.AccountID == me {
seated = true
}
}
if !seated {
t.Errorf("account not found among seats of returned game %s", g.ID)
}
}
if !seen[g1.ID] || !seen[g2.ID] {
t.Errorf("returned games %v missing g1=%s or g2=%s", seen, g1.ID, g2.ID)
}
other := provisionAccount(t)
og, err := svc.ListForAccount(ctx, other)
if err != nil {
t.Fatalf("list other: %v", err)
}
if len(og) != 0 {
t.Errorf("outsider sees %d games, want 0", len(og))
}
}
// TestGameLifecycleAndStats drives a greedy two-player game to its natural end
// through the service and checks the finish state and statistics.
func TestGameLifecycleAndStats(t *testing.T) {
+3 -1
View File
@@ -66,10 +66,12 @@ type moveRecordDTO struct {
Total int `json:"total"`
}
// seatDTO is one seat's public standing.
// seatDTO is one seat's public standing. DisplayName is resolved from the account
// store by the handler (the game domain keys seats by account id only).
type seatDTO struct {
Seat int `json:"seat"`
AccountID string `json:"account_id"`
DisplayName string `json:"display_name"`
Score int `json:"score"`
HintsUsed int `json:"hints_used"`
IsWinner bool `json:"is_winner"`
+11
View File
@@ -37,8 +37,17 @@ func (s *Server) registerRoutes() {
u.GET("/profile", s.handleProfile)
}
if s.games != nil {
u.GET("/games", s.handleListGames)
u.POST("/games/:id/play", s.handleSubmitPlay)
u.GET("/games/:id/state", s.handleGameState)
u.POST("/games/:id/pass", s.handlePass)
u.POST("/games/:id/exchange", s.handleExchange)
u.POST("/games/:id/resign", s.handleResign)
u.POST("/games/:id/hint", s.handleHint)
u.POST("/games/:id/evaluate", s.handleEvaluate)
u.GET("/games/:id/check_word", s.handleCheckWord)
u.POST("/games/:id/complaint", s.handleComplaint)
u.GET("/games/:id/history", s.handleHistory)
}
if s.matchmaker != nil {
u.POST("/lobby/enqueue", s.handleEnqueue)
@@ -46,6 +55,8 @@ func (s *Server) registerRoutes() {
}
if s.social != nil {
u.POST("/games/:id/chat", s.handleChatPost)
u.GET("/games/:id/chat", s.handleChatList)
u.POST("/games/:id/nudge", s.handleNudge)
}
s.admin.GET("/ping", s.handleAdminPing)
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
// The /api/v1/admin/* endpoints are reached through the gateway's Basic-Auth
// reverse proxy (docs/ARCHITECTURE.md §12). The backend trusts the gateway to
// have authenticated the operator; the admin surface itself (complaint review,
// dictionary versions) lands in Stage 9. handleAdminPing is the proxy target that
// dictionary versions) lands in Stage 10. handleAdminPing is the proxy target that
// proves the path end to end until then.
func (s *Server) handleAdminPing(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
+321
View File
@@ -0,0 +1,321 @@
package server
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"scrabble/backend/internal/engine"
"scrabble/backend/internal/game"
)
// The handlers below extend the Stage 6 vertical slice with the remaining game and
// chat operations the UI needs (PLAN.md Stage 7). They follow the same pattern as
// handlers_user.go: X-User-ID identity, the domain service call, a JSON DTO mapped
// from the result.
// hintResultDTO is the top-ranked move plus the remaining hint budget.
type hintResultDTO struct {
Move moveRecordDTO `json:"move"`
HintsRemaining int `json:"hints_remaining"`
}
// evalResultDTO is an unlimited move preview: legality, score and the words formed.
type evalResultDTO struct {
Legal bool `json:"legal"`
Score int `json:"score"`
Words []string `json:"words"`
}
// wordCheckDTO is the result of the unlimited dictionary lookup tool.
type wordCheckDTO struct {
Word string `json:"word"`
Legal bool `json:"legal"`
}
// historyDTO is a game's decoded move journal, the source for client board replay.
type historyDTO struct {
GameID string `json:"game_id"`
Moves []moveRecordDTO `json:"moves"`
}
// gameListDTO is the caller's games (active and finished) for the lobby.
type gameListDTO struct {
Games []gameDTO `json:"games"`
}
// chatListDTO is a game's chat history.
type chatListDTO struct {
Messages []chatDTO `json:"messages"`
}
// exchangeRequest swaps the given rack tiles back into the bag.
type exchangeRequest struct {
Tiles []string `json:"tiles"`
}
// complaintRequest disputes a word-check result.
type complaintRequest struct {
Word string `json:"word"`
Note string `json:"note"`
}
// fillSeatNames resolves each seat's display name from the account store, memoising
// across seats and games within one request.
func (s *Server) fillSeatNames(ctx context.Context, g *gameDTO, memo map[string]string) {
for i := range g.Seats {
id := g.Seats[i].AccountID
name, ok := memo[id]
if !ok {
if uid, err := uuid.Parse(id); err == nil {
if acc, err := s.accounts.GetByID(ctx, uid); err == nil {
name = acc.DisplayName
}
}
memo[id] = name
}
g.Seats[i].DisplayName = name
}
}
// moveRecordDTOFromHistory projects a journal move into the shared move DTO.
func moveRecordDTOFromHistory(m game.HistoryMove) moveRecordDTO {
tiles := make([]tileDTO, 0, len(m.Tiles))
for _, t := range m.Tiles {
tiles = append(tiles, tileDTO{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank})
}
return moveRecordDTO{
Player: m.Seat,
Action: m.Action,
Dir: m.Dir,
MainRow: m.MainRow,
MainCol: m.MainCol,
Tiles: tiles,
Words: m.Words,
Count: len(m.Words),
Score: m.Score,
Total: m.RunningTotal,
}
}
// handlePass forfeits the player's turn.
func (s *Server) handlePass(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
res, err := s.games.Pass(c.Request.Context(), gameID, uid)
if err != nil {
s.abortErr(c, err)
return
}
s.writeMoveResult(c, res)
}
// handleExchange swaps the chosen rack tiles back into the bag.
func (s *Server) handleExchange(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
var req exchangeRequest
if err := c.ShouldBindJSON(&req); err != nil {
abortBadRequest(c, "invalid request body")
return
}
res, err := s.games.Exchange(c.Request.Context(), gameID, uid, req.Tiles)
if err != nil {
s.abortErr(c, err)
return
}
s.writeMoveResult(c, res)
}
// handleResign resigns the player from the game.
func (s *Server) handleResign(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
res, err := s.games.Resign(c.Request.Context(), gameID, uid)
if err != nil {
s.abortErr(c, err)
return
}
s.writeMoveResult(c, res)
}
// handleHint reveals the top-ranked move and spends a hint.
func (s *Server) handleHint(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
h, err := s.games.Hint(c.Request.Context(), gameID, uid)
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, hintResultDTO{
Move: moveRecordDTOFrom(h.Move),
HintsRemaining: h.HintsRemaining,
})
}
// handleEvaluate previews a tentative play's legality and score.
func (s *Server) handleEvaluate(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
var req submitPlayRequest
if err := c.ShouldBindJSON(&req); err != nil {
abortBadRequest(c, "invalid request body")
return
}
dir, ok := parseDirection(req.Dir)
if !ok {
abortBadRequest(c, "dir must be H or V")
return
}
tiles := make([]engine.TileRecord, 0, len(req.Tiles))
for _, t := range req.Tiles {
tiles = append(tiles, engine.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank})
}
ev, err := s.games.EvaluatePlay(c.Request.Context(), gameID, uid, dir, tiles)
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, evalResultDTO{Legal: ev.Valid, Score: ev.Score, Words: ev.Words})
}
// handleCheckWord looks a word up in the game's pinned dictionary.
func (s *Server) handleCheckWord(c *gin.Context) {
_, gameID, ok := s.userGame(c)
if !ok {
return
}
word := c.Query("word")
legal, err := s.games.CheckWord(c.Request.Context(), gameID, word)
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, wordCheckDTO{Word: word, Legal: legal})
}
// handleComplaint files a word-check complaint into the admin review queue.
func (s *Server) handleComplaint(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
var req complaintRequest
if err := c.ShouldBindJSON(&req); err != nil {
abortBadRequest(c, "invalid request body")
return
}
if _, err := s.games.FileComplaint(c.Request.Context(), gameID, uid, req.Word, req.Note); err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, okResponse{OK: true})
}
// handleHistory returns a game's decoded move journal for board replay / history.
func (s *Server) handleHistory(c *gin.Context) {
_, gameID, ok := s.userGame(c)
if !ok {
return
}
view, err := s.games.History(c.Request.Context(), gameID)
if err != nil {
s.abortErr(c, err)
return
}
moves := make([]moveRecordDTO, 0, len(view.Moves))
for _, m := range view.Moves {
moves = append(moves, moveRecordDTOFromHistory(m))
}
c.JSON(http.StatusOK, historyDTO{GameID: gameID.String(), Moves: moves})
}
// handleListGames returns the caller's active and finished games for the lobby.
func (s *Server) handleListGames(c *gin.Context) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return
}
games, err := s.games.ListForAccount(c.Request.Context(), uid)
if err != nil {
s.abortErr(c, err)
return
}
memo := map[string]string{}
out := make([]gameDTO, 0, len(games))
for _, g := range games {
dto := gameDTOFromGame(g)
s.fillSeatNames(c.Request.Context(), &dto, memo)
out = append(out, dto)
}
c.JSON(http.StatusOK, gameListDTO{Games: out})
}
// handleChatList returns a game's chat history for the viewer.
func (s *Server) handleChatList(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
msgs, err := s.social.Messages(c.Request.Context(), gameID, uid)
if err != nil {
s.abortErr(c, err)
return
}
out := make([]chatDTO, 0, len(msgs))
for _, m := range msgs {
out = append(out, chatDTOFrom(m))
}
c.JSON(http.StatusOK, chatListDTO{Messages: out})
}
// handleNudge posts a nudge to the player whose turn is awaited.
func (s *Server) handleNudge(c *gin.Context) {
uid, gameID, ok := s.userGame(c)
if !ok {
return
}
msg, err := s.social.Nudge(c.Request.Context(), gameID, uid)
if err != nil {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, chatDTOFrom(msg))
}
// userGame reads the authenticated account and the :id game param, aborting with the
// right status when either is missing. ok is false when the request was aborted.
func (s *Server) userGame(c *gin.Context) (uuid.UUID, uuid.UUID, bool) {
uid, ok := userID(c)
if !ok {
abortBadRequest(c, "missing identity")
return uuid.UUID{}, uuid.UUID{}, false
}
gameID, ok := gameIDParam(c)
if !ok {
abortBadRequest(c, "invalid game id")
return uuid.UUID{}, uuid.UUID{}, false
}
return uid, gameID, true
}
// writeMoveResult emits a committed move with seat display names filled in.
func (s *Server) writeMoveResult(c *gin.Context, res game.MoveResult) {
dto := moveResultDTOFrom(res)
s.fillSeatNames(c.Request.Context(), &dto.Game, map[string]string{})
c.JSON(http.StatusOK, dto)
}
+14 -4
View File
@@ -68,7 +68,7 @@ func (s *Server) handleSubmitPlay(c *gin.Context) {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, moveResultDTOFrom(res))
s.writeMoveResult(c, res)
}
// handleGameState returns the player's view of a game.
@@ -88,7 +88,9 @@ func (s *Server) handleGameState(c *gin.Context) {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, stateDTOFrom(view))
dto := stateDTOFrom(view)
s.fillSeatNames(c.Request.Context(), &dto.Game, map[string]string{})
c.JSON(http.StatusOK, dto)
}
// enqueueRequest joins the per-variant auto-match pool.
@@ -118,7 +120,11 @@ func (s *Server) handleEnqueue(c *gin.Context) {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, matchDTOFrom(res))
dto := matchDTOFrom(res)
if dto.Game != nil {
s.fillSeatNames(c.Request.Context(), dto.Game, map[string]string{})
}
c.JSON(http.StatusOK, dto)
}
// handlePoll reports whether the caller has been paired since queueing.
@@ -133,7 +139,11 @@ func (s *Server) handlePoll(c *gin.Context) {
s.abortErr(c, err)
return
}
c.JSON(http.StatusOK, matchDTOFrom(res))
dto := matchDTOFrom(res)
if dto.Game != nil {
s.fillSeatNames(c.Request.Context(), dto.Game, map[string]string{})
}
c.JSON(http.StatusOK, dto)
}
// chatPostRequest posts a per-game chat message.
+21 -10
View File
@@ -24,9 +24,20 @@ Three executables plus per-platform side-services:
administration. Embeds the **`scrabble-solver`** engine **as a library,
in-process** — there is no per-game container. The only network consumer of
`backend` is `gateway` (plus platform side-services over an internal API).
- **`ui`** *(planned)* — pure-HTML5 client (plain Svelte + Vite, static build).
Talks to `backend` only through `gateway`. Embeddable in platform webviews;
packageable to native (iOS/Android) via Capacitor.
- **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite, static build;
no SvelteKit). Talks to `backend` only through `gateway` over Connect-RPC +
FlatBuffers, with the edge TS bindings generated from the **same** `edge.proto`
and `scrabble.fbs` and committed under `ui/src/gen/`. The **playable slice**
(Stage 7) covers auth, "my games", auto-match, the board (play/pass/exchange/
resign), hint, word-check, chat/nudge, the live stream, i18n (en/ru) and a profile
view; the social/account/history surfaces follow in Stage 8. There is no board on
the wire — the client **reconstructs the 15×15 board by replaying the move
journal** (§9.1) and renders board, tiles, premium squares and effects as pure
CSS + Unicode (no image/font/SVG assets). Tiles are placed by Pointer-Events drag
or tap; a CSS-token theme is light/dark and Telegram-themeParams-ready; navigation
is a hash router and the session token is held in memory + IndexedDB. A build-flagged
in-memory mock transport (`pnpm start`) runs the whole slice with no backend.
Embeddable in platform webviews; packageable to native (iOS/Android) via Capacitor.
- **`platform/<name>`** *(planned)* — per-platform side-services (Telegram bot
first): deep-link invites and platform-native push notifications. They talk
to `backend` over an internal API.
@@ -108,7 +119,7 @@ arrive from a platform rather than completing a mandatory registration).
TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a
development log mailer when none is configured) and, once verified, attaches a
confirmed email identity. An email already confirmed by **another** account is
refused — adopting it would be a merge, which Stage 10 owns. Accounts and
refused — adopting it would be a merge, which Stage 11 owns. Accounts and
identities use application-generated **UUIDv7** primary keys.
- **Linking** is initiated from an authenticated profile: choose a platform →
complete that platform's web-auth confirm → attach the identity to the
@@ -140,7 +151,7 @@ Key points:
word-check tool through `Registry.Lookup`.
- **Dictionary versioning — pin per game.** A game records the `dict_version` it
started on and finishes on that version; new games use the latest. Multiple
versions may be resident at once. An admin reload *(planned, Stage 9)*
versions may be resident at once. An admin reload *(planned, Stage 10)*
registers a new version through `Registry.Load`; delivery is the DAWG file in
the image / a volume mounted at the dictionary directory. (A future split of
the solver into engine + dictionary generator with versioned artifacts is
@@ -202,7 +213,7 @@ Key points:
- **Word-check tool**: unlimited dictionary lookups against the game's pinned
dictionary; each result offers a **complaint** (complainant, game, variant,
dict_version, word, the disputed result, an optional note) that lands in an
admin review queue *(admin side planned, Stage 9)*.
admin review queue *(admin side planned, Stage 10)*.
## 7. Robot opponent
@@ -250,7 +261,7 @@ requires (there is no DM surface; chat is per-game).
emits a **match-found** notification (§10), delivered over the live stream;
`Poll` remains as a fallback for a client that is not currently streaming.
- **Friends**: a **request → accept** graph (one `friendships` table) — add by
friend list or internal ID now, by platform deep-link with Stage 8. Declining or
friend list or internal ID now, by platform deep-link with Stage 9. Declining or
cancelling removes the pending request; blocking someone severs an existing
friendship.
- **Block**: two independent **global** account toggles (`block_chat`,
@@ -275,7 +286,7 @@ requires (there is no DM surface; chat is per-game).
(confirm-code binding, see §4), **timezone** (drives the away window and the
robot's sleep; user-editable), the daily **away window** and the block toggles —
all editable through `account.UpdateProfile`. Linked platform accounts and merge
are Stage 10.
are Stage 11.
## 9. Persistence
@@ -337,7 +348,7 @@ does not cover.
## 10. Notifications
Two channels: the **in-app live stream** (delivered from Stage 6) and
**platform-native push** (out-of-app, via the platform side-service — Stage 8).
**platform-native push** (out-of-app, via the platform side-service — Stage 9).
The backend emits notification intents through an in-process hub
(`internal/notify`, a `Publisher` seam installed on the game, social and lobby
services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`,
@@ -348,7 +359,7 @@ robot-driver and timeout-sweeper moves emit too), **chat-message** and **nudge**
(from the social service), and **match-found** (from the matchmaker, §8). Event
payloads are FlatBuffers-encoded by the backend and forwarded verbatim. A client
that is not currently streaming falls back to the matchmaker's `Poll` for
match-found. Out-of-app platform push (your-turn, nudge) is wired in Stage 8;
match-found. Out-of-app platform push (your-turn, nudge) is wired in Stage 9;
session-revocation events and cursor-based stream resume are deferred
(single-instance MVP).
+13 -3
View File
@@ -9,6 +9,16 @@ the detail is authored.
## Domains
### Client app *(Stage 7 / 8)*
The web/app client (Svelte + Vite) realizes these stories. The **playable slice**
(Stage 7) covers signing in (guest or email), the "my games" lobby, starting an
auto-match, playing the board (place tiles by drag or tap, pass, exchange, resign),
the top-1 hint, the unlimited word-check with complaint, per-game chat and nudge,
real-time in-app updates, switching interface language (en/ru) and theme, and a
read-only profile. Managing friends and blocks, creating friend games (invitations),
editing the profile, the statistics screen and the history/GCG viewer arrive in
Stage 8.
### Identity & sessions *(Stage 1 / 6)*
A player arrives from a platform (Telegram first), via email login, or as an
ephemeral guest. The gateway validates the credential once and mints a thin
@@ -17,7 +27,7 @@ session-only with restricted features (auto-match only; no friends, stats or
history). While the app is open the client keeps a live stream and receives
in-app updates in real time — the opponent's move, your turn, chat, nudges and a
found match; out-of-app push (your turn, nudge) is delivered by the platform
later (Stage 8).
later (Stage 9).
### Accounts, linking & merge *(Stage 1 / 10)*
First platform contact auto-provisions a durable account. From the profile a
@@ -76,7 +86,7 @@ Edit language (en/ru), display name, timezone, the daily away window and the blo
toggles, and bind an email by confirm-code: the backend emails a short code that,
once entered, attaches the email to the account (an email already confirmed by
another account cannot be taken — that is a merge, a later stage). Linked platform
accounts and merge arrive in Stage 10.
accounts and merge arrive in Stage 11.
### History & statistics *(Stage 3)*
Finished games are archived in a dictionary-independent form and exportable to
@@ -84,6 +94,6 @@ GCG. Statistics (durable accounts only): wins, losses, draws, max points in a
game, and max points for a single move (the best play, which already includes
every word it formed plus the all-tiles bonus).
### Administration *(Stage 9)*
### Administration *(Stage 10)*
Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary
versions, and inspects users/games.
+13 -3
View File
@@ -8,6 +8,16 @@
## Домены
### Клиентское приложение *(Stage 7 / 8)*
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. **Играбельный срез**
(Stage 7) покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
профиль только для чтения. Управление друзьями и блоками, создание дружеских игр
(приглашения), редактирование профиля, экран статистики и просмотр истории/GCG
появятся в Stage 8.
### Личность и сессии *(Stage 1 / 6)*
Игрок приходит с платформы (сначала Telegram), через email-вход или как
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
@@ -16,7 +26,7 @@ session-токен; backend сопоставляет его с внутренн
статистики и истории). Пока приложение открыто, клиент держит живой стрим и
получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и
найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит
позже (Stage 8).
позже (Stage 9).
### Аккаунты, привязка и слияние *(Stage 1 / 10)*
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
@@ -76,7 +86,7 @@ push доставляется через платформу.
confirm-коду: backend шлёт на почту короткий код, и после ввода email
привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
слияние появятся в Stage 10.
слияние появятся в Stage 11.
### История и статистика *(Stage 3)*
Завершённые партии архивируются в независимом от словаря виде и экспортируются
@@ -84,6 +94,6 @@ confirm-коду: backend шлёт на почту короткий код, и
макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все
образованные им слова и бонус за все фишки).
### Администрирование *(Stage 9)*
### Администрирование *(Stage 10)*
Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями
словаря, смотрит пользователей/игры.
+1 -1
View File
@@ -2,7 +2,7 @@
// reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway
// validates the operator credential and forwards authenticated requests to
// backend /api/v1/admin/*; the backend trusts the gateway on this segment. The
// admin API itself is filled in Stage 9.
// admin API itself is filled in Stage 10.
package admin
import (
+118
View File
@@ -56,6 +56,7 @@ type MoveRecordResp struct {
type SeatResp struct {
Seat int `json:"seat"`
AccountID string `json:"account_id"`
DisplayName string `json:"display_name"`
Score int `json:"score"`
HintsUsed int `json:"hints_used"`
IsWinner bool `json:"is_winner"`
@@ -189,3 +190,120 @@ func (c *Client) ChatPost(ctx context.Context, userID, gameID, body, clientIP st
map[string]string{"body": body}, &out)
return out, err
}
// HintResultResp is the top-ranked move plus the remaining hint budget.
type HintResultResp struct {
Move MoveRecordResp `json:"move"`
HintsRemaining int `json:"hints_remaining"`
}
// EvalResultResp is an unlimited move preview.
type EvalResultResp struct {
Legal bool `json:"legal"`
Score int `json:"score"`
Words []string `json:"words"`
}
// WordCheckResp is a dictionary lookup outcome.
type WordCheckResp struct {
Word string `json:"word"`
Legal bool `json:"legal"`
}
// HistoryResp is a game's decoded move journal.
type HistoryResp struct {
GameID string `json:"game_id"`
Moves []MoveRecordResp `json:"moves"`
}
// GameListResp is the caller's games for the lobby.
type GameListResp struct {
Games []GameResp `json:"games"`
}
// ChatListResp is a game's chat history.
type ChatListResp struct {
Messages []ChatResp `json:"messages"`
}
func (c *Client) gamePath(gameID, suffix string) string {
return "/api/v1/user/games/" + url.PathEscape(gameID) + suffix
}
// Pass forfeits the player's turn.
func (c *Client) Pass(ctx context.Context, userID, gameID string) (MoveResultResp, error) {
var out MoveResultResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/pass"), userID, "", struct{}{}, &out)
return out, err
}
// Exchange swaps the chosen rack tiles back into the bag.
func (c *Client) Exchange(ctx context.Context, userID, gameID string, tiles []string) (MoveResultResp, error) {
var out MoveResultResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/exchange"), userID, "",
map[string]any{"tiles": tiles}, &out)
return out, err
}
// Resign resigns the player from the game.
func (c *Client) Resign(ctx context.Context, userID, gameID string) (MoveResultResp, error) {
var out MoveResultResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/resign"), userID, "", struct{}{}, &out)
return out, err
}
// Hint reveals the top-ranked move and spends a hint.
func (c *Client) Hint(ctx context.Context, userID, gameID string) (HintResultResp, error) {
var out HintResultResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/hint"), userID, "", struct{}{}, &out)
return out, err
}
// Evaluate previews a tentative play's legality and score.
func (c *Client) Evaluate(ctx context.Context, userID, gameID, dir string, tiles []TileJSON) (EvalResultResp, error) {
var out EvalResultResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/evaluate"), userID, "",
map[string]any{"dir": dir, "tiles": tiles}, &out)
return out, err
}
// CheckWord looks a word up in the game's pinned dictionary.
func (c *Client) CheckWord(ctx context.Context, userID, gameID, word string) (WordCheckResp, error) {
var out WordCheckResp
err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/check_word")+"?word="+url.QueryEscape(word), userID, "", nil, &out)
return out, err
}
// Complaint disputes a word-check result.
func (c *Client) Complaint(ctx context.Context, userID, gameID, word, note string) error {
return c.do(ctx, http.MethodPost, c.gamePath(gameID, "/complaint"), userID, "",
map[string]string{"word": word, "note": note}, nil)
}
// History returns a game's decoded move journal.
func (c *Client) History(ctx context.Context, userID, gameID string) (HistoryResp, error) {
var out HistoryResp
err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/history"), userID, "", nil, &out)
return out, err
}
// ChatList returns a game's chat history.
func (c *Client) ChatList(ctx context.Context, userID, gameID string) (ChatListResp, error) {
var out ChatListResp
err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/chat"), userID, "", nil, &out)
return out, err
}
// Nudge posts a nudge to the player whose turn is awaited.
func (c *Client) Nudge(ctx context.Context, userID, gameID string) (ChatResp, error) {
var out ChatResp
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/nudge"), userID, "", struct{}{}, &out)
return out, err
}
// GamesList returns the caller's active and finished games.
func (c *Client) GamesList(ctx context.Context, userID string) (GameListResp, error) {
var out GameListResp
err := c.do(ctx, http.MethodGet, "/api/v1/user/games", userID, "", nil, &out)
return out, err
}
+101 -4
View File
@@ -100,9 +100,8 @@ func encodeMatch(m backendclient.MatchResp) []byte {
return b.FinishedBytes()
}
// encodeChat builds a ChatMessage payload.
func encodeChat(c backendclient.ChatResp) []byte {
b := flatbuffers.NewBuilder(192)
// buildChatMessage builds a ChatMessage table and returns its offset.
func buildChatMessage(b *flatbuffers.Builder, c backendclient.ChatResp) flatbuffers.UOffsetT {
id := b.CreateString(c.ID)
gid := b.CreateString(c.GameID)
sid := b.CreateString(c.SenderID)
@@ -115,7 +114,103 @@ func encodeChat(c backendclient.ChatResp) []byte {
fb.ChatMessageAddKind(b, kind)
fb.ChatMessageAddBody(b, body)
fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix)
b.Finish(fb.ChatMessageEnd(b))
return fb.ChatMessageEnd(b)
}
// encodeChat builds a ChatMessage payload.
func encodeChat(c backendclient.ChatResp) []byte {
b := flatbuffers.NewBuilder(192)
b.Finish(buildChatMessage(b, c))
return b.FinishedBytes()
}
// encodeHintResult builds a HintResult payload.
func encodeHintResult(r backendclient.HintResultResp) []byte {
b := flatbuffers.NewBuilder(512)
move := buildMoveRecord(b, r.Move)
fb.HintResultStart(b)
fb.HintResultAddMove(b, move)
fb.HintResultAddHintsRemaining(b, int32(r.HintsRemaining))
b.Finish(fb.HintResultEnd(b))
return b.FinishedBytes()
}
// encodeEvalResult builds an EvalResult payload.
func encodeEvalResult(r backendclient.EvalResultResp) []byte {
b := flatbuffers.NewBuilder(256)
words := buildStringVector(b, r.Words, fb.EvalResultStartWordsVector)
fb.EvalResultStart(b)
fb.EvalResultAddLegal(b, r.Legal)
fb.EvalResultAddScore(b, int32(r.Score))
fb.EvalResultAddWords(b, words)
b.Finish(fb.EvalResultEnd(b))
return b.FinishedBytes()
}
// encodeWordCheck builds a WordCheckResult payload.
func encodeWordCheck(r backendclient.WordCheckResp) []byte {
b := flatbuffers.NewBuilder(64)
word := b.CreateString(r.Word)
fb.WordCheckResultStart(b)
fb.WordCheckResultAddWord(b, word)
fb.WordCheckResultAddLegal(b, r.Legal)
b.Finish(fb.WordCheckResultEnd(b))
return b.FinishedBytes()
}
// encodeHistory builds a History payload (the decoded move journal).
func encodeHistory(r backendclient.HistoryResp) []byte {
b := flatbuffers.NewBuilder(1024)
moveOffs := make([]flatbuffers.UOffsetT, len(r.Moves))
for i, m := range r.Moves {
moveOffs[i] = buildMoveRecord(b, m)
}
fb.HistoryStartMovesVector(b, len(moveOffs))
for i := len(moveOffs) - 1; i >= 0; i-- {
b.PrependUOffsetT(moveOffs[i])
}
moves := b.EndVector(len(moveOffs))
gid := b.CreateString(r.GameID)
fb.HistoryStart(b)
fb.HistoryAddGameId(b, gid)
fb.HistoryAddMoves(b, moves)
b.Finish(fb.HistoryEnd(b))
return b.FinishedBytes()
}
// encodeGameList builds a GameList payload (the caller's games).
func encodeGameList(r backendclient.GameListResp) []byte {
b := flatbuffers.NewBuilder(1024)
gameOffs := make([]flatbuffers.UOffsetT, len(r.Games))
for i, g := range r.Games {
gameOffs[i] = buildGameView(b, g)
}
fb.GameListStartGamesVector(b, len(gameOffs))
for i := len(gameOffs) - 1; i >= 0; i-- {
b.PrependUOffsetT(gameOffs[i])
}
games := b.EndVector(len(gameOffs))
fb.GameListStart(b)
fb.GameListAddGames(b, games)
b.Finish(fb.GameListEnd(b))
return b.FinishedBytes()
}
// encodeChatList builds a ChatList payload (a game's chat history).
func encodeChatList(r backendclient.ChatListResp) []byte {
b := flatbuffers.NewBuilder(512)
msgOffs := make([]flatbuffers.UOffsetT, len(r.Messages))
for i, m := range r.Messages {
msgOffs[i] = buildChatMessage(b, m)
}
fb.ChatListStartMessagesVector(b, len(msgOffs))
for i := len(msgOffs) - 1; i >= 0; i-- {
b.PrependUOffsetT(msgOffs[i])
}
msgs := b.EndVector(len(msgOffs))
fb.ChatListStart(b)
fb.ChatListAddMessages(b, msgs)
b.Finish(fb.ChatListEnd(b))
return b.FinishedBytes()
}
@@ -124,12 +219,14 @@ func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers
seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
for i, s := range g.Seats {
aid := b.CreateString(s.AccountID)
dname := b.CreateString(s.DisplayName)
fb.SeatViewStart(b)
fb.SeatViewAddSeat(b, int32(s.Seat))
fb.SeatViewAddAccountId(b, aid)
fb.SeatViewAddScore(b, int32(s.Score))
fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
fb.SeatViewAddIsWinner(b, s.IsWinner)
fb.SeatViewAddDisplayName(b, dname)
seatOffs[i] = fb.SeatViewEnd(b)
}
fb.GameViewStartSeatsVector(b, len(seatOffs))
+169
View File
@@ -26,6 +26,17 @@ const (
MsgLobbyEnqueue = "lobby.enqueue"
MsgLobbyPoll = "lobby.poll"
MsgChatPost = "chat.post"
MsgGamesList = "games.list"
MsgGamePass = "game.pass"
MsgGameExchange = "game.exchange"
MsgGameResign = "game.resign"
MsgGameHint = "game.hint"
MsgGameEvaluate = "game.evaluate"
MsgGameCheckWord = "game.check_word"
MsgGameComplaint = "game.complaint"
MsgGameHistory = "game.history"
MsgChatList = "chat.list"
MsgChatNudge = "chat.nudge"
)
// Request is one decoded Execute call.
@@ -69,6 +80,17 @@ func NewRegistry(backend *backendclient.Client, tg auth.TelegramValidator) *Regi
r.ops[MsgLobbyEnqueue] = Op{Handler: enqueueHandler(backend), Auth: true}
r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true}
r.ops[MsgChatPost] = Op{Handler: chatPostHandler(backend), Auth: true}
r.ops[MsgGamesList] = Op{Handler: gamesListHandler(backend), Auth: true}
r.ops[MsgGamePass] = Op{Handler: passHandler(backend), Auth: true}
r.ops[MsgGameExchange] = Op{Handler: exchangeHandler(backend), Auth: true}
r.ops[MsgGameResign] = Op{Handler: resignHandler(backend), Auth: true}
r.ops[MsgGameHint] = Op{Handler: hintHandler(backend), Auth: true}
r.ops[MsgGameEvaluate] = Op{Handler: evaluateHandler(backend), Auth: true}
r.ops[MsgGameCheckWord] = Op{Handler: checkWordHandler(backend), Auth: true}
r.ops[MsgGameComplaint] = Op{Handler: complaintHandler(backend), Auth: true}
r.ops[MsgGameHistory] = Op{Handler: historyHandler(backend), Auth: true}
r.ops[MsgChatList] = Op{Handler: chatListHandler(backend), Auth: true}
r.ops[MsgChatNudge] = Op{Handler: nudgeHandler(backend), Auth: true}
return r
}
@@ -219,3 +241,150 @@ func decodeTiles(in *fb.SubmitPlayRequest) []backendclient.TileJSON {
}
return tiles
}
// decodeEvalTiles reads the tentative tiles from an EvalRequest.
func decodeEvalTiles(in *fb.EvalRequest) []backendclient.TileJSON {
n := in.TilesLength()
tiles := make([]backendclient.TileJSON, 0, n)
var t fb.TileRecord
for i := 0; i < n; i++ {
if in.Tiles(&t, i) {
tiles = append(tiles, backendclient.TileJSON{
Row: int(t.Row()),
Col: int(t.Col()),
Letter: string(t.Letter()),
Blank: t.Blank(),
})
}
}
return tiles
}
// decodeStringVector reads the exchange tiles from an ExchangeRequest.
func decodeStringVector(in *fb.ExchangeRequest) []string {
n := in.TilesLength()
out := make([]string, 0, n)
for i := 0; i < n; i++ {
out = append(out, string(in.Tiles(i)))
}
return out
}
func gamesListHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
res, err := backend.GamesList(ctx, req.UserID)
if err != nil {
return nil, err
}
return encodeGameList(res), nil
}
}
func passHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.Pass(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeMoveResult(res), nil
}
}
func resignHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.Resign(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeMoveResult(res), nil
}
}
func exchangeHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsExchangeRequest(req.Payload, 0)
res, err := backend.Exchange(ctx, req.UserID, string(in.GameId()), decodeStringVector(in))
if err != nil {
return nil, err
}
return encodeMoveResult(res), nil
}
}
func hintHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.Hint(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeHintResult(res), nil
}
}
func evaluateHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsEvalRequest(req.Payload, 0)
res, err := backend.Evaluate(ctx, req.UserID, string(in.GameId()), string(in.Dir()), decodeEvalTiles(in))
if err != nil {
return nil, err
}
return encodeEvalResult(res), nil
}
}
func checkWordHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsCheckWordRequest(req.Payload, 0)
res, err := backend.CheckWord(ctx, req.UserID, string(in.GameId()), string(in.Word()))
if err != nil {
return nil, err
}
return encodeWordCheck(res), nil
}
}
func complaintHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsComplaintRequest(req.Payload, 0)
if err := backend.Complaint(ctx, req.UserID, string(in.GameId()), string(in.Word()), string(in.Note())); err != nil {
return nil, err
}
return encodeAck(true), nil
}
}
func historyHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.History(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeHistory(res), nil
}
}
func chatListHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.ChatList(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeChatList(res), nil
}
}
func nudgeHandler(backend *backendclient.Client) Handler {
return func(ctx context.Context, req Request) ([]byte, error) {
in := fb.GetRootAsGameActionRequest(req.Payload, 0)
res, err := backend.Nudge(ctx, req.UserID, string(in.GameId()))
if err != nil {
return nil, err
}
return encodeChat(res), nil
}
}
@@ -139,3 +139,96 @@ func TestDomainErrorSurfacesBackendCode(t *testing.T) {
t.Fatalf("DomainCode = (%q, %v), want (not_your_turn, true)", code, ok)
}
}
// gameActionPayload builds a GameActionRequest payload (pass / resign / hint / etc.).
func gameActionPayload(gameID string) []byte {
b := flatbuffers.NewBuilder(32)
gid := b.CreateString(gameID)
fb.GameActionRequestStart(b)
fb.GameActionRequestAddGameId(b, gid)
b.Finish(fb.GameActionRequestEnd(b))
return b.FinishedBytes()
}
func TestGamesListRoundTripDecodesSeatNames(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-User-ID"); got != "u-9" {
t.Errorf("X-User-ID = %q, want u-9", got)
}
if r.URL.Path != "/api/v1/user/games" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"games":[{"id":"g-1","variant":"english","status":"active","players":2,"to_move":0,"seats":[{"seat":0,"account_id":"u-9","display_name":"You","score":10},{"seat":1,"account_id":"a-1","display_name":"Ann","score":7}]}]}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGamesList)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-9"})
if err != nil {
t.Fatalf("handler: %v", err)
}
gl := fb.GetRootAsGameList(payload, 0)
if gl.GamesLength() != 1 {
t.Fatalf("games length = %d, want 1", gl.GamesLength())
}
var g fb.GameView
gl.Games(&g, 0)
if string(g.Id()) != "g-1" {
t.Errorf("game id = %q, want g-1", g.Id())
}
var seat fb.SeatView
g.Seats(&seat, 1)
if string(seat.DisplayName()) != "Ann" {
t.Errorf("seat display name = %q, want Ann", seat.DisplayName())
}
}
func TestPassRoundTrip(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/games/g-2/pass" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"pass"},"game":{"id":"g-2","status":"active","seats":[{"seat":0,"account_id":"u-1","display_name":"You"}]}}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGamePass)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: gameActionPayload("g-2")})
if err != nil {
t.Fatalf("handler: %v", err)
}
mr := fb.GetRootAsMoveResult(payload, 0)
var move fb.MoveRecord
mr.Move(&move)
if string(move.Action()) != "pass" {
t.Errorf("action = %q, want pass", move.Action())
}
}
func TestHintRoundTrip(t *testing.T) {
backend, cleanup := fakeBackend(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/user/games/g-3/hint" {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"move":{"player":0,"action":"play","words":["CAT"],"score":9},"hints_remaining":2}`))
})
defer cleanup()
reg := transcode.NewRegistry(backend, nil)
op, _ := reg.Lookup(transcode.MsgGameHint)
payload, err := op.Handler(context.Background(), transcode.Request{UserID: "u-1", Payload: gameActionPayload("g-3")})
if err != nil {
t.Fatalf("handler: %v", err)
}
hr := fb.GetRootAsHintResult(payload, 0)
if hr.HintsRemaining() != 2 {
t.Errorf("hints remaining = %d, want 2", hr.HintsRemaining())
}
var move fb.MoveRecord
hr.Move(&move)
if move.Score() != 9 {
t.Errorf("hint move score = %d, want 9", move.Score())
}
}
+69 -1
View File
@@ -23,13 +23,15 @@ table TileRecord {
blank:bool;
}
// SeatView is one seat's public standing in a game.
// 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.
@@ -143,6 +145,67 @@ table StateView {
hints_remaining: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.
table ExchangeRequest {
game_id:string;
tiles:[string];
}
// EvalRequest previews a tentative play without committing it.
table EvalRequest {
game_id:string;
dir:string;
tiles:[TileRecord];
}
// EvalResult is an unlimited move preview: legality, score and the words formed.
table EvalResult {
legal:bool;
score:int;
words:[string];
}
// CheckWordRequest looks a word up in the game's pinned dictionary.
table CheckWordRequest {
game_id:string;
word:string;
}
// 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.
table HintResult {
move:MoveRecord;
hints_remaining:int;
}
// 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.
table GameList {
games:[GameView];
}
// --- lobby (authenticated) ---
// EnqueueRequest joins the per-variant auto-match pool.
@@ -174,6 +237,11 @@ table ChatMessage {
created_at_unix:long;
}
// ChatList is a game's chat history.
table ChatList {
messages:[ChatMessage];
}
// --- push event payloads ---
// YourTurnEvent signals that it is now the recipient's turn.
+75
View File
@@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type ChatList struct {
_tab flatbuffers.Table
}
func GetRootAsChatList(buf []byte, offset flatbuffers.UOffsetT) *ChatList {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ChatList{}
x.Init(buf, n+offset)
return x
}
func FinishChatListBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsChatList(buf []byte, offset flatbuffers.UOffsetT) *ChatList {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ChatList{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedChatListBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ChatList) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ChatList) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ChatList) Messages(obj *ChatMessage, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *ChatList) MessagesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func ChatListStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func ChatListAddMessages(builder *flatbuffers.Builder, messages flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messages), 0)
}
func ChatListStartMessagesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func ChatListEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+71
View File
@@ -0,0 +1,71 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type CheckWordRequest struct {
_tab flatbuffers.Table
}
func GetRootAsCheckWordRequest(buf []byte, offset flatbuffers.UOffsetT) *CheckWordRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &CheckWordRequest{}
x.Init(buf, n+offset)
return x
}
func FinishCheckWordRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsCheckWordRequest(buf []byte, offset flatbuffers.UOffsetT) *CheckWordRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &CheckWordRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedCheckWordRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *CheckWordRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *CheckWordRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *CheckWordRequest) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *CheckWordRequest) Word() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func CheckWordRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func CheckWordRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func CheckWordRequestAddWord(builder *flatbuffers.Builder, word flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(word), 0)
}
func CheckWordRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+82
View File
@@ -0,0 +1,82 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type ComplaintRequest struct {
_tab flatbuffers.Table
}
func GetRootAsComplaintRequest(buf []byte, offset flatbuffers.UOffsetT) *ComplaintRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ComplaintRequest{}
x.Init(buf, n+offset)
return x
}
func FinishComplaintRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsComplaintRequest(buf []byte, offset flatbuffers.UOffsetT) *ComplaintRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ComplaintRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedComplaintRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ComplaintRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ComplaintRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ComplaintRequest) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *ComplaintRequest) Word() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *ComplaintRequest) Note() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func ComplaintRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func ComplaintRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func ComplaintRequestAddWord(builder *flatbuffers.Builder, word flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(word), 0)
}
func ComplaintRequestAddNote(builder *flatbuffers.Builder, note flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(note), 0)
}
func ComplaintRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+97
View File
@@ -0,0 +1,97 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type EvalRequest struct {
_tab flatbuffers.Table
}
func GetRootAsEvalRequest(buf []byte, offset flatbuffers.UOffsetT) *EvalRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &EvalRequest{}
x.Init(buf, n+offset)
return x
}
func FinishEvalRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsEvalRequest(buf []byte, offset flatbuffers.UOffsetT) *EvalRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &EvalRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedEvalRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *EvalRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *EvalRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *EvalRequest) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EvalRequest) Dir() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EvalRequest) Tiles(obj *TileRecord, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *EvalRequest) TilesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func EvalRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func EvalRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func EvalRequestAddDir(builder *flatbuffers.Builder, dir flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(dir), 0)
}
func EvalRequestAddTiles(builder *flatbuffers.Builder, tiles flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(tiles), 0)
}
func EvalRequestStartTilesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func EvalRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+102
View File
@@ -0,0 +1,102 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type EvalResult struct {
_tab flatbuffers.Table
}
func GetRootAsEvalResult(buf []byte, offset flatbuffers.UOffsetT) *EvalResult {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &EvalResult{}
x.Init(buf, n+offset)
return x
}
func FinishEvalResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsEvalResult(buf []byte, offset flatbuffers.UOffsetT) *EvalResult {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &EvalResult{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedEvalResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *EvalResult) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *EvalResult) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *EvalResult) Legal() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *EvalResult) MutateLegal(n bool) bool {
return rcv._tab.MutateBoolSlot(4, n)
}
func (rcv *EvalResult) Score() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *EvalResult) MutateScore(n int32) bool {
return rcv._tab.MutateInt32Slot(6, n)
}
func (rcv *EvalResult) Words(j int) []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.ByteVector(a + flatbuffers.UOffsetT(j*4))
}
return nil
}
func (rcv *EvalResult) WordsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func EvalResultStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func EvalResultAddLegal(builder *flatbuffers.Builder, legal bool) {
builder.PrependBoolSlot(0, legal, false)
}
func EvalResultAddScore(builder *flatbuffers.Builder, score int32) {
builder.PrependInt32Slot(1, score, 0)
}
func EvalResultAddWords(builder *flatbuffers.Builder, words flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(words), 0)
}
func EvalResultStartWordsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func EvalResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+83
View File
@@ -0,0 +1,83 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type ExchangeRequest struct {
_tab flatbuffers.Table
}
func GetRootAsExchangeRequest(buf []byte, offset flatbuffers.UOffsetT) *ExchangeRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ExchangeRequest{}
x.Init(buf, n+offset)
return x
}
func FinishExchangeRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsExchangeRequest(buf []byte, offset flatbuffers.UOffsetT) *ExchangeRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ExchangeRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedExchangeRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ExchangeRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ExchangeRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ExchangeRequest) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *ExchangeRequest) Tiles(j int) []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.ByteVector(a + flatbuffers.UOffsetT(j*4))
}
return nil
}
func (rcv *ExchangeRequest) TilesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func ExchangeRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func ExchangeRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func ExchangeRequestAddTiles(builder *flatbuffers.Builder, tiles flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(tiles), 0)
}
func ExchangeRequestStartTilesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func ExchangeRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+60
View File
@@ -0,0 +1,60 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type GameActionRequest struct {
_tab flatbuffers.Table
}
func GetRootAsGameActionRequest(buf []byte, offset flatbuffers.UOffsetT) *GameActionRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &GameActionRequest{}
x.Init(buf, n+offset)
return x
}
func FinishGameActionRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGameActionRequest(buf []byte, offset flatbuffers.UOffsetT) *GameActionRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &GameActionRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedGameActionRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *GameActionRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *GameActionRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *GameActionRequest) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func GameActionRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func GameActionRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func GameActionRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+75
View File
@@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type GameList struct {
_tab flatbuffers.Table
}
func GetRootAsGameList(buf []byte, offset flatbuffers.UOffsetT) *GameList {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &GameList{}
x.Init(buf, n+offset)
return x
}
func FinishGameListBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGameList(buf []byte, offset flatbuffers.UOffsetT) *GameList {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &GameList{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedGameListBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *GameList) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *GameList) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *GameList) Games(obj *GameView, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *GameList) GamesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func GameListStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func GameListAddGames(builder *flatbuffers.Builder, games flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(games), 0)
}
func GameListStartGamesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func GameListEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+80
View File
@@ -0,0 +1,80 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type HintResult struct {
_tab flatbuffers.Table
}
func GetRootAsHintResult(buf []byte, offset flatbuffers.UOffsetT) *HintResult {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &HintResult{}
x.Init(buf, n+offset)
return x
}
func FinishHintResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsHintResult(buf []byte, offset flatbuffers.UOffsetT) *HintResult {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &HintResult{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedHintResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *HintResult) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *HintResult) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *HintResult) Move(obj *MoveRecord) *MoveRecord {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MoveRecord)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *HintResult) HintsRemaining() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *HintResult) MutateHintsRemaining(n int32) bool {
return rcv._tab.MutateInt32Slot(6, n)
}
func HintResultStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func HintResultAddMove(builder *flatbuffers.Builder, move flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(move), 0)
}
func HintResultAddHintsRemaining(builder *flatbuffers.Builder, hintsRemaining int32) {
builder.PrependInt32Slot(1, hintsRemaining, 0)
}
func HintResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+86
View File
@@ -0,0 +1,86 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type History struct {
_tab flatbuffers.Table
}
func GetRootAsHistory(buf []byte, offset flatbuffers.UOffsetT) *History {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &History{}
x.Init(buf, n+offset)
return x
}
func FinishHistoryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsHistory(buf []byte, offset flatbuffers.UOffsetT) *History {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &History{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedHistoryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *History) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *History) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *History) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *History) Moves(obj *MoveRecord, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *History) MovesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func HistoryStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func HistoryAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func HistoryAddMoves(builder *flatbuffers.Builder, moves flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(moves), 0)
}
func HistoryStartMovesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func HistoryEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+12 -1
View File
@@ -97,8 +97,16 @@ func (rcv *SeatView) MutateIsWinner(n bool) bool {
return rcv._tab.MutateBoolSlot(12, n)
}
func (rcv *SeatView) DisplayName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func SeatViewStart(builder *flatbuffers.Builder) {
builder.StartObject(5)
builder.StartObject(6)
}
func SeatViewAddSeat(builder *flatbuffers.Builder, seat int32) {
builder.PrependInt32Slot(0, seat, 0)
@@ -115,6 +123,9 @@ func SeatViewAddHintsUsed(builder *flatbuffers.Builder, hintsUsed int32) {
func SeatViewAddIsWinner(builder *flatbuffers.Builder, isWinner bool) {
builder.PrependBoolSlot(4, isWinner, false)
}
func SeatViewAddDisplayName(builder *flatbuffers.Builder, displayName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(displayName), 0)
}
func SeatViewEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+75
View File
@@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package scrabblefb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type WordCheckResult struct {
_tab flatbuffers.Table
}
func GetRootAsWordCheckResult(buf []byte, offset flatbuffers.UOffsetT) *WordCheckResult {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &WordCheckResult{}
x.Init(buf, n+offset)
return x
}
func FinishWordCheckResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsWordCheckResult(buf []byte, offset flatbuffers.UOffsetT) *WordCheckResult {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &WordCheckResult{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedWordCheckResultBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *WordCheckResult) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *WordCheckResult) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *WordCheckResult) Word() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *WordCheckResult) Legal() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *WordCheckResult) MutateLegal(n bool) bool {
return rcv._tab.MutateBoolSlot(6, n)
}
func WordCheckResultStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func WordCheckResultAddWord(builder *flatbuffers.Builder, word flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(word), 0)
}
func WordCheckResultAddLegal(builder *flatbuffers.Builder, legal bool) {
builder.PrependBoolSlot(1, legal, false)
}
func WordCheckResultEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+69
View File
@@ -0,0 +1,69 @@
# scrabble-ui
Pure-HTML5 game client — **plain Svelte 5 (runes) + TypeScript + Vite**, no
SvelteKit. Talks to the `gateway` over **Connect-RPC + FlatBuffers**; embeddable in
platform webviews and packageable to native via Capacitor.
Stage 7 ships the **playable slice**: sign in (guest / email), the "my games" lobby,
auto-match, the board (place tiles by drag or tap, pass, exchange, resign), hint,
word-check + complaint, per-game chat and nudge, the live in-app stream, i18n (en/ru),
theme, and a read-only profile. Friends/blocks, friend-game invitations, profile
editing, the stats screen and the history/GCG viewer are Stage 8.
## Scripts
```sh
pnpm install
pnpm start # mock mode (VITE_MOCK): lobby -> game with no backend, :5173
pnpm dev # against a running gateway (Vite proxies /scrabble.edge.v1.Gateway -> :8081)
pnpm check # svelte-check / tsc
pnpm test:unit # Vitest (pure logic + FlatBuffers codec)
pnpm test:e2e # Playwright smoke against the mock
pnpm build # static bundle into dist/ (prod ~67 KB gzip JS)
pnpm codegen # regenerate src/gen from edge.proto + scrabble.fbs (dev-time)
```
`GATEWAY_URL` overrides the dev proxy target; `VITE_GATEWAY_URL` sets the runtime
gateway origin for a packaged (non-proxied) build.
## How it talks to the gateway
A single Connect `Execute(message_type, payload)` carries every unary op; the request
and response bodies are **FlatBuffers** tables (`pkg/fbs/scrabble.fbs`) in `payload`.
The session token rides in `Authorization: Bearer`; a domain failure comes back in
`result_code`. `Subscribe` is the live event stream. `lib/transport.ts` is the real
client; `lib/mock/` is an in-memory fake selected by `MODE === 'mock'` (and tree-shaken
out of production). Both speak the plain `lib/model.ts` types via `lib/codec.ts`.
**No board on the wire:** `StateView` is a summary + rack only, so the client
reconstructs the 15×15 board by replaying the decoded move journal (`game.history`).
Premium squares and tile values (`lib/premiums.ts`) are a client-side map **ported from
`scrabble-solver/rules/rules.go`** (pinned by a Vitest parity test). Board, tiles and
effects are pure CSS + Unicode — no image/font/SVG assets.
## Codegen
`src/gen/` is **committed**; CI builds it, it is not regenerated there (the same model
as the Go committed jet/fbs output). `pnpm codegen` runs `flatc --ts` on
`../pkg/fbs/scrabble.fbs` and `buf generate` (`protoc-gen-es`) on the edge proto. Needs
`flatc` 23.5.26 and `buf` on PATH.
## Theming
Design tokens are CSS custom properties (`src/app.css`); light/dark follows
`prefers-color-scheme` or an explicit choice in Settings. The token system is
**Telegram-themeParams-ready** (`lib/theme.ts`) — a Mini App can override the tokens at
runtime; the Telegram SDK itself is wired in the Telegram stage.
## Layout
```
src/
lib/ model, client facade, transport (+ mock), codec, board replay,
placement state machine, premiums, i18n, theme, session, router, app store
components/ Header, Modal, Toast
screens/ Login, Lobby, NewGame, Profile, Settings, About
game/ Game, Board, Rack, Controls, MakeMove, Chat
gen/ committed edge codegen (FlatBuffers + Connect)
e2e/ Playwright smoke (mock)
```
+9
View File
@@ -0,0 +1,9 @@
# Generates the TypeScript Connect client for the edge envelope service from the
# same gateway/proto/edge/v1/edge.proto the Go gateway uses. The committed output
# lives under src/gen/edge/; CI only builds it (dev-time codegen, like pkg/Makefile).
version: v2
plugins:
- local: ./node_modules/.bin/protoc-gen-es
out: src/gen
opt:
- target=ts
+8
View File
@@ -10,11 +10,19 @@
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"codegen": "rm -rf src/gen && flatc --ts -o src/gen/fbs ../pkg/fbs/scrabble.fbs && buf generate ../gateway --template buf.gen.yaml",
"test:unit": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@bufbuild/protobuf": "^2.12.0",
"@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-web": "^2.1.0",
"flatbuffers": "^25.9.23"
},
"devDependencies": {
"@bufbuild/protoc-gen-es": "^2.12.0",
"@playwright/test": "^1.49.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/node": "^22.10.0",
+94
View File
@@ -7,7 +7,23 @@ settings:
importers:
.:
dependencies:
'@bufbuild/protobuf':
specifier: ^2.12.0
version: 2.12.0
'@connectrpc/connect':
specifier: ^2.1.0
version: 2.1.1(@bufbuild/protobuf@2.12.0)
'@connectrpc/connect-web':
specifier: ^2.1.0
version: 2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0))
flatbuffers:
specifier: ^25.9.23
version: 25.9.23
devDependencies:
'@bufbuild/protoc-gen-es':
specifier: ^2.12.0
version: 2.12.0(@bufbuild/protobuf@2.12.0)
'@playwright/test':
specifier: ^1.49.0
version: 1.60.0
@@ -35,6 +51,33 @@ importers:
packages:
'@bufbuild/protobuf@2.12.0':
resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==}
'@bufbuild/protoc-gen-es@2.12.0':
resolution: {integrity: sha512-d9htF6jEkSwPbp9d/vSmZOBF7eeG18AvTMKmVg4I23afnrQOxL2w3WOXa9TaufMCyu24QakEUb4vux8apI5e7A==}
engines: {node: '>=20'}
hasBin: true
peerDependencies:
'@bufbuild/protobuf': 2.12.0
peerDependenciesMeta:
'@bufbuild/protobuf':
optional: true
'@bufbuild/protoplugin@2.12.0':
resolution: {integrity: sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow==}
'@connectrpc/connect-web@2.1.1':
resolution: {integrity: sha512-J8317Q2MaFRCT1jzVR1o06bZhDIBmU0UAzWx6xOIXzOq8+k71/+k7MUF7AwcBUX+34WIvbm5syRgC5HXQA8fOg==}
peerDependencies:
'@bufbuild/protobuf': ^2.7.0
'@connectrpc/connect': 2.1.1
'@connectrpc/connect@2.1.1':
resolution: {integrity: sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==}
peerDependencies:
'@bufbuild/protobuf': ^2.7.0
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
@@ -385,6 +428,11 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@typescript/vfs@1.6.4':
resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==}
peerDependencies:
typescript: '*'
'@vitest/expect@3.2.6':
resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
@@ -506,6 +554,9 @@ packages:
picomatch:
optional: true
flatbuffers@25.9.23:
resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -638,6 +689,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
typescript@5.4.5:
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
engines: {node: '>=14.17'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -737,6 +793,33 @@ packages:
snapshots:
'@bufbuild/protobuf@2.12.0': {}
'@bufbuild/protoc-gen-es@2.12.0(@bufbuild/protobuf@2.12.0)':
dependencies:
'@bufbuild/protoplugin': 2.12.0
optionalDependencies:
'@bufbuild/protobuf': 2.12.0
transitivePeerDependencies:
- supports-color
'@bufbuild/protoplugin@2.12.0':
dependencies:
'@bufbuild/protobuf': 2.12.0
'@typescript/vfs': 1.6.4(typescript@5.4.5)
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
'@connectrpc/connect-web@2.1.1(@bufbuild/protobuf@2.12.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0))':
dependencies:
'@bufbuild/protobuf': 2.12.0
'@connectrpc/connect': 2.1.1(@bufbuild/protobuf@2.12.0)
'@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.12.0)':
dependencies:
'@bufbuild/protobuf': 2.12.0
'@esbuild/aix-ppc64@0.25.12':
optional: true
@@ -954,6 +1037,13 @@ snapshots:
'@types/trusted-types@2.0.7': {}
'@typescript/vfs@1.6.4(typescript@5.4.5)':
dependencies:
debug: 4.4.3
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.2.6':
dependencies:
'@types/chai': 5.2.3
@@ -1079,6 +1169,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.4
flatbuffers@25.9.23: {}
fsevents@2.3.2:
optional: true
@@ -1226,6 +1318,8 @@ snapshots:
tinyspy@4.0.4: {}
typescript@5.4.5: {}
typescript@5.9.3: {}
undici-types@6.21.0: {}
+21
View File
@@ -0,0 +1,21 @@
// Bundle-size budget gate. Sums the gzipped size of the built app JS and fails if it
// exceeds the budget — a guard against an accidental heavy dependency. The real
// transport build is ~69 KB gzip today; the budget leaves headroom.
import { readdirSync, readFileSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
const BUDGET = 100 * 1024; // gzip bytes for app JS
const dir = 'dist/assets';
let total = 0;
for (const f of readdirSync(dir)) {
if (!f.endsWith('.js')) continue;
const gz = gzipSync(readFileSync(`${dir}/${f}`)).length;
total += gz;
console.log(`${f}: ${(gz / 1024).toFixed(1)} KB gzip`);
}
console.log(`total app JS: ${(total / 1024).toFixed(1)} KB gzip (budget ${BUDGET / 1024} KB)`);
if (total > BUDGET) {
console.error('bundle exceeds size budget');
process.exit(1);
}
+159
View File
@@ -0,0 +1,159 @@
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file edge/v1/edge.proto (package scrabble.edge.v1, syntax proto3)
/* eslint-disable */
// Package scrabble.edge.v1 is the client <-> gateway Connect-RPC contract. It is
// deliberately minimal (ARCHITECTURE.md §2): a single unary Execute that routes
// by message_type, and a server-streaming Subscribe for the in-app live channel.
// The actual request/response and event bodies travel as FlatBuffers bytes in the
// payload fields (pkg/fbs). The session token rides in the Authorization header,
// not the envelope (no per-request signing — ARCHITECTURE.md §3).
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file edge/v1/edge.proto.
*/
export const file_edge_v1_edge: GenFile = /*@__PURE__*/
fileDesc("ChJlZGdlL3YxL2VkZ2UucHJvdG8SEHNjcmFiYmxlLmVkZ2UudjEiSwoORXhlY3V0ZVJlcXVlc3QSFAoMbWVzc2FnZV90eXBlGAEgASgJEg8KB3BheWxvYWQYAiABKAwSEgoKcmVxdWVzdF9pZBgDIAEoCSJLCg9FeGVjdXRlUmVzcG9uc2USEgoKcmVxdWVzdF9pZBgBIAEoCRITCgtyZXN1bHRfY29kZRgCIAEoCRIPCgdwYXlsb2FkGAMgASgMIhIKEFN1YnNjcmliZVJlcXVlc3QiOAoFRXZlbnQSDAoEa2luZBgBIAEoCRIPCgdwYXlsb2FkGAIgASgMEhAKCGV2ZW50X2lkGAMgASgJMqUBCgdHYXRld2F5Ek4KB0V4ZWN1dGUSIC5zY3JhYmJsZS5lZGdlLnYxLkV4ZWN1dGVSZXF1ZXN0GiEuc2NyYWJibGUuZWRnZS52MS5FeGVjdXRlUmVzcG9uc2USSgoJU3Vic2NyaWJlEiIuc2NyYWJibGUuZWRnZS52MS5TdWJzY3JpYmVSZXF1ZXN0Ghcuc2NyYWJibGUuZWRnZS52MS5FdmVudDABQidaJXNjcmFiYmxlL2dhdGV3YXkvcHJvdG8vZWRnZS92MTtlZGdldjFiBnByb3RvMw");
/**
* ExecuteRequest is the unary envelope. message_type selects the operation;
* payload is its FlatBuffers-encoded request body; request_id is an optional
* client correlation id echoed back.
*
* @generated from message scrabble.edge.v1.ExecuteRequest
*/
export type ExecuteRequest = Message<"scrabble.edge.v1.ExecuteRequest"> & {
/**
* @generated from field: string message_type = 1;
*/
messageType: string;
/**
* @generated from field: bytes payload = 2;
*/
payload: Uint8Array;
/**
* @generated from field: string request_id = 3;
*/
requestId: string;
};
/**
* Describes the message scrabble.edge.v1.ExecuteRequest.
* Use `create(ExecuteRequestSchema)` to create a new message.
*/
export const ExecuteRequestSchema: GenMessage<ExecuteRequest> = /*@__PURE__*/
messageDesc(file_edge_v1_edge, 0);
/**
* ExecuteResponse is the unary reply. result_code is "ok" on success or a stable
* error code; payload is the FlatBuffers-encoded response body (empty on error).
*
* @generated from message scrabble.edge.v1.ExecuteResponse
*/
export type ExecuteResponse = Message<"scrabble.edge.v1.ExecuteResponse"> & {
/**
* @generated from field: string request_id = 1;
*/
requestId: string;
/**
* @generated from field: string result_code = 2;
*/
resultCode: string;
/**
* @generated from field: bytes payload = 3;
*/
payload: Uint8Array;
};
/**
* Describes the message scrabble.edge.v1.ExecuteResponse.
* Use `create(ExecuteResponseSchema)` to create a new message.
*/
export const ExecuteResponseSchema: GenMessage<ExecuteResponse> = /*@__PURE__*/
messageDesc(file_edge_v1_edge, 1);
/**
* SubscribeRequest opens the live stream. It is empty: the session is taken from
* the Authorization header.
*
* @generated from message scrabble.edge.v1.SubscribeRequest
*/
export type SubscribeRequest = Message<"scrabble.edge.v1.SubscribeRequest"> & {
};
/**
* Describes the message scrabble.edge.v1.SubscribeRequest.
* Use `create(SubscribeRequestSchema)` to create a new message.
*/
export const SubscribeRequestSchema: GenMessage<SubscribeRequest> = /*@__PURE__*/
messageDesc(file_edge_v1_edge, 2);
/**
* Event is one live event. kind is the notification catalog kind; payload is its
* FlatBuffers-encoded body; event_id is a correlation id.
*
* @generated from message scrabble.edge.v1.Event
*/
export type Event = Message<"scrabble.edge.v1.Event"> & {
/**
* @generated from field: string kind = 1;
*/
kind: string;
/**
* @generated from field: bytes payload = 2;
*/
payload: Uint8Array;
/**
* @generated from field: string event_id = 3;
*/
eventId: string;
};
/**
* Describes the message scrabble.edge.v1.Event.
* Use `create(EventSchema)` to create a new message.
*/
export const EventSchema: GenMessage<Event> = /*@__PURE__*/
messageDesc(file_edge_v1_edge, 3);
/**
* Gateway is the public edge service.
*
* @generated from service scrabble.edge.v1.Gateway
*/
export const Gateway: GenService<{
/**
* Execute runs one unary operation identified by message_type. Auth operations
* (auth.*) are unauthenticated and return a minted session; all others require
* a valid session token in the Authorization header.
*
* @generated from rpc scrabble.edge.v1.Gateway.Execute
*/
execute: {
methodKind: "unary";
input: typeof ExecuteRequestSchema;
output: typeof ExecuteResponseSchema;
},
/**
* Subscribe opens the in-app live-event stream for the authenticated session.
*
* @generated from rpc scrabble.edge.v1.Gateway.Subscribe
*/
subscribe: {
methodKind: "server_streaming";
input: typeof SubscribeRequestSchema;
output: typeof EventSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_edge_v1_edge, 0);
+3
View File
@@ -0,0 +1,3 @@
// automatically generated by the FlatBuffers compiler, do not modify
export * as scrabblefb from './scrabblefb.js';
+36
View File
@@ -0,0 +1,36 @@
// automatically generated by the FlatBuffers compiler, do not modify
export { Ack } from './scrabblefb/ack.js';
export { ChatList } from './scrabblefb/chat-list.js';
export { ChatMessage } from './scrabblefb/chat-message.js';
export { ChatPostRequest } from './scrabblefb/chat-post-request.js';
export { CheckWordRequest } from './scrabblefb/check-word-request.js';
export { ComplaintRequest } from './scrabblefb/complaint-request.js';
export { EmailLoginRequest } from './scrabblefb/email-login-request.js';
export { EmailRequestRequest } from './scrabblefb/email-request-request.js';
export { EnqueueRequest } from './scrabblefb/enqueue-request.js';
export { EvalRequest } from './scrabblefb/eval-request.js';
export { EvalResult } from './scrabblefb/eval-result.js';
export { ExchangeRequest } from './scrabblefb/exchange-request.js';
export { GameActionRequest } from './scrabblefb/game-action-request.js';
export { GameList } from './scrabblefb/game-list.js';
export { GameView } from './scrabblefb/game-view.js';
export { GuestLoginRequest } from './scrabblefb/guest-login-request.js';
export { HintResult } from './scrabblefb/hint-result.js';
export { History } from './scrabblefb/history.js';
export { MatchFoundEvent } from './scrabblefb/match-found-event.js';
export { MatchResult } from './scrabblefb/match-result.js';
export { MoveRecord } from './scrabblefb/move-record.js';
export { MoveResult } from './scrabblefb/move-result.js';
export { NudgeEvent } from './scrabblefb/nudge-event.js';
export { OpponentMovedEvent } from './scrabblefb/opponent-moved-event.js';
export { Profile } from './scrabblefb/profile.js';
export { SeatView } from './scrabblefb/seat-view.js';
export { Session } from './scrabblefb/session.js';
export { StateRequest } from './scrabblefb/state-request.js';
export { StateView } from './scrabblefb/state-view.js';
export { SubmitPlayRequest } from './scrabblefb/submit-play-request.js';
export { TelegramLoginRequest } from './scrabblefb/telegram-login-request.js';
export { TileRecord } from './scrabblefb/tile-record.js';
export { WordCheckResult } from './scrabblefb/word-check-result.js';
export { YourTurnEvent } from './scrabblefb/your-turn-event.js';
+46
View File
@@ -0,0 +1,46 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class Ack {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):Ack {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsAck(bb:flatbuffers.ByteBuffer, obj?:Ack):Ack {
return (obj || new Ack()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsAck(bb:flatbuffers.ByteBuffer, obj?:Ack):Ack {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new Ack()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
ok():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
static startAck(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addOk(builder:flatbuffers.Builder, ok:boolean) {
builder.addFieldInt8(0, +ok, +false);
}
static endAck(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createAck(builder:flatbuffers.Builder, ok:boolean):flatbuffers.Offset {
Ack.startAck(builder);
Ack.addOk(builder, ok);
return Ack.endAck(builder);
}
}
+66
View File
@@ -0,0 +1,66 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { ChatMessage } from '../scrabblefb/chat-message.js';
export class ChatList {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ChatList {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsChatList(bb:flatbuffers.ByteBuffer, obj?:ChatList):ChatList {
return (obj || new ChatList()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsChatList(bb:flatbuffers.ByteBuffer, obj?:ChatList):ChatList {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ChatList()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
messages(index: number, obj?:ChatMessage):ChatMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new ChatMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
messagesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startChatList(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addMessages(builder:flatbuffers.Builder, messagesOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messagesOffset, 0);
}
static createMessagesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startMessagesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endChatList(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createChatList(builder:flatbuffers.Builder, messagesOffset:flatbuffers.Offset):flatbuffers.Offset {
ChatList.startChatList(builder);
ChatList.addMessages(builder, messagesOffset);
return ChatList.endChatList(builder);
}
}
+106
View File
@@ -0,0 +1,106 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class ChatMessage {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ChatMessage {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsChatMessage(bb:flatbuffers.ByteBuffer, obj?:ChatMessage):ChatMessage {
return (obj || new ChatMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsChatMessage(bb:flatbuffers.ByteBuffer, obj?:ChatMessage):ChatMessage {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ChatMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderId():string|null
senderId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
kind():string|null
kind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
kind(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
createdAtUnix():bigint {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startChatMessage(builder:flatbuffers.Builder) {
builder.startObject(6);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, gameIdOffset, 0);
}
static addSenderId(builder:flatbuffers.Builder, senderIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, senderIdOffset, 0);
}
static addKind(builder:flatbuffers.Builder, kindOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, kindOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, bodyOffset, 0);
}
static addCreatedAtUnix(builder:flatbuffers.Builder, createdAtUnix:bigint) {
builder.addFieldInt64(5, createdAtUnix, BigInt('0'));
}
static endChatMessage(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createChatMessage(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, senderIdOffset:flatbuffers.Offset, kindOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset, createdAtUnix:bigint):flatbuffers.Offset {
ChatMessage.startChatMessage(builder);
ChatMessage.addId(builder, idOffset);
ChatMessage.addGameId(builder, gameIdOffset);
ChatMessage.addSenderId(builder, senderIdOffset);
ChatMessage.addKind(builder, kindOffset);
ChatMessage.addBody(builder, bodyOffset);
ChatMessage.addCreatedAtUnix(builder, createdAtUnix);
return ChatMessage.endChatMessage(builder);
}
}
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class ChatPostRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ChatPostRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsChatPostRequest(bb:flatbuffers.ByteBuffer, obj?:ChatPostRequest):ChatPostRequest {
return (obj || new ChatPostRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsChatPostRequest(bb:flatbuffers.ByteBuffer, obj?:ChatPostRequest):ChatPostRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ChatPostRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startChatPostRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, bodyOffset, 0);
}
static endChatPostRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createChatPostRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
ChatPostRequest.startChatPostRequest(builder);
ChatPostRequest.addGameId(builder, gameIdOffset);
ChatPostRequest.addBody(builder, bodyOffset);
return ChatPostRequest.endChatPostRequest(builder);
}
}
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class CheckWordRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CheckWordRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCheckWordRequest(bb:flatbuffers.ByteBuffer, obj?:CheckWordRequest):CheckWordRequest {
return (obj || new CheckWordRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCheckWordRequest(bb:flatbuffers.ByteBuffer, obj?:CheckWordRequest):CheckWordRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CheckWordRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
word():string|null
word(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
word(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCheckWordRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addWord(builder:flatbuffers.Builder, wordOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, wordOffset, 0);
}
static endCheckWordRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCheckWordRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, wordOffset:flatbuffers.Offset):flatbuffers.Offset {
CheckWordRequest.startCheckWordRequest(builder);
CheckWordRequest.addGameId(builder, gameIdOffset);
CheckWordRequest.addWord(builder, wordOffset);
return CheckWordRequest.endCheckWordRequest(builder);
}
}
@@ -0,0 +1,72 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class ComplaintRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ComplaintRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsComplaintRequest(bb:flatbuffers.ByteBuffer, obj?:ComplaintRequest):ComplaintRequest {
return (obj || new ComplaintRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsComplaintRequest(bb:flatbuffers.ByteBuffer, obj?:ComplaintRequest):ComplaintRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ComplaintRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
word():string|null
word(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
word(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
note():string|null
note(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
note(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startComplaintRequest(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addWord(builder:flatbuffers.Builder, wordOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, wordOffset, 0);
}
static addNote(builder:flatbuffers.Builder, noteOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, noteOffset, 0);
}
static endComplaintRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createComplaintRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, wordOffset:flatbuffers.Offset, noteOffset:flatbuffers.Offset):flatbuffers.Offset {
ComplaintRequest.startComplaintRequest(builder);
ComplaintRequest.addGameId(builder, gameIdOffset);
ComplaintRequest.addWord(builder, wordOffset);
ComplaintRequest.addNote(builder, noteOffset);
return ComplaintRequest.endComplaintRequest(builder);
}
}
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class EmailLoginRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EmailLoginRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEmailLoginRequest(bb:flatbuffers.ByteBuffer, obj?:EmailLoginRequest):EmailLoginRequest {
return (obj || new EmailLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEmailLoginRequest(bb:flatbuffers.ByteBuffer, obj?:EmailLoginRequest):EmailLoginRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EmailLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
email():string|null
email(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
email(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
code():string|null
code(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
code(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startEmailLoginRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addEmail(builder:flatbuffers.Builder, emailOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, emailOffset, 0);
}
static addCode(builder:flatbuffers.Builder, codeOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, codeOffset, 0);
}
static endEmailLoginRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEmailLoginRequest(builder:flatbuffers.Builder, emailOffset:flatbuffers.Offset, codeOffset:flatbuffers.Offset):flatbuffers.Offset {
EmailLoginRequest.startEmailLoginRequest(builder);
EmailLoginRequest.addEmail(builder, emailOffset);
EmailLoginRequest.addCode(builder, codeOffset);
return EmailLoginRequest.endEmailLoginRequest(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class EmailRequestRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EmailRequestRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEmailRequestRequest(bb:flatbuffers.ByteBuffer, obj?:EmailRequestRequest):EmailRequestRequest {
return (obj || new EmailRequestRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEmailRequestRequest(bb:flatbuffers.ByteBuffer, obj?:EmailRequestRequest):EmailRequestRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EmailRequestRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
email():string|null
email(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
email(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startEmailRequestRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addEmail(builder:flatbuffers.Builder, emailOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, emailOffset, 0);
}
static endEmailRequestRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEmailRequestRequest(builder:flatbuffers.Builder, emailOffset:flatbuffers.Offset):flatbuffers.Offset {
EmailRequestRequest.startEmailRequestRequest(builder);
EmailRequestRequest.addEmail(builder, emailOffset);
return EmailRequestRequest.endEmailRequestRequest(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class EnqueueRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EnqueueRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEnqueueRequest(bb:flatbuffers.ByteBuffer, obj?:EnqueueRequest):EnqueueRequest {
return (obj || new EnqueueRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEnqueueRequest(bb:flatbuffers.ByteBuffer, obj?:EnqueueRequest):EnqueueRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EnqueueRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
variant():string|null
variant(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
variant(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startEnqueueRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addVariant(builder:flatbuffers.Builder, variantOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, variantOffset, 0);
}
static endEnqueueRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEnqueueRequest(builder:flatbuffers.Builder, variantOffset:flatbuffers.Offset):flatbuffers.Offset {
EnqueueRequest.startEnqueueRequest(builder);
EnqueueRequest.addVariant(builder, variantOffset);
return EnqueueRequest.endEnqueueRequest(builder);
}
}
+90
View File
@@ -0,0 +1,90 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { TileRecord } from '../scrabblefb/tile-record.js';
export class EvalRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EvalRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEvalRequest(bb:flatbuffers.ByteBuffer, obj?:EvalRequest):EvalRequest {
return (obj || new EvalRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEvalRequest(bb:flatbuffers.ByteBuffer, obj?:EvalRequest):EvalRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EvalRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
dir():string|null
dir(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
dir(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
tiles(index: number, obj?:TileRecord):TileRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? (obj || new TileRecord()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
tilesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startEvalRequest(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addDir(builder:flatbuffers.Builder, dirOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dirOffset, 0);
}
static addTiles(builder:flatbuffers.Builder, tilesOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, tilesOffset, 0);
}
static createTilesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startTilesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endEvalRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEvalRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, dirOffset:flatbuffers.Offset, tilesOffset:flatbuffers.Offset):flatbuffers.Offset {
EvalRequest.startEvalRequest(builder);
EvalRequest.addGameId(builder, gameIdOffset);
EvalRequest.addDir(builder, dirOffset);
EvalRequest.addTiles(builder, tilesOffset);
return EvalRequest.endEvalRequest(builder);
}
}
+85
View File
@@ -0,0 +1,85 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class EvalResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):EvalResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsEvalResult(bb:flatbuffers.ByteBuffer, obj?:EvalResult):EvalResult {
return (obj || new EvalResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsEvalResult(bb:flatbuffers.ByteBuffer, obj?:EvalResult):EvalResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new EvalResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
legal():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
score():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
words(index: number):string
words(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array
words(index: number,optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb!.__vector(this.bb_pos + offset) + index * 4, optionalEncoding) : null;
}
wordsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startEvalResult(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addLegal(builder:flatbuffers.Builder, legal:boolean) {
builder.addFieldInt8(0, +legal, +false);
}
static addScore(builder:flatbuffers.Builder, score:number) {
builder.addFieldInt32(1, score, 0);
}
static addWords(builder:flatbuffers.Builder, wordsOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, wordsOffset, 0);
}
static createWordsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startWordsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endEvalResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createEvalResult(builder:flatbuffers.Builder, legal:boolean, score:number, wordsOffset:flatbuffers.Offset):flatbuffers.Offset {
EvalResult.startEvalResult(builder);
EvalResult.addLegal(builder, legal);
EvalResult.addScore(builder, score);
EvalResult.addWords(builder, wordsOffset);
return EvalResult.endEvalResult(builder);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class ExchangeRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ExchangeRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsExchangeRequest(bb:flatbuffers.ByteBuffer, obj?:ExchangeRequest):ExchangeRequest {
return (obj || new ExchangeRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsExchangeRequest(bb:flatbuffers.ByteBuffer, obj?:ExchangeRequest):ExchangeRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ExchangeRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
tiles(index: number):string
tiles(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array
tiles(index: number,optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb!.__vector(this.bb_pos + offset) + index * 4, optionalEncoding) : null;
}
tilesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startExchangeRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addTiles(builder:flatbuffers.Builder, tilesOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, tilesOffset, 0);
}
static createTilesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startTilesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endExchangeRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createExchangeRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, tilesOffset:flatbuffers.Offset):flatbuffers.Offset {
ExchangeRequest.startExchangeRequest(builder);
ExchangeRequest.addGameId(builder, gameIdOffset);
ExchangeRequest.addTiles(builder, tilesOffset);
return ExchangeRequest.endExchangeRequest(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class GameActionRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GameActionRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGameActionRequest(bb:flatbuffers.ByteBuffer, obj?:GameActionRequest):GameActionRequest {
return (obj || new GameActionRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGameActionRequest(bb:flatbuffers.ByteBuffer, obj?:GameActionRequest):GameActionRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GameActionRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startGameActionRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static endGameActionRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGameActionRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
GameActionRequest.startGameActionRequest(builder);
GameActionRequest.addGameId(builder, gameIdOffset);
return GameActionRequest.endGameActionRequest(builder);
}
}
+66
View File
@@ -0,0 +1,66 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { GameView } from '../scrabblefb/game-view.js';
export class GameList {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GameList {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGameList(bb:flatbuffers.ByteBuffer, obj?:GameList):GameList {
return (obj || new GameList()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGameList(bb:flatbuffers.ByteBuffer, obj?:GameList):GameList {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GameList()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
games(index: number, obj?:GameView):GameView|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new GameView()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
gamesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startGameList(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGames(builder:flatbuffers.Builder, gamesOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gamesOffset, 0);
}
static createGamesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startGamesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endGameList(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGameList(builder:flatbuffers.Builder, gamesOffset:flatbuffers.Offset):flatbuffers.Offset {
GameList.startGameList(builder);
GameList.addGames(builder, gamesOffset);
return GameList.endGameList(builder);
}
}
+166
View File
@@ -0,0 +1,166 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { SeatView } from '../scrabblefb/seat-view.js';
export class GameView {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GameView {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGameView(bb:flatbuffers.ByteBuffer, obj?:GameView):GameView {
return (obj || new GameView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGameView(bb:flatbuffers.ByteBuffer, obj?:GameView):GameView {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GameView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
variant():string|null
variant(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
variant(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
dictVersion():string|null
dictVersion(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
dictVersion(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
status():string|null
status(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
status(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
players():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
toMove():number {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
turnTimeoutSecs():number {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
moveCount():number {
const offset = this.bb!.__offset(this.bb_pos, 18);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
endReason():string|null
endReason(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
endReason(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 20);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
seats(index: number, obj?:SeatView):SeatView|null {
const offset = this.bb!.__offset(this.bb_pos, 22);
return offset ? (obj || new SeatView()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
seatsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 22);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startGameView(builder:flatbuffers.Builder) {
builder.startObject(10);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addVariant(builder:flatbuffers.Builder, variantOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, variantOffset, 0);
}
static addDictVersion(builder:flatbuffers.Builder, dictVersionOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, dictVersionOffset, 0);
}
static addStatus(builder:flatbuffers.Builder, statusOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, statusOffset, 0);
}
static addPlayers(builder:flatbuffers.Builder, players:number) {
builder.addFieldInt32(4, players, 0);
}
static addToMove(builder:flatbuffers.Builder, toMove:number) {
builder.addFieldInt32(5, toMove, 0);
}
static addTurnTimeoutSecs(builder:flatbuffers.Builder, turnTimeoutSecs:number) {
builder.addFieldInt32(6, turnTimeoutSecs, 0);
}
static addMoveCount(builder:flatbuffers.Builder, moveCount:number) {
builder.addFieldInt32(7, moveCount, 0);
}
static addEndReason(builder:flatbuffers.Builder, endReasonOffset:flatbuffers.Offset) {
builder.addFieldOffset(8, endReasonOffset, 0);
}
static addSeats(builder:flatbuffers.Builder, seatsOffset:flatbuffers.Offset) {
builder.addFieldOffset(9, seatsOffset, 0);
}
static createSeatsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startSeatsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endGameView(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGameView(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, variantOffset:flatbuffers.Offset, dictVersionOffset:flatbuffers.Offset, statusOffset:flatbuffers.Offset, players:number, toMove:number, turnTimeoutSecs:number, moveCount:number, endReasonOffset:flatbuffers.Offset, seatsOffset:flatbuffers.Offset):flatbuffers.Offset {
GameView.startGameView(builder);
GameView.addId(builder, idOffset);
GameView.addVariant(builder, variantOffset);
GameView.addDictVersion(builder, dictVersionOffset);
GameView.addStatus(builder, statusOffset);
GameView.addPlayers(builder, players);
GameView.addToMove(builder, toMove);
GameView.addTurnTimeoutSecs(builder, turnTimeoutSecs);
GameView.addMoveCount(builder, moveCount);
GameView.addEndReason(builder, endReasonOffset);
GameView.addSeats(builder, seatsOffset);
return GameView.endGameView(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class GuestLoginRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GuestLoginRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGuestLoginRequest(bb:flatbuffers.ByteBuffer, obj?:GuestLoginRequest):GuestLoginRequest {
return (obj || new GuestLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGuestLoginRequest(bb:flatbuffers.ByteBuffer, obj?:GuestLoginRequest):GuestLoginRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GuestLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
locale():string|null
locale(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
locale(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startGuestLoginRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addLocale(builder:flatbuffers.Builder, localeOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, localeOffset, 0);
}
static endGuestLoginRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createGuestLoginRequest(builder:flatbuffers.Builder, localeOffset:flatbuffers.Offset):flatbuffers.Offset {
GuestLoginRequest.startGuestLoginRequest(builder);
GuestLoginRequest.addLocale(builder, localeOffset);
return GuestLoginRequest.endGuestLoginRequest(builder);
}
}
+59
View File
@@ -0,0 +1,59 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { MoveRecord } from '../scrabblefb/move-record.js';
export class HintResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):HintResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsHintResult(bb:flatbuffers.ByteBuffer, obj?:HintResult):HintResult {
return (obj || new HintResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsHintResult(bb:flatbuffers.ByteBuffer, obj?:HintResult):HintResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new HintResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
move(obj?:MoveRecord):MoveRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MoveRecord()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
hintsRemaining():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
static startHintResult(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addMove(builder:flatbuffers.Builder, moveOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, moveOffset, 0);
}
static addHintsRemaining(builder:flatbuffers.Builder, hintsRemaining:number) {
builder.addFieldInt32(1, hintsRemaining, 0);
}
static endHintResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createHintResult(builder:flatbuffers.Builder, moveOffset:flatbuffers.Offset, hintsRemaining:number):flatbuffers.Offset {
HintResult.startHintResult(builder);
HintResult.addMove(builder, moveOffset);
HintResult.addHintsRemaining(builder, hintsRemaining);
return HintResult.endHintResult(builder);
}
}
+78
View File
@@ -0,0 +1,78 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { MoveRecord } from '../scrabblefb/move-record.js';
export class History {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):History {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsHistory(bb:flatbuffers.ByteBuffer, obj?:History):History {
return (obj || new History()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsHistory(bb:flatbuffers.ByteBuffer, obj?:History):History {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new History()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
moves(index: number, obj?:MoveRecord):MoveRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new MoveRecord()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
movesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startHistory(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addMoves(builder:flatbuffers.Builder, movesOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, movesOffset, 0);
}
static createMovesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startMovesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endHistory(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createHistory(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, movesOffset:flatbuffers.Offset):flatbuffers.Offset {
History.startHistory(builder);
History.addGameId(builder, gameIdOffset);
History.addMoves(builder, movesOffset);
return History.endHistory(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class MatchFoundEvent {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MatchFoundEvent {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMatchFoundEvent(bb:flatbuffers.ByteBuffer, obj?:MatchFoundEvent):MatchFoundEvent {
return (obj || new MatchFoundEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMatchFoundEvent(bb:flatbuffers.ByteBuffer, obj?:MatchFoundEvent):MatchFoundEvent {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MatchFoundEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startMatchFoundEvent(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static endMatchFoundEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMatchFoundEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
MatchFoundEvent.startMatchFoundEvent(builder);
MatchFoundEvent.addGameId(builder, gameIdOffset);
return MatchFoundEvent.endMatchFoundEvent(builder);
}
}
+53
View File
@@ -0,0 +1,53 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { GameView } from '../scrabblefb/game-view.js';
export class MatchResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MatchResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMatchResult(bb:flatbuffers.ByteBuffer, obj?:MatchResult):MatchResult {
return (obj || new MatchResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMatchResult(bb:flatbuffers.ByteBuffer, obj?:MatchResult):MatchResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MatchResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
matched():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
game(obj?:GameView):GameView|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new GameView()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startMatchResult(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addMatched(builder:flatbuffers.Builder, matched:boolean) {
builder.addFieldInt8(0, +matched, +false);
}
static addGame(builder:flatbuffers.Builder, gameOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, gameOffset, 0);
}
static endMatchResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
}
+179
View File
@@ -0,0 +1,179 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { TileRecord } from '../scrabblefb/tile-record.js';
export class MoveRecord {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MoveRecord {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMoveRecord(bb:flatbuffers.ByteBuffer, obj?:MoveRecord):MoveRecord {
return (obj || new MoveRecord()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMoveRecord(bb:flatbuffers.ByteBuffer, obj?:MoveRecord):MoveRecord {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MoveRecord()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
player():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
action():string|null
action(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
action(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
dir():string|null
dir(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
dir(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
mainRow():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
mainCol():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
tiles(index: number, obj?:TileRecord):TileRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? (obj || new TileRecord()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
tilesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
words(index: number):string
words(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array
words(index: number,optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.__string(this.bb!.__vector(this.bb_pos + offset) + index * 4, optionalEncoding) : null;
}
wordsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
count():number {
const offset = this.bb!.__offset(this.bb_pos, 18);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
score():number {
const offset = this.bb!.__offset(this.bb_pos, 20);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
total():number {
const offset = this.bb!.__offset(this.bb_pos, 22);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
static startMoveRecord(builder:flatbuffers.Builder) {
builder.startObject(10);
}
static addPlayer(builder:flatbuffers.Builder, player:number) {
builder.addFieldInt32(0, player, 0);
}
static addAction(builder:flatbuffers.Builder, actionOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, actionOffset, 0);
}
static addDir(builder:flatbuffers.Builder, dirOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, dirOffset, 0);
}
static addMainRow(builder:flatbuffers.Builder, mainRow:number) {
builder.addFieldInt32(3, mainRow, 0);
}
static addMainCol(builder:flatbuffers.Builder, mainCol:number) {
builder.addFieldInt32(4, mainCol, 0);
}
static addTiles(builder:flatbuffers.Builder, tilesOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, tilesOffset, 0);
}
static createTilesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startTilesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addWords(builder:flatbuffers.Builder, wordsOffset:flatbuffers.Offset) {
builder.addFieldOffset(6, wordsOffset, 0);
}
static createWordsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startWordsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addCount(builder:flatbuffers.Builder, count:number) {
builder.addFieldInt32(7, count, 0);
}
static addScore(builder:flatbuffers.Builder, score:number) {
builder.addFieldInt32(8, score, 0);
}
static addTotal(builder:flatbuffers.Builder, total:number) {
builder.addFieldInt32(9, total, 0);
}
static endMoveRecord(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMoveRecord(builder:flatbuffers.Builder, player:number, actionOffset:flatbuffers.Offset, dirOffset:flatbuffers.Offset, mainRow:number, mainCol:number, tilesOffset:flatbuffers.Offset, wordsOffset:flatbuffers.Offset, count:number, score:number, total:number):flatbuffers.Offset {
MoveRecord.startMoveRecord(builder);
MoveRecord.addPlayer(builder, player);
MoveRecord.addAction(builder, actionOffset);
MoveRecord.addDir(builder, dirOffset);
MoveRecord.addMainRow(builder, mainRow);
MoveRecord.addMainCol(builder, mainCol);
MoveRecord.addTiles(builder, tilesOffset);
MoveRecord.addWords(builder, wordsOffset);
MoveRecord.addCount(builder, count);
MoveRecord.addScore(builder, score);
MoveRecord.addTotal(builder, total);
return MoveRecord.endMoveRecord(builder);
}
}
+54
View File
@@ -0,0 +1,54 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { GameView } from '../scrabblefb/game-view.js';
import { MoveRecord } from '../scrabblefb/move-record.js';
export class MoveResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MoveResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMoveResult(bb:flatbuffers.ByteBuffer, obj?:MoveResult):MoveResult {
return (obj || new MoveResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMoveResult(bb:flatbuffers.ByteBuffer, obj?:MoveResult):MoveResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MoveResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
move(obj?:MoveRecord):MoveRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MoveRecord()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
game(obj?:GameView):GameView|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new GameView()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startMoveResult(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addMove(builder:flatbuffers.Builder, moveOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, moveOffset, 0);
}
static addGame(builder:flatbuffers.Builder, gameOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, gameOffset, 0);
}
static endMoveResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
}
+60
View File
@@ -0,0 +1,60 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class NudgeEvent {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):NudgeEvent {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsNudgeEvent(bb:flatbuffers.ByteBuffer, obj?:NudgeEvent):NudgeEvent {
return (obj || new NudgeEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsNudgeEvent(bb:flatbuffers.ByteBuffer, obj?:NudgeEvent):NudgeEvent {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new NudgeEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
fromUserId():string|null
fromUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
fromUserId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startNudgeEvent(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addFromUserId(builder:flatbuffers.Builder, fromUserIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, fromUserIdOffset, 0);
}
static endNudgeEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createNudgeEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, fromUserIdOffset:flatbuffers.Offset):flatbuffers.Offset {
NudgeEvent.startNudgeEvent(builder);
NudgeEvent.addGameId(builder, gameIdOffset);
NudgeEvent.addFromUserId(builder, fromUserIdOffset);
return NudgeEvent.endNudgeEvent(builder);
}
}
@@ -0,0 +1,90 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class OpponentMovedEvent {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):OpponentMovedEvent {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsOpponentMovedEvent(bb:flatbuffers.ByteBuffer, obj?:OpponentMovedEvent):OpponentMovedEvent {
return (obj || new OpponentMovedEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsOpponentMovedEvent(bb:flatbuffers.ByteBuffer, obj?:OpponentMovedEvent):OpponentMovedEvent {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new OpponentMovedEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
seat():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
action():string|null
action(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
action(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
score():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
total():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
static startOpponentMovedEvent(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addSeat(builder:flatbuffers.Builder, seat:number) {
builder.addFieldInt32(1, seat, 0);
}
static addAction(builder:flatbuffers.Builder, actionOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, actionOffset, 0);
}
static addScore(builder:flatbuffers.Builder, score:number) {
builder.addFieldInt32(3, score, 0);
}
static addTotal(builder:flatbuffers.Builder, total:number) {
builder.addFieldInt32(4, total, 0);
}
static endOpponentMovedEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createOpponentMovedEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, seat:number, actionOffset:flatbuffers.Offset, score:number, total:number):flatbuffers.Offset {
OpponentMovedEvent.startOpponentMovedEvent(builder);
OpponentMovedEvent.addGameId(builder, gameIdOffset);
OpponentMovedEvent.addSeat(builder, seat);
OpponentMovedEvent.addAction(builder, actionOffset);
OpponentMovedEvent.addScore(builder, score);
OpponentMovedEvent.addTotal(builder, total);
return OpponentMovedEvent.endOpponentMovedEvent(builder);
}
}
+124
View File
@@ -0,0 +1,124 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class Profile {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):Profile {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsProfile(bb:flatbuffers.ByteBuffer, obj?:Profile):Profile {
return (obj || new Profile()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsProfile(bb:flatbuffers.ByteBuffer, obj?:Profile):Profile {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new Profile()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
userId():string|null
userId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
userId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
displayName():string|null
displayName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
displayName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
preferredLanguage():string|null
preferredLanguage(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
preferredLanguage(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
timeZone():string|null
timeZone(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
timeZone(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
hintBalance():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
blockChat():boolean {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
blockFriendRequests():boolean {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
isGuest():boolean {
const offset = this.bb!.__offset(this.bb_pos, 18);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
static startProfile(builder:flatbuffers.Builder) {
builder.startObject(8);
}
static addUserId(builder:flatbuffers.Builder, userIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, userIdOffset, 0);
}
static addDisplayName(builder:flatbuffers.Builder, displayNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, displayNameOffset, 0);
}
static addPreferredLanguage(builder:flatbuffers.Builder, preferredLanguageOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, preferredLanguageOffset, 0);
}
static addTimeZone(builder:flatbuffers.Builder, timeZoneOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, timeZoneOffset, 0);
}
static addHintBalance(builder:flatbuffers.Builder, hintBalance:number) {
builder.addFieldInt32(4, hintBalance, 0);
}
static addBlockChat(builder:flatbuffers.Builder, blockChat:boolean) {
builder.addFieldInt8(5, +blockChat, +false);
}
static addBlockFriendRequests(builder:flatbuffers.Builder, blockFriendRequests:boolean) {
builder.addFieldInt8(6, +blockFriendRequests, +false);
}
static addIsGuest(builder:flatbuffers.Builder, isGuest:boolean) {
builder.addFieldInt8(7, +isGuest, +false);
}
static endProfile(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createProfile(builder:flatbuffers.Builder, userIdOffset:flatbuffers.Offset, displayNameOffset:flatbuffers.Offset, preferredLanguageOffset:flatbuffers.Offset, timeZoneOffset:flatbuffers.Offset, hintBalance:number, blockChat:boolean, blockFriendRequests:boolean, isGuest:boolean):flatbuffers.Offset {
Profile.startProfile(builder);
Profile.addUserId(builder, userIdOffset);
Profile.addDisplayName(builder, displayNameOffset);
Profile.addPreferredLanguage(builder, preferredLanguageOffset);
Profile.addTimeZone(builder, timeZoneOffset);
Profile.addHintBalance(builder, hintBalance);
Profile.addBlockChat(builder, blockChat);
Profile.addBlockFriendRequests(builder, blockFriendRequests);
Profile.addIsGuest(builder, isGuest);
return Profile.endProfile(builder);
}
}
+100
View File
@@ -0,0 +1,100 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class SeatView {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SeatView {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSeatView(bb:flatbuffers.ByteBuffer, obj?:SeatView):SeatView {
return (obj || new SeatView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSeatView(bb:flatbuffers.ByteBuffer, obj?:SeatView):SeatView {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SeatView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
seat():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
accountId():string|null
accountId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
accountId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
score():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
hintsUsed():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
isWinner():boolean {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
displayName():string|null
displayName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
displayName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startSeatView(builder:flatbuffers.Builder) {
builder.startObject(6);
}
static addSeat(builder:flatbuffers.Builder, seat:number) {
builder.addFieldInt32(0, seat, 0);
}
static addAccountId(builder:flatbuffers.Builder, accountIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, accountIdOffset, 0);
}
static addScore(builder:flatbuffers.Builder, score:number) {
builder.addFieldInt32(2, score, 0);
}
static addHintsUsed(builder:flatbuffers.Builder, hintsUsed:number) {
builder.addFieldInt32(3, hintsUsed, 0);
}
static addIsWinner(builder:flatbuffers.Builder, isWinner:boolean) {
builder.addFieldInt8(4, +isWinner, +false);
}
static addDisplayName(builder:flatbuffers.Builder, displayNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, displayNameOffset, 0);
}
static endSeatView(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createSeatView(builder:flatbuffers.Builder, seat:number, accountIdOffset:flatbuffers.Offset, score:number, hintsUsed:number, isWinner:boolean, displayNameOffset:flatbuffers.Offset):flatbuffers.Offset {
SeatView.startSeatView(builder);
SeatView.addSeat(builder, seat);
SeatView.addAccountId(builder, accountIdOffset);
SeatView.addScore(builder, score);
SeatView.addHintsUsed(builder, hintsUsed);
SeatView.addIsWinner(builder, isWinner);
SeatView.addDisplayName(builder, displayNameOffset);
return SeatView.endSeatView(builder);
}
}
+82
View File
@@ -0,0 +1,82 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class Session {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):Session {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSession(bb:flatbuffers.ByteBuffer, obj?:Session):Session {
return (obj || new Session()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSession(bb:flatbuffers.ByteBuffer, obj?:Session):Session {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new Session()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
token():string|null
token(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
token(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
userId():string|null
userId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
userId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
isGuest():boolean {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
displayName():string|null
displayName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
displayName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startSession(builder:flatbuffers.Builder) {
builder.startObject(4);
}
static addToken(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, tokenOffset, 0);
}
static addUserId(builder:flatbuffers.Builder, userIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, userIdOffset, 0);
}
static addIsGuest(builder:flatbuffers.Builder, isGuest:boolean) {
builder.addFieldInt8(2, +isGuest, +false);
}
static addDisplayName(builder:flatbuffers.Builder, displayNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, displayNameOffset, 0);
}
static endSession(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createSession(builder:flatbuffers.Builder, tokenOffset:flatbuffers.Offset, userIdOffset:flatbuffers.Offset, isGuest:boolean, displayNameOffset:flatbuffers.Offset):flatbuffers.Offset {
Session.startSession(builder);
Session.addToken(builder, tokenOffset);
Session.addUserId(builder, userIdOffset);
Session.addIsGuest(builder, isGuest);
Session.addDisplayName(builder, displayNameOffset);
return Session.endSession(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class StateRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):StateRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsStateRequest(bb:flatbuffers.ByteBuffer, obj?:StateRequest):StateRequest {
return (obj || new StateRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsStateRequest(bb:flatbuffers.ByteBuffer, obj?:StateRequest):StateRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new StateRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startStateRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static endStateRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createStateRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
StateRequest.startStateRequest(builder);
StateRequest.addGameId(builder, gameIdOffset);
return StateRequest.endStateRequest(builder);
}
}
+108
View File
@@ -0,0 +1,108 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { GameView } from '../scrabblefb/game-view.js';
export class StateView {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):StateView {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsStateView(bb:flatbuffers.ByteBuffer, obj?:StateView):StateView {
return (obj || new StateView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsStateView(bb:flatbuffers.ByteBuffer, obj?:StateView):StateView {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new StateView()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
game(obj?:GameView):GameView|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new GameView()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
seat():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
rack(index: number):string
rack(index: number,optionalEncoding:flatbuffers.Encoding):string|Uint8Array
rack(index: number,optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb!.__vector(this.bb_pos + offset) + index * 4, optionalEncoding) : null;
}
rackLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
bagLen():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
hintsRemaining():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
static startStateView(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addGame(builder:flatbuffers.Builder, gameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameOffset, 0);
}
static addSeat(builder:flatbuffers.Builder, seat:number) {
builder.addFieldInt32(1, seat, 0);
}
static addRack(builder:flatbuffers.Builder, rackOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, rackOffset, 0);
}
static createRackVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startRackVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addBagLen(builder:flatbuffers.Builder, bagLen:number) {
builder.addFieldInt32(3, bagLen, 0);
}
static addHintsRemaining(builder:flatbuffers.Builder, hintsRemaining:number) {
builder.addFieldInt32(4, hintsRemaining, 0);
}
static endStateView(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createStateView(builder:flatbuffers.Builder, gameOffset:flatbuffers.Offset, seat:number, rackOffset:flatbuffers.Offset, bagLen:number, hintsRemaining:number):flatbuffers.Offset {
StateView.startStateView(builder);
StateView.addGame(builder, gameOffset);
StateView.addSeat(builder, seat);
StateView.addRack(builder, rackOffset);
StateView.addBagLen(builder, bagLen);
StateView.addHintsRemaining(builder, hintsRemaining);
return StateView.endStateView(builder);
}
}
@@ -0,0 +1,90 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
import { TileRecord } from '../scrabblefb/tile-record.js';
export class SubmitPlayRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SubmitPlayRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSubmitPlayRequest(bb:flatbuffers.ByteBuffer, obj?:SubmitPlayRequest):SubmitPlayRequest {
return (obj || new SubmitPlayRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSubmitPlayRequest(bb:flatbuffers.ByteBuffer, obj?:SubmitPlayRequest):SubmitPlayRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SubmitPlayRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
dir():string|null
dir(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
dir(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
tiles(index: number, obj?:TileRecord):TileRecord|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? (obj || new TileRecord()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
tilesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startSubmitPlayRequest(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addDir(builder:flatbuffers.Builder, dirOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, dirOffset, 0);
}
static addTiles(builder:flatbuffers.Builder, tilesOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, tilesOffset, 0);
}
static createTilesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startTilesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endSubmitPlayRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createSubmitPlayRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, dirOffset:flatbuffers.Offset, tilesOffset:flatbuffers.Offset):flatbuffers.Offset {
SubmitPlayRequest.startSubmitPlayRequest(builder);
SubmitPlayRequest.addGameId(builder, gameIdOffset);
SubmitPlayRequest.addDir(builder, dirOffset);
SubmitPlayRequest.addTiles(builder, tilesOffset);
return SubmitPlayRequest.endSubmitPlayRequest(builder);
}
}
@@ -0,0 +1,48 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class TelegramLoginRequest {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):TelegramLoginRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsTelegramLoginRequest(bb:flatbuffers.ByteBuffer, obj?:TelegramLoginRequest):TelegramLoginRequest {
return (obj || new TelegramLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsTelegramLoginRequest(bb:flatbuffers.ByteBuffer, obj?:TelegramLoginRequest):TelegramLoginRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new TelegramLoginRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
initData():string|null
initData(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
initData(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startTelegramLoginRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addInitData(builder:flatbuffers.Builder, initDataOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, initDataOffset, 0);
}
static endTelegramLoginRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createTelegramLoginRequest(builder:flatbuffers.Builder, initDataOffset:flatbuffers.Offset):flatbuffers.Offset {
TelegramLoginRequest.startTelegramLoginRequest(builder);
TelegramLoginRequest.addInitData(builder, initDataOffset);
return TelegramLoginRequest.endTelegramLoginRequest(builder);
}
}
+78
View File
@@ -0,0 +1,78 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class TileRecord {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):TileRecord {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsTileRecord(bb:flatbuffers.ByteBuffer, obj?:TileRecord):TileRecord {
return (obj || new TileRecord()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsTileRecord(bb:flatbuffers.ByteBuffer, obj?:TileRecord):TileRecord {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new TileRecord()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
row():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
col():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
letter():string|null
letter(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
letter(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
blank():boolean {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
static startTileRecord(builder:flatbuffers.Builder) {
builder.startObject(4);
}
static addRow(builder:flatbuffers.Builder, row:number) {
builder.addFieldInt32(0, row, 0);
}
static addCol(builder:flatbuffers.Builder, col:number) {
builder.addFieldInt32(1, col, 0);
}
static addLetter(builder:flatbuffers.Builder, letterOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, letterOffset, 0);
}
static addBlank(builder:flatbuffers.Builder, blank:boolean) {
builder.addFieldInt8(3, +blank, +false);
}
static endTileRecord(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createTileRecord(builder:flatbuffers.Builder, row:number, col:number, letterOffset:flatbuffers.Offset, blank:boolean):flatbuffers.Offset {
TileRecord.startTileRecord(builder);
TileRecord.addRow(builder, row);
TileRecord.addCol(builder, col);
TileRecord.addLetter(builder, letterOffset);
TileRecord.addBlank(builder, blank);
return TileRecord.endTileRecord(builder);
}
}
@@ -0,0 +1,58 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class WordCheckResult {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):WordCheckResult {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsWordCheckResult(bb:flatbuffers.ByteBuffer, obj?:WordCheckResult):WordCheckResult {
return (obj || new WordCheckResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsWordCheckResult(bb:flatbuffers.ByteBuffer, obj?:WordCheckResult):WordCheckResult {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new WordCheckResult()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
word():string|null
word(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
word(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
legal():boolean {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
static startWordCheckResult(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addWord(builder:flatbuffers.Builder, wordOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, wordOffset, 0);
}
static addLegal(builder:flatbuffers.Builder, legal:boolean) {
builder.addFieldInt8(1, +legal, +false);
}
static endWordCheckResult(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createWordCheckResult(builder:flatbuffers.Builder, wordOffset:flatbuffers.Offset, legal:boolean):flatbuffers.Offset {
WordCheckResult.startWordCheckResult(builder);
WordCheckResult.addWord(builder, wordOffset);
WordCheckResult.addLegal(builder, legal);
return WordCheckResult.endWordCheckResult(builder);
}
}
@@ -0,0 +1,58 @@
// automatically generated by the FlatBuffers compiler, do not modify
import * as flatbuffers from 'flatbuffers';
export class YourTurnEvent {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):YourTurnEvent {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsYourTurnEvent(bb:flatbuffers.ByteBuffer, obj?:YourTurnEvent):YourTurnEvent {
return (obj || new YourTurnEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsYourTurnEvent(bb:flatbuffers.ByteBuffer, obj?:YourTurnEvent):YourTurnEvent {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new YourTurnEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
deadlineUnix():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startYourTurnEvent(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, gameIdOffset, 0);
}
static addDeadlineUnix(builder:flatbuffers.Builder, deadlineUnix:bigint) {
builder.addFieldInt64(1, deadlineUnix, BigInt('0'));
}
static endYourTurnEvent(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createYourTurnEvent(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, deadlineUnix:bigint):flatbuffers.Offset {
YourTurnEvent.startYourTurnEvent(builder);
YourTurnEvent.addGameId(builder, gameIdOffset);
YourTurnEvent.addDeadlineUnix(builder, deadlineUnix);
return YourTurnEvent.endYourTurnEvent(builder);
}
}
+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { lastPlayTiles, replay } from './board';
import type { MoveRecord } from './model';
function play(tiles: { row: number; col: number; letter: string; blank: boolean }[]): MoveRecord {
return {
player: 0,
action: 'play',
dir: 'H',
mainRow: tiles[0].row,
mainCol: tiles[0].col,
tiles,
words: [],
count: 0,
score: 0,
total: 0,
};
}
const pass: MoveRecord = {
player: 1,
action: 'pass',
dir: '',
mainRow: 0,
mainCol: 0,
tiles: [],
words: [],
count: 0,
score: 0,
total: 0,
};
describe('board replay', () => {
it('places play tiles and ignores non-play moves', () => {
const moves = [
play([
{ row: 7, col: 7, letter: 'A', blank: false },
{ row: 7, col: 8, letter: 'B', blank: true },
]),
pass,
play([{ row: 8, col: 7, letter: 'C', blank: false }]),
];
const b = replay(moves);
expect(b[7][7]?.letter).toBe('A');
expect(b[7][8]?.blank).toBe(true);
expect(b[8][7]?.letter).toBe('C');
expect(b[0][0]).toBeNull();
expect(b.length).toBe(15);
expect(b[0].length).toBe(15);
});
it('lastPlayTiles returns the most recent play, skipping passes', () => {
const moves = [play([{ row: 7, col: 7, letter: 'A', blank: false }]), pass];
expect(lastPlayTiles(moves)).toHaveLength(1);
expect(lastPlayTiles([pass])).toHaveLength(0);
});
});
+80
View File
@@ -0,0 +1,80 @@
import { Builder, ByteBuffer } from 'flatbuffers';
import { describe, expect, it } from 'vitest';
import * as fb from '../gen/fbs/scrabblefb';
import { decodeGameList, decodeSession, encodeSubmitPlay } from './codec';
describe('codec', () => {
it('encodes a SubmitPlayRequest the gateway can read', () => {
const buf = encodeSubmitPlay('g1', 'H', [
{ row: 7, col: 7, letter: 'A', blank: false },
{ row: 7, col: 8, letter: 'B', blank: true },
]);
const r = fb.SubmitPlayRequest.getRootAsSubmitPlayRequest(new ByteBuffer(buf));
expect(r.gameId()).toBe('g1');
expect(r.dir()).toBe('H');
expect(r.tilesLength()).toBe(2);
expect(r.tiles(0)?.letter()).toBe('A');
expect(r.tiles(1)?.blank()).toBe(true);
});
it('decodes a Session', () => {
const b = new Builder(64);
const token = b.createString('tok');
const uid = b.createString('u1');
const name = b.createString('Me');
fb.Session.startSession(b);
fb.Session.addToken(b, token);
fb.Session.addUserId(b, uid);
fb.Session.addIsGuest(b, true);
fb.Session.addDisplayName(b, name);
b.finish(fb.Session.endSession(b));
expect(decodeSession(b.asUint8Array())).toEqual({
token: 'tok',
userId: 'u1',
isGuest: true,
displayName: 'Me',
});
});
it('decodes a GameList including nested seat display names', () => {
const b = new Builder(256);
const aid = b.createString('a1');
const dn = b.createString('Ann');
fb.SeatView.startSeatView(b);
fb.SeatView.addSeat(b, 1);
fb.SeatView.addAccountId(b, aid);
fb.SeatView.addScore(b, 13);
fb.SeatView.addHintsUsed(b, 0);
fb.SeatView.addIsWinner(b, false);
fb.SeatView.addDisplayName(b, dn);
const seat = fb.SeatView.endSeatView(b);
const seats = fb.GameView.createSeatsVector(b, [seat]);
const id = b.createString('g1');
const variant = b.createString('english');
const dv = b.createString('v1');
const status = b.createString('active');
const er = b.createString('');
fb.GameView.startGameView(b);
fb.GameView.addId(b, id);
fb.GameView.addVariant(b, variant);
fb.GameView.addDictVersion(b, dv);
fb.GameView.addStatus(b, status);
fb.GameView.addPlayers(b, 2);
fb.GameView.addToMove(b, 0);
fb.GameView.addTurnTimeoutSecs(b, 86400);
fb.GameView.addMoveCount(b, 4);
fb.GameView.addEndReason(b, er);
fb.GameView.addSeats(b, seats);
const game = fb.GameView.endGameView(b);
const games = fb.GameList.createGamesVector(b, [game]);
fb.GameList.startGameList(b);
fb.GameList.addGames(b, games);
b.finish(fb.GameList.endGameList(b));
const gl = decodeGameList(b.asUint8Array());
expect(gl.games).toHaveLength(1);
expect(gl.games[0].id).toBe('g1');
expect(gl.games[0].seats[0].displayName).toBe('Ann');
expect(gl.games[0].seats[0].score).toBe(13);
});
});
+384
View File
@@ -0,0 +1,384 @@
// FlatBuffers <-> domain-model codec. The real transport encodes each request table
// and decodes each response/event table here, mirroring the gateway's Go encoders in
// reverse. The screens only ever see the plain model (lib/model), never these wire
// types.
import { Builder, ByteBuffer, type Offset } from 'flatbuffers';
import * as fb from '../gen/fbs/scrabblefb';
import type { PlacedTile } from './client';
import type {
ChatMessage,
EvalResult,
GameList,
GameView,
History,
HintResult,
MatchResult,
MoveRecord,
MoveResult,
Profile,
PushEvent,
Seat,
Session,
StateView,
Tile,
Variant,
WordCheckResult,
} from './model';
// --- request encoders ---
function buildTile(b: Builder, t: PlacedTile): Offset {
const letter = b.createString(t.letter);
fb.TileRecord.startTileRecord(b);
fb.TileRecord.addRow(b, t.row);
fb.TileRecord.addCol(b, t.col);
fb.TileRecord.addLetter(b, letter);
fb.TileRecord.addBlank(b, t.blank);
return fb.TileRecord.endTileRecord(b);
}
function finish(b: Builder, root: Offset): Uint8Array {
b.finish(root);
return b.asUint8Array();
}
export const empty = (): Uint8Array => new Uint8Array();
export function encodeGameAction(gameId: string): Uint8Array {
const b = new Builder(64);
const gid = b.createString(gameId);
fb.GameActionRequest.startGameActionRequest(b);
fb.GameActionRequest.addGameId(b, gid);
return finish(b, fb.GameActionRequest.endGameActionRequest(b));
}
export function encodeStateRequest(gameId: string): Uint8Array {
const b = new Builder(64);
const gid = b.createString(gameId);
fb.StateRequest.startStateRequest(b);
fb.StateRequest.addGameId(b, gid);
return finish(b, fb.StateRequest.endStateRequest(b));
}
export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
const b = new Builder(256);
const tileOffs = tiles.map((t) => buildTile(b, t));
const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs);
const gid = b.createString(gameId);
const d = b.createString(dir);
fb.SubmitPlayRequest.startSubmitPlayRequest(b);
fb.SubmitPlayRequest.addGameId(b, gid);
fb.SubmitPlayRequest.addDir(b, d);
fb.SubmitPlayRequest.addTiles(b, vec);
return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b));
}
export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
const b = new Builder(256);
const tileOffs = tiles.map((t) => buildTile(b, t));
const vec = fb.EvalRequest.createTilesVector(b, tileOffs);
const gid = b.createString(gameId);
const d = b.createString(dir);
fb.EvalRequest.startEvalRequest(b);
fb.EvalRequest.addGameId(b, gid);
fb.EvalRequest.addDir(b, d);
fb.EvalRequest.addTiles(b, vec);
return finish(b, fb.EvalRequest.endEvalRequest(b));
}
export function encodeExchange(gameId: string, tiles: string[]): Uint8Array {
const b = new Builder(128);
const offs = tiles.map((s) => b.createString(s));
const vec = fb.ExchangeRequest.createTilesVector(b, offs);
const gid = b.createString(gameId);
fb.ExchangeRequest.startExchangeRequest(b);
fb.ExchangeRequest.addGameId(b, gid);
fb.ExchangeRequest.addTiles(b, vec);
return finish(b, fb.ExchangeRequest.endExchangeRequest(b));
}
export function encodeCheckWord(gameId: string, word: string): Uint8Array {
const b = new Builder(128);
const gid = b.createString(gameId);
const w = b.createString(word);
fb.CheckWordRequest.startCheckWordRequest(b);
fb.CheckWordRequest.addGameId(b, gid);
fb.CheckWordRequest.addWord(b, w);
return finish(b, fb.CheckWordRequest.endCheckWordRequest(b));
}
export function encodeComplaint(gameId: string, word: string, note: string): Uint8Array {
const b = new Builder(256);
const gid = b.createString(gameId);
const w = b.createString(word);
const n = b.createString(note);
fb.ComplaintRequest.startComplaintRequest(b);
fb.ComplaintRequest.addGameId(b, gid);
fb.ComplaintRequest.addWord(b, w);
fb.ComplaintRequest.addNote(b, n);
return finish(b, fb.ComplaintRequest.endComplaintRequest(b));
}
export function encodeEnqueue(variant: Variant): Uint8Array {
const b = new Builder(64);
const v = b.createString(variant);
fb.EnqueueRequest.startEnqueueRequest(b);
fb.EnqueueRequest.addVariant(b, v);
return finish(b, fb.EnqueueRequest.endEnqueueRequest(b));
}
export function encodeChatPost(gameId: string, body: string): Uint8Array {
const b = new Builder(128);
const gid = b.createString(gameId);
const bd = b.createString(body);
fb.ChatPostRequest.startChatPostRequest(b);
fb.ChatPostRequest.addGameId(b, gid);
fb.ChatPostRequest.addBody(b, bd);
return finish(b, fb.ChatPostRequest.endChatPostRequest(b));
}
export function encodeGuestLogin(locale: string): Uint8Array {
const b = new Builder(64);
const l = b.createString(locale);
fb.GuestLoginRequest.startGuestLoginRequest(b);
fb.GuestLoginRequest.addLocale(b, l);
return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b));
}
export function encodeEmailRequest(email: string): Uint8Array {
const b = new Builder(128);
const e = b.createString(email);
fb.EmailRequestRequest.startEmailRequestRequest(b);
fb.EmailRequestRequest.addEmail(b, e);
return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b));
}
export function encodeEmailLogin(email: string, code: string): Uint8Array {
const b = new Builder(128);
const e = b.createString(email);
const c = b.createString(code);
fb.EmailLoginRequest.startEmailLoginRequest(b);
fb.EmailLoginRequest.addEmail(b, e);
fb.EmailLoginRequest.addCode(b, c);
return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b));
}
// --- response decoders ---
function s(v: string | null): string {
return v ?? '';
}
function decodeTile(t: fb.TileRecord): Tile {
return { row: t.row(), col: t.col(), letter: s(t.letter()), blank: t.blank() };
}
function decodeSeat(v: fb.SeatView): Seat {
return {
seat: v.seat(),
accountId: s(v.accountId()),
displayName: s(v.displayName()),
score: v.score(),
hintsUsed: v.hintsUsed(),
isWinner: v.isWinner(),
};
}
function decodeGameView(g: fb.GameView): GameView {
const seats: Seat[] = [];
for (let i = 0; i < g.seatsLength(); i++) {
const sv = g.seats(i);
if (sv) seats.push(decodeSeat(sv));
}
return {
id: s(g.id()),
variant: s(g.variant()) as Variant,
dictVersion: s(g.dictVersion()),
status: s(g.status()),
players: g.players(),
toMove: g.toMove(),
turnTimeoutSecs: g.turnTimeoutSecs(),
moveCount: g.moveCount(),
endReason: s(g.endReason()),
seats,
};
}
function decodeMove(m: fb.MoveRecord): MoveRecord {
const tiles: Tile[] = [];
for (let i = 0; i < m.tilesLength(); i++) {
const t = m.tiles(i);
if (t) tiles.push(decodeTile(t));
}
const words: string[] = [];
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)));
return {
player: m.player(),
action: s(m.action()),
dir: s(m.dir()),
mainRow: m.mainRow(),
mainCol: m.mainCol(),
tiles,
words,
count: m.count(),
score: m.score(),
total: m.total(),
};
}
function decodeChatMsg(m: fb.ChatMessage): ChatMessage {
return {
id: s(m.id()),
gameId: s(m.gameId()),
senderId: s(m.senderId()),
kind: s(m.kind()),
body: s(m.body()),
createdAtUnix: Number(m.createdAtUnix()),
};
}
export function decodeSession(buf: Uint8Array): Session {
const t = fb.Session.getRootAsSession(new ByteBuffer(buf));
return { token: s(t.token()), userId: s(t.userId()), isGuest: t.isGuest(), displayName: s(t.displayName()) };
}
export function decodeProfile(buf: Uint8Array): Profile {
const p = fb.Profile.getRootAsProfile(new ByteBuffer(buf));
return {
userId: s(p.userId()),
displayName: s(p.displayName()),
preferredLanguage: s(p.preferredLanguage()),
timeZone: s(p.timeZone()),
hintBalance: p.hintBalance(),
blockChat: p.blockChat(),
blockFriendRequests: p.blockFriendRequests(),
isGuest: p.isGuest(),
};
}
export function decodeStateView(buf: Uint8Array): StateView {
const v = fb.StateView.getRootAsStateView(new ByteBuffer(buf));
const g = v.game();
const rack: string[] = [];
for (let i = 0; i < v.rackLength(); i++) rack.push(s(v.rack(i)));
return {
game: g ? decodeGameView(g) : emptyGame(),
seat: v.seat(),
rack,
bagLen: v.bagLen(),
hintsRemaining: v.hintsRemaining(),
};
}
export function decodeMoveResult(buf: Uint8Array): MoveResult {
const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf));
const m = r.move();
const g = r.game();
return { move: m ? decodeMove(m) : emptyMove(), game: g ? decodeGameView(g) : emptyGame() };
}
export function decodeHintResult(buf: Uint8Array): HintResult {
const r = fb.HintResult.getRootAsHintResult(new ByteBuffer(buf));
const m = r.move();
return { move: m ? decodeMove(m) : emptyMove(), hintsRemaining: r.hintsRemaining() };
}
export function decodeEvalResult(buf: Uint8Array): EvalResult {
const r = fb.EvalResult.getRootAsEvalResult(new ByteBuffer(buf));
const words: string[] = [];
for (let i = 0; i < r.wordsLength(); i++) words.push(s(r.words(i)));
return { legal: r.legal(), score: r.score(), words };
}
export function decodeWordCheck(buf: Uint8Array): WordCheckResult {
const r = fb.WordCheckResult.getRootAsWordCheckResult(new ByteBuffer(buf));
return { word: s(r.word()), legal: r.legal() };
}
export function decodeHistory(buf: Uint8Array): History {
const h = fb.History.getRootAsHistory(new ByteBuffer(buf));
const moves: MoveRecord[] = [];
for (let i = 0; i < h.movesLength(); i++) {
const m = h.moves(i);
if (m) moves.push(decodeMove(m));
}
return { gameId: s(h.gameId()), moves };
}
export function decodeGameList(buf: Uint8Array): GameList {
const gl = fb.GameList.getRootAsGameList(new ByteBuffer(buf));
const games: GameView[] = [];
for (let i = 0; i < gl.gamesLength(); i++) {
const g = gl.games(i);
if (g) games.push(decodeGameView(g));
}
return { games };
}
export function decodeMatchResult(buf: Uint8Array): MatchResult {
const m = fb.MatchResult.getRootAsMatchResult(new ByteBuffer(buf));
const g = m.game();
return { matched: m.matched(), game: m.matched() && g ? decodeGameView(g) : undefined };
}
export function decodeChatMessage(buf: Uint8Array): ChatMessage {
return decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(new ByteBuffer(buf)));
}
export function decodeChatList(buf: Uint8Array): ChatMessage[] {
const cl = fb.ChatList.getRootAsChatList(new ByteBuffer(buf));
const out: ChatMessage[] = [];
for (let i = 0; i < cl.messagesLength(); i++) {
const m = cl.messages(i);
if (m) out.push(decodeChatMsg(m));
}
return out;
}
export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null {
const bb = new ByteBuffer(payload);
switch (kind) {
case 'your_turn': {
const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb);
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()) };
}
case 'opponent_moved': {
const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb);
return { kind: 'opponent_moved', gameId: s(e.gameId()), seat: e.seat(), action: s(e.action()), score: e.score(), total: e.total() };
}
case 'chat_message':
return { kind: 'chat_message', message: decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(bb)) };
case 'nudge': {
const e = fb.NudgeEvent.getRootAsNudgeEvent(bb);
return { kind: 'nudge', gameId: s(e.gameId()), fromUserId: s(e.fromUserId()) };
}
case 'match_found': {
const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb);
return { kind: 'match_found', gameId: s(e.gameId()) };
}
case 'heartbeat':
return { kind: 'heartbeat' };
default:
return null;
}
}
function emptyGame(): GameView {
return {
id: '',
variant: 'english',
dictVersion: '',
status: '',
players: 0,
toMove: 0,
turnTimeoutSecs: 0,
moveCount: 0,
endReason: '',
seats: [],
};
}
function emptyMove(): MoveRecord {
return { player: 0, action: '', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
}
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { en } from './i18n/en';
import { ru } from './i18n/ru';
import { errorKey, translate } from './i18n/catalog';
describe('i18n catalog', () => {
it('has identical keys in en and ru', () => {
expect(Object.keys(ru).sort()).toEqual(Object.keys(en).sort());
});
it('interpolates parameters', () => {
expect(translate('en', 'game.bag', { n: 7 })).toBe('Bag 7');
expect(translate('ru', 'game.bag', { n: 7 })).toBe('Мешок 7');
});
it('maps error codes to keys with a generic fallback', () => {
expect(errorKey('not_your_turn')).toBe('error.not_your_turn');
expect(errorKey('totally_unknown')).toBe('error.generic');
});
});
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import {
BLANK,
direction,
newPlacement,
place,
rackView,
recallAt,
reset,
toSubmit,
} from './placement';
const rack = ['A', 'Q', BLANK, 'N', 'I', 'W', 'E'];
describe('placement state machine', () => {
it('places a tile and marks the rack slot used', () => {
const p = place(newPlacement(rack), 0, 7, 7);
expect(p.pending).toHaveLength(1);
expect(rackView(p)[0].used).toBe(true);
expect(rackView(p)[1].used).toBe(false);
});
it('rejects reusing a slot or an occupied cell', () => {
let p = place(newPlacement(rack), 0, 7, 7);
p = place(p, 0, 7, 8); // same slot -> no-op
expect(p.pending).toHaveLength(1);
p = place(p, 1, 7, 7); // occupied cell -> no-op
expect(p.pending).toHaveLength(1);
});
it('requires a letter for a blank slot', () => {
const noLetter = place(newPlacement(rack), 2, 7, 7);
expect(noLetter.pending).toHaveLength(0);
const withLetter = place(newPlacement(rack), 2, 7, 7, 'x');
expect(withLetter.pending[0]).toMatchObject({ letter: 'X', blank: true });
});
it('recalls a tile by cell', () => {
let p = place(newPlacement(rack), 0, 7, 7);
p = recallAt(p, 7, 7);
expect(p.pending).toHaveLength(0);
expect(reset(place(p, 0, 7, 7)).pending).toHaveLength(0);
});
it('infers direction H for a row, V for a column, null for a single tile', () => {
let h = place(newPlacement(rack), 0, 7, 7);
h = place(h, 1, 7, 8);
expect(direction(h)).toBe('H');
let v = place(newPlacement(rack), 0, 7, 7);
v = place(v, 1, 8, 7);
expect(direction(v)).toBe('V');
expect(direction(place(newPlacement(rack), 0, 7, 7))).toBeNull();
});
it('builds a sorted submit payload and honours a direction override', () => {
let p = place(newPlacement(rack), 1, 7, 9);
p = place(p, 0, 7, 7);
const sub = toSubmit(p);
expect(sub?.dir).toBe('H');
expect(sub?.tiles.map((t) => t.col)).toEqual([7, 9]);
expect(toSubmit(place(newPlacement(rack), 0, 7, 7), 'V')?.dir).toBe('V');
expect(toSubmit(newPlacement(rack))).toBeNull();
});
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import { alphabet, BOARD_SIZE, centre, premiumGrid, tileValue } from './premiums';
// Parity with scrabble-solver/rules/rules.go: english/russian share standardBoard
// (centre is a double word); erudit shares the geometry but a non-doubling centre.
describe('premium layout', () => {
it('is a 15x15 grid with TW corners', () => {
const g = premiumGrid('english');
expect(g.length).toBe(BOARD_SIZE);
expect(g[0].length).toBe(BOARD_SIZE);
for (const [r, c] of [
[0, 0],
[0, 14],
[14, 0],
[14, 14],
]) {
expect(g[r][c]).toBe('TW');
}
});
it('doubles the centre for standard variants but not for erudit', () => {
expect(centre('english')).toEqual({ row: 7, col: 7 });
expect(premiumGrid('english')[7][7]).toBe('DW');
expect(premiumGrid('russian')[7][7]).toBe('DW');
expect(centre('erudit')).toEqual({ row: 7, col: 7 });
expect(premiumGrid('erudit')[7][7]).toBe('');
});
it('keeps the standard premium counts', () => {
const flat = premiumGrid('english').flat();
const count = (p: string) => flat.filter((x) => x === p).length;
expect(count('TW')).toBe(8);
expect(count('TL')).toBe(12);
expect(count('DL')).toBe(24);
expect(count('DW')).toBe(17); // 16 double-word squares + the centre
});
});
describe('tile values', () => {
it('scores letters per variant and zero for a blank', () => {
expect(tileValue('english', 'A')).toBe(1);
expect(tileValue('english', 'Q')).toBe(10);
expect(tileValue('english', '?')).toBe(0);
expect(tileValue('russian', 'Ф')).toBe(10);
expect(tileValue('erudit', 'Ё')).toBe(0);
});
it('exposes the full alphabet for the blank chooser', () => {
expect(alphabet('english')).toHaveLength(26);
expect(alphabet('russian')).toHaveLength(33);
expect(alphabet('erudit')).toHaveLength(33);
});
});
+132 -31
View File
@@ -1,36 +1,137 @@
// Placeholder for the real Connect-web + FlatBuffers transport, wired in the edge
// codegen task. Until then, selecting a non-mock mode surfaces a clear error instead
// of failing silently. The mock (lib/mock) backs `pnpm start`.
// The real Connect-RPC + FlatBuffers transport. Every unary op rides the single
// Execute envelope (message_type + FlatBuffers payload); the live stream is
// Subscribe. The session token rides in the Authorization header; domain outcomes
// come back in result_code, edge failures as Connect error codes — both normalised to
// a thrown GatewayError. In dev the Vite proxy forwards the RPC path to the h2c
// gateway; in a packaged app VITE_GATEWAY_URL points at the real origin.
import type { GatewayClient } from './client';
import { GatewayError } from './client';
import { Code, ConnectError, createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-web';
import { Gateway } from '../gen/edge/v1/edge_pb';
import { GatewayError, type GatewayClient } from './client';
import * as codec from './codec';
function toGatewayError(e: unknown): GatewayError {
if (e instanceof ConnectError) {
switch (e.code) {
case Code.Unauthenticated:
return new GatewayError('session_invalid', e.message);
case Code.ResourceExhausted:
return new GatewayError('rate_limited', e.message);
case Code.Unavailable:
return new GatewayError('unavailable', e.message);
case Code.NotFound:
return new GatewayError('not_found', e.message);
default:
return new GatewayError('internal', e.message);
}
}
return new GatewayError('unavailable', String(e));
}
export function createTransport(baseUrl: string): GatewayClient {
const origin = baseUrl || (typeof location !== 'undefined' ? location.origin : '');
const transport = createConnectTransport({ baseUrl: origin, useBinaryFormat: true });
const client = createClient(Gateway, transport);
let token: string | null = null;
const headers = (): Record<string, string> | undefined =>
token ? { authorization: `Bearer ${token}` } : undefined;
async function exec(messageType: string, payload: Uint8Array): Promise<Uint8Array> {
let res;
try {
res = await client.execute({ messageType, payload, requestId: '' }, { headers: headers() });
} catch (e) {
throw toGatewayError(e);
}
if (res.resultCode && res.resultCode !== 'ok') throw new GatewayError(res.resultCode);
return res.payload;
}
export function createTransport(_baseUrl: string): GatewayClient {
const ni = (): never => {
throw new GatewayError('unavailable', 'real transport not wired yet');
};
return {
setToken: () => {},
authGuest: ni,
authEmailRequest: ni,
authEmailLogin: ni,
profileGet: ni,
gamesList: ni,
lobbyEnqueue: ni,
lobbyPoll: ni,
gameState: ni,
gameHistory: ni,
submitPlay: ni,
pass: ni,
exchange: ni,
resign: ni,
hint: ni,
evaluate: ni,
checkWord: ni,
complaint: ni,
chatPost: ni,
chatList: ni,
nudge: ni,
subscribe: ni,
setToken(t) {
token = t;
},
async authGuest(locale) {
return codec.decodeSession(await exec('auth.guest', codec.encodeGuestLogin(locale ?? '')));
},
async authEmailRequest(email) {
await exec('auth.email.request', codec.encodeEmailRequest(email));
},
async authEmailLogin(email, code) {
return codec.decodeSession(await exec('auth.email.login', codec.encodeEmailLogin(email, code)));
},
async profileGet() {
return codec.decodeProfile(await exec('profile.get', codec.empty()));
},
async gamesList() {
return codec.decodeGameList(await exec('games.list', codec.empty()));
},
async lobbyEnqueue(variant) {
return codec.decodeMatchResult(await exec('lobby.enqueue', codec.encodeEnqueue(variant)));
},
async lobbyPoll() {
return codec.decodeMatchResult(await exec('lobby.poll', codec.empty()));
},
async gameState(id) {
return codec.decodeStateView(await exec('game.state', codec.encodeStateRequest(id)));
},
async gameHistory(id) {
return codec.decodeHistory(await exec('game.history', codec.encodeGameAction(id)));
},
async submitPlay(id, dir, tiles) {
return codec.decodeMoveResult(await exec('game.submit_play', codec.encodeSubmitPlay(id, dir, tiles)));
},
async pass(id) {
return codec.decodeMoveResult(await exec('game.pass', codec.encodeGameAction(id)));
},
async exchange(id, tiles) {
return codec.decodeMoveResult(await exec('game.exchange', codec.encodeExchange(id, tiles)));
},
async resign(id) {
return codec.decodeMoveResult(await exec('game.resign', codec.encodeGameAction(id)));
},
async hint(id) {
return codec.decodeHintResult(await exec('game.hint', codec.encodeGameAction(id)));
},
async evaluate(id, dir, tiles) {
return codec.decodeEvalResult(await exec('game.evaluate', codec.encodeEval(id, dir, tiles)));
},
async checkWord(id, word) {
return codec.decodeWordCheck(await exec('game.check_word', codec.encodeCheckWord(id, word)));
},
async complaint(id, word, note) {
await exec('game.complaint', codec.encodeComplaint(id, word, note));
},
async chatPost(id, body) {
return codec.decodeChatMessage(await exec('chat.post', codec.encodeChatPost(id, body)));
},
async chatList(id) {
return codec.decodeChatList(await exec('chat.list', codec.encodeGameAction(id)));
},
async nudge(id) {
return codec.decodeChatMessage(await exec('chat.nudge', codec.encodeGameAction(id)));
},
subscribe(onEvent, onError) {
const ctrl = new AbortController();
void (async () => {
try {
for await (const ev of client.subscribe({}, { headers: headers(), signal: ctrl.signal })) {
const pe = codec.decodeEvent(ev.kind, ev.payload);
if (pe) onEvent(pe);
}
} catch (e) {
if (!ctrl.signal.aborted) onError?.(toGatewayError(e));
}
})();
return () => ctrl.abort();
},
};
}
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config';
// Unit tests cover the pure client logic (no Svelte components, no network): the
// board replay, the placement state machine, premium/value maps, the FlatBuffers
// codec and the i18n catalog. Component/interaction coverage is the Playwright smoke.
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
environment: 'node',
},
});