Close

Tanks for the Code

A project log for RIP OFF for the PDP-1

RIP OFF was released as an arcade video game by Cinematronics in 1980. I am going to make a version of RIP OFF for the PDP-1.

michael-gardiMichael Gardi 06/20/2026 at 16:390 Comments

After a few weeks of procrastination (enjoying the warmer weather up here in the great white north) I finally got back at this project. My initial goal was to get the player's tank showing up on the screen and moving around. Easier said than done.

With my Lunar Lander for the PDP-1 project I was able to create the LEM as a series of five discrete bitmaps, one for each of it's possible orientations [degrees] (left [0], left-up [45], up [90], right-up [135], and right [180]). Each bitmap fit nicely into an 18 x 18 bit grid which worked perfectly with the PDP-1's 18-bit word architecture.

The tanks in RIP OFF are a completely different animal. If you look at the video in the project description, you see that the tanks can seemingly rotate and move in any arbitrary direction (angle).  Yikes!

I will be the first to admit that left to my own devices, I would have been hard pressed to come up with code to implement this behavior (especially on a PDP-1). Fortunately for me this problem was solved about 55 years ago when the original Spacewar! game came out since both of the space ships can do this.

I could have gone back to the original Spacewar! source to figure this out, but even there I had a leg up. Norbert Landsteiner used this approach when he wrote his Retrochallenge 2016/10: Ironic Computer Space Simulator (ICSS)  implementation of Computer Space. In Episode 3 of the writeup (both text description and source code) of his ICSS effort, he describes the technique in great detail, to the point where even I could mostly understand it.

Please refer to Norbert's Episode 3 description for the gory details (and code) as I did, but the basic flow is:

  1. Apply any rotational change based on player inputs.
  2. Calculate a "unit vector" for the new rotation using using sine and cosine routines.
  3. Calculate scaled versions of the unit vector in 1, 2, and 4 steps (simple shifts).
  4. Find the top most point of the "sprite" outline and apply the delta x and delta y representing the tank's current position.
  5. Starting from that top most point of the sprite outline, apply the differentials as scaled vectors  between each point and plot the result. 

In the above flow, calculations for steps 1-4 are performed for each frame of the game.  Step 5 is efficiently performed by "static" code that was generated based on the differentials between points.

To generate the code for step 5 I wrote a small Python program that takes the coordinates of the sprite outline points as input.

Note that only one side is of the sprite is defined because after that side is "drawn" the transformation "matrix" can be adjusted and a second pass can be run to generate the other side.  

Here is the code.

# Define the points that form the left side of the sprite.
# Tank.
left_side_points=((0,0),(1,1),(3,1),(5,1),(6,6),(6,8),(6,4),
                  (7,1),(8,3),(9,8),(10,4),(13,8),(13,4),
                  (17,4),(17,8),(20,4),(21,8),(22,3),(23,1),
                  (24,4),(24,6),(24,8))

# Emit add or sub commands based on the delta value passed. 
# Maps vectors to combinations of the 1, 2, and 4 vectors.
def show_command(command:str, delta:int):
    if delta == 0:
        return
    elif delta in (1,2,4):
        print("\t",end='')
        print(command + str(delta))
    elif delta == 3:
        print("\t",end='')
        print(command+"2")
        print("\t",end='')
        print(command+"1")
    elif delta == 5:
        print("\t",end='')
        print(command+"4")
        print("\t",end='')
        print(command+"1")
    elif delta == 6:
        print("\t",end='')
        print(command+"4")
        print("\t",end='')
        print(command+"2")
    elif delta == 8:
        print("\t",end='')
        print(command+"4")
        print("\t",end='')
        print(command+"4")
    elif delta == 10:
        print("\t",end='')
        print(command+"4")
        print("\t",end='')
        print(command+"4")
        print("\t",end='')
        print(command+"2")
    else:
        print("Delta not defined.")
        
# Calculate the deltas.
scale = 2
print("Deltas between points.")
deltas = []
for i in range(1, len(left_side_points)):
    deltas.append(((left_side_points[i][0]-left_side_points[i-1][0])*scale, 
                   (left_side_points[i][1]-left_side_points[i-1][1])*scale))
print(deltas)

# Generate the code to emit the sprite.
print("\n\t/ Sprite code.")
for delta in deltas:
    print(f"\t/ y={delta[0]}, x={delta[1]}")

    print("\tswap")
    # Process Y. Y always positive. X sometime negative.
    show_command("sub \\cn", delta[0])
    if delta[1] < 0:
        show_command(f"sub \\sm",-delta[1])
    else:
        show_command(f"add \\sm",delta[1])
    print("\tswap")
    
    # # Process Y. Y always positive. X sometime negative.

    show_command("add \\sn",delta[0])
    if delta[1] < 0:
        show_command("sub \\cm", -delta[1])
    else:
        show_command("add \\cm", delta[1])
    print("\tdisp")
    print()

When this Python code is run the following output is emitted.

Deltas between points.
[(2, 2), (4, 0), (4, 0), (2, 10), (0, 4), (0, -8), (2, -6), 
 (2, 4), (2, 10), (2, -8), (6, 8), (0, -8), (8, 0), (0, 8), 
 (6, -8), (2, 8), (2, -10), (2, -4), (2, 6), (0, 4), (0, 4)]

    / Sprite code.
    / y=2, x=2
    swap
    sub \cn2
    add \sm2
    swap
    add \sn2
    add \cm2
    disp

    / y=4, x=0
    swap
    sub \cn4
    swap
    add \sn4
    disp

    / y=4, x=0
    swap
    sub \cn4
    swap
    add \sn4
    disp

    / y=2, x=10
    swap
    sub \cn2
    add \sm4
    add \sm4
    add \sm2
    swap
    add \sn2
    add \cm4
    add \cm4
    add \cm2
    disp

    / y=0, x=4
    swap
    add \sm4
    swap
    add \cm4
    disp

    / y=0, x=-8
    swap
    sub \sm4
    sub \sm4
    swap
    sub \cm4
    sub \cm4
    disp

    / y=2, x=-6
    swap
    sub \cn2
    sub \sm4
    sub \sm2
    swap
    add \sn2
    sub \cm4
    sub \cm2
    disp

    / y=2, x=4
    swap
    sub \cn2
    add \sm4
    swap
    add \sn2
    add \cm4
    disp

    / y=2, x=10
    swap
    sub \cn2
    add \sm4
    add \sm4
    add \sm2
    swap
    add \sn2
    add \cm4
    add \cm4
    add \cm2
    disp

    / y=2, x=-8
    swap
    sub \cn2
    sub \sm4
    sub \sm4
    swap
    add \sn2
    sub \cm4
    sub \cm4
    disp

    / y=6, x=8
    swap
    sub \cn4
    sub \cn2
    add \sm4
    add \sm4
    swap
    add \sn4
    add \sn2
    add \cm4
    add \cm4
    disp

    / y=0, x=-8
    swap
    sub \sm4
    sub \sm4
    swap
    sub \cm4
    sub \cm4
    disp

    / y=8, x=0
    swap
    sub \cn4
    sub \cn4
    swap
    add \sn4
    add \sn4
    disp

    / y=0, x=8
    swap
    add \sm4
    add \sm4
    swap
    add \cm4
    add \cm4
    disp

    / y=6, x=-8
    swap
    sub \cn4
    sub \cn2
    sub \sm4
    sub \sm4
    swap
    add \sn4
    add \sn2
    sub \cm4
    sub \cm4
    disp

    / y=2, x=8
    swap
    sub \cn2
    add \sm4
    add \sm4
    swap
    add \sn2
    add \cm4
    add \cm4
    disp

    / y=2, x=-10
    swap
    sub \cn2
    sub \sm4
    sub \sm4
    sub \sm2
    swap
    add \sn2
    sub \cm4
    sub \cm4
    sub \cm2
    disp

    / y=2, x=-4
    swap
    sub \cn2
    sub \sm4
    swap
    add \sn2
    sub \cm4
    disp

    / y=2, x=6
    swap
    sub \cn2
    add \sm4
    add \sm2
    swap
    add \sn2
    add \cm4
    add \cm2
    disp

    / y=0, x=4
    swap
    add \sm4
    swap
    add \cm4
    disp

    / y=0, x=4
    swap
    add \sm4
    swap
    add \cm4
    disp

The above "compiled" PDP-1 assembler code was transferred to the RIP OFF game  and used to draw the tank. Note that the values for \sn1, \sn2, \sn4, \cm1, \cm2, and \cm4 are all recalculated every time the tank's rotation changes.

swap and disp are macros:

define swap
rcl 9s        / Swaps the AC and IO registers.
rcl 9s
term

define disp   / X in AC register, Y in IO
dpy-i         / Display a dot at brightness level 0 (default).
term        

Note that the dpy-i op code above is expecting the X coordinate to be in the AC register and the Y coordinate to be in the IO register in a format that the dpy-i opcode understand (ie in the top 10 bits of each register). So the above sprite code is called with the starting X and Y coordinates in AC and IO with the differential point coordinates applied in turn directly to the registers without having to save intermediate values to memory. Thus this is very beautiful and efficient code!

It's worth noting the since the original Spacewar! had multiple ship types, there was code to "compile" the outlines stored as data at runtime directly into memory and execute (draw) them from there.  Norbert's ICSS had only one ship so he coded it by hand. I thought I would probably iterate the tank's shape a few times at first so an external "compiler" seemed to make the most sense.  

The arcade version of RIP OFF went with futuristic looking tanks to the point that I thought they were space ships for the longest time. Since I was porting the game back from 1980 to 1960 I decided to draw the tanks old school. Still a work in progress.

Discussions