My foray into programming began with GW-BASIC on a Tandy 1000, an 8-bit IBM PC clone. In those days, one had only a few avenues of learning for coding knowledge: physical books and more experienced friends (who had probably read physical books). I spent many weekends poring over the Tandy 1000 BASIC Reference Manual, only half-understanding it. Eventually I got to the point where I could write simple text-driven “games.” (I have a vague memory of working on a math quiz program which randomly generated arithmetic problems and displayed snarky messages if you got a wrong answer.) Those were the days! GOTO was not only encouraged but the only reasonable way to handle many common situations (e.g. “subroutines”). It was a simpler time, indeed.
(Nostalgia aside, BASIC was actually quite difficult to work with. BASIC programs tended to be impervious to modularization. It was not uncommon to see the same calculations and code snippets littered throughout a program. One needed an almost unreasonable degree of cleverness to reduce duplication, such as writing BASIC programs to generate other BASIC programs. Results were rarely reusable as such given the boutique nature of these systems — hand-crafted and built to solve exactly one problem in one way. Okay, back to the past….)
At the same time I was learning GW-BASIC, I was playing PC adventure games. In that era, Sierra was the dominant player in the adventuring space, producing text-driven titles which also included graphics and sound! It was only natural that I would want to try to write my own similar game — text-only of course, given my skill level. As luck would have it, my school library had a copy of Christopher Lampton‘s How to Create Adventure Games. I checked it out and tried to digest the lessons within, but I don’t think I really “got” it beyond the level of being able to type in and run the included program listing. (Troy Gilbert writes about his experience with this book, though it seems he actually understood its contents quite a bit better.)
Having been in a reminiscing mood lately, I discovered a PDF copy of the book. Armed with simple OCR (still with a lot of hand-correcting of I’s, 1’s, O’s, and 0’s) and the excellent QB64, I was able to play “The Quest” on a modern PC. More importantly, I now actually understand the material in the book well enough to do the next logical(?) thing — create a GW-BASIC to C# translation!
First, we need a strategy on how to represent BASIC concepts within higher-level OO scaffolding. We can start by examining the program to see which keywords and constructs are used; note that my intent is not for truly general purpose translation, but one that first works with “The Quest.” This table shows the analysis, along with a possible C# representation for each item:
GW-BASIC construct | Translation in C# |
---|---|
The program as a whole | Generate a class with a Run method. The class should define the necessary GW-BASIC intrinsic functions as private static methods and bookkeeping data (e.g. DATA array) as private fields. |
Line numbers | Remove the line number if it is not referenced by a GOSUB/GOTO. Otherwise, use labeled statements by prefixing the number with ‘L’ (since labels must follow identifier rules). |
Comments (REM ) |
Directly translate to single-line comments. |
Multiple statements on one line (separated by : ) |
Translate to multiple single-line statements. |
Clearing the screen (CLS ) |
Console.Clear |
Allocating/assigning variables and arrays of one or more dimensions, using single precision and string types (e.g. DIM A(10) : A = 1 : A$ = "a" : DIM A$(5) ) |
Use float and string instance variables and arrays with a name suffix to distinguish them, given variables of different types can share the same names (e.g. this.A_1a = new float[10]; this.A_1 = 1f; this.A_s = "a"; this.A_sa = new string[5]; ). |
Printing text | Console.Write or Console.WriteLine depending on line breaks |
Calling subroutines (GOSUB ) |
Generate a new instance method for each subroutine; assume that it begins at the line number mentioned in the corresponding GOSUB and ends at the last RETURN not part of another subroutine. In GW-BASIC, subroutines do not have return values, so use the value instead to determine if the program should exit. |
Reading input (INPUT ) |
Console.ReadLine |
Jumps (GOTO ) |
Directly translate, C# supports goto ! |
Conditionals (IF ... THEN ... ) |
Directly translate to if statements, parsing out the equivalent Boolean expression. |
Arithmetic expressions and operators (e.g. A=A+1 ) |
Directly translate. |
Boolean operators (e.g. A=1 AND B=2 ) |
Directly translate. |
String functions (e.g. LEN , LEFT$ , etc.) |
Translate to equivalent string methods and properties (e.g. string.Length , string.Substring() , etc.). |
Loops (FOR ... NEXT ) |
Translate to equivalent for block. Since variables in GW-BASIC are program-scoped, use an instance index variable (for (this.i = 0; ... ) rather than a local (for (int i = 0; ... ). |
Declaring numeric and string constants (e.g. DATA 1,a string,2,"Hello, world!" ) and reading them (e.g. READ A,B$(I),C,B$(I+1) |
Translate declared data to a private list of data fields. Keep track of a read index to determine the next data value. |
Restarting the program (RUN ) |
Use an outer while loop for the program and translate to return true . (Be sure to reset the read index for data!) |
Exiting the program (END ) |
Translate to return false , indicating the program should exit. This ensures that even if END appears inside a subroutine, the right thing will happen. |
Whew! Even in this relatively small program (a little over 300 lines of GW-BASIC), there are dozens of language elements we have to handle. Additionally, we have to translate this code into C# somehow and have yet to decide on a technology. Let’s address that now.
If we were living in the year 2012, we might have chosen CodeDOM. However, the modern era demands a modern approach, so off to the .NET Compiler Platform (aka “Roslyn”) we go! (One can install it via VS 2017, minding the the instructions here to get started.)
Making a program that can translate all the language elements above is going to take some time. So let’s first build an MVP. I will choose the classic beginner’s program:
10 REM My first BASIC program 20 PRINT "HELLO, WORLD!" 30 GOTO 20
When translated into C# source code, it should look like this:
using System; internal sealed class hello { public void Run() { while (this.Main()) { } } private static void PRINT(string line) { Console.WriteLine(line); } private bool Main() { // My first BASIC program L20: ; PRINT(""HELLO, WORLD!""); goto L20; return false; } }
When executed, it should print…
HELLO, WORLD! HELLO, WORLD! HELLO, WORLD! . . .
…until you get tired of it and press CTRL+BREAK.
Indeed, after only a little bit of coding, one could build just such an app: check out AdventureSample on GitHub.
There’s a lot more to do here, so stay tuned! You’ll be seeing more BASIC goodness in future posts.
Pingback: BASIC complications: parser combinators – WriteAsync .NET
Pingback: BASIC solutions: statements – WriteAsync .NET
Pingback: Building an adventure game: part 1 – WriteAsync .NET