I like systems that I can hold completely in my head. I like teeny tiny things that are, more or less, totally knowable. This is why I like Forth.
The most useful (perhaps better read as “good for the sorts of things I’m interested in doing” (aka, “not embedded systems programming”)) Forth system that I’ve run across in my adventures is RetroForth. Retro describes itself as:
…not a traditional Forth. Drawing influence from colorForth, it uses prefixes to guide the compiler. From Joy and Factor, it uses quotations (anonymous, nestable functions) and combinators (functions that operate on functions) for much of the stack and flow control. It also adds vocabularies for working with strings, arrays, and other data types.
It runs on a bespoke virtual machine that is implemented in C, Python, Pascal, C#, JavaScript, TypeScript, Nim and Retro itself. This means that Retro is bananas portable.
Best of all, perhaps, if you ask me, is that Retro treats literate programming as the norm! Here is a collection of examples maintained by the mind behind Retro, @crc and here is a little snippet I wrote. They’re all valid Retro programs, complete with human-readable prose.
I’ve probably got another blog blergh in me about why I like literate programming, and what I think the advantages to literate programming are over, say, really heavily commented programs…another day! Here is pbat.ch on the subject of literate programming.
Have I sold you on Retro? If so (I hope so) here is a very quick, naïve tour of the barest of bones text game engine that I’m working on.
First and foremost, this is a derivative work, based heavily on some example code shared with me by @crc on irc.
My goal writing this was to produce a basic starting point for a text-based game. I set out to achieve a few things:
- Define and maintain global state
- Display info about this global state
- Provide a very basic game ui
- Process player input and act accordingly to this input
- Allow a player to exit the game (arguably the most important feature of any game…)
State
My state lives in variables of the shape Player.thing
, e.g. Player.xp
, Player.location
, etc.
I build these variables by iterating over an array of strings, and applying each to my prefix (Player
). This leaves me with a bunch of empty variables.
{ 'xp 'loc 'whoops } [ 'Player.%s s:format var ] a:for-each
Next up, I define initial values for the newly minted variables. This is done by defining a new word that dumps values (here, all integers) into the variables. Note — I’ve only defined the word to do this as this point, I haven’t actually called the word, yet, so the variables are, at this point, all still empty.
I also define a word to display the current game state. This works by putting the current game state, @Player.xp
and @Player.loc
, onto the stack, then a format string (this should be a wee bit familiar to anyone familiar with format strings from C), and then barfing that contents out to stdout
with s:put
, the string-specific equivalent to a more classical Forth system’s .
word.
:state-init
#7 !Player.loc
#0 !Player.whoops
#100 !Player.xp ;
:state-display
@Player.whoops
@Player.xp
@Player.loc
'_LOC:_%n\n__XP:_%n\nUhOh:_%n\n s:format s:put ;
With these three items complete I have what I need for the time being to manage state. First I define some variables to hold my state, then a word to initialize those variables with some values, and finally a word to display the state to the player.
Next I will define what I’ll call my “UX” words — these are the words triggered by player input. They’re all pretty trivial and follow the same pattern — they dump a new line, nl
onto the stack, then a string (anything starting with a single quote, '
with underscores in place of spaces) and print both of those to stdout
. Next, another nl
for cleanliness and then I do something with my global state, either incrementing or decrementing a variable.
:mic-check
nl 'testing_testing_1_2_3! s:put nl &Player.xp v:inc ;
:banana-boat
nl 'banana_boat! s:put nl &Player.xp v:dec ;
:unknown-input
nl '_is_not_a_known_input s:append s:put nl &Player.whoops v:inc ;
The most interesting of my “UX” words is the final one, unknown-input
. It does basically the same thing as the other two, but appends whatever the player input to a predefined string to let the player know that the program doesn’t know what to do with their input…and then it increments our “whoops” counter.
Now that the “UX” words are defined I can put them to work! They’re all going to be triggered by specific player input. If a player inputs a t
I’ll trigger mic-check
, a b
will trigger banana-boat
, q
will trigger the in-built bye
word, exiting the program, and anything else will fall back to the unknown-input
word.
I’ve also defined a hint
word that triggers the state-display
word, defined earlier, as well as displaying a string acting as the game’s ui.
:process-input
't [ mic-check ] s:case
'b [ banana-boat ] s:case
'q [ bye ] s:case
unknown-input ;
:hint
nl state-display nl '(t)est_(b)anana_(q)uit s:put nl nl ;
Game loop
Now in the home stretch, the next step is to define a basic game loop that pulls everything we’ve written together. The core of the game loop, like most game loops, is a while loop. Before the while loop is started, though, I’ve gotta initiate my starting state using state-init
, and displaying a friendly welcome message. That done, I then start the game loop itself. The game loop listens for player input using s:get
(akin to something like io.read
in Lua), triggers a turn (which is a wrapper around the process-input
word) and then returns TRUE
. As long as the game loop continues to return TRUE
the game will march on.
:turn
clear process-input hint ;
:welcome-player
nl 'Welcome!_╰(˙ᗜ˙)੭━☆゚.*・。゚ s:put nl ;
state-init welcome-player hint
[ s:get turn TRUE ] while
That is it! That is a wicked minimal text based game thing!
Here is a link to a cleaned up version of the same code.
Follow up
For more on Forth and Retro, check out these links:
“Hold it!?” you may have shouted — “why the heck should I care about Forth when I’ve got Swift, TypeScript and JAVA!?”
Touche. Those languages are wickedly more powerful and … dare I say … pragmatic than Forth. Even if you don’t plan to write a heap of Forth, not a single line even, I think Forth is worth learning a bit about. Forth is built around the core concept of a stack. Stacks aren’t unique to Forth; they are everywhere. Understanding how to leverage the stack can be really useful. I’ve found my love of Forth has made it easier for me to groke how C and Lua inter-op. When you call a Lua function from C you don’t pass the parameters into the function directly, first you push the Lua function on to the stack, then any parameters that the function takes. Then you call the function and tell C what items from the stack to pop into it. Stacks and stacks!
Another point I wasn’t able to sneak in up above is that most Forth systems (with some exceptions) have very minimal supporting ecosystems. This is a blessing and a curse. A curse because if you want to do a thing you probably have to do that thing fully or near-to-fully on your own. A blessing because there isn’t a package manager or a complicated build tool to get eaten by. Heck! I don’t even use syntax highlighting when I write Forth most of the time.