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:8080- 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 |
|---|---|
./gradlew run |
Run the development server |
./gradlew build |
Build everything |
./gradlew shadowJar |
Build executable JAR with all dependencies |
./gradlew test |
Run tests |
docker build -t marble-game . |
Build Docker image (~221MB) |
src/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
| 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