- Permanent, by Joy Division: Compilation's not on iTunes or Youtubes. This has the best version of "Love Will Tear Us Apart". I hate the remasters on iTunes, they sound like kiddie music today.
- Mask, by Bauhaus
- Press the Eject and Give Me the Tape, by Bauhaus
- Seventeen Seconds, by The Cure: Sorry, they don't have a non-deluxe version, the extra stuff is probably not worth listening to.
- Pornography, by The Cure
- First and Last and Always, by The Sisters of Mercy
- Some Girls Wander by Mistake, by The Sisters of Mercy
- Honey's Dead, by The Jesus and Mary Chain: Again, stop when the B-sides start.
Blog
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.
Bundle of Hoody-Hoo
And a bunch of the side comics. Some of the best/only comics about actual tabletop gaming, as opposed to vaguely in genre superhero comics or a few webcomics.
7 Days of Roguelike Development to Die
Well, I'm exhausted, but I have a working #7DRL shipped. I may come back tonight and do some more, otherwise a post-7DRL update will fix the missing bits.
- PortalWorlds on itch.io
- 7DRL submission: Rate my submission!
Play, and if you liked it, shower me in gold, or at least rattle some change at my tip jar. If you have other feedback, comment but be kind, I'm so very tired.
Archive of my dev diary/comments:
- Day 1 (Sun, 2020-03-01)
Getting started now. Will be working until next Saturday!
Tiling, basic animation, and keyboard input.
Minor annoyance, discovered Oryx’s “wee fantasy” figures, which are otherwise quite excellent, are left-handed. Flipping the images gets them all fixed except right-facing still has a shield in left hand. I don’t mind a few sinister figures, but that’s too much to ask of heroes not named “Link”.
Basic random character and stat display.
-
Day 2 (Mon, 2020-03-02)
Didn’t get a lot of uninterrupted time today, but made useful progress finally:
Items now exist, can now be got (auto when you move; I might add a Get command), put down, readied/removed, and used.
-
Day 3 (Tues, 2020-03-03)
Very productive day. Mana recovery, added a Get command, visibility calculations so walls occlude vision.
Changed the way I write objects and monsters to use prototype objects (an object system on top of an object system, but it’s more convenient).
Monsters (well, just Goblins today) are placed and can be fought. They don’t move yet.
-
Day 4 (Wed, 2020-03-04)
Very little free time today, but I got a monster list completed, with a lot of tricky multi-level monsters, and I can spray them all over the junk level. Got some design notes for spells and missiles, and how I’ll split up the levels tomorrow. Then try some kind of AI once I have a place to navigate.
-
Day 5 (Thur, 2020-03-05)
Floating damage numbers & effects. Basic mob AI, no pathfinding yet. Sleep, Invisible, Heal spells.
Running low on time, so much stuff left to do and I just did some detail work today, didn’t even get missiles in (and Fireballs, which are a slow case of missiles that explode). Levels, absolutely first thing tomorrow.
-
Day 6 (Fri, 2020-03-06)
DON’T PANIC. Well, panic a little. Got door opening (but not closing, needs a whole new command), Portals (AT LAST), and 6 level generators + tutorial level. They’re very simplistic levels, and it’s possible to be trapped in some, but usually works?
Tomorrow I should really get missiles flying, trapped levels, maybe an overview map are optional. Strength/“experience” gain is a little harsh right now, but balance is low on my priorities.
Found an absolutely horrible off-by-1 math bug in a library I’ve been using for years. Programming: It’s always harder than you think.
-
Day 7 (Sat, 2020-03-07)
Exhausted, I don’t ever work this many days in a row anymore. So I got the overview map done, and rebalanced it a bit more sanely. It is playable and a fun challenge until you get curb-stomped by wights or stuck in a dead-end world.
I have until tonight (Saturday) at midnight, so if I feel up to it later I’ll get to some of my TODO list.
Playable version is up now, let me know what you think!
-
Day 7, Later
Got back to adding missiles and fireballs! Done! Ship it!
(Levels still kind of suck, I’ll improve those later. But the gameplay should be finished.)
Last Stand of the California Browncoats
Amusing that the start of the apocalypse is now in the past (the main books are set 20 years later), but I really want to point back at these, and in particular the prequel story, when people could have stayed home, avoided the plague, but instead wanted to go cosplay Firefly for a weekend.
Feed's a great epidemiology story, and a fairly good vlogging/social media (they say "blog" but only appear to do video) story, with zombies as almost a totally irrelevant side effect.
The main two caveats I have are technical: The blood testing system is basically what Theranos was pushing, and it probably can't be made to work, the sample sizes are too small. And later has a tiresomely impossible (at least with their tech) medical technology. There's a creepy personal thing, too, but you know, people gotta get their bone on with someone/thing.
I think I haven't read a bunch of the short stories, only the above and "How Green This Land, How Blue This Sea", which is a little silly because it's about Strines vs zombie kangaroos.
"Mira Grant" has a few other horror books, the Parasite series is very zombie-like as well; she's grim and serious but has just a little genre fanservice goofiness to lighten the mood. But the author under her real name, Seanan McGuire, also writes urban fantasy books, and they're dire. Easily some of the trashiest "I'm Wonder Woman and I wanna fuck a monster" books since Laurell K. Hamilton's Anita Blake went off the rails straight into bondage-mutilation-porn-land.
Lust for Tuesday Music
The Masque of the Red Death
- The Works of Edgar Allan Poe, The Raven Edition, by Edgar Allan Poe: Click thru the table of contents to "The Masque of the Red Death"
- or, Peter Lamb reads the story, if you are unable to read more than 140 characters at a time
Just a little light topical reading. By all means seal up your castle and debauch while the Red Death spreads outside…
Remix Sunday Music
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:
- Chicken's
test
egg is pretty nice but non-standard. - 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.
- 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:
- scheme-test-unit: BSD license, do what thou wilt.
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
Programming on Your Phone
Pythonista lets you use your pocket UNIX workstation as a workstation. I use Pythonista, if not every day, very heavily on the days I use it. As always it's crippling of Apple that there's no upgrade pricing, so I can't give him more money every year that I keep using it. The new keyboard module is an interesting script launcher, but I already wrap a bunch of utilities in a main menu program.
There should really be more of these mobile programming environments. In the early days, Apple severely restricted you from shipping one; you could kind of cheat with JavaScript, and a few games snuck in some bytecode interpreters, but scripting was right out. They loosened up eventually, but are still dicks about you saving code anywhere it could be shared, so for example I have to keep my Pythonista stuff in iCloud, not DropBox where it'd make more sense.
- Panic's Coda and Coda for iOS (née "Code Editor" WTF) is the only other one that's really functional; I've built real web sites out of it, but I mostly use it for ssh. Sweet baby Cthulhu, I hate Panic's crooked-text "designer" sites, I hit Reader view on those instantly. Designers shouldn't be allowed access to CSS or JS.
- Hotpaw BASIC still works (as does his Chipmunk BASIC on the Mac), but hasn't been updated in 2 years. Not that I want to program in BASIC, but it's better than no programming at all.
- The iPad used to have a very nice "BASIC!" (with a structured BASIC and a bunch of system functionality), and a very limited "iSkeme" (scheme interpreter, R5RS-ish? with nothing but text I/O), but they were killed in the 64-bit-pocalypse. Update 2020-09: miSoft Basic! has been updated. Searching for this is utterly impossible!
- Workflow (née Apple Shortcuts) is great for putting a few tasks in a row but you'd go insane trying to write anything complex from drag-and-drop clicky boxes.
- Apple's Swift Playgrounds on iPad is a tutorial, not really usable for applications AIUI.
- There's a bunch of "kids learn to code!" apps that are mostly ripoffs charging $60/year to play robot tanks. Do not buy anything like this.
I dunno if the 'droids have anything comparable, I'm sure they can root their phone and try to use vi in a busybox shell, but that's not a reasonable work environment for a thumb-sized on-screen keyboard.