Retrochallenge 2016/10:
Ironic Computer Space Simulator (ICSS)

Episode 3: Rocketry — Or, Fly Me to the Moon Stars

In 1971, there was still a realistic, while rather exclusive, chance to go to the Moon. Not so today. — Therefor we humbly head for the stars, in stead. What will we need for this? A rocket, in the first place.

Edited Image

Going for the Moon Stars.

Maybe the most important part of Computer Space — and maybe every video game that is not rendering a first person perspective — is the player's representation on the screen. In the case of Computer Space it's a rocket-like spaceship. Famously, the layout of the rocket is traced out on the PCB generating the moving objects (the memory board), and so it is in the schematics.

There are 4 discrete positional layouts to represent the ship at any of 4 angles in half a quadrant:

Computer Space: Rocket schematics

Computer Space: Schematics and layout of player's rocket. (Nutting Associates, 1971)

A closer inspection reveals that there's no straight angle in this. Rather, the ship is always slightly at an angle and even a little skewed. While pratically this may have saved the circuitry required for another position, on the screen we enjoy the impression of the ship always drifting a bit, which adds much to the charms of the game.

As we already noted in our platform considerations, we won't be able to reproduce this on the PDP-1, because of the afterglow of the Type 30 CRT Display and the need for a rather seamless motion arising from this. Just rotating the ship as sketched in the schematics to a straight angle won't do, since then we'd still lack a symmetric outline. After some tinkering, we eventuall arrive at a representation that is close in features to the average outline and also matches the dots to some unit grid.

Ironic Computer Space Simulator: Rocket outlines

Finding the outline for our spaceship.

And here are the coordinates (top down), the respective differantials, and the differential offsets as sums of the discrete values 1, 2, and 4 (we'll see soon why):

 x, y        diff.        components

 0, 0        0,  0          0,    0
 3, 3        3,  3        2+1,  2+1
 7, 5        4,  2          4,    2
11, 6        4,  1          4,    1
15, 5        4, -1          4,   -1
20, 9        5,  4        4+1,    4
22, 3        2, -6          2, -4-2
26, 6        4,  4          4,    4

The way to seamlessly rotate an outline, as pioneered by Steve Russell and Spacewar!, is to calculate the unit vector (sine/cosine) of the rotational angle, scale it to the unit of choice and have a drawing routine that advances form bow to stern and displays each of the dots that are making up the outline. Since our outline is symmetric, we just encode one side, adjust some of the components of our matrix and repeat for a second pass to generate the other side of the shape.

Spacewar! features configurable outlines (encoding 5 leteral and/or downwards movements, a code to alternately store and restore the drawing position — think of fins —, and one to mark the end of the outline-encoding). First, this codes had been interpreted, but soon Dan Edwards came up with an ingenious out-line compiler (one of the first JIT compilers in history) that gave the program a decisive boost in runtime, freeing the computational resources for gravity calculations while maintaining a flickerfree display. (See "Inside Spacewar!", Part 4 for details.)

The product of this outline compiler is a procedural sprite — and this is also, what we're going for. Since we're not interested in configurable sprites, we'll directly code it by hand. And this is, how we move around in our grid:

direction           y               x

sternwards       - cos            + sin
outwards         + sin*           + cos*
inwards          - sin*           - cos*

*) sin* and cos* complemented for second, mirrored pass.

Having a look at our differential offsets, we observe all sorts of values from 1 to 6 in our table. As a compromise in costs of setting up the values and costs in repeated calculations, we settle for the three most frequent offsets that may also be used to produce the other ones: 1, 2, and 4. (Coincidentally, these are also powers of 2!)

But, first we need a sine-cosine routine. As did Steve Russell and Spacewar!:

« Russell, never one to "do something" when there was an alternative, begged off for one reason or another. One of the excuses for not doing it, Slug [Steve Russell; N.L.] remembers, was "Oh, we don't have a sine-cosine routine and gee, I don't know how to write a sine-cosine routine..." Then Alan Kotok came back from a trip all the way to Maynard (DEC headquarters) with paper tapes saying "All right, Russell, here's a sine-cosine routine; now what's your excuse?"
"Well," says Slug, "I looked around and I didn't find an excuse, so I had to settle down and do some figuring. »
(J. M. Graetz, "The Origin of Spacewar"; Creative Computing, August 1981)

The sine-cosine in question originated at Adams Associates, Inc. (DEC Preliminary Memo M-1094, 22 Nov 1960) and approximates its results by the well known Taylor series:

              x3    x5    x7
sin(x) = x - ––– + ––– - –––
              3!    5!    7!

       = C1x + C3x3 + C5x5 + C7x7

       = ((((C7)x2 + C5)x2 + C3)x2 + C1)x

(See here for all the nasty details.)

The routine accepts radiants as input (0...+2π), where the fractional point is considered to be right after bit 3 (sign, 3 integer bits, 14 fractional bits; 2π = octal 311040). By this we've a general format for any angles, we're going to deal with.

The other internal format, we'll use, is for any positional values. Here we'll use a fixed point format derived from the display command, consisting of a sign, 9 integer bits and 8 fractional bits. The origin of the Type 30 CRT is in the center, stretching in x-direction to the left to -511 and to the right to + 511, positive y is up and negative y is down (apparently, -0/-0, is just left and below of 0/0):

                 +511

      -511        0/0         +511

                 -511

Thanks to this system "toroidal space" (meaning, the ship will reappear at the opposite side, as it disappears at any of the borders) is handled automatically by overflow/run-around and we don't have to care for it. — Life can be simple, at times.

Now we're ready to layout a plan for a rocket routine:

  1. Read inputs (and merge two player inputs into a single one).
  2. Parse and apply rotation (update thew rotational angle).
  3. Get the unit vector (sine, cosine).
  4. Get scaled versions of the unit vector representing 1, 2, and 4 steps for the display routine.
  5. Put it on the screen.
  6. Finally, we'll add a consant delta x and delta y to have the rocket floating across the screen.

And this is what we eventually get:

Screenshot of ICSS in development.

Screenshot of version 0.02. — See it live: www.masswerk.at/icss/.

Player Input

We've already addressed our plan of repurposing input methods for the PDP-1 already in place thanks to Spacewar! Let's have a closer look.

Our favorite method of input is, of course, the first gamepads to be attached to a computer ever, MIT's original "control boxes". (For the purpose of the restored PDP-1 at the CHM they have been surrogated by a more audiences-proof solution involving arcade buttons and a layout similar to "Space Duel".)

Original control boxes for the PDP-1 and Spacewar!

The original control boxes: Dan Edwards (left) and Peter Samson (right) playing Spacewar!, 1962 ca.,
and a modern reconstruction by Thomas A. Tilley, 2015
(Images: DEC, via the Computer History Museum (accession 102631264), Thomas A. Tilley, 2015.)

The other input method is the testword, an array of console switches to set up an 18-bit register.

PDP-1 testword switches and Spacewar!

PDP-1 control pannel and usage of the testword switches for Spacewar!
(Image: Marcin Wichary, 2008, CreativeCommons; edited, N.L.)

Whatever device we're using, the signal for any of the player controls is encoded as follows:

bits:          0  1  2' 3  4  5' 6  7  8' 9 10 11'12 13 14'15 16 17
usage:         -PLAYER 1-                                -PLAYER 2- 
semantics:     L  R  T  F                                L  R  T  F

For our purpose we have to take care of two issues here: a) selecting the input method of choice, and b) mixing the player inputs into a single one to be used by the game (so it doesn't make a difference which one of the two player inputs is supplied).

For chosing the input method, we go with the proven method used by Spacewar!, a double start address. The usual one at 4 will configure the game for the control boxes, an alternate one at 5 for use with the testword switches:

3/	jmp sbf				/ ignore seq. break
	jmp a0				/ start addr, use control boxes
	jmp a1				/ alt. start addr, use testword controls

/...

/here from start

a0,	law rcb				/ configure to read control boxes (sa 4)
	dap rcw
	jmp a2

a1,	law rtw				/ configure to read testword (sa 5)
	dap rcw

/start a new run

a2,	jsp bsi				/ start of main

/...

rcw,	jsp .				/ read player input

/...


/control word get routines

/read control boxes
rcb,	dap rcx
	cli
	iot 11
rcx,	jmp .

/read testword
rtw,	dap rtx
	lat
	swap
rtx,	jmp .

For the control boxes, we use instruction "iot 11", mixing the new input with the contents of IO. (Therefor we'll have to clear the IO register by "cli" before.)

For the testword controls, instruction "lat" transfers the current state of the switches into AC. We normalize this method by transferring this into IO by a swap.

The location of the chosen subroutine is put into the address part of the "jsr" to read the input, either by the path from start address 4 over label "a0", or by start address "5" and the code at label "a1". (Label "a2" marks our old start of the main routine.)

Now that we've managed the input sources, we'll have to merge the two possible player inputs. We prefer to have the result in the highest 4 bits (so that we may apply simple checks for the sign to inspect the bit in msb-position).

rcw,	jsp .				/read control word
	cla				/ clear ac
	dio \cw				/ store io in variable \cw
	rcr 4s				/ rotate lowest 4 bits of io into ac 
	ior \cw				/ inclusive or \cw
	dac \cw				/ deposit merged inputs in \cw

	swap				/ put it into IO

By this we've the merged input stored in variable \cw and (after the final swap) in the IO-register. To parse the sign-bit in IO, the instruction "spi" (skip on IO positive) is exactly what we want. Let's parse and process rotations:

Rotating the Ship

	lac \rth			/load angle
	spi				/sign cleared in io?
	add raa				/no, add angular acceleration (ccw)
	ril 1s				/next bit
	spi
	sub raa				/rotate clockwise

/game parameters
raa,	2000				/ rocket angular acceleration

After normalizing our angle (0 .. 2π) we calculate the sine cosine and store them in variables "\sn" and "\cs":

	sma				/normalize 0 >= angle >= 2Pi (311040)
	sub (311040
	spa
	add (311040

	dac \rth			/update angle
	jda sin				/get sin, store it in \sn
	dac \sn
	lac \rth
	jda cos				/get cos, store it in \cs
	dac \cs

There are some new instructions: "sma" skips on minus AC (sign-bit set), "spa" is the opposite, skipping on a piositive contents of AC, and finally, instruction "jda" is a jump to subroutine that puts the contents of AC into the given address and jumps, quite like a normal jsr, to the location immediatly after this. Here, jda is used to transfer our angle (\rth) as the input to the sine and cosine routines.

This code is much like the one we find in Spacewar!. The beauty of this is that we're following a single path, regardless of the input. We may skip a single addition or subtraction, but all the time consuming processing is done in any case. (Since we do not have a clock, we would have to count instructions and spend them in a delay loop, otherwise.)

Now we've to introduce another macro, "scale". This is as simple as loading the contents of an address, shifting it right by given number of bit-positions and storing it in another address. We'll use it here mainly for its expressive qualities. As soon as we're seeing this, we know that it's about scaling a value.

define scale A,B,C
	lac A
	sar B
	dac C
	term

Also, we add a two common definitions to the head of our code:

	mul=mus
	div=dis

These are defining symbols for mnemonics that are not defined in Macro out of the box, concerning the hardware multiply/divide. Early machine came just with a special shift instruction for a partial multiplication step (mus) or division step (dis). While the hardware multiply/divide option repurposes the opcodes of the earlier, partial instructions, new mnemonics are used to mark the difference.

What's missing? Updating the spaceship.

Position and Graphics

Since our drawing routine starts at the tip of the rocket, we must get ahead of the center of our ship (as in "\rpx" and "\rpy"). To do so, we apply a scaled version of the unit vector. While we have to handle the ship position anyway, we may also update this one by adding a constant delta x and delta y in order to let the ship float accross the screen.

Finally, we compose the components for our drawing routine, also scaled versions of the unit vector. With these in place, we call the drawing routine at label "rdo": First we draw a the dot at the stern, then we draw one of the sides in a first pass, invert the lateral offset components and redo with a second pass for the other side. As we're, again, sure to spend at least 50 microseconds between any two display commands, we may go with the fire and forget varsion dpy-i.

And here is the complete code, so far:

ironic computer space  0.02  nl 2016-10-14

                                                         
	mul=mus
	div=dis

define initialize A,B
	law B
	dap A
	term

define index A,B,C
	idx A
	sas B
	jmp C
	term

define swap
	rcl 9s
	rcl 9s
	term

define load A,B
	lio (B
	dio A
	term

define setup A,B
	law i B
	dac A
	term

define count A,B
	isp A
	jmp B
	term

/macros specific to the program

define scale A,B,C
	lac A
	sar B
	dac C
	term

define random
	lac ran
	rar 1s
	xor (355671
	add (355671
	dac ran
	term


3/	jmp sbf				/ ignore seq. break
	jmp a0				/ start addr, use control boxes
	jmp a1				/ alt. start addr, use testword controls


/game parameters

raa,	2000		/ rocket angular acceleration
ran,	0		/ random number


/routine to flush sequence breaks, if they occur.

sbf,	tyi
	lio 2
	lac 0
	lsm
	jmp i 1


/subroutines for background display

nos=77 	/number of stars

/table of stars coors (nos words, 9 bits x and 9 bits y)
bst,	. nos/

/setup (nos random coors starting at bst)

bsi,	dap bsx				/ deposit return address
	init bsc, bst			/ deposit first addr of bst in bsc
bsl,	random				/ get a new random number
bsc,	dac .				/ store it in current loc of st
	index bsc, (dac bst+nos, bsl	/ increment bsl, repeat at bgl for nos times
bsx,	jmp .				/ return


/display background stars (displays every 2nd frame only)

bg,	dap bgx				/ deposit return address
	isp bgc				/ increment bgc, skip on positive
bgx,	jmp .				/ return
	law i 2				/ load -2 into ac
	dac bgc				/ deposit in bgc to reset frame counter
	init bgl, bst			/ init bgl to first addr of stars table
bgl,	lac .				/ get coors of next star (x/y)
	cli				/ clear io
	scr 9s				/ shift low 9 bits in high 9 bits of io (x)
	sal 9s				/ move remaining 9 bits in ac in high part (y)
	add bgv				/ add vertical offset
	swap				/ swap contents of ac and io
	add bgh				/ add horizontal offset
	dpy-i				/ display a dot at coors in ac (x) and io (y)
	index bgl, (lac bst+nos, bgl	/ repeat the loop at bgl nos times
	jmp bgx				/ return

bgc,	0
bgh,	0
bgv,	0

/sine-cosine subroutine_ Adams associates
/calling sequence= number in AC, jda sin or jdacos.
/argument is between .+2 pi, with binary point to right of bit 3.
/answer has binary point to right of bit 0.  Time = 2.35-? ms.
/changed for auto-multiply , ddp 1/19/63

cos,	0
	dap csx
	lac (62210
	add cos
	dac sin
	jmp .+4

sin,	0
	dap csx
	lac sin
	spa
si1,	add (311040
	sub (62210
	sma
	jmp si2
	add (62210

si3,	ral 2s
	mul (242763
	dac sin
	mul sin
	dac cos
	mul (756103
	add (121312
	mul cos
	add (532511
	mul cos
	add (144417
	mul sin
	scl 3s
	dac cos
	xor sin
	sma
	jmp csx-1
	lac (377777
	lio sin
	spi
	cma
	jmp csx

	lac cos
csx,	jmp .

si2,	cma
	add (62210
	sma
	jmp si3
	add (62210
	spa
	jmp .+3
	sub (62210
	jmp si3

	sub (62210
	jmp si1



/here from start

a0,	law rcb				/configure to read control boxes (sa 4)
	dap rcw
	jmp a2

a1,	law rtw				/configure to read testword (sa 5)
	dap rcw

/start a new run

a2,	jsp bsi				/ initial setup of bg-stars
	dzm bgh				/ reset offsets of stars to zero
	dzm bgv
	
/rocket reset

ar,	dzm \rth			/ rotational angle
	lac (-140000
	dac \rpx			/ pos x
	cma
	dac \rpy			/ pos y
	lac (1000
	dac \rdx			/ dx
	lac (-400
	dac \rdy			/ dy

/main loop

fr0,	load \ict, -4500		/ initial instruction budget (delay)

	jsp bg				/ display background
	jsp rkt				/ rocket routine

	count \ict, .			/ use up rest of time of main loop
	jmp fr0				/ next frame


/player rocket routine

rkt,	dap rkx
rcw,	jsp .				/read control word
	cla				/merge (or) spacewar player inputs
	dio \cw				/ highest and lowest 4 bits in io
	rcr 4s				/ (rotate ccw, rotate cw, trust, fire)
	ior \cw				/ store them in high pos of \cw
	dac \cw

	swap				/parse and process rotation
	lac \rth			/load angle
	spi				/sign cleared in io?
	add raa				/no, add angular acceleration
	ril 1s				/next bit
	spi
	sub raa
	sma				/normalize 0 >= angle >= 2Pi (311040)
	sub (311040
	spa
	add (311040
	dac \rth			/update angle
	jda sin				/get sin, store it in \sn
	dac \sn
	lac \rth
	jda cos				/get cos, store it in \cs
	dac \cs

	scale \sn, 5s, \sn1		/get position of rocket tip
	scale \cs, 5s, \cn1
	lac \rpx
	add \rdx
	dac \rpx
	sub \sn1
	dac \px
	lac \rpy
	add \rdy
	dac \rpy
	add \cn1
	dac \py

	scale \sn, 6s, \sn4		/scaled sine 4 steps
	dac \sm4
	sar 1s
	dac \sn2			/scaled sine 2 steps
	dac \sm2
	sar 1s				/scaled sine single step
	dac \sn1
	dac \sm1
	scale \cs, 6s, \cn4		/same for cosine
	dac \cm4
	sar 1s
	dac \cn2
	dac \cm2
	sar 1s
	dac \cn1
	dac \cm1
	
	jsp rod				/display it
	
	lac \ict			/update instruction count
	add (1000
	dac \ict

rkx,	jmp .


/control word get routines

/read control boxes
rcb,	dap rcx
	cli
	iot 11
rcx,	jmp .

/read testword
rtw,	dap rtx
	lat
	swap
rtx,	jmp .


/rocket display
/ step rearwards -  x add \sn1, y sub \cn1
/ step outwards  -  x add \cm1, y add \sm1
/ step inwards   -  x sub \cm1, y sub \sm1

rod, 	dap rox
	stf 6			/set flag 6
	lac px
	lio py
	dpy-i

rop,	swap			/y +3, x +3
	sub \cn2
	sub \cn1
	add \sm2
	add \sm1
	swap
	add \sn2
	add \sn1
	add \cm2
	add \cm1
	dpy-i

	swap			/y +4, x +2
	sub \cn4
	add \sm2
	swap
	add \sn4
	add \cm2
	dpy-i

	swap			/y +4, x +1
	sub \cn4
	add \sm1
	swap
	add \sn4
	add \cm1
	dpy-i

	swap			/y +4, x -1
	sub \cn4
	sub \sm1
	swap
	add \sn4
	sub \cm1
	dpy-i

	swap			/y +5, x +4
	sub \cn4
	sub \cn1
	add \sm4
	swap
	add \sn4
	add \sn1
	add \cm4
	dpy-i

	swap			/y +2, x-6
	sub \cn2
	sub \sm4
	sub \sm2
	swap
	add \sn2
	sub \cm4
	sub \cm2
	dpy-i

	swap			/y +4, x+4
	sub \cn4
	add \sm4
	swap
	add \sn4
	add \cm4
	dpy-i
	
	szf i 6			/flag 6 set? (skip on not zero)
rox,	jmp .			/no, return
	clf 6			/clear flag 6
	lac \cm1		/invert unit vector components
	cma
	dac \cm1
	lac \sm1
	cma
	dac \sm1
	lac \cm2
	cma
	dac \cm2
	lac \sm2
	cma
	dac \sm2
	lac \cm4
	cma
	dac \cm4
	lac \sm4
	cma
	dac \sm4
	lac px			/load first pos
	lio py
	jmp rop			/second pass for other side
	


	constants
	variables

	start 4

Note: The amount used to update the instruction count is completely arbitrary.
And in case, we didn't have this before: Instruction "cma" complements the value in the accumulator.

Thanks to the gods of wires and transistors, almost everything worked out of the box. That's especially nice, because I stripped any debugging from the emulator long ago, since it was original meant to run authentic, historical programs.

That is, with one exception: The spaceship was rotating constantly. I stared at the input instructions, I stared at the code for parsing the inputs — nothing of notice here, and still… Let's comment something out, — and still…

Finally, I found, I had used a "dac" for a "dap" where the address of the control word get routine of choice is inserted into the instruction at label "rcw", thus rather owerwriting the whole instruction word instead of fixing up the address part only. — The dangers and perils of self-modifying program code!

Anyway, see the code live in action: www.masswerk.at/icss/.

*****

P.S.: An Interesting Find
While tinkering around, I remebered that an early version of Spacewar! (2B) was using a slighly different constant to modify the random seed, namely 355671 instead of the constant 355670, we're finding in later versions. And using this one, we get a much nicer starfield, even reminding of some constellations! I've always been wondering, why the random number was changed at all. Coincidentially, Spacewar! started with a random starfield, too, before Peter Samson added his realisticly moving starfield, the Expensive Planetarium. Maybe, the earlier version of the constant was specially crafted for the purpose of the starfield, while exposing properties that where not that ideal for features that became more important later on, like the particle effect of the explosions.

 

That's all for this post.

 

Next:   Episode 4: “A Rocket Ship That You Control”

Previous:   Episode 2: Stars & Sync

Back to the index.

— This series is part of Retrochallenge 2016/10. —