mirror of
https://github.com/ThisTine/Showdown.git
synced 2026-08-18 23:18:48 +07:00
add readme
This commit is contained in:
@@ -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<HashMap<roomId, Room>>` 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
|
||||||
Reference in New Issue
Block a user