💾 Archived View for ainent.xyz › devlog › mud › 2022-04-23-mud-spinoff-projects.gmi captured on 2022-04-29 at 11:25:53. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-04-28)
-=-=-=-=-=-=-
This is definitely my longest post so far, though that was not my intention when starting to write. Does this count as longform technical content that a (now former) Geminaut would prefer to have available?
I find it interesting that sometimes an idea for a project can end up in a spinoff, that spinoff itself then having a spinoff, whose goal, if ever achieved, will benefit not only the original project, but future projects wholly unrelated. For instance, my idea to start coding a MUD from scratch a few years ago has ultimately morphed into an idea to make my usage of my Librem 5 faster and more nerdtastic. How?
Even though I haven't really worked on my MUD in any real capacity lately and while it does have a lot of work left to do per my satisfaction, there are a lot of built-to-scale systems in place:
This list is from memory and even though I expanded it after doing a brief scan of my code, I can guarantee you I am forgetting several things. I could go into each one of these in detail, but that would be outside the scope of this post. Suffice it to say, that the infrastructure for a mostly functioning MUD is implemented. Even so, there are currently only 5 rooms implemented, but in these five rooms you can test out the features mentioned above.
This many working systems gets me thinking about content, which leads me to think about the difficulty of building new rooms, areas, NPCs, items, weapons, etc. And the world building process now is a bit ... awkward:
let jeans = Armor( name: "battered denim jeans", description: "A blue demin pair of jeans, battered and torn to pieces.", weight: 1, aliases: [], limbs: [Limb.Armor.leftLeg, Limb.Armor.rightLeg, Limb.Armor.waist], defense: 1, health: .init(maximum: 5) )
Not so bad, right?
let sue = NPC( characterName: "Sue Susity", title: nil, aliases: [], race: human, gender: .female, class: Bard(), stats: startingStats, alignment: .init(name: .evil(.notVeryNice)), level: 1, currentRoom: Rooms.startingLocation, inventory: [], armor: [Items.hat, Items.shirt], weapons: [Items.wand, Items.staff] )
Still a bit wordy, but, I mean, the properties are needed for a believable world.
let sewer = Room( id: 2, title: "The Sewer", description: "A sewage-filled sewer.", sound: "Rats squeak incessantly.", smell: "The sewage overwhelms the senses.", items: [Items.trash, Items.trash, Items.trash], npcs: [] )
Ok, now we're starting to get a bit awkward because providing an NPC to the npcs parameter doesn't work. I forget why or what happens, but I think it's just an ugly implementation detail that needs cleaned up.
// `World` is declared elsewhere and is unimportant here // basically other code reads this property and defines the world thusly extension World { internal static var example: World = { let startingLocation = Rooms.startingLocation startingLocation.npcs = [NPCs.harry] startingLocation.exits = [ .init(direction: .north, destination: Rooms.park), .init(direction: .south, destination: Rooms.cityGate), .init(direction: .west, destination: Rooms.grotto), .init(direction: .down, destination: Rooms.sewer) ] Rooms.grotto.npcs = [NPCs.tsiugnil] Rooms.grotto.exits = [ .init(direction: .east, destination: startingLocation) ] Rooms.sewer.npcs = [NPCs.mal] Rooms.sewer.exits = [ .init(direction: .up, destination: Rooms.startingLocation) ] Rooms.park.npcs = [NPCs.bob, NPCs.sue] Rooms.park.exits = [ .init(direction: .south, destination: Rooms.startingLocation) ] Rooms.cityGate.npcs = [NPCs.frank] Rooms.cityGate.exits = [ .init(direction: .north, destination: startingLocation) ] let rooms = [ Rooms.startingLocation, Rooms.sewer, Rooms.park, Rooms.cityGate, Rooms.grotto ] let startingDomain = World.Continent.Domain(name: "Example Town", startingRoom: Rooms.startingLocation, rooms: rooms) let startingContinent = World.Continent(name: "Example Continent", startingDomain: startingDomain, domains: [startingDomain]) let world = World(name: "Example Starter World", startingContinent: startingContinent, continents: [startingContinent]) return world }() }
This is where I really don't like it. You have to write everything imperatively, and you kind of have to keep the whole game world in your head while doing so. Works for now, but it's not exactly scalable to a decently-sized playable world.
What I would rather do is either code something declaratively, or better yet, not have to code anything at all for basic content, and instead just provide data. Still better yet, create a visual map that then spits out data files that the MUD reads at runtime. Solving this problem leads to the first spinoff. I have this little program, currently dubbed 'WorldBuilder'. The technical details of it would justify a separate post, but it is a custom text user interface (TUI) application that looks like this:
Screenshot of WorldBuilder TUI
A bit of an explanation:
It's a bit buggy, and far from done, and this 'form' you see doesn't even accept input yet, but, it is a good start. However, the code is also imperative. What about something declarative, like SwiftUI, only for TUI applications? To the uninitiated, SwiftUI is Apple's newish system for coding the UI portion for macOS, iPadOS and iOS apps. How about a SwiftTUI instead? I've tried this in the past, and gave up, but I've now figured out a solution using a Swift language feature called resultBuilders. As this post is not a Swift tutorial, I won't delve into the how of that, but rather, the kind of code that it enables:
Window { VStack { HStack { Text("+") HorizontalLine(length: 5) Text("+") } Text("| |") Text("| |") HStack { Text("+") HorizontalLine(length: 5) Text("+") } } }
VStack represents a vertically stacked group of UI elements; HStack is for horizontally stacking them.
To save some bandwidth, I won't post another screenshot, but this (along with some other code irrelevant to my point) renders the following box (which will represent a 'room') into a TUI window:
+-----+ | | | | +-----+
This implementation is also buggy (nesting HStacks or VStacks causes unexpected layouts), but it's declarative and easier on my eyes. I originally planned on migrating the code to this kind of system at some undetermined point in the future, with gradual refactors along the way to make the migration easier, but I was having fun and decided just to rewrite the layout code. The hardest part is now out of the way, so whenever I get back into it I'll need to fix the aforementioned defects and any others that crop up, and can then start refactoring the old code into this system.
Technically, this idea should work just fine on any system that can run Swift (except iOS or iPadOS, as those don't allow you terminal access), but I'd like to use this to build quick (or even advanced, as time allows) TUI applications that I can use on my computer-that-doubles-as-a-phone. I'm becoming fonder of the terminal all the time, and if I can add useful computing time in it instead of in a graphical user interface (GUI), that's a win for me.
WorldBuilder, in its current visual design, wouldn't really scale well to a phone, but that's beside the point.
That is the story of how 'I should build a MUD!' morphed into making my as-of-then-unheard-of (by me, at least) Librem 5 more fun to use.