Close
0%
0%

Lunar Lander for the PDP-1

My PDP-1 Replica (PiDP-1) from Obsolescence Guaranteed has arrived and I want to do something cool with it.

Similar projects worth following
In his December 2024 newsletter Oscar Vermeulen of Obsolescence Guaranteed wrote, "With the PDP-1 team at the Computer History Museum, we plan to do an annual programming competition: the winner gets invited to run his [or her] code on the last working PDP-1 in the CHM in Mountain View." In 2026 I would like for that person to be me. This is the beginning of my journey.

I've been bitten by the PDP-1 bug.  When I saw that Obsolescence Guaranteed was shipping their PDP-1 Replica I knew that I wanted one so I placed my order. Due to some well deserved high demand I had to wait a while for my kit to ship,  so I had time to think about what I wanted to do with my PiDP-1 when it arrived (other than playing Spacewar!).

Hardware

My first instinct, given that I mostly make reproductions myself, was to create a DEC Precision CRT Display Type 30 Reproduction since the PDP-1 was already well in hand. I ended up making three reproductions of this unique Type 30 Display that was used with the PDP-1.  

Why three?

  1. DEC Precision CRT Display Type 30 Reproduction - When I made this first version I was under the impression that the PiDP-1 was a 2/3 scale Replica of the original.  I later found out that PiDP-1 is actually at 57% scale plus I had based my model on an invalid assumption w.r.t. the circular screen opening size. Long story short my reproduction was actually at 75% scale and it's big. Too big for my desktop but it will be great for Spacewar! games at my local retro computer meetings.
  2. Honey, I Shrunk the Type 30 Display Reproduction - I wanted to make a smaller display so I zeroed in on a 50% scale model. Why 50%? Because at that scale a Pimoroni PIM-372 8" 4:3 Display could be easily installed.  Later when I factored in my invalid circular screen opening size I realized that the scale was actually 56%, an almost perfect match to the PiDP-1.
  3. Another Type 30 Display Reproduction At 36% Scale - When I started my Type 30 reproduction project(s) I sent a request to the Computer History Museum (CHM) through their contact page asking if it would be possible to get some measurements of the Type 30 display. In mid-October 2025 I received a reply from a wonderful archivist at the CHM who kindly agreed to take some measurements for me. Based on those measurements I decided to make one last Type 30 display.  I chose 36% scale to accommodate a Waveshare 5 inch HDMI Circular Touch Display which other PiDP-1 enthusiasts were using. Because of the relatively small size I decided to model the whole display not just the front part. It was the CHM measurement that the circular screen opening was actually 14" that made me realize that my assumption of a 16" opening based on my misreading of the Precision CRT Display Type 30 manual was wrong, and I had to recalculate the scales of my first two attempts.

Software

With that out of my system I turned to the programming side of the PDP-1.  Since I have never written about a software only project before I am in new territory here. But I have a great role model.

Inspiration

I wanted to write some PDP-1 code but didn't know where to begin. The Obsolescence Guaranteed folks have a PROGRAMMING THE DEC PDP-1 - A QUICK WAY TO GET STARTED guide.  It's great, but it mostly focuses on using the tools they have provided as part of their distribution (lovingly recreated from original 60s source code) to build an assembly language program and run it on the PDP-1 using a simple circle program as an example. 

When I posed the "How do I get started?" question on the PiDP-1 Google Group Oscar pointed to this document, Retrochallenge 2016/10: Ironic Computer Space Simulator (ICSS). Boy was he right. This link documents Norbert Landsteiner's entry to Retrochallenge 2016/10, where he implements a version of Computer Space on the PDP-1. Computer Space was the very first coin operated video arcade game introduced by Nolan Bushnell in 1971.

After a short overview of the origins of the PDP-1 machine and the Computer Space game the ICSS document is broken up into Episodes (chapters).  Episode 1 has a brief description of the PDP-1 architecture, the characteristics of the Type 30 Display, an overview of Computer Space, and an outline of how Computer Space might be implemented...

Read more »

lunar_lander.txt

Lunar Lander as of Jan. 15, 2026

text/plain - 47.87 kB - 12/15/2025 at 15:25

Download

  • How Did I Do?

    Michael Gardi5 hours ago 0 comments

    With game mechanics in place it was time to start making Lunar Lander into an actual game, starting with answering the question posed above, "How did I do?".

    Scoring

    I described the scoring scheme from the arcade version of Lunar Lander in my Targets log.  Following their lead I implemented the following:

    1. A "Good Landing" is defined as one where the LEM:
      1. Is upright.
      2. Lands inside a target zone.
      3. Has a vertical velocity is less than 100.     (I will probably tighten these up.)
      4. Has a horizontal velocity is less than 50. 
    2. I did not implement the Hard Landing" for 15 points.
    3. The score for a good landing is 50 points x the target multiplier.
    4. A crash is worth 5 points. (I'm thinking about making this only if the LEM hits a landing zone.)
    5. There is also a 50 "gallon" fuel bonus for a good landing.

    Good Landings

    I had already gotten collision detection working, so now I needed to add code to determine if the LEM was within a "landing zone". To accomplish this I added a couple of tables that enumerated the left and right x coordinates of the six zones. To save me from having to multiply the 50 points bonus x the target multiplier on a machine with no multiply I made a little table with the score value for each target as well.

    / The left endpoints of landing zones.
    lzx,    50                    / 5x
            322                   / 2x
            466                   / 3x
            632                   / 1x
            1320                  / 4x
            1642                  / 5x
            0                     / End of data marker.
    
    / The right endpoints of landing zones.
    rzx,    144                   / 5x
            416                   / 2x
            574                   / 3x
            1104                  / 1x
            1414                  / 4x
            1724                  / 5x
    
    / Scores for successful landings by target multiplier.
    skz,    372                   / 5 x 50 = 250
            144                   / 2 x 50 = 100
            226                   / 3 x 50 = 150
            62                    / 1 x 50 = 50
            310                   / 4 x 50 = 200
            372                   / 5 x 50 = 250
    

    These are of course all in octal format.  I added the following code to make the  "soft landing" call.

    / Check for a "soft" landing. On entry scx will have the "center" x value of the LEM.
           lac rot                / Get the LEM's rotation. Must be upright.
           sas (2                 / Upright?
           jmp sex                /   No - Set for explosion.
           init lx, lzx           /  Yes - Setup for landing zone check. Left point.
           init rx, rzx           / Right point.
           init sc, skz           / Score.
        
    lx,    lac .                  / Get the left landing zone end point.
           sza i                  / Not Zero?
           jmp sex                /   No - End of landing zone table. Set for explosion.
           dac dgx                /  Yes - Save left end.
    rx,    lac .                  / Get the right landing zone end point.
           dac dgy                / Save right end.
        
           lac scx                / Get the x center of the LEM.
           sub dgx                / Check against left edge of landing zone.
           spa                    / Positive?
           jmp nlz                /   No - Point is outside landing zone.
           lac scx                /  Yes - Check right edge of landing zone.
           sub dgy
           sma                    / Negative?
           jmp nlz                /   No - Point is outside landing zone.
           jmp cvv                /  Yes - Hit landing zone check velocity.
    
    nlz,   idx lx                 / Check next landing zone.
           idx rx
           idx sc
           jmp lx
    
    cvv,   lac vy                 /  Yes - Make sure verticle velocity < 100.
           sub (144
           sma                    / Negative?
           jmp sex                /   No - Landing too hard.
           lac vx                 /  Yes - Check horizontal velocity.
           spa                    / Positive?
           cma                    /   No - Compliment want absolute value.
           sub (62                /  Yes - Make sure horizontal velocity < 50.
           sma                    / Negative?
           jmp sex                /   No - Too much drift.

    In the above code a jump to sex is NOT a good thing. 

    Fuel

    The limiting factor in Lunar Lander is fuel. When you run out of fuel the game is over. Implementing fuel ended up being fairly trivial. I created a variable ful and initialized it to 999 "units" (lets say gallons just for fun) at the beginning of a new game. Every time through the main loop I subtract 1 if there is still fuel and the thrust button is pressed. 

    / Process inputs.
    pip,    dap pix                / Set return address.
    ...
    ckt,    dzm thr                / Set thrust to off.
            lac cin                / Refetch the input.
            and (100000            / Check for thrust.
            sza i                  / Is thrust?
            jmp pix                /   No - Keep going.
                    
            lac ful                /  Yes - Apply thrust velocities if fuel.            
            sza i                  / Is fuel?
            jmp pix                /   No - Don't apply velocities.
            lac vy                 /  Yes - Apply velocities (already calculated).            
            add tv
            dac vy
            lac vx
            sub th
            dac vx    
    
            lac ful                / Reduce the fuel by 1.
            sub (1
            dac ful
        
     lac (1 / Indicate that thrust...
    Read more »

  • Refactoring

    Michael Gardi6 days ago 0 comments

    Adding explosions, while satisfying, caused the code to start looking a little disheveled. This was because up till this point the code was pretty linear.

    Default Game Code Running

    • Check Inputs
    • Apply Velocities
    • Show Status
    • Draw Terrain
    • Draw LEM
    • Check Collisions

    With explosions, a number of these steps needed to be bypassed while the explosion animation was playing.

    Explosion Animation Running

    • Show Status (with 0 velocities).
    • Draw Terrain
    • Draw Explosion Frames

    So I started by moving the bulk of the code from my main line off into subroutines (such as they are on a PDP-1). I guess I could have done this sooner, but I found it advantageous to have all of the code in one place while doing initial development.

    Then I took advantage of a PDP-1 feature, Flags. The DEC PDP-1 computer has six independent "program flags," which are user-addressable flip-flops that function as software-controlled switches or synchronizers. The are controlled by the opcodes:

    • stf n - Set the selected program flag where n is a number 1-6.
    • clf n - Clear the selected program flag where n is a number 1-6.
    • szf n - Skip the next instruction if program flag n is zero where n is a number 1-6.

    In addition these flags are mapped to 6 lights on the Console. I wish I had been more aware of Flags in my early development stage as they would have been an easy way to display runtime debugging "codes".

    So with that done here is my current main line.

    / Start a new game.
    a0,     law rcb            / Configure to read control boxes.
            dap rcw
            jmp a2    
    
    a1,     law rtw            / Configure to read testword.
            dap rcw    
            jmp a2                     
    
    / Start over.
    a2,     lac (17500         / Set the LEM at 8,000 meters.
            dac gmy    
            lac (7640          / Center of the game space.
            dac gmx            
            dzm vy             / Initialize velocities to zero.
            dzm vx
            lac (2
            dac rot            / Set LEM to upright position.
    
    / Setup the initial active game components.
            clf 1              / Enable input checking.
            clf 2              / Enable velocity application to game space.
            clf 3              / Enable collision detection.
            clf 4              / Enable draw LEM. Disable explosion.
            clf 5              / Enable show status.
            clf 6              / Enable show targets.
    
    / Main loop. 
    fr0,    load \ict, -1      / Loops about every 100 milliseconds.
        
            lac sec            / Get the second count.
            sub (1             / Reduce by one.
            sza i              / Not Zero?
            jmp dsc            /  No - Do the one second code.
            dac sec            / Yes - Save the new second count.
            jmp inp            / Continue to check inputs.
    
    dsc,    lac (12            / Reset the second counter.
            dac sec                
    
    // Put the one second code here.
    
            lac 5sc            / Check the 5 second counter.
            sub (1
            dac 5sc            / Save result.
            sza                / Is AC Zero?
            jmp inp            /   No - Bypass to check inputs.
            lac (5             /  Yes - Reset 5 second counter.
            dac 5sc                
    
    // Put the five second code here.
        
    / Draw the targets.
            szf 6              / Is the Targets flag zero?
            jmp inp            /   No - Bypass drawing targets.
            jsp dtv            /  Yes - Draw the target landing locations.
    
    / Check for inputs.
    inp,    szf 1              / Is the Input flag 0?
            jmp avl            /   No - Bypass input checks.
            jsp pip            /  Yes - Process inputs.
    
    / Apply velocities to game position.
    avl,    szf 2              / Is the Velocity flag 0?
         jmp dst...
    Read more »

  • Lunar Lander Fall Down Go Boom

    Michael Gardi01/08/2026 at 21:56 0 comments

    Now that I had collision detection working its was time to deal with what happens if the LEM comes in too hot! Before I started implementing an explosion animation I had a look at what my precursors did.  First the venerable Spacewar! 

    Not bad, but as Norbert Landsteiner points out in his ICSS blog Addendum: All New Pyrotechnics of his initial explosions based on Spacewar! they were just  "pretty clouds of saucer dust, almost as if they were painted onto the display". So Mr. Landsteiner set out to do better. 

    I definitely think that he succeeded. The explosion looks great and is much more three-dimensional as is appropriate for a space environment. I can't shake the thought that I have seen something similar on the movie screen.

    I wanted to put my own "signature" on the Lunar Lander explosion. Since LL takes place on the moon I knew that my explosions had to reflect the effect of gravity.  I based the explosion of Norbert's implementation where each particle (48 in my case) had their own persistent delta, but in my code over time gravity is also applied to those deltas.

    My explosion code is broken up into two parts. The first section sets up the explosion "table" with initial start coordinates, plus x and y deltas.

    / Explosion stuff.
        nep=70                / Number of explosion particles.
    exx,    . nep/            / Particle x coordinate.
    exy,    . nep/            / Particle y coordinate.
    edx,    . nep/            / Particle delta x.
    edy,    . nep/            / Particle delta y.
    nel,    0                 / Number of times to go through the explosion loop.
    skp,    0                 / Explosion frames to skip.
    egr,    0                 / Gravity imposed on each particle.
    dx,     0                 / Delta x applied to particles.
    dy,     0                 / Delta y applied to particles.
    
    / Setup for an explosion.
    exp,    dap ext           / Deposit return address.
            law exx           / Load address x origin into AC.
            dap ex1           / Deposit AC in address part at label ex1.
            law exy           / Load address y origin into AC.
            dap ex2           / Deposit AC in address part at label ex2.
            law edx           / Load address x delta into AC.
            dap ex3           / Deposit AC in address part at label ex3.
            law edy           / Load address y delta into AC.
            dap ex4           / Deposit AC in address part at label ex4.
    
            lac xmn           / Explosion starts from the LEM center.
            add xmx           / Calculate center x of bounding box.
            sar 1s            / Divide by 2.
            dac scx           / Save center x.
            lac ymn                
            add ymx           / Calculate center y of bounding box.
            sar 1s            / Divide by 2.
            dac scy           / Save center y.
    
    elp,    random            / Create a random x start offset.
            and (17
            sub (10
            add scx           / Add the x origin.
    ex1,    dac .             / Save the particle x coordinate.
            random            / Create a random y start offset.
            and (17
            sub (10
            add scy           / Add the y origin.
    ex2,    dac .             / Save the particle y coordinate.
    
            random            / New random number.
            and (17           / Reduce to 0 to 15.
            add (1            / Range 1 to 16.        
            dac dy
    
            random            / Get a new random number.
            and (17           / Reduce to 0 to 15.
            sub (10           / Range -8 to 7.
          dac dx / Save as x...
    Read more »

  • Are We There Yet?

    Michael Gardi01/03/2026 at 15:46 0 comments

    Right now the LEM is like a ghost to the terrain, it just slips right through on its way down to the bottom of the screen. Obviously I will need to know when the LEM reaches the ground in order to update the score with a good landing or generate an explosion.  Collision detection is a big part of video games. 

    Conceptually the technique here is pretty simple:

    • create a "bounding box" around the LEM, basically just the coordinates of 4 points just outside the LEM's image on the screen,
    • then for each frame after the LEM has been moved, check to see if any of the terrain pixels lie within with that box.

    Again here is where the screen coordinates, with the origin 0,0 being at the center of the screen, caused me some grief. I was having issues with negative values. It didn't help that the PDP-1 uses 1's compliment for negatives whereas everything else I was familiar with used 2's compliment. So I struggled with my implementation.

    Finally I took a step back and decided that for purposes of collision detection I could use only positive screen coordinates where the x and y axis ranged from 0 to 1023. Once I made that decision the coding took less than an hour to get working. 

    The first thing I did was to add some code in the terrain generation Python script to emit a table with the y values expressed as 0-1023 integers.

    print("y-coordinates")
    count = 0
    for y in smoothed_data:
        print(count,str(int(y)).zfill(3),'\t\t',oct(int(y))[2:].zfill(6))
        count = count+1
    

    It turned out that this approach was faster too because I bypassed having to unpack and convert the Y coordinate from the terrain point. I still converted the values to octal so that they could be inserted into the Lunar Lander code.

    / Y-coordinates of the terrain points in 0-1023 screen coordinates. 
    ypt, 000620
        000601
        000534
        000455
        000404
        000360
        000360
        000360
        000360
        000360
        000360
        000353
        000322
        000253
        000203
        000151
        000143
        000130
        000105
        000061
        000042
        000035
        000036
        000036
        000036
        000036
        000036
        000041
        000052
        000066
        000100
        000105
        000106
        000106
        000106
        000106
        000106
        000105
        000100
        000067
        000057
        000051
        000050
        000047
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000050
        000051
        000061
        000074
        000110
        000117
        000120
        000127
        000142
        000156
        000166
        000170
        000175
        000207
        000223
        000234
        000237
        000240
        000240
        000240
        000240
        000240
        000245
        000266
        000315
        000343
        000357
        000362
        000401
        000430
        000457
        000475
        000500
        000506
        000521
        000535
        000545
        000550
        000547
        000550
        000550
        000550
        000550
        0                   / End of data marker.
    

     Then I added the following code:

    / Check for LEM terrain intersection.
         lac gmx            / Convert the LEM game coordinates to 0-1023 
         sar 3s             /  screen coordinates by dividing by 8.
         dac scx
         lac gmy
         sar 3s
         dac scy
        
         lac scx            / Create a bounding box.
         add (27
         dac xmx
         lac scx
         sub (7
         dac xmn
         lac scy
         add (11
         dac ymx
         lac scy
         sub (22
         dac ymn
    
    / Iterate through the terrain points to see if any intersect with the LEM's bounding box.
         dzm xct            / Zero x coordinate counter.
         init chi,ypt       / Point to first word of terrain y 0-1023 data.
    chi, lac .              / Get the next y coordinate.
         sza i              / Not zero?
         jmp vvl            /   No - Done checking intersections.
         dac yct            /  Yes - Save Y value for further checking.
    
         sub ymx            / Check top of bounding box.
         sma                / Negative?
         jmp nxp            /   No - y is above the bounding box.
    
         lac yct            /  Yes - Check bottom of bounding box.
         sub ymn    
         spa                / Positive?
         jmp nxp            /   No - y is below the bounding box.
    
         lac xct            /  Yes - Check left of bounding box.
         sub xmn        
         spa                / Positive?
         jmp nxp            /   No - y is left of the bounding box.    
    
         lac xct            /  Yes - check right of bounding box.
         sub xmx
         sma                / Negative?
         jmp nxp            /   No - point does not intersect.
    
     jmp a2 / If we get here the point is inside...
    Read more »

  • Targets

    Michael Gardi01/02/2026 at 18:30 0 comments

    The scoring for Lunar Lander worked as follows:

    Base Point Values
    Points are awarded for every landing attempt based on the "softness" of the contact: 

    • Good Landing: 50 points.
    • Hard Landing: 15 points.
    • Crash: 5 points. 

    Landing Requirements

    • To achieve a "Good" landing and avoid a crash, the lander must meet specific telemetry criteria:
    • Vertical Speed: Must be below 15.
    • Horizontal Speed: Must be below 31.
    • Orientation: The module must be nearly vertical and centered on the landing pad. 

    Score Multipliers

    Landing pads are marked with flashing multipliers (e.g., 2x, 3x, 4x, 5x). The base points from the landing quality are multiplied by the pad's value. Narrower or more difficult terrain typically features higher multipliers. 

    Fuel Bonuses and Penalties 
    Fuel acts as the primary resource and "time limit" for the game: 

    • Good Landings: Award a bonus of 50 fuel units.
    • Crashes: Can result in significant fuel penalties or the immediate end of the game if fuel is depleted.

    My next little goal here is to add the "flashing multipliers" to the screen. Since we are just displaying numbers and the letter x most of the code necessary to do this is already in place. Frankly the hard part was tweaking the coordinates used to display the multipliers so that they were directly under the target zone and centered. 

    Here is the code:

    / Draw the target values.
    dtv,    lac (565000            / First target 5x.
            dac dgy
            lac (436000            
            dac dgx
            lac (5                 / 5
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16
    
            lac (414000            / Second target 2x.
            dac dgy
            lac (557000            
            dac dgx
            lac (2                 / 2
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16
    
            lac (440000            / Third target 3x.
            dac dgy
            lac (637000            
            dac dgx
            lac (3                 / 3
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16
    
            lac (421000            / Fourth target 1x
            dac dgy
            lac (764000            
            dac dgx
            lac (1                 / 1
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16
    
            lac (514000            / Fifth target 4x.
            dac dgy
            lac (155000            
            dac dgx
            lac (4                 / 4
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16
    
            lac (661000            / Sixth target 5x.
            dac dgy
            lac (331000            
            dac dgx
            lac (5                 / 5
            dac num
            jsp ddg
            lac dgx
            add (27000
            dac dgx                / x
            jsp c16

    The task of aligning the target values is exacerbated by the necessity of expressing the starting...

    Read more »

  • Let There Be Land

    Michael Gardi01/01/2026 at 18:30 0 comments

    Right now my Lunar Lander looks a lot like Spacewar! and Computer Space, a ship flying around in the blackness of space. What makes Lunar Lander unique is the moonscape at the bottom of that screen. One might say that Lunar Lander is more grounded as a result.

    I use the term "moonscape" loosely. In the original, the ground is just a 2D low detail representation of some hilly terrain drawn using vector graphics as a single solid line. Never the less the way that line smoothly transitions as it changes direction "feels" like the land it represents.

    On the PDP-1, I do not have the pixel "budget" to draw a curvy solid line from the left to the right side of the screen. That would be over a thousand pixels. At most the PDP-1 is only capable of refreshing a few hundred pixels each frame. Since the terrain is an important part of the game I think a hundred pixels is a reasonable allocation for drawing it.

    So generate some "realistic" looking terrain using only a hundred pixels. No Problem. 

    Actually it turns out there is a LOT of information on generating 2D and 3D terrain, mostly coming out of the gaming community.  There are a number of techniques for taking some height samples and smoothing them into something that mimics "land" to the eye. One of these is cosine interpolation.  

    Cosine interpolation is a smooth, curved method for estimating values between two points, using a portion of a cosine wave to create a natural-looking transition. It is often used to generate more aesthetically pleasing curves for periodic or oscillating data.

    I was lucky enough to find a Python script that was exactly tailored to my needs. I spent considerable time trying to re-locate the source so that I could credit them, but was unsuccessful. So to the author: Thank you so much.

    import numpy as np
    import matplotlib.pyplot as plt
    
    def cosine_interpolation(y1, y2, mu):
        """
        Performs cosine interpolation between two points.
        
        Args:
            y1 (float): The value of the first point.
            y2 (float): The value of the second point.
            mu (float): The interpolation factor (0.0 to 1.0) indicating the
                        relative distance between the points.
                        
        Returns:
            float: The interpolated value.
        """
        mu2 = (1 - np.cos(mu * np.pi)) / 2
        return y1 * (1 - mu2) + y2 * mu2
    
    def smooth_terrain_with_cosine(data, num_new_points):
        """
        Smooths 1D terrain data by interpolating new points between existing ones
        using cosine interpolation.
    
        Args:
            data (np.array): The original 1D terrain data.
            num_new_points (int): The total number of points desired in the smoothed data.
    
        Returns:
            np.array: The new, smoothed terrain data.
        """
        # Original data points indices
        x_old = np.linspace(0, 1, len(data))
        
        # New points indices
        x_new = np.linspace(0, 1, num_new_points)
        y_new = np.zeros(num_new_points)
        
        # Calculate the number of original data points
        n_original = len(data)
    
        for i, x_val in enumerate(x_new):
            # Find the two nearest original data points
            # np.searchsorted finds the index where x_val would be inserted to maintain order
            idx_right = np.searchsorted(x_old, x_val, side='right')
            
            # Handle boundary cases for the last point
            if idx_right == n_original:
                y_new[i] = data[-1]
                continue
                
            idx_left = idx_right - 1
    
            # Get the y values of the surrounding points
            y1 = data[idx_left]
            y2 = data[idx_right]
            x1 = x_old[idx_left]
            x2 = x_old[idx_right]
            
            # Calculate the interpolation factor (mu)
            mu = (x_val - x1) / (x2 - x1)
            
            # Perform cosine interpolation
            y_new[i] = cosine_interpolation(y1, y2, mu)
            
        return y_new
    
    # --- Example Usage ---
    
    # 1. Generate some sample terrain data (e.g., random heights at fixed intervals)
    original_data = [400, 240, 240, 100, 30, 30, 70, 70, 40, 40, 40, 40, 80, 120, 160, 160, 240, 320, 360,360]
    
    # 2. Define the desired total number of points for the smoothed terrain
    smoothed_points_count = 100
    
    # 3. Smooth the data
    smoothed_data = smooth_terrain_with_cosine(original_data, smoothed_points_count)
    #print(smoothed_data)
    
    # 4. Plot...
    Read more »

  • Flying The LEM

    Michael Gardi12/31/2025 at 21:05 0 comments

    All the pieces are now in place to convert the LEM "elevator" into a space ship.  First of all you have to be able to change the direction that the LEM is pointing in. There are 5 possible orientations. To use a compass analogy they are West, North West, North, North East, and East. The LEM is not allowed to point downward (yet?).  I assigned a rotation number to each orientation.

    I covered inputs in the previous log, but here is the complete input code including the LEM rotation controls.

    / Check for inputs.
            cla               / Clear the accumulator.
    rcw,    jsp .             / Call the input routine.
            dac cin           / Save as current input.
            rar 4s            / Combine plyaer 1 and player 2 inputs.
            ior cin    
            dac cin
    
            and (400000       / Check for left.
            sza i             / Is left?
            jmp ckr           /   No - Check for right bit.
        
            lac pin           /  Yes - Check the previous input.
            and (400000            
            sza               / Is previous input left?
            jmp ckr           /  Yes - Wait for left bit to be cleared.
            lac rot           / Get the current rotation.
            sza i             / Already at 0?
            jmp ckr           /  Yes - Check for right bit.
    
            sub (1            / Rotate counter-clockwise by one.
            dac rot           / Save change.
            jmp ckt           / Skip right bit check.
    
    ckr,    lac cin           / Refetch the input.
            and (200000       / Check for right.
            sza i             / Is right?
            jmp ckt           /   No - Check for thrust bit.
    
            lac pin           /  Yes - Check the previous input.
            and (200000            
            sza               / Is previous input right?
            jmp ckt           /  Yes - Wait for right bit to be cleared.
            lac rot           / Get the current rotation.
            sub (4            / 
            sza i             / Already at 4?
            jmp ckt           /  Yes - Check for thrust bit.
            lac rot           /   No - Update rotation.
            add (1            / Rotate clockwise by one.
            dac rot           / Save change.
    
    ckt,    lac cin           / Refetch the input.
            dac pin           / Move to previous input.
            and (100000       / Check for thrust.
            dac thr
            sza i             / Is thrust?
            jmp ckm           /   No - Keep going.
    
            lac vy            /  Yes - Apply thrust velocities.
            add tv
            dac vy
            lac vx
            sub th
            dac vx     
    
    / Apply velocties to game position.
    ckm,    

    It's pretty straight forward except for the extra checks for left and right rotation to force the player to click the button for each rotation change. Without this check the LEM would always rotate to the end stop before the user could release the button.  There are also end stop checks that keep the rotation value rot in the 0-4 range.

    You can see in this code that there is a new velocity vector vx to keep track of horizontal movement, plus velocity change values in the variables tv (thrust vertical) and th (thrust horizontal).

    A lot depends on the orientation of the LEM:

    • What LEM and exhaust bitmaps to display.
    • The offset to display the exhaust bitmap relative to the LEM.
    • The thrust allocated to vertical and horizontal velocities.

    This orientation checking work is performed at the front end of the draw the LEM subroutine.

    / Setup to display the LEM, exhaust, and apply velocity.
    lt1,    jmp . 
    lm1,    dap lt1
         lac scx            /...
    Read more »

  • Pushing Back

    Michael Gardi12/30/2025 at 17:26 0 comments

    I started to feel sorry for the poor little LEM falling helplessly to the bottom of the screen so it was time to give it a rocket engine. The engine would provide a certain amount of thrust. 

    Thrust is a vector quantity because it has both magnitude (strength) and direction. Given the shape of the LEM, the direction of the thrust is pretty obvious, and the LEM will move in the opposite direction to the thrust (at least according to Newton :-)  But what should the magnitude be?  Since I'm not a rocket scientist the math escapes me, but from the Lunar Lander game prospective the thrust has to be "stronger" than the force of gravity being exerted on the LEM or there could be no soft landing. In the end I just picked a value, four times that of lunar gravity, that would be applied to the velocity of the LEM each time through the game loop (at the same frequency as gravity), provided of course the rocket is operational.

    Now each time through the vertical velocity calculation part of the the game loop it does this.

    acu,    lac vy            / Increase velocity by the acceleration of moon's gravity.
            add lac           / Add acceleration due to gravity. 
            dac vy            / Save updated value.
    
    ckt,    lac cin           / Refetch the input.
            and (100000       / Check for thrust.
            sza i             / Is thrust?
            jmp ckm           /   No - Keep going.
    
            lac vy            /  Yes - Apply thrust velocity.
            sub tac           / Apply the rocket thrust.           
            dac vy            / Save updated value.
    ckm,
    

    Where Does the Game Input Come From?

    There are two primary ways that a standard PDP-1 can get external input suitable for game play. 

    Front Panel Switches

    There is a row of 18 toggle switches on the front of the PDP-1 Control Panel labeled Test Word.

    You can read the value of these switches with a special opcode lat which loads the switch values into the AC register. A switch in the up position will register as 0, and 1 if in the down position. For Spacewar! Player 1 would used the leftmost 4 switches, and Player 2 the rightmost 4 switches.

    Game Controllers

    Game controllers were not part of the original, standard equipment of the PDP-1 computer. The computer was typically controlled using switches on its console and a typewriter. However, a custom-built external gamepad was developed later by users specifically to play the game Spacewar!.  For my purposes the PDP-1 at the Computer History Museum does in fact have a pair of game controllers. On the left below is a reproduction of an original controller from 1962 and on the right the game controller that ships with the PiDP-1 Replica in a custom case.

    Controllers were wired directly to the PDP-1's I/O system, and could be read by issuing an "iot 11" instruction which returned the switch values in the IO register. By convention the bits returned by the controllers were as follows. 

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

    Often the fifth switch "hyperspace" was indicated by setting both the left and right rotation bits to on simultaneously. 

    My Lunar Lander game can work with either the front panel switches or game controllers. Using an ICSS technique, the interface to the inputs is the same for either method. Furthermore since Lunar Lander is a single player game, the Player 1 and Player 2 bits are combined so it doesn't matter what controller is being used....

    Read more »

  • Display The State

    Michael Gardi12/28/2025 at 21:20 0 comments

    When I was working as a software developer our team would often talk about "the long pole in the tent", meaning "the most important issue or problem that prevents or slows progress". This is especially true in a new project like Lunar Lander for the PDP-1. Of course as each "long pole" gets understood and resolved, the next longest takes its place.

    So far displaying the LEM and implementing gravity felt like the most important features. For me the next "longest pole" was how to display the current state of the game. Arcade Lunar Lander showed the game "status" in the upper corners of the screen.

    There was an optional piece of hardware that provided an automated way to display alphanumeric and special characters on the Type 30 Display, the Type 33 Symbol Generator. This saved valuable processing time that would otherwise be spent by the main CPU constantly drawing each character from scratch.  Unfortunately, for the default Type 30 Display that I am targeting, drawing text requires the programmer (me) to write software to generate the character shapes as a series of points, rather than using a dedicated hardware circuit. And to reiterate, with no "frame buffer" memory this means redrawing each character each frame. So to that end, drawing text needs to be as efficient as possible.

    I mentioned back in the Drawing the LEM log post that for the ICSS project Norbert Landsteiner "compiled" small 5 x 7 character bitmaps directly into PDP-1 opcodes, a very efficient way of rendering a bitmap of dots to the screen. I will do the same with one small difference. 

    Norbert's code had each character's bitmap (0-9,A,B,C,D,E,F) coded as two 18-bit words (7 rows of 5-bits with a bit left over) of data.  His character compiler, written in PDP-1 assembler, was part of the ICSS code and run at program initialization. Each character was compiled into "machine" code which was stored at the "end" of the program space,  with the code's start address stored in a lookup table. 

    What I ended up doing was writing a Python script to convert the 5x7 character bitmaps into assembler which I inserted into the main Lunar Lander code. 

    CHARS = [
       [[' ', 'x', 'x', 'x', ' '],  #0
        ['x', ' ', ' ', ' ', 'x'],
        ['x', ' ', ' ', ' ', 'x'],
        ['x', ' ', ' ', ' ', 'x'],
        ['x', ' ', ' ', ' ', 'x'],
        ['x', ' ', ' ', ' ', 'x'],
        [' ', 'x', 'x', 'x', ' ']],
        
    ...
        
       [[' ', 'x', 'x', 'x', ' '],  #9
        ['x', ' ', ' ', ' ', 'x'],
        ['x', ' ', ' ', ' ', 'x'],
        [' ', 'x', 'x', 'x', 'x'],
        [' ', ' ', ' ', ' ', 'x'],
        [' ', ' ', ' ', 'x', ' '],
        [' ', 'x', 'x', ' ', ' ']]
    ]
    
    scale = 4
    for d_idx, digit in enumerate(CHARS):
        has_plotted = False
        print(f"c{d_idx},\tlio dgy\t\t\t\t/{d_idx}")
        print("\tlac dgx")
        for r_idx, row in enumerate(digit):
            # Process the next row.
            last_x = 0
            for c_idx, char in enumerate(row):
                if char == 'x':
                    if (c_idx - last_x) > 0:
                        diff = oct((scale*(c_idx - last_x))<<8)[2:]
                        print(f"\tadd ({diff}")
                    last_x = c_idx
                    if has_plotted:
                        print("\tioh")
                    print("\tdpy-i 4300")
                    has_plotted = True
            # Skip to the next row if there is one.
            if r_idx <= len(row):
                print("\tlac dgx")
                print("\trcr 9s")
                print("\trcr 9s")
                step = oct(scale<<8)[2:]
                print(f"\tsub ({step}")
                print("\trcr 9s")
                print("\trcr 9s")
                
        print(f"\tjmp ndg\n")
    

    Which produced:

    c0, lio dgy                /0
        lac dgx
        add (2000
        dpy-i 4300
        add (2000
        ioh
        dpy-i 4300
        add (2000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        ioh
        dpy-i 4300
        add (10000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        ioh
        dpy-i 4300
        add (10000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        ioh
        dpy-i 4300
        add (10000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        ioh
        dpy-i 4300
        add (10000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        ioh
        dpy-i 4300
        add (10000
        ioh
        dpy-i 4300
        lac dgx
        rcr 9s
        rcr 9s
        sub (2000
        rcr 9s
        rcr 9s
        add (2000
     ioh
    ...
    Read more »

  • Moving The LEM

    Michael Gardi12/23/2025 at 22:09 0 comments

    Getting the LEM to show up on the screen was cool but pretty boring. I decided that my next task was to get the LEM moving around a little. Since this game is supposed to represent a moon landing I thought gravity might be a good place to start.

    Acceleration due to gravity on the moon is 1.625 meters per second squared (m/s²).  It's pretty straight forward.

    Time (t) in seconds Velocity (V) in m/s
    00.00
    11.625
    23.25
    34.875
    ......

    For each second the velocity of the LEM increases by 1.625 meters (m/s). So I will have to have a "one second" loop. Each time through the loop I would increase the velocity by the acceleration, then move the LEM downward by "velocity" meters. Simple right? Only three problems:

    1. How do you implement a timing loop on a PDP-1 with no built-in timer circuits or hardware assistance for general purpose timing?
    2. What does a "meter" mean in the game?
    3. How do you express 1.625 on a system that has no floating point numbers?

    Game Space

    I'll start with 2 first. A meter in game space is whatever I want it to be. My first thought was to let each "pixel" on the screen represent a 10 x 10 meter square. Since the screen has 1024 x 1024 pixels, the game space would then be 10,240 x 10,240 meters square or about 10 km square.  It felt about right to have the LEM make its final decent from a 10 km height. Then I realized that converting from game space to screen space would be an issue with no HW division so I adjusted my pixel "size" to be 8 x 8 meters so that converting to screen from game space could be accomplished with a simple "shift right 3" (divide by 8) operation.  

    The only elephant in the room with this scheme is that the LEM, which is being drawn right now in about a 30 x 30 pixel grid, would be 240 meters x 240 meters in game space. An actual LEM is only 7 meters tall and 9.4 meters wide or about 1 pixel in screen space, not very interesting. At this point it really sinks in that this is just a GAME. As such I can simply state that the LEM on screen is being viewed through a telescope. There fixed. I'm sure there will be many other tweaks to "reality" made in order to get Lunar Lander working on a PDP-1.  

    Timing Loop

    The "standard" PDP-1 did not have a default timer interrupt; this function required an optional hardware module. Since I am writing towards a basic PDP-1, I will need another way to create a "one second" timing loop. 

    A timing loop on the PDP-1 computer with no HW timer relies on the fixed execution times of instructions, which are multiples of the basic 5-microsecond memory cycle. You can create a fairly accurate delay loop of any length using this technique, but the problem is that It's hard to do anything else while the delay is running. Not very useful for a high speed interactive game.

    It should be no surprise that I will adopt Norbert Landsteiner's approach in the ICSS code. Based on his code (copied, cough, cough) my loop looks something like:

    define load A,B
         lio (B              / Load the constant B into the IO register.
         A                   / Save that value into A.
         term
    
    define count A,B
         isp A               / Increase the value of A by 1 and skip
         jmp B               /  next instruction of result >= 0.
         term
    
    sec, 12                  / Initialize to 10 loops per second.
    
    / Main loop. 
    fr0, load \ict, -1       / Will loop about every 100 milliseconds.
        
         lac sec             / Get the second count.
         sub (1              / Reduce by one.
         sza i               / Not Zero?
         jmp dsc             /  No - Do the one second code.
         dac sec             / Yes - Save the new second count.
         jmp acu             / Continue with acceleration calculation.
    
    dsc, lac (12 / Reset...
    Read more »

View all 13 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates