feat(backend): scaffold matchmaking server project

Bare HTTP + WebSocket bootstrap (health check route, ws upgrade,
connection logging) with matchmaking/ and rooms/ reserved but
unimplemented. No matchmaking logic yet.

Also adds root README.md (project overview) and AGENTS.md (architecture
map, code style, git workflow).
This commit is contained in:
Gabriel Franco 2026-09-10 10:37:52 -03:00
parent 6f9a298aef
commit 970206fb99
9 changed files with 219 additions and 0 deletions

70
AGENTS.md Normal file
View file

@ -0,0 +1,70 @@
# AGENTS.md
Context and working agreements for anyone (human or agent) changing this
repo. See `README.md` for what the product is; this file is about how we
build it.
## Architecture
`frontend/` (Vite + TypeScript, **no UI framework** — plain DOM) is
layered bottom-up; each layer only imports from the ones below it:
```
types.ts shared domain types (Token, CategoryState, ThemeDefinition, ...)
data/ static content: theme palettes, code samples
engine/ pure, DOM-free utilities: tokenizer, color math, particles, sound
game/ canvas rendering, scoring, panel/result DOM views, ThemeGuessGame
preview/ the pre-round "flash the real theme" renderer
ui/ composable DOM widgets (theme grid, preview countdown flow)
main.ts composition root — wires the above to the page, nothing else
```
New game logic goes in `engine/` if it doesn't touch the DOM, `game/` if
it does. Don't reach for React/Vue/etc.: the canvas rendering and the
handful of DOM widgets don't need a framework, and adding one is a
bigger discussion than a drive-by PR.
`backend/` is currently a bare HTTP+WebSocket bootstrap
(`src/index.js` + `src/ws/connectionHandler.js`) with `src/matchmaking/`
and `src/rooms/` reserved but empty (see the `README.md` in each). Plain
Node with ESM (`"type": "module"`), no framework, no TypeScript — keep it
that way until there's an actual reason to add one.
## Code style
- Frontend TypeScript runs under `strict`. Don't widen types to work
around an error without understanding why it's there.
- Small static string-keyed lookup tables are `Record<K, V>`, not `Set`/
`Map` — reach for `Set`/`Map` only for dynamic membership, non-string
keys, or when you need `.size`/iteration order/`.clear()`.
- Don't wrap a single expression in a named function just to name it,
unless the name is a stable public API, a callback whose identity
matters, or documents a non-obvious formula. Inline the rest.
- A property literally named `constructor` in an object literal typed as
`Record<string, V>` gets its value type silently widened by TS (see
`engine/tokenizer.ts`'s `toLookup`) — build such tables from an array
via `Object.fromEntries` instead of a literal when a key might collide.
## Git
- Trunk is `master`; keep it green (typechecks, builds, game loads).
- One short-lived branch per change, prefixed by intent:
`feat/…`, `fix/…`, `refactor/…`, `chore/…`, `docs/…`.
- Commit messages follow Conventional Commits:
`type(scope): summary` — e.g. `refactor(frontend): split game into modules`,
`feat(backend): add matchmaking queue`. Scope is usually `frontend`,
`backend`, or a package-relative area (`engine`, `ui`, `ws`, ...).
- Squash-merge feature branches into `master`; delete the branch after.
Keep history readable — no "wip", "fix typo", "asdf" commits on `master`.
- Frontend and backend evolve independently; a PR touching only one
should only bump/discuss that package's version and deps.
## Verification expectations
- Frontend change: `npm run typecheck` (or `build`) in `frontend/`, plus
an actual smoke test (open the dev server, play a round) for anything
touching rendering, input, or scoring — type-checking alone doesn't
catch a canvas drawn one line too high.
- Backend change: whatever's implemented must actually boot
(`npm start`) and the touched transport (HTTP route / WS message) must
be exercised manually or with a script; there's no test suite yet.

56
README.md Normal file
View file

@ -0,0 +1,56 @@
# Theme Guess
A browser game about syntax-highlighting themes. A snippet of code is
rendered with every token type (keywords, strings, functions, variables,
comments, ...) in a neutral color. You click a token, every token of that
same kind lights up together, and you pick a color for it — repeat until
the whole file is themed. Reveal shows how close your from-scratch palette
landed to a real theme (Gruvbox, Tokyo Night, VS Code Dark+, GitHub Dark,
Dracula, Catppuccin), token kind by token kind.
The project is moving from a single-player-only page toward supporting
**multiplayer matchmaking** (race another player to the closest match on
the same theme/snippet). This repo is split into a frontend game client
and a backend matchmaking/realtime server so that work can land
independently.
## Layout
```
frontend/ Canvas game client: Vite + TypeScript, no UI framework.
backend/ Matchmaking + realtime session server: Node, HTTP + WebSocket.
```
Each package has its own `package.json`, dependencies, and scripts — see
`frontend/README.md`-equivalent notes below and `backend/src/*/README.md`
for what's reserved but not implemented yet.
## Running the frontend
```sh
cd frontend
npm install
npm run dev # Vite dev server with HMR
npm run build # type-check + production bundle to dist/
npm run typecheck # tsc --noEmit only
```
## Running the backend
```sh
cd backend
npm install
npm start # node src/index.js
npm run dev # same, with --watch
```
Boots a plain HTTP server (with a `/health` check) and attaches a
WebSocket server to it. Matchmaking and room/session logic are not
implemented yet — see `backend/src/matchmaking/README.md` and
`backend/src/rooms/README.md`.
## Status
Single-player game: playable. Multiplayer: backend is a bare HTTP+WS
scaffold with no matchmaking logic yet; frontend has no multiplayer UI
yet. See `AGENTS.md` for architecture notes and contribution conventions.

1
backend/.env.example Normal file
View file

@ -0,0 +1 @@
PORT=8080

36
backend/package-lock.json generated Normal file
View file

@ -0,0 +1,36 @@
{
"name": "theme-guess-backend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "theme-guess-backend",
"version": "0.1.0",
"dependencies": {
"ws": "^8.18.0"
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

15
backend/package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "theme-guess-backend",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Matchmaking and realtime session server for Theme Guess (HTTP + WebSocket).",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
},
"dependencies": {
"ws": "^8.18.0"
}
}

24
backend/src/index.js Normal file
View file

@ -0,0 +1,24 @@
import http from 'node:http';
import { WebSocketServer } from 'ws';
import { handleConnection } from './ws/connectionHandler.js';
const PORT = process.env.PORT || 8080;
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});
// The WebSocket server shares the HTTP server's port via the upgrade
// handshake, so one process serves both plain HTTP and `ws://`.
const wss = new WebSocketServer({ server });
wss.on('connection', handleConnection);
server.listen(PORT, () => {
console.log(`Matchmaking server listening on :${PORT}`);
});

View file

@ -0,0 +1,4 @@
# matchmaking/
Reserved for the matchmaking queue: pairing waiting players, party/lobby
handling, and handing off matched players to a room. Not implemented yet.

View file

@ -0,0 +1,5 @@
# rooms/
Reserved for game-session/room state: tracking which sockets belong to a
match, broadcasting round state, and cleaning up on disconnect. Not
implemented yet.

View file

@ -0,0 +1,8 @@
// Placeholder connection handler. Matchmaking, room assignment, and
// game-state sync are not implemented yet — this only proves the
// WebSocket transport itself works end to end.
export function handleConnection(socket) {
console.log('client connected');
socket.on('close', () => console.log('client disconnected'));
}