Silence: A text adventure using the GTI game engine

(rainnw) #1

Description

Silence is an example text adventure leveraging a new multiplatform text game engine I wrote today. The idea was to create a choose your own adventure-style game that could be ran on a variety of platforms – especially microcontrollers with little memory. The adventure game is compiled from a text file script into a compact byte code that can be interpreted by a GTI player for Arduino. GTI stands for Game Text Interpreter.

There is also a GTI player written in Python to test on a much more powerful PC. I may also write one for Javascript or PHP so you can write/test games in your browser.

Instructions

Silence gets silly really quick and its a decent example of what can be done. Like any other text game, you simply read and make a selection at a choice screen. Given the limited screen real estate and buttons, you get two choices to drive the direction of the story.

Writing your own games requires very little Arduino skills, so it might possibly be useful for newbies. Right now the “source code” could be written in excel or text editor (the compiler only accepts CSV’s at this point).

Media


Installation

  1. Download from GitHub: https://github.com/spaceneedle/GameTextInterpreter/tree/master/Players/ArduBoyGTI

  2. Make light modifications to Arduboy library by:

    a. add to arduboy.h: uint8_t getCursorX(); uint8_t getCursorY();

    b. add to arduboy.cpp: uint8_t Arduboy::getCursorX() { return cursor_x; } uint8_t Arduboy::getCursorY() { return cursor_y; }

  3. Upload to Arduboy

The README on GitHub contains some instructions on how to write and compile your own games to byte code.

Here is what the source to silence looks like (SPOILER ALERT): https://github.com/spaceneedle/GameTextInterpreter/blob/master/Example%20Games/silence.src

Version 1.0

Version 1.0 features most basic functions to build a text based adventure game. It needs a little more work to become more powerful and easier to write games for. I hope to add some additional graphics support soon (especially the vector animation mode).

Known Bugs

No text wrapping (might not be worthwhile)
Some characters get clipped during some scenes
Very little error checking when compiling
Game text is in ASCII. ITA2 will result in almost 40% space reduction for even larger text games (right now ITA2 support is too buggy)

10 Likes

(Ross) #2

This is awesome. I love the scripting engine behind it… I mean really love it!

0 Likes

(John) #3

This is absolut great and exactly what we want!
I Love it.

0 Likes

(Kevin) #4

How many full screen pages of text? I think we figured this to be somewhere like 15-20 screens?

0 Likes

#5

How was that calculated?

The Arduboy has 32kb of flash, of which 4kb is reserved for the bootloader. The Arduino and Arduboy libraries chug an additional ~15kb of space, leaving about 13kb for program and data. I don’t think this program should take more than 5kb, which finally leaves 8kb for text.

128x64 display with a 6x8 font is 168 characters per screen, 8kb worth of text with no compression is nearly 50 screens.

1 Like

(rainnw) #6

Right now, the game comes in around 150 screens. The screens are variable length (uses simple null termination), so it can use space efficiently.

The sketch has a base size (out the door with Arduino, Arduboy, and interpreter) of 12,752. You can turn off some of the modules in defines if you dont want sound, serial port, etc. but it will probably not be leaner than 11,680.

This gives you enough room for about 16-17kB of text.

With ITA2, you can store letters and numbers within 5 bits. You do not have the luxury of unique case per character, but given the screen size, this might not be a bad thing to have a game in all caps.

My ITA2 setup works, but my compiler isn’t handling the letters/figures switch properly. I have bigger fish to fry now anyway.

1 Like

(Ross) #7

All you guys had to do was count the lines in the text file : P

0 Likes

(Kevin) #8

That’s amazing! Maybe I was thinking about full screen graphics and not text… oops… Nice work!

0 Likes

(Andrew Dent) #9

I’m interested in how you will read the 5bit characters for ITA2 after packing each character code of 5bits into PROGMEM bytes. You can store the first 3 characters across a Double (taking 15bits). To decode the 4th character, you need to read the last bit of the Double and then the next 4bits of the following Double. Is there an efficient way of doing this? Doing a bitshift operation doesn’t help us if we span a byte boundary. Is there a way around this?
A (conceptually) simple way would be to effectively double buffer: Read PROGMEM -> Store Double / Long (RAM) -> Loop though, splitting out byte aligned chunks (RAM) -> Finally processing output. But this has an undesirably high load for RAM and Cycles…

See also ~ http://wiki.nesdev.com/w/index.php/Fixed_Bit_Length_Encoding
It may not be worth the overhead, but it’s also interesting to consider variable-length coding using a simple LZ engine: http://excamera.com/sphinx/article-compression.html

1 Like

Arduventure (RPG)
(John) #10

What lines need to be changed exactly?
One in each file, or several times?

Perhaps you could offer to download additional files already changed again?

0 Likes

(Josh Goebel) #11

Cycle count is NOT going to be an issue for a TEXT game unless you’re really really really really doing something stupid. :smile:

I was going to write a text adventure and planned on doing sentence compression with a dictionary of words (or pages of words)… so then a sentence goes from:

“The red fox crossed the road.” (30 bytes) to
"1 2 3 4 1 5 ." (7 bytes)

Of course you need the dictionary… but it could be 5 or 6 bit compressed if that mattered. And if you use a lot of common words this very very quickly will start to pay off in huge savings.

0 Likes

(Josh Goebel) #12

To be clear you’d never actually buffer an entire string… you’d have a getCharacterAt(position, *encoded_string) function so you’d just do a for loop calling getCharacterAt() and that would do the appropriate math to fetch whatever character you needed… or you could imagine an IO class that did the same thing but kept track of where it was and had a small read-ahead buffer… but again if you’re building a text based adventure game you aren’t going to need the performance.

How open would you be to changing the format so it was a lot more human readable (and writable)? OTTOMH:

:begin
BOOM! RUMBLE! FLASH!
!lighting
!rumble

An explosion somewhere within the city rocks your apartment! 
A fury of car alarms, flashes of light, screams and yells come from outside your window.
Another explosion rocks the city. This time, it seems closer. 
Your gut instinct is to run down the stairs and into the old fallout shelter.
Your heart is pounding. There isn't a moment to lose.
[stay] Wait it out
[stairs2] Run down stairs
[teddy] Grab your teddy bear.

:stay
!rumble
Another explosion and flash of light knocks you off your feet. Your ears ring.
[stay2] It'll prolly stop
[stairs2] Run down stairs

:stay2
Your apartment building is hit by something and is destroyed. You manage to survive, but you are trapped in rubble.
!jump you_lose

:stairs2
Running down the stairs, you overhear someone with an open apartment door on the phone. "..No please! Stop bombing!"

0 Likes

(Josh Goebel) #13

And then some basic variables and boolean logic would be nice:

Your heart is pounding. There isn't a moment to lose.
:intro_decision
?not has_teddy? 
  You see your teddy bear in the corner of the room.
  [teddy] Grab your teddy bear.
[stay] Wait it out
[pray] Pray to god
[stairs2] Run down stairs
  

:pray
!+ prayed
?prayed<5?
You pray. Is anyone listening?
?prayed>=5?
Really?  There is a time for prayer, but now feels like time for action.
!jump intro_decision

:teddy
!+ has_teddy
You'd never lead home without your teddy bear.
Your heart is still pounding. What now?
!jump intro_decision
1 Like

(Josh Goebel) #14

OK, i2t is super cool but if I were doing this today I’d built my own 5-bit encoding from scratch. This is just off the top of my head:

When in letters mode

0       NULL
1-26    A-Z
27      .
28      Space
29      Shift (single character or shift lock if Shift Shift sent in sequence)
30      Carriage return
31      Figures mode (stateful)

When in figures mode

0       NULL
1       "
2       #
3       $
4       %
5       &
6       '
7       (
8       )
9       +
10      ,
11      -
12      .
13      /
14      0-9
25      :
26      ;
27      !
27      ?
28      Space
29      Carriage return
30      Extended figures mode
31      Letters mode (stateful)

When in extended figures mode…

No idea, but obviously you could have about 30 more characters if you REALLY needed them. 30 and 31 would be reserved for escaping extended figures and the other 29 characters could be additional glyphs.


I was trying to borrow the symbols straight from ASCII (so you could use offsets instead of a mapping table) but it might make more sense to use the most common characters and then push weirder ones to “extended figures”. Less mode switching then at the expense of a little more complex decoding logic.

I left NULL, Space, and CR the same in all modes just for simplicity… This allows a 5 bit encoding to easily represent:

A-Z (uppercase)
a-z (lowercase)
0-9
All common punctuation marks.
Rarer punctuation or characters.

All while keeping a 5.x bits per character baseline. Closer to 5 than 6 if you were encoding mostly text.

0 Likes

Blog post about handling text in our Arduboy game EVADE
(Josh Goebel) #15

I threw some Ruby code together just for the encoding side and ended up with pretty good stats (using Pride and Prejudice txt as the source material):

Before: 692585
After: 453306
Size:  65.5%

That’s pretty close to the theoretical maximum of 62.5% that would be achieved if every character only took up 5 bits instead of having a mode-shifting mechanism. There is room for improvement but only by making the algorithm code more complex.

For example (in this source material at least) some punctuation is more common than some letters… so moving things like quotes, single-quote, comma, semi-colon, and dash into the “letters” table and moving a few letters into the symbols table would shrink things further - getting you a little closer (but not much) to the theoretical maximum.

This is probably about as good as it gets for 5 bit encoding.

0 Likes

(Andrew Dent) #16

Neat! Encoding on PC seemed ok, but I really struggled finding an efficient approach to doing 5bit/char access on the 32u4. Looking forward to seeing your decoder :smile:

0 Likes

(Josh Goebel) #17

It looks I’ll be writing one since I now likely need this on another project… shouldn’t be super hard though… fetching a byte is just calculating the offset and then reading 1-2 bytes and doing the appropriate bit-shifting… doing is sequentially is even easier. To me the 8/5 bit conversion is entirely separate from the encoding. So my encoded builds a long 8 bit string then at the last step smashes it into 5 bits… although actually there is no reason it couldn’t do it simultaneously as both encoder pieces are just working with one byte at a time.

(offset * 5) / 8 to get byte offset
(offset *5) % 8 to get bit offset

After that it’s just bit operations on 1 or 2 bytes to get the value at that offset.

1 Like

(Josh Goebel) #18

The encoding half of things:

Just got to 64.2% my making symbol mode non-sticky, i.e. you get one symbol then you snap back to letter mode (which is what you usually want).

2 Likes

#19

I love text bases adventures!

1 Like

(rainnw) #20

Awesome work! I found my Arduboy again today. Time to start writing more fun things. Now if I can only get the hardware serial port to work.

1 Like