{"id":51005962,"url":"https://github.com/radiantnode/tensies","last_synced_at":"2026-06-20T20:30:25.270Z","repository":{"id":360246031,"uuid":"1249261333","full_name":"radiantnode/tensies","owner":"radiantnode","description":"A real-time multiplayer dice game for the bar, the beach, or anywhere you forgot to bring actual dice.","archived":false,"fork":false,"pushed_at":"2026-06-14T16:36:43.000Z","size":248811,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T16:44:07.190Z","etag":null,"topics":["browser-game","dice-game","docker","fastapi","multiplayer","party-game","python","real-time","redis","vanilla-javascript","websocket"],"latest_commit_sha":null,"homepage":"https://tensies.app","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/radiantnode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T14:13:12.000Z","updated_at":"2026-06-13T23:20:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/radiantnode/tensies","commit_stats":null,"previous_names":["radiantnode/tensies"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/radiantnode/tensies","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radiantnode%2Ftensies","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radiantnode%2Ftensies/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radiantnode%2Ftensies/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radiantnode%2Ftensies/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/radiantnode","download_url":"https://codeload.github.com/radiantnode/tensies/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/radiantnode%2Ftensies/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34585195,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browser-game","dice-game","docker","fastapi","multiplayer","party-game","python","real-time","redis","vanilla-javascript","websocket"],"created_at":"2026-06-20T20:30:24.403Z","updated_at":"2026-06-20T20:30:25.260Z","avatar_url":"https://github.com/radiantnode.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tensies\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"static/images/logo.svg\" alt=\"Tensies\" width=\"220\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/radiantnode/tensies/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/radiantnode/tensies/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/radiantnode/tensies/actions/workflows/codeql.yml\"\u003e\u003cimg src=\"https://github.com/radiantnode/tensies/actions/workflows/codeql.yml/badge.svg\" alt=\"CodeQL\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nA real-time multiplayer dice game for the bar, the beach, or anywhere you forgot to bring actual dice.\n\n---\n\n## The origin story\n\nThere's a dice game called Tensies. You roll ten dice, lock the ones that match the target number, keep rolling until all ten match, first one to do it wins the round. Simple, fast, and very good at ruining friendships.\n\nMy friends and I played it most weekends. One night, a few heated rounds in and a few drinks down, I decided it would be great to play anywhere, including nights when nobody remembered to bring the dice. So I started building it. Sketched the first board myself, then kept tinkering from my barstool between rounds, with [Claude](https://claude.ai) doing most of the heavy lifting on the code.\n\nVersion 1.0 shipped on a Monday. I built the multi-instance rewrite (the one that lets a whole crowd pile in across a row of servers) from a beach chair in Cap Cana, Dominican Republic, dodging back to the pool between edits.\n\nThis is not serious software. It's a hobby project that got a little out of hand in the best way.\n\n\u0026nbsp;\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/hero.png\" alt=\"Tensies Screenshots\" /\u003e\n\u003c/p\u003e\n\n---\n\n## How to play\n\n- One person creates a game and shares the code (or just texts the link).\n- Up to five players join. The host hits **Start**.\n- A target number appears. Everybody rolls their ten dice at once.\n- Dice that match the target lock automatically. Keep rolling the rest.\n- First player to lock all ten wins the round.\n- Targets count up: 1, 2, 3, 4, 5, 6, then back around.\n- Most round wins by the time everyone's ready to close out wins the night.\n\n---\n\n## Features\n\nThe dice physics feel right. They gather, shake, scatter across a virtual bar top, and settle. Matched dice slide over and lock. The reveal animation waits for each roller before broadcasting the new state, so nobody sees your result before you do.\n\nEverything else:\n\n- Live multiplayer over WebSocket. Updates in under a second.\n- Reconnect grace period: 30 seconds normally, an hour if the game is paused. Phone goes dark mid-round, you get your seat back.\n- Host pause, for a bar run, a bathroom break, or figuring out who's buying the next round. Hangs for up to an hour.\n- If the host vanishes, the next person in the room quietly takes over. Nobody waits.\n- Share by link, SMS, or [audio](docs/audio-sharing/README.md) from the lobby. One phone chirps the code, the other listens and fills it in.\n- Dice positions stay put across refreshes.\n- Scales horizontally: game state lives in Redis, so you can run as many server instances as you want behind a plain round-robin load balancer. Any instance can serve any game.\n\n---\n\n## Stack\n\n### Server\n\nPython + [FastAPI](https://fastapi.tiangolo.com/) for the async WebSocket server, with a thin REST layer for static assets, metrics, and admin stats.\n\n[Redis](https://redis.io/) is where game state lives. Cross-instance fan-out runs over pub/sub. All player data is a Redis hash, so rolling is parallel: distinct players write distinct fields. The one contested write (crowning a round winner) is an atomic Lua compare-and-set.\n\n[Uvicorn](https://www.uvicorn.org/) is the ASGI server, with `--workers` in prod and `--reload` in dev. [asyncpg](https://github.com/MagicStack/asyncpg) + [Postgres](https://www.postgresql.org/) handle the telemetry event log and rollup tables; those writes are async and stay off the hot path.\n\n[Prometheus](https://prometheus.io/) runs in-process tracking active games, players, roll latency, and WS frame counts. [Grafana](https://grafana.com/) provisions five dashboards automatically from `ops/grafana/dashboards/`; one uses Grafana Live for sub-second push updates. [nginx](https://nginx.org/) sits in front of the web instances in prod as the load balancer.\n\n### Client\n\nVanilla JavaScript split by concern across `static/js/`, loaded as ES modules with no framework. In dev it loads straight from the browser with no build step; cache-busting is a content hash appended at server startup. In prod an esbuild pipeline bundles and minifies everything into a single JS file and a single CSS file, fingerprints all assets, and pre-compresses them for nginx: 39 requests down to 7, 132 KB of JS+CSS+HTML down to 21 KB on the wire. Details in [`docs/ASSET_PIPELINE.md`](docs/ASSET_PIPELINE.md).\n\nThe dice are pure CSS 3D transforms on `.die-3d` faces. The bar-top background is a photo.\n\n### Infrastructure\n\n[Docker + Docker Compose](https://docs.docker.com/compose/) for everything. `docker-compose.yml` is local dev: bind mount, hot reload, relaxed auth. `docker-compose.prod.yml` is production: pinned digest images, non-root user, internal networking, bearer-gated endpoints.\n\n[Playwright](https://playwright.dev/) handles integration testing via Claude Code's MCP server. It covers full two-player games, reconnect, pause, host handoff, and animation timing.\n\n### Built with\n\nI built this with [Claude Code](https://claude.ai/code), Anthropic's CLI for Claude. Claude wrote most of the code. The game design, visual direction, and \"that doesn't feel right\" instincts were mine. Claude also wrote this README, which is exactly the kind of thing it would do.\n\n---\n\n## Running it\n\n```bash\n# Clone\ngit clone --recurse-submodules \u003crepo-url\u003e\ncd tensies\n\n# Dev (starts web + Redis + telemetry stack)\ndocker compose up -d\nopen http://localhost:8888\n```\n\nThe volume mount means edits take effect immediately. You only need to rebuild if `requirements.txt` changes. Telemetry (Postgres + Grafana) is optional: `TELEMETRY_ENABLED=0` skips it for a lightweight run.\n\n```bash\n# Production (multi-instance, 3 servers)\ndocker compose -f docker-compose.prod.yml --env-file .env.prod up -d --scale web=3\n```\n\nSee `.env.prod.example` for the full env var list. The ones that matter most: `REDIS_URL`, `ALLOWED_ORIGINS`, `METRICS_TOKEN`, `MAX_GAMES`, and the rate limits.\n\n---\n\n## Architecture in brief\n\nGame state lives in Redis so any instance can serve any game. Per-process state is strictly what can't be serialized: open WebSocket handles, live `asyncio` objects. A periodic reaper process is the backstop for grace-drops and pause timeouts whose owning instance might have died.\n\nThe roll animation is the most choreographed part. When you roll, the server sends your result back to you first, waits for your `roll_done` ack (or a timeout), then broadcasts to everyone else. Other players never see your dice change before your animation finishes.\n\nFull architecture notes, the WebSocket message protocol, and the telemetry reference are in [`CLAUDE.md`](CLAUDE.md) and [`docs/TELEMETRY.md`](docs/TELEMETRY.md).\n\n---\n\n## Credits\n\nThe dice game has been around forever; I just built a digital bar top for it.\n\n[Claude](https://claude.ai) (Anthropic) wrote the code, suggested the architecture, iterated on the CSS with me, and spent an unreasonable amount of time on broken-dice fracture glow effects.\n\nMy friends playtested it, broke things regularly, and were never once patient about any of it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradiantnode%2Ftensies","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fradiantnode%2Ftensies","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fradiantnode%2Ftensies/lists"}