Svarog Design Talk #1: Pillars

Svarog Design Talk #1: Pillars

Yet Another Roguelike Library (maybe I should have called it YARL...)

·

6 min read

Svarog is a library I'm working on. It's meant to be anyone's dream library for when engaging with roguelikes. Svarog, originally, is the deity in whose dreams the whole of reality dwells and resolves, so it was only fitting. It's lofty goals, ones that are basically the New Years Resolutions of any roguelike dev. I, however, have a secret: unlike other roguelike developers who become engine writers along the way, I am an engine writer by trade, who loves roguelikes as a passion! That surely changes everything.

So, what is Svarog? In short, it's a library of features bound together by a common purpose and a bevy skeleton. I want it to be good enough to support complete beginners better than the other language ecosystems. I want it to be versatile with regards to the presentation aspects. I want the next 7DRL to be full of Svarog entries. So -- what's the plan?

Traditional-friendly

There are many easy ways to make good roguelites, in my opinion. Running 2d and 3d physics simulations is the easy part nowadays. Having good turn-based queue logic, an easy way to add actions, reactions, mobs, items, to build, test, and feel safe about emergent complexity and procedural generation, on the other hand... not so many. This doesn't mean it's exclusive, rather that if you're building something non-traditional, you might either only need a few tools from this shed, or other libraries would suit you better. I want to make a toolbox that pushes the envelope on code clarity and maintainability when designing a traditional roguelike.

"Traditional" mostly refers to turns and grids, from a technical standpoint, and what I've seen so far is that engines and libraries remain at that level of abstraction throughout -- procedural generation works with layered grids, the turn economy is based on the same grid distances, and these are lifted directly into gameplay code: enemies search the grid and do pathfinding using that same grid. This brings us to...

Properly Abstracted

Gameplay entities can be driven by lower-level happenings, but shouldn't directly be contacting them. For example, a mob might understand that where the player is or what's in their line of sight, as well as what they want to do next, but not have access to the grid map, the player's FOV, etc. Svarog is heavily component-based, pushing both knowledge and intent through this funnel. This kind of abstraction leaves you with clean, short functions that do a single thing, fed with exactly the data it needs, and being called only if conditions are met. Easy to read smaller projects and maintaining the maintainability of larger projects is why this is one of the tallest pillars.

Non-Destructive

This one is hard to pinpoint so I'll give the most blatant example! The procedural generation toolkit in Svarog is made to be non-destructive, almost like how vector graphics work (or procgen in UE5). For example, building a very simple room with walls and a door looks something like this:

let insides = new_rect();
let bounds = expand(insides, 1);
let walls = cutout(bounds, insides);
let door = sample(walls, 1);

This whole process is parametrized by the rectangle made in new_rect: we haven't specified how large it is, nor where, which allows us to make non-destructive transformations - imagine if instead of new_rect, we had some outside value:

fn wall_out(insides: &Proc) -> (Proc, Proc) {
    let bounds = expand(insides, 1);
    let walls = cutout(bounds, insides);
    let door = sample(walls, 1);
    (walls, door)
}

The Proc type is basically a top-level type for procedural content. Whatever we pass to wall_out, no matter what shape it has, will now be walled out and a door put in place. To actually use Procs in your game then, Svarog needs us to stamp a Proc into one or more maps - these can be maps of different kinds, including EntityMap (one where we index a stack of Entity objects by their position), ValueMap (one where every position maps to a value of some type, usually integer or enum), DistanceMap (that builds up a SDF as you input values), and BinaryMap (where every position maps to a boolean).

In the example above, we could have something like this:

let insides = /* our cool room generator goes here */;
let (walls, door) = wall_out(&insides);

stamp(walkable_map, &insides, true);
stamp(walkable_map, &walls, false);
stamp(entity_map, &door, DoorEntity);

The benefit of stamping is that it can remember the Proc every point comes from, allowing for changes to the Proc to affect the map. This whole way of thinking comes from functional programming, where programming elements are by their nature composable and transformable, making complex transformative pipelines easy. This trace can be a huge help when debugging and maintaining your game. I'm imagining transformers that can influence the higher-up elements (like the whole room conceptually, and not just one point in the room, or even all the points that make the room), which then boils down to rewriting the floor itself.

Admittedly, there's a long way to go to get full non-destructiveness and usefulness, but it's a worthy goal, I think.

Data-oriented

By which I mean: I want as many code segments that should be data in data, as soon as possible! Do I want to code UI layouts? No, I most certainly don't. How about procgen? Well, I'd rather not if I can evade it: if the process is cut into a variety of tools that don't have to be compiled and recompiled, I'd rather have that. How about mob data, and character data, and interaction data? Those all have the word "data" in it so -- no!

There isn't yet a good basis for this but Bevy has paved the way with hot-reloadable resources and custom assets, it's just applying itself a bit more widely and generally that what we need. Other language toolkits know how to think about this, abstracting things to Lua or custom text/CSV formats, but a unified idea here could save days of serialization and deserialization, parsing, et al. A data protocol we've been building from the ground up, called RLyeah is used to unify all of these different things into one shape easily used with both frontends and databases. More on that a bit later!

Measurable

Except when it fully panics (and it never should) I want to make Svarog keep perfect track of what's happening in the game, filling multiple logs with data from all levels and layers. Easily build dungeon fuzzers by passing the same data that would move a player around into an automated tester, and get back traces that you can play back to learn things about some aspect of your game. We mostly with return old-fashioned-looking game interfaces, but our tools don't have to be old-fashioned. No reason we can't recover or measure any trend that comes to mind. Also choice and option tracking, to make statistics for developers easier to collect.