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:
parent
6f9a298aef
commit
970206fb99
9 changed files with 219 additions and 0 deletions
70
AGENTS.md
Normal file
70
AGENTS.md
Normal 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
56
README.md
Normal 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
1
backend/.env.example
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
PORT=8080
|
||||||
36
backend/package-lock.json
generated
Normal file
36
backend/package-lock.json
generated
Normal 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
15
backend/package.json
Normal 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
24
backend/src/index.js
Normal 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}`);
|
||||||
|
});
|
||||||
4
backend/src/matchmaking/README.md
Normal file
4
backend/src/matchmaking/README.md
Normal 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.
|
||||||
5
backend/src/rooms/README.md
Normal file
5
backend/src/rooms/README.md
Normal 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.
|
||||||
8
backend/src/ws/connectionHandler.js
Normal file
8
backend/src/ws/connectionHandler.js
Normal 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'));
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue