From Terminal to UI

A week ago I played my first game of Magic in the terminal. Now I'm playing actual games in TCG Lightning — cards on the stack, paying mana costs, the whole thing — all driven by a TypeScript client talking to XMage through the proxy.

Playing against myself — two TCG Lightning clients connected to the same XMage server.

What Is XMage?

For those who don't know — XMage is a Java-based client/server that provides full Magic rules enforcement and an automated stack. It's open source and has been around for years. The goal of my integration is to let TCG Lightning connect to XMage servers so you can play games with proper rules enforcement directly from the app.

The Bridge So Far

The tcg-lightning-xmage-bridge has come a long way since the last post. The proxy now handles login, lobby interactions, table creation & joining, and full game sessions. On the TypeScript side there's a callback API — you subscribe to events like onChatMessage, onGameUpdate, or onSessionExpired and get properly typed payloads back.

Rethinking Server Messages

There are a lot of different messages coming from the XMage server, and many different things the UI needs to do when they arrive. I started by passing the raw objects straight through to the client, but honestly the server messages leave a lot to wish for when it comes to building a modern client on top of them.

So I'm now splitting them up — providing more fine-grained events that let a consumer handle each situation in a specific way. The client library supports both approaches: if you want the raw, unprocessed server events you can subscribe to those, but if you want something more modern and granular, the library breaks things up into distinct, well-typed events. I think this makes building a UI on top of XMage much more pleasant.

Protobuf for the Wire

I'm also introducing Protocol Buffers into the message layer. Right now messages go over the wire as JSON, which works but is verbose. Protobuf will shrink the transfer size and — just as importantly — let me generate types for both the Java proxy and the TypeScript client from a single schema. Less duplicated code, fewer drift bugs, one source of truth for all message types.

What's Next

There's still a lot to handle on the client side — properly converting all the remaining server messages, rendering game state changes smoothly, and polishing the UI. But the core loop works: I can play cards, they land on the stack, I can pay costs, and the game progresses. Right now I'm playing against myself (someone has to be my first opponent), but maybe soon I'll be playing against you. If you want to follow along, the bridge repo is where it's all happening.

Get In Touch

Join my Discord — I'd love to hear from you ❤