Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AbraTabia Game Server

A self-hosted matchmaking and turn sync server for turn-based multiplayer games. One small PHP app, one SQLite file, no websockets, no game engine lock-in. The server holds zero game rules, your clients define what the moves mean, and the server handles finding opponents, private lobbies, and delivering every action to every player in order. Any device that can make an HTTPS call can use it, so it works for web games, Unity, mobile, or anything else that talks to the web.

Project page: AbraTabia Game Server on abratabia.com

What you get

  • Public matchmaking: players join a queue with one or more game modes and a party size, and the server groups compatible players into a match. Modes are just strings your game invents, like "1V1", "RANKED", or "TICTACTOE".
  • Private matches: a player opens a lobby and gets a short join code to share. Friends join with the code and the match starts when the lobby is full.
  • Matches of 2 to 10 players: party size is part of matchmaking, so 4 player free-for-alls queue separately from duels.
  • Ordered action log: each move is an action with a type and three opaque data fields. Clients poll for actions newer than the last one they saw and always receive them in order, so every client replays the same game.
  • Player state recovery: a client that reconnects can ask where it is (matched, waiting, or nothing) and re-pull the full action log, so a dropped phone connection does not kill the game.
  • Self-cleaning storage: queue entries and abandoned matches expire on their own, no cron needed.
  • Admin area: watch the live queue and active matches, inspect any match's players and full action log, and end stuck matches.
  • Demo game: a playable two-player tic tac toe page. Open it in two browser tabs and the tabs match against each other through your server.
  • Legacy split-string mode: every endpoint can answer in a plain text comma-and-newline protocol instead of JSON, made for clients that would rather .Split(',') than parse JSON (this is how the original Unity integration works).

Quick start (local)

Requires PHP 8.1+ with the pdo_sqlite extension (standard almost everywhere).

cp config.sample.php config.php
# edit config.php: set apiKey and adminPassword
php -S localhost:8080 -t public

Then open http://localhost:8080/demo.php in two browser tabs, enter your API key in both, and click Find Public Match in each.

Quick start (Docker)

cp config.sample.php config.php
# edit config.php as above
docker compose up --build

The app is at http://localhost:8080/ with the same admin and demo pages.

Shared hosting

Upload the project and point your docroot (or a subdomain) at the public/ folder. If you can only upload into an existing docroot, upload everything into a subfolder and the API lives at yoursite.com/subfolder/public/api.php. The SQLite database is created automatically in data/.

Project structure

config.sample.php   copy to config.php, holds keys and settings
public/
  api.php           the API your game clients call
  admin.php         admin area for the queue, matches, and action logs
  demo.php          playable tic tac toe demo (two tabs = two players)
lib/
  gameserver.php    matchmaking, lobbies, and action sync core
  db.php            SQLite storage layer
data/               SQLite database lives here (gitignored)

Configuration

Everything is in config.php (copied from config.sample.php):

Setting What it does
apiKey the key game clients must send with every API call
adminPassword password for admin.php
corsOrigin which browser origins may call the API, "*" or your game's origin
dbPath SQLite file location
queueSeconds how long a waiting queue entry lives (default 120)
matchSeconds how long a match lives with no new actions (default 3600)
maxPlayers largest match size a client may request (default 10)

API

All endpoints are POST with a JSON body (a form post works too). Send your key in the X-API-Key header, or as an apiKey field in the body. Paths use path info, and if your server does not support that, api.php?action=join works the same as api.php/join.

POST api.php/join

Join public matchmaking, or open or join a private lobby.

curl -X POST http://localhost:8080/api.php/join \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{"playerid": "player-abc123", "gameMode": "1V1", "numPlayers": 2, "username": "Paul"}'

Request fields:

Field Notes
playerid required, your client's unique ID for this player (3+ characters)
gameMode one mode or several joined with &, like "1V1&2V2". Players match when they share a mode. Default "1V1"
numPlayers how many players this match should have, 2 to maxPlayers, default 2
username optional display name shared with the other players
playerInfo, talentInfo, charInfo optional opaque strings shared with the other players, for loadouts, decks, skins, whatever your game needs
private send "1" to open a private lobby instead of public matchmaking
joinCode join an open private lobby by its code. With private: "1" and no code, the server generates one for you
timeLimit optional string stored on the match for your clients to read

Response: either a matched object (see below) if enough compatible players were already waiting, or:

{"status": "waiting", "numPlayers": 2}

Private lobbies also return joinCode and playersWaiting. After a waiting response, poll status every couple of seconds.

POST api.php/status

Where is this player right now? Fields: playerid. Returns one of:

  • A matched object, the same shape join returns when a match forms.
  • {"status": "waiting", ...} still in the queue (private lobbies include joinCode and playersWaiting).
  • {"status": "none"} not in a queue or match, either the queue entry expired or the player never joined. Re-join to try again.

The matched object:

{
  "status": "matched",
  "matchid": "g17529339421234",
  "gameMode": "1V1",
  "numPlayers": 2,
  "yourPlayerNum": 1,
  "timeLimit": "0",
  "private": 0,
  "joinCode": "",
  "lastActionID": 0,
  "players": [
    {"playerNum": 0, "username": "Paul", "playerInfo": "", "talentInfo": "", "charInfo": ""},
    {"playerNum": 1, "username": "Ana", "playerInfo": "", "talentInfo": "", "charInfo": ""}
  ]
}

yourPlayerNum is this player's slot. A common convention is that slot 0 moves first, but that is your game's call.

POST api.php/action

Send one game action to a match.

curl -X POST http://localhost:8080/api.php/action \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{"playerid": "player-abc123", "matchid": "g17529339421234", "actionType": "move", "data1": "e2e4"}'

Fields: playerid, matchid, actionType, and optional data1, data2, data3, totalTime. All data fields are opaque strings, the server never interprets them. Actions like quitting, offering a draw, or chatting are just action types your game defines. Returns {"status": "ok", "actionID": 1752933999}.

POST api.php/actions

Poll for actions newer than the last one this client has. Fields: matchid, since (the highest actionID already received, 0 for everything).

{"status": "ok", "count": 1, "lastActionID": 1752933999, "actions": [
  {"actionID": 1752933999, "player": 0, "actionType": "move", "data1": "e2e4", "data2": "", "data3": "", "totalTime": "0"}
]}

Send the returned lastActionID as since on the next poll. The list includes your own actions too, which doubles as delivery confirmation. Polling every 1 to 3 seconds is plenty for turn-based games.

POST api.php/leave

Leave the matchmaking queue. Fields: playerid. Returns {"status": "left"}. Leaving a match already in progress is a game-level event, send an action your game defines (like actionType: "quit") so the other players hear about it.

The flow at a glance

  1. Each client joins with a playerid you generate, a gameMode, and a numPlayers.
  2. Clients that get waiting poll status until they get a matched object.
  3. Everyone learns their yourPlayerNum and the other players' info from the matched object.
  4. Players take turns: send an action, and poll actions to receive everyone's moves in order.
  5. Your clients decide what actions mean, whose turn it is, and when the game ends.

Legacy split-string mode

Add format=legacy to any call (query string or body field) and responses become plain text lines instead of JSON, matching the original Unity integration this server grew out of. JSON field names are unchanged on the request side, and the legacy field names currentActionID (for since), multirequest (as actionType=data1=data2=data3), and totalMoveTimer are also accepted.

Responses:

Call Legacy response
join, status matched: line one is 1,matchid,numPlayers,yourPlayerNum,timeLimit,gameMode,private,0, then one line per player slot as playerInfo*talentInfo*charInfo. Waiting: 2. Not anywhere: 3. Error: 0,message
action 1 ok, 0 error
actions one line per action as actionID,player,actionType,data1,data2,data3,totalTime, or OK when there is nothing new
leave 3

A minimal Unity example using the legacy mode:

IEnumerator JoinQueue() {
    WWWForm form = new WWWForm();
    form.AddField("apiKey", "YOUR_KEY");
    form.AddField("format", "legacy");
    form.AddField("playerid", myPlayerId);
    form.AddField("gameMode", "1V1");
    using (UnityWebRequest req = UnityWebRequest.Post(baseUrl + "/api.php/join", form)) {
        yield return req.SendWebRequest();
        string[] lines = req.downloadHandler.text.Split('\n');
        string[] header = lines[0].Split(',');
        if (header[0] == "1") { matchId = header[1]; myPlayerNum = int.Parse(header[3]); }
        else if (header[0] == "2") { /* waiting, poll status */ }
    }
}

Note that the legacy format uses commas, asterisks, and newlines as separators, so keep those characters out of playerInfo, talentInfo, and charInfo if you use this mode. JSON mode has no such restriction.

The demo game

demo.php is a complete two-player tic tac toe built on nothing but the five API calls above, in one page of plain JavaScript. It is the fastest way to check your setup and a working reference client: join, poll status, render state from the action log, send moves. Open it in two tabs, or on two devices, and play.

Security notes

  • Set apiKey and adminPassword to long random strings before exposing the server.
  • A key shipped inside a browser game is visible to players. That lets anyone call your API, which for a game server mostly means they could fill your queue, so lock corsOrigin down and keep queueSeconds modest, or route calls through your own game server so the key stays private.
  • The server trusts clients on game rules by design. For casual play between friends that is fine. For competitive games, have your clients validate each other's moves from the shared action log and end the game on any illegal move.
  • Serve over HTTPS in production.

Roadmap

  • Optional server-side turn enforcement (reject actions out of turn order).
  • Spectator access to a match's action log.
  • Skill brackets for public matchmaking (the protocol already reserves the field).

Author

Paul Crinigan, https://www.aiappsapi.com/

More from the author

License

MIT, see LICENSE.

About

Self-hosted matchmaking and turn sync server for turn-based multiplayer games. Public matchmaking, private join codes, ordered action log over plain HTTPS. One PHP app, one SQLite file, no websockets.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages