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

  • Cleanup And Optomization

    Michael Gardi6 hours ago 0 comments

    I wanted to review the code I had written. When I started this project I was a complete noob to both the PDP-1 machine and assembly language. After about a month's effort I felt that I was beginning to "get it".

    So I went back and reviewed an early piece of code, drawing the LEM, and sure enough there were a number of improvements to be had.

    / Draw the LEM based on an 18-bit by 18 word bitmap.
       
       
            OLD                                    NEW
            ===                                    ===    
    
                                                   lac (400000 
                                                   dac bit
                                                   
                                                   lac bsx
                                                   dac tmp
    
    lbn,    lac .                          lbn,    lac .       
            sza i                                  sza i       
            jmp lt1                                jmp lt1     
    
            dac bmr                                dac bmr     [-20]
            lac (400000         
            dac bit                        
                                           
    nb0,    lac bit                        nb0,    lac bit     [0]
            and bmr                                and bmr     
            sza i                                  sza i       
            jmp nb1                                jmp nb1     
                                           
            lac bsx                                lac bsx     
            lio bsy                                lio bsy
            dpy-i 4700                             dpy-i 4700
                                               
    nb1,    lac bit                        nb1,    lac bit     [-10]
            sad (1                                 rar 1s      
            jmp nrw                                dac bit 
                                                   sad (400000 
            lac bit                                jmp nrw     
            rar 1s                         
            dac bit                                  
                                                  
            lac bsx                                lac bsx  
            add scl                                add scl   
            dac bsx                                dac bsx
            jmp nb0                                jmp nb0  
                                           
    nrw,    lac lbn                        nrw,    idx lbn     [-50]  
            add (1                         
            dac lbn                                
                                                   
            lac bsy                                lac bsy    
            sub scl                                sub scl
            dac bsy                                dac bsy 
                                                   
            lac scx                                lac tmp 
            sub (4400                              dac bsx 
            dac bsx 
                                                   jmp lbn 
            lac (400000         
            dac bit
            jmp lbn

    I'm not going to go into detail on the code above. Suffice it to say that I'm pretty happy with the improvements. The important thing is the numbers off to the right. They represent the number of microseconds I was able to eliminate from the OLD code when creating the NEW. Both of course do exactly the same thing.

    PDP-1 instruction have the following performance characteristics:

    • Memory Cycle Time: The fundamental memory cycle time of the PDP-1 is 5 microseconds.
    • Instruction Speed: The jmp instruction, along with other single-cycle instructions, requires only one memory reference and completes within that 5-microsecond cycle.
    • Indirect Addressing: If a jmp instruction uses indirect addressing (indicated by a 1 in the defer bit), it requires an additional 5 microseconds for each level of redirection.
    • Comparison: Unlike jmp, two-cycle instructions (such as add, sub, deposit, or load) take 10 microseconds to complete. 

    In the above code comparison the numbers on the right indicate the how many "microseconds" were removed from that block of the code (demarked by the labels). Another thing to consider is how any times that block of code will be executed when drawing the LEM.  The lbn and nrw blocks only execute once per row of the bitmap, while the nb1 code executes once per pixel. So the total savings for optimizing this code is:

    (20 + 50) x 18 rows =          1,260 microseconds
    10 x 18 columns x 18 rows =    3,240 microseconds
                                   ==================
                                   4,500 microseconds

    4.5 ms every time the LEM is drawn which is about 10 times per second.  That doesn't sound like much but it leaves more time to draw the other pixels and thus reduce flicker (say).

  • Finally A Playable Game

    Michael Gardi10 hours ago 0 comments

    I integrated the NEW GAME attract screen, the GAME OVER screen, and Good Landing animation with the gameplay stuff and with all of this is place you can now play Lunar Lander. The flow:

    1. Boot up the game.
    2. From the Attract Mode NEW GAME screen press a button on the game controller. Gameplay starts.
    3. Attempt to make as many Good Landings on high value target sites as fuel allows to increase your score.
    4. When you run out of fuel you will probably crash one last time (for 5 points ;-) unless you were lucky to be very close to a soft landing.
    5. From the GAME OVER screen observe your score then press a button to go back to 2.

    Of course you will need a PDP-1 to play this version of Lunar Lander. Before I received my physical Console PiDP-1, I was able to test early versions of the game using the Obsolescence Guaranteed virtual PDP-1 console by toggling the virtual Test Word switches with mouse clicks.  In all honesty this was pretty frustrating and unsatisfying, but for testing purposes it was great.

    You can find this latest playable version in the Files section of this project.

  • Game Wrappers

    Michael Gardia day ago 0 comments

    Arcade games usually had an Attract Mode screen that was shown when the game was not actually being played. The screen often had some animation that showed what the game was about, sometimes simple instructions, and for sure had the words INSERT COINS.  Lunar Lander was no exception, and showed the lunar landscape scrolling right to left and the LEM flying across the top of the screen. 

    I wanted to do something similar so I started by figuring out how I was going to add text to the center of he screen. 

    Now I already had a mechanism for drawing characters to the screen that involved "compiling" the letter images into assembler code, so I compiled the extra letters I wanted and added them to the code. As I have explained in a previous log Display The State this produces incredibly fast code, but takes up a lot of the 4K words of memory that is available on a stock PDP-1. Sure enough when I added  the new compiled letters I "blew" memory.  So I started down the path of writing a utility to encode the individual letters into bitmap data that could be drawn to the screen like I did with the LEM images. Before I got too far though, I realized that it was going to be quite a bit of work outputting the letters individually so I decided to make a larger bitmap with the whole message that I wanted to show. In hindsight since these messages are going to be shown on a "static" screen anyway, the speed of compiled letters would have been overkill.

    My other problem was that showing the terrain, moving LEM, and text message on the screen was pushing the number of pixels the poor little PDP-1 could write to the screen in a single frame without introducing annoying flickering. Now I realize that Lunar Lander for the PDP-1 does flicker somewhat. It can't be helped because as I have mentioned several times, the Type 30 display is not memory mapped and therefore all pixels have to be drawn to the screen every frame.  But there is a certain threshold where the flickering gets to be distracting. So the solution was to keep the message short. NEW GAME would have to do.

    I wrote a Python script to encode the bitmap into octal data words.

    NEW_GAME = [ 
        ['x', ' ', ' ', ' ', 'x', ' ', 'x', 'x', 'x', 'x', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', 'x', 'x', ' ', ' ', ' ', 'x', 'x', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', ' ', 'x', 'x', 'x', 'x', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', 'x', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', ' ', 'x', ' ', 'x', ' ', 'x', 'x', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', ' ', 'x', 'x', 'x', ' ', 'x', 'x', 'x', 'x', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', 'x', 'x', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', ' ', ' ', 'x', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
        ['x', ' ', ' ', ' ', 'x', ' ', 'x', 'x', 'x', 'x', 'x', ' ', ' ', 'x', ' ', 'x', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'x', 'x', 'x', ' ', '...
    Read more »

  • How Did I Do?

    Michael Gardia day 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 Gardi01/10/2026 at 20:00 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 »

View all 16 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