Atari ST Programming: STOS Text Adventure Engine

in Retro Technology4 months ago

Screen Shot 2022-04-12 at 1.59.18 PM.png

While so far in our STOS programming journey we looked at sprites and graphics, it is quite possible to have a fun game experience with only (or mostly) text, so let's take a look at that.

In Defense of Text Adventures

Why text adventures? Why would we go from sprites to text?

Firstly, as this is a programming tutorial, it allows us to introduce some extremely useful programming concepts:

  • Arrays and pre-defined data
  • Object definition and tracking
  • Parsing strings for textual input
  • Subroutines and modular coding

Second, knowing how to build an adventure game focuses us on game development ideas that make many other game genres better, such as world building, narrative, puzzles, player agency, and so on.

Data Structures

Beyond the granular numbers, characters/bytes, strings, and so on, we often need to create more macro data concepts for our programs.

Many BASIC programming languages are based off of work that Microsoft did way back in the 1970s, who were in turn inspired by BASIC interpreters they were used to.

This means for us today that BASIC very often has an array-centric view of the world. While not a terrible hinderance in fact, for a modern developer it can be quite an adjustment.

An array is simply an ordered list of data with a numerical index. Consider it for most purposes as a pointer to a space that has been reserved in memory.

If we create an array that has ten slots, then we can later refer back to see what slot 5 currently contains.

This is not as convenient as having an object class with properties, and often means we need multiple arrays to represent one game item, but it does the job well enough.

So in my planning I will come up with several arrays to represent different data needs, such as room layout, room description, room contents, and so on.

We first dimension an array with a fixed size:

dim list$(100)

Then we can set the individual array entries, eg.:

list$(0)="foo"

Yes, well spotted, STOS arrays start at index 0, which confusingly is not always the case, even in the Amiga version of STOS, AMOS.

Once we have set our value, we can recall it in a similar fashion:

print list$(0)

A cool thing about STOS arrays is you can have multiple dimensions, think of it as being able to create a table or spreadsheet instead of a simple list:

dim grid$(10,10)

We will use this concept to create our room layout, with 100 total cells.

Why a grid for rooms? I have used a couple of ways of planning floor layouts with text adventures, and chose this method because I want to have a monster that moves from room to room and hunts you (as opposed to bad guys who live in a specific room and "wake up" on your entering said room). Being able to set a monster position using X/Y coordinates makes that easier.

At the same time, I am using room numbers. This is so I can have a description for room 50, as well as set the contents of that room, and even things like if that room is lit or not.

We will get to all that in future iterations of the code, at the moment we are working on the absolute basics.

Interpreting Instructions with String Manipulation

Right now as I type this I could use any number of hundreds of natural language processing systems, not just able to understand what I type, but understand my spoken voice too.

But we don't need anything like that, even if anything close could be implemented on an air-gapped Atari ST from the 1980s.

We need just a limited vocabulary, and only a very basic sentence structure.

In fact, we don't need to translate entire commands, if we are smart the list of commands can be checked against just the first letter of the command word.

To get input we use, naturally, the input command and put the result into a variable. For matching against our list of instructions we grab the first letter, and also convert to uppercase to save having to check both upper and lower versions.

input CMD$ : IN$=upper$(left$(CMD$,1))

For directions we can simply check the compass letters, N, E, S, W, and this will allow the player to enter "north" or "N" and still go where they need to go.

Before the player is moved, we check that direction is available by looking to our room layout grid for a non-zero result. Our rooms are using strings so to check for a numeric comparison we must use val to get the mathematical value of the string.

100 print "Command"; : input CMD$ : IN$=upper$(left$(CMD$,1))
101 sp=instr(CMD$," ") : rem Space between words
102 obj$=right$(CMD$,len(CMD$)-sp) : rem Specified Object
110 if IN$="N" and val(ROOMS$(X,Y-1))>0 then Y=Y-1
120 if IN$="E" and val(ROOMS$(X+1,Y))>0 then X=X+1
130 if IN$="W" and val(ROOMS$(X-1,Y))>0 then X=X-1
140 if IN$="S" and val(ROOMS$(X,Y+1))>0 then Y=Y+1
150 if IN$="G" then gosub 2000 : rem get
160 if IN$="U" then gosub 3000 : rem use
170 if IN$="D" then gosub 4000 : rem drop
180 if IN$="K" then gosub 5000 : rem kill
190 if IN$="Q" then print "Goodbye!" : end 
200 goto 50

For interacting with objects, we will need both the command and the object, separately. To do this we find the first occurrence of a space character then get the portion of the string that is from that character to the end of the string. Unless the player types something weird, that should be our object name, even of the object name has a space in it.

Rather than try to fit all the code for dealing with those object commands right in the if statement, instead we use the GOSUB command which allows us to execute another part of the program and then come back to continue where we left off. This splits our code into much more manageable chunks.

Rooms, Navigation & Foundational Code

This has already run long so let's take a look at where we are at, then develop more features in another post.

Our code here will allow us to navigate between rooms (doesn't currently check for locked doors, of course) and it displays any room descriptions (of which there are only a few).

Rather than specify lots of ROOM$(10)="Something", we use the DATA and READ commands. Each time you read you pull the next value from the next data, which allows you to have long, arbitrary lists of values in your code. The data statements can be anywhere in the code, but just have to line up with what you are expecting to read, IE. you can't read a string into a numeric variable.

For us this means we can go through and add the room descriptions all in one place rather than lots of variable declarations or store them on disk as a file.

05 rem Adventure boilerplate by Chris Garrett @retrogamecoders
06 rem =======================================================
07 :
10 mode 1 : key off : cls
20 dim ROOMS$(40,40) : dim ROOMDESC$(100) 
30 for Y=0 to 9 : for X=0 to 9 : read R : ROOMS$(X,Y)=str$(R) : next X : next Y
40 X=4 : Y=5 : gosub 10000 : rem initialize start room
45 rem ========================================
46 rem USER INPUT
47 rem ========================================
50 cls : locate 0,0 : room = (y * 10) + x : inverse on: print roomdesc$(room): inverse off: print "Available Exits:"
60 if val(ROOMS$(X,Y-1))>0 then print "> North"
70 if val(ROOMS$(X+1,Y))>0 then print "> East"
80 if val(ROOMS$(X-1,Y))>0 then print "> West"
90 if val(ROOMS$(X,Y+1))>0 then print "> South"
100 print "Command"; : input CMD$ : IN$=upper$(left$(CMD$,1))
101 sp=instr(CMD$," ") : rem Space between words
102 obj$=right$(CMD$,len(CMD$)-sp) : rem Specified Object
110 if IN$="N" and val(ROOMS$(X,Y-1))>0 then Y=Y-1
120 if IN$="E" and val(ROOMS$(X+1,Y))>0 then X=X+1
130 if IN$="W" and val(ROOMS$(X-1,Y))>0 then X=X-1
140 if IN$="S" and val(ROOMS$(X,Y+1))>0 then Y=Y+1
150 if IN$="G" then gosub 2000 : rem get
160 if IN$="U" then gosub 3000 : rem use
170 if IN$="D" then gosub 4000 : rem drop
180 if IN$="K" then gosub 5000 : rem kill
190 if IN$="Q" then print "Goodbye!" : end 
200 goto 50
900 :
1000 rem ROOM LAYOUT
1010 data 0,0,0,0,0,0,0,0,0,0
1020 data 0,1,0,0,1,0,0,1,0,0
1030 data 0,1,0,0,1,0,0,1,0,0
1040 data 0,1,1,1,1,1,1,1,0,0
1050 data 0,0,0,1,1,1,0,1,0,0
1060 data 0,0,0,1,1,0,0,1,0,0
1070 data 0,0,0,0,0,0,0,1,0,0
1080 data 0,0,0,0,0,0,1,1,0,0
1090 data 0,0,0,0,0,0,0,0,0,0
1100 data 0,0,0,0,0,0,0,0,0,0
2000 :
2010 rem GET OBJECT
2020 print obj$
2040 wait key
2900 return
3000 :
3010 rem USE OBJECT
3020 print obj$
3040 wait key
3900 return
4000 :
4010 rem DROP OBJECT
4020 print obj$
4040 wait key
4900 return
5000 :
5010 rem KILL CHARACTER
5020 print obj$
5040 wait key
5900 return
10000 :
10010 rem ROOM DATA
10020 for r=0 to 100
10021 read desc$ 
10022 roomdesc$(r)=desc$
10023 next r
10024 return
10100 data " "
10101 data " "
10102 data " "
10103 data " "
10104 data " "
10105 data " "
10106 data " "
10107 data " "
10108 data " "
10109 data " "
10111 data " "
10112 data " "
10113 data " "
10114 data " "
10115 data " "
10116 data " "
10117 data " "
10118 data " "
10119 data " "
10120 data " "
10121 data " "
10122 data " "
10123 data " "
10124 data " "
10125 data " "
10126 data " "
10127 data " "
10128 data " "
10129 data " "
10130 data " "
10131 data " "
10132 data " "
10133 data " "
10134 data " "
10135 data " "
10136 data " "
10137 data " "
10138 data " "
10139 data " "
10140 data " "
10141 data " "
10142 data " "
10143 data " "
10144 data " "
10145 data "Room 45 "
10146 data " "
10147 data " "
10148 data " "
10149 data " "
10150 data " "
10151 data " "
10152 data " "
10153 data " "
10154 data "West Room "
10155 data "Start room"
10156 data "East Room "
10157 data " "
10158 data " "
10159 data " "
10160 data " "
10161 data " "
10162 data " "
10163 data " "
10164 data " "
10165 data "Room 65 "
10166 data " "
10167 data " "
10168 data " "
10169 data " "
10170 data " "
10171 data " "
10172 data " "
10173 data " "
10174 data " "
10175 data " "
10176 data " "
10177 data " "
10178 data " "
10179 data " "
10180 data " "
10181 data " "
10182 data " "
10183 data " "
10184 data " "
10185 data " "
10186 data " "
10187 data " "
10188 data " "
10189 data " "
10190 data " "
10191 data " "
10192 data " "
10193 data " "
10194 data " "
10195 data " "
10196 data " "
10197 data " "
10198 data " "
10199 data " "
10200 data " "
10201 data " "
10202 data " "
Sort:  

I wrote one of these for both my C64 and Amiga 500. Writing a parser was great fun. I learnt so much from that.