6d0dd4fb14
backend/internal/engine wraps the sibling scrabble-solver library in-process: - Registry: versioned DAWG load via dafsa.Load, keyed by (variant, dict_version), latest-per-variant; English / Russian / Эрудит handled uniformly. - Bag: own deterministic, seeded tile bag with Draw + Return (for exchanges), since the solver's self-play bag cannot return tiles. - Game: pure rules engine — deal, play/pass/exchange/resign, refill, per-move scoring, turn order, and end-condition detection (empty bag + empty rack, six scoreless turns, resignation) with end-game rack adjustment. - decode/ReplayBoard: dictionary-independent MoveRecords and board replay via scrabble.Apply (no internal/encoding), realising ARCHITECTURE §9.1. Wiring: go.work gains "replace scrabble-solver => ../scrabble-solver"; backend requires scrabble-solver (placeholder) and github.com/iliadenisov/dafsa directly. Both Go CI workflows clone the public solver sibling (master HEAD, no token) and set BACKEND_DICT_DIR. Docs: ARCHITECTURE §5/§14, TESTING engine layer, backend README, and PLAN refinements + deferred TODOs (publish/version solver; split engine vs dictionary generator).
102 lines
5.1 KiB
Markdown
102 lines
5.1 KiB
Markdown
# 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 server in Stage 3.
|
|
|
|
## Package layout
|
|
|
|
```
|
|
cmd/backend/ # process entrypoint: telemetry -> db+migrate -> cache -> server
|
|
cmd/jetgen/ # dev tool: regenerate go-jet code from a throwaway container
|
|
internal/config/ # env configuration (composes postgres + telemetry 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
|
|
```
|
|
|
|
## 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`. |
|
|
|
|
## 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' \
|
|
go run ./cmd/backend
|
|
```
|
|
|
|
On boot the backend opens the pool, creates the `backend` schema if needed, and
|
|
applies the embedded migrations. `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. A configurable
|
|
`BACKEND_DICT_DIR` is wired when the first consumer needs it (Stage 3); 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.
|