Random Thursday Update

Redesigned the front as a software gallery, got Portal Worlds working with my common input system, adapted Amazing (the dumb maze game).

I might get Heist adapted this weekend. Cityscape needs either a custom character set, or I add sprite graphics to the retro screen, which is a better plan. I have a bunch more JS games and demos, most can be adapted pretty quick. Porting the Mystic Dungeon RPG from Python is harder, but on the list.

Still thinking about the forum idea, I haven't seen a lot of interest yet, but a place for smack-talking would be nice.

It's really quite nice just having an easy way to focus on the game design or UI mechanics, and not have to make infrastructure from scratch every time.

Everything should be usable without an account, but you can't post scores unless you do!

I've finally got my web games/tech demo site MysticDungeon fully running SSL, a proper Node & database server, and all the existing games ported to my common "Learn2JS" framework. High scores and hit counters work for all of them; I haven't set up a really stable migration tool yet but that's on the TODO list before anything more serious gets stored there.

If you run into any bugs, let me know here or on fediverse.

Upcoming will be getting a couple features in PortalWorlds finished, then the rest of the Umbral Adventure world, and some more tools in Grimoire, which will be a tabletop RPG journal/toolkit, more for Referees to use as a virtual screen/notebook than as a coop gaming tool, but you could screen-share it if you needed to. Proper user accounts instead of an unverified screen name will be part of that.

I'm still thinking about if I should replace the old BBS with a forum, or what. Rebuilding the Mystic Dungeon game is on the list, that's part of what the Umbral Atari-like screen is for; nice ATASCII line-drawing characters instead of the few ANSI chars it supported.

Project Status

Made a lot of progress on Perilar Dark Weaver map generation. Hopefully this week I'll get ruins finished.

Spent a little too much time on the new Mystic Dungeon, the TTMS-76 virtual retro-console started on but is now replacing the BBS. It now has scoreboards for the games! I still need to finish Heist, which turned out to be bigger than I first thought. In a bit I'll get yet another damned login system done, and from there a forum. I wonder if anyone else would be interested in making games for it? I've got a trivially easy framework, so if you know any Javascript at all it's fun to work with. All BSD licensed.

Updated StupidComments.css to block some more inline "affiliate" blocks and Youtube spam segments.

I started making a console Pomodoro timer, and it works, but needs persistence and a teeny bit of task management before I can release it. Very soon.

RPG-wise, I wrote a bit more of my "survival D&D" game Delvers in Darkness (aka Dungeon Hell), which is looking to come in well under 16 pages for a full Holmes-type dungeon game; maybe 32 if I write more on the setting, which since I complain about that in everyone else's games, I should. Haven't looked at my light game in a bit, and don't know when I'll get back to that.

Mystic Dungeon BBS Update

I just added a doorgame conversion of Lost Treasure to Mystic Dungeon BBS; just go into the Door Games menu and hit L. Just a little toy to compete on the high score list.

Mystic Dungeon doorgame got a new patch, mostly just fixing display bugs, but I've nerfed poisons & paralysis to scale by damage inflicted; I'll see how that works in higher-level play, it may need another patch, so carry an antidote if you're cautious (you can get them from the alchemist in the NE of town). It's a fun, hard little RPG, I can get down to level 2 and 3 now, though that's very very dangerous; I'm nearly Level 3. Once anyone nears Level 4 legit, I'll add some more magic spells.

I'd like to see more competition; log in every day and do your 96 turns, and you can progress pretty fast. I like the turn limit, it prevents anyone (self included) from grinding for hours and jumping up the board unfairly.

I'm in progress at getting my Scheme text adventure completed, and then I'll doorgame convert it, and it'll go in the doors, too.

I need to look again at the file upload system, or maybe replace that entire feature with a script so you can browse files sanely.

Been pretty quiet, got a few users from Fediverse, but after doing some stability testing and system updates, I think it's in good shape now to advertise on /r/bbs, probably tomorrow morning.

Lost Treasure

In 1979, I learned to program in BASIC on a TRS-80 Model I. Sometime in the next year, I read one of my first programming books:

I played Monster Chase and Lost Treasure, modified them extensively, and combined them, so the cave on the island had a monster chase to reach the exit. I recall having problems getting Starship Alpha and Devil's Dungeon to work, but they joined my software library eventually.

One of my earliest and happiest programming memories was sitting at the dining room table, reading Monster Chase, and writing out a smarter movement system and obstacles in a notebook; at the time the only computers were at school, so I wrote code on paper and typed them in later.

So when I found the book again on last night, I was very excited, and had to reimplement it. I actually typed this into Pythonista on my phone with the PDF open on an iPad, only moved it to the computer to do some final cleanup and upload it.

The book suggests some modifications, and I did some minor ones: Lowered the movement error to 10%, and risk of shark attack to 10%, rising by 1.5x rather than a flat +50% each time; being anywhere near the island edge killed you too often in the original. I also don't move you out of the water automatically, that should cost a turn.

I realized in converting it that I hate, hate, hate Row,Column coordinates instead of Cartesian X,Y; tons of mainframe-era computing resources used Row,Column, and you can still see it in some APIs like Curses. Note that the original program is 74 lines, mine's 214; BASIC is a terrible language, but it's terse.

I could adapt this into another doorgame for my Mystic Dungeon BBS, but I'm not sure what the multiplayer aspect would be, and it has limited replayability without doing some randomization.

Writing a Doorgame

So, how did I write this? Mystic BBS has a Pascal-based scripting, but I didn't really like using that; it's very powerful, but a pain in the ass to code/compile/test on, and it's just different enough from normal FreePascal to annoy me. And then tried to use Mystic's "integrated Python", which just doesn't work; I tried everything to make it find Python. And it's 2.7, so bleh. BUT: Adding a doorgame with the menu system lets you specify any executable, including a Python 3.7 script, and pass in a few command-line arguments about user and directories, so that's what I did (though the server I'm using only has Python 3.6, which hasn't been a problem yet, but be aware)…

The BBS handles all the redirection from network to stdin/stdout, otherwise I could use inetd for the same thing, but then I'd have to deal with all the login and networking state. Doing this as a door simplifies programming back to 1970s level!

I'll eventually open source it, but while I'm iterating and early on, I want to keep some of my secrets.

ANSI Color

There's ANSI color everywhere, of course. curses is fucking awful and archaic, and built to handle many incompatible terminals, which isn't a problem anymore. Mystic uses the same trick I'm using, and many others back in the day: Strings have escape codes |XX in them. Mystic uses numbers for colors, which is awful, and has hundreds of BBS-specific strings, which I have no need of. So all I did is this:

# set to 1200, 9600, etc. to simulate modem transmission rate
kModemSpeed = 0
kLinesPerScreen = 24

kAnsiEsc = "\x1b["

#      0   1   2   3   4   5   6   7   8   9   10
kAnsiBoxLine =  ["─",   "│",    "┌",    "┬",    "┐",    "├",    "┼",    "┤",    "└",    "┴",    "┘",]
kAnsiBoxDouble =["═",   "║",    "╔",    "╦",    "╗",    "╠",    "╬",    "╣",    "╚",    "╩",    "╝",]
kAnsiBoxHeavy = ["━",   "┃",    "┏",    "┳",    "┓",    "┣",    "╋",    "┫",    "┗",    "┻",    "┛",]

# ansiFilter, called by write/writeln, replaces |XX with escape codes:
kAnsiEscapeCode = {
    "CD": kAnsiEsc+"J",         # Clear Down
    "CL": kAnsiEsc+"2K",            # Clear Line
    "CS": kAnsiEsc+"2J"+kAnsiEsc+"H",   # Clear Screen
    "DI": kAnsiEsc+"2m",            # Dim
    "ES": "\x1b",               # Escape ASCII 27
    "GG": "\x07",               # Bell ASCII 7
    "HR": kAnsiBoxDouble[0] * 79,       # Horizontal Ruler
    "LI": kAnsiEsc+"1m",            # Light/Bright
    "PI": "|",              # Pipe char |
    "RE": kAnsiEsc+"0m",            # Reset Attributes
    "UN": kAnsiEsc+"4m",            # Underscore

    "FK": kAnsiEsc+"30m",           # Foreground Black
    "FR": kAnsiEsc+"31m",           # Foreground Red
    "FG": kAnsiEsc+"32m",           # Foreground Green
    "FY": kAnsiEsc+"33m",           # Foreground Yellow
    "FB": kAnsiEsc+"34m",           # Foreground Blue
    "FM": kAnsiEsc+"35m",           # Foreground Magenta
    "FC": kAnsiEsc+"36m",           # Foreground Cyan
    "FW": kAnsiEsc+"37m",           # Foreground White

    "LK": kAnsiEsc+"1;30m",         # Foreground Light Black
    "LR": kAnsiEsc+"1;31m",         # Foreground Light Red
    "LG": kAnsiEsc+"1;32m",         # Foreground Light Green
    "LY": kAnsiEsc+"1;33m",         # Foreground Light Yellow
    "LB": kAnsiEsc+"1;34m",         # Foreground Light Blue
    "LM": kAnsiEsc+"1;35m",         # Foreground Light Magenta
    "LC": kAnsiEsc+"1;36m",         # Foreground Light Cyan
    "LW": kAnsiEsc+"1;37m",         # Foreground Light White

    "BK": kAnsiEsc+"40m",           # Background Black
    "BR": kAnsiEsc+"41m",           # Background Red
    "BG": kAnsiEsc+"42m",           # Background Green
    "BY": kAnsiEsc+"43m",           # Background Yellow
    "BB": kAnsiEsc+"44m",           # Background Blue
    "BM": kAnsiEsc+"45m",           # Background Magenta
    "BC": kAnsiEsc+"46m",           # Background Cyan
    "BW": kAnsiEsc+"47m",           # Background White

def ansiFilter(s):
    """Replaces |XX codes with values defined in `kAnsiEscapeCode`."""
    out = ""
    slen = len(s)
    i = 0
    while i < slen:
        c = s[i]
        i += 1
        if c == "|":
            esc = s[i:i+2]
            i += 2
            out += kAnsiEscapeCode.get(esc, esc)
            out += c
    return out

def ansiGoto(pt):
    return "%s%s;%sH" % (kAnsiEsc, pt[1], pt[0])

def readln(prompt):
    """Calls input after filtering prompt."""
    return input(ansiFilter(prompt))

def write(s, end=None):
    """Filters `s` and writes to stdout, writes `end` if needed, flushes stdout."""
    if type(s) != str:
        s = str(s)
    lines = 0
    out = ansiFilter(s)
    for c in out:
        if kModemSpeed:
        if c == '\n':
            lines += 1
        if lines >= kLinesPerScreen:
            lines = 0
    if end:

def writeln(s):
    """Calls write with `end='\n'`."""
    write(s, end='\n')

Now if I want someone to talk in bold cyan (which I chose as my standard NPC color), I just
writeln("|RE|LC%s says, \"Mellow greetings, sir!\"|RE" % (npc["name"],))
RE before and after text is important, resetting colors from previous strings.

The one problem is I can't use | in ASCII art, so to generate my big character logos, I write a script mysticFiglet.zsh, making : the vertical char:

figlet -f small "$*"|sed "-es/|/:/g"

Flow Control

The great part about a doorgame is, no event loop. Oh, there's a REPL of sorts:

    def run(self):
        self.running = True
            : Python 3.8: while line := self.prompt():
            while self.running:
                line = self.prompt()
                if line == None:
                words = line.split()
                if len(words) == 0:
            self.doHelp("?", "_outro")
            return 0
        except (EOFError, KeyboardInterrupt):
            return 0
        except Exception:
            writeln("|GG|RE|BR|FWERROR! Something went wrong.|RE")
            logging.error("", exc_info=True)
            return 1

It just blocks at input (called by readln, called by prompt), and continues on until self.running = False, or an exception is thrown. Whee! The usual game loop makes it impossible to get stateful input without leaving giant memos of current state, so the event loop can keep cycling at 60fps.

Data Files

One solution would be to just make everything in classes, and use Python's pickle to archive everything. But the problem is that's pure binary, you can't easily hack on it or see what's happened, and on load you can't change any values. So instead I only use primitive types and archive everything to and from JSON. Floor maps and Players have a toData() method which produces a JSON-compatible data structure, and their initializers take a data structure and pull out or translate the values they need. It's a little more work, but means I can debug faster and add new fields faster.

The floors use a super-simple data file format, with an ASCII roguelike map and a series of command lines to create NPCs, traps, text, etc. The weapons and armor are listed in an even simpler format, with space-delimited fields.

Both the command lines and item formats are just run thru a tokenizer I wrote, that reads words, "strings", numbers, and [lists]:

type    name        cost    stat    bonus   damage  inflicts    twoh
wpn "Club"      5   Str 0   3   [blunt]     0


{'type': 'wpn', 'name': 'Club', 'cost': 5, 'bonus': 0, 'damage': 3, 'inflicts': ['blunt'], 'twoh': 0}

Mystic Dungeon BBS

As previously mentioned, I'm interested in BBS's.
So I set mine up! After 30 years, The Dungeon is back as The Mystic Dungeon BBS!


Connect with: telnet 1666
(soon I'll have a proper domain, and SSL cert so you can ssh host 2666)

Set your terminal for black background, 80x25, and UTF-8, I dunno what it'll do to DOS CP, but everything's Unicode now.

If your Mac doesn't ship with a telnet anymore, you can grab the previous OS's one from a backup, or port install inetutils and then use gtelnet.

Of course visit the Doors to try the Mystic Dungeon doorgame!


I wrote the start of this last weekend in a Ballmer Peak, and have been adding to it since. Now it has a town and a dungeon with 8 levels, 24 kinds of monsters, combat, an innkeeper for resting, a merchant for buying equipment, and banker for saving money between deaths.

Coming up are potions and other useful items, traps, chests (which are often trapped), and magic.

Right now, only Fighters make any sense to play, though different races have some advantages (Dwarf especially for long-range darkvision in the dungeon, since I don't sell light sources yet!). The character shown is a bit cheaty on cash and XP; it'll take a long, long time to reach that level legit.