Blasting from the past
Tuesday, August 5th, 2008Someone with far too much time on their hands has ported the original BASIC version of StarTrek to C#
May God have mercy on us all.
Ah, good old line-numbered BASIC. It’s all coming back to me now. Those line numbers were there to provide targets for GOTO and GOSUB statements. But, line numbers made editing a tad difficult. It was convention to enter in line numbers that were multiples of 10. That way, as you developed the program, you could go back and insert up to 9 additional statements in between existing lines without reworking all the GOTO/GOSUB references.
Oh bloody hell, I remember that, in fact I was thinking about it the other day, and not in a nostalgic way. Now machine code, thats nostalgia, though I guess line numbers were a natural successor in a way, like flexible addressing, hmmmm … I better stop or I’ll make it sound good.
due to the caps, I feel like the code is screaming at me
Ha ha, yeah …. things sure have changed …
Next, I had to decide what language to port it to. Staring at that BASIC code reminded me that C# brought goto back into the mainstream.
So it’s C#’s fault, eh ? I better move swiftly on before I start quoting Dijkstra, may God have mercy on us all.
Would it be possible to do an exact line-by-line port from BASIC to C#? Apparently so… and the result is some of the sickest code I’ve ever keyed into a computer. Want a comparison? Here’s a segment of BASIC code:
2950 PRINT "TORPEDO TRACK:"
2960 LET X=X+X[1]
2970 LET Y=Y+X[2]
2980 IF X<.5 OR X >= 8.5 OR Y<.5 OR Y >= 8.5 THEN 3420
2990 LET V[4]=X
2991 LET V[5]=Y
2992 GOSUB 9000
2993 PRINT
3020 IF A[INT(X+.5),INT(Y+.5)]#0 THEN 3080
3060 GOTO 2960
3080 IF A[INT(X+.5),INT(Y+.5)]#2 THEN 3230
3120 PRINT “*** KLINGON DESTROYED ***”
3130 LET P[1]=P[1]-1
3140 LET P[3]=P[3]-1
3150 IF P[3] <= 0 THEN 4040
3160 FOR I=1 TO 3
3170 IF INT(X+.5)#K[I,1] THEN 3190
3180 IF INT(Y+.5)=K[I,2] THEN 3200
3190 NEXT I
3200 LET K[I,3]=0
3210 GOTO 3370
3230 IF A[INT(X+.5),INT(Y+.5)]#4 THEN 3290
3270 PRINT "YOU CAN'T DESTROY STARS SILLY"
3280 GOTO 3420
And the C# version:
_2950: Console.WriteLine("TORPEDO TRACK:");
_2960: X = X + _X[1];
_2970: Y = Y + _X[2];
_2980: if (X < .5 || X >= 8.5 || Y < .5 || Y >= 8.5) goto _3420;
_2990: _V[4] = X;
_2991: _V[5] = Y;
_2992: _9000();
_2993: Console.WriteLine();
_3020: if (_A[(int)(X + .5), (int)(Y + .5)] != 0) goto _3080;
_3060: goto _2960;
_3080: if (_A[(int)(X + .5), (int)(Y + .5)] != 2) goto _3230;
_3120: Console.WriteLine(”*** KLINGON DESTROYED ***”);
_3130: _P[1] = _P[1] - 1;
_3140: _P[3] = _P[3] - 1;
_3150: if (_P[3] <= 0) goto _4040;
_3160: for(I = 1; I <= 3; I += 1) {
_3170: if ((int)(X + .5) != _K[(int)I, 1]) goto _3190;
_3180: if ((int)(Y + .5) == _K[(int)I, 2]) goto _3200;
_3190: ;} I = 3;
_3200: _K[(int)I, 3] = 0;
_3210: goto _3370;
_3230: if (_A[(int)(X + .5), (int)(Y + .5)] != 4) goto _3290;
_3270: Console.WriteLine("YOU CAN'T DESTROY STARS SILLY");
_3280: goto _3420;
To simulate line numbers, each line starts with a label consisting of an underscore followed by a number. That works fine for GOTO, but what about GOSUB? Examine line 2992. Subroutines were replaced with methods. That almost worked. In BASIC, you’re not forced to RETURN from subroutines. You can leave them via GOTO. That was used only in the case that the player is destroyed to send them back to the beginning of the program to start over. I replaced that GOTO with a return statement that passes a flag back to the caller. The caller inspects the flag and jumps back to the program start if need be. I also discovered that at one point, there is a GOTO that jumps into a FOR loop. C# won’t let you jump to a label in a sub-block of code. I transformed the FOR loop into a GOTO loop to make C# happy.
All the variables in the BASIC program, including the arrays, are real number type. However, in BASIC, an array and a scalar can share the same name; the interpreter is able to sort it all out. But, C# is less kind. To solve the problem, I prefixed array names with underscores. Also, arrays in BASIC are indexed from 1 instead of 0. To compensate, I increased the length of all arrays by 1. Index 0 is never used.
Well, BASIC to C# using labels is a fairly neat hack, but …. it is still painful. Notice the use of reals to compensate for the typing conversion. Whenever anyone goes on about the superiority of dynamic typing I always think of them as BASIC programmers.
Anyway pain aside, here comes the real gem and the reason for posting the article ….
Rewriting the game brought up an interesting aspect of the BASIC version. Targeting is done using polar coordinates, but you won’t find any trigonometric functions in the BASIC code. I assume the functions were unavailable. Instead, the angle is converted into a direction vector using different ratios that approximate the trigonometric functions. That means even if you worked out perfect targeting using trigonometry, when you entered in the angle, the actual trajectory will be slightly off. Nonetheless, it’s a pretty clever math trick. As for me, I took advantage of Math.Sin() and Math.Cos().
Now that is nostalgia, using math hacks because there wasn’t any support for things like trigonometry, not that from time to time in certain situations these old hacks don’t resurface, for example I remember coming across a pretty slick circle drawing algorythm a few years ago using ints and no trig at all, or having to provide your own divide route for chips without a divide instruction.
