Sanguosha, or loosely translated as "Battles of the Three Kingdoms", is a multiplayer turn-based board game. This project implements Sanguosha as an online multiplayer game.
Implementation: It includes both the game server written in Java and a game client implemented with Java Swing library. The server and client communicate via Java Socket.
Size: As of 06/11/2022, about 20K lines of code.
Click to see details
Sanguosha was a popular board game in China. The game is based on the historical events of the Three Kingdoms, a historical period in ancient China around 220–280 AD. See also Sanguosha official website (in Chinese).
The core part of the game is the variety of heroes and their skills, which by an implicit rule maintain that the name and description of a hero's skill should more or less reflect the historical facts about the hero. Many even learned a lot about the history of the Three Kingdoms through playing different heroes. In the early 2010s, many players of the game invented their own versions of Sanguosha, in which heroes are replaced with friends, family members, celebrities, etc. Most of these "heroes" also abide by the rule of accuracy of facts. For example, a student with skill "diligence" (whose effect may be "drawing extra X cards each turn"), a teacher with skill "homework", or perhaps Obama with skill "medicare" (description may be "let a player discard 1 card to regain 1 HP when their HP is below 2").
However, most of these designs were limited to a graphic design of the hero card, as actually printing and playing these hero cards were infeasible to most people. The player base was primarily students busy at schoolwork who had little time to gather for a game, which usually lasts more than 20 minutes. Additionally, due to the complex system of rules of Sanguosha, face-to-face games are prone to human errors (e.g. forgetting to trigger a passive skill), which sometimes can be very frustrating. As an example, see this page, also in Chinese, for a detailed breakdown of rules and game flow.
These issues were the original motives of my project. A big fan of Sanguosha, I wanted to build a secure, consistent, and extensible Sanguosha framework that allowed my friends and I to play online with few errors in game flow and allowed easy addition of custom "heroes" and skills.
- Run Server.java without arguments
- Run any number of Client.java without arguments.
Both the game server and Java Swing client are MVC based and event-driven to maximize extensibility and modularity. Several components are used by both server and client because of their similarity.
The game essentially evolves around the states of its Player
s, each of whom has a Role
, a Hero
, some Card
s, and some statuses such as Health Points. The game ends when a player of certain Role
dies, see game-end conditions.
The majority of player information is public (e.g. a player's Hero
, Equipment
s, HP) to all players. The only information private to a player themselves is their hand (of cards) and their Role
(except the Emperor), which is revealed only upon death. As such, a player sees other players as SimplePlayer
s without Role
and hand information, and themselves as a CompletePlayer
with all information available.
- At server side, all
Player
s are initialized asCompletePlayer
as server is omniscient. - At client side, only a player themselves is initialized as
CompletePlayer
, while all other players are represented asSimplePlayer
.
The view on both server and client is represented by a number of Listener
s on Player
reacting to state changes such as receiving/losing Card
s, gaining/losing HP.
- At server side, Server-side Listeners are synchronizers that simply send
SyncCommand
s to clients to update client-side player statuses, see example. - At client side, Client-side Listeners are GUI components that visually shows the statuses to the human player, see example.
- At server side, the game employs a Stack of
GameController
s that control the gameflow and modify Players, each with aStage
to track the progress of its own lifecycle. See an example. - At client side, there is no "Controller". The client is designed with Server-driven UI, specifically, only a
SyncCommand
from server may modify client-side Model, which must always be consistent with server.
Sanguosha is characterized by a turn-based game flow with many actively or passively triggered Hero Skills and Equipment abilities, which frequently interrupt the game to wait for player actions to proceed.
At server side, the game continuously drive the current GameController
(which may push other GameController
s on top of itself), until a player action is required and the GameController
throws a GameFlowInterrupedException
to "pause" the game and send a PlayerActionGameClientCommand
to request player action. The player may respond with an InGameServerCommand
(or server takes a default response upon timeout), which "resumes" the game until the next player action is required.
The design concerns are listed below ranked by importance (high to low):
- Extensibility: The framework must be extensible to allow easy addition of new heroes, skills, features, etc.
- Security: The game must avoid cheating, leaking, and sniffing for personal information, etc.
- Recovery: Must be internally tolerant to errors like race conditions, and externally tolerant to user failures like internet disconnection.
- Performance: Minimize network usage, memory & CPU usage, and avoid memory leaks.
- Learnability/Maintainability (of the codebase): The framework must be intuitive to learn and minimize errors due to human oversights.
A few other common design concerns have been deprioritized for various reasons, listed below:
- Scalability: As a turn-based game, the frequency of client-server communication is significantly lower than a real-time game. If server capacity is reached, we can move ongoing games (the most traffic-intense part) to dedicated servers.
- Cross-platform API: For the first iteration, the game is built with a Java Swing client communicating with server via Socket. In future iterations, a generic API for cross-platform clients (e.g. Android/iOS, web, PC/Mac native app) can be built on top of the existing framework.
- Unit/Integration Testing: In my opinion, testing is necessary for a real-world online game, but the workload overhead is beyond the capability of one developer.
Additionally, the game client's UI and UX are deprioritized, because (1) the first iteration focuses on functionality and proof of concept, and (2) players of this game should already know how to play without hints, as they should have played the in-person board game already.
The MVC architecture is built so that new heroes and skills can be added quickly, with minimum edits to existing code. For example, see the commits for:
- Wei Yan, a hero with a passive skill (no player action). 100 lines.
- Huang Zhong, a hero with a passively-triggered skill (player must confirm). 150 lines.
- Yuan Shao, a hero with an active skill (player proactively activates). 240 lines.
The game's security consists of 3 parts, Anti-cheat, Anti-leak, and Anti-sniff.
-
Anti-cheat: there are
-
Illegal Player Actions. A player may attempt to set an invalid target, use a Card/Equipment/Skill they do not have, use fewer/more than required Cards, etc.
- Solution 1: Set Allowed Action Types to accept only valid actions. See example.
- Solution 2: If an action type is allowed, also sanity-check the action. See example.
-
Game Flow Disruption. A player may attempt to act when not allowed to, or the game may receive a previously valid but currently illegal action due to network delay.
- Solution: Send a server-generated Response ID to accept actions only from players who may act. See example.
-
Impersonation. A player may attempt to act on behalf of another player.
- Solution: Set action's source at server-side.
-
-
Anti-leak: The game does not send to players any information they should not know of. For example, a player's hand is private, so when a player receives/loses a Card, only that player is sent the concrete Card information, whereas other players only receive a command to increment/decrement that player's card count.
-
Anti-sniff: The client-server communication is currently unencyrpted, because no password information is present. However, it is therefore vulnerable to man-in-the-middle attacks. See TODO list.
If a player disconnects while in-game, the server marks them as disconnected, and if they reconnects in time, they will be redirected back into the game and sent the latest game states, so that they can resume playing. If a player disconnects while not in-game, they're considered logged out.
As a turn-based game, the server sends frequent updates to clients while in-game, while clients only send sporadic actions to server.
- Throttling: Server periodically flushes all updates sent to a client. Current "server tick" interval is 0.1s
- Aggregated Request: All updates to be sent to a client during a server tick is combined into a single request
As the project grows in size, components must be easily understandable and built to avoid human errors such as using the wrong method by accident. For example:
- AbstractMultiCardMultiTargetOperation: A generic template that controls how the player interacts with game UI. Most player actions can be easily and correctly implemented this way, see example.
- Build generic JSON-based API for clients on different platforms
- Add HTTPS-like encryption for client-server communication
- Cover major game flows with integration tests
- Add CPU & Memory monitoring and create alerts
- Synchronous Execution. Make sure to execute updates sent by the server in chronological order
- Prettify client UI and add animations
- Allow Chats