Skip to content

TheJaydenProject/type-or-die

Repository files navigation

Contributors Forks Stargazers Issues MIT License

Type or Die

Real-time multiplayer typing survival game with Russian roulette mechanics
Play Demo »

Report Bug

Table of Contents
  1. About The Project
  2. Getting Started
  3. Project Structure
  4. Architecture
  5. Load Testing
  6. Known Issues
  7. License
  8. Contact
  9. Acknowledgments

About The Project

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.

Tech Stack

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.

(back to top)

Getting Started

Prerequisites

  • Docker & Docker Compose
  • Node.js 22+ (for local dev without Docker)

Run with Docker (recommended)

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

Run without Docker (local dev)

Requires Postgres and Redis running locally. Then:

npm install
npm run dev

This runs the server and client in parallel via concurrently. Client runs on :5173, server on :3001.

Environment variables

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=3600

Linting & formatting

This project uses Biome. Run from the root:

npx biome check .        # check for issues
npx biome check --write .  # auto-fix

(back to top)

Project Structure

type-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

Database seed file

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 sentences

The file is automatically run by Docker on first postgres startup via the volume mount in docker-compose.local.yml.

(back to top)

Architecture

System Design

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.

Data Flow & State Management

  • 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.

Performance & Reliability

  • Sentence Selection: sentenceService.ts runs 3 parallel queries (one per difficulty tier) with ORDER 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.ts automatically clean up inactive rooms and orphaned IP tracking.

(back to top)

Load Testing

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.ts

(back to top)

Known Issues

(back to top)

License

Distributed under the MIT License. See LICENSE for more information.

(back to top)

Contact

Jayden Wong - @thejaydenproject

Project Link: https://github.com/thejaydenproject/type-or-die

(back to top)

Acknowledgments

  • Inspired by Final Sentence on Steam.

(back to top)

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors