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
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
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
This is what typical example looks like:
You are in a clearing in the forrest and find yourself at a
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
to generate the descriptions and then tweak them accordingly.
Since it would be great to have images for each entry, I decided
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
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!):
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
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:
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
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.
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
I needed a logo for the adventure. And at this point I had decided
that I would provide different player characters. They also needed
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
(I had already purchased a license a while ago).
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
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:
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.
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
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
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
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:
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:
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
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
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
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
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
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
Since the player can choose which character they want to play,
there is a list of available characters in the file
Another great advantage of having the adventure data completely
separate is that it can be easily reused to create a different
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.
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
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
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:
For the renderers.js (which generates the current view of the
entry the encounters and other elements) my first instinct was to
to an area of the page optionally with some variables in it.
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
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:
of using template literals?
And this is what chatGPT responded:
Yes, there are reasons to use 'createElement' instead of
Firstly, 'createElement' is the more traditional way of creating
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
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
Refactoring the single-line template literal example above to use
createElement looks like so:
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.
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:
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.
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
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.
After downloading the APK file, open it using the Package
Confirm that you want to install the app.
On the warning page open "More Details".
And confirm one more time that you want to install the app
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
LumaFusion, screen recordings of the game, as well as music and background