I am very excited to announce my newest project: Dracula, the card game . My older brother taught me the rules of this game, created by David Parlett , back in July, and I thought turning it into a standalone web app seemed like an interesting way to explore machine learning and use some new tools such as AWS Lambda.

Dracula card game interface showing a round in progress

Dracula is played over six rounds using a standard deck of cards, including two jokers. Players take turns placing cards in a shared 3x3 grid known as the coffin. The Queen scores its rows and the King scores its columns. Matching suits and colors increase line scores, while jokers, also known as Vampires, make the row and column in which they land count for zero. To do well, a player has to balance defense and offense while avoiding the mistake of setting up their opponent for a high-multiplier combo.

Creating a Card-Playing Villain

After creating a deterministic game engine to manage card dealing, legal moves, and round scoring, the challenge was to create an AI strong enough to serve as an interesting opponent. I used two components: a neural network trained to play the card game and an LLM cued to provide the voice of Count Dracula.

To generate training data for the neural model, I used an adapted version of UCT, Upper Confidence Bounds applied to Trees. It selects moves by simulating possible continuations and evaluating their results. In this implementation, each search runs 128 simulations of the remainder of the current round to produce a single choice.

The search builds a tree in which each node represents a decision state visible to the searching player, and each outgoing branch represents an available move. It records how often each move is tested and the average difference between the two players’ round scores under a hypothetical continuation. An exploration parameter balances testing less-explored moves with further testing of promising moves.

This algorithm was made famous in perfect-information games like Go and chess, but applying it to a card game where much of the game state is hidden involves an additional wrinkle. The simulation relies on randomly sampling a hidden state based on the known game information and scoring possible moves based on these educated guesses. The choice of how the opponent plays these imaginary hands affects the quality of play pretty dramatically. This is called the continuation policy.

With fully random opponent placement, the resulting UCT policy was extremely weak: it played only offensively and would let me complete same-suit combos without attempting to block them. After a lot of experimentation, I settled on a continuation policy I called belief-greedy: something like a less expensive version of the outer UCT search.

For each continuation move by the opponent, belief-greedy samples eight plausible hands and evaluates every legal move against those samples. For each candidate move, it fills the remaining coffin spaces with the remaining cards in a deterministic order. It then scores the completed coffin and computes the score difference. The move with the highest average difference across the eight samples is selected.

Training the Neural Opponent

The problem with UCT is that it can be computationally expensive. The trick is to curate a large dataset of positions evaluated by the slow search algorithm and then train a lightweight neural model to mimic those results. I mined an initial dataset of around 500,000 rows over a few days using my Mac laptop’s ordinary 8-core CPU, then trained a model to replace search during gameplay.

The model I used has 754,601 parameters and receives a 659-bit representation of the information available to the acting player, including their own hand. The model scores each of up to four cards in hand paired with eight possible coffin destinations, then selects the highest-scoring legal move. The training targets were not the actual moves selected by UCT, but UCT’s visit counts normalized into a probability distribution. This reflects the fact that in most game states there are multiple top contenders for the best move to play. Rather than training against a single selected move, the neural network’s loss function compares its predicted probabilities with the visit distribution, which reflects the UCT search’s relative preference for different available moves.

I improved on the first model trained on the belief-greedy dataset by using the resulting neural network as the continuation policy for a second round of UCT data mining. The outer search still ran 128 simulations and used exact completed-round scoring; the neural model replaced the belief-greedy continuation decisions. The second neural network was measurably stronger than its predecessors, and at this point I decided to stop training.

Hosting a Stateless Game

In order to make this project cheap to run online, I made the game a stateless and serverless web app. I built an API around the game engine and opponent model in which the browser sends the initial game seed and accepted command history with every gameplay request. The service keeps a cache of ongoing games, but it does not depend on that cache. If there is no cached state, it replays the game from the seed and history, validates the commands, and returns the updated public game state to the user.

This allowed me to serve the game through AWS Lambda without a game database. The container image includes FastAPI, the frontend, the engine, the trained model, and their dependencies. AWS starts copies of the application as needed and may reuse them for later requests. Each copy has its own memory, but successive requests can reach different copies, and AWS can shut them down, so replaying the history allows the game to continue regardless of which copy handles the next request.

Giving Dracula a Voice

Dracula game interface showing scoring and Count Dracula narration

The Dracula character narration is provided separately through AWS Bedrock, Amazon’s service for calling hosted language models. The application generates a short English summary containing the round result, changes in the lead, and the winning combination, and sends it to the LLM with instructions to respond in the character of a villainous Count Dracula.

The game is now live and free to play in your browser. Give Dracula a try and let me know how you do.