DEV Community

David Ballester Mena
David Ballester Mena

Posted on

A postmortem of A dirty trail, a text based game

A dirty trail is a choose-your-own-adventure kind of game, with simple mechanics and a turn-based combat system.

In this post I'll tell you how it came to be, what the development was like and what I've learned from it.

What is it?

A couple of screenshots are probably more explanatory than anything I can tell you. And you can also visit adirtytrail.com and give it a try!

Narration Combat
Narration screenshot Combat screenshot

It's a web game in which you'll make decisions that'll affect the rest of the narration and lead you towards one of the possible endings.

But this is not about the story itself, but the engine that powers it.

How it began

I had something very different in mind when I started hacking away in VS Code. My intention was to create a procedural narrative.

I'm a gamer myself, and I've seen the raise of all these open world games in which you face a scenario and just, well, move through it without any real purpose. I don't really enjoy these kind of games because I need a narrative to keep me engaged.

So what I thought about was the possibility of generating a narrative as you go, dynamically. Ambitious, I know! So much that look where I ended up.

That kind of challenge is huge, and although I have not completely given up on it, I decided to settle for something affordable in a shorter period of time.

What it started with plain JS and a bunch of libraries for natural language processing turned into a pure Typescript project, heavily object-oriented with mechanics built from scratch that evolved from the initial idea to what it ended up being.

Architecture

Technologically, a dirty trail is quite simple.

The engine has its own repository, and there is another one for the webapp.

The engine is pure Typescript. Its behavior revolves around the concept of a scene, that is, a piece of text with actions related to it, and for the combat scenes, some enemies.

Scenes are written in Markdown, and I've used Handlebars for the text generation, so based on the current status of the narration, the author (that is, me!) can include tags to represent things. This is an example of the text part of a scene written in Markdown.

The road climbed up, and the higher they got, the colder the air was. The fog came over them, and it started to drizzle.

"This could not get worse," {{playerName}} said under her breath.

"Never say something like that!" the minister protested. "Do you want Him to prove you wrong?" and at this, he pointed at the sky, which was heavy with clouds.

"Those are not appropriate things to say for a minister," Lady Willsbourgh said. She could hardly speak, breathless as she was from carrying so many things.

The two of them kept discussing for a while, but {{playerName}} stopped, redid her blouse, and breathed deep.

Something in the woods caught her attention.
Enter fullscreen mode Exit fullscreen mode

The metadata part of the Markdown files is where the scene info is, such as its title and possible actions from it. Here's an example:

id: theRoad_theWalk
title: The road
actions:
    She tried to focus on it (_perception_):
        check:
            skill: perception
            success:
                nextSceneId: theRoad_somethingInTheWoods
            failure:
                nextSceneId: theRoad_nothingInTheWoods
Enter fullscreen mode Exit fullscreen mode

The core library is covered by unit tests, and the templating language used to describe scenes through the metadada of the Markdown files is described by a JSON schema file generated by typescript-json-schema. You'll see the usage of that in a second!

Now, the webapp itself is a Next.js app. Why Next.js? Because it's an awesome framework I don't want to keep in touch with! Here, the core library is imported as a dependency.

In this webapp I have written the narrative (I intend to add multiple ones, but, for now, I only have one). Each scene I split in two separate files: an .md one for the text and a .yaml for the metadata. The reason? I've used the YAML VS code extension to have intellisense for my data model, and VS Code doesn't allow intellisense for injected languages, such as the yaml portion of the Markdown file. If you want to know more about this, check out this GitHub thread.

With this in mind, I've created several scripts that help me with building the final Markdown files served via the /public folder of my Next.js application, as well as for testing purposes.

Because testing on this side of the project is lacking. I think it's too complex to do it properly via unit tests, or maybe a happy idea hasn't popped up in my brain yet, so for now I've settled with a couple of scripts and a basic unit test.

The unit test verifies the integrity of the generated scenes. One of the scripts generates a report with the amount of scenes and words in a narration, as well as the ending scenes so I can know if I have inavertedly created a scene from which the player cannot progress. The final script generates a graph of the whole narrative in a format compatible with grapher.tech another project of mine for displaying and editing graphs.

A sample of how grapher renders the A dirty trail narration
This is what grapher looks like.

Some other technologies

For the styling, I used react-bootstrap for the sole reason that I felt nostalgic about the good old days in which all apps seemed to use Bootstrap, and wanted to catch up with the latest version of it.

In retrospective, I think I should have given Tailwind a try. I've heard many good things about it and this app feels like a good fit for it.

For the hosting, I've gone with Netlify. I think their service is awesome and, so far, I don't have a single complain about it.

The only thing I found out was that I had to clean the cache for every deploy in which I had updated the core dependency. But, come one, that's nothing compared to what it offers!

On react-spring

I've used react-spring for the animations, on which I rely heavily. I know it is the most beloved animations library for React, but it didn't click for me, so I tried to look for something else. Then I realized react-spring was the best by far, which saddened me because I didn't enjoy using it! But okay, that's on me.

Now I found that v9 had a great API, so I went for it, even though it wasn't released yet, and built the whole app using it (I know). Then, when it was time to deploy the first version, the build wouldn't work!

I spent a lot of time debugging and found out it was because of an issue with react-spring v9. (I know!)

Reverting back to v8 was too painful, so I decided to patch things up myself, and ended up doing a mix of some of the solutions proposed in the GitHub thread about it. Ugly, but it got me going.

What I learned

Bullet points time!

  • Starting without a clear idea and going with the flow until it crystalizes into something you can actually do takes a lot of time. I think that, if I had the idea of what A dirty trail would end up being from the beginning, it would have required less than half the time I've spent on it.
  • Object-oriented comes naturally to me when creating a data model, but I think a functional approach would have benefitted the core engine and made it more extensible.
  • Testing is hard and sometimes I sighed when I found myself writing repetitive tests. After all, this is my free time and I'm digging through Jest docs! Bummer! But it is completely worth it. Add Typescript to the equation and refactoring is a breeze. And I'm all about refactoring.
  • Doing TDD still does not work for me. Specially when I don't have a clear idea. I need to tinker around before I can know how my API is going to look like, and it does not make sense to write a single test before knowing your API. So I'm 100% TDR: test-driven refactoring.
  • When you leave behind something that doesn't feel good, trust your instinct. It'll come around and bite you later one! To bypass an issue with a hacky solution is fine if you come back to it tomorrow and think it through again. Otherwise, it'll rot the foundations of what you build on top of it.
  • Use Trello to keep track of things when your project starts getting big! I didn't use it for a while, but once things got serious, I found it perfect to write down all the things I wanted to do in the future and didn't want to forget about.

Roadmap

I've had enough of A dirty trail for a while, but I intend to keep adding stuff to it in the future:

  • Internationalization.
  • Keep working on accessibility.
  • More narrations.
  • Dark mode.
  • Refactor the templating system (it doesn't feel right!).
  • Add more mechanics.

Thanks!

Thanks for having read through this kind of post mortem of mine! If you feel like it, give A dirty trail a chance and share your thoughts about it.

Thanks!

Top comments (0)