Merchant

2025 July 25

I built an homage to Drug Wars, the classic terminal game, in Rust, my favorite programming language these days.

TL;DR: follow instructions in this README to play a game in your terminal that looks like this:

Gathering Provisions

In the spring of 2021 I was focused on expanding my understanding of programming languages. The previous few years saw me ditch Javascript for fully embracing TypeScript as well as dabble with more functional languages like Elm, and a growing language called Unison. I was working my way through Haskell Programming from First Principles when I first heard about Rust.

A smart, trusted coworker had fallen in love with it and strongly recommended I check it out. I started messing around with tutorials and found that the design decisions (especially the type system) really resonated with me.

After about a year and a half I found a legitimate use case for it in some work I was doing for a client. We wanted to build a custom Redis module, which was usually done in C / C++ but also had official Rust bindings. We didn’t have particular experience in either, so we took a bet on Rust.

Since that fall of 2022, our trust in Rust and its presence in that codebase steadily grew from that Redis module being called from our Python HTTP server application, to wrapping core Rust logic in PyO3 and calling it directly from the Python, to finally deciding to rewrite all the existing Python in Rust. We now have 80k lines of application and test Rust, and 0 lines of Python, and it feels great.

At the same time in early 2023 ChatGPT exploded in popularity, and with it the general notoriety of LLMs. After my first few conversations with it of course I started thinking about how it could be incorporated in a game experience.

Setting Sail

With the desire to gain experience with Rust and ChatGPT serving as my personal backdrop, I made the first commit on this new project June 18, 2023. My goal was to sprint to a complete, playable game first, and then use that as a foundation to experiment with LLM integrations. I decided to basically make a clone of Drug Wars because:

  1. I loved playing it on my TI-84 calculator in high school.
  2. It has a simple but entertaining set of mechanics which I knew well.
  3. It’s a terminal UI game, meaning I could preserve some focus in my personal education by avoiding more complex graphics stuff.
  4. I could already imagine interesting LLM integrations for it, like being able to chat with NPCs to get hints as to where good deals would be.

On the technology side, I adopted the Rust terminal UI library crossterm as the primary framework, which I found to have a decently easy API and gave me a product that works cross-platform on Mac, Linux, and Windows pretty much for free.

On top of crossterm I built a small rendering and state engine that is based around a main loop that pretty much works like this:

  1. Wipe the entire screen.
  2. Feed the current GameState, a big struct that contains every bit of data the game tracks, into a render function that prints out the entire frame.
  3. Await user keyboard input, at which point update the state accordingly
  4. Repeat

I’m a big believer in automated testing, especially for these little side projects that you might come back to once every three months and inevitably forget all the invariants you need to maintain, so I also wrote a little integration testing framework for the game. It simulates the main loop and captures the rendered output, allowing assertions on the entire frame. Here’s an example that just tests that you can press some key on the splash screen to progress to the main game scene.

The trickiest part about the testing framework came from the fact that crossterm uses ANSI escape sequences to position and format text in the rendered terminal output (which is great, that’s how you do it). But I didn’t want to test that some rendered output was something like \x1b[1mH\033[5CW\033[22m\033[6Dello\033[2Corld, which would look to the user like “Hello World” in the terminal, since that would be way too brittle and almost certainly cross some testing cost-benefit threshold where I would give up on testing altogether. I figured it was more important to test the characters (including their ordering) and less important to test styling, so here I would want to assert that this rendered output matches the regular UTF-8 string Hello World. So I built a small separate crate that does just that - it takes a string with ANSI escape sequences, ignores the text styling codes, and interprets the cursor positioning codes to produce that UTF-8 output.

Port of arrival

It turns out, as always, building something to completion always takes much longer than I think. I chose a simple and straightforward project, so, in and out in 15 minutes? How about almost 2 years (of very intermittent development). It’s taken about 100 commits to get to this place where I feel like it’s ready to show people. Squinting at the commit history and remembering the past couple years, I’m guessing I could have knocked this out in maybe a couple full work weeks. But finding that time for a side project I guess is the real challenge.

In any case, what we ended up with 2 years later is what I’ve called Merchant. It’s basically a reskin of Drug Wars. The mechanics are almost entirely the same but you deal in goods like tea instead of drugs, you store those goods in a ship’s hold instead of a trench coat, and you’re navigating 18th century trading ports instead of modern day New York. Overall, it’s just a little more family friendly.

I took a few liberties with the mechanics beyond strictly adhering to those of Drug Wars. For example, I introduced this idea of “personalities” for each location, randomly generated and assigned for each playthrough. The personality affects the variance in price distributions as well as random event probabilities, so there’s a reason not to just go back and forth between 2 different ports for the whole game. Also, since your home port has additional features like access to the bank and stash, its personality is set to be a little more “boring”. I’m not sure if the original game did this or not, but I also did some balancing things like making sure that the random event where you find free goods is at least somewhat based on current game state so you don’t suddenly 50000x your net worth.

Exporting the goods

It was pretty fun figuring out how to distribute the game. Obviously people can go pull down the Github repo and launch it with cargo run. But I wanted it to be as easy as possible for people to check the game out.

Luckily, I found cargo-dist, or I guess now just dist. It’s fantastic. What I had to do was provide this small configuration file, set up a homebrew tap repository for Homebrew distribution, set up an NPM account and package, and define auth tokens for each as repository secrets. dist generated and continuously manages this complex github actions file, and now whenever I tag a commit with a version string my Homebrew and NPM projects are updated, and a release like this is all automatically generated. And as far as I can tell, it just works. Not bad, dist.

So go ahead and run npx merchant-game in your terminal, or brew install samgqroberts/tap/merchant then merchant, or use the Microsoft Installer, or download the binaries directly!

Future horizons

or, Wait what about ChatGPT?

Right, so, I never ended up doing any experiments with integrating an LLM. I’d like to release as-is for now, as it represents a product that can be considered finished, and it took enough time just to get to this point.

Now having achieved that first goal of a fully fleshed game, I still think it’s a fertile foundation for doing these LLM-based game design experiments. A couple ideas have been floating around. One builds on top of the location personality system, where interacting with an LLM might help give you clues as to where a good place to go might be, or a place to avoid. Like you might be able to visit something like a tavern and chat with people there, who based on your conversation with them might provide useful information.

Another bigger idea is kind of like a sequel to the game that I’d call Admiral, where instead of interacting with game state at the level you do in Merchant, you are instead at a higher organizational level, and you interact with a team of LLMs that are each captains that effectively play the Merchant game for you. You would receive reports and send out orders, perhaps always simply sitting at your admiral’s desk. Would that be fun? Would it work? Really unsure, but sounds interesting to explore.

Thank you for reading!