Categories
Gaming MUD Programming

Programing a MUD: Part 2—CQRS and Event-Sourcing

Introduction

I decided to work towards making the MUD work on an event-sourcing model, or CQRS. CQRS stands for something like Command Query Responsibility Separation. It just a fancy way to say that the manner in which we inject data into the system (in this case, as events) can be different from the way in which one reads the state (such as a Redis query).

This change in technical direction requires that I create an event for every kind of state mutation I plan to allow. I’ve never implemented CQRS, so I expect to learn a few things.

Currently, to seed the data, I run a Yaml-to-Redis converted I wrote. This is a generic facility that will basically take any YAML and mirror it in Redis. Whereas Redis doesn’t support hierarchical values, I simulate this by extending the key names as a kind of path. At the top level we simply have values, lists, and maps. Map entries are scalar.

90) "monsters:monster:blob"
91) "monsters:monster:golem"
92) "items:armor:medium armor"
93) "places:population-center:Karpus"
94) "places:population-centers"

But this won’t do in a system based on the event-source model. I can’t just mutate the state all at once like that. It will invalidate principles of event-sourcing like being able to replay, and, during replay, have the system process an event with the correct starting state.

For instance, if a character starts out at level 1 with 10hp, and we reply a set of events created during an adventure, the character might be level 2 at the end of those events. If we choose to replay and ignore the state changes of the character (such as them starting at level 1), then the events will be handled and the system will think the character started at level 2. So, spawn character is an important event as it is an event which establishes a new character and interactions with that character should start from that point.

It’s possible I could create an “event” such as “load-yaml-file” and the event has the yaml file contents, but I think the lack of granularity might prove unworkable.

Instead of injecting a map for a monster:

Monster::Gargoyle
    Hit-dice: 4d6

I’ll say something like “add-update-monster”, and this event should have all relevant data related to adding or updating an existing monster state.

This way, as the event makes its way across the system, components have a signal to do something or ignore the message. This is the replay ability to a CQRS system.

Redis Streams

Redis has a stream feature which is ideal for modeling these events. For now, I’m going to use the simple XREAD function, which allows any client to digest every message in the stream.

I did some benchmarking

Hardware:

  • Redis runs on a dedicated linux box (Intel Core i5-3570K CPU @ 3.40GHz)
  • stream writer/reader programs to send/receive data from Redis (AMD Ryzen 9 3900X 12-Core Processor)

When source/sink are running on my Windows desktop, running in a WSL2 Debian container, we get:

~ 770.1650232441272mps

But the CPU was more or less idle on both Windows and Redis. So, my network must be slow. I repeated the test by running the writer/reader apps on the Redis Linux box and results were 10x faster:

When I the run source/sink programs on the redis hardware, eliminating the network, the results are much better:

~ 7011.405656912864mps~

I could not get single-threaded writes to be much faster than that, but I did find a big in the reader and when I fixed it, I was able to received 100-200k messages/sev, which is great.

Now that I have a technical foundation well-understood, it’s time to start defining some mutation messages and modeling how they’ll appear on the stream.

Based on work so far, I have these events to define first:

  • UserInputReceived
  • UserOutputSent
  • MonsterAddUpdate
  • ItemAddUpdate
  • PlaceAddUpdate

This is probably suboptimal naming, but I’m still new to this, so I expect to refine the event taxonomy over time as I see more events.

More on that later.

Categories
Programming

Building a MUD – Part 1

This is part 1 of N parts where I'll discuss my adventures in building a MUD (Multi-User Dungeon) in C++. I've always wanted to write games but never got around to it for a number of reasons I'll discuss over time. So when I decided to finally try my hand at creating one, I chose the language and tools I'm most familiar with, and which I use in my day-job.

You might be wondering why I chose a MUD over a platformer, or a shooter, or a mobile game of some kind. The reason is because I'm primarily a backend developer and a MUD seems more or less like a backend project to me. I have a buddy who's a JavaScript/3D expert and I figure possibly one day I can enhance the protocol to drive the MUD with a bit more flair down the road. It's a bit early for that.

I'm also choosing to make the repo public as I do this for two main reasons:

  • I'm chatting about my progress and I want to send code links to my friends and don't want to bother with adding them as special collaborators
  • I invite comments and criticism on the way I do things because this is a learning project above all else

The Stack

First, the repo is here: https://github.com/NickCody/cpp-game. Be warned, the repo is not clean, as it contains numerous little code projects tucked away in various directories as I played around with ncurses, graph algorithms, the game of life, etc. If you poke around, you may be surprised at how many little things are in there.

Visual Studio Code is the primary tool, and more specifically I started with one of their stock C++ dev containers (described here). I've heavily modified the container's Dockerfile to include the latest tools I could, among these are:

  • Debian Bullseye
  • gcc 10.2 (for C++ 20 compatibility)
  • vim, graphviz, ninja, clang-format, and a few other goodies
  • The CAF Actor Framework (more on this below)
  • Bazel for builds
  • Yaml for configuration
  • Redis as the primary store for user/game data
  • Google Cloud SDK for "production" deployments

Of these, I feel most people might shake their head at why I'm using an actor library for my C++ code. I'm a huge fan of the actor model having worked with Akka on Scala for years. The model makes sense to me and I wanted to learn more about this library for C++. I'll be speaking about it in detail in coming posts.

Home Setup

At home, I have my Windows desktop on which I have Docker Desktop and WSL2 installed. I don't find myself in WSL2 much, not directly, but the repo lives there and I spawn vscode devcontainer from WSL2.

For edit/compile/run cycles, I run the MUD on my desktop. Redis is running on a home Linux server I have tucked under my desk. It's only a Intel Core i5-3570K CPU @ 3.40GHz with 32GB RAM. Its plenty of power for what I need. This machine is my "QA" environment, where I can test my deploy scripts. I have a version running in the cloud, too, using Google's Cloud SDK.

Why Redis?

There may be better choices for my persistent store, since Redis is primarily a fast in-memory data structure storage, with persistence capabilities. I was curious to learn it so I figure if it turns out to not be the right choice, I'll eventually figure out why and then I'll have learned something.

For now, it's pretty amazing. Fast, lean, and no bullshit. I'm using hiredis, which is a bare-bones simple thin wrapper based on C. I considered one of the many "modern" C++ wrappers, but I felt like I could write my own wrapper and use my own wrapper as an abstraction around the storage engine that might help me move off Redis down the road if I eventually decide it's not the right storage engine for this project.

Conclusion

That's all for now. I'm not sure what the topic of the next post will be, as there are dozens of topics to discuss, but thanks for reading.