Files
Showdown/README.md
T
sittichok Ouamsiri b841c0a79b Enhance README with additional images
Added images to README for better visual representation.
2026-06-12 22:14:38 +07:00

6.5 KiB

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

image
image image image image

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:

git clone https://github.com/ThisTine/showdown.git
cd showdown
docker compose up --build

Open http://localhost:8080.


Local development

You need Node 22+ and Rust 1.80+.

1. Frontend (dev server with mock bots — no backend needed)

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

cd backend
cargo run            # http://localhost:8080

3. Frontend talking to the local backend

cd frontend
VITE_WS=1 npm run dev

Vite proxies /wslocalhost:8080. The demo chip disappears and you're on the real WebSocket.

4. Production build (what Docker does)

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 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):

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