Videogames and Storytelling Mix like Water and Sodium

At best you get tears & corrosive salt water, at worst you get a sodium explosion.

My philosophy of games:

  1. Games are about environment and gameplay only.
  2. Graphics don't matter much, as long as they communicate.
  3. Character and story are what you bring to it, they should not be part of the game.

So, I just dropped a lot of words there with fuzzy definitions:

  • Games: I mean all of tabletop boardgames, role-playing games, and most often videogames of all genres. There's less difference between the Warlock of Firetop Mountain gamebook and Myst than there is between that gamebook and David Foster Wallace's Infinite Jest. And if you tear out the system from Warlock, you get Advanced Fighting Fantasy or Troika!, which is a very nice little RPG for wandering a weird, almost hallucinatory fantasy world with no book, no defined character, no story.

  • Environment: The world you explore. Some of this uses traditional writing skills for designing non-player characters and describing the tone and events, but also architecture, painting, 3D modelling for designing environments, music for writing soundtracks, foley for making environmental sounds.

    I recently enough mentioned this in Videogame Exploration, and I want to especially repeat my suggestion of Bernband, which is goofy, low-rez, standee sprites… and one of the most immersive environments I've ever played in.

  • Gameplay: The continuous loop of doing something, getting feedback on what happened, maybe scores or your position or just your understanding of the environment changes, and then repeat forever. That loop might take milliseconds in action games, to minutes or hours in hard adventures. There's a… fixation? a high… you get from that loop when it works right. "Just one more turn" says the Civilization junkie at 4AM before blowing off work. "Just one more mineshaft" says the Minecraft player. "Just one more quest" says the ESO player.

  • Graphics: This is almost irrelevant, really, despite the huge amount of effort and money spent on it. It doesn't matter if it's text adventures like Colossal Cave Adventure or Infocom's games, character-grids like Rogue and many descendants, 2D or 3D tiled graphical environments like Ultima IV, Super Mario Bros, or Castlevania, painted images along with text like Sierra's King's Quest or the LucasArts SCUMM games, up to 3D FPS graphics like Doom or Elder Scrolls Online. Good gameplay with any graphics is immersive, bad gameplay with perfect graphics is not.

    Easy way to test that: The most popular videogames of all time are: Mario (2D tiles), Zelda (2D & very simple 3D), Minecraft (blocky 3D with the worst programmer-art textures), Animal Crossing (very simple 3D imitating 2D). Graphics-intensive games pop up and vanish, because they're uninteresting.

  • Character: Who you are. In the better kinds of games, this is left blank for you to fill in. If the game engine doesn't accomodate dialogue even as well as Ultima I did, you're a mute wanderer who breaks into peoples' homes, smashes their crockery looking for coins & drugs/potions, maybe hits X to hear if they have any rumors or leads, then leaves. In action games, very little dialogue is necessary, your weapons speak for you.

    If you can freely define your Character, that interferes with Story. Until recently, at least you could rename your character, but with full voice acting for many games, they either obnoxiously refer to you as "Vestige", "Adept", "Friend", etc., or don't refer to you at all… or don't let you rename your character.

  • Story: This ties in closely with Character: What do you do? If you can wander as you please, make your own fun, whether that's good or harmful to the environment or NPCs, then you have no story, only gameplay. If you can only ride along like an amusement park railroad ride, get a story told to you and then pew-pew-pew to shoot targets, move on to the next stop, you have no gameplay, only story.

    The Disneyland ride model is a big influence, but AAA "games" with story are mostly frustrated Hollywood wastrels in the wrong medium. The obvious recent example is Death Stranding, which has hours of awful cutscenes with Hollywood people who have nothing to do with the game: A mediocre walking simulator/platformer; without the cutscenes, it might even be fun, if tedious.

An unfortunate result of focusing on Story has been forcing the player to make bad dialogue/action choices to advance, stay on the railroad unable to get out and wander away. Heavy Rain's no-choice "Press X to Cry Jason" rather than man up and go look for your lost child.

The now-defunct Telltale Games' Minecraft Story Mode had a painfully fixed main character and plot, and a doomed character, but let you choose social consequences with allies… which were then forgotten in the next chapter.

Early Final Fantasy games had a totally blank slate. FF3 is right on the cusp; it gives you a sandbox to explore, eventually hit a switch to open the next, bigger sandbox, repeat a couple more times, finally a long multi-part endgame and post-game sidequests. The characters have a secret backstory, but you can rename them, give them any job you want, play them however you want. I did one playthrough with boring Warrior, Thief, White Mage, Black Mage, another using Monk/Black Belt, Red Mage/Dragoon, Scholar/Geomancer, Evoker/Summoner. Utterly different gameplay even if I ended up clearing the same dungeons. My bizarro party got to level 99 to fight the giants.

By FF4, the characters and story are locked in place, you can enjoy it or not, and certainly the art's great and I quote "you spoony bard!" all the time, but you have no choices. Not that I'm blaming that all on JRPGs — there's Japanese games with freedom of choice, and Western games fixed on one character, Gabriel Knight is one of the earliest of this archetype.

Gamebooks like Tunnels & Trolls solos, Fighting Fantasy, Lone Wolf, etc. are odd hybrids since they have story, but almost never have a defined character (a few do, like Creature of Chaos). The more linear the gamebook is, the better the story is, but the less interesting it is to play; there's several I've done that had one win and many deaths, and so cannot be replayed. The more meaningful choices they offer, the more incoherent the gamebook becomes, just a bunch of random scenes because you can't build up any meaning like linear fiction does.

My objection to Dungeons & Dragons adventures from Dragonlance (1984) on, is that it went from a game of freeform dungeon crawls, hex crawls, or "West Marches", wandering the Referee's world, maybe loosely using a Greyhawk map or Outdoor Survival, often made up in the days between games or improvised on the spot; to railroaded "adventure paths" with fixed character roles (either named and unkillable like DL or just "must have fighter, thief, cleric, magic-user, bard, or you will fail"). 5E has become entirely that, their healing/action economy even requires a specific pacing along the railroad, and their world maps are just one-path flowcharts you move along like Candyland.

So in conclusion (almost), just say no to story in your games. Look for that infinite high of gameplay.

  • The Devil's Advocate: There are some attempts to make character or story "gameable", rather than just a railroad, most notably Chris Crawford's Erasmatazz, which he then replaced with Storytron, now Wumpus (no relation to the real Hunt the Wumpus game). These have computer-controlled drama, you talk/choose interactions with different "emotional weights", and the NPCs react appropriately. These suck as games. They can be a little interesting as a puzzle to talk to the NPCs, find out what's going on, maybe push one of them into a "win" state. Nobody'd spend long on one.

It's worth looking at Chris's development woes. Sequentiality and list of encounters in Le Morte d'Arthur he gave up on gameplay, it's a railroad click-thru of Mallory's book, with a single fame/piety score to get win/lose.

His Gamers or Storytellers seems to be an admission of defeat. Yet he still has bigoted, ignorant ideas like:

This also plays into the old “evolution versus revolution” dilemma. I have long held that games will never evolve into anything with artistic merit, because the gaming audience does not expect artistic content from games. You can’t sell Beef Wellington to people who want candy. You can’t sell poetry to people who read comic books. You can’t sell art-house movies to people who watch cartoons. And you can’t sell artistic content to gamers who want action and instant gratification. Games as a a medium are ill-disposed to evolve in a storytelling direction.

This is why he fails. Games can have artistic content, just not inbred Hollywood-imitating content. There is plenty of poetry in comic books, obviously Sandman but many an issue of Detective Comics (the smarter Batman series) has moved me deeply. Many art-house movies are cartoons, or vice versa, or were when theatres were a thing, I'd start with Don Hertzfeldt's Rejected and Ralph Bakshi's Wizards. You can't sell poison apples to gamers, not more than once anyway.

I had a look at his soi-disant "Wumpus", and got this, his "non-technical" user interface. It's incredible to me that this is the guy who made Eastern Front and Balance of Power, which were techy but not a giant wall of UI clickies, badly sized in a window. Yes, it's Java, but you can make attractive and usable Java UI, it just requires effort.

I figured out eventually that you can hit Editor/Run Rehearsal (?) to play in something like a dialog box UI, was able to play through a very dull conversation, and then it gets stuck with Jeff explaining widgets to Sam in an infinite loop. Excellent. Obviously story-gaming is a solved problem. 🙄

Reinventing the Wheel

Sure, there's existing code. Somebody Else's Code. It works fine, maybe not as fast as you'd like, or the interface isn't quite right. That's how it often is with me and SRFI-13. Olin Shivers is a skilled Schemer, back when that wasn't cool (OK, it's still not cool), but some of his APIs enshrined in early SRFIs drive me a little nuts, and the implementation is slow because it's so generalized.

So after a few false starts and failed tests, I now have these pretty things: (updated 2020-11-10, enforced hstart, hend boundaries)

;; Returns index of `needle` in `haystack`, or  if not found.
;; `cmp`: Comparator. Default `char=?`, `char-ci=?` is the most useful alternate comparator.
;; `hstart`: Starting index, default 0.
;; `hend`: Ending index, default (- haystack-length needle-length)
(define string-find (case-lambda
    [(haystack needle)  (string-find haystack needle char=? 0 )]
    [(haystack needle cmp)  (string-find haystack needle cmp 0 )]
    [(haystack needle cmp hstart)  (string-find haystack needle cmp hstart ) ]
    [(haystack needle cmp hstart hend)
        (let* [ (hlen (string-length haystack))  (nlen (string-length needle)) ]
            (set! hstart (max 0 (min hstart (sub1 hlen))))
            (unless hend (set! hend (fx- hlen nlen)))
            (set! hend (max 0 (min hend hlen)) )
            (if (or (fxzero? hlen) (fxzero? nlen))
                
                (let loop [ (hi hstart)  (ni 0) ]
                    ;; assume (< ni nlen)
                    ;(errprintln "hi=" hi ", ni=" ni ", hsub=" (substr haystack hi hlen) ", bsub=" (substr needle ni nlen))
                    (cond
                        [(cmp (string-ref haystack (fx+ hi ni)) (string-ref needle ni))  (set! ni (fx+ ni 1))
                            ;; end of needle?
                            (if (fx>=? ni nlen)  hi  (loop hi ni) )
                        ]
                        [else  (set! hi (fx+ hi 1))
                            ;; end of haystack?
                            (if (fx>? hi hend)    (loop hi 0) )
                        ]
        ))))
    ]
))

;; Test whether 'haystack' starts with 'needle'.
(define (string-has-prefix? haystack needle)
    (let [ (i (string-find haystack needle char=? 0 0)) ]
        (and i (fxzero? i))
))

;; Test whether 'haystack' ends with 'needle'.
(define (string-has-suffix? haystack needle)
    (let* [ (hlen (string-length haystack))  (nlen (string-length needle))
            (i (string-find haystack needle char=? (fx- hlen nlen)))
        ]
        (and i (fx=? i (fx- hlen nlen)))
))

Written for Chez Scheme, caveat implementor. BSD license, do what thou wilt. If you find a bug, send me a failing test case.

I don't normally bother with fx (fixnum) operations, but in tight loops it makes a difference over generic numeric tower +, etc.

I Think We're Property

Dangers of near approach -- nevertheless our own ships that dare not venture close onto a rocky shore can send rowboats ashore --
Why not diplomatic relations established between the United States and Cyclorea -- which, in our advanced astronomy, is the name of a remarkable wheel-shaped world or super-construction? Why not missionaries sent here openly to convert us from our barbarous prohibitions and other taboos, and to prepare the way for a good trade in ultra-bibles and super-whiskeys; fortunes made in [155/156] selling us cast-off super-fineries, which we'd take to like an African chief to some one's old silk hat from New York or London?
The answer that occurs to me is so simple that it seems immediately acceptable, if we accept that the obvious is the solution of all problems, or if most of our perplexities consist in laboriously and painfully conceiving of the unanswerable, and then looking for answers -- using such words as "obvious" and "solution" conventionally --
Or:
Would we, if we could, educate and sophisticate pigs, geese, cattle?
Would it be wise to establish diplomatic relation with the hen that now functions, satisfied with mere sense of achievement by way of compensation?

I think we're property.

I should say we belong to something:
That once upon a time, this earth was No-man's Land, that other worlds explored and colonized here, and fought among themselves for possession, but that now it's owned by something:
That something owns this earth -- all others warned off.
Nothing in our own times -- perhaps -- because I am thinking of certain notes I have -- has ever appeared upon this earth, from somewhere else, so openly as Columbus landed upon San Salvador, or as Hudson sailed up his river. But as to surreptitious visits to this earth, in recent times, or as to emissaries, perhaps, from other worlds, or voyagers who have shown every indication of intent to evade or avoid, we shall have data as convincing as our data of oil or coal-burning aerial super-constructions.
Book of the Damned (1919), ch. 12, by Charles Fort

Eerily repeated in a lot of science fiction. Ironically, H.G. Wells' "War of the Worlds" is nearly a "we're property" story, but Wells irrationally hated Charles Fort. Likes repel, I suppose.

A few obvious ones, but there are dozens more (put them in comments if you like; archive.org links preferred!):

Haunted Halloween

I've added a new seasonal game to my Mystic Dungeon: Haunted Halloween.

A text-mode twin-stick shooter (well, except it's an emulated Atari 800 "text mode", and the sticks are WASD and IJKL, I haven't written joystick support yet).

Five different levels:

  • Pumpkin Patch: Collect pumpkins for ammo.
  • Dark Forest: Find the path through.
  • Graveyard: Easy, just dodge gravestones and monsters.
  • Corn Maze: Unless you're Ted Forth you won't have a problem with this maze.
  • Haunted House: Just run thru the spooky house full of ghosts, and other monsters crawling in from the woods, get candy, get out!

Three difficulty levels:

  • Treat is turn-based, but there's so few monsters & candies you won't get a high score.
  • Trick is real-time, but you can generally outrun monsters.
  • Nightmare has twice as many monsters & candies, so it's the best way to get a high score… or die quickly, completely overwhelmed.

You can also play it like a stealth game, H hides you from non-adjacent monsters, so you can just run in, hide, wait for them to move off.

You collect candies for score (and banishing monsters earns candy), but every time you move to the next level you eat 10 candy to recover a hit and get some free pumpkins. So it's usually better to stay and clear all monsters, pick up all candies, then move on. But if you have a bunch of witches and ghosts, might be worth running away early.

The interesting thing from development is how little code is required for this kind of game. Halloween is under 1000 LOC, and that includes some long text blobs! Portal Worlds was 3000 LOC, Dungeon's over 3200 LOC and not even close to "done".

I'm still working on Public Caves, moving from BASIC to web-tech requires a lot more infrastructure!

Computer Archaeology: Public Caves Discovered!

Exploring the archives of the People's Computer Company (a public timesharing computer center in the early '70s, yes before home computers), and many of the programs we're familiar with from David H. Ahl's Creative Computing come from here. 15 different variations on guess the number and guess a coordinate, sure, but also some really important things, many of them long forgotten.

Then I find this artifact:

pcaves

What the. This is basically a MUD†, from 1973!

Source code (uses a very long TREES library on previous pages).

Everyone knows WUMPUS, which is based on CAVES, but this is the rock star of these! How does everyone not have a copy of PCAVES? This is like finding a working Airwolf helicopter in a cave with ochre handprints on the walls. HOW THE FUCK did cavemen do that? Why don't we all have an Airwolf, if it existed 47 years ago?!

So anyway some barely-modernized version of this will be added to the MysticDungeon soon, you'll all be able to graffiti up a cave!

† more like a MUSH ("Multi-User Shared Hallucination") with one user at a time, specifically.

  • Note: You can play a version from the Narrascope conference 2019: PublicCavesNarrascope
  • Renga in Blue typed it in for the above event, and briefly reviews it as an adventure game. Which it's not, this is a social environment, literally a MUSH.

What I'm Playing: Clubhouse Games: 51 Worldwide Classics

What I'm Playing: Clubhouse Games: 51 Worldwide Classics

I loved the version on the DS, so I got this the moment it was released on Switch.

You start by picking from a set of little human pieces, which you can recolor their skin & hair, but not their appalling clothes. I almost went with Dad there, but in the end Cool Bro looks better. Looking at the random other players later, I see a lot of them chose that or Suit Guy. As I've noted before, Nintendo has Mii avatars, and then doesn't use them in games even where it'd make sense. You see a little face photo of your Mii in some games, but it should use your Mii in the world! Nintendo is so frustrating and anti-social.

Then you go to a globe UI, with figures representing "guides" that give you a menu of a few games. Or you can just pick any game from a preposterously long line menu, or you can hit X (up button) to switch to a grid which is more reasonable. UX is very confused, always a couple extra button presses or spinning the cursor around a too-large area to get to anywhere you want. You unlock more guides by playing games, earning trophies.

Your piece has up to 5 "recommendations", but you can't set them from in the game, you have to go all the way back out to the globe, find your piece, and add them from a list. And these don't help you jump back to a game fast, you have to find it in the grid every time.

Like almost all Nintendo software now, there's no settings for audio, and the "music" is driving me insane, but I need the sound to play some of these, so I'm constantly muting and unmuting. At least in the old days, Nintendo's music had complete scores, but they've apparently fired all their musicians, this is just beep-doo-beep-de-beep, over and over until I stab someone.

Each game starts with couple figures playing the game with often amusing commentary—the kids narrating Connect Four as a samurai duel is fantastic—often enough tutorial for anyone, but it immediately comes up to a menu with "How to Play" and Play, and hitting + in game usually gets a help menu. They're trying to teach you games you may be unfamiliar with. However, showing the tutorial EVERY time you start a game until you hit X (up) is insipid.

There are medals for winning against the AI and playing at least 2-4 times depending on the game, so there's a little grind possible if you're into that.

Nintendo History guide gives you: Hanafuda, Gomoku, President, Shogi, and Riichi Mahjong, which Nintendo made for a century before going into the videogames business.

Many of the games have local and Internet multiplayer, which I haven't yet tried. I expect the usual Nintendo® Quality™ networking, which is to say everything will drop out constantly. I'd rather play against AIs.

Current playlist of 11 good, 16 bad, 25 unplayed doesn't seem all that positive, but the good games are usually very good, and you can just ignore the stupid ones. The constant terrible music is the only strong negative.

I'll keep updating this post as I play more of them.

★★★★☆

bold is good, italic is bad, plain is I haven't bothered to play it yet.

  1. Mancala: aka Awari. Anyone who's typed in games from Basic Computer Games is intimately familiar with Awari. It's a weird little game, but fast and fun, and there's just enough strategy against a smart player (not the AI) to make it hard to win.
  2. Dots and Boxes: "Boxing" is also very familiar from school. The first player (default to you) is at a severe disadvantage, but it's possible to only give up a few boxes to the second player, and then clean up the rest.
  3. Yacht Dice: aka Yahtzee, Poker Dice. Nice enough, but I found the controls a little finnicky, it should not use the "do stuff" button for both pick and reroll. Slaughtered the AI, as one would expect.
  4. Four in a Row: aka Connect Four. Pretty dull, aside from the tutorial.
  5. Hit and Blow: aka Mastermind, Bagels, etc. with an unfortunate translation name. But I dislike the color-matching version, I'm a numbers person.
  6. Nine Men's Morris: I don't understand this game. You start playing while setting up, and it just screws anyone who loses one piece. Also obviously should have been #9.
  7. Hex: Again, should've been #6. It's a road-building game, dumb low-challenge game.
  8. Checkers
  9. Hare and Hounds
  10. Gomoku
  11. Dominoes
  12. Chinese Checkers: aka Pegboard. Not Chinese, sort of checkers except nothing is captured.
  13. Ludo: aka Parcheesi, Sorry!, Trouble, etc. I switch to the 3 dice to come out rule, rather than automatic/on 6, otherwise it's Parcheesi (not quite Indian Pachesi), a good being-dicks-to-each-other race game.
  14. Backgammon: An ancient dice game, a good fun game. I dislike the joycon controls, cursor-moving by spike around the track instead of selecting individual pieces left/right.
  15. Renegade: aka Othello, Reversi, etc., pretty standard. I lost really badly the first round, and then eked out a win, I've always been bad at this game, or anything that requires me to do deep analysis of simple positions (go, checkers, etc.), I'm a broad strategy for complex positions (wargames) thinker.
  16. Chess: There's a sort of lesson program, but the starter AI is incredibly suicidal, so it's not even interesting. Probably it gets harder, but I'm not that interested yet. I dislike the set design, it's very hard to tell the pawns apart from bishops, queen from king, and there's no alternate set option. Still, it's Chess.
  17. Shogi
  18. Mini Shogi
  19. Hanafuda: Very pretty cards, but I've never learned the sets, and visual association like this is harder for me. AI let me win 3/3 on this, which is crazy since I was just clearing chaff, I never saw more than 2 cards of a good set. However, after winning the guide "gave me a gift" of Mario-themed Hanafuda cards, so that might be easier for me. I think this might be worth practicing at.
  20. Riichi Mahjong
  21. Last Card: aka Uno, Crazy Eights. The card branding is almost but not quite infringing on Uno, so it hits that uncanny valley effect, and I kinda hate looking at it. AI didn't stand a chance, I don't know what they were even doing, picking cards at random? There isn't much strategy to Uno, but no strategy means you lose.
  22. Blackjack: Gives a limited number of rounds, and chips but you can go into debt. Does not have Split, which is kind of amateurish, and it doesn't have the dealer check their down card on A or 10 up, so you might play a round and find out they have Blackjack. It's bizarre beyond belief that they didn't make Blackjack be game #21, but #22. But I can always play a few hands of Blackjack.
  23. Texas Hold 'em: aka Poker.
  24. President: aka Asshole, Daifugo, etc. Kind of an annoying party game, giant hand of cards to manage at start. I hate the rich-get-richer mechanic, which is why it's sometimes called Capitalism, but it's more like Monarchy.
  25. Sevens
  26. Speed
  27. Matching
  28. War:

    War, huh!
    What is it good for? Absolutely nothin'
    Say it again, war, huh!
    What is it good for? Absolutely nothin', come on!
    —Bruce Springsteen, "War"

    I timed this, and it takes 5 seconds and one button-press for each card, and it always resets the cursor to the rightmost card, so it takes a minimum of 2.5 minutes. Was this included as a prank?

  29. Takoyaki: Ten octopus. Almost as random as War, but you get a choice when Joker is drawn, and it's much faster. Winning this nonsense unlocked a Mario-themed card deck!

  30. Pig's Tail: aka Buta no shippo. Instead of a little action game of throwing drawn cards into a pile but avoiding matches, it's a completely random War-like, with a slow "penalty cards" deck.
  31. Golf: Cute little putting game, only has 3 clubs: Driver, Iron, Putter. It's not quite a wacky golf or mini-golf, but it's not any kind of realistic golf simulator.
  32. Billiards
  33. Bowling: Has touch controls or joy-cons, but I have a Switch Lite, so I just went with touch. A little rocky start, but then I can get a strike most throws. IRL, my aim is a little too erratic, but I've played hundreds of hours of Ramp Champ and other touch-stroke games on iPhone, so this isn't hard for me.
  34. Darts
  35. Carrom: Like marbles or pogs, but without the freedom of motion, and a strange "queen" you have to take another coin after or you put it back. I don't know that I like this game, it takes too long and the controls are stupid (stick to move up/down only, L/R to aim?!), but it's competent and kind of interesting.
  36. Toy Tennis
  37. Toy Soccer
  38. Toy Curling
  39. Toy Boxing: Lightly based on Rock'em Sock'em Robots, but without the pop-up heads or movement forward/back, just button-mashing. Controls are A/B to wobble your guy's arms out to hit or up to block, which is implausibly hard to switch between, they should've used L & R shoulder buttons. Normal AI is easy, Hard AI is brutal, I assume the others are unwinnable?
  40. Toy Baseball: Accurately simulates a cheap mechanical baseball game from the '60s, with maybe the worst pitching stick control I've ever seen. Once I got the hang of it, I recovered from 0 runs to 3, while the machine that doesn't fumble with sticks got 6. Not likely to play more. There's no Toy Football, as that's not "worldwide".
  41. Air Hockey
  42. Slot Cars
  43. Fishing
  44. Battle Tanks
  45. Team Tanks
  46. Shooting Gallery
  47. 6-Ball Puzzle: A weird collapsing ball Tetris variant, not as interesting as Bejewelled or Tetris.
  48. Sliding Puzzle
  49. Mahjong Solitaire: 20 layouts each for Beginner, Standard, Advanced difficulty. Has a nice color-assist, which is good if you can't easily make out the stack depth. I can see this being a big time-killer for me.
  50. Klondike Solitaire
  51. Spider Solitaire
  52. Bonus: Piano: The piano has a single octave, the help says the buttons or shaking joycons does something, but it does nothing on the Switch Lite at least. Turning the device upside down gets you a synth with 4 octaves selectable by button, but the keys don't rotate into normal position, so it's pretty unusable. I would prefer a real "toy piano" simulator, but then you may as well buy a teaching piano toy or a proper cheap synth, they're $30 or less on the 'zon.

LISP Machines

I really wish we had a front-end to software even half as useful as this in the 21st C, but technology has regressed massively since the '80s and '90s. Some of the old IDEs, before they became bloated "enterprise" software (because giant mega-corporations paid for them, not individual programmers, so the IDE makers serve their paymasters), started to slouch towards this kind of usefulness but fast and small. CodeWarrior, Project Builder/Interface Builder, Borland's Turbo Whatever.

emacs isn't the answer, it's an abandonment of the question; "modern" emacs turned away from the LM zmacs model, it's now just a terrible LISP interpreter with a bad text-only-editor front end; you can make tools in it, but nobody sane will want to use them. I'm perfectly comfortable with the emacs editing keys (well, obviously, Macs use emacs keys for all text areas), but the machinery in it is just broken.

I get excited about Chez Scheme having a nice REPL, with history and can edit multi-line blocks in the REPL, unlike the crappy readline almost every other Scheme uses. But it doesn't do hypertext, it doesn't do graphics, it barely has tab completion (procedure names only), it doesn't have any inline documentation & source inspector; all of those were in the LISP Machine.

When I'm working, I have my text editor (BBEdit or Atom mostly), a terminal with the REPL running and I copy-paste to it, 3-4 PDFs open (R6RS, R6RS-lib, CSUG, sometimes TSPL), and a web browser pointed at the SRFIs. If the Internet connection went down, I'd have to search the SRFI sources to figure out what's in there. I really need a better tool for this.

DrRacket can do some graphics inline, and the tab completion shows documentation hints in the top right corner, but as I note every time, it doesn't really have a REPL because it destroys the interactive environment every time you edit code; utterly useless for code exploration. And in practice, Racket is really horrifyingly slow; it does fine in focused benchmarks but real-world use it just falls over drooling.

The graphics part's sort of irrelevant, and sort of not; the way the LISP Machine worked was getting a "presentation" form for an object, which would render as text or drawings or images, and interacting with it sent messages back to that object. That's probably out of scope for anything except a complete new OS and terminal. A simplistic number-tagged hypertext would be good enough and orders of magnitude easier.

So. I'm not sure what to do here, I don't want to just complain about tools and not do anything about them. I could try to extract the docs to make a hypertext doc system; a lot of text processing on TeX source sounds painful, and a one-off job, I want a more universal solution. It may be possible to hook into Chez's completion to call a help system. Or it could be a standalone program that you feed several doc sources into, and it lets you search against them. Dash does that, but it's Mac and C/Objective-C primarily, and does poorly at other docsets.

Retrospective: PortalWorlds

What worked, what sucked, lessons learned, The More You Know, and knowing is half the battle, go Cobra, etc.

JS: Test Your Libraries -1

Especially the hard-to-test parts. I had one "obviously correct" array shuffle function I've been using for maybe 10 years:

WRONG RIGHT
function arrayShuffle(arr) {
    for (let i = arr.length-1; i >>= 1; --i) {
        const j = Math.floor(Math.random() * i); // WRONG. NO.
        const tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    return arr;
}
/** Fisher-Yates */
function arrayShuffle(arr) {
    for (let i = arr.length-1; i >= 1; --i) {
        const j = Math.floor(Math.random() * (i+1)); // RIGHT. YES.
        const tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
    return arr;
}

And because of that, I got a distorted shuffle. I only noticed because no mob using my dumb-as-rocks not-even-AI would ever choose to move north… everything ended up piled up at the bottom of the map.

It's hard to test randomness, but you can check that values are in a sane range. For a decade I've had wrong code just copy-pasted into new projects because I never checked my assumptions.

JS: UI Libraries are Great +1

I mean, I knew that. But how easy it is to throw something up is a bit of a surprise every time I do it. It's a fast, dynamic, functional/OOP language that also has good graphics and sound libraries (I didn't do sound in PortalWorlds, but I will before 1.0), and tolerable event handling these days.

I'm not releasing the full source, but in a week or two I'll add a few more things and release my common library under BSD license. This is mostly stuff from my Learn2JS project, and some from TTMS-76, but those have a giant framework for running scripts, this is just a few functions for a pure JS application, and greatly condensed.

Pulling in common code makes this stuff so much easier.

JS: Application Structure -1

There's no proper "run loop" in PortalWorlds, and late in the process that bit me in the ass.

What you should do is, collect events, put them in a queue. Every X milliseconds (I usually use 30 FPS, so 1000/30=33.333ms) have setInterval do an event-update-redraw loop. If something takes a long time or requires animation updates, it must be done in chunks or in a background thread (using Web Workers ). This is true of any application, not just games.

What I did wrong is having events immediately perform actions, and the setInterval just does update-redraw. This is much easier, but it's wrong. I have no control currently over blocking user actions, and animation has to all happen at once.

The trick I used is to keep a "frame" counter, and that chooses animation frames, moves floating text and the tracers of missiles and fireballs. Next turn invoked by user event just wipes those out. An amusing side-effect is missiles vaporize corpses as they fly past.

Switching to a correct run loop isn't super hard, but does require changes to all my timing and animation hacks, so at this point it's not worth it.

JS: Fonts -1

I used Oryx's "simplex" font, which looks fine and bitmappy in web pages, but in Canvas it gets antialiased, and I wasn't able to make it stop. So I have to make all fonts slightly larger than I'd like, and they're still kind of blurry.

The smarter way would be to use a bitmap font, and a very simple bitmap text renderer. That's what I do in Perilar: Dark Weaver, with an ATASCII-inspired fantasy font. But in 7 days I didn't have time to write and test that.

That's on a "maybe later" list for 1.0. Alternately, I could figure out how to get them antialiased in Canvas?

JS: Minimizer -1

I used to use yuicompressor, which renames variables and aggressively minimizes your code into unreadable but very compact line noise. But with the long-drawn-out death of Yahoo!, that hasn't been updated in a decade, and it doesn't handle ES2020.

I could probably have run everything thru Babel and then yuicompressor, but it's time to move on? So I just used Crockford's jsmin which only removes whitespace. It might be a good time to look into compiling JS to WASM binaries.

My build.zsh script:

#!/bin/zsh
rm -rf portalworlds*.zip
rm -rf build
mkdir -p build
mkdir -p build/js

# *.js -> .min.js
for f in js/*; do
    n=`basename $f .js`
    perl -ne 's/const DEBUG = true;/const DEBUG = false;/; s/^\s*DLOG.*$//; print;' $f >build/$f
    jsmin-crockford <build/$f >build/js/$n.min.js "Copyright (c) 2020 by Mark Damon Hughes. All Rights Reserved."
    rm build/$f
done

# index.html
perl -ne "s/\.js'/.min.js'/; print;" index.html >build/index.html

cp -R favicon.* style.css i ttf build
cd build
zname=portalworlds-$(date "+%Y%m%d_%H%M%S").zip
zip -r9Xq ../$zname * -x "*.DS_Store"
cd ..
echo $zname

Game: Combat Design +1

So I knew I wanted combat to be very swingy (allowing anything from instant death to instant kill), but not have levels or many stats; I didn't have time or inclination to make another detailed RPG!

Instead, combat works by adding your current strength and enemy's current strength, rolling 2 dice in half that range, so central results are more likely, but it's easy to get results at either far end. Then apply the difference from your strength as damage to you or the monster, depending on which side of the line it's on:

const roll = Math.floor(dice(2, this.strength + mob.strength)/2);
if (roll <= this.strength) {
    const dmg = Math.floor( (this.strength - roll) * (100 + this.getDamage()) / 100 );
    mob.takeDamage(dmg, true);
} else {
    const dmg = Math.floor( (roll - this.strength) * (100 - this.getDefense()) / 100 );
    message(mob.toString()+" hits you for "+dmg+" damage");
    this.takeDamage(dmg);
}

Kind of ridiculously simple, but it makes for symmetric combat, so player-attacks-monster and monster-attacks-player have the same results. It's not practical for a tabletop game, but this is a really fun mechanic in a computer game.

Damage and Defense from gear just modify the result by percentiles, they don't affect attack roll at all. If your strength + defense % is less than the monster's strength, you can be one-hit-killed, and vice versa.

Experience adds to current strength, and increases base strength if you're near max; you need to finish fights against slightly superior foes unharmed to grind up strength.

Game: Spells +1/-1

I knew I didn't have time for a lot of magic, and I never feel like I'm finished with magic systems anyway. So here I just picked 4 types: AOE damage, AOE control, escape, and heal; or as the game knows them, Fireball, Sleep, Invisible, and Heal. Then rather than have MP and worry about regeneration, I just give you "base" spells in each based on class, and you can pick up scrolls to add new points to current spell total. When you heal with mana potions or going thru a portal, you get back your base spells.

Last three were trivial: Sleep just searches a radius and has a die(100) > strength chance to give humanoid or animal targets a sleep condition for 2d4 turns; if they're sleeping, they skip the turn. Invis just sets an invis condition for 2d4 turns; mobs ignore you if you have that condition. Couple places like melee and taking damage I manually clear the Invis and Sleep conditions. Heal just heals 50% of base strength, woo.

Even with just 4, doing Fireball turned out to be quite challenging (the Application Structure problem); how do I show it go across the board, maybe have a turn or two delay, then explode? Well, it does it by moving 4 steps every turn, and only having the fake animation for a tracer of where it's been.

Game: Permadeath +0

Permadeath happened more by lack of save state than any intention. I much prefer to have save slots, and then you can choose to save your game and reload last or earlier, or you can play hardcore and never reload. It puts the moral burden of choosing permadeath on the player, not the developer.

But JS localStorage doesn't really have room to stash a huge amount of data. In Reaper's Crypt I had a very aggressive compression for the map (which looks like a giant grid of tiles but it's really about 64 rooms per level), and it's still problematic, sometimes maps just can't be saved.

I could save the character as of last portal you entered, and regenerate maps. But that encourages "stair-scumming", which I do have a problem with. So instead this is very arcade-like, you just play until you die, then insert another quarter.

I do plan to add a scoreboard, both local and maybe server-based for a copy hosted on mysticdungeon.club.

Personal: Shipping is Awesome +1

Actually finishing something and making it public without a decade-long process is amazing. It's not perfect, and I know nothing is perfect, I should just ship things, but I can't normally do that. Having a hard deadline and meeting it was the best feeling.

Personal: Drone-Slack Balance -1

I was desperately exhausted afterwards. Bone tired all day Sunday, and I'm still feeling it on Monday. 7 days in a row, even with a couple shorter days, is about 3 or 4 days too many.

I'm nocturnal. I like to wake up ideally just before midnight (but sometimes backslide to evening if Real Life interferes), eat and coffee, then work until dawn, walk the dog at sunrise, maybe get my own walk in (tiny dog cannot keep up for a mile+), then finish up and goof off until early afternoon when I can sleep. The schedule weirds out some daywalkers, but it's quieter and more compatible with "morning people" (ugh) than waking in afternoon and sleeping at dawn, which I did for decades.

But normally that work is 3 days of code, 1-2 days of writing or art, 2-3 days of just playing videogames or going Outside, doing Real Life AFK stuff.

Drones who can work all the time frighten me, they're basically Terminators. People who Slack all the time frighten me, they're a waste of precious oxygen & water, and I may foolishly try to rely on these people and get nothing. You need to be in the middle area.

Scheme-Test-Unit

Moving my library from Chicken to Chez (when I moved it from Chez to Chicken originally, it was much smaller and I only used a few asserts to test), I discovered that:

  1. Chicken's test egg is pretty nice but non-standard.
  2. SRFI-64 (from Thunderchez ) is OK as far as it goes, but has an inadequate test runner (the default just lists PASS/FAIL with no explanation for each test, and has one total). Ridiculous when you have dozens or hundreds of tests.
  3. There's no good alternative. There's a SchemeUnit which is for PLT Scheme née Racket, and a couple others which aren't SRFI-64 and aren't for Chez.

So I ended up writing my own:

Here's how it works:

#!/usr/bin/env scheme-script
;; example-test.ss

(import (chezscheme)
    (srfi s64 testing) ;; thunderchez
    (scheme-test-unit)
)

(define (all-tests)
(test-group "All Tests"

(test-group "Math"
    (test-equal "add" 4 (+ 2 3))
)

(test-group "Strings"
    (test-equal "append" "foobar" (string-append "foo" "bar"))
)

) ;; test group "All Tests"
) ;; all-tests

(define (main argv)
    (scheme-test-configure argv)
    (all-tests)
)

(main (command-line-arguments))

----
% chmod 755 example-test.ss
% ./example-test.ss --help
Usage: scheme-test-configure [-v|--verbose|-q|--quiet|-o FILENAME|--output FILENAME]
% ./example-test.ss
*** All Tests START
*** Math START
- add [(+ 2 3)] expected <<<5>>> but got <<<4>>>
*** Math END, PASS: 0 / FAIL: 1
*** Strings START
+ append [(string-append foo bar)]
*** Strings END, PASS: 1 / FAIL: 0
*** All Tests END, PASS: 0 / FAIL: 0
FINAL PASS: 1 / FAIL: 1

My own test cases come out:

% ./marklib-test.ss -q
*** Control END, PASS: 4 / FAIL: 0
*** Logic END, PASS: 12 / FAIL: 0
*** Math END, PASS: 18 / FAIL: 0
*** Strings END, PASS: 29 / FAIL: 0
*** Any END, PASS: 31 / FAIL: 0
*** Hashtable END, PASS: 9 / FAIL: 0
*** List END, PASS: 6 / FAIL: 0
*** Maybe END, PASS: 6 / FAIL: 0
*** Stack END, PASS: 10 / FAIL: 0
*** Vector END, PASS: 8 / FAIL: 0
*** Data Structures END, PASS: 0 / FAIL: 0
*** Dice END, PASS: 7 / FAIL: 0
*** All Tests END, PASS: 0 / FAIL: 0
FINAL PASS: 140 / FAIL: 0

Green Window Means Thunderchez is Working

This weekend, I wanted to have dynamic access to SDL for some interactive graphing, and again ran into the problem from More Fun and Swearing with Scheme. With more experienced eyes, I read the Chez Scheme User's Guide and discovered everything I'd done wrong a year+ ago.

First, install Thunderchez somewhere, I just put it in ~/Code/CodeChezScheme

In particular, read ffi-utils.sls which provides somewhat easier wrapping of binary flags; thunderchez is a very low-level port.

Second, install SDL2, I used port install libsdl2 and that puts it in /opt/local/lib, but in production I'll use a local dir. Read the SDL2 API for reference.

Took me a few hours to translate the basic functions I needed, and now I have:

sdltest.ss:

#!/usr/bin/env scheme-script
;; sdltest.ss
;; Copyright © 2020 by Mark Damon Hughes. All Rights Reserved.

(import (chezscheme)
    (sdl2)  ;; thunderchez
)

;; Set this to your location. I used `port install libsdl2`, YMMV.
;; In production, I'll use a local dir with libs.
(load-shared-object "/opt/local/lib/libSDL2.dylib")

(define kWindowWidth 320)
(define kWindowHeight 240)

(define gWindow )
(define gRender )
(define gState 'quit)

(define (gr-init title x y w h)
    (sdl-set-main-ready)
    (sdl-init sdl-initialization-everything)
    (set! gWindow (sdl-create-window title x y w h (sdl-window-flags 'shown 'allow-highdpi)) )
    (set! gRender (sdl-create-renderer gWindow -1 (sdl-renderer-flags 'accelerated)) )
    (sdl-show-window gWindow)
    (sdl-raise-window gWindow)
)

(define (gr-event)
    (make-ftype-pointer sdl-event-t (foreign-alloc (ftype-sizeof sdl-event-t)))
)

(define (gr-rect x y w h)
    (let [ (rect (make-ftype-pointer sdl-rect-t (foreign-alloc (ftype-sizeof sdl-rect-t)))) ]
        (ftype-set! sdl-rect-t (x) rect x)
        (ftype-set! sdl-rect-t (y) rect y)
        (ftype-set! sdl-rect-t (w) rect w)
        (ftype-set! sdl-rect-t (h) rect h)
        rect
))

(define (gr-mainloop)
    (let [ (event (gr-event)) ]
        (let mainloop []
            ;; event
            (sdl-poll-event event)
            (let [ (evt-type (ftype-ref sdl-event-t (type) event)) ]
                (cond
                    [(eqv? evt-type (sdl-event-type 'quit))  (set! gState 'quit)]
                    [(eq? gState 'main)  (main-event evt-type event) ]
                ))
            ;; TODO: loop polling until no event left
            ;; update
            (case gState
                [(main)  (main-update) ]
            )
            ;; render
            (sdl-render-clear gRender)
            (case gState
                [(main)  (main-render) ]
            )
            (sdl-render-present gRender)
            ;; loop
            ;; TODO: wait timer, currently burns CPU
            (unless (eq? gState 'quit) (mainloop))
    ))
    (gr-shutdown)
)

(define (gr-shutdown)
    (sdl-quit)
)

(define (main-event evt-type event)
     ;; no event handling yet
)

(define (main-update)
     ;; no updates yet
)

(define (main-render)
    (sdl-set-render-draw-color gRender 0 255 0 255) ;; green
    (sdl-render-fill-rect gRender (gr-rect 0 0 kWindowWidth kWindowHeight))
)

(define (main argv)
    (gr-init "Hello, Thunderchez!" 100 100 kWindowWidth kWindowHeight)
    (set! gState 'main)
    (gr-mainloop)
)

(main (command-line-arguments))

To start it I just need:

% CHEZSCHEMELIBDIRS=thunderchez-trunk: ./sdltest.ss

Green window means it's working! And it can be closed with the window close button or Cmd-Q, but menu or other events don't work yet.

So… to port my graphics library from Chicken to Chez is probably a week's work, maybe less—I only fill rects, blit images, play sounds, and read a few events. My sound support on Chicken is minimal because the maintainer didn't bother to port the audio API, I had to do that myself.

Rehosting my game on Chez is an unknowably difficult debugging problem, but I code in a way that's usually portable. Large chunks of my large marklib library exist only to work around the non-portable parts and missing features of R5RS.

But the advantages are:

  1. Chez's R6RS is a better language than Chicken's R5RS++/R7RS-not-really.
  2. Chez is much faster, sometimes just 10-25%, sometimes hundreds of times, even compiled.
  3. Chez's JIT compiler is fast and invisible. Working on Chez is instant gratification. Chicken has a 30-second compile before I see anything, which has been impossibly frustrating.

At the very least, if I get my graphics library ported I can do the graphing I wanted last weekend.