Stage 7: UI playable slice + remaining edge ops (#7)
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
# The Playwright system libraries are provisioned once on the runner host
|
||||||
|
# (`sudo npx playwright@<version> install-deps chromium`), so the job needs no
|
||||||
|
# apt and no sudo: it only downloads the browser binary into the runner cache
|
||||||
|
# (persisted by the host executor) and runs the smoke. The timeouts guard
|
||||||
|
# against a future hang. Keep this in lockstep with @playwright/test in
|
||||||
|
# package.json — re-run install-deps on the host after a major bump.
|
||||||
|
- name: Install Playwright browser
|
||||||
|
run: pnpm exec playwright install chromium
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
- name: E2E smoke (mock)
|
||||||
|
run: pnpm run test:e2e
|
||||||
|
timeout-minutes: 5
|
||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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 2–4 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.5–0.7 MB DAWGs are regenerated wholesale and bloat git
|
git submodule (the ~0.5–0.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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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) разбирает жалобы на слова, управляет версиями
|
||||||
словаря, смотрит пользователей/игры.
|
словаря, смотрит пользователей/игры.
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.svelte-kit/
|
||||||
|
*.tsbuildinfo
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Do not run an implicit install before `pnpm run <script>` — CI and dev install
|
||||||
|
# explicitly. esbuild's platform binary ships as an optional dependency, so its
|
||||||
|
# build script is not required for Vite to work; it is allow-listed in package.json
|
||||||
|
# (pnpm.onlyBuiltDependencies) for a clean fresh install.
|
||||||
|
verify-deps-before-run=false
|
||||||
@@ -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)
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
// The playable-slice smoke against the mock transport: guest login -> lobby shows the
|
||||||
|
// seeded active game -> open it -> the board renders committed tiles -> place a rack
|
||||||
|
// tile (tap) and see the score preview.
|
||||||
|
test('guest reaches a board and previews a placement', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /guest/i }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Active games')).toBeVisible();
|
||||||
|
const activeRow = page.getByRole('button', { name: /Ann/ });
|
||||||
|
await expect(activeRow).toBeVisible();
|
||||||
|
await activeRow.click();
|
||||||
|
|
||||||
|
// Board renders, including a committed tile from the seeded HELLO play.
|
||||||
|
await expect(page.locator('[data-cell]').first()).toBeVisible();
|
||||||
|
await expect(page.locator('[data-cell] .letter', { hasText: 'H' }).first()).toBeVisible();
|
||||||
|
|
||||||
|
// Tap a rack tile, then an empty board cell -> a pending tile + score preview.
|
||||||
|
const rackTile = page.locator('.rack .tile').first();
|
||||||
|
await rackTile.click();
|
||||||
|
await page.locator('[data-cell]:not(.filled)').nth(30).click();
|
||||||
|
await expect(page.locator('[data-cell].pending')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.preview')).toContainText(/\d/);
|
||||||
|
|
||||||
|
// The contextual MakeMove control appears once a tile is pending.
|
||||||
|
await expect(page.getByRole('button', { name: /make move/i })).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- user-scalable=no: the board owns zoom; we do not want the browser's pinch
|
||||||
|
to fight our two-state zoom. viewport-fit=cover for native (Capacitor). -->
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<title>Scrabble</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "scrabble-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Scrabble game client (plain Svelte 5 + Vite). Talks to the gateway over Connect-RPC + FlatBuffers.",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"start": "vite --mode mock",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"codegen": "rm -rf src/gen && flatc --ts -o src/gen/fbs ../pkg/fbs/scrabble.fbs && buf generate ../gateway --template buf.gen.yaml",
|
||||||
|
"test:unit": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/protobuf": "^2.12.0",
|
||||||
|
"@connectrpc/connect": "^2.1.0",
|
||||||
|
"@connectrpc/connect-web": "^2.1.0",
|
||||||
|
"flatbuffers": "^25.9.23"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@bufbuild/protoc-gen-es": "^2.12.0",
|
||||||
|
"@playwright/test": "^1.49.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"svelte": "^5.15.0",
|
||||||
|
"svelte-check": "^4.1.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
// Hermetic e2e: Playwright boots the Vite dev server in `mock` mode (the in-memory
|
||||||
|
// fake transport), so the smoke needs no backend/gateway/Postgres.
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 1 : 0,
|
||||||
|
reporter: 'list',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:4173',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'pnpm exec vite --mode mock --port 4173 --strictPort',
|
||||||
|
url: 'http://localhost:4173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 60_000,
|
||||||
|
},
|
||||||
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||||
|
});
|
||||||
Generated
+1410
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
# pnpm 11 records build-script approval here. esbuild's postinstall materialises
|
||||||
|
# its CLI shim; the platform binary itself ships as an optional dependency.
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { app, bootstrap } from './lib/app.svelte';
|
||||||
|
import { router } from './lib/router.svelte';
|
||||||
|
import { t } from './lib/i18n/index.svelte';
|
||||||
|
import Toast from './components/Toast.svelte';
|
||||||
|
import Login from './screens/Login.svelte';
|
||||||
|
import Lobby from './screens/Lobby.svelte';
|
||||||
|
import NewGame from './screens/NewGame.svelte';
|
||||||
|
import Profile from './screens/Profile.svelte';
|
||||||
|
import Settings from './screens/Settings.svelte';
|
||||||
|
import About from './screens/About.svelte';
|
||||||
|
import Game from './game/Game.svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void bootstrap();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !app.ready}
|
||||||
|
<div class="splash">{t('common.loading')}</div>
|
||||||
|
{:else if router.route.name === 'login'}
|
||||||
|
<Login />
|
||||||
|
{:else if router.route.name === 'new'}
|
||||||
|
<NewGame />
|
||||||
|
{:else if router.route.name === 'game'}
|
||||||
|
<Game id={router.route.params.id} />
|
||||||
|
{:else if router.route.name === 'profile'}
|
||||||
|
<Profile />
|
||||||
|
{:else if router.route.name === 'settings'}
|
||||||
|
<Settings />
|
||||||
|
{:else if router.route.name === 'about'}
|
||||||
|
<About />
|
||||||
|
{:else}
|
||||||
|
<Lobby />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.splash {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Design tokens — pure CSS custom properties, no framework, no image/font/SVG
|
||||||
|
* assets. Light is the default; dark is applied either by the OS
|
||||||
|
* (prefers-color-scheme) or by an explicit [data-theme] set from Settings. A
|
||||||
|
* Telegram Mini App can override these same variables at runtime from
|
||||||
|
* WebApp.themeParams (see lib/theme — SDK wiring lands in the Telegram stage), so
|
||||||
|
* the whole UI re-themes without touching components.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--bg: #f3f4f6;
|
||||||
|
--bg-elev: #ffffff;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #eef0f3;
|
||||||
|
--text: #14181f;
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
--border: #d8dce2;
|
||||||
|
--accent: #2f6df6;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--danger: #d6453d;
|
||||||
|
--ok: #1f9d57;
|
||||||
|
--warn: #c9881b;
|
||||||
|
|
||||||
|
/* board + tiles (all drawn with CSS primitives) */
|
||||||
|
--board-bg: #cdd6cf;
|
||||||
|
--cell-bg: #e7ece8;
|
||||||
|
--cell-line: #b6c0b8;
|
||||||
|
--tile-bg: #f4e2b8;
|
||||||
|
--tile-edge: #d8c190;
|
||||||
|
--tile-text: #2a2113;
|
||||||
|
--tile-pending: #ffe7a3;
|
||||||
|
--tile-recent: #fff6d8;
|
||||||
|
--prem-tw: #e06a5b; /* triple word */
|
||||||
|
--prem-dw: #efa6a0; /* double word + centre */
|
||||||
|
--prem-tl: #4f8fd6; /* triple letter */
|
||||||
|
--prem-dl: #a8cdec; /* double letter */
|
||||||
|
--prem-text: #2a2113;
|
||||||
|
|
||||||
|
/* shape + type */
|
||||||
|
--radius: 10px;
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--gap: 8px;
|
||||||
|
--pad: 12px;
|
||||||
|
--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||||
|
"Noto Sans", "Liberation Sans", sans-serif;
|
||||||
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--bg: #0f1115;
|
||||||
|
--bg-elev: #171a21;
|
||||||
|
--surface: #171a21;
|
||||||
|
--surface-2: #1f242d;
|
||||||
|
--text: #e7eaf0;
|
||||||
|
--text-muted: #9aa3b2;
|
||||||
|
--border: #2a313c;
|
||||||
|
--accent: #5b8cff;
|
||||||
|
--accent-text: #0b0e13;
|
||||||
|
--danger: #f0635a;
|
||||||
|
--ok: #44c87f;
|
||||||
|
--warn: #e0a93a;
|
||||||
|
|
||||||
|
--board-bg: #2a3330;
|
||||||
|
--cell-bg: #222a27;
|
||||||
|
--cell-line: #38433d;
|
||||||
|
--tile-bg: #d9c79a;
|
||||||
|
--tile-edge: #b6a473;
|
||||||
|
--tile-text: #20190d;
|
||||||
|
--tile-pending: #f0d98f;
|
||||||
|
--tile-recent: #4a4636;
|
||||||
|
--prem-tw: #b1493d;
|
||||||
|
--prem-dw: #8c5450;
|
||||||
|
--prem-tl: #34608f;
|
||||||
|
--prem-dl: #3b5a72;
|
||||||
|
--prem-text: #e7eaf0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Explicit dark chosen in Settings (overrides OS preference). */
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #0f1115;
|
||||||
|
--bg-elev: #171a21;
|
||||||
|
--surface: #171a21;
|
||||||
|
--surface-2: #1f242d;
|
||||||
|
--text: #e7eaf0;
|
||||||
|
--text-muted: #9aa3b2;
|
||||||
|
--border: #2a313c;
|
||||||
|
--accent: #5b8cff;
|
||||||
|
--accent-text: #0b0e13;
|
||||||
|
--danger: #f0635a;
|
||||||
|
--ok: #44c87f;
|
||||||
|
--warn: #e0a93a;
|
||||||
|
--board-bg: #2a3330;
|
||||||
|
--cell-bg: #222a27;
|
||||||
|
--cell-line: #38433d;
|
||||||
|
--tile-bg: #d9c79a;
|
||||||
|
--tile-edge: #b6a473;
|
||||||
|
--tile-text: #20190d;
|
||||||
|
--tile-pending: #f0d98f;
|
||||||
|
--tile-recent: #4a4636;
|
||||||
|
--prem-tw: #b1493d;
|
||||||
|
--prem-dw: #8c5450;
|
||||||
|
--prem-tl: #34608f;
|
||||||
|
--prem-dl: #3b5a72;
|
||||||
|
--prem-text: #e7eaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
/* never let the page scroll/zoom out from under the board */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reduce-motion * {
|
||||||
|
animation-duration: 0.001ms !important;
|
||||||
|
transition-duration: 0.001ms !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { navigate } from '../lib/router.svelte';
|
||||||
|
|
||||||
|
let { title, back, menu }: { title: string; back?: string; menu?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="topbar">
|
||||||
|
{#if back}
|
||||||
|
<button class="icon" onclick={() => back && navigate(back)} aria-label="Back">◄</button>
|
||||||
|
{:else}
|
||||||
|
<span class="spacer"></span>
|
||||||
|
{/if}
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<div class="end">{#if menu}{@render menu()}{/if}</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--gap);
|
||||||
|
padding: 10px var(--pad);
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.icon,
|
||||||
|
.spacer,
|
||||||
|
.end {
|
||||||
|
width: 40px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.end {
|
||||||
|
width: auto;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.icon:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
title = '',
|
||||||
|
onclose,
|
||||||
|
children,
|
||||||
|
}: { title?: string; onclose?: () => void; children?: Snippet } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="backdrop" onclick={() => onclose?.()}>
|
||||||
|
<div class="sheet" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||||
|
{#if title}<h2>{title}</h2>{/if}
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
.sheet {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: var(--pad);
|
||||||
|
width: min(94vw, 420px);
|
||||||
|
max-height: 86vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { app } from '../lib/app.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if app.toast}
|
||||||
|
<div class="toast {app.toast.kind}" role="status" aria-live="polite">{app.toast.text}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 24px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: min(92vw, 420px);
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
z-index: 50;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { BoardCell } from '../lib/board';
|
||||||
|
import type { Premium } from '../lib/premiums';
|
||||||
|
import { tileValue } from '../lib/premiums';
|
||||||
|
import type { Variant } from '../lib/model';
|
||||||
|
|
||||||
|
let {
|
||||||
|
board,
|
||||||
|
premium,
|
||||||
|
pending,
|
||||||
|
recent,
|
||||||
|
centre,
|
||||||
|
zoomed,
|
||||||
|
variant,
|
||||||
|
oncell,
|
||||||
|
ontogglezoom,
|
||||||
|
}: {
|
||||||
|
board: (BoardCell | null)[][];
|
||||||
|
premium: Premium[][];
|
||||||
|
pending: Map<string, { letter: string; blank: boolean }>;
|
||||||
|
recent: Set<string>;
|
||||||
|
centre: { row: number; col: number };
|
||||||
|
zoomed: boolean;
|
||||||
|
variant: Variant;
|
||||||
|
oncell: (row: number, col: number) => void;
|
||||||
|
ontogglezoom: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const premClass: Record<Premium, string> = {
|
||||||
|
'': '',
|
||||||
|
TW: 'tw',
|
||||||
|
DW: 'dw',
|
||||||
|
TL: 'tl',
|
||||||
|
DL: 'dl',
|
||||||
|
};
|
||||||
|
const premLabel: Record<Premium, string> = { '': '', TW: '3W', DW: '2W', TL: '3L', DL: '2L' };
|
||||||
|
|
||||||
|
// Double-tap toggles zoom.
|
||||||
|
let lastTap = 0;
|
||||||
|
function onTap(row: number, col: number) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastTap < 300) {
|
||||||
|
ontogglezoom();
|
||||||
|
lastTap = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastTap = now;
|
||||||
|
oncell(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal pinch: two pointers spreading apart zoom in, pinching together zoom out.
|
||||||
|
const pts = new Map<number, { x: number; y: number }>();
|
||||||
|
let startDist = 0;
|
||||||
|
function dist(): number {
|
||||||
|
const p = [...pts.values()];
|
||||||
|
if (p.length < 2) return 0;
|
||||||
|
return Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
|
||||||
|
}
|
||||||
|
function onPointerDown(e: PointerEvent) {
|
||||||
|
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (pts.size === 2) startDist = dist();
|
||||||
|
}
|
||||||
|
function onPointerMove(e: PointerEvent) {
|
||||||
|
if (!pts.has(e.pointerId)) return;
|
||||||
|
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (pts.size === 2 && startDist > 0) {
|
||||||
|
const d = dist();
|
||||||
|
if (!zoomed && d > startDist * 1.25) {
|
||||||
|
ontogglezoom();
|
||||||
|
startDist = 0;
|
||||||
|
} else if (zoomed && d < startDist * 0.8) {
|
||||||
|
ontogglezoom();
|
||||||
|
startDist = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onPointerUp(e: PointerEvent) {
|
||||||
|
pts.delete(e.pointerId);
|
||||||
|
if (pts.size < 2) startDist = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function key(r: number, c: number): string {
|
||||||
|
return `${r},${c}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="viewport"
|
||||||
|
class:zoomed
|
||||||
|
onpointerdown={onPointerDown}
|
||||||
|
onpointermove={onPointerMove}
|
||||||
|
onpointerup={onPointerUp}
|
||||||
|
onpointercancel={onPointerUp}
|
||||||
|
>
|
||||||
|
<div class="grid" class:zoomed>
|
||||||
|
{#each board as rowCells, r (r)}
|
||||||
|
{#each rowCells as cell, c (c)}
|
||||||
|
{@const p = pending.get(key(r, c))}
|
||||||
|
{@const letter = cell?.letter ?? p?.letter ?? ''}
|
||||||
|
{@const blank = cell?.blank ?? p?.blank ?? false}
|
||||||
|
<button
|
||||||
|
class="cell {premClass[premium[r][c]]}"
|
||||||
|
class:filled={!!cell}
|
||||||
|
class:pending={!!p && !cell}
|
||||||
|
class:recent={recent.has(key(r, c))}
|
||||||
|
data-cell
|
||||||
|
data-row={r}
|
||||||
|
data-col={c}
|
||||||
|
onclick={() => onTap(r, c)}
|
||||||
|
>
|
||||||
|
{#if letter}
|
||||||
|
<span class="letter">{letter}</span>
|
||||||
|
{#if !blank}<span class="val">{tileValue(variant, letter)}</span>{/if}
|
||||||
|
{:else if r === centre.row && c === centre.col}
|
||||||
|
<span class="star">★</span>
|
||||||
|
{:else if premLabel[premium[r][c]]}
|
||||||
|
<span class="plabel">{premLabel[premium[r][c]]}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.viewport {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--board-bg);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.viewport.zoomed {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(15, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.grid.zoomed {
|
||||||
|
grid-template-columns: repeat(15, 2.6rem);
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.cell {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--cell-bg);
|
||||||
|
color: var(--prem-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cell.tw {
|
||||||
|
background: var(--prem-tw);
|
||||||
|
}
|
||||||
|
.cell.dw {
|
||||||
|
background: var(--prem-dw);
|
||||||
|
}
|
||||||
|
.cell.tl {
|
||||||
|
background: var(--prem-tl);
|
||||||
|
}
|
||||||
|
.cell.dl {
|
||||||
|
background: var(--prem-dl);
|
||||||
|
}
|
||||||
|
.cell.filled,
|
||||||
|
.cell.pending {
|
||||||
|
background: var(--tile-bg);
|
||||||
|
color: var(--tile-text);
|
||||||
|
box-shadow: inset 0 -2px 0 var(--tile-edge);
|
||||||
|
}
|
||||||
|
.cell.pending {
|
||||||
|
background: var(--tile-pending);
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
.cell.recent {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 -2px 0 var(--tile-edge),
|
||||||
|
0 0 0 2px var(--warn);
|
||||||
|
}
|
||||||
|
.letter {
|
||||||
|
font-size: 1.05em;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.grid:not(.zoomed) .letter {
|
||||||
|
font-size: 2.6vw;
|
||||||
|
}
|
||||||
|
.val {
|
||||||
|
position: absolute;
|
||||||
|
right: 1px;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: 0.55em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.plabel {
|
||||||
|
opacity: 0.85;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.star {
|
||||||
|
font-size: 1.1em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ChatMessage } from '../lib/model';
|
||||||
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
messages,
|
||||||
|
myId,
|
||||||
|
busy,
|
||||||
|
onsend,
|
||||||
|
onnudge,
|
||||||
|
}: {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
myId: string;
|
||||||
|
busy: boolean;
|
||||||
|
onsend: (text: string) => void;
|
||||||
|
onnudge: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let text = $state('');
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
const v = text.trim();
|
||||||
|
if (!v) return;
|
||||||
|
onsend(v);
|
||||||
|
text = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chat">
|
||||||
|
<div class="list">
|
||||||
|
{#if messages.length === 0}
|
||||||
|
<p class="empty">{t('chat.empty')}</p>
|
||||||
|
{/if}
|
||||||
|
{#each messages as m (m.id)}
|
||||||
|
{#if m.kind === 'nudge'}
|
||||||
|
<div class="note">{t('chat.nudge')}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="msg" class:mine={m.senderId === myId}>{m.body}</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input
|
||||||
|
maxlength="60"
|
||||||
|
placeholder={t('chat.placeholder')}
|
||||||
|
bind:value={text}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && send()}
|
||||||
|
/>
|
||||||
|
<button onclick={send} disabled={busy}>{t('chat.send')}</button>
|
||||||
|
<button class="nudge" onclick={onnudge} disabled={busy}>{t('chat.nudge')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
height: 56vh;
|
||||||
|
}
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
align-self: flex-start;
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 7px 11px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
.msg.mine {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.input input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.input button {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EvalResult } from '../lib/model';
|
||||||
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
preview,
|
||||||
|
hints,
|
||||||
|
busy,
|
||||||
|
ambiguous,
|
||||||
|
dir,
|
||||||
|
ondraw,
|
||||||
|
onskip,
|
||||||
|
onshuffle,
|
||||||
|
onhint,
|
||||||
|
ondir,
|
||||||
|
}: {
|
||||||
|
preview: EvalResult | null;
|
||||||
|
hints: number;
|
||||||
|
busy: boolean;
|
||||||
|
ambiguous: boolean;
|
||||||
|
dir: 'H' | 'V';
|
||||||
|
ondraw: () => void;
|
||||||
|
onskip: () => void;
|
||||||
|
onshuffle: () => void;
|
||||||
|
onhint: () => void;
|
||||||
|
ondir: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="preview">
|
||||||
|
{#if preview}
|
||||||
|
{#if preview.legal}
|
||||||
|
<span class="ok">{t('game.preview', { n: preview.score })}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="bad">{t('game.previewIllegal')}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if ambiguous}
|
||||||
|
<button class="dir" onclick={ondir} title="direction">{dir === 'H' ? '↔' : '↕'}</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button onclick={ondraw} disabled={busy}>{t('game.draw')}</button>
|
||||||
|
<button onclick={onskip} disabled={busy}>{t('game.skip')}</button>
|
||||||
|
<button onclick={onshuffle} disabled={busy}>{t('game.shuffle')}</button>
|
||||||
|
<button class="hint" onclick={onhint} disabled={busy || hints <= 0}>
|
||||||
|
{t('game.hint')}{hints > 0 ? ` (${hints})` : ''}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
min-height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
.bad {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.dir {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 34px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.row button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 11px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.row button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,741 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import Header from '../components/Header.svelte';
|
||||||
|
import Modal from '../components/Modal.svelte';
|
||||||
|
import Board from './Board.svelte';
|
||||||
|
import Rack from './Rack.svelte';
|
||||||
|
import MakeMove from './MakeMove.svelte';
|
||||||
|
import Controls from './Controls.svelte';
|
||||||
|
import Chat from './Chat.svelte';
|
||||||
|
import { gateway } from '../lib/gateway';
|
||||||
|
import { app, handleError, showToast } from '../lib/app.svelte';
|
||||||
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
|
import type { ChatMessage, Direction, EvalResult, MoveRecord, StateView } from '../lib/model';
|
||||||
|
import { lastPlayTiles, replay } from '../lib/board';
|
||||||
|
import { alphabet, centre, premiumGrid } from '../lib/premiums';
|
||||||
|
import {
|
||||||
|
BLANK,
|
||||||
|
direction,
|
||||||
|
newPlacement,
|
||||||
|
place,
|
||||||
|
rackView,
|
||||||
|
recallAt,
|
||||||
|
reset,
|
||||||
|
toSubmit,
|
||||||
|
type Placement,
|
||||||
|
} from '../lib/placement';
|
||||||
|
|
||||||
|
let { id }: { id: string } = $props();
|
||||||
|
|
||||||
|
let view = $state<StateView | null>(null);
|
||||||
|
let moves = $state<MoveRecord[]>([]);
|
||||||
|
let placement = $state<Placement>(newPlacement([]));
|
||||||
|
let preview = $state<EvalResult | null>(null);
|
||||||
|
let dirOverride = $state<Direction | undefined>(undefined);
|
||||||
|
let busy = $state(false);
|
||||||
|
let zoomed = $state(false);
|
||||||
|
let selected = $state<number | null>(null);
|
||||||
|
let panel = $state<'none' | 'chat' | 'history'>('none');
|
||||||
|
let menuOpen = $state(false);
|
||||||
|
let blankPrompt = $state<{ rackIndex: number; row: number; col: number } | null>(null);
|
||||||
|
let exchangeOpen = $state(false);
|
||||||
|
let exchangeSel = $state<number[]>([]);
|
||||||
|
let checkOpen = $state(false);
|
||||||
|
let checkWord = $state('');
|
||||||
|
let checkResult = $state<{ word: string; legal: boolean } | null>(null);
|
||||||
|
let resignOpen = $state(false);
|
||||||
|
let messages = $state<ChatMessage[]>([]);
|
||||||
|
let drag = $state<{ letter: string; blank: boolean; x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
const variant = $derived(view?.game.variant ?? 'english');
|
||||||
|
const board = $derived(replay(moves));
|
||||||
|
const premium = $derived(premiumGrid(variant));
|
||||||
|
const ctr = $derived(centre(variant));
|
||||||
|
const pendingMap = $derived(
|
||||||
|
new Map(placement.pending.map((p) => [`${p.row},${p.col}`, { letter: p.letter, blank: p.blank }])),
|
||||||
|
);
|
||||||
|
const recent = $derived(new Set(lastPlayTiles(moves).map((tt) => `${tt.row},${tt.col}`)));
|
||||||
|
const slots = $derived(rackView(placement));
|
||||||
|
const isMyTurn = $derived(
|
||||||
|
!!view && view.game.status === 'active' && view.game.toMove === view.seat,
|
||||||
|
);
|
||||||
|
const gameOver = $derived(!!view && view.game.status !== 'active');
|
||||||
|
const dir = $derived(dirOverride ?? direction(placement) ?? 'H');
|
||||||
|
const ambiguous = $derived(placement.pending.length === 1);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const [st, hist] = await Promise.all([gateway.gameState(id), gateway.gameHistory(id)]);
|
||||||
|
view = st;
|
||||||
|
moves = hist.moves;
|
||||||
|
placement = newPlacement(st.rack);
|
||||||
|
preview = null;
|
||||||
|
selected = null;
|
||||||
|
dirOverride = undefined;
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChat() {
|
||||||
|
try {
|
||||||
|
messages = await gateway.chatList(id);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const e = app.lastEvent;
|
||||||
|
if (!e) return;
|
||||||
|
if ((e.kind === 'opponent_moved' || e.kind === 'your_turn') && e.gameId === id) void load();
|
||||||
|
else if (e.kind === 'chat_message' && e.message.gameId === id && panel === 'chat') void loadChat();
|
||||||
|
else if (e.kind === 'nudge' && e.gameId === id && panel === 'chat') void loadChat();
|
||||||
|
});
|
||||||
|
|
||||||
|
function isCoarse(): boolean {
|
||||||
|
return typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tile placement: pointer drag + tap, both feeding the placement model ---
|
||||||
|
let downInfo: { index: number; x0: number; y0: number } | null = null;
|
||||||
|
let dragMoved = false;
|
||||||
|
let swallowClick = false;
|
||||||
|
|
||||||
|
function onRackDown(e: PointerEvent, index: number) {
|
||||||
|
if (!isMyTurn || busy) return;
|
||||||
|
downInfo = { index, x0: e.clientX, y0: e.clientY };
|
||||||
|
dragMoved = false;
|
||||||
|
window.addEventListener('pointermove', onWinMove);
|
||||||
|
window.addEventListener('pointerup', onWinUp);
|
||||||
|
}
|
||||||
|
function onWinMove(e: PointerEvent) {
|
||||||
|
if (!downInfo) return;
|
||||||
|
if (!dragMoved && Math.hypot(e.clientX - downInfo.x0, e.clientY - downInfo.y0) > 6) {
|
||||||
|
dragMoved = true;
|
||||||
|
const slot = placement.rack[downInfo.index];
|
||||||
|
drag = { letter: slot, blank: slot === BLANK, x: e.clientX, y: e.clientY };
|
||||||
|
if (isCoarse() && !zoomed) zoomed = true; // auto zoom-in on touch placement
|
||||||
|
}
|
||||||
|
if (drag) drag = { ...drag, x: e.clientX, y: e.clientY };
|
||||||
|
}
|
||||||
|
function onWinUp(e: PointerEvent) {
|
||||||
|
window.removeEventListener('pointermove', onWinMove);
|
||||||
|
window.removeEventListener('pointerup', onWinUp);
|
||||||
|
const di = downInfo;
|
||||||
|
downInfo = null;
|
||||||
|
if (drag && dragMoved && di) {
|
||||||
|
const el = (document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null)?.closest(
|
||||||
|
'[data-cell]',
|
||||||
|
) as HTMLElement | null;
|
||||||
|
drag = null;
|
||||||
|
if (el?.dataset.row && el.dataset.col) {
|
||||||
|
attemptPlace(di.index, Number(el.dataset.row), Number(el.dataset.col));
|
||||||
|
}
|
||||||
|
swallowClick = true;
|
||||||
|
setTimeout(() => (swallowClick = false), 60);
|
||||||
|
} else if (di) {
|
||||||
|
selected = selected === di.index ? null : di.index;
|
||||||
|
drag = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('pointermove', onWinMove);
|
||||||
|
window.removeEventListener('pointerup', onWinUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onCell(row: number, col: number) {
|
||||||
|
if (swallowClick) return;
|
||||||
|
if (pendingMap.has(`${row},${col}`)) {
|
||||||
|
placement = recallAt(placement, row, col);
|
||||||
|
recompute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected != null) {
|
||||||
|
attemptPlace(selected, row, col);
|
||||||
|
selected = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attemptPlace(index: number, row: number, col: number) {
|
||||||
|
if (board[row]?.[col]) return;
|
||||||
|
if (pendingMap.has(`${row},${col}`)) return;
|
||||||
|
if (placement.rack[index] === BLANK) {
|
||||||
|
blankPrompt = { rackIndex: index, row, col };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
placement = place(placement, index, row, col);
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseBlank(letter: string) {
|
||||||
|
if (!blankPrompt) return;
|
||||||
|
placement = place(placement, blankPrompt.rackIndex, blankPrompt.row, blankPrompt.col, letter);
|
||||||
|
blankPrompt = null;
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
let previewTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
function recompute() {
|
||||||
|
preview = null;
|
||||||
|
if (previewTimer) clearTimeout(previewTimer);
|
||||||
|
const sub = toSubmit(placement, dirOverride);
|
||||||
|
if (!sub) return;
|
||||||
|
previewTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
preview = await gateway.evaluate(id, sub.dir, sub.tiles);
|
||||||
|
} catch {
|
||||||
|
/* preview is best-effort */
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commit() {
|
||||||
|
const sub = toSubmit(placement, dirOverride);
|
||||||
|
if (!sub) return;
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
await gateway.submitPlay(id, sub.dir, sub.tiles);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPlacement() {
|
||||||
|
placement = reset(placement);
|
||||||
|
preview = null;
|
||||||
|
selected = null;
|
||||||
|
dirOverride = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doPass() {
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
await gateway.pass(id);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doResign() {
|
||||||
|
resignOpen = false;
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
await gateway.resign(id);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doHint() {
|
||||||
|
try {
|
||||||
|
const h = await gateway.hint(id);
|
||||||
|
const word = h.move.words[0] ?? h.move.tiles.map((x) => x.letter).join('');
|
||||||
|
showToast(t('game.hintShown', { word, n: h.move.score }));
|
||||||
|
if (view) view = { ...view, hintsRemaining: h.hintsRemaining };
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shuffle() {
|
||||||
|
if (placement.pending.length > 0) return;
|
||||||
|
const r = [...placement.rack];
|
||||||
|
for (let i = r.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[r[i], r[j]] = [r[j], r[i]];
|
||||||
|
}
|
||||||
|
placement = newPlacement(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDir() {
|
||||||
|
dirOverride = dir === 'H' ? 'V' : 'H';
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExchange() {
|
||||||
|
menuOpen = false;
|
||||||
|
resetPlacement();
|
||||||
|
exchangeSel = [];
|
||||||
|
exchangeOpen = true;
|
||||||
|
}
|
||||||
|
function toggleExch(i: number) {
|
||||||
|
exchangeSel = exchangeSel.includes(i) ? exchangeSel.filter((x) => x !== i) : [...exchangeSel, i];
|
||||||
|
}
|
||||||
|
async function doExchange() {
|
||||||
|
if (!view || exchangeSel.length === 0) return;
|
||||||
|
const tiles = exchangeSel.map((i) => view!.rack[i]);
|
||||||
|
exchangeOpen = false;
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
await gateway.exchange(id, tiles);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCheck() {
|
||||||
|
menuOpen = false;
|
||||||
|
checkWord = '';
|
||||||
|
checkResult = null;
|
||||||
|
checkOpen = true;
|
||||||
|
}
|
||||||
|
async function runCheck() {
|
||||||
|
const w = checkWord.trim();
|
||||||
|
if (!w) return;
|
||||||
|
try {
|
||||||
|
checkResult = await gateway.checkWord(id, w);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function complain() {
|
||||||
|
if (!checkResult) return;
|
||||||
|
try {
|
||||||
|
await gateway.complaint(id, checkResult.word, '');
|
||||||
|
showToast(t('game.complaintSent'));
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChat() {
|
||||||
|
menuOpen = false;
|
||||||
|
panel = 'chat';
|
||||||
|
void loadChat();
|
||||||
|
}
|
||||||
|
async function sendChat(text: string) {
|
||||||
|
try {
|
||||||
|
const m = await gateway.chatPost(id, text);
|
||||||
|
messages = [...messages, m];
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function nudge() {
|
||||||
|
try {
|
||||||
|
const m = await gateway.nudge(id);
|
||||||
|
messages = [...messages, m];
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultText(): string {
|
||||||
|
if (!view) return '';
|
||||||
|
const me = view.game.seats[view.seat];
|
||||||
|
if (me?.isWinner) return t('game.won');
|
||||||
|
return view.game.seats.some((s) => s.isWinner) ? t('game.lost') : t('game.tied');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Header title={t('app.title')} back="/">
|
||||||
|
{#snippet menu()}
|
||||||
|
<button class="icon" onclick={() => (menuOpen = !menuOpen)} aria-label="Menu">≡</button>
|
||||||
|
{#if menuOpen}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="backdrop" onclick={() => (menuOpen = false)}></div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button onclick={() => { menuOpen = false; panel = 'history'; }}>{t('game.history')}</button>
|
||||||
|
<button onclick={openChat}>{t('game.chat')}</button>
|
||||||
|
<button onclick={openCheck}>{t('game.checkWord')}</button>
|
||||||
|
<button onclick={() => { menuOpen = false; resignOpen = true; }}>{t('game.dropGame')}</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
{#if view}
|
||||||
|
<div class="scoreboard">
|
||||||
|
{#each view.game.seats as s (s.seat)}
|
||||||
|
<div class="seat" class:turn={view.game.toMove === s.seat && !gameOver} class:win={s.isWinner}>
|
||||||
|
<div class="nm">{s.accountId === app.session?.userId ? t('common.you') : s.displayName}</div>
|
||||||
|
<div class="sc">{s.score}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="boardwrap">
|
||||||
|
<Board
|
||||||
|
{board}
|
||||||
|
{premium}
|
||||||
|
pending={pendingMap}
|
||||||
|
{recent}
|
||||||
|
centre={ctr}
|
||||||
|
{zoomed}
|
||||||
|
{variant}
|
||||||
|
oncell={onCell}
|
||||||
|
ontogglezoom={() => (zoomed = !zoomed)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<span>{t('game.bag', { n: view.bagLen })}</span>
|
||||||
|
{#if gameOver}
|
||||||
|
<strong class="over">{t('game.over')} — {resultText()}</strong>
|
||||||
|
{:else}
|
||||||
|
<span class="turn-ind">{isMyTurn ? t('game.yourTurn') : t('game.waiting', { name: view.game.seats[view.game.toMove]?.displayName ?? '' })}</span>
|
||||||
|
{/if}
|
||||||
|
<span>{t('game.hints', { n: view.hintsRemaining })}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !gameOver}
|
||||||
|
<div class="rack-row">
|
||||||
|
<div class="rack-wrap">
|
||||||
|
<Rack {slots} {variant} {selected} ondown={onRackDown} />
|
||||||
|
</div>
|
||||||
|
{#if placement.pending.length > 0}
|
||||||
|
<MakeMove
|
||||||
|
label={t('game.makeMove')}
|
||||||
|
resetLabel={t('game.reset')}
|
||||||
|
onmake={commit}
|
||||||
|
onreset={resetPlacement}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controls
|
||||||
|
{preview}
|
||||||
|
hints={view.hintsRemaining}
|
||||||
|
busy={busy || !isMyTurn}
|
||||||
|
{ambiguous}
|
||||||
|
{dir}
|
||||||
|
ondraw={openExchange}
|
||||||
|
onskip={doPass}
|
||||||
|
onshuffle={shuffle}
|
||||||
|
onhint={doHint}
|
||||||
|
ondir={toggleDir}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="loading">{t('common.loading')}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if drag}
|
||||||
|
<div class="ghost" style="left:{drag.x}px; top:{drag.y}px">
|
||||||
|
<span>{drag.blank ? '' : drag.letter}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if blankPrompt}
|
||||||
|
<Modal title={t('game.chooseBlank')} onclose={() => (blankPrompt = null)}>
|
||||||
|
<div class="alpha">
|
||||||
|
{#each alphabet(variant) as ch (ch)}
|
||||||
|
<button onclick={() => chooseBlank(ch)}>{ch}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if exchangeOpen && view}
|
||||||
|
<Modal title={t('game.exchangeTitle')} onclose={() => (exchangeOpen = false)}>
|
||||||
|
<div class="exch">
|
||||||
|
{#each view.rack as letter, i (i)}
|
||||||
|
<button class="etile" class:sel={exchangeSel.includes(i)} onclick={() => toggleExch(i)}>
|
||||||
|
{letter === BLANK ? '?' : letter}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button class="confirm" disabled={exchangeSel.length === 0} onclick={doExchange}>
|
||||||
|
{t('game.exchangeConfirm', { n: exchangeSel.length })}
|
||||||
|
</button>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if checkOpen}
|
||||||
|
<Modal title={t('game.checkWord')} onclose={() => (checkOpen = false)}>
|
||||||
|
<div class="check">
|
||||||
|
<input placeholder={t('game.checkWordPrompt')} bind:value={checkWord} onkeydown={(e) => e.key === 'Enter' && runCheck()} />
|
||||||
|
<button onclick={runCheck}>{t('game.checkWord')}</button>
|
||||||
|
</div>
|
||||||
|
{#if checkResult}
|
||||||
|
<p class:ok={checkResult.legal} class:bad={!checkResult.legal}>
|
||||||
|
{checkResult.legal
|
||||||
|
? t('game.wordLegal', { word: checkResult.word })
|
||||||
|
: t('game.wordIllegal', { word: checkResult.word })}
|
||||||
|
</p>
|
||||||
|
<button class="complain" onclick={complain}>{t('game.complain')}</button>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if resignOpen}
|
||||||
|
<Modal title={t('game.confirmResign')} onclose={() => (resignOpen = false)}>
|
||||||
|
<div class="confirm-row">
|
||||||
|
<button class="cancel" onclick={() => (resignOpen = false)}>{t('common.cancel')}</button>
|
||||||
|
<button class="danger" onclick={doResign}>{t('game.dropGame')}</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if panel === 'chat'}
|
||||||
|
<Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
|
||||||
|
<Chat {messages} myId={app.session?.userId ?? ''} {busy} onsend={sendChat} onnudge={nudge} />
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if panel === 'history' && view}
|
||||||
|
<Modal title={t('game.history')} onclose={() => (panel = 'none')}>
|
||||||
|
<ol class="history">
|
||||||
|
{#each moves as m, i (i)}
|
||||||
|
<li>
|
||||||
|
<span class="hp">{view.game.seats[m.player]?.displayName ?? m.player}</span>
|
||||||
|
<span class="ha">{m.action === 'play' ? m.words.join(', ') : m.action}</span>
|
||||||
|
<span class="hs">{m.score}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.scoreboard {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px var(--pad);
|
||||||
|
background: var(--bg-elev);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.seat {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.seat.turn {
|
||||||
|
background: var(--surface-2);
|
||||||
|
outline: 1px solid var(--accent);
|
||||||
|
}
|
||||||
|
.seat.win .sc {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
.nm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sc {
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.boardwrap {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--pad) 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.turn-ind {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.over {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.rack-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 0 var(--pad);
|
||||||
|
}
|
||||||
|
.rack-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
:global(.rack-row .wrap) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 44px;
|
||||||
|
z-index: 9;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.dropdown button {
|
||||||
|
padding: 11px 14px;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.dropdown button:hover {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
.ghost {
|
||||||
|
position: fixed;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: var(--tile-pending);
|
||||||
|
color: var(--tile-text);
|
||||||
|
border-radius: 5px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
.alpha {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.alpha button {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.exch {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.etile {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--tile-bg);
|
||||||
|
color: var(--tile-text);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.etile.sel {
|
||||||
|
outline: 3px solid var(--accent);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
.confirm {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.confirm:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.check {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.check input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.check button {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
.bad {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.complain {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.confirm-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.confirm-row button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 11px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.danger {
|
||||||
|
background: var(--danger) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--danger) !important;
|
||||||
|
}
|
||||||
|
.history {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.history li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.hp {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.ha {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hs {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// The contextual commit control: appears when tiles are pending. A short tap opens a
|
||||||
|
// minimalist popup (Make move / Reset); a press-and-hold (~1s) commits immediately.
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
resetLabel,
|
||||||
|
onmake,
|
||||||
|
onreset,
|
||||||
|
}: { label: string; resetLabel: string; onmake: () => void; onreset: () => void } = $props();
|
||||||
|
|
||||||
|
let popup = $state(false);
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let held = false;
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function down() {
|
||||||
|
held = false;
|
||||||
|
clear();
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
held = true;
|
||||||
|
popup = false;
|
||||||
|
onmake();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
function up() {
|
||||||
|
clear();
|
||||||
|
if (!held) popup = true;
|
||||||
|
}
|
||||||
|
function leave() {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<button
|
||||||
|
class="make"
|
||||||
|
onpointerdown={down}
|
||||||
|
onpointerup={up}
|
||||||
|
onpointerleave={leave}
|
||||||
|
onpointercancel={leave}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if popup}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="backdrop" onclick={() => (popup = false)}></div>
|
||||||
|
<div class="popup">
|
||||||
|
<button
|
||||||
|
class="go"
|
||||||
|
onclick={() => {
|
||||||
|
popup = false;
|
||||||
|
onmake();
|
||||||
|
}}>{label}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="rs"
|
||||||
|
onclick={() => {
|
||||||
|
popup = false;
|
||||||
|
onreset();
|
||||||
|
}}>{resetLabel}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.make {
|
||||||
|
height: 100%;
|
||||||
|
min-width: 64px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 18;
|
||||||
|
}
|
||||||
|
.popup {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
z-index: 19;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.popup button {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.popup .go {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { RackSlot } from '../lib/placement';
|
||||||
|
import { BLANK } from '../lib/placement';
|
||||||
|
import { tileValue } from '../lib/premiums';
|
||||||
|
import type { Variant } from '../lib/model';
|
||||||
|
|
||||||
|
let {
|
||||||
|
slots,
|
||||||
|
variant,
|
||||||
|
selected,
|
||||||
|
ondown,
|
||||||
|
}: {
|
||||||
|
slots: RackSlot[];
|
||||||
|
variant: Variant;
|
||||||
|
selected: number | null;
|
||||||
|
ondown: (e: PointerEvent, index: number) => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rack">
|
||||||
|
{#each slots as slot (slot.index)}
|
||||||
|
{#if slot.used}
|
||||||
|
<span class="slot empty"></span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="slot tile"
|
||||||
|
class:selected={selected === slot.index}
|
||||||
|
data-rack-index={slot.index}
|
||||||
|
onpointerdown={(e) => ondown(e, slot.index)}
|
||||||
|
>
|
||||||
|
<span class="letter">{slot.letter === BLANK ? '' : slot.letter}</span>
|
||||||
|
{#if slot.letter !== BLANK}<span class="val">{tileValue(variant, slot.letter)}</span>{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rack {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.slot {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.tile {
|
||||||
|
position: relative;
|
||||||
|
background: var(--tile-bg);
|
||||||
|
color: var(--tile-text);
|
||||||
|
border: none;
|
||||||
|
box-shadow: inset 0 -3px 0 var(--tile-edge);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.tile.selected {
|
||||||
|
outline: 3px solid var(--accent);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
.val {
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 1px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// automatically generated by the FlatBuffers compiler, do not modify
|
||||||
|
|
||||||
|
export * as scrabblefb from './scrabblefb.js';
|
||||||
@@ -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';
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Central app state + actions. Holds the session/profile, client preferences, a
|
||||||
|
// transient toast, and the latest live event (screens react to it via $effect). All
|
||||||
|
// gateway calls funnel through here so errors map to one user-facing toast and an
|
||||||
|
// expired session logs out.
|
||||||
|
|
||||||
|
import type { Profile, PushEvent, Session } from './model';
|
||||||
|
import { gateway } from './gateway';
|
||||||
|
import { GatewayError } from './client';
|
||||||
|
import { navigate, router } from './router.svelte';
|
||||||
|
import { errorKey, localeFrom, setLocale, t, type Locale } from './i18n/index.svelte';
|
||||||
|
import { applyReduceMotion, applyTheme, type ThemePref } from './theme';
|
||||||
|
import { clearSession, loadPrefs, loadSession, saveSession, savePrefs } from './session';
|
||||||
|
|
||||||
|
export interface Toast {
|
||||||
|
kind: 'error' | 'info';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const app = $state<{
|
||||||
|
ready: boolean;
|
||||||
|
session: Session | null;
|
||||||
|
profile: Profile | null;
|
||||||
|
toast: Toast | null;
|
||||||
|
lastEvent: PushEvent | null;
|
||||||
|
theme: ThemePref;
|
||||||
|
locale: Locale;
|
||||||
|
reduceMotion: boolean;
|
||||||
|
localeLocked: boolean;
|
||||||
|
}>({
|
||||||
|
ready: false,
|
||||||
|
session: null,
|
||||||
|
profile: null,
|
||||||
|
toast: null,
|
||||||
|
lastEvent: null,
|
||||||
|
theme: 'auto',
|
||||||
|
locale: 'en',
|
||||||
|
reduceMotion: false,
|
||||||
|
localeLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let unsubscribeStream: (() => void) | null = null;
|
||||||
|
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
export function showToast(text: string, kind: Toast['kind'] = 'info'): void {
|
||||||
|
app.toast = { kind, text };
|
||||||
|
if (toastTimer) clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(() => (app.toast = null), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** handleError maps a GatewayError to a toast; an invalid session logs out. */
|
||||||
|
export function handleError(err: unknown): void {
|
||||||
|
if (err instanceof GatewayError) {
|
||||||
|
if (err.code === 'session_invalid' || err.code === 'unauthenticated') {
|
||||||
|
void logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(t(errorKey(err.code)), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(t('error.generic'), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStream(): void {
|
||||||
|
closeStream();
|
||||||
|
unsubscribeStream = gateway.subscribe(
|
||||||
|
(e) => {
|
||||||
|
app.lastEvent = e;
|
||||||
|
if (e.kind === 'chat_message' && e.message.senderId !== app.session?.userId) {
|
||||||
|
showToast(e.message.kind === 'nudge' ? t('chat.nudge') : e.message.body, 'info');
|
||||||
|
} else if (e.kind === 'nudge') {
|
||||||
|
showToast(t('chat.nudge'), 'info');
|
||||||
|
} else if (e.kind === 'your_turn') {
|
||||||
|
showToast(t('game.yourTurn'), 'info');
|
||||||
|
} else if (e.kind === 'match_found') {
|
||||||
|
navigate(`/game/${e.gameId}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => showToast(t('error.unavailable'), 'error'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStream(): void {
|
||||||
|
unsubscribeStream?.();
|
||||||
|
unsubscribeStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function adoptSession(s: Session): Promise<void> {
|
||||||
|
gateway.setToken(s.token);
|
||||||
|
app.session = s;
|
||||||
|
await saveSession(s);
|
||||||
|
try {
|
||||||
|
app.profile = await gateway.profileGet();
|
||||||
|
if (!app.localeLocked) setLocale(localeFrom(app.profile.preferredLanguage, app.locale));
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
openStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bootstrap(): Promise<void> {
|
||||||
|
const prefs = await loadPrefs();
|
||||||
|
app.theme = prefs.theme ?? 'auto';
|
||||||
|
app.reduceMotion = prefs.reduceMotion ?? false;
|
||||||
|
applyTheme(app.theme);
|
||||||
|
applyReduceMotion(app.reduceMotion);
|
||||||
|
if (prefs.locale) {
|
||||||
|
app.locale = prefs.locale;
|
||||||
|
app.localeLocked = true;
|
||||||
|
setLocale(prefs.locale);
|
||||||
|
} else {
|
||||||
|
const guess = localeFrom(typeof navigator !== 'undefined' ? navigator.language : 'en');
|
||||||
|
app.locale = guess;
|
||||||
|
setLocale(guess);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = await loadSession();
|
||||||
|
if (saved) {
|
||||||
|
await adoptSession(saved);
|
||||||
|
if (router.route.name === 'login') navigate('/');
|
||||||
|
} else if (router.route.name !== 'login') {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
app.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginGuest(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const s = await gateway.authGuest(app.locale);
|
||||||
|
await adoptSession(s);
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestEmailCode(email: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await gateway.authEmailRequest(email);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginEmail(email: string, code: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const s = await gateway.authEmailLogin(email, code);
|
||||||
|
await adoptSession(s);
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(): Promise<void> {
|
||||||
|
closeStream();
|
||||||
|
gateway.setToken(null);
|
||||||
|
await clearSession();
|
||||||
|
app.session = null;
|
||||||
|
app.profile = null;
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistPrefs(): void {
|
||||||
|
void savePrefs({ theme: app.theme, locale: app.locale, reduceMotion: app.reduceMotion });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(theme: ThemePref): void {
|
||||||
|
app.theme = theme;
|
||||||
|
applyTheme(theme);
|
||||||
|
persistPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalePref(locale: Locale): void {
|
||||||
|
app.locale = locale;
|
||||||
|
app.localeLocked = true;
|
||||||
|
setLocale(locale);
|
||||||
|
persistPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setReduceMotion(on: boolean): void {
|
||||||
|
app.reduceMotion = on;
|
||||||
|
applyReduceMotion(on);
|
||||||
|
persistPrefs();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Pure board reconstruction. The wire carries no board (StateView is summary + rack
|
||||||
|
// only), so the live grid is rebuilt by replaying the decoded move journal — exactly
|
||||||
|
// the dictionary-independent history invariant (ARCHITECTURE §9.1): apply each play's
|
||||||
|
// placed tiles onto an empty grid.
|
||||||
|
|
||||||
|
import type { MoveRecord, Tile } from './model';
|
||||||
|
import { BOARD_SIZE } from './premiums';
|
||||||
|
|
||||||
|
export interface BoardCell {
|
||||||
|
letter: string;
|
||||||
|
blank: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Board = (BoardCell | null)[][];
|
||||||
|
|
||||||
|
export function emptyBoard(): Board {
|
||||||
|
return Array.from({ length: BOARD_SIZE }, () =>
|
||||||
|
Array.from({ length: BOARD_SIZE }, () => null as BoardCell | null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inBounds(r: number, c: number): boolean {
|
||||||
|
return r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** replay folds every play move's tiles onto an empty board (pass/exchange/resign
|
||||||
|
* change no squares). */
|
||||||
|
export function replay(moves: MoveRecord[]): Board {
|
||||||
|
const b = emptyBoard();
|
||||||
|
for (const m of moves) {
|
||||||
|
if (m.action !== 'play') continue;
|
||||||
|
for (const t of m.tiles) {
|
||||||
|
if (inBounds(t.row, t.col)) b[t.row][t.col] = { letter: t.letter, blank: t.blank };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** lastPlayTiles returns the tiles of the most recent play (for highlighting). */
|
||||||
|
export function lastPlayTiles(moves: MoveRecord[]): Tile[] {
|
||||||
|
for (let i = moves.length - 1; i >= 0; i--) {
|
||||||
|
if (moves[i].action === 'play') return moves[i].tiles;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// GatewayClient — the typed facade the screens call. Both the real Connect/
|
||||||
|
// FlatBuffers transport and the in-memory mock implement it. Domain failures (the
|
||||||
|
// gateway's result_code) and edge failures (Connect error codes) are normalised
|
||||||
|
// into a thrown GatewayError carrying a stable `code` the UI maps to an i18n
|
||||||
|
// message.
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ChatMessage,
|
||||||
|
EvalResult,
|
||||||
|
GameList,
|
||||||
|
GameView,
|
||||||
|
History,
|
||||||
|
HintResult,
|
||||||
|
MatchResult,
|
||||||
|
MoveResult,
|
||||||
|
Profile,
|
||||||
|
PushEvent,
|
||||||
|
Session,
|
||||||
|
StateView,
|
||||||
|
Tile,
|
||||||
|
Variant,
|
||||||
|
WordCheckResult,
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
/** GatewayError carries a stable code (the gateway result_code, or an edge code). */
|
||||||
|
export class GatewayError extends Error {
|
||||||
|
readonly code: string;
|
||||||
|
constructor(code: string, message?: string) {
|
||||||
|
super(message ?? code);
|
||||||
|
this.name = 'GatewayError';
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A tile the player is submitting (rack/blank already resolved to a letter). */
|
||||||
|
export interface PlacedTile {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
letter: string;
|
||||||
|
blank: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unsubscribe handle for the live stream. */
|
||||||
|
export type Unsubscribe = () => void;
|
||||||
|
|
||||||
|
export interface GatewayClient {
|
||||||
|
// --- auth (unauthenticated) ---
|
||||||
|
authGuest(locale?: string): Promise<Session>;
|
||||||
|
authEmailRequest(email: string): Promise<void>;
|
||||||
|
authEmailLogin(email: string, code: string): Promise<Session>;
|
||||||
|
|
||||||
|
// --- profile / lists ---
|
||||||
|
profileGet(): Promise<Profile>;
|
||||||
|
gamesList(): Promise<GameList>;
|
||||||
|
|
||||||
|
// --- lobby ---
|
||||||
|
lobbyEnqueue(variant: Variant): Promise<MatchResult>;
|
||||||
|
lobbyPoll(): Promise<MatchResult>;
|
||||||
|
|
||||||
|
// --- game ---
|
||||||
|
gameState(gameId: string): Promise<StateView>;
|
||||||
|
gameHistory(gameId: string): Promise<History>;
|
||||||
|
submitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Promise<MoveResult>;
|
||||||
|
pass(gameId: string): Promise<MoveResult>;
|
||||||
|
exchange(gameId: string, tiles: string[]): Promise<MoveResult>;
|
||||||
|
resign(gameId: string): Promise<MoveResult>;
|
||||||
|
hint(gameId: string): Promise<HintResult>;
|
||||||
|
evaluate(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Promise<EvalResult>;
|
||||||
|
checkWord(gameId: string, word: string): Promise<WordCheckResult>;
|
||||||
|
complaint(gameId: string, word: string, note: string): Promise<void>;
|
||||||
|
|
||||||
|
// --- chat ---
|
||||||
|
chatPost(gameId: string, body: string): Promise<ChatMessage>;
|
||||||
|
chatList(gameId: string): Promise<ChatMessage[]>;
|
||||||
|
nudge(gameId: string): Promise<ChatMessage>;
|
||||||
|
|
||||||
|
// --- live stream ---
|
||||||
|
subscribe(onEvent: (e: PushEvent) => void, onError?: (err: unknown) => void): Unsubscribe;
|
||||||
|
|
||||||
|
/** Set or clear the bearer token used for authenticated calls and the stream. */
|
||||||
|
setToken(token: string | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { GameView, Tile };
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user