Hey, dial this number. It's a cool BBS!

I was rewatching Jason Scott's BBS Documentary, as one does.

See, I was making a fake BBS:

Doors BBS

The only purpose of this was to test out text render/entry in Kawa Scheme for Lisp Game Jam. It also does a modem squeal on "connection", so I know audio works, & this brings back all the emotions.

Back in the '80s, early '90s, there were four (ish) kinds of BBS's.

  1. Corpo spaces like GEnie, Delphi, CashWe$erve. Pay per hour (pretty cheap on Delphi), but only place you could get some corpo information & celebrity chats, for me with real cyberpunk writers like Walter Jon Williams & Bruce Sterling; for normies, with, I dunno, Debbie Gibson?

  2. Straight-laced BBS's for specific topics, official forums for game companies, etc. Boring, incredibly oppressive rules. Get in, get info, get out. A few had doorgames, but you had to avoid smack-talk, which was no fun.

  3. FidoNet. I was on a couple of these but didn't engage much. Watched the doc up thru the Fidonet chapter, & jiminy I'm glad I avoided that nest of vipers. Yes, continent-wide communication, but also the people in charge of that were the worst kind of petty unelected tyrants. Smaller networks like CitNet & WWIVnet were more forgiving, & once Internet access was gained, it was a free for all on email & USENET. But local culture was still better on non-networked boards.

  4. Insane local h4xx0r boards where we "stole" (without hurting anyone much), broke laws (that needed breaking), & had a good time. If the Sysop was your friend and/or you uploaded cool stuff, you could do anything. If not, you could hit the road to the next one. I learned an enormous amount both technically & otherwise on these boards.

My own boards were type 4. The Dungeon was a STarNet (Atari 520ST, floppy) board, mostly messages & files for my TTRPG games. I remember I typed in complete character creation for RoleMaster 2E + my setting stuff, so I wouldn't have to do what's now called "session zero" at the table. But we did all sorts of weird stuff on there! Turns out "role-play" isn't just rolling dice at the table!

The Caves of Steel was Fnordadel (Citadel on Mega 2 ST), some TTRPG chat but mostly doorgames I wrote or modified, & file share either from local warez or that I'd download from the Internet; full-time Interent access was hard. Users could pay for higher minutes (game turns & download time), so mostly it paid for the lines & then some. I ruled with an iron but exasperated fist; half the users were very non-technical, & I had to write long HOWTOs; the other half were l33t h4xx0rz who literally were trying to get shell.

And what's weird is 40 years later, these are STILL your choices, but on social media.

  1. Is still corpo hell-site silos, f-c-b--k, shitbird, taktak, & insta-not-matic, if you know what I mean. If you stay there, you deserve what you get. At this point, you have to know there's alternatives, but to willingly say "but Britney Spears is there! I have to follow BREETNEE!", just ignore all the hate crimes going on there.

  2. Masto soc, bluhski, threads (is that still up?), & other wannabe-open but not really sites. Dull. RSS feeds without the virtue of open protocols. Sometimes a "starter site" to reach the better levels.

  3. Accidental hell-sites. Reddit, tumblr, slashdot (still exists!), like nobody reasonable would go there, the social graph is horrific, but sometimes amazing things get spewed out of them. Don't engage, unless you like living in toxic codependence with people you hate. I was hopeful the Digg reboot would pull out of this & into Wild, but it got overrun by spam.

  4. Wild Fediverse. Instances outside masto soc tend towards small, focused communities. radical hacker, lgbtqia+, furry, specific local areas, or weird subcultures. I stay on an instance based on a past attempt to pay for a private garden community. Just enough barrier to entry that we rarely have Nazi bars, & actively hunt down & block advertisers & propagandists. Fedi LITERALLY posts cheering images of Mario (not the videogame) & guillotines & burning warehouses. Like sticking a bunch of corpo skulls & neckties on pikes outside your post-apocalyptic wasteland town.

Beyond Thunderdome

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.

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)
        else:
            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:
        sys.stdout.write(c)
        if kModemSpeed:
            time.sleep(1.0/kModemSpeed)
        if c == '\n':
            lines += 1
        if lines >= kLinesPerScreen:
            readln("|RE|FC[MORE]|RE")
            lines = 0
    if end:
        sys.stdout.write(end)
    sys.stdout.flush()

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
        try:
            #TODO: Python 3.8: while line := self.prompt():
            while self.running:
                line = self.prompt()
                if line == None:
                    break
                words = line.split()
                if len(words) == 0:
                    continue
                self.parseWords(words)
            self.save()
            self.doHelp("?", "_outro")
            return 0
        except (EOFError, KeyboardInterrupt):
            self.save()
            return 0
        except Exception:
            writeln("|GG|RE|BR|FWERROR! Something went wrong.|RE")
            logging.error("", exc_info=True)
            self.save()
            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

becomes:

{'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!

mystic-dungeon-2019-02-23

Connect with: telnet mysticdungeon.club 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!

Dungeon-2019-02-23-23.56.20
Dungeon-2019-02-23-23.56.45

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.

BBS

My teenage years IRL were difficult; I grew up in a time and place where to be a nerd was as bad as being a fag, you'd get beaten or murdered for that by the inbred hillbilly apes. Happily, there were local BBS's full of other nerds, COMER (cbbs? Terrible people, but it had dnd, a fairly complex D&D command-line game), Elvin Forest (Apple ][ board, nicer social group), Three Roses Inn (WWIV, Wil's board was our game hangout), a few others, and eventually my own, The Dungeon (first on Atari 800 running software I don't recall, later on Atari ST running STarnet) and later in Spokane I ran The Caves of Steel (Fnordadel on Atari ST/MiNT UNIX-like).

I still hit up telnet-based boards sometimes, I just tried lmorchard's Decafbad and did some turns in LORD, and I'll check back on that; I don't think he has Trade Wars, which was my game of choice, and the source of my old nickname Kamikaze. I've tinkered with reviving The Dungeon on MysticBBS and that may work out, but hosting's difficult. A better thing might be to make a web-based BBS, which is pretty trivial, but then it's not really a "board" anymore, you know?

So I watched the BBS Documentary again, it's been… 14 years?

The early days part of this, the culture of sharing and flame-warring, ah, the good old days. I had very little contact with/interest in FIDOnet and their horrible internal politics, I was on WWIVnet and Cit-net, both of which were much more chill. I loathed then and still loathe the cracker/h4xx0r parasites, totally useless wastes of skin. The ANSI art scene kiddies were wankers, but at least they made something. My bias then as now: I program, and I write, and those are what I respected back most.

But I find I'm still angry about SEA vs PKWare. SEA released public domain ARC software with source but no spec for the format, and then sued Phil Katz for using that source to make a better product, and later to make ZIP with a shared format everyone could use… Thom's sad he got some hate mail in among his giant corporate paychecks, oh no. Phil was so fucked up by it he drank himself to death. Fuck you, Thom, there is no justice in the world that you're alive and Phil's not.