DEV Community

Paul Crinigan
Paul Crinigan

Posted on

A Turn-Based Multiplayer Server That Fits in One PHP File

Adding online play to a turn-based game usually stalls on infrastructure rather than on gameplay. The moment you decide two people should be able to play your chess or card game from different machines, the conversation turns into websockets, a persistent process, a host that will keep that process alive, and a lobby system you now have to design. None of that is about your game.

For turn-based play, almost all of it is avoidable. A turn game does not need a live socket, it needs an agreed order of moves. That single reframe collapses the problem into a handful of ordinary HTTPS requests, which is the design behind the free server at https://www.abratabia.com/AbraTabia-Game-Server/.

Why Turn-Based Games Do Not Need Websockets

A websocket earns its keep when milliseconds matter. In a shooter or a racing game, the difference between a 30 millisecond update and a 300 millisecond update is the difference between a playable game and an unplayable one, so you accept the cost of a persistent connection and a process that has to stay alive to hold it.

A turn game has no such deadline. Nobody notices whether their opponent's move arrives 200 milliseconds or two seconds after it was made, because they were reading the board anyway. What players do notice is a move arriving out of order, or a move that one client saw and another did not. Ordering is the hard requirement, latency is not.

Once you accept that, polling stops being a compromise and starts being the correct tool. A client asks for anything newer than the last thing it saw, every couple of seconds. Each request is an ordinary HTTPS call that returns immediately, so there is no long-lived connection, no process to supervise, and no special hosting requirement. It runs on shared hosting, a small VPS, a container, or a laptop during development.

How Matchmaking And Join Codes Work

Public matchmaking is a queue with three inputs: a player ID your game generates, one or more game modes, and a party size. Game modes are strings your game invents, so RANKED, BLITZ, and CASUAL all mean whatever you decide, and a player can queue for several at once to say "either of these, whichever fills first". The server groups compatible players and forms a match anywhere from a two player duel to a ten player free-for-all.

When the match forms, each player learns their player number, the agreed mode, and the other players' display names plus a few free-form fields for things like decks, characters, or skins. Players still waiting poll a status endpoint until their match appears, and a queue entry that never fills expires by itself, so stale entries do not pile up.

Playing with friends skips the queue entirely. One player opens a private lobby and gets back a six character join code, generated to avoid look-alike characters so it survives being read out loud over voice chat. Friends join with the code, and when the lobby reaches the size its creator chose, the match starts. There are no accounts on the server, so the code is the entire invitation.

The Ordered Action Log Is The Whole Sync Model

Once a match is running, gameplay reduces to two calls. A player sends an action, the server assigns it a permanently increasing action ID, and every client asks for actions newer than the highest ID it has already seen. Every player receives every action in the same order, which is the property that makes deterministic turn-based games work: each client replays the same log and arrives at the same state.

The interesting consequence is that reconnection comes for free. A player whose phone dropped its connection asks where the match is, gets the state back, and re-pulls the log from zero to rebuild the board. There is no separate resync path to write, because rebuilding from the log is what every client already does on every update.

Actions also cover more than moves. Because the types are yours, a resignation is an action, a draw offer is an action, an emote is an action, and your clients decide how to react to each one. The server stores an action as a type plus three opaque data fields, which is why it can hold zero game rules and still run a chess game, a card game, and a word game on the same install at the same time.

What It Runs On

The requirements are PHP 8.1 or newer with pdo_sqlite, which is standard almost everywhere. Copy the sample config, set an API key and an admin password, and the SQLite database creates itself on first run. Docker users run docker compose up instead, and shared hosting users upload the project and point a docroot at the public folder.

The demo is the fastest way to understand it. Open demo.php in two browser tabs and they will match against each other through your own server in about ten seconds, playing a complete two-player game of tic tac toe built on nothing but the five API calls. It doubles as a reference client, one page of plain JavaScript showing join, poll, render from the log, and send.

The client side is whatever your project already is, since the API is plain HTTPS with JSON. A browser game uses fetch, Unity uses UnityWebRequest, Godot uses HTTPRequest, and native mobile apps and Discord bots speak the same five endpoints. There is also a comma and newline separated response format for clients that would rather split strings than parse JSON.

The Takeaway

If your game takes turns, the multiplayer problem you actually have is agreement on order, not speed of delivery. Solving the smaller problem lets you drop the persistent process, the websocket layer, and most of the hosting requirements that make people abandon online play before they start it.

The whole project is MIT licensed and the source is at https://www.abratabia.com/AbraTabia-Game-Server/, including the getting started guide, the API reference, and the walkthrough that builds the demo game line by line.

Top comments (0)