So far in our Atari ST text adventure using STOS, we have been loading test data via inline data statements. There are some advantages to that, which we can discuss, but also some frustrations so let's take a look at moving our data outside of the code so we can work on data and the game itself separately.
Pros and Cons of External Data
As software gets more complicated, you will find programmers lean toward "separation" - ie. not having everything all in one big file.
So we will use libraries, will split the front-end from the back-end, have modules for UI, logic and data, and so on.
Not only does it simplify things, but it also allows us or someone else to work on, say, level/map creation while the coding happens in parallel. In theory, I could code up this game and then other people could take the code and build their own adventures by just changing the data files ...
For us retro-focused folks there is another major benefit - having data load from files means that the game does not have to all load into memory at once, and the game could just load data across many disks as necessary. In fact, lots of games did exactly that back in the day.
So why would you include the data right in your code as we have been doing up to now?
The first reason is portability, in that everything gets compiled together so the data is all there with the game binary in the deliverable end result.
Secondly, loading from floppy disk can be slow, and loading from tape extremely slow, therefore a lot of coders back in the day would aim to have the whole thing load into memory to prevent having to put their players through longer load times or disk-swapping.
For the coder, initially scrolling down in the listing and adding a value can be easier than having to work on multiple files and build into the code the file handling logic.
Clearly, there is no wrong or right answer, so it's best to learn both approaches and then take things from there.
The Atari ST File System
One of the many things that made the ST stand out once people got their hands on one was the "almost DOS-compatible" TOS disk operating system. 3 1/2" floppies formatted on a PC can be read on an Atari ST, but to do the reverse required special software initially.
This CP/M and almost-DOS compatible heritage meant that files are named with 8.3 characters and there is a file allocation table.
Most home gamers would have had just the included, 360k single-sided floppy drive back in the mid-1980s, but prosumers and professionals would have been much more productive with the "massive" 20mb+ hard drives and of course external floppy drives. While later models had double-sided floppy drives, much of the media that was released stuck to the more common single-sided format to keep everyone happy, even if that meant having to include more disks.
For us as programmers, this means that if you want your game to run on original hardware or the most strict of modern emulators, then your game will need to fit comfortably on one or more 360kb disks.
STOS File Functions
As in DOS, STOS provides a
DIR command to get a directory listing, but unlike DOS you use a variable,
DIR$, to specify the current directory.
DIR you can also optionally specify the path and a switch for wide view (
This only displays the results onscreen, however. In our code we would instead using
DIR FIRST$ and then
DIR NEXT$ until an empty string is returned:
10 rem disk 20 FILENAME$=dir first$("c:\*.asc",-1) 30 print FILENAME$ 40 while FILENAME$<>"" 50 FILENAME$=dir next$ 60 print FILENAME$ 70 wend
As well as the path and wildcard parameter, you must also specify a flag for the type of file to return:
|0||Normal Read/Write files|
For file system housekeeping there are also the following commands:
|DFREE||Disk space remaining|
|MKDIR||Make a folder|
|RMDIR||Delete a folder|
|KILL||Delete a file|
|RENAME||Rename a file|
There are two types of file that we would concern ourselves with in STOS, regular sequential text files and random access files.
If you want to edit your files in a normal text editor then you would use the former, while the latter have the advantage that you can access a single record of data without having to load and iterate through all the data to get to the part that you want.
In our adventure game we have a list of rooms with (currently) a room name for each. Eventually we will expand this to a room name and also a description.
This is a good case for using text files, which can be easily loaded in like so:
10 dim ROOMDESC$(100) 20 print "rooms" 30 open in #1,"rooms.asc" 40 for r=0 to 99 50 input #1, desc$ : roomdesc$(r)=desc$ 60 print desc$ 70 next r 80 close #1
Checking for End of File
We open a file and assign it the number #1, then read a line using
input #1, specifying we want the result to go into our
desc$ string variable.
This is fine when we know there will be for sure exactly the correct number of rows in the file, but it would error if there are too few.
An alternative that is a little bit more forgiving is to check for
eof(1), aka End of File.
You can use this in a
REPEAT UNTIL depending on where you want your check to take place.
This is fine when your data is a whole line of text, but what if it is more structured?
After getting the whole line into our string variable we could then work on splitting it up into component parts, but STOS actually makes it very straightforward.
Our room layout map is a comma-delimited grid, and that can be simply loaded like thus:
input #1, ROOMS$(0,Y), ROOMS$(1,Y), ROOMS$(2,Y), ROOMS$(3,Y), ROOMS$(4,Y), ROOMS$(5,Y), ROOMS$(6,Y), ROOMS$(7,Y), ROOMS$(8,Y), ROOMS$(9,Y)
There is also a
LINE INPUT command that allows you to specify a different delimiter for your data.
Full Listing for Part 3
10 rem Adventure boilerplate by Chris Garrett @retrogamecoders 20 rem ======================================================= 30 : 40 rem init game 50 mode 1 : key off : cls 60 dim ROOMS$(10,10) : dim ROOMDESC$(100) : dim OBJECTS$(100,100) 70 gosub 890 80 : 90 rem initialize start room 100 X=5 : Y=5 110 : 120 rem ======================================== 130 rem USER INPUT 140 rem ======================================== 150 rem @usercommands 160 cls : locate 0,0 : room = (y * 10) + x : print "room: ",room: inverse on: print roomdesc$(room): inverse off: print "Available Exits:" 170 if val(ROOMS$(X,Y-1))>0 then print "> North" 180 if val(ROOMS$(X+1,Y))>0 then print "> East" 190 if val(ROOMS$(X-1,Y))>0 then print "> West" 200 if val(ROOMS$(X,Y+1))>0 then print "> South" 210 gosub 800 220 print "Command"; : input CMD$ : IN$=upper$(left$(CMD$,1)) 230 sp=instr(CMD$," ") : rem Space between words 240 obj$=right$(CMD$,len(CMD$)-sp) : rem Specified Object 250 if IN$="N" and val(ROOMS$(X,Y-1))>0 then Y=Y-1 260 if IN$="E" and val(ROOMS$(X+1,Y))>0 then X=X+1 270 if IN$="W" and val(ROOMS$(X-1,Y))>0 then X=X-1 280 if IN$="S" and val(ROOMS$(X,Y+1))>0 then Y=Y+1 290 if IN$="G" then gosub 370 : rem get 300 if IN$="U" then gosub 480 : rem use 310 if IN$="D" then gosub 540 : rem drop 320 if IN$="A" then gosub 650 : rem attack 330 if IN$="I" then gosub 710 : rem inventory 340 if IN$="Q" then print "Goodbye!" : end 350 goto 150 360 : 370 rem @getobject 380 rem GET OBJECT 390 ofound=0 400 for ob=0 to 99 410 if upper$(objects$(ob,0))=upper$(obj$) then ofound=ob : objects$(ofound,1)="0" : rem zero = inventory 420 next ob 430 if ofound > 0 then print "You picked up: "+objects$(ofound,0) 440 if ofound = 0 then print obj$ + " not found" 450 wait key 460 return 470 : 480 rem @useobject 490 rem USE OBJECT user action 500 print obj$ 510 wait key 520 return 530 : 540 rem @dropobject 550 rem DROP OBJECT user action 560 ofound=0 570 for ob=0 to 99 580 if upper$(objects$(ob,0))=upper$(obj$) then ofound=ob : objects$(ofound,1)=str$(room) : rem set to current room 590 next ob 600 if ofound > 0 then print "You dropped: "+objects$(ofound,0) 610 if ofound = 0 then print obj$ + " not found in your carried items" 620 wait key 630 return 640 : 650 rem @attack 660 rem ATTACK user action 670 print obj$ 680 wait key 690 return 700 : 710 rem @inventory 720 rem INVENTORY user action 730 print "INVENTORY:" 740 for ob=1 to 100 750 if objects$(ob,1)="0" then print " . " + objects$(ob,0) 760 next ob 770 wait key 780 return 790 : 800 rem @objectsinroom 810 rem LOOKUP OBJECTS IN ROOM 820 objinroom$="" 830 for o=1 to 100 840 if val(OBJECTS$(o,1))=room then objinroom$=objinroom$ + " " + OBJECTS$(o,0) 850 next o 860 if objinroom$<>"" then print "OBJECTS VISIBLE:"+objinroom$ 870 return 880 : 890 rem @loaddata 900 rem LOAD GAME DATA FROM FILE 910 locate 0,0 920 print "Starting, please wait" 930 : 940 rem MAP DATA 950 print "loading map" 960 open in #1,"map.asc" 970 for Y=0 to 9 980 input #1, ROOMS$(0,Y), ROOMS$(1,Y), ROOMS$(2,Y), ROOMS$(3,Y), ROOMS$(4,Y), ROOMS$(5,Y), ROOMS$(6,Y), ROOMS$(7,Y), ROOMS$(8,Y), ROOMS$(9,Y) 990 next Y 1000 close #1 1010 : 1020 rem ROOM DATA 1030 print "loading rooms" 1040 open in #1,"rooms.asc" 1050 rem room zero is the inventory 1060 for r=1 to 100 1070 input #1, desc$ : roomdesc$(r)=desc$ 1080 next r 1090 close #1 1100 : 1110 rem OBJECTS in ROOMS 1120 print "loading objects" 1130 open in #1,"objects.asc" 1140 r=1 1150 repeat 1160 input #1, obj$,roomno$ 1170 objects$(r,0)=obj$ : objects$(r,1)=roomno$ 1180 inc r 1190 until eof(1) 1200 close #1 1210 : 1220 return 1230 : 1240 :