Java 8 / TypeScript (Bun) / Python / MetaObjects
Three runtimes in three languages share one poker protocol — 155 messages declared once as metadata, 116 of them the 2005 engine's own. The TypeScript codec, the Python bot client and the docs are generated from that declaration, and a test gates it against the engine's source in both directions.
The poker engine is production Java from a real-money site that ran 2005–2009. It still deals every hand. Around it sit a TypeScript arena and a fleet of Python bots that are now language models playing for measurement rather than money — and all three have to agree, exactly, on what a poker message means. That agreement used to live in one 239-line constants file and in people's heads. It is now 155 declared messages, and the other two languages are generated from them.
A legacy engine's wire protocol is the most expensive kind of undocumented knowledge. It is load-bearing — every seat, every bet, every showdown rides it — and it exists as integers in a constants file, decoded by whoever last touched the code. Add a second language and you copy those integers. Add a third and you copy them again, slightly differently, and the bugs that follow are the ones nobody can reproduce.
FeltBots had exactly that shape: a Java engine that owns all game state, a TypeScript bridge translating TCP to WebSocket for browsers and bots, and Python bots making decisions. Three implementations of one protocol, kept in step by discipline.
The protocol is declared as MetaObjects metadata using one project-registered subtype, object.message — not a fork of MetaObjects, an extension of it, the same move the Wizards of Odd SSE envelope makes. Each message carries what it is for: which direction it travels, when it is sent, what the receiver does with it, and who is meant to see it.
What it deliberately does not carry is the Java wire format. The declaration describes the semantic protocol — what information moves between a player and a table — and encoding, sessions and sentinel values are handled in a later codec pass. The engine is a reference for what each message must carry, not the thing being transcribed.
From that one declaration, owned generators emit the TypeScript codec, the Python bot client, and the protocol documentation.
116 of the 155 messages carry the original engine's command number. The other 39 are arena-only, invented for the modern platform. You can tell which is which — but there is no @source attribute saying so, and that omission is the design:
# @code PRESENT -> the message rides the 2005 engine's COMMAND space
# @code ABSENT -> arena-only, no engine counterpart
# provenance is DERIVED from that, never duplicated into a second attribute
A second attribute would be a second thing to keep true. One of them would eventually be wrong, and nothing would catch it. The same instinct shows up where several messages share a single command number: each payload shape is its own declaration with its own discriminator, and codegen fails closed if two shapes share a number without distinguishing themselves. An ambiguity that would have become a runtime mystery is a build error instead.
Doug works directly with teams evaluating MetaObjects. Tell us what you're building and we'll tell you, honestly, whether MetaObjects fits.
Email doug@dougmealing.com