Unscramble letters. Find words. Beat the clock. Improve touch typing skills
Word Twist presents you with scrambled letters and challenges you to find all valid English words that can be formed from them. The game features progressive difficulty — puzzles start with 6 letters and grow to 7 and then 8 letters as you advance through levels.
| Mode | Description |
|---|---|
| Timed | Race against the clock with 2 minutes per round |
| Untimed | Take your time and find every word |
| Key | Action |
|---|---|
A-Z |
Type letters |
Enter |
Submit word |
Space |
Shuffle letters |
Backspace |
Delete last letter |
Tab |
Clear selection |
| Level | Letters | Words to Find |
|---|---|---|
| 1-5 | 6 letters | 3-6 letter words |
| 6-10 | 7 letters | 3-7 letter words |
| 11+ | 8 letters | 3-8 letter words |
| Word Length | Points |
|---|---|
| 3 letters | 30 pts |
| 4 letters | 45 pts |
| 5 letters | 60 pts |
| 6 letters | 75 pts |
| 7 letters | 90 pts |
| 8 letters | 105 pts |
Tip: Find at least one full-length word to advance to the next level!
|
React Frontend |
Express Backend API |
MySQL 8 Database |
Redis Rate Limiting |
Docker Deployment |
- Node.js 20+
- Docker & Docker Compose (for full stack)
- MySQL 8 (if running without Docker)
# Clone and run
git clone https://github.com/mbuckingham74/text-scramble.git
cd text-scramble
# Set up environment
cp .env.example .env
# Edit .env with your JWT_SECRET
# Launch
docker compose up -d --build# Install dependencies
cd frontend && npm install
cd ../backend && npm install
# Start backend (Terminal 1)
cd backend && npm run dev
# Start frontend (Terminal 2)
cd frontend && npm startThe frontend runs on http://localhost:3000 and proxies API requests to the backend on port 3001.
| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Secret key for JWT tokens |
DB_HOST |
Yes | MySQL host |
DB_USER |
Yes | MySQL username |
DB_PASSWORD |
Yes | MySQL password |
DB_NAME |
Yes | MySQL database name |
REDIS_URL |
No | Redis URL for rate limiting |
NODE_ENV |
No | Set to production for prod |
# Generate a secure JWT secret
openssl rand -base64 32word-twist/
├── frontend/ # React application
│ ├── src/
│ │ ├── App.js # Main game component
│ │ ├── App.css # Styles
│ │ └── sounds.js # Sound effects
│ ├── Dockerfile
│ └── nginx.conf
├── backend/ # Express API
│ ├── src/
│ │ ├── index.js # API endpoints
│ │ ├── game.js # Game logic
│ │ ├── auth.js # JWT authentication
│ │ └── words.txt # Dictionary (~53K words, 3-8 letters)
│ ├── init.sql # Database schema
│ └── Dockerfile
├── docker-compose.yml
└── CLAUDE_CONTEXT.md # Detailed project documentation
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/puzzle?level=N |
Generate puzzle for given level | |
POST |
/api/validate |
Validate a word submission | |
POST |
/api/solutions |
Get all valid words for letters | |
POST |
/api/register |
Register a new user | |
POST |
/api/login |
User login | |
POST |
/api/scores |
Required | Submit a score |
GET |
/api/leaderboard |
Get top 10 scores per mode | |
GET |
/api/scores/me |
Required | Get current user's scores |
- JWT authentication for score submission
- Redis-backed rate limiting with automatic fallback
- Zod input validation on all endpoints
- CORS restricted to approved origins
- bcrypt password hashing (10 rounds)
- Multi-proxy chain support (Cloudflare/NPM)
How Rate Limiting Works
┌─────────────────────────────────────────────────────────────────────────────┐
│ REQUEST FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
User Request
│
▼
┌─────────────────┐
│ Cloudflare │ ◄── Adds X-Forwarded-For header with real IP
└────────┬────────┘
│
▼
┌─────────────────┐
│ Nginx Proxy │ ◄── SSL termination, forwards to backend
│ Manager │
└────────┬────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ WORD TWIST BACKEND │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ app.set('trust proxy', 2) // 2 proxies: Cloudflare → NPM │ │
│ │ ► Extracts real client IP from X-Forwarded-For chain │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ RATE LIMITER MIDDLEWARE │ │
│ │ │ │
│ │ For each request: │ │
│ │ 1. Get client IP │ │
│ │ 2. Build key: "wordtwist:game:192.168.1.1" │ │
│ │ 3. Check Redis for current count │ │
│ │ 4. If under limit → increment & allow │ │
│ │ 5. If over limit → return 429 "Too Many Requests" │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌───────────┘ └───────────┐ │
│ │ Redis Available Redis Down │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ REDIS STORE │ │ MEMORY STORE │ │
│ │ (Persistent) │ │ (Fallback) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ REDIS (Database 1) │
│ │
│ Keys stored with auto-expiry: │
│ │
│ wordtwist:auth:192.168.1.1 → { count: 3, resetTime: ... } │
│ wordtwist:game:192.168.1.1 → { count: 47, resetTime: ... } │
│ wordtwist:score:192.168.1.1 → { count: 2, resetTime: ... } │
│ wordtwist:general:192.168.1.1 → { count: 15, resetTime: ... } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ RATE LIMITS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Endpoint Type Limit Window Key Prefix │
│ ───────────────────────────────────────────────────────────────────────── │
│ Auth (login/reg) 10 requests 15 min wordtwist:auth: │
│ Game (puzzle) 300 requests 1 min wordtwist:game: │
│ Score submit 20 requests 1 min wordtwist:score: │
│ General (other) 200 requests 1 min wordtwist:general: │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by the classic Text Twist game