In a dead-drawn endgame — the two most recent journal moves are both passes, so the board and the robot's rack are frozen and the robot is bound to pass again — the robot still waited out its long late-game think time (up to 90 min) before passing, needlessly dragging out a decided game. Shorten that delay to a [0.8, 1.5]x band around the human's last-move think time (the gap between the last two journal entries), clamped to [30s, 8min] and taken as a min with the normal schedule, so the robot never moves slower. A slow human collapses to the 8-min cap; a fast human is tracked, with the floor keeping the robot from passing suspiciously instantly. The anchor reads the move journal only (no schema change), stays deterministic from the seed, and still defers to the sleep window. RobotTurns now carries EndgamePass + OppLastMove, filled by one batched journal query on the scan; the honest-AI single-game trigger keeps the normal path (it moves at once). NextMoveAt (admin ETA) is left as the normal-schedule upper bound.
scrabble-game
Multiplatform Scrabble game. Players arrive from a platform (Telegram first; later VK/MAX/iOS/Android) or from standalone web (email / guest). The game supports English Scrabble, Russian Scrabble and Эрудит.
Components
gateway— the only public ingress: anti-abuse, platform authentication (resolves the player and injectsX-User-ID), routing tobackend, and an admin surface behind Basic Auth.backend— internal-only service that owns every domain concern and embeds thescrabble-solverengine library in-process.ui— pure-HTML5 client (plain Svelte 5 + TypeScript + Vite) over Connect-RPC- FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See
ui/README.md.
- FlatBuffers, embeddable in platform webviews and packageable to native via
Capacitor. See
platform/*— per-platform side-services (e.g. the Telegram bot).
Documentation (sources of truth)
docs/ARCHITECTURE.md— global architecture, transport, security, cross-service contracts.docs/FUNCTIONAL.md(+_ru) — per-domain user stories.docs/TESTING.md— test layers and the per-stage CI gate.PLAN.md— the staged implementation plan and stage tracker.CLAUDE.md— project guide and the mandatory per-stage workflow.
Build & test
go build ./backend/... ./pkg/... ./gateway/... # per module (the workspace spans several)
go vet ./backend/... ./pkg/... ./gateway/...
gofmt -l . # must print nothing
go test -count=1 ./backend/... ./pkg/... ./gateway/... # unit tests
go test -tags=integration -count=1 -p=1 ./backend/... # + Postgres (needs Docker)
The integration-tagged tests start a throwaway postgres:17-alpine container
via testcontainers-go and require a reachable Docker daemon; they live in the
backend module. The wire contracts in pkg and the Connect edge in gateway
have committed generated code (regenerate dev-time with make -C pkg gen /
make -C gateway gen).
Run the backend locally
The backend now owns persistence, so it needs Postgres and applies its embedded migrations at startup:
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 ./backend/cmd/backend # HTTP API + probes on :8080, push gRPC on :9090
Run the gateway locally
The gateway is the public edge; point it at a running backend:
GATEWAY_BACKEND_HTTP_URL=http://localhost:8080 \
GATEWAY_BACKEND_GRPC_ADDR=localhost:9090 \
go run ./gateway/cmd/gateway # Connect/h2c edge on :8081
Key environment: BACKEND_HTTP_ADDR (default :8080), BACKEND_LOG_LEVEL
(debug|info|warn|error, default info), BACKEND_POSTGRES_DSN (required).
The full configuration surface and the go-jet regeneration step live in
backend/README.md.
Run the UI locally
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.
Deploy (deploy/)
The full contour is deploy/docker-compose.yml:
backend + gateway (with the UI embedded via go:embed, baked in by its node
build stage) + Postgres + the Telegram connector (with a VPN sidecar) + an
observability stack (OTel Collector → Prometheus + Tempo → Grafana) + a front
caddy that owns a single /_gm Basic-Auth (admin console + Grafana). The Go
services build from multi-stage distroless */Dockerfile.
docker build -f backend/Dockerfile -t scrabble-backend . # pulls the DAWG release artifact
docker build -f gateway/Dockerfile -t scrabble-gateway . # node stage builds + embeds the UI
docker compose -f deploy/docker-compose.yml config # validate (needs the TEST_/PROD_ env)
CI auto-deploys the test contour on a PR into — or push to — development
(.gitea/workflows/ci.yaml); the prod contour is a manual deploy after
development → master. Env reference: deploy/.env.example;
the topology and the two-contour model are in
docs/ARCHITECTURE.md §13.