Files
scrabble-game/backend/README.md
T
Ilia Denisov bfa8797f8c
Tests · Go / test (push) Successful in 6s
Tests · Integration / integration (push) Successful in 9s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 9s
Stage 4: lobby & social (matchmaking, friends, blocks, chat+nudge, invitations, profile, email, multi-player drop-out)
Engine: multi-player drop-out-and-continue with a per-game tile disposition (remove default / return), resigned seats skipped and excluded from the win, leaver rack never revealed; 2-player behaviour unchanged.

New domains (service/store, no HTTP yet): internal/social (friend request/accept graph, per-user blocks, per-game chat with nudge as a message kind, content filter via mvdan.cc/xurls/v2 + leet/separator normaliser + phone heuristic) and internal/lobby (in-memory variant-keyed matchmaking pool, friend-game invitations invite->accept with lazy 7-day expiry). account gains profile editing and the email confirm-code flow (Mailer seam: SMTP or log mailer).

Migration 00003_social.sql + regenerated jet. main wires the new services into the server (accessors for the Stage 6 handlers); robot substitution stays in Stage 5, REST/stream/push in Stage 6/8. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, README) updated.
2026-06-02 19:29:30 +02:00

144 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# backend
Internal-only domain service for the Scrabble platform (module `scrabble/backend`).
It owns identity/sessions, accounts, and — in later stages — the lobby, game
runtime, robot, chat, history and administration. Its only network consumers are
the `gateway` and the platform side-services; it is never exposed publicly.
As of Stage 1 the backend provides the foundation: configuration, the HTTP
listener with the `/api/v1` route-group skeleton and probes, the Postgres pool
with embedded goose migrations, OpenTelemetry wiring, an in-memory session cache,
and the durable accounts / identities / sessions data model. The session and
account REST endpoints are added with the `gateway` (Stage 6); Stage 1 ships the
store/service layer they will call.
Stage 2 adds `internal/engine`, the in-process bridge to the `scrabble-solver`
library: a versioned dictionary registry, a deterministic tile bag, and a pure
rules `Game` (legal plays, passes, exchanges, resignations and end-condition
detection) that emits dictionary-independent move records. It is a library only;
the game domain wires it into the process in Stage 3.
Stage 3 adds `internal/game`, the game domain over the engine. Active games are
event-sourced: a `games` row plus an append-only decoded move journal, with the
live `engine.Game` kept warm in a cache and rebuilt by replay on a miss. It
provides create, the play/pass/exchange/resign transitions, an unlimited
score/legality preview, the hint (per-game allowance plus a profile wallet), the
word-check tool with complaint capture, per-player game state, history and GCG
export, per-account statistics on finish, and a background turn-timeout sweeper
that auto-resigns overdue turns (honouring each player's daily away window). Like
Stages 12 it is a service/store layer; the HTTP surface lands with the
`gateway` (Stage 6).
Stage 4 adds the lobby and social fabric. `internal/lobby` holds an in-memory
matchmaking pool (FIFO per variant, pairs two humans into an auto-match) and
friend-game invitations (invite → accept, starting a 24 player game once every
invitee accepts). `internal/social` owns the friend graph (request/accept),
per-user blocks, and per-game chat with nudges folded in as a message kind; chat
messages are length-capped, content-filtered (no links/emails/phone numbers,
including obfuscated forms) and stored with the sender's IP. `internal/account`
gains profile editing and the email confirm-code flow (a `Mailer` seam: SMTP or a
development log mailer). The engine now also handles **multi-player drop-out**: in
a 34 player game a resignation or timeout drops that seat and the rest play on
(the tile disposition is a per-game setting), the game ending when one active seat
remains. As before this is a service/store layer — chat and nudges are persisted
but their live delivery, and all REST endpoints, arrive with the `gateway`
(Stage 6); the services are exposed via `Server` accessors for those handlers.
## Package layout
```
cmd/backend/ # entrypoint: telemetry -> db+migrate -> registry -> cache -> game+sweeper -> lobby+social -> server
cmd/jetgen/ # dev tool: regenerate go-jet code from a throwaway container
internal/config/ # env configuration (composes postgres + telemetry + game config)
internal/telemetry/ # OpenTelemetry providers + per-request timing middleware
internal/postgres/ # pgx-over-database/sql pool (otelsql), goose migrations
migrations/ # embedded *.sql (goose), schema `backend`
jet/ # generated go-jet models + table builders (committed)
internal/account/ # durable accounts + platform/email identities (store)
internal/session/ # opaque tokens, sessions store, write-through cache, service
internal/server/ # gin engine, route groups, X-User-ID middleware, probes
internal/engine/ # in-process scrabble-solver bridge: registry, bag, Game, replay
internal/game/ # game domain: lifecycle, journal+cache, hint, word-check, GCG, sweeper
internal/social/ # friend graph, per-user blocks, per-game chat + nudge, content filter
internal/lobby/ # in-memory matchmaking pool + friend-game invitations
```
## Configuration (environment)
| Variable | Default | Notes |
| --- | --- | --- |
| `BACKEND_HTTP_ADDR` | `:8080` | HTTP listen address. |
| `BACKEND_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error`. |
| `BACKEND_POSTGRES_DSN` | — | **Required.** pgx/libpq URL; must pin `search_path=backend`. |
| `BACKEND_POSTGRES_MAX_OPEN_CONNS` | `25` | Pool max open connections. |
| `BACKEND_POSTGRES_MAX_IDLE_CONNS` | `5` | Pool max idle connections. |
| `BACKEND_POSTGRES_CONN_MAX_LIFETIME` | `30m` | Max connection lifetime. |
| `BACKEND_POSTGRES_OPERATION_TIMEOUT` | `5s` | Connect attempt + `/readyz` ping bound. |
| `BACKEND_SERVICE_NAME` | `scrabble-backend` | OpenTelemetry `service.name`. |
| `BACKEND_OTEL_TRACES_EXPORTER` | `none` | `none` or `stdout` (OTLP arrives later). |
| `BACKEND_OTEL_METRICS_EXPORTER` | `none` | `none` or `stdout`. |
| `BACKEND_DICT_DIR` | — | **Required.** Directory of committed `.dawg` dictionaries. |
| `BACKEND_DICT_VERSION` | `v1` | Dictionary version new games pin. |
| `BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL` | `1m` | How often the turn-timeout sweeper runs. |
| `BACKEND_GAME_CACHE_TTL` | `24h` | Idle window before a live game is evicted from cache. |
| `BACKEND_SMTP_HOST` | — | Email relay host. **Empty selects the development log mailer** (the confirm-code is logged, not sent). |
| `BACKEND_SMTP_PORT` | `587` | Email relay port. |
| `BACKEND_SMTP_USERNAME` | — | SMTP user; empty relays without authentication. |
| `BACKEND_SMTP_PASSWORD` | — | SMTP password. |
| `BACKEND_SMTP_FROM` | `no-reply@localhost` | Envelope/From address for confirm-codes. |
## Run
```sh
docker run -d --name scrabble-pg -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:17-alpine
BACKEND_POSTGRES_DSN='postgres://postgres:dev@localhost:5432/postgres?search_path=backend&sslmode=disable' \
BACKEND_DICT_DIR=../../scrabble-solver/dawg \
go run ./cmd/backend
```
On boot the backend opens the pool, creates the `backend` schema if needed,
applies the embedded migrations, loads the dictionaries into the engine registry
(a hard dependency — a missing dictionary aborts the boot), warms the session
cache and starts the game turn-timeout sweeper. `GET /healthz` reports liveness;
`GET /readyz` reports 200 only when the database answers and the session cache is
warmed.
## Migrations & generated code
Migrations are plain goose SQL under `internal/postgres/migrations` (sequential
`NNNNN_name.sql`), embedded and applied at startup. After changing the schema,
regenerate the committed go-jet code (needs Docker):
```sh
go run ./cmd/jetgen # rewrites internal/postgres/jet against a temp container
```
## Engine & dictionaries
`internal/engine` consumes the sibling `scrabble-solver` module in-process. Its
bare module path (`scrabble-solver`, not a URL) cannot be fetched via VCS, so the
workspace `go.work` carries `replace scrabble-solver => ../scrabble-solver` and
the build must run from the repository root (the workspace), not from this module
in isolation. `github.com/iliadenisov/dafsa` (the DAWG loader) is a direct
dependency. CI clones the public solver repository into `../scrabble-solver`
before building (see `.gitea/workflows/`); locally, check it out next to this
repository. Committed dictionaries (`en_sowpods.dawg`, `ru_scrabble.dawg`,
`ru_erudit.dawg`) live in the solver's `dawg/` directory; the engine loads them
by `(variant, dict_version)` from a directory path. Since Stage 3 the backend
loads them at startup from the **required** `BACKEND_DICT_DIR` (a missing
dictionary aborts the boot); the future versioned-artifact direction is recorded
in [`../PLAN.md`](../PLAN.md) TODO-2.
## Tests
```sh
go test -count=1 ./... # unit tests (no Docker)
go test -tags=integration -count=1 -p=1 ./... # Postgres-backed (needs Docker)
```
Integration tests are guarded by the `integration` build tag and run against a
throwaway `postgres:17-alpine` container; they fail loudly when Docker is absent
rather than skipping. The `internal/engine` tests load the committed DAWGs from
`BACKEND_DICT_DIR` (defaulting to the sibling `../scrabble-solver/dawg`) and fail
loudly when that directory is absent.