← Back home

The Empty Seat

How two guys from an 80s game studio gave a family card game an AI that talks trash


In the late 1980s, my friends Jim, John and I started a small company called Banana Development, building video games for the original IBM PC. You can still find our titles on MobyGames and Hardcore Gaming 101. Then life did what life does: Jim became a VP at Merritt Industries and ran their development team; John went on to work on titles at Electronic Arts. I spent two decades building the deck designer used in every Home Depot in the US and Canada. Different roads, same trade.

A couple of years ago, Jim was in town and invited me to dinner with his whole family — his three sisters, his mom and dad, everyone, which was a small miracle given how scattered they are. After dinner we all played Hand and Foot, the card game they'd played since Jim and his sisters were kids.

The next day we started talking about building it as a video game. Not to sell. Just so the family could keep playing when they weren't in the same room.

Forty years after Banana Development, we were making a game together again.

Jim starts, I pile on

Jim wrote the first version the way you'd expect from a man who learned C# before async existed: Windows Forms, because that's what he knew. When he wanted better graphics than GDI could give him, he found unvell.D2DLib, an open-source wrapper that puts hardware-accelerated Direct2D inside a WinForms app. It's a great little library and it let him keep his whole mental model while the cards got smooth.

Once he had his jumpstart, I joined in. I did what the other half of an old two-man studio does: I got the project into git, then grafted on a demo project I'd been building that already had a database layer and basic user handling. (The database is a story of its own — I built it Cosmos DB–compatible, but underneath it's a JSON file record store. Someday it'll be real Cosmos. Someday.)

We split the work along old, comfortable lines. I built login and registration; Jim built the lobby and game-creation forms; together we designed the data structures — cards, game tables, teams — and the web services. Jim wired the services into the game. The architecture was client-centric: each player's client owned its state, and the server mostly pushed changes around.

That design decision would come back to bite us.

SignalR and the async tapeworm

Real-time card play needs push messaging, so we brought in SignalR. And SignalR brought in async. And async, in a codebase written by a man whose C# instincts predate the await keyword, was less a feature than a tapeworm — it got into one method and then it had to get into everything that method touched, and everything those methods touched, all the way up.

The threading bugs arrived like an invasive species. Race conditions between the render loop and the game state. Duplicate turn execution from rapid SignalR callbacks. A deal that took 30 seconds on a slow machine because the SignalR handshake blocked it. We fixed them one at a time, and slowly I pushed the architecture toward server-authoritative state — the server owns the scores, the deck, the vote to start the next round — because every client-owned piece of state was one lost message away from a desync.

Here's the part I'm most proud of, and it isn't code. Jim is protective of his game logic, and he's earned the right to be. So instead of bulldozing it — which is what we developers usually do to each other's work — I encouraged him to get the logic out of the Forms classes, and then I subclassed the entire thing. Today the game ships two engines side by side: Original (Jim's, untouched) and V2 / Enhanced (mine, server-authoritative, decomposed into managers). Both play at the same tables against the same server, and part of my job is keeping them coordinated. When I fix something on the V2 side that has a V1 twin, Jim gets a written note — what it is, how to fix it if he wants to, and confirmation that his code was not touched.

A forty-year friendship is worth more than a clean architecture diagram.

The empty seat problem

Hand and Foot wants four players. Getting four family members online at once is hard; getting four players online while testing at 2 a.m. is impossible. We solved it in three generations:

Autopilot. Jim built an in-game AI — flip a switch and your seat plays itself, right inside the UI. Born as much for testing as for filling seats.

The Bot. I broke the autopilot out into a headless program. Same deterministic strategy — play red threes, build clean melds, dump dead weight — but it needs only the server, not the game client. Now we could stand up a full table of robots and stress-test the server all night.

The Grok player. The bot could fill a seat, but it couldn't fill the room. So we built a third player type that uses Grok (xAI's LLM) to make its decisions — and, more importantly, to run its mouth.

How you let an LLM play cards without letting it cheat

Here's the architecture lesson the whole project taught me, and it's the reason this post exists:

Never put the LLM in charge. Make it a talented, untrusted advisor.

The C# host does all the rule work. Each time it's the AI's turn, the host computes the legal moves — every meld that could be started or extended, every discardable card — and sends Grok a snapshot: your hand, the table, the scores, the legal menu, and a set of strategy hints. Grok's entire job is to pick one item off the menu and say something entertaining about it. The response is locked down with a strict JSON schema (structured outputs), so a malformed answer is rejected before it ever reaches the game.

And then — this is the important part — the host validates the move again anyway. Card actually in hand? Ranks match? Wild-to-natural ratio legal? Meld meets the round minimum? If anything fails, the cards bounce back and the turn is salvaged.

Salvaged by what? By the deterministic bot, which still lives inside the Grok player as a fallback. There's a comment in the source that sums up the whole philosophy:

Grok is an ENHANCEMENT (personality + judgement), never a single point of failure: when it can't answer we still play a competent, rules-based turn instead of wasting it.

Timeout, 500 error, nonsense move — doesn't matter. The seat always plays a legal, competent turn. The LLM makes it interesting; the rules engine makes it reliable.

The subtler lesson: when the LLM plays badly, don't beg it in the prompt — shape its choices. Early on, Grok kept "dirtying" clean melds, spending wild cards where three natural cards would do (a clean book scores 500 to a dirty book's 300, so this genuinely hurt). We tried strategy hints. What actually fixed it: stop offering wilds in the legal-move list except in the specific situations where spending a wild is correct. You can't talk an LLM out of a temptation nearly as effectively as you can remove the temptation from the menu.

The personalities

The chat and bot personas are recent additions, and they're what changed the AI from a seat-filler into a character at the table.

Each AI seat can carry a persona brief, and the system prompt tells Grok to fully embody it. The guardrails took real tuning: the bot may banter at most once per turn; it replies to humans but never to other bots (one fromBot flag is all that stands between a three-bot table and an infinite loop of robots heckling each other); it's told who its teammate is so it never taunts its own partner; it gets the live score with every quip so it can't talk smug while getting crushed; and it is forbidden from bragging about cards it didn't actually manage to play — or from leaking the cards still in its hand.

The family's reaction has been everything we hoped. Jim's wife found Ara interesting — Ara runs a little racy. And Brianna got paired with HAL, whose persona brief says, in essence, that this player is never going to win. Nobody got the joke until they looked it up. They laughed. The personas stayed.

What the family actually thinks

They love the game. Their one complaint has nothing to do with cards or AI: it's the downloads. We auto-update, and it's still friction for the older folks. So the next chapter is already in progress — a pure-web Blazor version, no install at all. From "download this" to "click this link," because for Mom and Dad, that's the killer feature.

The game found a second life, too: a group of gamers, Jim among them, plays it every Friday night as the wind-down game after the louder stuff. When people outside the family choose your game to end their night with, it's not just being loved out of loyalty.

The banana

One more thing. While building V2 I wanted animations, and that side quest became BananAnimation — an open-source tool that takes AI-generated video (Grok's Imagine, in my case) and, with FFMPEG and ImageMagick, turns it into transparent spritesheets a game can use.

Which is why, when you win a round in V2, an animated banana dances. It has about ten idle animations, because the pipeline made them cheap to produce.

That banana is Banana Development — the company two kids started in the 80s to make PC games — resurrected as a sprite, in a game those same two guys built in their sixties, for a family that just wanted to keep playing cards together.

The empty seat was never really a testing problem. It was a missing-person problem. Now the seat is filled, it plays a decent game, and it talks a little trash.

Mom approves.


A footnote about the tools, told straight because it's simply true: this game was built by two humans and a staff of AIs. Grok fills the fourth seat and voices the personas. Claude has been a major contributor to the V2 engine — the threading war was won with an AI pair programmer in the trenches — and it coordinates the other coding agents through a set of shared house rules in the repo. Forty years ago we shipped games by ourselves. It's a different era. We like this one.

Hand and Foot lives at the Banana Development alumni site. BananAnimation is open source at GitLab.

An unhandled error has occurred. Reload 🗙