Mansion of Trepidation

From First Idea to Final Implementation

April 2023

Summary

I wrote Mansion of Trepidation, a short choice-based solo fantasy adventure in the style of the "Choose Your Own Adventure" and "Fighting Fantasy" books, in vanilla JavaScript, and using Bootstrap 5. I generated the artwork using Midjourney and Bing Image Creator. The location descriptions are generated using Chat GPT. The player can choose to play one of three characters to explore the "Mansion of Trepidation", defeat the villain Mortacion, and rescue their five-year old nephew, William (at the very end of this page you can find a map with a solution to the game).

The game runs on the web but I've also turned it into a quick Android app.

Play the Game Go to GitHub Repo Download Android Beta

Here's a short promotional video for the game.

Watch on Youtube

Introduction

For a long time, I've wanted to try my hand at developing my own choose-your-own-adventure game in JavaScript.

This type of game originated in the late 70s and early 80s in book form. It is a type of gamebook that allows the reader to become the hero of the story and make choices that determine the outcome of the adventure. The book presents a series of numbered sections that describe the story and present choices for the reader to make by turning to other numbered sections. These choices often involve deciding where to go, what to do, and how to interact with other characters.

This is what typical example looks like:

123

You are in a clearing in the forrest and find yourself at a crossroads.

  • To go north turn to section 87.
  • To go west turn to section 24.

Then the reader would flip to the section with the right number and continue reading/playing the book.

Using simple HTML this would not be much of a challenge. You simply have a number of HTML pages and link to the pages. But of course, I wasn't going to do it the easy way.

Creating the Adventure with the Help of AI

The challenging part was always to write the actual adventure first and then adapt it for the web. The books would usually have hundreds of sections. You'd set out on a quest to find a mcguffin, escape from a labyrinthine dungeon or to rescue someone. The adventure has multiple endings, depending on the choices made by the reader. Some endings may be happy, while others may be tragic or even fatal.

First I needed to create the actual adventure story with its branches and each entry providing a description of the location or the encounter. That is a lot of work. So I decided make a simple map of the adventure in the style of the classic "haunted house" setting and then use ChatGPT to generate the descriptions and then tweak them accordingly. Since it would be great to have images for each entry, I decided to use Midjourney AI to generate those. The Midjourney test access only allows for two dozen or so images to be generated. After registering multiple email addresses it became a bit tedious to use Midjourney, so when the Bing image creator became available, I used it to generate the remaining images.

I was set to create my own little adventure called "Mansion of Trepidation". The evil necromancer Mortacion has abducted William, the player character's five year old nephew. Mortacion wants to harness William's youthful energy to rejuvinate himself. This would kill William. Armed with their sword, shield and cunning, the player has to search Mortacion's mansion and rescue William before it's too late.

Each entry consists of a location with an image, a description and options to choose from. On their journey the player encounters vile foes, monsters, and demons to fight but also benevolent characters who will help them on their way. There are also items vital to completing the quest and provisions to replenish the player character's lost endurance points. The player explores each room in the mansion and uses the items in the right rooms to defeat Mortacion and find William.

I started out by sketching out the haunted house. I didn't do any actual research but just went with my gut feeling for which would typically be in a house (entrance hall, bedroom, dining room, kitchen, etc.). I spiced it up with some Victorian rooms inspired by a monastary (e.g. infirmary, conservatory) because they sound cool. This is my initial map with the locations in the mansion (and yes it was easier to make a photo of my note-taking eInk tablet than to export the map. LOL!):

The first rough map of the mansion

I used this map to get an idea of the locations I will need and then to get the descriptions from ChatGPT. In the final adventure I added some more locations, more enemies, more food/provisions and non-player characters as well.

Generating Description Text using ChatGPT

I used ChatGPT to try out different prompts for descriptions of the locations on my map to get the right tone and length. The descriptions were often too long. So I edited them down to the length that I needed and adapted them to fit to the adventure. (To be fair, sometimes ChatGPT would generate the one or the other description that gave me inspiration for other aspects of the adventure.)

Here's an example for ChatGPT providing the location descriptions:

Generating the description for a room in ChatGPT

Usually, I was super happy with the descriptions provided by ChatGPT. In the case above, the description is obviously too long. So I shortened it. Another thing that I noticed is a repetition of text elements across multiple different descriptions. I edited those or outright removed the repetitions.

But one thing is certain: This is a great way to get an initial versions of large amounts of descriptions. It helped me get a lot of perfectly usable text with comparatively small effort.

Generating Images using Midjourney AI and Bing Creator

Once I had the descriptions, I needed images for the locations, the encounters with the foes, the items and the non-player characters. At first, I went used Midjourney AI to generate the images. This gave me 25 images free-of-charge. Midjourney uses Discord as an interface for the AI. You enter a prompt and then get four suggestions for images. You can then choose one of the suggestions or enter a new prompt. I used this to generate the images I needed for the adventure. I had to register multiple email addresses to get more than the 25 images.

Generating images using Midjourney AI

Since I kept tweaking and changing the adventure, I needed more images than I had originally planned. It became a bit of a hassle to keep registering new email addresses with Midjourney. Fortunately, just about then, Microsoft introduced the Bing image creator. I used this to generate more images.

While I was coming up with the premise, locations and situations in the game, the name of the adventure started to materialize. I called it "Mansion of Trepidation" as a nod to the typical gamebook titles that are often something like "The SOMETHING of SOMETHING", e.g. "Forrest of Doom", "The Caves of Time", "City of Thieves" or "House of Danger".

I needed a logo for the adventure. And at this point I had decided that I would provide different player characters. They also needed logos.

I didn't want to design logos from the ground up, it would have taken forever. Therefore I decided to use a logo design package for MacOS called ArtText 4 (I had already purchased a license a while ago).

Generating the titles using ArtText4

3D Looping Video from 2D Images

So excited by the possibilities provided by out-of-the-box tools using AI or otherwise, I started looking for more tools to make my life easier.

I found the LeiaPix Converter. This service can turn 2D photos and images into animated 3D videos. I started feeding the AI generated 2D images of my locations and encounters into LeiaPix and got videos I could loop that reminded me a bit of the moving photos in Harry Potter's wizarding newspapers. I probably generated around 50 videos using LeiaPix without paying a cent. I had to register but never had to provide any payment information. I planned on using these videos instead of static images and loop them.

Please find an example of a looping video with a 3D depth effect below (you might have to tap on it to play on a mobile device):

Even though I generated a bunch of videos out of the AI generated images, I realized once I tested them in the game that autoplaying videos in Google Chrome does not work by default. Furthermore, on an iPhone, the videos needed to be tapped to play and then they occupied the whole screen. Given the file size of the videos, which should not be underestimated, I then decided to revert to using static images.

I had my AI generated description text, the images, and the logos. The adventure was taking form.

Structure of the Adventure Data

I added even more rooms and a few puzzles that need the player to find an object first so that they can solve them.

The adventure needed to be more or less complete so that I could find out which features I would need to implement. While writing the adventure and implementing the individual functions, I went back and forth between the data and the functions. A few things were clear from the beginning: I'd need a branching logic and a simple combat system. Once I started coming up with traps and obstacles, I realized that I needed a logic for incidents. And when started adding non-player characters, I needed to implement features to handle them.

This is what my final map looks like:

The final map of the mansion

Now I need to create the structure of my adventure data. Each entry needed a certain number of things:

  • The unique ID of the entry,
  • the title of the location,
  • the description text,
  • the connections (the choice options available to the player),
  • the encounters a player can have at the location,
  • the items that can be found at the location,
  • the incidents that can happen at the location,
  • the NPCs a player can meet at the location,

Here is an entry without any encounters, incidents, items and NPCs. It only shows the connections for the options a player can choose from. Each connection needs a text and the ID of the entry it should lead the player to.

An entry without any encounters, incidents, items and NPCs

You may ask, where the related image file is referenced. I decided not to put it into the adventure data. Every entry will have an image. So I will simply name the image after the entry's unique ID. I'd simply display the image with this unique ID when I displayed the entry data.

This was the structure of an individual entry in the adventure. I decided to put all entries into a single adventure object. The entry ID would be the key and the entry itself the value.

The basic data that would allow me to write a logic to move between different locations (entries) was now ready. That would make for quite a boring adventure: simply moving around without actually doing anything. I wanted to add opponents a player can encounter and fight, as well as items they can pick up and use, and non-player characters they can talk to.

When the player starts a new adventure, I create a copy of the adventure data and store it in currentAdventure. I will update this object whenever a player gets new options by defeating a foe or removes an item from a location by placing it in their inventory.

Each entry shows the player an image, a description of the location and a number of options to choose from. They select an option and then move to the selected location.

Breathing Life into the Adventure

One of the British variation of the choose-your-own-adventure gamebooks were called Fighting Fantasy in the early 80s. This incorporated aspects from tabletop role-playing games like "Dungeons & Dragons" (albeit in a simplified form). In addition to the branching in the story, they also provided a system for player statistics like skill and stamina as well as a combat system to fight against foes encountered during the adventure. To round things up, "Fighting Fantasy" also has an inventory system where a player finds items that can help them on their quest.

I wanted to incorporate these incident, combat and inventory mechanics into my little game.

Encounters and Combat

In "Fighting Fantasy", the player has the stats for skill which describes their proficiency with a weapon and stamina which is basically their hit points, i.e. how much damage they can take before dying. An opponent they player encounters also has values for skill and stamina. Combat is usually resolved using dice rolls to determine the outcome of attacks and defensive maneuvers.

The combat system is actually quite simple. When a player fights an opponent, they roll a pair of dice and add their skill to them, this is the value for the player character. Then the player rolls two dice for their foe and adds the foe's skill value. Then the two values are compared with each other. Whoever has a higher value manages to land a hit. The fight goes on until the stamina of the player character or of the opponent is depleted. If the player dies then the adventure is over.

I personally find that the battles in a gamebook rather boring. So I wanted to simplify the already simple fight mechanics used in "Fighting Fantasy" even further. This fight mechanic can be implemented very easily in JavaScript.

The exact combat logic is quite simple. Both the player and the foe roll a die and add it to their agility. Whoever has the higher value strikes a blow on their opponent. This reduces the endurance of the hit opponent by one endurance point.

This is what the opponent encounter data looks like in an entry for a location in the adventure.js:

An entry with opponent encounter data

When a player defeats a foe, their experience points increase.

Incidents and Test Rolls

On specific locations, a player is confronted with an incident, e.g. a trap. Then they have to test their agility or fortune to see if they manage to evade the trap. The typical Indiana-Jones-style traps are poison darts shot from the wall or trapdoors that open up into a pit full of spikes.

Incidents are represented by a value in the entry of a location. Their data includes a description, the type of test (agility or fortune) and, in the event of failing test, the number of points to be deducted from the player's endurance.

And the incident contains more: the options that open up to the player if they succeed and the options that open up if they fail.

This is a bit of a recurring theme in the adventure. The player will be confronted with an incident or a foe. They will not have any options to choose from until they have defeated the foe or tested for the incident. If they manage to pass, then the location will be updated with the options available to choose from. Unless - of course - the player's character dies. In that case, the adventure is over.

This is what the incident data looks like in an entry for a location in the adventure.js:

An entry with incident data

Items and Inventory

Throughout the adventure, the player must manage their character's inventory, which may include keys, magical objects, potions, and other items. Using the right items at the right time can help the character overcome obstacles.

A player can only carry three items in their inventory. So when a player picks up an item from a location I check whether they still have room in their inventory. If they do then the item is added to their inventory and Collecting Items - player character can only carry three items in their inventory

Some items are provisions. A player can make use of them on any location. They select "use" and then consume the provisions. This replenishes them and increases their endurance (but never past the maximum value).

There are also other special items, tools that can only be used at specific locations. These items enable the player's character to solve an obstacle at a location, therefore they can only be used at this specific location.

Without getting too "spoilery" about the adventure, there is a section where the character enters a maze and gets lost. They can only navigate the maze if they have a compass.

So items with the type of tools also have an entry for a relevant reference. This contains the ID of the location entry that is relevant to this option. Should the player reach the location, they can find hints in the description as to which item could help them at the location. If the character has the item in their inventory and select "use" then they are presented with new options to choose from.

When a player gets an item, their experience points increase. Some items are very valuable and they are useful, others only provide experience points but cannot be put to use in the adventure. And there is one item in the Mansion that is a red herring that only uses up space in the player character's inventory without providing any value.

This is how the data for an item is structures in an entry in the adventure.js:

An entry with item data

Non-Player Characters (NPCs)

NPCs in the adventure are similar to incidents. On specific locations, a player is confronted with an NPCs. They should not be fought but when talked to they will provide the player with some hints. As a bit of a joke, I added the option to fight an NPC. But the player character will die every time they try.

Where applicable, I've added them as a distinct item to the respective entry in the adventure object.

This is what the NPC data looks like in an entry for a location in the adventure.js:

An entry with NPC data

Implementing the Game

I structured the whole game with a separation of concerns between the adventure's story data, the game logic and the display functionality.

I decided to implement this in vanilla JavaScript and generate the HTML elements. I used Bootstrap 5 for styling because I am very familiar with it and does the job. You might be right if you think that Bootstrap-styling is not super-exciting to look at. But with some tweaking it can be made to look quite workable for this type of adventure.

1. Data

The data contains the the adventure's entries with their description, their choice options, if applicable the incident, the foe and the NPs they are all stored in adventure.js.

Since the player can choose which character they want to play, there is a list of available characters in the file characters.js

Another great advantage of having the adventure data completely separate is that it can be easily reused to create a different adventure.

2. Logic

The logic of the game is stored in game.js. This contains the functions to move between location entries, to fight opponents, to test for the success or failure of passing an incident, etc.

3. View

All the functions to display an entry, an opponent, an incident, an NPC and the player character's current stats are distributed among three files.

index.html is very bare-bones. It defines the areas that will be populated with their respective data.

I decided that I'd use the Bootstrap library for to style all the outputs and view. Some of the data would be displayed in modals that overlay the page. I decided to place all the functions to instantiate the modals in modals.js.

Finally, the functions to update the view by generating the current entry or showing the opponent and the progress of combat, etc. are in renderers.js.

I am sure that my code did not quite meet Robert C. Martin's clean code standards. Some functions are much too long and could be split into multiple functions, and there is a bit of an intermixing of the view functions and the controller functions. Nevertheless, there is a separation of concerns and most importantly, the adventure works and can be extended. It definitely will be worth going back to the game to refactor it. If there is a development philosophy I had adhered to, it might have been that of Programming, Mother******.

Saving and Loading Game States

The adventure only has about three dozen entries. It is quite short compared to the 400 odd entries a gamebook usually has. But when I was playtesting the game, I realized that players might still take a while to play through the whole game. So I decided to add the functionality to save and load games.

I decided to do this using the browser's local storage. When a player saves a game, their current character with its stats and inventory and the current state of the adventure with the available updated choices and the opponents left in the entries are both stored in a local storage object.

When the player wants to load the game, they need to use the same browser. The saved game is retrieved from local storage and the player can continue playing where they left off.

The User Interface

This is what the initial sketch of the user interface looks like:

The initial sketch of the user interface

For the renderers.js (which generates the current view of the entry the encounters and other elements) my first instinct was to use template literals. Basically, Javascript assigns straight HTML to an area of the page optionally with some variables in it.

Here's a simple example:


  targetedElement.innerHTML = `<p class="myClass">${myText}</p>`;
                  

While this is a very straightforward way of doing it that has the additional advantage that it is quite readable, I did have a nagging feeling that it is not the best way of doing it. In one of the coding bootcamps I had participated in a few years ago, I had gotten a stern talking to by the instructor when I used template literals instead of using JavaScript to create each element programmatically and assign attributes. His reasoning back then was that template literals are a security risk but I am murky on the details. Searching around on Google did not provide a definitive answer, so I decided to ask chatGPT.

Here's my prompt:

In Javascript is there any reason to use createElement instead of using template literals?

And this is what chatGPT responded:

Yes, there are reasons to use 'createElement' instead of template literals in JavaScript, depending on the situation.

Firstly, 'createElement' is the more traditional way of creating elements in JavaScript and is widely supported across different browsers. Template literals, on the other hand, are a relatively newer feature and may not be supported in older browsers.

Secondly, 'createElement' allows you to create and manipulate elements programmatically. This means that you can dynamically create elements based on user input or other conditions in your code. In contrast, template literals are static and can only be used to create elements with fixed content.

Finally, 'createElement' also allows you to set attributes and properties of the created elements, as well as add event listeners and child elements. While it is possible to achieve some of this functionality with template literals and DOM manipulation, using 'createElement' can often be more efficient and easier to read and maintain.

In summary, while template literals can be a useful way to create static HTML content, 'createElement' is still the preferred method for creating and manipulating HTML elements programmatically in JavaScript.

I didn't feel that this was a convincing enough argument. Nevertheless, I decided to use createElement to generate the views. This was quite laborious, but I was going to make my code public on GitHub and I dreaded some smartass commenting on the use of template literals. There might even be a better reason for using createElement instead of template literals that neither chatGPT nor my superficial googling had revealed.

It definitely was a lot more work. And to be frank, I find the code ugly.

Refactoring the single-line template literal example above to use createElement looks like so:


  const paragraph = document.createElement('p');
  paragraph.textContent = myText;
  paragraph.classList.add('myClass');
  targetedElement.appendChild(paragraph);         
                

This way of writing my views yields much more verbose and complex code given the cascading nature of some of the views I wanted to use. In some cases I simply put my template literal code into chatGPT to have it refactor it to use createElement. In many cases the AI produced usedful code that I could immediately use. In some cases the AI generate code was missing some parts that I had in my original code so I had to got over it a again and adapt it before actually using it.

In the end, my application contains lines and lines of JavaScript code using createElement and setAttribute and classlist.add that could have been easily simplified with template literals. But for what its worth, I could quiet my nagging concience that I might not be "doing it the proper way".

This is what the final user interface looks like:

Introduction Screen
Entrance Hall Entry with an Enemy
Greenhouse Entry Showing an Item
Stables Entry Showing an NPC

Audio Effects

I wanted to add some audio feedback when changing an entry and during the fights. The audio effects should not be too intrusive, too long or simply too silly.

After some searching on freesound.org, I found suitable effects. I converted them to MP3 and went ahead to add the play triggers to all render functions that change a state or open a modal.

Finishing and Tweaking the Adventure

When implemented the game, I went back and forth between tweaking the adventure's data structure, the implementation of the logic, and user interface. It was a bit of an iterative process because, e.g. when implementing the logic I'd realize that I could structure the adventure data differently to make life easier for me, or the user interface would need to be changed to make it more intuitive given the data available.

I had started out with the design of the adventure data, but this was put to the test during the implementation. This involved writing the code that processes the adventure's data, calculates the outcomes of combat and incidents and triggers the right renders in the user interface. During this phase, the I revised the adventure data based on the logic implementation. Simultaneously, I implemented the user interface. This involved designing and creating the visual elements of the game that the player interacts with, such as the entries with their location description, their location image and the available option buttons to choose from.

Only when I had the first version up and running, could I actually see who well the user interface and the logic work with the data of the adventure's scenario. Only when testing them together could I figure out which parts I didn't like and make the necessary adjustments to the data design or the logic implementation to accommodate changes in the user interface or vice versa.

After this process of constant back and forth, I could meet my own requirements and expectations resulting a nifty little game.

The Mansion of Trepidation

You can play Mansion of Trepidation here online:

Mansion of Trepidation

You can find the game's Git repository here:

Mansion GitHub Repo

Accidental Android App

Once I was happy with the game running in a browser, I decided to try turning it into an Android app.

I was pleasantly surprised how easy it is to turn a JS/HTML/CSS application into an Android webview app. In Android Studio, I dragged all the files into the correct directories, changed the manifest file and the main Java file to set the permissions and the initial onCreate() method.

The Android development documentation about using webviews and loading locally stored web content is quite good.

So within a few hours I was excited to have an Android app running in the simulator. I removed the save and load game feature because the webview does not support local browser storage the same way that the browser does. Also, the state of the game app persists even when you leave it, so the game will continue at the same location you left it at.

Once I was happy with the result in the simulator (and playtested it properly just to make sure that everything works as expected after I had removed the funcionality to save and load a game), I generated the APK file to run it on an Android device.

The Beta is available here:
APK of Mansion of Trepidation Beta

How to Install the Beta

Installation Step 1

After downloading the APK file, open it using the Package Installer.

Installation Step 2

Confirm that you want to install the app.

Installation Step 3

On the warning page open "More Details".

Installation Step 4

And confirm one more time that you want to install the app anyway.

Since I haven't put the app up on the Google Play Store, you can only install the game by side-loading it. If you want to install it, you'll need to download the APK and have to confirm that you really want to install it on the Android phone or tablet. Rest assured that the game app does not track any user data.

Conclusion: The Adventure of Making An Adventure

I'm quite happy with the result. I'm also happy that I managed to create a game that is playable on a mobile device.

Creating the game did not take a lot of effort to make because ChatGPT and the AI image generators helped with the scenario of the adventure. I know that this game is a throwback to a simpler time. Fans of the original gamebooks can relive the experience. I hope that I managed to capture the appeal of choose your own adventure gamebooks with their interactivity, personalized reading experience, and fast-paced format.

Finally, I slapped together a quick promotional video on my iPad Pro using LumaFusion, screen recordings of the game, as well as music and background footage from StoryBlocks.

Watch on Youtube