This is a postmortem for Causality Couriers, my entry into Ludum Dare 53. I’ve kept spoilers to a minimum, but you might want to play it before reading anyway (it’s pretty short).
It’s been eight years and twenty contests since the last time I successfully finished a game for Ludum Dare. I’ve attempted one or two in the interim, as well as some other jams, but that was the last time I was reasonably happy with the output. There are actually quite a few things that have to come together for me to actually complete one of these successfully.
- The theme has to be inspiring. This is perhaps more about my state of mind than whatever the theme is; sometimes, I just can’t come up with an idea I like for a given theme. Not that I don’t have strong opinions about what constitutes a good theme – see below.
- The jam needs to fall on a weekend when I have a relatively empty schedule.
- The idea I come up with needs to be doable in a weekend.
- The idea needs to be something I won’t completely change halfway through.
Ludum Dare 53 (theme: “Delivery”) kicked off on the weekend of 29 April, perfectly positioned between two public holidays. That satisfied criteria 1 and 2. Criteria 3 and 4 were not satisfied, but fortunately Ludum Dare has recently introduced a new category, Extra, which allows participants to submit their game any time during the three week judging period which follows the main weekend. Games submitted in Extra are not in the running for winner of the Competition (48 hours), or even the Jam (72 hours), but can still get feedback and ratings, which is the main attraction in any case.
This ended up being a pretty long post, so here’s a table of contents:
# Concept
Much like my previous LD entry, I Hunger, Causality Couriers is text-based. This is one of the easier sorts of games to make in a short timeframe. The controls and gameplay, such as they are, come pre-defined: click on links or type into a parser. The content is easy to generate, for a writer – rooms, objects, characters, can all be constructed in a few lines of text. Challenges arise and bugs appear, but if, like me, your main gamedev skills happen to be writing and programming, you’ll be completely in your element dealing with such things. Also you can get away with leaving out things like graphics and sound, both of which would be indispensable for any other type of game.
I came across Robin Johnson’s Gruescript around the time it was initially released in late 2021 and had quite a bit of fun playing around with it – I even wrote a Vim syntax file for the language. The project I first started in it never quite came together, but the tool stuck in my mind as something I wanted to use. It strikes a very satisfactory balance between a hypertext tool like Twine and a heavyweight parser interactive fiction tool like Inform 7 – gameplay is mouse-driven1 and there’s a simple world model with rooms, objects and people. The language itself is terse and very fit for purpose.
Apart from using Gruescript, I wanted to experiment with AI, mostly by using Stable Diffusion to illustrate the game, but also by having ChatGPT as a creative assistant.2 It wasn’t any good at writing Gruescript, having essentially none in its corpus, but it was very handy for brainstorming. On the Saturday morning after the jam began, I asked it for some ideas.
None of these ideas were bad, but they all seemed a bit obvious, so I asked for some more:
I went with number 2 (though number 3 was a close second). A time-travelling courier bring packages from the future to the past seemed like a cool concept with a lot of potential.
# Story
In the initial version of the game, I had the player start off in a delivery hub with three unmarked parcels, all different sizes and shapes. The delivery hub also contained three different time portals, and the idea was that the parcels had all lost their tags and you had to guess which one should go where. It was going to be possible to deliver any of the three packages to any of the time periods, and depending on which one was delivered where, the present time would change and so would later time periods.
This, of course, was a classic combinatorial explosion. I started mapping out all the different possibilities and realised that not only would it be very time-consuming to do every different path justice, but it would ultimately make this game very similar to my last Ludum Dare game, I Hunger, in which a human society develops on different lines depending on what combinations of sacrifices are demanded by a volcano god. The other problem was that the different time periods, packages and changes to history were kind of arbitrary and the story didn’t really feel coherent.
At this point I was halfway through the Jam period and found myself needing to scrap a lot of content and return to the drawing board. Luckily there was still the three-week Extra category, so I decided to go through with a radical rewrite and not pressure myself to finish it by the Monday evening. I pared the story down to a single package, and opted to start with the player collecting the package from a more interesting location than a delivery hub. The player would then proceed to deliver the package to one of the locations I’d already implemented, allowing me to salvage a fair amount of content from the original version.
I managed to get about 70% of the game implemented by the Monday evening, i.e. most of the content in the two main areas. But there was still the matter of the illustrations I wanted to add. Up until that point, I’d been developing in Gruescript’s online interface, as it allows for rapid testing and has some neat debugging features. However, it does not provide any means for editing the game’s HTML, which I would have to do if I wanted to include illustrations. So on Monday evening, I shifted to local development.
# Engine hacking
Luckily, I’ve played around with local Gruescript development before, and the engine is simple and cleanly written enough to be very amenable for hacking on. When you export a game from the online interface, you get an HTML file containing three things:
- The HTML and CSS which makes up the interface. This is mostly hardcoded, though Gruescript does let you change the colours of most things from your game code.
- The JavaScript that powers the game engine. This is pretty simple and easy to follow – many elements of Gruescript code map directly to JavaScript functions.
- Your own Gruescript code, included in a
textarea
at the bottom of the page. This code is evaluated when the page is loaded to create the game’s content.
The CSS wasn’t quite as responsive as I’d have liked, so I rewrote some of it to use flexbox. Given more time I probably would have changed more of it. I also had to add an area for illustrations, and create a JavaScript function for changing the illustration. I made a few alterations to the game engine to do things like make the controls disappear when the game ends. At the last minute before I was about to publish the game, I remembered that Gruescript included save and load functionality, and had to make some quick hacks to ensure that functionality was illustration-aware. There were still a couple of bugs with these engine and interface changes that I fixed3 after another Jam entrant brought them to my attention.
I didn’t want to have to edit my Gru code inside a massive HTML file, so I copied it to its own .gru
file, and had ChatGPT write me a build script in Python, which just copied the latest contents of the .gru
file into the HTML file’s textarea
. With these few things in place, I had a solid local development workflow, and could continue development with the game in its intended interface.
# Game code
Being a highly specialised DSL, Gruescript looks very different from a standard C-like language. It reminded me most of the language used by AGT,4 mixed with a bit of Inform 7, but far more terse than either. Its syntax is very simple, containing little nesting or even special characters. Rooms, objects and characters are defined in room
and thing
blocks:
# A ROOM
room before_barricade You're standing in front of an enormous barricade made of junk.
prop display Before the junk barricade
prop year Unknown year
tags start
# AN OBJECT
thing sword Courier's Blade
desc This is the first job you've had where a massive sword is considered required equipment.
carried
tags portable
# A PERSON
thing fletcher shifty character
name shifty character
desc The man looks to be in his sixties, with a white beard and a mane of grey hair.
tags alive male conversation
loc wooden_shed
prop callme Fletcher
prop start_conversation "You must be the courier," says the man, looking you up and down. "Name's Fletcher." Your collection tag mentions a Darius Fletcher – this must be him.
prop end_conversation "G'bye then."
Each of these blocks starts with an identifier and a description, followed by series of properties – the room’s exits, the object’s initial location, the text to use when starting a conversation with a person, and so on. Each room and thing can also have a set of tags: some of these are meaningful to the engine – portable
, for example, means the thing can be picked up and placed in the player’s inventory – but you can also create your own tags for your own purposes. You can also define custom attributes with prop
, which can be strings or numbers.
Moving from object definitions to functions, we have the verb
block. Here we can define what a given verb used with a given thing will do.
verb twist collection_tag
say Twisting a collection tag will instantly transport the holder to the place and time of the parcel's collection. But only once, and you've already used this one.
Verbs can also be defined generically, with the special variable $this
used to refer to the thing involved.
verb twist
say You twist the $this.
A noun has a set of attribute definitions, and a verb has a sequence of commands to execute and expressions to evaluate. Initially, it was unclear to me how complex conditional logic was intended to work. A simple, single path Gruescript verb definition might look like this (comments are prefaced with #
):
verb eat lunchbox # defining a verb called eat on a thing called lunchbox
carried $this # is the player carrying the lunchbox?
has $this full # does the lunchbox have the tag 'full'?
untag $this full # remove the 'full' tag from the lunchbox
say You devour the banana, peanut butter and marmite sandwiches in your lunchbox. # print some text
The carried
and has
lines are expressions. If an expression evaluates to true, execution continues to the next line; if it evaluates to false, execution of the entire verb
block ends. This means that if the player is carrying a full lunchbox, the full code will execute and the final say
line will be printed. If the player is not carrying the lunchbox, or has already eaten from it, nothing will happen.
Gruescript provides simple syntax for printing something in the event of an expression’s failure – just append :
to the expression line and add the message after it.
verb eat lunchbox
carried $this: You'll need to pick it up first.
has $this full: The lunchbox is empty.
untag $this full
say You devour the banana, peanut butter and marmite sandwiches in your lunchbox.
This syntax is a bit confusing at first, but works well for a lot of adventure game puzzles, where you often have to take a number of intermediate steps to achieve some goal. However, I soon found cases where I needed to do more than just print a message when an expression was false. In some instances, the right solution was to flip the logic (e.g. !has $this full
will be true if $this
does not have the tag full
), but in other instances I needed a sequence of multiple commands for both true and false cases.
After rereading the documentation and perusing some of Gruescript’s example code (especially the ~2000 LoC implementation of The Party Line), I realised that the solution was to create multiple verb
blocks with opposite expressions.
verb eat lunchbox
carried $this
has $this full
untag $this full
say You devour the banana, peanut butter and marmite sandwiches in your lunchbox.
verb eat lunchbox
!carried $this
give $this
say You pick up the lunchbox and look inside.
has $this full: The lunchbox is empty.
untag $this full
say There are some banana, peanut butter and marmite sandwiches here. You devour them.
Per the documentation, when a verb button is clicked, Gruescript will evaluate every applicable verb block from specific to general, stopping when one succeeds (runs all the way to completion without a failed expression). The same is true for other procedural logic blocks in the language.
Once I understood that properly, I was able to use Gruescript pretty proficiently. The engine provides a js
command for executing arbitrary JavaScript, which I used to change the current illustration and force items into holding section of the player’s inventory at certain moments – for example, to make the player hold the collection tag at the start of the game.
Overall I’d say I had a positive experience with the language, but I would have liked some syntax for embedding conditionals inside strings, like what Inform 7 does with square brackets:
Candy Storage is a room. "As you enter the room, the white cubes inside all turn to face you.[if unvisited] They're square candies that look round.[endif]"
This code will only print the second line of the room description the first time you enter the room. And you can add an else statement and nest the conditions for truly varied text. To do something similar in Gruescript, which only supports direct string interpolation, you need a bit more code and some creative hacks.
room candy_storage As you enter the room, the white cubes inside all turn to face you.{candy_storage.extra}
prop extra They're square candies that look round.
verb go
at candy_storage
tag candy_storage visited
write candy_storage.extra &zerowidthspace; # setting a variable with nothing will give it a value of 0. To avoid printing 0 in our room description, we need to set it to an invisible character instead.
continue
Gruescript was designed for more textually minimal games than Causality Couriers ended up being, games in the spirit of the highly terse Adventure Interational text adventures by Scott Adams, so it’s understandable that this wasn’t an implementation priority.
Ludum Dare encourages entrants to release their source code (this is a hard requirement for the 48 hour event), so I’ve made all of the code available in this Github repository.
# Illustrations
I’ve been playing around with Stable Diffusion since its initial public release, and one of the purposes I’ve long planned for it is creating the art for a game. Illustrating a text adventure seemed like an appropriately humble first outing – as long as I could get pictures that roughly corresponded to my room descriptions, I could use them. There would be no great need to worry about animation or even (with a small enough game) creating consistent characters across different images.
Since my post about Stable Diffusion in August 2022, the tech has advanced in leaps and bounds. In the beginning, it was all about carefully crafting prompts to wring as much as possible out of the Stable Diffusion 1.4 model, but since then there’s been an explosion of custom models for different subjects and art styles, as well as sophisticated tools like ControlNet.
So, armed with a custom model and a few months of prompt engineering experience, I went to work generating illustrations. I wanted one for each location, one for each character, and a few more for specific events. For most of the pictures, I played around with the prompt and random seeds until I got something that looked good, and then fed it into img2img at varying denoising strengths to finetune.
A couple of the pictures started out as crude GIMP sketches, but I have a habit of making these as terrible as possible just so that I can feel astonished when the AI denoises into something good. As a result, the images I liked best from this process were invariably the ones that bore the least resemblance to my initial sketch. Being a bit more intentional with the initial sketch would definitely give me greater control over the final result. Here are some of the final pieces:
Illustrations for a text adventure are pretty low-stakes – the player does not need to physically move around in them and the game is entirely playable without them. Because of the lack of real practical constraints, and the limited amount of time I could (or wanted to) spend on any one image, I tended to be happy once I got something that broadly corresponded to the room or character description, even if it didn’t have all the details I would have wanted. The illustration didn’t need to be something the player scrutinised for gameplay-relevant details, it just had to create an impression. In a couple of cases, I modified the corresponding description to better fit an image I liked, but this didn’t happen too often.
The one case where I wasn’t able to get something I wanted was the interior of a tent in the second part of the game. This was probably the most specifically described room in the game, and I had a clear mental picture of it as being very sparsely furnished. I couldn’t get the AI to play along with this – it kept adding all sorts of junk to the scene, no matter how much I loaded the negative prompt. So in the end I just threw up my hands and generated a nice image of a tent exterior to use (pictured above). I could probably have gotten closer to my original vision with a decent sketch and some time dedicated to inpainting, but I didn’t feel that such a minor room (which the player doesn’t even have to visit) was worth the effort.
I did some minor clean-up and alterations on a couple of images – scrubbing a TV screen and manually adding bars to a prison cell. There were a couple of places where I superimposed a character image on an existing background, and I also used a GIMP filter or two when it was easier to do that than to try get what I wanted out of prompting.
Overall, I’m satisfied with the illustrations I was able to put together in half a day of work, and feel that they add a lot to the game’s mood. In future, I’d like to spend more time with the process and use more and better input sketches. I’d also like to try a more demanding project, such as animated character sprites, or a navigable environment. I know it can be done!
Below, some cool pictures I didn’t end up using:
A lot of other games in this Ludum Dare made use of AI art to varying degrees, from a few posters in Phantom Package to most (all?) of the non-UI art in The Dirty Inbox. There was even a game for which all of the art, text and music was AI generated.
# Release
A nice thing about doing Ludum Dare in 2023 is that most modern gamedev platforms can export to HTML5: Unity, GameMaker, Godot, RPGMaker, Adventure Game Studio, even PyGame all have some way to produce games for browsers. I still had to get the Windows VM out for a few games, but a surprising number of even quite graphically intensive games worked perfectly in my browser. Thank you WebAssembly!
My own entry was already a webpage, so no conversion was needed. Reception was quite positive, although, as mentioned above, there did turn out to be a bug with saving and restoring games, related to some of the interface changes I’d made to Gruescript’s default HTML. But that was easy enough to fix once it was reported to me. Testing is always going to be less than perfectly thorough on a one-man highly time-limited project.
A few commenters remarked on the lack of background music, and I recall getting this feedback last time as well. I did think about adding music and spent a bit of time scrolling through Incompetech, but ultimately it felt like any music I could add would be an afterthought and not really integrated. AI-generated music might have been fun to play with, but I didn’t think of that until literally right now.
After a couple of text games for Ludum, I think I’d like to do something more graphical next time. But then, you never know where the theme is going to take you…
-
There are ways to show hyperlinks in an Inform game, but I’m not sure if you can disable the parser entirely. ↩︎
-
This immediately disqualified me for the 48 hour event, as Ludum Dare has yet to formulate rules about LLM-assisted development and asks that developers keep anything of that nature to the Jam and Extra categories. Use of more primitive AI generation tools is permitted though. I’m hopeful that this will change in future events. ↩︎
-
In proper game jam spirit, I’ve restricted post-release updates to bug and spelling fixes. ↩︎
-
Incidentally, this was the first language I ever tried programming in, sometime in primary school. I wrote many lines of AGT code but was unable to compile them because I didn’t understand file extensions. ↩︎