GithubHelp home page GithubHelp logo

skx / lighthouse-of-doom Goto Github PK

View Code? Open in Web Editor NEW
94.0 4.0 3.0 383 KB

A simple text-based adventure game

Home Page: https://steve.fi/Software/lighthouse/

License: GNU General Public License v2.0

Makefile 6.14% C 93.86%
cpm z80 z80asm game console-application adventure-game spectrum zx-spectrum zxspectrum

lighthouse-of-doom's Introduction

The Lighthouse of Doom

This repository contain a simple text-based adventure game, implemented twice, once in portable C, and once in Z80 assembly language.

The Z80 version of the game will run under the CP/M operating system, and the humble 48k ZX Spectrum.

My intention was to write a simple text-based adventure game to run under CP/M. Starting large projects in Z80 assembly language from scratch is a bit of a daunting prospect, so I decided to code the game in C first, so that I could get the design right, and avoid getting stuck in too many low-level details initially. Later I ported to the Spectrum, because it seemed like a fun challenge for myself!

Quick links within this README file:

Play Online

Thanks to the excellent jsspeccy ZX Spectrum emulator you can play this game with your browser here:

Game Plot

  • You're inside a lighthouse (trapped? doesn't really matter I guess).
  • You see a boat on the horizon, heading your way.
  • But "oh no!", the lighthouse is dark.
    • The boat will surely crash if you don't turn on the main light.

The game is over, when you either fix the light, or find another solution.

If you do not achieve victory within a turn-limit the boat runs aground, and death will consume you all. (It is a very big boat!)

C Implementation

The implementation is mostly concerned with creating the correct series of data-structures, which are essentially arrays of objects. Because if we can make the game table-based we simplify the coding that needs to be done - we don't need to write per-object handlers anywhere, we can just add pointers to tables/structures.

The C implementation defines most of the important things in the file globals.h such as:

  • The structure to define a location.
  • The structure to define an object.

The game-state itself is stored in a couple of global variables, there isn't too much state to care about:

  • The current location (i.e. index into location-table).
  • A list of any items you're carrying.
  • The number of turns you've taken.
    • Incremented by one each time you enter a command, be it recognized or not.
  • Whether you won/lost.

Z80 Implementation

The Z80 implementation is based upon the C-implementation, with a few small changes.

The implementation uses a simple set of structures:

  • A command-table to map input-commands to handlers.
  • An item-table to store details about each object in the game.
  • A person table to store telephone messages.

The main implementation can be found in the file game.z80, but because we support two targets (CP/M 2.x and the ZX Spectrum) there is a small amount of platform-specific code found in bios.z80.

The Makefile should build everything appropriately for both systems, defining SPECTRUM, and ENTRYPOINT as appropriate.

Z80 Changes

  • Along the way I realized that having fixed inventory slots made the coding more of a challenge, so I made the location of each object a property of the object itself.
  • The Z80 version has more easter-eggs (Try typing "xyzzy" a few times).
  • There is rudimentary support for text-wrapping.
    • Enter WRAP 80 to wrap output around column 80.
    • Enter WRAP to view the current wrapping value.
  • There are two victory conditions.
  • The game can be built with the text-strings, and game code, protected by simple XOR encryption:
    • This stops users from looking through the binary for hints.
    • Run make release to build both "normal" and "protected" versions of the release.
    • The encrypted versions of the games have an X suffix in their filenames.

Memory Map

Roughly speaking the game consists of two parts:

  • The driver/game which is about 3k of code.
  • The text of the game along with the state of the world (flags, items carried, etc), which is approximately 9k.

All told the binaries for both CP/M and the ZX Spectrum are approximately 14k.

The layout in memory is basically the same for both variants, however the starting address is different:

  • CP/M
    • Game loaded between the range 256-14000
      • [0x0100-0x3500]
    • Copy of state made to 53248 / 0xD000
  • ZX Spectrum
    • Game loaded between the range 32768-46000
      • [0x5000-0xB400]
    • Copy of state made to 53248 - 0xD000

When the game starts a copy of the "state" is made to 0xD000 before anything has been modified, and then when the game is started that state is copied back to where it is used. This consists of all content between per_game_state_start and the end of the file. This has the location, flags, and similar, as well as all the static-text which will not change between runs. However copying this region was easier than just copying, or otherwise resetting, the state that changes during play.

No doubt things will change over time, however the (undocumented) BIOS command will show you rough sizes for the various parts of the game.

Compiling & Running It

Ensure you have the pasmo assembler installed, and then use the supplied Makefile to compile the game.

Running make will generate the default targets, if you wish to build only individual things then :

  • make game-cpm to build a normal CP/M version.
    • make game-cpm-encrypted to build an encrypted CP/M version.
  • make game-spectrum to build the ZX Spectrum version.
    • make game-spectrum-encrypted to build the encrypted ZX Spectrum version.
  • make lighthouse will build the C-game for Linux
  • make release will build both versions of the CP/M and ZX Spectrum release.

If you have a working golang system you can run the game under this trivial CP/M emulator:

Downloading It

If you look on our release page you can find the latest stable build.

  • For CP/M download lihouse.com to your system, and then run LIHOUSE to launch it.
    • lihousex.com is the encrypted version.
  • For the ZX Spectrum download lihouse.tap to your system, and then launch in your favourite emulator.
    • lihousex.tap is the encrypted version.

Bugs?

Report any bugs as you see them:

  • A crash of the game is a bug.
  • Bad spelling, grammar, or broken punctuation are also bugs.
  • Getting into a zombie-state where winning or losing are impossible is a bug.

Steve

lighthouse-of-doom's People

Contributors

jes avatar skx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

lighthouse-of-doom's Issues

Add objects

We need to add the rest of the objects, including the invisible ones.

(Invisible objects are ones that don't show up when you LOOK, but which can still be EXAMINEd. e.g. The desk & painting.)

LOOK is broken

Reminder for myself; sometimes dropping an item results in it being present in a location, but not shown via LOOK.

Could be an artifact of the recent "invisible" additions.

Allow objects to be toggled, cleanly.

For the purpose of this bug I'm considering only the TRAPDOOR, however a similar process applies to the TORCH - we pretend we have state "TORCH ON" vs. "TORCH OFF", but instead have two objects "TORCH" which is unlit, and "TORCH-LIT" which has text saying "We're on".

At the moment we have two items in the game:

  • TRAPDOOR
  • TRAPDOOR-OPEN

When the appropriate action is carried out we remove TRAPDOOR from the world, and replace it with TRAPDOOR-OPEN. Doing the switch in reverse is difficult because if we tried to handle:

>CLOSE TRAPDOOR

We'd then use our find-object-by-name code and find TRAPDOOR, and never find TRAPDOOR-OPEN.

So we either need to rename the items, dynamically, allow aliases, or handle things in a different way. What we could do is remove TRAPDOOR-OPEN and instead use a state-flag to determine the state. This would be clean and easy:

  • Each item does have a "STATE" flag.
    • We can decide "STATE: 0 == closed".
    • We can decide "STATE: 1 == opened".

The only problem with this approach is the description of the item. We do have an examine function pointer so we can use a dedicated "examine_trapdoor" function:

  • if state == 0 : show "this is a trapdoor .."
  • if state == 1 : show "this is an open trapdoor .."

The place where this falls down is the "LOOK" handler, which will not call the examine handler, so it will always show " the contents of the items description attribute.

Either we add a function "LOOK_fn" to call, or we have to manually update the pointer to make the appearance dynamic. I think the latter is possible, but it might be a pain to code.

Updating the pointers will add extra logic to the "use_xxx_fn" but in some ways that has to be easier than swapping locations and duplicating objects?

Hidden objects behave weirdly ..

Both of these work oddly:

>examine meteor
This piece of rock looks like it came from a larger meteor, it
is glowing with an eerie light.
>examine trapdoor
You cannot see anything special about the trapdoor.
Perhaps you should open it to explore further?

Yet taking them fails, as expected:

>take metor
I can't see that here!

>take trapdoor
You can't take that.

>open trapdoor
I can't see that here!

The issue is that examining things that we sometimes allow you to do stuff with hidden objects, and sometimes not. Examining them shouldn't be possible.

Encryption breaks CP/M build

In the CP/M version, the initial Z80 instructions should translate to

$0100: CALL $0D21 ; bios_init
$0D21: RET

...but in fact $0D21 is inside the encrypted zone and hasn't been decrypted yet, so what CP/M actually executes is

$0100: CALL #0D21 ; bios_init
$0D21: CALL #D00F

...which jumps off into never-never land. Amazingly, this actually works on most emulators/systems, because #D00F typically just contains a lot of NOP opcodes and the program will eventually hit a RET and find its way back to the correct place. But this is obviously not guaranteed and will crash on many systems. (The unencrypted version works as intended.)

[This extremely obscure bug report has been brought to you by me trying to figure out why this game runs in most emulators but crashes under my own in-progress CP/M compatibility layer for SymbOS.]

Coding change ..

I should use the IX/IY registers to point to our object table, as this would make fetching items from it much simpler.

It'd be a lot of code-churn for little benefit, but with the aims of clarity it might be worthwhile. I'll make this wishlist on that basis.

Weirdness with opening a thing ...

The following sequence of actions shouldn't work

  • down
  • down
  • open trapdoor
  • look

Output is:

>open trapdoor
The trapdoor opens, showing a murky set of steps leading downwards into shadow.

>look
You are in the ground floor of the lighthouse.

The ground floor seems very crowded, with most of the room
taken up by a coat-rack, boots, and similar things.

You see:
     A small rug.

Actually going down works too - we shouldn't be able to open the trapdoor if it isn't visible. I guess this is related to using item-state (introduced in ba68969) rather than having two objects.

Bug: When examining a moved rug

Start of game:

  • down
  • down
  • get rug
  • up
  • drop rug
  • examine rug

Leads to this output:

You examine the rug, which shows nothing special. But while looking
at the ground you notice that the rug covered a trapdoor.

BUG:Failed to find trapdoor

Show ship getting closer

The C-version of the game shows "The ship nears .." every five-ten turns (can't remember).

The Z80 version doesn't, and should.

Optimize the size more

We overhauled the output of strings when we added support for wrapping output:

  • When we see an 0x0d
    • We print 0x0a and 0x0d
    • (Except on Spectrum where we just print 0x0d)
  • When we see an 0x0a
    • We ignore it.

That means we could save a lot of bytes by just deleting the 0x0a from our string collection.

Bug: Dropping an item you're not carrying seems to succeed

This is an artifact of the way we search for objects. We have two valid results when it comes to finding objects:

  • You're carrying the object.
  • The object is in the space place as you, and is not invisible.

This allows "USE TORCH" to work, whether you've got it in your inventory, or in the same room as you. However the side-effect of this is that you can drop an item you're not carrying.

For example:

  • down
    • You are in the middle floor of the lighthouse.
  • inv
    • You are not carrying anything
  • drop book
    • You drop it

Compare that to:

  • drop torch
    • You're not carrying that!
  • drop rug
    • You're not carrying that!

Those would appear to succeed if you were in the appropriate locations.

Need "get" and "use handler"

We need to implement custom behaviour:

  • when an item is picked up. (e.g. RUG).
  • when an item is used (e.g. MIRROR, and GENERATOR).

With those we can implement the rug/trapdoor puzzle, and setup a victory condition which makes the game "complete".

Port to ZX Spectrum

Since I've been on a nostalgia trip recently why not port this "game" to the ZX Spectrum?

  • We compile via pasmo which can generate appropriate images.
  • Most of our code is pure assembly, our operating system interface is pretty much:
    • Read input.
    • Output string content.

It shouldn't be hard to move that over ..

GET/DROP behaviour is hardcoded

Right now we have hardwired behaviour for get and drop.

Ideally each item would have a "get_fn" and "drop_fn", which would be invoked. Hardwired behaviour is bad.

Need red herrings

There are a few items in the game at the moment, though only two are essential.

We should add more.

Improve rug handling

Right now it is possible to open trapdoor without discovering it.

The trapdoor should appear under either:

  • TAKE RUG
  • EXAMINE RUG

It should not be possible to OPEN without it being present.

The TAKE RUG solution is currently horrid, because we have no per-item take-handler. I think we should add a handler for taking items, and another for examining. They can be used to do the necessary jobs.

I filed #18 to make it possible to have state for objects/items, with the intention that this would make adding new hooks/pointers easy. But now I realize I didn't actually add any!

Items should have state

If we gained the ability for items to have state, just a single byte we could simplify the code a little:

  • Only need one torch
    • The state would describe ON vs OFF
  • We could remove the hardcoded rug behaviour
    • It could have been moved/examined, or not.
  • The trapdoor could be open/closed

We'd also be able to add a second condition easily, something like a cupboard being open, etc.

String-scrambling our binary

Of course our "puzzles" are trivial, but when I complete the Z80 port and have the thing fully running under CP/M then it needs a little obfuscation.

I don't want players to be able to "TYPE GAME.COM" and see the strings inside the binary.

(Just a trivial XOR will work - providing I place all the command-tables and strings in an adjacent piece of memory.)

Playthrough notes

I watched a volunteer play, and that was educational.

Some commands entered (which did not work as expected) included:

  • LOOK AT DOG
  • PET THE DOG
  • READ THE BOOK
  • OPEN THE DOOR
    • (TRAPDOOR was meant)
  • SWITCH ON GENERATOR
  • TURN ON GENERATOR
  • EXAMINE ROOM
  • LIGHT THE TORCH
  • SLEEP
  • WAIT
  • LOOK AT BASKET

Other random comments:

  • Spelling: The boat is approaching - not "an approach ship"
  • Spelling: "I did not understnad that" -> "understand"
  • We agreed that invalid commands should not increase the turn-count.
  • The player did not even attempt to move, take, get, or carry the generator. She said "It was too big!". So it needs to be noted that this is both small and portable.

It is past time I implemented (simple) text wrapping

I guess updating the bios_output_string function would magically add support for both the CP/M port and the Spectrum version - though it is the latter which needs it more desperately.

For diagnostics I could add a "WRAP N" command to set the width, though perhaps that is overkill and simply defaulting to 70 characters would suffice.

(WRAP 40 to wrap at 40 colums, WRAP 0, to disable. Only minor issue is that I have no current code that reads an integer from the user. But that's trivial)

Fix navigation

Our navigation is broken at the moment - we need to fix DOWN/UP to avoid out of bound accesses.

Simple enough, but annoying.

Possible changes to item-table for z80 implementation?

I'm writing this down not because I'm committing to making the change, but because it has been on my mind for a couple of days.

We started our implementation with the obvious commands "GET", "TAKE", "DROP". Each item in the item-table has pointers for those basic-functions. Later we added space for overriding the default behaviour. So TAKE RUG does something distinct from the generic take_command handler, which just looks up the appropriate object and stores it in the inventory.

After a while we augmented our command-table with synonyms. So for example these three commands both invoke the same use_command:

  • READ BOOK
  • USE TORCH
  • OPEN TRAPDOOR

However this doesn't always make sense. Because "READ" is mapped to "USE" it is now possible to:

  • READ GENERATOR
    • You win. (If you're in the right location, etc, etc).

It does make sense to have more commands than USE. However it doesn't make sense for those to be global. So perhaps our item-structure should change from this:

        DEFW item_3_name  ; torch
        DEFW item_3_desc
        DEFW item_3_long
        DEFB 0,0          ; No take function
        DEFB 0,0          ; No drop function
        DEFB 0,0          ; No examine function
        DEFW use_torch_fn ; USE TORCH
        DEFB 0            ; item state
        DEFB 1            ; this item can be picked up
        DEFB 0x00         ; top-floor

To something like this:

        DEFW item_3_name  ; torch
        DEFW item_3_desc
        DEFW item_3_long
        DEFB 5, "LIGHT", light_torch_fn
        DEFB 3, "USE", light_torch_fn
        DEFB 0xff

i.e. Rather than have the actions be global READ, LIGHT, CALL, DIAL, PHONE, we should scope the commands to the objects.

This feels logical, correct, and good. The downside is that we'd have to make a hell of a lot of updates to change it. The only real addition would be that we could say:

  • PET DOG
  • KILL GRUE

Without also allowing KILL BOOK, and PET TORCH. Which realistically? Nobody is going to type. I think.

There is still going to be duplication, because I guess we'll have EXAMINE xx and TAKE xxx for essentially all objects. That's why in my updated structure I still have name, description, and extended description. I guess I'll also need to keep the location field. And state.

Document memory-map

Might be a useful "BIOS" sub-command, but it would be nice to know:

  • Size of "driver".
  • Size of "state" (i.e. the areas that get modified and the associated text).

Right now we copy stuff around as a means of resetting the game, and the addresses were picked at random.

I suspect the size of the text is bigger than the size of the driver, but would be good to know. We could implement compression - everything between per_game_state_start and end_of_source could be compressed and decompressed into the buffer-area on-load. However I suspect we'd have to write a perl/python script to compress and the more complex build would make it hard to understand and modify.

Losing isn't possible

The assembly version of the game doesn't kill you if the ship runs aground.

We do count turns, but we don't act on them.

Drop the turn-count to 50, and make you lose after that.

Add CI process

Once the open bugs are ready we'll be ready for releasing binaries.

Automate this via github actions..

Bug: Turning off the torch still lets you see

Start the game, get to the basement with a lit-torch.

Once in the basement you can turn off the torch:

>inv
You are carrying:
     A small torch, which is turned on.

>down
You are in the lighthouse basement.

This seems to be a graveyard for discarded machinery, and
other random junk.

You see:
     A small, portable, generator.

>use torch
You turn the torch off.

>inv

You are carrying:
     A small torch.

>look
You are in the lighthouse basement.

This seems to be a graveyard for discarded machinery, and
other random junk.

You see:
     A small, portable, generator.

So the bug here is that when you turn off the torch you should be taken to the dark place - as happens in reverse when you turn on the torch when inside the basement.

Using an item twice causes oddness ..

I think this is related to #33, and is caused by the cleanup we made when we used flag-states rather than duplicate objects (27eda49).

Anyway:

  • use torch
  • use torch

Torch goes on, then when you turn it off you drop into undefined behaviour. (Missing ret?)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.