From 6cec93630e3e42e9bafd809a7aaddc37a0fd0c91 Mon Sep 17 00:00:00 2001 From: sittichok Ouamsiri Date: Fri, 12 Jun 2026 22:10:40 +0700 Subject: [PATCH] add readme --- README.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..956c65f --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +# Showdown 🃏 + +Real-time scrum poker for your team. Create a room, share the link, everyone votes — no accounts, no database, no setup. When the tab closes, the player disappears. + +**Live demo → [showdown.thistine.com](https://showdown.thistine.com)** + +--- + +## Features + +- **Instant rooms** — hit the home page, pick a name and deck, get a shareable link +- **No login** — identity lives in your browser's localStorage (stable across refreshes) +- **Fibonacci, T-Shirt, or custom decks** — change mid-session, resets the round for everyone +- **Anyone can reveal or reset** — no host privilege, no admin +- **TV / big-screen view** — open the 📺 popup, cast it to a display in the meeting room; shows the room code, join URL, and oversized seats. Goes fullscreen with one click +- **Mobile-first card hand** — switches from an overlapping fan to a tappable grid on screens ≤ 700 px +- **Heavy animations** — spring-physics cards, confetti on consensus, staggered reveals +- **No database** — all state lives in RAM; rooms vanish when the last person leaves + +--- + +## Stack + +| Layer | Tech | +|---|---| +| Frontend | React 19 + TypeScript, Vite 6, Motion (Framer Motion v12) | +| Backend | Rust, axum 0.8, tokio | +| Transport | WebSockets (JSON snapshots) | +| Deploy | Single Docker image (binary + static files) | + +--- + +## Quick start (Docker) + +The easiest way — one command, no Node or Rust required: + +```sh +git clone https://github.com/ThisTine/showdown.git +cd showdown +docker compose up --build +``` + +Open [http://localhost:8080](http://localhost:8080). + +--- + +## Local development + +You need **Node 22+** and **Rust 1.80+**. + +### 1. Frontend (dev server with mock bots — no backend needed) + +```sh +cd frontend +npm install +npm run dev # http://localhost:5173 +``` + +The app runs in **demo mode**: three western bots join, think, and vote automatically. Every animation, card flip, confetti burst, and deck-edit flow works without a backend. You'll see a `demo` chip in the top bar. + +### 2. Backend + +```sh +cd backend +cargo run # http://localhost:8080 +``` + +### 3. Frontend talking to the local backend + +```sh +cd frontend +VITE_WS=1 npm run dev +``` + +Vite proxies `/ws` → `localhost:8080`. The `demo` chip disappears and you're on the real WebSocket. + +### 4. Production build (what Docker does) + +```sh +cd frontend && npm run build # → frontend/dist/ +cd ../backend && cargo run # serves /ws + frontend/dist on :8080 +``` + +--- + +## Project layout + +``` +showdown/ +├── frontend/ +│ ├── src/ +│ │ ├── lib/ +│ │ │ ├── connection.ts WsConnection (prod) + MockConnection (dev bots) +│ │ │ ├── session.ts localStorage profile, stable playerId, room-id words +│ │ │ ├── decks.ts deck presets + custom deck parser +│ │ │ └── router.ts 30-line history router +│ │ ├── pages/ +│ │ │ ├── Home.tsx create room: name, avatar, deck +│ │ │ ├── Room.tsx the game: table, hand, deck modal, confetti +│ │ │ └── Tv.tsx big-screen spectator view +│ │ ├── components/ Table, PlayerSeat, Cards, CardHand, Results, +│ │ │ Confetti, ProfileForm, DeckModal, Logo +│ │ └── index.css full "toy poker table" design system +│ └── vite.config.ts +├── backend/ +│ └── src/ +│ ├── main.rs axum router + static-file SPA fallback +│ ├── room.rs game rules: vote / reveal / reset / deck +│ └── ws.rs socket lifecycle: hello → select loop → cleanup +├── PROTOCOL.md full wire protocol spec (client ↔ server) +├── Dockerfile 3-stage build: Node → Rust → alpine runtime +└── docker-compose.yml +``` + +--- + +## Wire protocol + +See [`PROTOCOL.md`](./PROTOCOL.md) for the full spec. Short version: + +- One WebSocket per player at `GET /ws/room/{roomId}` +- First frame is always `{ "type": "join", ... }` (or `"watch"` for the TV view) +- Every change triggers one full-state JSON snapshot broadcast to the whole room +- Votes are hidden in snapshots until `revealed: true` — only `voted: true/false` leaks before the showdown + +--- + +## Environment variables + +| Variable | Default | Description | +|---|---|---| +| `PORT` | `8080` | Port the server listens on | +| `STATIC_DIR` | `../frontend/dist` | Path to the built frontend files | + +--- + +## Self-hosting + +The Docker image is a single alpine container (~15 MB). Drop it behind any reverse proxy that supports WebSocket upgrades. + +**nginx example** (the only non-obvious part is `proxy_read_timeout` — keep-alive pings are 30 s, so set it higher): + +```nginx +server { + listen 443 ssl; + server_name showdown.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 120s; + } +} +``` + +**Fly.io / Railway / Render** — point them at the `Dockerfile` in the repo root, set `PORT` to whatever the platform expects, done. + +--- + +## How "no database" works + +A `Room` is a Rust struct — a deck, a `revealed` flag, a round counter, and a `Vec` of players. All rooms sit in one `Mutex>` in RAM. When a socket closes, that player is removed. When the last socket of a room closes, the room is deleted from the map. Restart the server and everything is gone — which is the point. + +Player identity is a UUID generated by the browser and stored in `localStorage`. A reconnect (page refresh) with the same UUID reclaims the seat and keeps the vote; the server uses a per-connection counter to tell a refresh from a genuine disconnect. + +--- + +## License + +MIT