In the first part of this series I described how I didn't want to get into fundamental changes to the stat system while in the middle of development, leading me to completely avoid modifying the underlying architecture despite greatly expanding the number of stats recorded. Among other limitations, this is also why we've gone for years now without a way to examine stats in the middle of a run. But that ends now!
Clearly "mid-run stat dumps" (AKA character dumps) are a really useful feature in a roguelike of decent length, and even more so in a roguelike with an active community that enjoys discussing strategy. Some reasons to support dumps:
- Share with others the full details of runs still in progress
- Check the current scoring breakdown and other stats
- Help track progress towards in-run achievements or other goals (this would be nice to have in game, but Cogmind has hundreds of achievements and it would be a ton of work to implement a way to review this progress in game, so I'm not sure if that will exist)
- Make the system easier for me to debug, and work with in general (I have been loving the ability to output stats at will since this feature was completed! Previously having to self-destruct and reload all the time was a pretty annoying and repetitive sequence compared to just a single command...)
I'm sure there are more, or more variations on what I have above, but the point is: they're useful!
Form and Architecture
Years ago when thinking of this kind of feature, I envisioned a mid-run stat dump as an alternative short summary of the run's progress and (most importantly) current situation, different from scoresheets. But other than the advantage of keeping it concise, that doesn't really make sense when we already have a great source of comprehensive info to draw on--the scoresheet itself. Stats for the scoresheet are maintained throughout the run and (mostly) ready to be output at any time, so why not just create an "in-progress scoresheet" on request?
So that's the form we're aiming for here, basically just a scoresheet-on-demand :)
This did, however, require a fair bit of work.
The original code for stats in Cogmind was quite simple: Have a class storing all the data in a few simple arrays, and when the run ends mix it in with whatever other player/run data is necessary, rework it all into a printable format, and write it to a file.
The problem is all of this was done in a single function--there was essentially no division of responsibilities, although clearly we can't use an identical process for mid-run stat dumps because there can no longer be an assumption that the run is ending! For example the original code would even change game data as part of its work without worrying about negative repercussions, since the run was ending anyway :P
And as you've seen in the samples before, Cogmind's scoresheets contain quite a lot of data, so there was a lot to go through...
The scoresheet architecture diagram that's been sitting on my desk for months now,
from when I first sketched out the difference between what we had and what we needed.
In terms of architecture, the main goal would be to migrate everything from a linear system to a properly compartmentalized one which could handle three separate possible outputs independently of each other: scoresheets, mid-run stat dumps, and uploading of online stat/leaderboard data.
The most important part here was separating out all the necessary data into a new intermediate format, basically a new data structure outside the game data that contained everything necessary for any of these purposes without relying on the game. Technically as described before I already had such a separate structure outside the main internal game data, but it still relied on a lot of support from the game data itself because that was what was easiest at the time :P. Now the code has been restructured around a whole new intermediate data structure that purely contains all the raw data necessary for any desired final form.
Although I haven't done it yet (though it will come soonish), this is also a stop on the road towards having an online stat database, so the dump feature's development comes at just the right time.
What's inside a mid-run stat dump? Well, it's almost identical to a scorehseet, simply stopping at the current point, except for one main difference: there's no Result to show in the header. I mean... of course there isn't :P
That said, it'd still be nice to use the expected "Result" line in the header to indicate that the run is still in progress, and show something else interesting instead (rather than just "in-progress," "stat dump" or something else mundane), so I came up with some new content for it.
I thought it would be neat to provide a short one-line summary of the run as it stands, which in cogmind basically boils down to two things: your build, and your status. Pretty simple, although each half of this would require a new system and take some time to realize.
I started out thinking the first element would use terms along the lines of what we've come to be using in the community, e.g. "flight hacker," "infowar combat," and so on, sketching out some related notes on how to make that determination.
At first it looked like it might be in the form of [prefix][main][subtype] (e.g. infowar combat hacker), with the possibility of other hybrids mixed in there and whatnot. It's a practical approach, but also given the kind of options I saw it was kinda boring and probably overcomplicated. (During this process I drew on my character archetypes post about Cogmind from a while back on r/RoguelikeDev, which you might find interesting if you haven't seen it before.)
So I thought what if we had it instead detect builds as something more akin to true classes? The focus would still be on clear, functional names rather than creating whole new ideas unique to Cogmind or anything crazy, but it would be more fun than the dry (and even more limited) terms we normally use.
Unlike many roguelikes/games where you select a class to play, as you know Cogmind is a dynamic, flowing experience and it's interesting to explore classification of these builds as they happen.
To this end I came up with a chart of characteristics and 13 names to go along with them. In the code, a long series of nested conditional statements determines which "class" most closely applies to the build.
A sample from the beginning of the base class determination--it continues for a while after this, filtering further and further down through possible builds based on state summary variables. Lots of if-elsing :)
Then, since a single name would have been too limiting and many more creative combinations can be captured via a two-name system (kind of like multiclassing!), I came up with an additional 15 special designations that can more specifically describe some aspect of the current class.
Unlike the main designation, this one is determined via a point system. Your build earns points in each category based on its parts (and sometimes other factors), and whichever category accrues the most points is applied as a modifier to the class. If nothing applies very well, then only a single base class is used.
Cogmind is not constantly recalculating your class. That would waste a lot of time, and also be pretty inaccurate when you're doing things like transitioning between builds, or lose a part or two for just a little while or are in the middle of swapping things in and out for whatever reason. What I did there is have it not only calculate only every so often, but also wait until there has been a decent stretch of turns without any actions that affect your loadout, like attaching, dropping, swapping, etc. Only when your build is considered "stable" will it take another sample for analysis.
In the end this automatically determined "build class" appears in the format [special modifier]-[base class], where the modifier is empty if nothing really applies. Everyone starts as a Mutant, the default base class, since that's more or less what Cogmind is at heart, a non-specific machine attaching a random variety of parts while not really capable enough in any one area to be noteworthy.
There are currently 195 multiclass combinations (plus the 13 pure base classes), some more specific and less likely to be discovered than others. Below are a few screenshots of my first test run in which I traveled and battled through five maps as I changed my loadout around and saw how the game classified it. For this purpose I put a nifty "class readout" right at the top of the parts list. (I wasn't aiming for any particular builds, this is just what I ended up with, among others I didn't screenshot.)
Build: HAULER-TANK. Armor and firepower!
Build: HAULER-GUNNER. A more offense-oriented combat build.
Build: SKIRMISHER. Combat bot supported by infowar utilities.
Although the class indicator seen on the HUD there was originally for testing purposes, I later decided to include that as an advanced player option as well. It's kinda non-immersive so I don't want it appearing there by default, but it's a fun thing for veteran players to turn on.
In designing the above system, I realized it could be expanded into an interesting permanent part of the full scoresheet, as described in the first part under the Build section. Thus we get two new scoresheet sections:
Sample scoresheet 2.0 excerpt: Class Distribution.
Sample scoresheet 2.0 excerpt: Dominant Class.
I'm happy to see that builds aren't whipping all over the place, which would otherwise be likely if the classification system wasn't stable enough. Having relatively broad definitions helps in that regard.
Builds also have a place in the History Log, to be described in Part 4 of the series.
But getting back to the idea of mid-run stat dumps and that Result line, this is where it all started, just to drop the class name in there like so:
Sample mid-run stat dump header, with build classification and situation analysis.
There you can see we have the current build type, followed by a second piece of information we'll get into in the next section.
Since we can't describe the final result of the run, I thought it would be interesting to try to summarize the current situation in one word. This is determined on a scale from dire to wonderful.
The full situation analysis scale for a run's status.
The default is "fine," and a number of factors are checked to simply raise and lower the scale to arrive at a final value. So having some negative factors mixed with some equally positive factors will still average out to a "fine" situation, but having extra factors in a given direction will start pushing the rating towards an extreme.
The calculations are all quite simple, just checking factors that we has Cogmind players tend to rely on when considering what's truly important in terms of how we're doing. Some of these are pretty common to roguelikes in general, like having low HP is not good, being surrounded by enemies is bad, and you don't want to be completely defenseless.
Here's a general list of factors used to analyze the situation:
- Core integrity
- Speed (can't be ridiculously slow)
- Essential parts (missing power or propulsion is dangerous)
- Spare parts (this is never included as a negative factor, but it's always a good thing to have spares of everything)
- Part condition (empty slots count against this)
- Matter levels
- Armed enemies and allies nearby
- Known exits
The entire thing is only 77 lines of code, but seems to work pretty well. Like build classifications I added a current situation analysis indicator to the HUD for testing purposes so I could see how it was changing throughout a run, but I don't plan to make this even an optional feature for players.
Current situation analysis debug mode view
(you can tell it's not for release because clearly it shouldn't be lowercase).
If you want to see that in action, I was using it in this Beta 9 preview stream where I ran the debug version of Cogmind. Actually for a simple example, here's a debug shot in -9/Materials:
Scoresheets are normally produced at the end of a run, so how do players get a mid-run stat dump?
There is a new command that can be used on the main UI, Shift-Alt-s (for "stats"), which creates the file and puts it in a new separate directory in the Cogmind file structure, /dumps/.
Of course this also needs to be mouse accessible, so for that we conveniently have a logical place to put it: the Records menu.
Even just in prerelease so far players have already been using the stat dumps, not to mention this feature has proven quite useful for my own needs whenever it comes to testing or debugging anything stat related.
This is the third article in a four-part Building the Ultimate Roguelike Morgue File series.