MateMate · An all-Swift chess stack

Every move answers to the server.

A SwiftUI iPhone app, a hand-written engine, and a Vapor multiplayer backend — three programs sharing one Swift package, so the client, the server, and the wire can never disagree about what chess is.

3build products, one package
9 + 11wire messages, client / server
±100 Elopairing window, widening 10/s
60 sgrace before abandonment
System map

Three programs, one vocabulary

Everything is Swift. The app and the server both link ChessKit, so “is this move legal?” has exactly one answer on both sides of the socket. Select a module to inspect it; its dependencies light up dashed.


Life of a move

1. e4 — traced end to end

The client never moves its own pieces. It sends a frame, the GameCoordinator actor rules on it, and the board only changes when the verdict comes back. Step through the honest case — then flip the toggle and watch the server swat away an injected illegal move.

Client A · White
OnlineGameSession → WebSocket /play
chess-server
routes.swift → GameCoordinator
actor GameCoordinator — all realtime state lives here
Client B · Black
local ChessKit mirror


The wire

One protocol, compiled twice

The ChessOnline target is the single source of truth for every message — linked by the app and the server, so the schema cannot drift. Each frame is self-describing JSON with a "type" discriminator. It deliberately depends on nothing, not even ChessKit: strings and UUIDs only, so the protocol stays stable while engine types evolve.


The engine

A negamax you can test to the node

NegamaxEngine is 465 lines of alpha-beta search over ChessKit’s evaluation. Fixed-seed Zobrist hashing and zero randomness (unless an opening book is attached) make it deterministic — regression tests pin exact node counts, not vibes. A search visits these stations in order:

    GameReview · move judgment

    Inaccuracy
    87 cp loss

    Exactly the thresholds in GameReview.swift — <25 best · <60 good · <120 inaccuracy · <250 mistake · else blunder. The evaluator is pluggable: the default is a cheap one-ply lookahead; the app’s ReviewView plugs in the real engine for trustworthy numbers. Works identically for engine games and online games.


    Design decisions

    Three rules the whole system leans on

    The server is authoritative

    Clients mirror the game locally for SAN and highlights, but every online move is legality-checked server-side with the same ChessKit before it’s broadcast. Clocks, timeouts, draw offers, Elo, and results are all enforced inside one actor — clients are mirrors, not referees.

    One wire protocol, compiled twice

    The ChessOnline target is linked by both the app and the server, so client/server messages have a single definition and no JSON drift. It depends on nothing — strings and UUIDs only — so the wire stays stable as engine types evolve.

    The engine is deterministic

    Fixed-seed Zobrist hashing, no randomness unless an opening book is attached. Search behavior is testable down to exact node counts, which is how blunder-detection and search regressions stay pinned in CI.