A collaborative writing game where players take turns adding sentences to a story while seeing only the previous line. The result is a hilariously disjointed masterpiece revealed at the end!
๐ Production: https://exquisite-corpse.dilger.dev
- Create a Room: Click "Create New Room" to start a game and get a 4-character room code
- Share the Code: Give the room code to your friends (works on mobile and desktop!)
- Write Your Sentence: When it's your turn, you'll see only the previous player's sentence
- Enjoy the Chaos: After everyone has written, the complete story is revealed!
- Real-time Multiplayer: Uses WebSockets for instant updates
- Mobile-Friendly: Responsive design works on all devices
- Privacy-First: Players only see the previous sentence, not the full story
- Simple 4-Character Room Codes: Easy to share and remember
- No Registration Required: Just enter your name and play
- Cloudflare Durable Objects: Manages game room state and WebSocket connections
- Cloudflare Workers: Serverless edge functions for routing
- Vanilla JavaScript: No frameworks, just clean WebSocket API
- Tailwind CSS: Utility-first CSS for responsive design
โโโโโโโโโโโโโโโ
โ Browser โโโโโ WebSocket โโโโ
โโโโโโโโโโโโโโโ โ
โผ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Browser โโโโโโโโโโโโ Durable โ
โโโโโโโโโโโโโโโ โ Object โ
โ (Game Room) โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Browser โโโโโโโโโโโ โ
โโโโโโโโโโโโโโโ โ
โผ
โโโโโโโโโโโโโโโโ
โ Game State โ
โ Storage โ
โโโโโโโโโโโโโโโโ
Each game room is a Durable Object instance that maintains:
- Full Story Array: Complete list of all sentences (server-side only)
- Player List: Names, IDs, and turn order
- Current Turn Index: Tracks whose turn it is
- Game Status: Lobby, in-progress, or complete
Privacy Layer: When sending turn notifications, the server only transmits the previous sentence to the next player, ensuring the "exquisite corpse" mechanic works correctly.
Client โ Server:
join: Player joins with their namestart_game: Host starts the gamesubmit_sentence: Player submits their sentence
Server โ Client:
connected: Connection established, playerId assignedplayer_joined: Player list updatedgame_started: Game beginsyour_turn: It's your turn (includes previous sentence)waiting_for_turn: Wait for another playergame_complete: Full story revealederror: Error message
# Clone the repository
git clone https://github.com/cdilga/exquisite-corpse.git
cd exquisite-corpse
# Install dependencies
npm install
# Run locally
npm run dev
# Open http://localhost:8787This project automatically deploys to Cloudflare Workers when you push to the main branch.
# Deploy to production
npm run deploy
# Deploy to staging
npm run deploy:staging
# Deploy to beta
npm run deploy:beta# Run unit tests (with Cloudflare Workers runtime)
npm test
# Watch mode
npm run test:watch
# Interactive UI
npm run test:ui# Run E2E tests against local server
npm run test:e2e
# Interactive mode
npm run test:e2e:ui
# Test deployed production site
npm run test:deployedexquisite-corpse/
โโโ src/
โ โโโ index.js # Worker entry point & routing
โ โโโ GameRoom.js # Durable Object for game state
โ โโโ pages/
โ โโโ home.js # Frontend HTML/CSS/JS
โโโ tests/
โ โโโ unit/
โ โ โโโ game.test.js # Unit tests
โ โโโ e2e/
โ โโโ game.spec.js # E2E tests
โโโ wrangler.toml # Cloudflare configuration
โโโ package.json
src/GameRoom.js: Durable Object class that handles WebSocket connections and game logicsrc/index.js: Worker that routes requests to Durable Objectssrc/pages/home.js: Complete frontend application (HTML/CSS/JS in one file)wrangler.toml: Cloudflare Workers configuration
The following secrets are configured in GitHub Actions:
CLOUDFLARE_API_TOKEN: For deploying to CloudflareCLOUDFLARE_ACCOUNT_ID: Your Cloudflare account ID
The magic happens in GameRoom.js in the handleSubmitSentence function:
// Add sentence to full story (server-side only)
state.story.push({
playerId: playerId,
playerName: currentPlayer.name,
sentence: sentence.trim(),
});
// Send ONLY the previous sentence to next player
const previousSentence = state.story[state.story.length - 1].sentence;
this.sendToPlayer(nextPlayer.id, {
type: 'your_turn',
previousSentence: previousSentence, // Only one sentence!
turnNumber: state.currentTurnIndex + 1,
});This ensures each player only sees the previous sentence, maintaining the "exquisite corpse" mechanic.
If WebSocket connections fail locally:
- Make sure you're using
wrangler dev(not a simple HTTP server) - Check that Durable Objects are properly configured in
wrangler.toml
Room codes are case-insensitive and stored using Durable Object names. Each unique code maps to a unique Durable Object instance.
This is a fun project! Feel free to add features like:
- Adjustable number of rounds (multiple sentences per player)
- Room passwords for private games
- Story export/sharing functionality
- Themed prompts or story starters
- Vote for favorite sentence
MIT
This project was automatically generated and implemented using the-ultimate-bootstrap and Claude AI.
Built with โค๏ธ using Cloudflare Workers and Durable Objects.