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 gofmt -l . # must print nothing
go test -count=1 ./backend/... go test -count=1 ./backend/...
go run ./backend/cmd/backend # /healthz, /readyz on :8080 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** | | 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
| 5 | Robot opponent | **done** | | 5 | Robot opponent | **done** |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** | | 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo | | 7 | UI playable slice (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | todo |
| 8 | Telegram integration (bot side-service, deep-link, push) | todo | | 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | todo |
| 9 | Admin & dictionary ops (complaint review, version reload) | todo | | 9 | Telegram integration (bot side-service, deep-link, push) | todo |
| 10 | Account linking & merge | todo | | 10 | Admin & dictionary ops (complaint review, version reload) | todo |
| 11 | Polish (observability, perf with evidence, deploy) | 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 Scaffolding is incremental: `go.work` lists only existing modules; each stage
adds the modules it needs. adds the modules it needs.
@@ -70,7 +71,7 @@ platform identities.
Open details: Postgres version + DSN/`search_path` convention; jet vs Open details: Postgres version + DSN/`search_path` convention; jet vs
sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque sqlc/sqlx (default jet); migration naming; exact session-token shape (opaque
random length, TTL, revocation); account/identity table shape; whether the 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 ### Stage 2 — Engine package
Scope: `backend/internal/engine` over scrabble-solver — versioned DAWG 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); Open details: detailed game-board UX (deferred by the owner to this stage);
client routing; offline/refresh behaviour; design system / theming. 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), Scope: bot side-service, deep-link invites, platform push (your-turn / nudge),
Mini App launch/auth; backend↔platform internal API. Mini App launch/auth; backend↔platform internal API.
Open details: bot framework/library; deep-link scheme; push message templates; Open details: bot framework/library; deep-link scheme; push message templates;
internal API contract; Mini App hosting/origin. 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 Scope: admin endpoints (users, games, complaint review queue, dictionary
versions + reload), complaint→dictionary update pipeline. versions + reload), complaint→dictionary update pipeline.
Open details: whether a server-rendered console is wanted or JSON-only; the Open details: whether a server-rendered console is wanted or JSON-only; the
dictionary rebuild/deploy pipeline; complaint resolution workflow. 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, Scope: link-via-confirm; merge-into-A (stats sum, transfer games/friends,
dedupe). High blast-radius — focused regression tests. dedupe). High blast-radius — focused regression tests.
Open details: conflict resolution (active games on both, duplicate friends, Open details: conflict resolution (active games on both, duplicate friends,
display-name collisions); irreversibility/audit; confirm-flow per platform. display-name collisions); irreversibility/audit; confirm-flow per platform.
### Stage 11 — Polish ### Stage 12 — Polish
Scope: observability dashboards, evidence-based performance work, prod Scope: observability dashboards, evidence-based performance work, prod
build/deploy. build/deploy.
Open details: deployment target/host; dashboards; load expectations. 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, - HTTP surface: **service/store/cache layer only**. `/api/v1/{public,user,
internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via internal,admin}` groups + `X-User-ID` middleware are scaffolding (exposed via
`Server` group accessors); the session/account REST handlers land with the `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 - 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 - Tests/CI: integration tests behind the `integration` build tag in
`backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk `backend/internal/inttest` + new `integration.yaml` (testcontainers, Ryuk
off, serial), firing on push and PR. Backend now **hard-depends on Postgres 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, wins/losses; `max_word_points` = best single **move** score; ties draw,
resign/timeout is a loss, guests get no stats. resign/timeout is a loss, guests get no stats.
- **Complaint** (interview): full payload with `game_id`; word-check is scoped - **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. lifecycle, so the `status` column carries no value CHECK yet.
- **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon` - **GCG** (interview): standard Poslfit dialect (UTF-8, `#player`/`#lexicon`
pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES` 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 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 (`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the
current account; an email already confirmed by another account → `ErrEmailTaken` 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 - **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 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 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`/ end-to-end — auth (`auth.telegram`/`auth.guest`/`auth.email.request`/
`auth.email.login`), `profile.get`, `game.submit_play`/`game.state`, `auth.email.login`), `profile.get`, `game.submit_play`/`game.state`,
`lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin `lobby.enqueue`/`lobby.poll`, `chat.post`, all five push events, and the admin
passthrough. The remaining domain operations (friends, blocks, invitations, passthrough. The remaining domain operations reuse the identical transcode
hint, word-check, pass/exchange/resign, history/GCG, profile editing) reuse the pattern and are wired as the UI needs them: the play-loop ops (pass/exchange/
identical transcode pattern and are wired in **Stage 7** as the UI needs them. 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 - **Wire contracts in a new shared `scrabble/pkg` module** (interview): the
backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads backend push proto (`pkg/proto/push/v1`) and the FlatBuffers edge payloads
(`pkg/fbs`, one `scrabblefb` namespace) live here with **committed** generated (`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). (the galaxy donor's crypto stack was dropped, per §3).
- **Admin = gateway validates Basic-Auth** (interview): the gateway checks - **Admin = gateway validates Basic-Auth** (interview): the gateway checks
`GATEWAY_ADMIN_USER`/`_PASSWORD` and reverse-proxies to backend `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, - **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 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 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). (unit) — integration stays `./backend/...` (the only module with tagged tests).
The solver clone + `BACKEND_DICT_DIR` steps are unchanged. 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) ## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable, - **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 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 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 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 the runtime contract: a new `.dawg` appears in it and is loaded with
`dawg.Load`. `dawg.Load`.
- **TODO-3 — garbage-collect abandoned guest accounts.** Stage 6 makes a guest a - **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)* admin surface behind Basic Auth. *(added in a later stage)*
- **`backend`** — internal-only service that owns every domain concern and - **`backend`** — internal-only service that owns every domain concern and
embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process. embeds the [`scrabble-solver`](../scrabble-solver) engine library in-process.
- **`ui`** — pure-HTML5 client (plain Svelte + Vite), embeddable in platform - **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC
webviews and packageable to native via Capacitor. *(added in a later stage)* + 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). - **`platform/*`** — per-platform side-services (e.g. the Telegram bot).
*(added in a later stage)* *(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**). (`debug|info|warn|error`, default `info`), `BACKEND_POSTGRES_DSN` (**required**).
The full configuration surface and the go-jet regeneration step live in The full configuration surface and the go-jet regeneration step live in
[`backend/README.md`](backend/README.md). [`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 is returned for an unparseable email address.
ErrInvalidEmail = errors.New("account: invalid email address") ErrInvalidEmail = errors.New("account: invalid email address")
// ErrEmailTaken is returned when the email is already confirmed by another // 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") ErrEmailTaken = errors.New("account: email already confirmed by another account")
// ErrAlreadyConfirmed is returned when the email is already confirmed by the // ErrAlreadyConfirmed is returned when the email is already confirmed by the
// requesting account. // requesting account.
@@ -52,7 +52,7 @@ var (
// Mailer and verifies it, binding a confirmed email identity to the requesting // 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), // 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 // 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. // using an email as a login is Stage 6, which reuses this mechanism.
type EmailService struct { type EmailService struct {
store *Store 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 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. // History returns a game's full, dictionary-independent move journal.
func (svc *Service) History(ctx context.Context, gameID uuid.UUID) (HistoryView, error) { func (svc *Service) History(ctx context.Context, gameID uuid.UUID) (HistoryView, error) {
g, err := svc.store.GetGame(ctx, gameID) 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) 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. // GetJournal loads the ordered, decoded move journal for a game.
func (s *Store) GetJournal(ctx context.Context, id uuid.UUID) ([]HistoryMove, error) { func (s *Store) GetJournal(ctx context.Context, id uuid.UUID) ([]HistoryMove, error) {
stmt := postgres.SELECT(table.GameMoves.AllColumns). stmt := postgres.SELECT(table.GameMoves.AllColumns).
+2 -2
View File
@@ -15,7 +15,7 @@ const (
StatusFinished = "finished" 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. // ever writes StatusComplaintOpen.
const StatusComplaintOpen = "open" const StatusComplaintOpen = "open"
@@ -176,7 +176,7 @@ type RobotTurn struct {
Seed int64 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 { type Complaint struct {
ID uuid.UUID ID uuid.UUID
ComplainantID 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 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 // TestGameLifecycleAndStats drives a greedy two-player game to its natural end
// through the service and checks the finish state and statistics. // through the service and checks the finish state and statistics.
func TestGameLifecycleAndStats(t *testing.T) { func TestGameLifecycleAndStats(t *testing.T) {
+8 -6
View File
@@ -66,13 +66,15 @@ type moveRecordDTO struct {
Total int `json:"total"` 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 { type seatDTO struct {
Seat int `json:"seat"` Seat int `json:"seat"`
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
Score int `json:"score"` DisplayName string `json:"display_name"`
HintsUsed int `json:"hints_used"` Score int `json:"score"`
IsWinner bool `json:"is_winner"` HintsUsed int `json:"hints_used"`
IsWinner bool `json:"is_winner"`
} }
// gameDTO is the shared game summary. // gameDTO is the shared game summary.
+11
View File
@@ -37,8 +37,17 @@ func (s *Server) registerRoutes() {
u.GET("/profile", s.handleProfile) u.GET("/profile", s.handleProfile)
} }
if s.games != nil { if s.games != nil {
u.GET("/games", s.handleListGames)
u.POST("/games/:id/play", s.handleSubmitPlay) u.POST("/games/:id/play", s.handleSubmitPlay)
u.GET("/games/:id/state", s.handleGameState) 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 { if s.matchmaker != nil {
u.POST("/lobby/enqueue", s.handleEnqueue) u.POST("/lobby/enqueue", s.handleEnqueue)
@@ -46,6 +55,8 @@ func (s *Server) registerRoutes() {
} }
if s.social != nil { if s.social != nil {
u.POST("/games/:id/chat", s.handleChatPost) 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) 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 // 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 // reverse proxy (docs/ARCHITECTURE.md §12). The backend trusts the gateway to
// have authenticated the operator; the admin surface itself (complaint review, // 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. // proves the path end to end until then.
func (s *Server) handleAdminPing(c *gin.Context) { func (s *Server) handleAdminPing(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"}) 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) s.abortErr(c, err)
return return
} }
c.JSON(http.StatusOK, moveResultDTOFrom(res)) s.writeMoveResult(c, res)
} }
// handleGameState returns the player's view of a game. // handleGameState returns the player's view of a game.
@@ -88,7 +88,9 @@ func (s *Server) handleGameState(c *gin.Context) {
s.abortErr(c, err) s.abortErr(c, err)
return 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. // enqueueRequest joins the per-variant auto-match pool.
@@ -118,7 +120,11 @@ func (s *Server) handleEnqueue(c *gin.Context) {
s.abortErr(c, err) s.abortErr(c, err)
return 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. // 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) s.abortErr(c, err)
return 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. // 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, administration. Embeds the **`scrabble-solver`** engine **as a library,
in-process** — there is no per-game container. The only network consumer of in-process** — there is no per-game container. The only network consumer of
`backend` is `gateway` (plus platform side-services over an internal API). `backend` is `gateway` (plus platform side-services over an internal API).
- **`ui`** *(planned)* — pure-HTML5 client (plain Svelte + Vite, static build). - **`ui`** — pure-HTML5 client (plain Svelte 5 + TypeScript + Vite, static build;
Talks to `backend` only through `gateway`. Embeddable in platform webviews; no SvelteKit). Talks to `backend` only through `gateway` over Connect-RPC +
packageable to native (iOS/Android) via Capacitor. 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 - **`platform/<name>`** *(planned)* — per-platform side-services (Telegram bot
first): deep-link invites and platform-native push notifications. They talk first): deep-link invites and platform-native push notifications. They talk
to `backend` over an internal API. 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 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 development log mailer when none is configured) and, once verified, attaches a
confirmed email identity. An email already confirmed by **another** account is 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. identities use application-generated **UUIDv7** primary keys.
- **Linking** is initiated from an authenticated profile: choose a platform → - **Linking** is initiated from an authenticated profile: choose a platform →
complete that platform's web-auth confirm → attach the identity to the complete that platform's web-auth confirm → attach the identity to the
@@ -140,7 +151,7 @@ Key points:
word-check tool through `Registry.Lookup`. word-check tool through `Registry.Lookup`.
- **Dictionary versioning — pin per game.** A game records the `dict_version` it - **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 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 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 image / a volume mounted at the dictionary directory. (A future split of
the solver into engine + dictionary generator with versioned artifacts is 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 - **Word-check tool**: unlimited dictionary lookups against the game's pinned
dictionary; each result offers a **complaint** (complainant, game, variant, dictionary; each result offers a **complaint** (complainant, game, variant,
dict_version, word, the disputed result, an optional note) that lands in an 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 ## 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; emits a **match-found** notification (§10), delivered over the live stream;
`Poll` remains as a fallback for a client that is not currently streaming. `Poll` remains as a fallback for a client that is not currently streaming.
- **Friends**: a **request → accept** graph (one `friendships` table) — add by - **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 cancelling removes the pending request; blocking someone severs an existing
friendship. friendship.
- **Block**: two independent **global** account toggles (`block_chat`, - **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 (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 — robot's sleep; user-editable), the daily **away window** and the block toggles —
all editable through `account.UpdateProfile`. Linked platform accounts and merge all editable through `account.UpdateProfile`. Linked platform accounts and merge
are Stage 10. are Stage 11.
## 9. Persistence ## 9. Persistence
@@ -337,7 +348,7 @@ does not cover.
## 10. Notifications ## 10. Notifications
Two channels: the **in-app live stream** (delivered from Stage 6) and 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 The backend emits notification intents through an in-process hub
(`internal/notify`, a `Publisher` seam installed on the game, social and lobby (`internal/notify`, a `Publisher` seam installed on the game, social and lobby
services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`, 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 (from the social service), and **match-found** (from the matchmaker, §8). Event
payloads are FlatBuffers-encoded by the backend and forwarded verbatim. A client 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 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 session-revocation events and cursor-based stream resume are deferred
(single-instance MVP). (single-instance MVP).
+13 -3
View File
@@ -9,6 +9,16 @@ the detail is authored.
## Domains ## 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)* ### Identity & sessions *(Stage 1 / 6)*
A player arrives from a platform (Telegram first), via email login, or as an 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 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 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 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 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)* ### Accounts, linking & merge *(Stage 1 / 10)*
First platform contact auto-provisions a durable account. From the profile a 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, 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 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 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)* ### History & statistics *(Stage 3)*
Finished games are archived in a dictionary-independent form and exportable to 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 game, and max points for a single move (the best play, which already includes
every word it formed plus the all-tiles bonus). 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 Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary
versions, and inspects users/games. 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)* ### Личность и сессии *(Stage 1 / 6)*
Игрок приходит с платформы (сначала Telegram), через email-вход или как Игрок приходит с платформы (сначала Telegram), через email-вход или как
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
@@ -16,7 +26,7 @@ session-токен; backend сопоставляет его с внутренн
статистики и истории). Пока приложение открыто, клиент держит живой стрим и статистики и истории). Пока приложение открыто, клиент держит живой стрим и
получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и
найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит
позже (Stage 8). позже (Stage 9).
### Аккаунты, привязка и слияние *(Stage 1 / 10)* ### Аккаунты, привязка и слияние *(Stage 1 / 10)*
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
@@ -76,7 +86,7 @@ push доставляется через платформу.
confirm-коду: backend шлёт на почту короткий код, и после ввода email confirm-коду: backend шлёт на почту короткий код, и после ввода email
привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
слияние появятся в Stage 10. слияние появятся в Stage 11.
### История и статистика *(Stage 3)* ### История и статистика *(Stage 3)*
Завершённые партии архивируются в независимом от словаря виде и экспортируются Завершённые партии архивируются в независимом от словаря виде и экспортируются
@@ -84,6 +94,6 @@ confirm-коду: backend шлёт на почту короткий код, и
макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все
образованные им слова и бонус за все фишки). образованные им слова и бонус за все фишки).
### Администрирование *(Stage 9)* ### Администрирование *(Stage 10)*
Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями
словаря, смотрит пользователей/игры. словаря, смотрит пользователей/игры.
+1 -1
View File
@@ -2,7 +2,7 @@
// reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway // reverse proxy to the backend admin API (docs/ARCHITECTURE.md §12). The gateway
// validates the operator credential and forwards authenticated requests to // validates the operator credential and forwards authenticated requests to
// backend /api/v1/admin/*; the backend trusts the gateway on this segment. The // 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 package admin
import ( import (
+123 -5
View File
@@ -54,11 +54,12 @@ type MoveRecordResp struct {
// SeatResp is one seat's public standing. // SeatResp is one seat's public standing.
type SeatResp struct { type SeatResp struct {
Seat int `json:"seat"` Seat int `json:"seat"`
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
Score int `json:"score"` DisplayName string `json:"display_name"`
HintsUsed int `json:"hints_used"` Score int `json:"score"`
IsWinner bool `json:"is_winner"` HintsUsed int `json:"hints_used"`
IsWinner bool `json:"is_winner"`
} }
// GameResp is the shared game summary. // GameResp is the shared game summary.
@@ -189,3 +190,120 @@ func (c *Client) ChatPost(ctx context.Context, userID, gameID, body, clientIP st
map[string]string{"body": body}, &out) map[string]string{"body": body}, &out)
return out, err 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() return b.FinishedBytes()
} }
// encodeChat builds a ChatMessage payload. // buildChatMessage builds a ChatMessage table and returns its offset.
func encodeChat(c backendclient.ChatResp) []byte { func buildChatMessage(b *flatbuffers.Builder, c backendclient.ChatResp) flatbuffers.UOffsetT {
b := flatbuffers.NewBuilder(192)
id := b.CreateString(c.ID) id := b.CreateString(c.ID)
gid := b.CreateString(c.GameID) gid := b.CreateString(c.GameID)
sid := b.CreateString(c.SenderID) sid := b.CreateString(c.SenderID)
@@ -115,7 +114,103 @@ func encodeChat(c backendclient.ChatResp) []byte {
fb.ChatMessageAddKind(b, kind) fb.ChatMessageAddKind(b, kind)
fb.ChatMessageAddBody(b, body) fb.ChatMessageAddBody(b, body)
fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix) 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() return b.FinishedBytes()
} }
@@ -124,12 +219,14 @@ func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers
seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats)) seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats))
for i, s := range g.Seats { for i, s := range g.Seats {
aid := b.CreateString(s.AccountID) aid := b.CreateString(s.AccountID)
dname := b.CreateString(s.DisplayName)
fb.SeatViewStart(b) fb.SeatViewStart(b)
fb.SeatViewAddSeat(b, int32(s.Seat)) fb.SeatViewAddSeat(b, int32(s.Seat))
fb.SeatViewAddAccountId(b, aid) fb.SeatViewAddAccountId(b, aid)
fb.SeatViewAddScore(b, int32(s.Score)) fb.SeatViewAddScore(b, int32(s.Score))
fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed)) fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed))
fb.SeatViewAddIsWinner(b, s.IsWinner) fb.SeatViewAddIsWinner(b, s.IsWinner)
fb.SeatViewAddDisplayName(b, dname)
seatOffs[i] = fb.SeatViewEnd(b) seatOffs[i] = fb.SeatViewEnd(b)
} }
fb.GameViewStartSeatsVector(b, len(seatOffs)) fb.GameViewStartSeatsVector(b, len(seatOffs))
+169
View File
@@ -26,6 +26,17 @@ const (
MsgLobbyEnqueue = "lobby.enqueue" MsgLobbyEnqueue = "lobby.enqueue"
MsgLobbyPoll = "lobby.poll" MsgLobbyPoll = "lobby.poll"
MsgChatPost = "chat.post" 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. // 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[MsgLobbyEnqueue] = Op{Handler: enqueueHandler(backend), Auth: true}
r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true} r.ops[MsgLobbyPoll] = Op{Handler: pollHandler(backend), Auth: true}
r.ops[MsgChatPost] = Op{Handler: chatPostHandler(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 return r
} }
@@ -219,3 +241,150 @@ func decodeTiles(in *fb.SubmitPlayRequest) []backendclient.TileJSON {
} }
return tiles 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) 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; 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 { table SeatView {
seat:int; seat:int;
account_id:string; account_id:string;
score:int; score:int;
hints_used:int; hints_used:int;
is_winner:bool; is_winner:bool;
display_name:string;
} }
// GameView is the shared (non-private) game summary. // GameView is the shared (non-private) game summary.
@@ -143,6 +145,67 @@ table StateView {
hints_remaining:int; 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) --- // --- lobby (authenticated) ---
// EnqueueRequest joins the per-variant auto-match pool. // EnqueueRequest joins the per-variant auto-match pool.
@@ -174,6 +237,11 @@ table ChatMessage {
created_at_unix:long; created_at_unix:long;
} }
// ChatList is a game's chat history.
table ChatList {
messages:[ChatMessage];
}
// --- push event payloads --- // --- push event payloads ---
// YourTurnEvent signals that it is now the recipient's turn. // 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) 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) { func SeatViewStart(builder *flatbuffers.Builder) {
builder.StartObject(5) builder.StartObject(6)
} }
func SeatViewAddSeat(builder *flatbuffers.Builder, seat int32) { func SeatViewAddSeat(builder *flatbuffers.Builder, seat int32) {
builder.PrependInt32Slot(0, seat, 0) builder.PrependInt32Slot(0, seat, 0)
@@ -115,6 +123,9 @@ func SeatViewAddHintsUsed(builder *flatbuffers.Builder, hintsUsed int32) {
func SeatViewAddIsWinner(builder *flatbuffers.Builder, isWinner bool) { func SeatViewAddIsWinner(builder *flatbuffers.Builder, isWinner bool) {
builder.PrependBoolSlot(4, isWinner, false) 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 { func SeatViewEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject() 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", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "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:unit": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:e2e": "playwright test" "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": { "devDependencies": {
"@bufbuild/protoc-gen-es": "^2.12.0",
"@playwright/test": "^1.49.0", "@playwright/test": "^1.49.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
+94
View File
@@ -7,7 +7,23 @@ settings:
importers: 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: devDependencies:
'@bufbuild/protoc-gen-es':
specifier: ^2.12.0
version: 2.12.0(@bufbuild/protobuf@2.12.0)
'@playwright/test': '@playwright/test':
specifier: ^1.49.0 specifier: ^1.49.0
version: 1.60.0 version: 1.60.0
@@ -35,6 +51,33 @@ importers:
packages: 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': '@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -385,6 +428,11 @@ packages:
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 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': '@vitest/expect@3.2.6':
resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
@@ -506,6 +554,9 @@ packages:
picomatch: picomatch:
optional: true optional: true
flatbuffers@25.9.23:
resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
fsevents@2.3.2: fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -638,6 +689,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'} 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: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -737,6 +793,33 @@ packages:
snapshots: 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': '@esbuild/aix-ppc64@0.25.12':
optional: true optional: true
@@ -954,6 +1037,13 @@ snapshots:
'@types/trusted-types@2.0.7': {} '@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': '@vitest/expect@3.2.6':
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
@@ -1079,6 +1169,8 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
flatbuffers@25.9.23: {}
fsevents@2.3.2: fsevents@2.3.2:
optional: true optional: true
@@ -1226,6 +1318,8 @@ snapshots:
tinyspy@4.0.4: {} tinyspy@4.0.4: {}
typescript@5.4.5: {}
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@6.21.0: {} 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 // The real Connect-RPC + FlatBuffers transport. Every unary op rides the single
// codegen task. Until then, selecting a non-mock mode surfaces a clear error instead // Execute envelope (message_type + FlatBuffers payload); the live stream is
// of failing silently. The mock (lib/mock) backs `pnpm start`. // 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 { Code, ConnectError, createClient } from '@connectrpc/connect';
import { GatewayError } from './client'; 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 { return {
setToken: () => {}, setToken(t) {
authGuest: ni, token = t;
authEmailRequest: ni, },
authEmailLogin: ni,
profileGet: ni, async authGuest(locale) {
gamesList: ni, return codec.decodeSession(await exec('auth.guest', codec.encodeGuestLogin(locale ?? '')));
lobbyEnqueue: ni, },
lobbyPoll: ni, async authEmailRequest(email) {
gameState: ni, await exec('auth.email.request', codec.encodeEmailRequest(email));
gameHistory: ni, },
submitPlay: ni, async authEmailLogin(email, code) {
pass: ni, return codec.decodeSession(await exec('auth.email.login', codec.encodeEmailLogin(email, code)));
exchange: ni, },
resign: ni,
hint: ni, async profileGet() {
evaluate: ni, return codec.decodeProfile(await exec('profile.get', codec.empty()));
checkWord: ni, },
complaint: ni, async gamesList() {
chatPost: ni, return codec.decodeGameList(await exec('games.list', codec.empty()));
chatList: ni, },
nudge: ni,
subscribe: ni, 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',
},
});