Posting some links here for future reference:
Wikipedia article:
https://en.wikipedia.org/wiki/Akalabeth:_World_of_Doom
MobyGames link:
https://www.mobygames.com/game/1256/akalabeth-world-of-doom/
1980 release of Akalabeth (full BASIC source is in the comments):
https://archive.org/details/a2_Akalabeth_1980_California_Pacific_Computer_a
1983 (?) rerelease (has pre-game instructions that were probably printed in the first edition)
https://archive.org/details/a2_Akalabeth_1980_California_Pacific_Computer
source code for the second disk:
https://gist.github.com/jtauber/9863811
Commented tour of the disk contents:
https://jtauber.github.io/game-hacking/akalabeth/
Port to GW-Basic and other platforms:
https://nanochess.org/akalabeth.html
Cleaned up source code (broke up one-liners, added label names, comments):
https://qb64phoenix.com/forum/showthread.php?tid=2570
Python port (seems quite faithful):
https://gitlab.com/promi1/pyakalabeth/-/tree/main/src/akalabeth
StrategyWiki guide
https://strategywiki.org/wiki/Akalabeth:_World_of_Doom
Part of me wants to make a consolized version of this for the Sega Master System, but another part of me just wants all these goshdang tabs closed. This non-public thread is my brain's compromise.
			
			
			
				oh, and i can't forget the video that sparked my recent interest:
			 
			
			
				There's a really funny coding mishap in in the "draw enemy" subroutine.
On line 270 there's this jump table:
270  ON MC GOTO 300,310,320,330,340,350,360,370,380,390
Depending on the value of "MC", it jumps to one of those target lines. It's basically equivalent to a switch statement in C.
Each of the jump destinations is simply a bunch of HPLOT statements to draw each monster (as a buncha vector lines). They all end the same way:
GOTO 490
Line 490 just closes up the drawing function then returns.
HOWEVER, one of the enemies does this instead:
387  GOTO 3087
Hmmm... what's so special about this code that it has to jump there... ?
Oh, it's just some more plotting instructions, ending with the standard "GOTO 490"
Now, the thing about many classic BASIC dialects is that you didn't type in programs as a whole text file. You would type them in line-by-line, and the interpreter would add each line to the program. However, the placement of the lines in the actual program would depend on the line numbers you manually assigned to the lines when typing them in.
Thus, say if you typed these lines into the interpreter:
10 PRINT "BEANS"
20 PRINT "TOAST"
15 PRINT "ON"
The actual BASIC code would end up being:
10 PRINT "BEANS"
15 PRINT "ON"
20 PRINT "TOAST"
With this in mind, there are two possible explanations for the "GOTO 3087" (one is funny and the other is practical):
(a) Lord British meant to type this line:
387 HPLOT [a buncha code]
but he fat-fingered that line as he was typing it in and wrote "3087 HPLOT", putting that line thousands of lines ahead. Rather than retype that (very long) line, he rolled with it and patched the code together with a couple of GOTOs.
(b) Lord British was afraid that he would run out of lines between line 380 and the next enemy at line 390, so towards the end of writing that branch he shuffled some code off to regions unknown. The problem with this theory is that the code would have fit regardless, and there's a high likelihood that he would have written his code on paper first.
I just thought this was neat.
			
			
			
				Line 90 appears to be unused:
90  FOR X = 0 TO 9: FOR Y = 0 TO 5: PRINT LD%(X,Y);" ";: NEXT : PRINT : NEXT : GET Q$
Cleaned up, it looks like this:
FOR X = 0 TO 9
   FOR Y = 0 TO 5
      PRINT LD%(X,Y);" ";
   NEXT
   PRINT
NEXT
GET Q$
I need to get an Apple II emulator working, but this appears to be a debug printout for a perspective-related table, followed by an empty wait for a keypress.
			
			
			
				what are you waiting for? go get Q$
			
			
			
				you gotta keep in mind this was programmed in 1979 -- there's no telling how expensive it is to GET Q$ in today's economy 
			
			
			
				This bit is really funny:
; Subroutine - Render Overworld
100 HGR
    FOR Y =  - 1 TO 1
        FOR X =  - 1 TO 1
105         HPLOT 138,75 TO 142,75: HPLOT 140,73 TO 140,77
110-170     [draw each of the overworld tiles]
190     NEXT
    NEXT
    RETURN 
On the overworld, the player is represented as an X in the middle of the screen, as plotted by line 105. Since that HPLOT command is in the middle of the loop, the player ends up being drawn 9 times (just to be sure).
Unfortunately, this idiosyncrasy of the original code is not reflected in pyakalabeth (https://gitlab.com/promi1/pyakalabeth/-/blob/main/src/akalabeth/world_renderer.py) :pensive: 
			
			
			
				I haven't written any code yet, but I am having three different thoughts (from simplest to most complex):
(a) Port the game to CVBasic (a compiled Basic language for several 8-bit platforms)
https://github.com/nanochess/CVBasic
(b) Port the game to C using devkitSMS:
https://github.com/sverx/devkitSMS
(c) Rewrite the whole game in Z80 assembly.
Much to consider.
			
			
			
				The ASCII "bell" character ␇ is used 28 times in the code to Akalabeth, such as in this instance:
7950  PRINT
      PRINT "...CALL CALIFORNIA PACIFIC COMPUTER"
      PRINT "AT (415)-569-9126 TO REPORT THIS"
      PRINT "AMAZING FEAT!␇␇␇␇␇"
It makes the computer make a little beep.
More information: https://en.wikipedia.org/wiki/Bell_character
On a completely different note, this project has made me learn that many early BASIC dialects had floating point math support, even back in the 70s. Wild stuff (that would also explain some of the performance issues...)
			
			
			
				been doing a clean-up of the BASIC code on a private repo to gain my own understanding of the code
the most frudged up thing i've learned is that for the dungeon map array, the monster ID occupying a given square is encoded in the hundredths and thousandths place
			
			
			
				aw yee things r happening (maybe)
			
			
			
				gonna go with CVBasic for now. Its differences from classic Applesoft BASIC (e.g. no line numbers, no inter-procedure GOTOs) will be helpful in properly restructuring the program.
https://github.com/nanochess/CVBasic
			
			
			
				Rudiments of world generation.
Note that CVBasic does not have floating point types, nor an exponentiation operator, so instead of going "INT(RND(1)^5 * 4.5)" I'm just going "RANDOM(5)", which produces a more even distribution (rather than the bottom-heavy distribution of the original). I think I'm gonna have to do something like "RANDOM(256)" followed by a bunch of IF-ELSEs to produce something resembling the original game (read: there are too many towns and dungeons (3s and 4s)).
			
			
			
				Anyhow, the most unhinged thing about CVBasic is that it does not support multidimensional arrays --- it's gonna drive me nuts.
			
			
			
				okay this looks a bit more visible
			
			
			
				Quote from: RT-55J on Sat, 2025 - 10 - 04, 11:21 PMINT(RND(1)^5 * 4.5)
Okay. I did some spreadsheet tomfoolery and approximated the distribution of integers from that function with this daisy-chained IF-ELSE series:
#A = RANDOM ' Random 16-bit number
IF #A < 42231 THEN
    TerrainMap(X+Y*OVERWORLD_SIZE) = 0
ELSEIF #A < 52608 THEN
    TerrainMap(X+Y*OVERWORLD_SIZE) = 1
ELSEIF #A < 58267 THEN
    TerrainMap(X+Y*OVERWORLD_SIZE) = 2
ELSEIF #A < 62323 THEN
    IF RANDOM(2) = 1 THEN
        TerrainMap(X+Y*OVERWORLD_SIZE) = 3
    ELSE
        TerrainMap(X+Y*OVERWORLD_SIZE) = 0
    END IF
ELSE
    TerrainMap(X+Y*OVERWORLD_SIZE) = 4
END IFThe results look a bit better.
			
 
			
			
				Note: I just profiled the performance of the world generator. My version (compiled with CVBasic and not using floating point arithmetic) takes 12 frames. The original version takes around, I dunno, 33-ish seconds. That's like 150 times faster.
			
			
			
				everybody movement
			
			
			
				this is what the people crave
			
			
			
				The people crave to pass the mountains.
			
			
			
				Anyhow, I was trying to figure out how to print a string at a pointer, and learned a distressing truth:
https://github.com/nanochess/CVBasic/blob/master/examples/strings.bas
' This is a macro, CVBasic doesn't process strings
			
			
			
				while:wend spotted
			
			
			
				Okay I got a proof-of-concept working for the proper zoomed-in overworld view (TODO: render the player).
This feels dizzyingly fast, for the record.
			
			
			
				I swear, either I'm not understanding how CVBasic handles strings and pointers, or the language is genuinely hampered in those respects. (I swear both C and assembly would both be easier to handle here.)
Maybe I should ask for help idk.
			
			
			
				is cvbasic the only basic fit for the job? idk how to help  :pensive: 
			
			
			
				I'm not fond of how it works, but I cribbed the "get_string(where, num)" macro from the strings.bas and it suits my purposes well enough.
The majority of the text here is stored in "arrays" of strings (that is to say, they are a contiguous set of strings, separated by null terminators (the get_string macro iterates from the beginning of the list it's given every time to get the desired string (rather than do something reasonable like dereferencing a pointer or something (I cannot stress how much less this would bother me if I was programming in C or ASM here)))).
tl;dr I got it working how I want, from a "replacing lines of BASIC code" and a player-view perspective. The issue is that I do not like how the solution works between those two points (many such cases).
			
			
			
				tonight i put a player sprite on the overworld
also here's a gif to give you an idea of what i meant by "dizzyingly fast"
			
			
			
				The shop is pretty functional at this point. That cursor there should be pretty generalizable too!
The main issue is that CVBasic doesn't let me easily tap the rising edge of inputs, so I have to put in some ugly delays to prevent it from being unusably fast. I need to find a more rigorous approach here.
The stats here are simply RANDOM(21)+4. The original code is SQR(RND(1))*21+4 to slightly nudge the distribution towards the upper end of the range. I'm not sure what the best approach to replicate this would be, without floating point math...
			
			
			
				Note to self: Get an Apple II emulator set up so I can investigate the dungeon rendering code directly.
			
			
			
				You can roll your stats and select a class.
The first two parts of the this screen (selecting an RNG seed and selecting a difficulty level) are not implemented because I haven't coded a number selecting widget.
			
			
			
				you can now enter towns and the castle on the overworld (no dungeons yet)
there might still be a visual bug when returning to the overworld
			
			
			
				sad news
i fixed that bug
rip to the E SHOP, you were too good for this WORLD OF DOOM
			
			
			
				working on dungeon generation
numbers:
game_012.png
just walls and ladders:
game_013.png
objects:
game_016.png
still need to place monsters
			
			
			
				Curiously, the monster placement code has two different formulas for calculating a monsters health.
The first one is simpler, and is applied to the monster slot regardless of whether or not it spawns:
MonsterHealth(X) = X + 3 + DungeonFloor
If a monster is spawned, then the previous formula is overwritten with the result of this formula:
MonsterHealth(X) = X * 2 + DungeonFloor * 2 * PlayLevel
curious
			
			
			
				Annoying edge case in CVBasic:
    X1 = SGN(PlayerX - MonsterX(MM))
    Y1 = SGN(PlayerY - MonsterY(MM))X1 and Y1 are signed, but unless I declare that PlayerX and PlayerY are also signed, then the SGN() function will never return a negative number.
Thus, all enemies would congregate to the lower right.
			
			
			
				this game is dangerously close to becoming fully functional
unfortunately, that is very different from being remotely presentable
			
			
			
				Combat is 90% working, from the looks of it.
game_018.png
			
			
			
				KILLLD BAD??S
game_020.png
			
			
			
				I found a very funny bug in the original on line 1667:
1667 LK = LK +  INT (MN * IN / 2): IF MN = TASK THEN TASK =  - TASK
specifically, we're looking at this part here:
IF Monster = Task THEN Task = -Task
The ostensible structure of the game is that Lord British gives you a quest (task) to slay a specific monster. You go and do that, return to him, get another task, rinse and repeat until you've slain a Balrog. The game marks a task as complete by setting the number to be negative.
This all seems well and good, and the code above does that task perfectly well.
The issue is that that line of code is called any time you hit and enemy, rather than when you defeat an enemy.
Thus, per the game's code, you could complete your final quest by tapping a Balrog's shoulder with an arrow, high-tailing it back out of the dungeon, and then telling Lord British about your stunning feat.
			
			
			
				wait, wouldn't it alternate between positive and negative every hit then? err
			
			
			
				If TASK were negative, then (Monster = Task) would evaluate to false.
			
			
			
				so TASK = -TASK doesn't keep inverting it? just makes it -|task| ?
			
			
			
				A = -A would invert a number from positive to negative to positive, etc.
It's just that all of the Monster IDs are positive, so the conditional check of "Monster = Task" would never evaluate to true after Task is set to negative.
			
			
			
				got it, monsters are positive :hey: 
			
			
			
				TODO: Implement hunger and death.
			
			
			
				thread has been freed from lounge jail :smug: