EmperorGame: an online card battle game running in a single Java process
EmperorGame is an online card battle game you play in the browser. The gameplay is the emperor-citizen-slave rock-paper-scissors variant: simple rules, plenty of mind games. The entire backend is packed into a single Java process, which I used to practice real-time communication.
For a small game I didn’t want to spin up a pile of services, so the backend is a single process: Java-WebSocket handles the real-time battle communication, and the JDK’s built-in HttpServer conveniently serves the frontend static pages and a few JSON endpoints. No Spring, no servlet container. mvn clean package builds a fat jar and java -jar starts it directly. The build output from the Vue 3 + Vite frontend gets dropped into the built-in HTTP server. In production I put Nginx in front as a same-origin reverse proxy: / serves static files, /api/ goes to HTTP, /ws/ goes to WebSocket, and under HTTPS it naturally switches to WSS. Default ports are 13337 for WebSocket and 13338 for HTTP.
The battle flow is plain. The client sends match_online to enter the queue; once enough players gather they’re matched into a GameRoom. Each round both sides lock in a card with play_online, and once both are ready the server decides and broadcasts. The decision is that counter cycle: emperor > citizen > slave > emperor. The traitor cheats and only beats the emperor, losing to everyone else; the madman is even wilder, forcing a draw and triggering a random event. The emperor loses the moment he drops a losing hand; a slave who topples the emperor wins instantly; when the deck runs out, it’s over. On the round a traitor or madman shows up, cards are forced face-up, and the mind game gets tense immediately. The deck has two modes, standard and random, switchable by typing /set randomcard on|off in the console or via the DECK_MODE environment variable. If you don’t want to play online, there’s also an AiGame single-player mode against the computer.
Online play needs to recognize who you are. Registration and login go through POST /api/register and /api/login, returning an accountId and a signed token. The token is signed with HMAC-SHA256, valid for 24 hours, and stored in the session_tokens table. Users and logs land in MySQL as utf8mb4, with tables created automatically on startup. The frontend caches accountId and token in localStorage and carries the identity when connecting the WebSocket. There’s also AUTH_SECRET for the signing key, ALLOWED_API_ORIGINS to control CORS, and a set of keystore variables to enable WSS.
I hit a few pitfalls. Shade produces two jars, and you have to run the one with the -shaded suffix or you get “no main manifest”. Corner cases like echoing back the Origin for CORS and the WSS keystore turned out to have a lot of detail that only surfaced once it was actually live. The rules and the Nginx example are all written up in gamelogic.md in the repo.