-
Something to Steal
07/06/2026 at 21:36 • 0 commentsI was sorely tempted to move onto creating the enemy tanks so I would have something to shoot at (followed by explosions), but instead I opted to do something simpler and created the "fuel cells". In the original game there are eight of them placed as follows at the beginning of a game.
![]()
Since I was making the tanks a little different, I thought I would change the shape of the fuel cells too. I tried circles first thinking they would represent an overhead view of "oil drums", but I did not like the look of the circles at the scale required. On a raster display it's hard to get them to look sufficiently round. I also tried square and hex shapes, but at the end of the day the triangles just looked the best. I still might change but for now it's triangles.
![]()
I used the same technique as with tank drawing and generated code to efficiently render the triangles based on a central x,y coordinate.
/ Offsets in screen coordinates. one, 000400 / 1 pixel. two, 001000 / 2 pixels tre, 001400 / 3 pixels. for, 002000 / 4 pixels. fiv, 002400 / 5 pixels. six, 003000 / 6 pixels. / Draw triangle center coordinates. tx, 0 ty, 0 / Draw a right pointing triangle. Center: X in tx, Y in ty. dtr, dap drx lac tx lio ty sub six sub two dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 ioh drx, jmp . / Draw a left pointing triangle. Center: X in tx, Y in ty. dtl, dap dlx lac tx lio ty add six add two dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 sub six swap sub six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 add six swap sub six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 swap add six swap ioh dpy-i 4200 ioh dlx, jmp .I setup some constants with the coordinates of the fuel cells at the start of the game.
/ Initial fuel cell coordinates. fcx, 746777 000000 031000 760777 017000 746777 000000 031000 000001 / End of data marker. fcy, 036000 024000 036000 000000 000000 741777 753777 741777In anticipation of having these cells move around the screen during the game I created "active" fuel cell tables.
/ Active fuel cell coordinates. acx, 0 0 0 0 0 0 0 0 0 acy, 0 0 0 0 0 0 0 0
At the beginning of a game I copy the initial coordinates to the active coordinates.
init cfi,fcx / Setup the coordinate table pointers. init cfo,acx lac (21 / Copy 17 initial cell coordinates to active. dac \tmp cfi, lio . / Get the next initial coordinate. cfo, dio . / Set the next active coordinate. lac \tmp sub (1 sza i / Is the value zero? jmp fr0 / Yes - Go to main loop. dac \tmp / No - Save counter. idx cfi / Increment table pointers. idx cfo jmp cfi / Next coordinate. fr0,
A couple of things about this. With the PDP-1, it is often the case that the only way to do a task efficiently is to use self modifying code. Since the PDP-1 did not have any indexed addressing modes for instance, the only way to "copy" these tables is to modify the addresses of the specific "load" (lio) and "save" (dio) instructions.
First the init macro gets the address at a specific label then inserts that address into an instruction at another label.
define init A,B law B dap A term
So the address of the initial coordinate table (fcx) is inserted into the lio instruction at cfi, and the address of the active coordinates (acx) is inserted into the dio instruction at cfo.
Furthermore, each time through the copy loop, the addresses at cfi and cfo are incremented by 1 via an idx instruction.
Easy peezy.
With the active fuel cell table updated a new subroutine was added to draw the fuel cells on the screen. It uses the same technique to step through the active cells.
/ Draw the fuel cells. dfc, dap dfx init in1,acx / Setup pointers to the fuel cell tables. init in2,acy init ex1,fcd in1, lac . / Get the X center. sub (000001 / Check for end of file marker. sza i / Is the value zero? jmp dfx / Yes - Done. add (000001 dac tx / No - Set X center. in2, lio . / Get the Y center. dio ty / Set the Y center. ex1, xct . / Show the triangle as left or right facing. idx in1 / Advance to the next fuel cell. idx in2 idx ex1 jmp in1 / Draw the next fuel cell. dfx, jmp .One more trick here that involves the "ex1, xct ." line of code. That line is initialized with the address of the following table ...
/ Fuel cells have either a left or right facing orientation. fcd, jsp dtl jsp dtl jsp dtr jsp dtl jsp dtr jsp dtl jsp dtr jsp dtr
... which contains a list of subroutine calls (jsp) to either the left or right facing drawing routines. The PDP-1 xct instruction executes the instruction that it's address points to as if it were "inline" at the point xct is executed.
So the fcd table's address is set into the xct instruction and for each fuel cell the appropriate drawing routine is called.
Ya this stuff makes my head hurt a little too, but it's really cool when it works.
Finally here is my main loop now with the call to draw the triangles.
fr0, load \ict, -4500 / Initial instruction budget (delay). idx \frc / Increment frame counter. jsp tnk / Draw and control the player's tank. jsp dfc / Draw fuel cells. jsp pop / Check to see if a shell has been fired. jsp mov / Move any active shells. count \ict, . / Use up rest of time of main loop. jmp fr0 / Next frame. -
Pew! Pew! Pew! Pew!
06/28/2026 at 01:05 • 0 commentsThere is a hard and fast game creation rule:
"When you create something that can shoot, you must immediately make it shoot."
Well maybe not :-), but for sure it's what I do next.
In the original RIP OFF game there are two ways to defend the precious fuel capsules. You can:
- Crash into the enemy tanks that are trying to steal your fuel, explosively destroying both them and yourself. Any nearby fuel capsules will be unharmed. The penalty for this action is the few seconds that you are offline and defenseless until you respawn.
- Shoot the enemy with the tank's main gun. There can only be a maximum of four shells active at any given time.
Since the game is limited to four shells, I didn't have to get too fancy with the implementation. I started by setting aside some memory to hold the shell's data.
/ Tank shells. Only 4 concurrent tank shells allowed. s1, 0 / Shell 1. Life,x,y,dx,dy. x1, 0 y1, 0 dx1, 0 dy1, 0 s2, 0 / Shell 2. Life,x,y,dx,dy. x2, 0 y2, 0 dx2, 0 dy2, 0 s3, 0 / Shell 2. Life,x,y,dx,dy. x3, 0 y3, 0 dx3, 0 dy3, 0 s4, 0 / Shell 4. Life,x,y,dx,dy. x4, 0 y4, 0 dx4, 0 dy4, 0 slf, -77 / Initial shell time to live.
Each shell has:
- A time to live (TTL) value which is 0 when a shell is inactive and set to a fixed negative value (slf) when the shell is "fired". TTL is incremented for each step the shell takes across the screen until it hits 0 again.
- The x, y screen coordinates used to display the shell.
- dx, dy deltas to apply to the x, y screen coordinates for each step the shell traverses.
Some code was added to detect when the fire button on the controller was pressed. A fire flag is set only when the button state goes from off to on. That is to say the button must be released before the next shell can be launched.
lac pin / Check fire button off in previous read. and (040000 sza / Is button off? jmp nfr / No - Skip checking for fire bit. ril 1s / Parse fire input. spi / Is the fire bit set? stf 4 / Yes - Set flag 4 for fire. nfr,A section of code was added to "launch" a shell if the fire flag is set. This is called from the main loop.
/ Check to see if a shell has been fired. pop, dap pox / Set return address. szf i 4 / Is flag 4 set? jmp pox / No - Return. clf 4 / Clear the fire flag. lac \sn / Calculate shell vector x. sar 6s cma dac \fdx lac \cs / Calculate shell vector y. sar 6s dac \fdy cs1, lac s1 / Yes - Check shell 1. sza / Is shell 1 available? jmp cs2 / No - Check shell 2. fire s1,x1,y1,dx1,dy1 / Start shell in slot 1. jmp pox / Exit. cs2, lac s2 / Yes - Check shell 2. sza / Is shell 2 available? jmp cs3 / No - Check shell 2. fire s2,x2,y2,dx2,dy2 / Start shell in slot 2. jmp pox / Exit. cs3, lac s3 / Yes - Check shell 3. sza / Is shell 3 available? jmp cs4 / No - Check shell 4. fire s3,x3,y3,dx3,dy3 / Start shell in slot 3. jmp pox / Exit. cs4, lac s4 / Yes - Check shell 4. sza / Is shell 4 available? jmp pox / No - Return. fire s4,x4,y4,dx4,dy4 / Start shell in slot 4. pox, jmp . / Return.If the fire flag is set it is cleared and a vector (dx, dy) is calculated that controls the shell's speed and direction based on the current orientation of the tank. Then a search begins for a shell "slot" that is not being used based on the time to live being 0. If an unused slot is found it is activated via the "fire" macro.
/ Fire a shell! define fire LIFE,X,Y,DX,DY lac slf / Get the shell time to live. dac LIFE / Set the shell life. lac \fx / Set the starting x,y coordinates. dac X lac \fy dac Y lac \fdx / Set the direction vector. dac DX lac \fdy dac DY termHere the time to live, x,y coordinates, and dx,dy deltas are set.
To move the shells another subroutine was written that gets called each time through the main loop of the game.
/ Move all active shells. mov, dap mvx / Set return address. lac s1 / Check shell 1. sma / Is shell 1 active? jmp mv2 / No - Check shell 2. move s1,x1,y1,dx1,dy1 / Yes - Move shell 1 one position. mv2, lac s2 / Check shell 2. sma / Is shell 2 active? jmp mv3 / No - Check shell 3. move s2,x2,y2,dx2,dy2 / Yes - Move shell 2 one position. mv3, lac s3 / Check shell 3. sma / Is shell 3 active? jmp mv4 / No - Check shell 4. move s3,x3,y3,dx3,dy3 / Yes - Move shell 3 one position. mv4, lac s4 / Check shell 4. sma / Is shell 4 active? jmp mvx / No - Return. move s4,x4,y4,dx4,dy4 / Yes - Move shell 4 one position. mvx, jmp .Each shell with a non-zero time to live is advanced one position based on the dx,dy values via the "move" macro.
/ Move the shell one step on the screen! define move LIFE,X,Y,DX,DY lac LIFE / Reduce time to live by 1. add (1 dac LIFE lac X / Show the shell on screen. lio Y dpy-i 300 add DX / Increment the X coordinate. dac X swap / Increment the Y coordinate. add DY dac Y termThe TTL is decremented by one, the shell is displayed, and dx and dy are added to the x and y coordinates for the next loop. It's not fancy but it gets the job done.
My main loop so far.
/ Main loop. fr0, load \ict, -4500 / Initial instruction budget (delay). idx \frc / Increment frame counter. jsp tnk / Draw and control the player's tank. jsp pop / Check to see if a shell has been fired. jsp mov / Move any active shells. count \ict, . / Use up rest of time of main loop. jmp fr0 / Next frame.Here is what it looks like.
-
Tanks for the Code
06/20/2026 at 16:39 • 0 commentsAfter 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:
- Apply any rotational change based on player inputs.
- Calculate a "unit vector" for the new rotation using using sine and cosine routines.
- Calculate scaled versions of the unit vector in 1, 2, and 4 steps (simple shifts).
- Find the top most point of the "sprite" outline and apply the delta x and delta y representing the tank's current position.
- 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 dispThe 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.
Michael Gardi

