A real-time multiplayer marble game inspired by the Korean drama "Squid Game". Built with Kotlin/Ktor using Server-Sent Events (SSE) for instant updates.
- Each player starts with 10 marbles
- Players take turns being the "placer"
- The placer hides 1 or more marbles in their hand
- Other players guess: EVEN or ODD
- Correct guessers split the placed marbles equally
- Wrong guessers lose marbles to the placer
- Players with 0 marbles become spectators
- Last player standing wins!
- Real-time multiplayer via SSE (Server-Sent Events)
- Mobile-first responsive design
- Shareable game links with unique codes
- Auto-reconnect on connection loss
- Player disconnect detection
- Session-based player names
- Backend: Kotlin + Ktor 3.x
- Real-time: SSE with 5-second keepalive pings
- Frontend: HTMX + vanilla JavaScript
- Templating: kotlinx.html (server-side)
- Styling: Custom CSS with dark theme
# Build the image
docker build -t marble-game .
# Run the container
docker run -d -p 8080:8080 --name marble-game marble-game
# Open in browser
open http://localhost:8080
# Stop the container
docker stop marble-game# Run the server
./gradlew run
# Open in browser
open http://localhost:8080For local development, use the provided scripts that automatically disable analytics:
./dev-start.sh # Start dev server in background (PostHog disabled)
./dev-stop.sh # Stop the dev server
./dev-reload.sh # Restart the dev server (after code changes)
# View server logs
tail -f dev-server.log- Open the game in your browser
- Enter your name and create a new game
- Share the game link with friends
- Wait for everyone to join, then start!
- Take turns placing marbles and guessing
| Task | Description |
|---|---|
./dev-start.sh |
Start dev server with analytics disabled |
./dev-stop.sh |
Stop the dev server |
./dev-reload.sh |
Restart the dev server |
./gradlew run |
Run the server (with analytics enabled) |
./gradlew build |
Build everything |
./gradlew shadowJar |
Build executable JAR with all dependencies |
./gradlew test |
Run unit tests |
npm run test:e2e |
Run E2E tests (Playwright) |
docker build -t marble-game . |
Build Docker image (~221MB) |
Run all tests with a single command:
./test.sh # Kotlin unit tests + fast E2E tests (~10s)
./test.sh --full # Kotlin unit tests + ALL E2E tests (~2-3 min)
./test.sh --headed # With browser visible
./test.sh --full --headed # All tests with browser visible./gradlew testE2E tests use Playwright and automatically start the server with POSTHOG_ENABLED=false to prevent analytics from being triggered during testing.
# Install dependencies (first time only)
npm install
npx playwright install chromium
# Run all E2E tests
npm run test:e2e
# Run fast tests only (~5 seconds) - recommended for CI
npm run test:e2e:fast
# Run slow tests only (~2 minutes) - gameplay + disconnect scenarios
npm run test:e2e:slow
# Run with browser visible
npm run test:e2e:headed
npm run test:e2e:fast:headed
npm run test:e2e:slow:headed
# Run with Playwright UI
npm run test:e2e:ui
# Debug mode
npm run test:e2e:debugTest categories:
- Fast tests (21): Homepage, game creation, joining, multiplayer flow, static pages
- Slow tests (4): Host disconnect transfer, player disconnect during game, winner determination, host disconnect on game over (require gameplay/SSE timeouts)
| Variable | Default | Description |
|---|---|---|
POSTHOG_ENABLED |
true |
Set to false to disable PostHog analytics and cookie banner |
Example:
# Run with analytics disabled
POSTHOG_ENABLED=false ./gradlew runsrc/main/kotlin/
├── Application.kt # Ktor setup, sessions, plugins
├── Game.kt # Game class, phases, and round result logic
├── GameManager.kt # Singleton managing all active games (with auto-cleanup)
├── GameRenderer.kt # HTML rendering functions for game UI
├── Player.kt # Player state, connection handling, grace period
├── Routing.kt # HTTP endpoints, SSE, form handling
└── Templating.kt # HTML templating configuration
src/main/resources/
├── application.yaml # Server configuration (port, etc.)
├── logback.xml # Logging configuration
└── static/
├── htmx.min.js # HTMX library (served locally)
├── index.html # SSE demo page (legacy)
└── style.css # Mobile-first responsive styles
e2e/ # Playwright E2E tests
└── game.spec.ts # Game flow tests
| File | Lines | Description |
|---|---|---|
Player.kt |
~150 | Player class with connection state, grace period logic |
Game.kt |
~600 | Core game logic, phases, marble distribution |
GameManager.kt |
~120 | Thread-safe game registry with TTL-based cleanup |
GameRenderer.kt |
~500 | Server-side HTML rendering with kotlinx.html |
Routing.kt |
~550 | HTTP routes, SSE endpoints, form handlers |
Application.kt |
~40 | Application entry point and session config |
- Session cookies use
httpOnlyandSameSite=Laxflags - Player names are limited to 30 characters
- HTML output is XSS-protected via
escapeHtml() - HTMX is served locally (no CDN dependency)
- Games auto-cleanup after inactivity (1h for finished, 4h for abandoned)
- Each player has a dedicated SSE channel for broadcasting
- Connection grace period (30s) handles brief disconnects
The server runs on port 8080 by default. To play with others on the same network, find your local IP (e.g., 192.168.x.x) and share http://<your-ip>:8080.
The project uses a multi-stage Dockerfile with Google's distroless base image for a minimal, secure container:
- Build stage:
gradle:8.14-jdk21- compiles and creates the shadow JAR - Runtime stage:
gcr.io/distroless/java21-debian12:nonroot- minimal JRE only
Benefits:
- ~221MB image size (vs ~400-500MB with full JDK)
- No shell or package manager (reduced attack surface)
- Runs as non-root user