9.7 KiB
scrabble-game — project guide
Multiplatform Scrabble game, in production at https://erudit-game.ru. Read this
first every session. The repository — not conversation memory — is the source of
continuity; keep it that way.
Sources of truth (read before changing behaviour)
docs/ARCHITECTURE.md— architecture, transport, security, the decision record. Always describes the current state.docs/FUNCTIONAL.md(+_rumirror) — per-domain user stories. English authoritative.docs/TESTING.md— test layers + the CI gate.docs/UI_DESIGN.md— theuivisual/interaction design system.deploy/README.md— the deploy contour + the production rollout / rollback runbook.
How we work
- Inspect the relevant code path and the docs above before changing behaviour.
- Interview the owner on every fork — do not silently pick borderline decisions; offer options with brief pros/cons.
- Smallest correct diff. Prefer compact code; reuse before adding; do not add deps, seams or knobs until they are needed.
- Update or add tests for every functional change, at the layers
docs/TESTING.mdcalls out. - Bake docs in the same PR: update
docs/ARCHITECTURE.md,docs/FUNCTIONAL.md(+_ru), the affected serviceREADMEand Go Doc comments alongside the change. - Document added packages, types, funcs, consts and vars with Go Doc comments.
Conventions
- All code, comments, identifiers, commits, docs, filenames in English.
- Chat with the owner follows the user-level
~/.claude/CLAUDE.md(Russian, the agreed persona and translation rules). - Mirror every point edit of
docs/FUNCTIONAL.mdintodocs/FUNCTIONAL_ru.mdin the same patch (translate only the touched paragraphs).
Branching, CI & production
- Two long-lived branches:
developmentis the integration branch;masteris the production trunk. Cutfeature/*fromdevelopmentand PR back into it; promotedevelopment → mastervia PR when ready to release. Both branches require one approval + theCI / gatecheck. - A commit to a
feature/*branch triggers nothing. The single workflow.gitea/workflows/ci.yamlruns the full suite (unit+integration+ui) on a PR intodevelopmentormaster, and the gateddeployjob auto-rolls the test contour on a PR into — or a push to —development(docker compose up -d --buildon the runner host + landing/SPA/backend probes). A PR intomasteris test-only. - Production is live on two hosts (main + the Telegram bot host) and deploys
only manually (
workflow_dispatch), never automatically:.gitea/workflows/prod-deploy.yaml(confirm=deploy, frommaster) builds + pushes the images to the registry, then SSH-deploys both hosts — rolling per service in dependency order, health-gated, auto-rollback to the previous tag; a schema migration adds a maintenance window + a consistentpg_dump. Four visible jobs: build → deploy-main → deploy-bot → verify..gitea/workflows/prod-rollback.yaml(confirm=rollback) re-deploys a prior release (blanktarget_version= the previous deployed version) — image-only, rolling, health-gated.- Releases are git tags
vX.Y.Zonmaster; the deploy stampsgit describe --tagsinto the image tag, every binary (pkg/versionvia-ldflags→ theservice.versiontelemetry attribute) and the SPA About screen. Tag the release before deploying. - Hosts are provisioned idempotently by
deploy/ansible/. Per-contour secrets/variables use theTEST_/PROD_prefix (Gitea 1.26 has no deployment environments). Migrations must be expand-contract (backward-compatible) so image rollback stays DB-safe. Full runbook + variable list indeploy/README.md.
- After any push, merge or deploy, watch the run to green before declaring done —
use the ready-made watcher (run it in the background), never an inline poll loop:
python3 ~/.claude/bin/gitea-ci-watch.py. It reads$GITEA_URL/$GITEA_TOKEN;gitea.iliadenisov.ruis allow-listed in.claude/settings.json. Remote:origin git@gitea.iliadenisov.ru:developer/scrabble-game.git.
Stack
Go 1.26.3, go.work monorepo, module paths scrabble/<name>. Backend uses gin +
zap + pgx/go-jet/goose/OTel. Client↔gateway is Connect-RPC + FlatBuffers
(h2c); gateway↔backend is REST/JSON + X-User-ID plus a gRPC server-stream for live
events. UI is pure HTML5/CSS on plain Svelte + Vite, packaged to native with
Capacitor. No Redis.
Reused engine: ../scrabble-solver (module scrabble-solver, Go 1.26.3)
Embedded in-process as a library (replace scrabble-solver => ../scrabble-solver
in go.work; CI checks out the sibling from
https://gitea.iliadenisov.ru/.../scrabble-solver.git). There is no per-game
container. Public API to reuse (do not reimplement):
scrabble.NewSolver(rs, finder)→GenerateMoves(b, r, mode)(ranked, highest score first),ValidatePlay(b, dir, tiles),ScorePlay(...);scrabble.Apply(b, m); typesMove/Word/Placement/Direction/Mode(scrabble-solver/scrabble/{solver,move,apply}.go).rules.English() / RussianScrabble() / Erudit()(scrabble-solver/rules/rules.go).board.New / Parse / Clone / Transpose;rack.New / Add / Remove / Clone;selfplay.NewBag / Draw / Len(bag pattern).- Load committed dictionaries with
dawg.Load(path)fromgithub.com/iliadenisov/dafsa:scrabble-solver/dawg/{en_sowpods,ru_scrabble,ru_erudit}.dawg.
Constraints:
- Words/tiles are alphabet-index bytes, meaningful only with the matching
rules.Ruleset(Alphabet.Decode); the blank flag is carried separately. Decode to real characters before persisting history (history must be dictionary-independent — seedocs/ARCHITECTURE.md§9.1). - The solver's
internal/*is NOT importable from this sibling module. - GCG is test-only in the solver (no public writer) — we ship our own.
- The solver uses published
github.com/iliadenisov/{alphabet,dafsa}(no local replace).
Repository layout
go.work # the go.work monorepo
backend/ # module scrabble/backend
cmd/backend/ # main: telemetry -> db+migrate -> cache -> server
cmd/jetgen/ # dev tool: regenerate go-jet code (throwaway container)
internal/config/ # env config (composes postgres + telemetry)
internal/telemetry/ # OTel providers + request-timing middleware
internal/postgres/ # pgx/database-sql pool, goose migrations/, jet/ (generated)
internal/account/ # durable accounts + identities (store)
internal/session/ # opaque tokens, sessions store, cache, service
internal/server/ # gin engine, /api/v1 groups, X-User-ID, probes
internal/inttest/ # //go:build integration Postgres-backed tests
gateway/ # module scrabble/gateway: Connect-RPC edge, embeds the SPA
ui/ # Svelte + Vite SPA + landing (Node project, not in go.work)
pkg/ # shared: telemetry, version, wire/FlatBuffers, proto, mtls
platform/telegram/ # Telegram side-service: cmd/validator (HMAC, no VPN) + cmd/bot (Bot API; dials gateway over reverse mTLS bot-link)
renderer/ # image-render sidecar (Node + skia-canvas): runs ui/src/lib/gameimage.ts server-side for the finished-game PNG export
loadtest/ # module scrabble/loadtest: the load/stress harness
docs/ .gitea/workflows/ CLAUDE.md README.md
backend/Dockerfile gateway/Dockerfile platform/telegram/Dockerfile loadtest/Dockerfile renderer/Dockerfile # multi-stage distroless (renderer: node:22-slim + skia-canvas + fonts); gateway/Dockerfile has the `landing` target, platform/telegram/Dockerfile has `validator`+`bot` targets
deploy/ # docker-compose (+ prod overlay + bot host) + ansible provisioning + caddy + landing + otelcol (OTLP + docker_stats) + prometheus/tempo/grafana + node_exporter + postgres_exporter; prod-deploy.sh
Build & test
go build ./backend/... # per module ('./...' from the root won't span the workspace)
go vet ./backend/...
gofmt -l . # must print nothing
go test -count=1 ./backend/...
go build ./platform/telegram/... && go test ./platform/telegram/... # Telegram validator + bot
go run ./backend/cmd/backend # /healthz, /readyz on :8080
cd ui && pnpm install && pnpm check && pnpm test:unit && pnpm build # the UI
pnpm start # UI mock mode: lobby -> game, no backend
cd renderer && pnpm install && pnpm test # image-render sidecar (bundles ui/src/lib/gameimage.ts, skia smoke)
docker build --build-arg DICT_VERSION=v1.3.0 -f backend/Dockerfile -t scrabble-backend . # DICT_VERSION required (no default); gateway embeds the SPA
docker build -f gateway/Dockerfile --target gateway -t scrabble-gateway .
docker build -f gateway/Dockerfile --target landing -t scrabble-landing . # static landing
docker compose -f deploy/docker-compose.yml config # validate the full contour
The ui module is a Node project (pnpm), not in go.work; it is the ui job of
the single .gitea/workflows/ci.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).
Agent field notes
Non-obvious, hard-won knowledge the agent has accumulated that is not captured in the docs above, kept in the repo so it travels with a clone. Verify any named file/flag against current code before acting on it.
@.claude/CLAUDE.md