No description
  • Rust 66%
  • TypeScript 23.4%
  • Svelte 9.2%
  • Shell 0.7%
  • Dockerfile 0.4%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Napat Srichan 9fdbba2648
Some checks failed
CI / Rust (fmt, check, test) (push) Failing after 9s
CI / Web (check, test, build) (push) Has been cancelled
docs(next-iteration): add final report (F0-F6, gates, smoke test)
2026-10-13 22:24:35 -07:00
.github/workflows chore(p4): Docker, Compose, CI, and operations docs 2026-10-13 01:14:24 -07:00
core test(core): clean up clippy warnings in engine tests 2026-10-12 18:15:32 -07:00
docs docs(next-iteration): add final report (F0-F6, gates, smoke test) 2026-10-13 22:24:35 -07:00
scripts chore(p4): Docker, Compose, CI, and operations docs 2026-10-13 01:14:24 -07:00
server feat(f5): wire NUMTILES_RNG_SEED and add gated debug rack route 2026-10-13 22:17:47 -07:00
web refactor(f6): extract match-log shaping into log.ts 2026-10-13 22:11:18 -07:00
.dockerignore chore(p4): Docker, Compose, CI, and operations docs 2026-10-13 01:14:24 -07:00
.gitignore docs(next-iteration): mark F0-F6 done, document debug envs, record execution status 2026-10-13 22:20:32 -07:00
Cargo.lock feat(p1): guest->account upgrade and HTTP auth 2026-10-13 01:13:43 -07:00
Cargo.toml scaffold: add Rust workspace, Axum server, and SvelteKit web app 2026-10-12 16:17:35 -07:00
compose.yaml chore(p4): Docker, Compose, CI, and operations docs 2026-10-13 01:14:24 -07:00
Dockerfile chore(p4): Docker, Compose, CI, and operations docs 2026-10-13 01:14:24 -07:00
Makefile scaffold: add Rust workspace, Axum server, and SvelteKit web app 2026-10-12 16:17:35 -07:00
README.md docs(next-iteration): mark F0-F6 done, document debug envs, record execution status 2026-10-13 22:20:32 -07:00
rustfmt.toml scaffold: add Rust workspace, Axum server, and SvelteKit web app 2026-10-12 16:17:35 -07:00

Numtiles

Numtiles is an online A-Math-style number tile game for two players. This repository is a monorepo:

Path Package Purpose
core/ numtiles-core Authoritative game rules library (Rust).
server/ numtiles-server HTTP/WebSocket match server (Rust, Axum + Tokio).
web/ numtiles-web SvelteKit client (TypeScript).
docs/ — Product, engine, contract, and rules documentation.
scripts/ — Local development helpers (PostgreSQL).

The game/rule source of truth is docs/rules.md; the engine design is in docs/engine-plan.md; the frozen core/server/web contracts are in docs/contracts.md; work-package sequencing is in docs/implementation-plan.md; operations and deployment are in docs/operations.md.

Status: the confirmed scope is implemented — the 100-tile rulebook with the official 15×15 premium map, the authoritative Rust engine, the online server with PostgreSQL persistence, and the SvelteKit client. The follow-on work packages are also implemented: optional accounts (P1), read-only match review (P2), server hardening (P3), and Docker/Compose + CI operations (P4). Six product behaviors are still unresolved and deliberately not implemented (see Open decisions).

Architecture and authority

  • core is the single source of truth for rules. It has no networking, storage, or process-global randomness. All randomness enters through TileDrawSource.
  • server is the only authority allowed to change a match. It owns guest identities, private rooms, matchmaking, live delivery, and persistence. It calls core for every accepted action.
  • web presents and submits intents. It may preview, but it never computes an authoritative score or decides legality.
  • Privacy: a PlayerView contains only the viewer's own rack plus public board/scores. The opponent's rack and the bag are never sent to a browser or exposed by a persistence query that feeds a client.

Prerequisites

  • Rust 1.98+ and Cargo
  • Node.js 26+ and npm
  • PostgreSQL 18 (optional; the server runs in memory when DATABASE_URL is unset). Nix users can get it with nix-shell -p postgresql.

Rust workspace

Run from the repository root:

cargo fmt --all -- --check   # formatting
cargo check --workspace      # type/compile check
cargo test --workspace       # unit + integration tests

Web client

Run from web/:

npm ci           # install dependencies from package-lock.json
npm run check    # svelte-check + strict TypeScript
npm run build    # production build
npm run dev      # local dev server
npm test         # vitest unit tests

Running the server with PostgreSQL

The server reads its bind address from NUMTILES_BIND_ADDR (default 127.0.0.1:3000) and its connection string from DATABASE_URL. When DATABASE_URL is set it connects, applies versioned migrations, and restores active matches after a restart; otherwise it runs in memory (development only).

# Start a throwaway local PostgreSQL and run the DB-gated tests.
scripts/pg-dev.sh

# Or run any command against a throwaway cluster:
scripts/pg-dev.sh cargo test -p numtiles-server

# Start the server against a database (migrations run automatically):
DATABASE_URL=postgres://numtiles@127.0.0.1:55432/numtiles \
  cargo run -p numtiles-server
curl -i http://127.0.0.1:3000/health

scripts/pg-dev.sh re-executes itself under nix-shell -p postgresql, starts a cluster in target/pgdata (override with NUMTILES_PGDATA/NUMTILES_PGPORT), exports DATABASE_URL, and stops the cluster on exit. Any PostgreSQL 18 instance works equally well; set DATABASE_URL yourself and run cargo test --workspace.

The WebSocket endpoint is GET /ws; the message envelope and all message types are frozen in docs/contracts.md §11.

Two developer-tooling variables are accepted, both inert unless set: NUMTILES_RNG_SEED (a u64 that makes per-match tile draws reproducible) and NUMTILES_DEBUG=1 (registers the debug-only POST /debug/rack route to force a seat's rack). Leave NUMTILES_DEBUG unset outside development; see docs/operations.md.

Accounts, review, and hardening

  • Accounts (P1). POST /auth/register, /auth/login, /auth/logout, and GET /auth/me add optional accounts on top of the guest identity. An account recovers the same guest (and its active matches/reconnect tokens) on another device; guest display-name semantics and game rules are unchanged. See docs/contracts.md §14.
  • Match review (P2). GET /matches/{matchId}/review returns the retained history: public board, scores, status, seat labels, and the ordered action log — never racks or the bag. The client route is /review/[matchId]. See §15.
  • Hardening (P3). Per-connection rate limiting, JSON 404s, WebSocket frame caps, input validation, graceful shutdown, and an aggregate Prometheus GET /metrics endpoint. See §16.

Docker and Compose

A Dockerfile builds the numtiles-server binary in a multi-stage image (runtime contains only the server). compose.yaml runs PostgreSQL 18 (named volume + healthcheck) plus the server wired to it via DATABASE_URL:

docker compose up --build
docker compose up -d db      # database only

Environment variables, migrations, health, deployment, and shutdown behavior are documented in docs/operations.md.

Verification

cargo fmt --all -- --check
cargo check --workspace
cargo test --workspace            # core engine, server flows, persistence
( cd web && npm ci && npm run check && npm test && npm run build )
DATABASE_URL=... cargo test --workspace   # enables the DB-gated tests

A Makefile wraps the common Rust and web commands (make verify), and scripts/verify.sh runs the same verification list (DATABASE_URL=... scripts/verify.sh enables the DB-gated tests). The same checks run in CI (.github/workflows/ci.yml).

Open decisions

These six behaviors are not specified by the product owner. They are marked with TODO(decision) in the server and must not be silently chosen. See docs/contracts.md §13 for the full list. Each item has a decision brief (context, options, recommendation, impact) under docs/next-iteration/briefs/; these are proposals, not implementations.

  1. Ready-confirmation expiry — how long a pending ready prompt lasts and what happens on expiry. Brief.
  2. Timed-turn expiry — what happens when a per-turn countdown reaches zero. Deadlines are tracked and delivered, but not enforced. Brief.
  3. Private room / invite-link expiry. Brief.
  4. Matchmaking-queue persistence across a server restart (queues are in-memory; rooms are not persisted). Brief.
  5. Reconnect credential format/rotation — scope and lifetime of the opaque resume token. Brief.
  6. Queued guest on disconnect — whether a guest waiting in the matchmaking queue survives a dropped connection (server/src/hub.rs:351). Brief.

A protocol gap is also recorded: public pairing reuses server.room with an empty code plus server.queue{state:"matched"}, because the frozen contract defines no dedicated public-ready carrier. See docs/contracts.md §11 for the documented re-use.

Roadmap

  • Resolve the six open decisions above, then implement the affected behavior. P1-P4 were deliberately kept independent of these decisions.
  • Address the demo follow-ups (one rack-state defect plus UX/debug suggestions) recorded in docs/demo-followups.md.
  • Execute the next iteration from docs/next-iteration/plan.md.