Real-time multiplayer typing survival game with Russian roulette mechanics
Play Demo »
Report Bug
Table of Contents
Type or Die is a high-stakes multiplayer typing game combining competitive typing with Russian roulette mechanics. Players race to type randomly generated sentences within a 20-second window. A three-strike error system triggers a roulette spin, where death probability increases as the game progresses.
Key Features:
- Risk/Reward Mechanics: Survival odds start at 1/6 and improve with survival, creating dynamic tension.
- Real-time Sync: Supports up to 16 concurrent players with live state synchronization.
- Difficulty Progression: Sentences are split 20% Easy, 50% Medium, 30% Hard and served in order so games ramp up naturally.
- Spectator Mode: Eliminated players and late joiners can observe active matches.
- Graceful Reconnection: A 5-second grace period allows players to resume sessions after a disconnect.
Monorepo Shared
- TypeScript: Strict type safety shared across full stack.
Frontend
- Vue 3 & Vite.
- Socket.IO Client for event-driven updates.
- CSS3 with a brutalist terminal aesthetic.
Backend
- Node.js (ESM) & Express.
- Redis (ioredis) for room state, session management, and atomic locking.
- PostgreSQL for persistent sentence storage.
- Lua Scripts for atomic gameplay logic within Redis.
Infrastructure
- Docker & Docker Compose.
- Docker & Docker Compose
- Node.js 22+ (for local dev without Docker)
docker compose -f docker-compose.local.yml up -d| Service | URL |
|---|---|
| Frontend | http://localhost:8080 |
| Backend | http://localhost:4900 |
After making code changes, rebuild and restart everything with:
docker compose -f docker-compose.local.yml down; docker compose -f docker-compose.local.yml up --build -d
Requires Postgres and Redis running locally. Then:
npm install
npm run devThis runs the server and client in parallel via concurrently. Client runs on :5173, server on :3001.
Copy these into a .env at the project root for local dev:
PORT=3001
NODE_ENV=development
CLIENT_URL=http://localhost:5173
DB_HOST=localhost
DB_PORT=5432
DB_NAME=typeordie_dev
DB_USER=devuser
DB_PASSWORD=devpass123
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=localdevpassword
JWT_SECRET=local-dev-jwt-secret-change-me-12345
SESSION_SECRET=local-dev-session-secret-not-secure-12345
MAX_ROOMS_PER_IP=15
MAX_GLOBAL_ROOMS=100
MAX_ROOM_CREATIONS_PER_HOUR=20
DEFAULT_TIME_PER_SENTENCE=20
MAX_WPM_THRESHOLD=200
DISCONNECT_GRACE_PERIOD_MS=5000
ROOM_TTL_SECONDS=3600This project uses Biome. Run from the root:
npx biome check . # check for issues
npx biome check --write . # auto-fixtype-or-die/
├── client/ # Vue 3 frontend (Vite)
├── server/ # Node.js + Socket.IO backend
│ └── src/
│ ├── handlers/ # Socket event handlers
│ ├── services/ # roomManager, sentenceService
│ ├── lua/ # Atomic Redis Lua scripts
│ └── utils/
├── shared/ # Shared TypeScript types (monorepo)
├── load-tests/ # Load test suite
├── init-sentences-db.sql # DB schema + seed data (not tracked, see below)
└── docker-compose.local.yml
Key server files:
| File | Purpose |
|---|---|
handlers/gameFlowHandlers.ts |
Countdown, game start/end |
handlers/playerActionHandlers.ts |
Typing input, mistype, timeout |
handlers/roomLifecycleHandlers.ts |
Room create/join/leave/settings |
services/roomManager.ts |
Room state, distributed locking, janitor |
services/sentenceService.ts |
Sentence fetching by difficulty tier |
lua/atomicCharUpdate.lua |
Atomic char validation + WPM calc in Redis |
init-sentences-db.sql is not tracked in this repo (the sentence pool is private). You need to create it yourself at the root before running Docker. The schema must match exactly:
CREATE TABLE IF NOT EXISTS sentences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
text VARCHAR(500) NOT NULL,
word_count INTEGER NOT NULL CHECK (word_count BETWEEN 8 AND 12),
char_count INTEGER NOT NULL,
language VARCHAR(10) DEFAULT 'en' CHECK (language = 'en'),
contains_emoji BOOLEAN DEFAULT FALSE CHECK (contains_emoji = FALSE),
difficulty VARCHAR(20) DEFAULT 'MEDIUM',
tags TEXT[],
created_at TIMESTAMP DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
death_rate FLOAT DEFAULT 0.0,
average_time FLOAT,
CONSTRAINT unique_sentence UNIQUE(text)
);
CREATE INDEX IF NOT EXISTS idx_word_count ON sentences(word_count);
CREATE INDEX IF NOT EXISTS idx_active ON sentences(is_active) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_difficulty ON sentences(difficulty);
CREATE INDEX IF NOT EXISTS idx_death_rate ON sentences(death_rate);
-- Add your sentences here. You need at minimum:
-- 20% EASY, 50% MEDIUM, 30% HARD to match the game's difficulty split.
-- Each sentence: 8-12 words, no apostrophes, ends with a period.
-- Only . , - punctuation allowed.
INSERT INTO sentences (text, word_count, char_count, difficulty, tags, language)
VALUES
('The dog ran across the open green field.', 8, 41, 'EASY', '{nature}', 'en'),
('She left her bag near the front door.', 8, 37, 'EASY', '{everyday}', 'en');
-- ... add more sentencesThe file is automatically run by Docker on first postgres startup via the volume mount in docker-compose.local.yml.
The project is organized as a TypeScript Monorepo using npm workspaces.
@typeordie/shared: Central source of truth for interfaces and Socket.IO protocols.@typeordie/server: Uses a handler-based pattern to decouple socket events from business logic.@typeordie/client: Separates typing logic (GameController.js) from the Vue rendering layer.
- Atomic Input Processing: Character validation is processed via a Redis Lua script (
atomicCharUpdate.lua). This calculates WPM and advancements in a single atomic step to prevent race conditions. - Concurrency Control: Room states are protected by a distributed locking mechanism in
roomManager.ts. - Event Pipeline: Player actions are managed through a Promise-based Event Queue to ensure sequential execution.
- Sentence Selection:
sentenceService.tsruns 3 parallel queries (one per difficulty tier) withORDER BY RANDOM()to serve a fresh, balanced set each game. - Rate Limiting: Integrated IP-based room registration and event rate limits protect against server abuse.
- Janitor Service: Background processes in
roomManager.tsautomatically clean up inactive rooms and orphaned IP tracking.
Simulates 16 players at ~140 WPM with random disconnects. See testing.md for full details.
cd load-tests
$env:TARGET_URL="http://localhost:4900"; npx ts-node loadtest.tsDistributed under the MIT License. See LICENSE for more information.
Jayden Wong - @thejaydenproject
Project Link: https://github.com/thejaydenproject/type-or-die
- Inspired by Final Sentence on Steam.