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