-- Leo's gemini proxy

-- Connecting to ainent.xyz:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini; charset=utf-8; lang=en

Spinoff Projects from my MUD


Intro


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?


MUD Systems


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:


User registration

Login

Player state persistence

Navigation

Vendors (though no currency yet)

Players and NPCs

Player and NPC inventories

Examining objects, players, and NPCs

Spoken languages (including language teachers with scaled learning difficulties, and spoken word garbling based on both the speaker's and listener's knowledge of that specific language -- the garbling was particular fun to code and test, I might add)

Containers (i.e., boxes, bags, backpacks)

Commands with various levels of access (unauthenticated, authenticating, authenticated player, authenticated imm (immortal / wizard / coder / etc.); though the latter doesn't actually have any commands per se, the infrastructure is in place

Communication: speaking (with all the language coolness mentioned above), private messages (tells, in MUD-lingo), chat channels with various levels of access

System-use notification mechanisms (implemented as 'chat channels' that are blocked from user usage, only the MUD itself can post messages)

Limb-based combat with severable limbs

Wieldable items (weapons: swords, etc.)

Wearable items (armor, though it currently doesn't actually defend you from anything in combat yet)

Death

Magic system

Classes

Races (in RPG / D&D terms, not social)

Skills

Abilities to train some skills (which will also let me make combat provide training eventually)

Leveling

Alignment

Grammar system so that everyone in a room sees the correct grammar for everything but user-entered emotes or chat commands (are you performing the action? its target? an observer? everyone will see grammatically correct verbiage, assuming the grammar functions are used correctly when developing them)


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.


MUD Content Creation, Current State


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.


MUD Content Creation, Future State


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:


The blue bar at the top is a rough list of keyboard actions you can take, the required letter being underlined

Each box represents a room in the MUD

Each line extending from a box is an exit to the north, northeast, east, southeast, south, southwest, west and / or northwest

The room with the 'e' is the one being 'Configured' or edited via the form in the middle


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.


Librem 5 Implications


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.

-- Response ended

-- Page fetched on Tue May 21 17:06:37 2024