I can do better, he thought. π€
After a long day of coding, I thought I would unwind and do some leisure programming. I had recently played Disco Elysium which is a text-heavy adventure game where you are an amnesiac detective trying to solve a murder. While I had initially enjoyed the game because it appeared the story could play out differently based on your choices, it turns out your choices didn't really matter. The murder was a random actor and the game left no clues or means for you to solve the mystery through your own actions.
Like all projects, I always think "I can do better". I know very well I will not complete this game or likely spend any more time on it, but its a fun endeavour to run with something for an hour or so before bedtime.
I thought I would share the code and introspective on what I had produced here in this post, instead of dumping this knowledge in the nearby trash.
Defining the Pillars ποΈ
I knew if I wanted to make a truly successful game I needed to define my Pillars of Success.
- Underbelly Every character no matter how seemingly insignificant, through your own actions, should unveil a dark underbelly that contributes to the story. eg. Turns out the shoeshine boy was actually a spy
- Forks The world should change based on key decisions that will remove/add or change characters and locations. We will call this story forking where it's a serious challenge to stay neutral (of course through great means you should be able to stay neutral). eg. Which side do you choose to be on? The axis or the allies?
- Senses Dialogue choices are not your only options here. We want a rich world of interaction.
- Tic-Tock There is a deadline that can be sped up or slow downed based on your actions. The game could end on day 2, 3 or 7. It up to you. You need to maintain something daily or the game could end abruptly. eg. pay your hotel room per day
World Architecting π
Now knowing some of our own high-level rules how can we achieve this?
If we want a rich and forking story with many possible interactions then we need to define the scope of our world and characters.
How many characters?
In Disco Elysium I could count from memory around ~35 characters. I would say only ~12 characters were of any true importance. So we could say here 12/35. I think I would like my game to be more 24/35.
Dividing the world?
In Disco Elysium on your third day, another half of the map was opened to you. I do like the gating of areas in games. What I didn't like about Disco Elysium was this second world was vast and empty. This caused the second half of the game to be a grind of running long distances to talk to characters at opposite ends.
I think for my game I would like to gate the world into 4 areas. I would want an in-game fast travel system eg. Trolly or Elevator. Also, I would want to avoid map design where you have two far points and more of a circular map design where one area always leads to another. You can always make your way back to the center. I guess you could say like the spokes on a bike wheel.
Where are we?
I watch a lot of Star Trek, so dreaming up a Science-Fiction would come naturally. Also, this means I won't have to research in detail the police procedures since this a fiction world I can make up any rules I see fit and give an in-universe explanation.
I want the boundaries of my world well defined so I think a large spaceship would be a well-suited setting. It is also really easy to define all the components (rooms) you'd expect in a starship.
Who are we?
An amnesiac detective. Unoriginal? Yes, but it works so well.
Building the game π§
I first thought I could make a game with Stardew Valley like quality graphics, but at this point what is going to get me most excited is the interactions of characters and rooms.
I was considering making the game entirely like a MUD but I really hate having to type look for x
, describe y
, talk to z
. So I thought it would be better just to present all the choices like a select box.
While coding my select box functionality within my terminal, I could not capture single keypresses with ruby just using gets
which requires you to hit enter after input and so I decided to use the Ruby implementation of Curses
Then I came across this article by chance while trying to figure out how to colour the background of a character. In this article, someone was building an ASCII map with a character (see image below).
And so from that point I thought, I can make my world out of ASCII characters.
Creating the Rooms
I needed an easy way to create the rooms, and so I found this very nice editor which would save me a considerable amount of time.
Building the Game
Since I've built games before I decided to ensure my Classes hold no state because it makes writing test code very painful down the road.
All the data for my game is held within one hash for now.
data = {
running: true,
player: {
position: {
room: 'hall',
x: 5,
y: 5
}
},
rooms: {
hall: {
data: File.read('rooms/hall.txt'),
position: {
x: 0,
y: 1
}
}
}
}
All classes do is preform operations on the passed in data and output which is then stored back in the data hash. So take the up here as an example. When we press left, we pass in the data and assign its output.
ch = getch
case ch
when 'h' # left
x = Player.left data[:player][:position][:x]
data[:player][:position][:x] = x
end
class Player
def self.up y
y-1
end
end
My rooms are all going to be defined by a text file accompanied by metadata about the room possibly a JSON file. So next I need to break up each room and set collision detection and interaction with objects.
So far I can render out a single room, which is centred in my terminal and I can move the player around.
Here's the code if you're interested:
How to run
ruby main.rb
Controls
key | action |
---|---|
h | move left |
j | move down |
k | move up |
l | move right |
f | enter / interact |
m | boost morale |
n | boost health |
i | open inventory |
x | exit popup |
Data Files
data/rooms
Contains rooms data A room is composed of two files, a text files and a json file.
- The json file contains the room data
- The text file contains the room layout
data/threads
inventory.json
Contains every possible object in the game These objects are referenced in strands of threads
Common Curses functions
crmode
Put the terminal into cbreak mode.
setpos(y, x)
A setter for the position of the cursor, using coordinates x and y
addstr
add a string of characters str, to the window and advance cursor
refresh
Refreshes the windows and lines.
getch
Read and returns a character from the window.
close_screen
A program should always call ::close_screen before exiting orβ¦
Top comments (6)
This is awesome! I recently have been learning the Phaser3 game engine, playing around with it and game development as a fun side project/hobby.
I like how you made a unified state hash in your game. You could literally turn that state into a JSON "save file"/store it on a cloud DB/local storage to give the game persistence.
I think I might start pushing all my graveyard games onto open-source repos and make more articles.
I would love to see these, if only to learn from them
Nice! I look forward to seeing your progress.
Oh this is super cool!
The only unfortunate part of building your own story-driven games is you already know how it unfolds.