Client server architecture

Mikolaj Konarski edited this page Nov 7, 2017 · 9 revisions

Introduction

Haskell effectively imposes the pattern of separating pure computation from IO effects. If you have any clue, you additionally keep the latter part minimal. That soon proved not enough, because a lot of the pure computation was about visualizing the game state and I obviously wanted to keep it separate from transforming the game state. Additionally, and that is a deliberate and arguable design decision, I wanted to keep the game arbiter separate from the game players and to have a common API for all game players, whether AI or human.

So I basically ended up with a MVC (or MVA) or perhaps it's better to call it a client-server architecture. Game arbiter is the server, game players are clients, they communicate over typed channels (will generalize that to network communication at some point; but even now that could give us, e.g., shared-screen multiplayer for free). Similarly, in principle, each game player (AI or human) can have a separate game frontend (there are a couple implemented), where the game player serves the visualization of the game state to the frontend, which displays it, handles mouse, tooltips, keyboard, etc., and sends back keystrokes. Again, there is a common API for all frontend kinds and again the communication channel is very narrow.

All this is quite limiting, but that's the point and the limits not only make our life miserable when we want to do something unplanned but, due to strong, expressive types, the limits actually catch a lot of low- and high-level mistakes, whether in code or in the protocol.

Preliminaries

In-game time

Time is discrete, but has enough granularity for any practical purpose and then some. Time is sampled at constant intervals called clips. Every clip, all possible actors moves in this clip are analysed in turn and affect game astate. At the end of the clip, if any discernible action occured, a screen frame is generated. Lastly, dungeon time is incremented by one clip and the process starts over.

A fixed number of clips make a turn, which represents half a second game time and is the time taken by an ordinary actor to move one tile (1 meter). UI communicates time to the player in turns or in natural time units, never in clips.

The dungeon as a whole has a time counter and each level has a totally independent time counter (but clips and turns have the same length everywhere). If the time of a level is not incremented while the dungeon time is, the level is called frozen.

Frozen levels

Each clip, the server determines which factions are still in the game and which of them have elected leaders. A faction can be in the game, but have no actors alive, e.g., a monster faction before the first monster is spawned. Morever, a faction may be inherently leaderless (e.g., an animal faction). Each level that has no leader on it is declared frozen.

No actions happen on a frozen level, but the level can be displayed and inspected. A frozen level can be thawed at any time, according to the game rules, and then the time starts to flow from the point it stopped flowing previously (it's not rewound in any way). Common causes of a level thaw are a leader ascending or descending to the level or a leader change to an actor on the level.

Move sequence

For each non-frozen level, that is one with at least one leader on it, the server initiates a move sequence involving all actors on the level (regardless if their faction has a leader on the level or not). Each move sequence is ended with incrementing the time of the level by one clip. When move sequences are completed for all non-frozen levels, the time of the dungeon is incremented by one clip.

All actors on a level with their age less or equal to the level time move in a sequence. If an actor is out of hit points, he dies. Otherwise, if an actor belongs to a faction controlled by a human player and the actor is a leader and so takes orders directly from the player, the server queries the human player client for an order to perform. Otherwise, the server queries the AI client of the human player or the AI client of the computer player for an order.

Server and clients

The game server knows the full game arena state and the extent of game arena knowledge accumulated by each of the clients. I particular, the server knows the Field of View of each client and uses the knowledge (together with other factors) to decide if a client notices a game state change or not. A client never receives even a single bit of knowledge on top of what the game rules determine his party can legally observe on the game board (excluding side-channels, e.g. the real-time timing of server messages, which can leak info about, e.g., the number of actor controlled by AI).

A faction controlled by a human player needs two clients to operate. One is the UI client that takes orders for the faction leader from the human behind the wheel. The other is an AI client that gives orders to the other faction's actors. The AI client is started and operated by the server, so that the human player can't cheat by micromanaging his actors. A faction controlled by a computer player has only the AI client, which controls all actors, including the leader.

Clients never communicate among themselves. All conversations can be seen as initiated by the server (the system is in server-push style) and involve one client at a time. In the future, clients will be allowed to send their orders in advance within the same clip (or possibly within some larger time period), to reduce the time spent waiting for other players (WIP). Right now clients let the human player automate running (for individual actors and in team formation), pathfinding, exploring and other tasks defined using a macro language and even give the control over to the AI client for a time.

Protocol of a single game move

The client requests the server to perform a game state change resulting from an actor's action. The server responds with atomic commands that represent the resulting game state changes. Then the server gets back to processing actor move order and, in turn, asks an appropriate client (AI client or UI client) to send it an order for the chosen actor. This query is the second (after the atomic commands) and last form of a server response.

The client has only one form of requests. When queried for an actor move, it sends back a context-dependent order, chosen based on its limited knowledge of game state. The server interprets that order in the context of the current full game state. E.g., moving north may imply attacking an actor, if there's any in the path, which may be unknown to the client, e.g., if the moving actor is blind or the attacked actor is invisible.

When the server generates a list of atomic commands that represent the effects of the move on the game state, it sends each command not only to the requesting client, but to all that can perceive the change (which may or may not include the client that sent the original request). In the process of determining where to send the atomic command, the server (most of the time) executes the atomic command on its own copy of the game state. This potentially changes the Field of View of actors or otherwise affects their ability to perceive the command. Then the server executes the command on its copy of client's game state for the clients that are candidates to receive the command (based of their FOV). If the execution succeeds, the command is sent to the respective client.

Each client that receives an atomic command performs it (most of the time) on its own copy of the game state and analyses and records the outcome (for the use of the AI algorithm or to present it via UI). The client may also store the received atomic command to enable undo/redo/replay (WIP). If the client is started in the same process as the server, optimizations are possible to avoid executing a command on a given client game state both by the server and then by the respective client.

Atomic commands and undo

The server uses atomic commands to communicate game state changes to the clients. Atomic commands are, more or less, just a notation for (a subset of) compressed (minimal or close to) difference sets between game states. Additionally they carry some intentional information, that is who and how made the state change. The intentional aspect can be used by UI and game logic (e.g., to hide some state changes from the players that were not directly involved and so wouldn't know how to interpret them).

Just like the text patch files produces by the diff utility, atomic commands are reversible. Consequently, if all atomic commands are preserved, the game has unlimited undo. Similarly to text patches, an atomic command can be applied in many contexts, though sometimes with a degree of 'fuzz', which can be used to detect internal inconsistencies or client cheating attempts. There is plenty of uses for he ability to rewind game state back and forth at will (WIP), but even unused, the feature constitutes a powerful constraint, guiding the design of the engine and of new commands.