INTRODUCTION
Item randomiser utilities are well known to aficionados of non-linear console games, being particularly common amongst Zelda and Metroid titles. They aim to breathe new life into well-worn classics by randomising the positions of items within the game, so a new and novel route needs to be found by the player in order to complete it.

Basic randomisers simply shuffle item locations brainlessly, which can lead to deadlocked situations in which the player cannot know for sure if a particular layout can be solved. The most simple example is when a key ends up stuck behind a door that requires that same key to open it. More advanced randomisers (including this one) incorporate solvers, which are able to check whether or not a particular permutation may be completed.

Originally written as a proof-of-concept in Ruby, this Citadel randomiser has been ported to BBC BASIC so it can be run directly on original hardware or from within an emulator.

SEEDS
Each supported permutation is represented by a single integer or "seed" in the range 1 to 999,999,999. The randomiser can either generate a solvable seed automatically, or the user may supply his or her own. Please note that since permutations are generated using BASIC's RND() keyword, seeds may not be compatible between different versions of BBC BASIC.

QUIRKS
Two items in the game are not randomised and are always available from their original locations. The first is the crystal obtained from the sarcophagus in The Pyramid, since this is not an item that appears on an item pad but rather is traded directly into the player's inventory. The second unrandomised item is the figurine on the alien planet. Once the Star Port has been destroyed, it is impossible to return to the alien planet to pick up any items that may have been left there. Choose your route wisely!

There is a "warp" in this game which involves taking a trampoline to The Ocean on the east side of The Temple (where the Egyptian statuette is usually found), and using it to climb up into the game's title screen. By falling off the right side of the title screen and down one more screen past The Ocean, then holding left as you fall further, it is possible to land on the Central Tower battlements and skip solving the puzzle that normally would require the bucket. The solver is aware of this trick, and may be configured to either accept or reject seeds that use this alternative to collecting the bucket. (This glitch may also be used to skip collecting the East Tower key, but currently the solver does not acknowledge this).

No special status is granted to the three secret rooms that normally contain the crowns. The player is expected to know where these are, and any item may appear in them. Additionally, players do not win £100 for collecting the three crowns.

Not all seeds will yield the maximum 99 points; however it should be possible to destroy the Star Port and prevent the alien invasion on any verified seed.

LIMITATIONS
This software was tested with the original BBC Micro Model B disc version Stairway To Hell disc image of Citadel (disc image "Citadel.ssd" having an MD5 of c03f242992408838157844070f08a6a4). Electron and BBC tape versions are not supported at this time, and it has not been tested on the "Play It Again, Sam" BBC version, either. At this time it has only been tried under emulation, and not on vintage hardware.

At present, this tool works by patching the CITAM file on the disc for each new seed, and as such requires write access to the disc. *** DO NOT USE IT ON AN ORIGINAL COPY OF THE GAME ***. Emulators will need to be configured to allow writes to the disc image. The software does have a facility for restoring the CITAM file to its original state (answer "N" to the "Randomise?" question). In-memory patching of the binary would be a neater solution, but this software is not currently capable of it.

The solver is written in BASIC, uses poor quality algorithms and is slow. On a standard BBC B, it takes several seconds to check each possible seed for solvability, and so in unlucky cases it may take a minute or two for it to find a suitable seed.

DISC IMAGE
This disc image contains a copy of the game with the randomiser utility added to the root directory:

Type CHAIN "RANDO" to start the randomiser. Once it has modified the CITAM file, you can CHAIN "CITADEL" to launch the game as normal. To re-randomise with a new seed or restore CITAM to its original state, simply press BREAK and then CHAIN "RANDO" again.

I altered $.RANDO so that it stores the calculated randomised data at &A00, and I added a short machine-code routine at &A28 which moves the data to &1780 after $.CITAM has been loaded by $.CITAL, which I modified by adding JSR &A28 at &547F.

I altered $.RANDO so that it stores the calculated randomised data at &A00, and I added a short machine-code routine at &A28 which moves the data to &1780 once $.CITAM has been loaded by $.CITAL, which I modified by adding JSR &A28 at &547F.

Now I feel embarrassed that I've made you do this with such a poorly optimised shuffler and solver.

There are two things that make it so slow:

a) it uses a brain-dead shuffling algorithm. I've never actually implemented the Knuth one myself since I've always been using languages that provide it as part of the standard library. The only time I've ever implemented my own shuffling algorithm was as a child, in (you guessed it) BBC BASIC, and that's the algorithm that appears here. It goes through the source slots sequentially; for each source slot it picks a destination slot at random. If the slot is already occupied, it picks another one at random, and continues in that fashion until it fills them all. It doesn't take a genius to work out that this has a pretty horrendous time order. I even knew it as a kid, but didn't understand how to improve it.

b) it uses a brain-dead lookup algorithm, FNibr(). The game employs a table with one slot per item pad, and in each slot it stores the room ID in which that item is currently placed. For the solver, the reverse is also needed -- you need to know which items are in which rooms, so a way of getting the item ID indexed by room ID is needed, rather than the other way around. The solver does this the same way as the game does, simply by linearly scanning the table for the current room ID each time and then counting how many slots it took to find it. While the item IDs have nice contiguous IDs, the IDs of rooms with item pads don't. My Ruby solver used a hashtable alongside a linear array to do lookups in either direction, but hashes are obviously not available in BBC BASIC. The way to do this would have been just to define a mostly empty 255-slot array so that items could be looked up instantly. I think I probably avoided this because I was worried about memory usage. In fact all the arrays in the program use 4-byte integer variables where single bytes would have sufficed, but I don't think there's a particularly clean way of doing this in BBC BASIC.

Anyway, I've already played through three randomiser seeds tonight, but I will test this now. Thanks.

Diminished wrote:The way to do this would have been just to define a mostly empty 255-slot array so that items could be looked up instantly. I think I probably avoided this because I was worried about memory usage. In fact all the arrays in the program use 4-byte integer variables where single bytes would have sufficed, but I don't think there's a particularly clean way of doing this in BBC BASIC.

I've just tested your 0.2 version of this in-browser, and beat a seed with 99 points (and 265 energy remaining. ) So this looks like it works just fine.

There are a few improvements I'd still like to make (improved performance, a configuration option for 100% completion -- and perhaps I should put some of the documentation into the program itself), so at some point I'll hopefully synthesise a 0.3 version that incorporates these. (It may have to wait until I've done my tax return >_>). However, the version you've produced here looks like the current best option (unless you want to play using a cheat loader, in which case the 0.1 on-disc patcher might work out better).

I've had a lot of fun with this; randomisers are really great for prolonging the life of a game you already know like the back of your hand, and I'm happy to apply this historically very Nintendo-centric idea to an Acorn title, particularly since I got it to work entirely on the original hardware. You can't do that on a NES!

Looks like prime real estate to stuff my payload into (lurkio's patch used it too), but I'm intrigued to know why it's there. Oddly, when I fill it with code, at least one of the payload bytes seems to get corrupted by something somehow, but I think I can just branch over the offending section.

Which is actually where this gets interesting for the archivists, because I'm using the version from STH (this one right here).

So if this is the hallmark of removed copy protection, then there seem to be two possibilities:

a) the protection in question was only applied to the tape version, and was removed in the disc version, but this seems unlikely; surely you'd just reassemble the game for the disc version without the NOP bloat?

or, b) the version in the STH archive isn't actually an original, which means that somebody really ought to archive a proper one from its original media.

You can check it yourself by breakpointing at 547F, and then disassembling from 547F when the breakpoint is hit.

(For the record, I do own an original copy of this game, but it's on tape, and therefore not very convenient to work with.)

This looks like a transfer from tape ("Press Play" in the loader), and is a Master-compatible version, which means it contains the patch to move some code to Sideways RAM bank 5. I'm fairly sure that those NOPs are in the released version (I remember also being baffled by them when transferring it to disk). My guess is that whoever did the Master compatibility patch for Superior (it wasn't Michael Jakobsen) just left things in a bit of a mess. I haven't compared it with the original release; maybe there was some basic decryption or something going on there before.

I just took a look at the original disk release. CITAL is a strange bit of code: in place of the NOPs there's a bunch of code which (after loading CITAM), reads the catalog info for CITAM, and then loads it again at its natural location. *INFO for CITAM looks like this:

I'm slightly disappointed that the STH version wasn't the original, since I'm pretty sure that's the one I spent two months disassembling!

I'm baffled as to why CITAM would be reloaded at &8000. I'm not an expert on the machine's internals, but ... isn't that all ROM and I/O up there? *scratches head*

The reason why I'm asking, of course, is that I'm trying to follow game modding best practices with the next (hopefully final) version of the randomiser. 0.1 patches things on-disc and would operate fine on probably any disc version of CITAM, and so can be distributed without needing to make available any of the original game's code -- the downside is that it requires destructive write access to the game disc. lurkio's 0.2 version is better in the sense that it will work with a write-protected disc, which has advantages for situations like jsbeeb, but the downside is that it achieves this using a patched CITAL, so in order to gain its advantage over 0.1 you're pretty much forced to distribute part of the original game code along with the randomiser.

The version I'm working on now is able to patch the game as it loads, so it requires neither a writeable disc nor a modified game. Unfortunately I've developed it against the STH copy, which corresponds to no official release of the game! I did think the "Press Play" thing was fishy.

Well, none of the STH images are originals, because the SSD format can't support protected disks. So all of those images are necessarily versions which have had their protection stripped and put onto a clean DFS format disk image.

The Citadel.ssd file on STH appears to be direct rip from tape of the Master-compatible release. I remember when I did the same thing, transferring the files from tape to disk was absolutely trivial - no tricky protection schemes, just locked tape files. So it's pretty much a 'clean' image.

I also have no idea what's going on with reloading CITAM at &8000 (and particularly that it almost expects it to generate an error) - maybe billcarr has an idea on what they wanted to do there?

To be honest, I don't think it's really a big deal to create a new disk image containing Citadel and your patch. If you take the STH Citadel.ssd as a clean starting point, I reckon it's fine to use that to create a new version with your randomiser built in. STH did that once before with a patch I made for Chuckie Egg which added extra colours. From the user's point of view, it's much easier to just load a new disk image and boot it, and have everything 'just work'.

The original disk isn't formatted in any strange way, just has some tracks with Deleted Data, but it isn't used as a form of protection.
Strangely enough, on the original disk version of Citadel that I imaged as an FSD, the JSR FFDD that would read the catalogue information has been NOP'd out

Rich Talbot-Watkins wrote:Well, none of the STH images are originals, because the SSD format can't support protected disks. So all of those images are necessarily versions which have had their protection stripped and put onto a clean DFS format disk image.

Ah, I didn't realise this.

billcarr2005 wrote:The original disk isn't formatted in any strange way, just has some tracks with Deleted Data, but it isn't used as a form of protection.
Strangely enough, on the original disk version of Citadel that I imaged as an FSD, the JSR FFDD that would read the catalogue information has been NOP'd out

It seems that when the NOP sled in the STH version actually runs, one of those opcodes really becomes an undocumented NOP &04.

Here's the fun conspiracy theory part: Maybe it's a coincidence, but it so happens that the SPEECH! code in the loader ("Ceetadel, Ceetadel") hammers some code placed at 547c, so you can't just breakpoint 547c before loading the game in order to catch CITAL before it loads CITAM -- it results in incessant breakpoint spam. However, there's no such restriction on breakpointing just one instruction later, at 547f. And, once you've picked your character's gender and key layout, the game cheekily drops in one final "Ceetadel!", so you can't breakpoint it during the character selection either.

For reference, here's the whole of CITAL according to WFDIS's disassembly of the STH copy.

To be honest, I don't think it's really a big deal to create a new disk image containing Citadel and your patch. If you take the STH Citadel.ssd as a clean starting point, I reckon it's fine to use that to create a new version with your randomiser built in. STH did that once before with a patch I made for Chuckie Egg which added extra colours. From the user's point of view, it's much easier to just load a new disk image and boot it, and have everything 'just work'.

Yeah, I'll probably have to resign myself to this; I just would have felt slightly better if I could have produced an entirely clean version.

Rich Talbot-Watkins wrote:Here's the .fdi image for the original disk release of Citadel I was using, by the way. Use B-Em to load it.

And earlier, as Diminished pointed out, the second OSFILE &FF is changed to an OSFILE &04 which attempts to write catalog information to the file (the original will be write-protected, so will cause the "Disc read only" error, though if it succeeds, I don't see that anything bad can happen).

It's just bizarre code. Obfuscated by jumping and self-modification, but not really difficult to understand if you have the time to go through it and look at all the places it amends itself in plain sight. It seems like a very weak attempt at 'protection'. The later release which patches a lot of this out with NOPs does equally weird things (like substituting a SEI/CLI instead of a NOP when removing old code - see &543D, &5445).

That routine at &5720 looks like the bit that patches the game for Master compatibility - it puts a small amount of code at &B001 and then patches the main code to jump there in certain situations. If anyone has the time to look at what it's doing, maybe we can see once and for all why it wasn't compatible with the Master series originally.

Diminished wrote:

Rich Talbot-Watkins wrote:Well, none of the STH images are originals, because the SSD format can't support protected disks. So all of those images are necessarily versions which have had their protection stripped and put onto a clean DFS format disk image.

Ah, I didn't realise this.

IMO it was a big mistake right from the start to use a format without any metadata, as it provides no possibility for upgrading the spec of the disk image. And the only way we can distinguish double sided images from single sided ones is from the file extension, which seems pretty horrible. I would prefer .fdi as a standard disk image format for all BBC disks (particularly since it can hold a simplified data format which works out little bigger than a .ssd), but what's done is done, I guess!

billcarr2005 wrote:Here's the 2 file (CITADEL and CITAX), no speech, no title screen, PIAS1 re-release BBC Master compatible disk version, which I guess would've been the last official release of the game

(CHAIN"CITADEL" to load!)

There will in fact have been another version, as that's not Master Compact compatible! (at &5728, it checks the OSBYTE 0 result for equality with 3, instead of being greater than 3).

Rich Talbot-Watkins wrote:That routine at &5720 looks like the bit that patches the game for Master compatibility - it puts a small amount of code at &B001 and then patches the main code to jump there in certain situations. If anyone has the time to look at what it's doing, maybe we can see once and for all why it wasn't compatible with the Master series originally.

Yes, I also just had a look at it. Seems like, on the Master, VDU variable &359 moved to &36D. On the Master &359 is "0 if plotting graphics foreground, 8 if plotting graphics background" - seems like any other value can make it hang completely! So they just replaced those writes with legal GCOLs instead. The only other patch is when clearing the screen - on the regular version it sets the left margin to 14, and on the Master patch it sets it to 0. No idea what difference this makes or why it was necessary.

For that tiny amount of code that was added, it surprises me that there was absolutely nowhere else in memory they could've put it. I suspect they could've just changed the STA &359 to STA &36D as well. Also, why is the patch code full of NOPs? It's all strange.

Incidentally, that weird code in the patch which is checking for certain magic values and converting them to other magic values: that's translating a 'stripy' colour value into the best flat colour that can be selected using a legal GCOL call. That's why stuff like the Witch's House roof looks flat red on the Master instead of the stripy texture that it has on the Beeb. I guess, had they poked the Master VDU variable directly, they could've had it looking identical.

Rich Talbot-Watkins wrote:Incidentally, that weird code in the patch which is checking for certain magic values and converting them to other magic values: that's translating a 'stripy' colour value into the best flat colour that can be selected using a legal GCOL call. That's why stuff like the Witch's House roof looks flat red on the Master instead of the stripy texture that it has on the Beeb. I guess, had they poked the Master VDU variable directly, they could've had it looking identical.

Ah, I wondered if that was the case. You'd think they could have corrected the triangle coordinates while they were at it ...

I might have a look, and see if we can maybe come up with a better Master version than Superior managed ...

I just tried it: I replaced all the STA &359 with STA &36D and disabled the Master patch. Result? Hangs like the original version.

So then I patched that LDA #&0E:STA &308 with LDA #0, and it no longer hangs, so that was the main problem. I have no idea why it was originally doing that, or why it causes a Master to hang. (Anyone got any ideas?)

But things are still broken. There are spurious cyan triangles in half the screens now. I think the reason for that is that Michael Jakobsen exploited undefined behaviour in OS 1.20 by setting the GCOL plot mode (via STA &35B) to values outside the 'allowed' range of 0-5 (you can actually do this directly with GCOL too). This does an out of range lookup in an OS table which is used to calculate the colour mask for pixels being plotted - you could exploit that to get stripey patterns in BASIC (e.g. try on a BBC B: GCOL 62,135:CLG). Now there was no need for him to do that, as he was setting the colour (via &359) to an arbitrary value anyway, but anyway... I don't know how the Master OS deals with this, but it's not really so easy to patch - so I guess that's why they resorted to doing it 'legally'... seems like the easiest option in the end.

I might as well post the current state of my CITAM disassembly ... It's unfinished and I know for a fact it contains plenty of inconsistencies and mistakes, and much of the variable and subroutine naming could be improved. I don't think WFDIS supports comments longer than a single line and it only allows them on their own line -- you can't place them after instructions, so many of them are more terse than I'd like. The PHP code I used to extract the tile sets is annotated a bit better, but overall that's even more of a mess than this is.

citadel-disassembly-1.txt.zip EDIT: replaced this with a slightly updated version 2 that adds a few acknowledgements along with the remarks in this post, and removes one instance of profanity that occurred when I got particularly frustrated with the room unpacking code ... <_<

I just let WFDIS grind away on the entire 64K address space, so both the VRAM and all the ROM is included.

Standard labels start with L. Loop labels are prefixed either with "Lloop", or "P_" which I started using later on so I could make the labels longer. Similarly, self-modified locations (of which there are plenty) start with "Lselfmod_" or, later on, "SM_". Tables are prefixed with "t_" for variable ones or "T_" for constant ones. OS-owned variables and routines I've tended to prefix with a single underscore. At some point I started labelling useful inline constants by placing comments containing the word CONSTANT above them, in the extraordinarily unlikely event that I got to the point where I might be able to produce a version of the source that can be reassembled.

Constant values use uppercase. Variables and subroutines just use unprefixed lowercase names.

Oh, and this was extracted from WFDIS in a very roundabout fashion, since that software has no facility currently for exporting disassembly in any useful format at all. Namely I saved the thing as an HTML page in Firefox, and then ran a very hastily hacked together PHP script on the saved HTML. I think the conversion is okay, but there may be a few issues with it.

By the way, this was produced from a memory dump of a running game, so everything has "real-world" values rather than initial ones ...

Last edited by Diminished on Sun Jan 14, 2018 4:32 pm, edited 2 times in total.

Rich Talbot-Watkins wrote:
There will in fact have been another version, as that's not Master Compact compatible! (at &5728, it checks the OSBYTE 0 result for equality with 3, instead of being greater than 3).

billcarr2005 wrote:Here's the 2 file (CITADEL and CITAX), no speech, no title screen, PIAS1 re-release BBC Master compatible disk version, which I guess would've been the last official release of the game

(CHAIN"CITADEL" to load!)

There will in fact have been another version, as that's not Master Compact compatible! (at &5728, it checks the OSBYTE 0 result for equality with 3, instead of being greater than 3).

The BBC Master Compact version just bypasses the OSBYTE 0 check altogether with

billcarr2005 wrote:Here's the 2 file (CITADEL and CITAX), no speech, no title screen, PIAS1 re-release BBC Master compatible disk version, which I guess would've been the last official release of the game

(CHAIN"CITADEL" to load!)

Thanks for these, Bill ... I quite fancy this one, as you don't have to sit through the tedious SPEECH! every time.

0.4 is mostly a rewrite. I probably should have tested it a little more but I have other things to do ...

- built-in PRNG means seeds now compatible between Model B and Master
- pyramid sarcophagus and its corresponding crystal are both now randomised
- solver now calculates maximum points for each seed, allowing filtering by 100% / low% games
- option to disable the attract mode (it betrays various item locations)
- universal loader should load any unprotected (i.e. Play It Again, Sam) version of the game from tape or disc
- solver / shuffler ported to assembler, performance improved by an order of magnitude