The nostalgia of 80s computers is something I have never stopped feeling. As a child, I was fortunate to use several 8-bit computers, such as the ZX-81, ZX-Spectrum, Apple II, and MSX, or rather, their national clones (TK-85, TK-90X, TK2000), since Brazil was experiencing the era of the Informatics Market Reserve.

In a time when there was no Internet, we spent our time typing programs. A series of books on game programming was published by the Lutécia publishing house in Brazil, but the original American versions were released by Usborne.

In this article, I will translate the main game from Computer Battlegames, called Missile, from Apple II Basic to Python with Pyglet. The original listing is on page 34 of the English book (see the PDF above).

Let’s see the game running in an emulator: {

Commented version:

    # Clears the screen
    10 HOME
    # Enters high-resolution mode 280x192
    20 HGR
    # Selects color 3 (purple/magenta)
    30 HCOLOR=3
    # Creates two vectors with 3 elements each
    40 DIM Y(3),F(3)
    # Initializes N equal to 1 and MS equal to 5
    50 N=1 : MS=5
    # Generates a random number between 0 and 6 + 4
    60 PS=INT(RND(1)*6+4)
    # Generates a random number between 0 and 135 + 11
    70 P=INT(RND(1)*135+11)
    # Jumps to subroutine
    80 GOSUB 400
    # Loop: repeats I from PS to 265, incrementing by PS
    90 FOR I=PS TO 265 STEP PS
    # Calculates the values of X, Y, and C. Jumps to subroutine at 300
    100 X=I-PS : Y=159-P : C=0 : GOSUB 300
    # Sets X and C. Jumps to subroutine 300
    110 X=I : C=3: GOSUB 300
    # Checks if a key has been pressed. If so, stores in F
    120 F$="" : IF PEEK(-16384)>127 THEN GET F$
    # If something was pressed or if N>3 jumps to 160
    130 IF F$="" OR N>3 THEN 160
    # F[N] = 1
    140 F(N)=1
    # N+=1
    150 N=N+1
    # Repeats J from 1 to 3
    160 FOR J=1 TO 3
    # Resets C and jumps to subroutine at line 350
    170 C=0 : GOSUB 350
    # If F[J]==0 or Y[J]>145 jumps to 230
    180 IF F(J)=0 OR Y(J)>145 THEN 230
    # Y[J]+=MS
    190 Y(J)=Y(J)+MS
    # C=3. Jumps to subroutine at line 350
    200 C=3 : GOSUB 350
    # Calculates X and Y
    210 X=J*70-I : Y=P-Y(J)
    # If X>-1 and X<15 and Y>-9 and Y<5 jumps to 270
    220 IF X>-1 AND X<15 AND Y>-9 AND Y<5 THEN 270
    # End of the loop, returns to the for loop at line 160
    230 NEXT
    # End of the loop, returns to the for loop at line 90
    240 NEXT
    # Positions the cursor at line 22 and prints MISSED
    250 VTAB 22 : PRINT "MISSED"
    # Ends the program
    260 END
    # Positions the cursor at line 22 and prints HIT!!!
    270 VTAB 22 : PRINT "HIT!!!"
    # Ends the program
    280 END
    # Changes the drawing color to C
    300 HCOLOR=C
    # Draws a line from X,Y to X,Y-8
    310 HPLOT X,Y TO X,Y-8
    # Continues the line
    320 HPLOT TO X+3,Y-2 : HPLOT TO X+12, Y-2
    330 HPLOT TO X+14,Y : HPLOT TO X,Y
    # Returns from the subroutine
    340 RETURN
    # Changes the drawing color to C
    350 HCOLOR=C
    # Draws line
    360 HPLOT 70*J,158-Y(J) TO 70*J,154-Y(J)
    # Returns from the subroutine
    340 RETURN
    # Changes the color to C
    350 HCOLOR=C
    # Draws line
    360 HPLOT 70*J, 158-Y(J) TO 70*J,154-Y(J)
    # Returns from the subroutine
    370 RETURN
    # Repeats J from 1 to 3
    400 FOR J=1 TO 3
    # Draws line
    410 HPLOT 70*J-5,159 TO 70*J+5,159
    # End of the loop, returns to line 400
    420 NEXT
    # Returns from the subroutine
    430 RETURN

Well, since we will need to draw, let’s install Pyglet:

pip3 install pyglet

The first thing to do is to create the Pyglet window and separate the game window itself. In this case, the window is responsible for receiving keyboard events. The OpenGL configuration also needs to be done here. Since the high resolution of the Apple II is very small at 280x192 points, I am using a scale of 4. This way, the coordinates will remain the same, but the graphics will be 4 times larger. If it gets too big on your monitor, you can adjust the scale.

I tried to keep the original comments to make it easier to relate the new code to the old one. Another change is the Y coordinates. On the Apple II, Y = 0 is the first line, and in OpenGL, it is the last.

Let’s see how the window code turned out:

    class Missile(pyglet.window.Window):
        # 20 HGR
        def __init__(self):
            self.scale = 4    # Scales the graphics 4x
            self.frames = 8   # Frames per second.
                              # Defines how many times we will update the screen per second
            super(Missile, self).__init__(280 * self.scale,
                                          192 * self.scale,
                                          caption="Missiles")
            self.set_mouse_visible(False)
            self.game = Game(self)
            self.schedule = pyglet.clock.schedule_interval(
                func=self.update, interval=1.0 / self.frames)

        def update(self, interval):
            pass

        def on_draw(self):
            window.clear()
            self.game.update()

        def on_resize(self, width, height):
            # Initializes the view
            glViewport(0, 0, width, height)
            glMatrixMode(gl.GL_PROJECTION)
            glLoadIdentity()
            # Inverts the Y-axis coordinates
            glOrtho(0, width, height, 0, -1, 1)
            # Applies the scale
            glScalef(self.scale, self.scale, 1.0)
            glMatrixMode(gl.GL_MODELVIEW)

        def on_key_press(self, symbol, modifiers):
            # Checks if a key has been pressed. If so, stores in F
            # 120 F$="" : IF PEEK(-16384)>127 THEN GET F$
            self.game.pressed = True

        def create_label(self, message, x=None, y=None,
                      font='Times New Roman', size=36):
            """Prepares a text message to be displayed"""
            x = x or self.width // 2
            y = y or self.height // 2
            return pyglet.text.Label(
                message, font_name=font, font_size=size,
                x=x, y=y, anchor_x='center', anchor_y='center')

In addition, the colors of the Apple II need to be defined:

    def rgb_to_f(r, g, b):
        return(r / 255.0, g / 255.0, b / 255.0)

    # Colors of high-resolution mode HGR
    # Source: https://github.com/AppleWin/AppleWin/issues/254
    COLORS = [(0.0, 0.0, 0.0),  # Black 0
             rgb_to_f(20, 245, 60),  # Green 1
             rgb_to_f(255, 68, 253),  # Magenta 2
             rgb_to_f(255, 255, 255),  # White 3
             rgb_to_f(255, 106, 60),  # Orange 5
             rgb_to_f(20, 207, 253),  # Medium Blue 6
             rgb_to_f(255, 255, 255),  # White 7
             ]

And finally, the Game class with the game itself. Note that the indices in Python start at 0 and in Basic start at 1. First step of the conversion:

    class Game():
        def __init__(self, parent):
            self.color = COLORS
            self.window = parent
            # Creates two vectors with 3 elements each
            # 40 DIM Y(3),F(3)
            self.Y = [0] * 3
            self.F = [0] * 3
            # Initializes N equal to 1 and MS equal to 5
            # 50 N=1 : MS=5
            self.N = 0
            self.MS = 5
            # Generates a random number between 0 and 6 + 4
            # 60 PS=INT(RND(1)*6+4)
            self.PS = random.randint(0, 6) + 4
            # Generates a random number between 0 and 135 + 11
            # 70 P=INT(RND(1)*135+11)
            self.P = random.randint(0, 135) + 11
            self.state = "playing"
            self.I = self.PS
            self.pressed = False
            self.label = None

        def update(self):
            if self.state == "playing":
                self.play()
            else:
                if self.label:
                    glMatrixMode(gl.GL_PROJECTION)
                    glLoadIdentity()
                    glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                    glMatrixMode(gl.GL_MODELVIEW)
                    self.label.draw()
                if self.pressed:
                    pyglet.app.event_loop.exit()

        def play(self):
            # Jumps to subroutine
            # 80 GOSUB 400
            self.sub_400()
            # Loop: repeats I from PS to 265, incrementing by PS
            # 90 FOR I=PS TO 265 STEP PS
            if self.I > 265:
                self.sub_250()
                return
            # Calculates the values of X, Y, and C. Jumps to subroutine at 300
            # 100 X=I-PS : Y=159-P : C=0 : GOSUB 300
            self.X = self.I - self.PS
            self.y = 159 - self.P
            self.C = 0
            self.sub_300()
            # Sets X and C. Jumps to subroutine 300
            # 110 X=I : C=3: GOSUB 300
            self.X = self.I
            self.C = 3
            self.sub_300()
            # If something was pressed or if N>3 jumps to 160
            # 130 IF F$="" OR N>3 THEN 160
            if self.pressed and self.N < 3:
                # F[N] = 1
                # 140 F(N)=1
                self.F[self.N] = 1
                # N+=1
                # 150 N=N+1
                self.N += 1
            self.pressed = False
            # Repeats J from 1 to 3
            # 160 FOR J=1 TO 3
            for self.J in range(0, 3):
                # Resets C and jumps to subroutine at line 350
                # 170 C=0 : GOSUB 350
                self.C = 0
                self.sub_350()
                # If F[J]==0 or Y[J]>145 jumps to 230
                # 180 IF F(J)=0 OR Y(J)>145 THEN 230
                if self.F[self.J] == 0 or self.Y[self.J] > 145:
                    continue
                # Y[J]+=MS
                # 190 Y(J)=Y(J)+MS
                self.Y[self.J] += self.MS
                # C=3. Jumps to subroutine at line 350
                # 200 C=3 : GOSUB 350
                self.C = 3
                self.sub_350()
                # Calculates X and Y
                # 210 X=J*70-I : Y=P-Y(J)
                self.X = (self.J + 1) * 70 - self.I
                self.y = self.P - self.Y[self.J]
                # If X>-1 and X<15 and Y>-9 and Y<5 jumps to 270
                # 220 IF X>-1 AND X<15 AND Y>-9 AND Y<5 THEN 270
                if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                    self.sub_270()
                # End of the loop, returns to the for loop at line 160
                # 230 NEXT
                # End of the loop, returns to the for loop at line 90
                # 240 NEXT
            # 90 FOR I=PS TO 265 STEP PS
            self.I += self.PS

        def sub_250(self):
            # Positions the cursor at line 22 and prints MISSED
            # 250 VTAB 22 : PRINT "MISSED"
            print("MISSED")
            self.print_message("MISSED")
            # Ends the program
            # 260 END
            self.change_state("gameover")

        def sub_270(self):
            # Positions the cursor at line 22 and prints HIT!!!
            # 270 VTAB 22 : PRINT "HIT!!!"
            print("HIT")
            self.print_message("HIT!!!")
            # Ends the program
            self.change_state("gameover")

        def sub_300(self):
            # Changes the drawing color to C
            # 300 HCOLOR=C
            self.set_color(self.C)
            # Draws a line from X,Y to X,Y-8
            # 310 HPLOT X,Y TO X,Y-8
            # Continues the line
            # 320 HPLOT TO X+3,Y-2 : HPLOT TO X+12, Y-2
            # 330 HPLOT TO X+14,Y : HPLOT TO X,Y
            pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                                 ('v2i', (self.X, self.y,
                                          self.X, self.y - 8,
                                          self.X + 3, self.y - 2,
                                          self.X + 12, self.y - 2,
                                          self.X + 14, self.y,
                                          self.X, self.y)))

            # Returns from the subroutine
            # 340 RETURN

        def sub_350(self):
            # Changes the color to C
            # 350 HCOLOR=C
            self.set_color(self.C)
            # Draws line
            # 360 HPLOT 70*J, 158-Y(J) TO 70*J,154-Y(J)
            J = self.J + 1
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J, 158 - self.Y[self.J],
                                          70 * J, 154 - self.Y[self.J])))
            # Returns from the subroutine
            # 370 RETURN

        def sub_400(self):
            self.set_color(3)
            # 400 FOR J=1 TO 3
            for J in range(1, 4):
                # Draws line
                # 410 HPLOT 70*J-5,159 TO 70*J+5,159
                pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                     ('v2i', (70 * J - 5, 159,
                                              70 * J + 5, 159)))
                # End of the loop, returns to line 400
                # 420 NEXT
            # Returns from the subroutine
            # 430 RETURN

        def set_color(self, color):
            glColor3f(*self.color[color])

        def change_state(self, state):
            print("State Change: {} --> {}".format(self.state, state))
            self.state = state

        def print_message(self, message):
            self.label = self.window.create_label(message)

As in OpenGL, we clear the screen every frame, the routine that erases the objects can be removed. In this case, the call to sub_300() with C=0. The names of the methods have not been changed yet; let’s rename: sub_250 to missed sub_270 to hit sub_300 to draw_plane sub_350 to draw_bullet sub_400 to draw_bases Removing the comments with the Basic code, we have:

    import pyglet
    from pyglet.gl import *
    import random


    def rgb_to_f(r, g, b):
        return(r / 255.0, g / 255.0, b / 255.0)

    # Colors of high-resolution mode HGR
    # Source: https://github.com/AppleWin/AppleWin/issues/254
    COLORS = [(0.0, 0.0, 0.0),  # Black 0
             rgb_to_f(20, 245, 60),  # Green 1
             rgb_to_f(255, 68, 253),  # Magenta 2
             rgb_to_f(255, 255, 255),  # White 3
             rgb_to_f(255, 106, 60),  # Orange 5
             rgb_to_f(20, 207, 253),  # Medium Blue 6
             rgb_to_f(255, 255, 255),  # White 7
             ]


    class Game():
        def __init__(self, parent):
            self.color = COLORS
            self.window = parent
            # Creates two vectors with 3 elements each
            self.Y = [0] * 3
            self.F = [0] * 3
            # Initializes N equal to 1 and MS equal to 5
            self.N = 0
            self.MS = 5
            # Generates a random number between 0 and 6 + 4
            self.PS = random.randint(0, 6) + 4
            # Generates a random number between 0 and 135 + 11
            self.P = random.randint(0, 135) + 11
            self.state = "playing"
            self.I = self.PS
            self.pressed = False
            self.label = None

        def update(self):
            if self.state == "playing":
                self.play()
            else:
                if self.label:
                    glMatrixMode(gl.GL_PROJECTION)
                    glLoadIdentity()
                    glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                    glMatrixMode(gl.GL_MODELVIEW)
                    self.label.draw()
                if self.pressed:
                    pyglet.app.event_loop.exit()

        def play(self):
            self.draw_bases()
            if self.posicao_do_aviao > 265:
                self.missed()
                return
            self.y = 159 - self.altura_do_aviao
            self.X = self.posicao_do_aviao
            self.C = 3
            self.draw_plane()
            if self.pressed and self.tiros_disparados < 3:
                self.F[self.tiros_disparados] = 1
                self.tiros_disparados += 1
            self.pressed = False
            self.process_bullets()
            self.posicao_do_aviao += self.velocidade_aviao

        def process_bullets(self):
            for self.J in range(0, 3):
                if self.F[self.J] == 0 or self.Y[self.J] > 145:
                    continue
                self.Y[self.J] += self.velocidade_do_tiro
                self.C = 3
                self.draw_bullet()
                self.X = (self.J + 1) * 70 - self.posicao_do_aviao
                self.y = self.altura_do_aviao - self.Y[self.J]
                if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                    self.hit()

        def missed(self):
            self.print_message("MISSED")
            self.change_state("gameover")

        def hit(self):
            self.print_message("HIT!!!")
            self.change_state("gameover")

        def draw_plane(self):
            self.set_color(self.C)
            pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                                 ('v2i', (self.X, self.y,
                                          self.X, self.y - 8,
                                          self.X + 3, self.y - 2,
                                          self.X + 12, self.y - 2,
                                          self.X + 14, self.y,
                                          self.X, self.y)))

        def draw_bullet(self):
            self.set_color(self.C)
            J = self.J + 1
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J, 158 - self.Y[self.J],
                                          70 * J, 154 - self.Y[self.J])))

        def draw_bases(self):
            self.set_color(3)
            for J in range(1, 4):
                pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                     ('v2i', (70 * J - 5, 159,
                                              70 * J + 5, 159)))

        def set_color(self, color):
            glColor3f(*self.color[color])

        def change_state(self, state):
            print("State Change: {} --> {}".format(self.state, state))
            self.state = state

        def print_message(self, message):
            self.label = self.window.create_label(message)


    class Missile(pyglet.window.Window):
        def __init__(self):
            self.scale = 4    # Scales the graphics 4x
            self.frames = 8   # Frames per second.
                              # Defines how many times we will update the screen per second
            super(Missile, self).__init__(280 * self.scale,
                                          192 * self.scale,
                                          caption="Missiles")
            self.set_mouse_visible(False)
            self.game = Game(self)
            self.schedule = pyglet.clock.schedule_interval(
                func=self.update, interval=1.0 / self.frames)

        def update(self, interval):
            pass

        def on_draw(self):
            window.clear()
            self.game.update()

        def on_resize(self, width, height):
            # Initializes the view
            glViewport(0, 0, width, height)
            glMatrixMode(gl.GL_PROJECTION)
            glLoadIdentity()
            # Inverts the Y-axis coordinates
            glOrtho(0, width, height, 0, -1, 1)
            # Applies the scale
            glScalef(self.scale, self.scale, 1.0)
            glMatrixMode(gl.GL_MODELVIEW)

        def on_key_press(self, symbol, modifiers):
            self.game.pressed = True

        def create_label(self, message, x=None, y=None,
                      font='Times New Roman', size=36):
            """Prepares a text message to be displayed"""
            x = x or self.width // 2
            y = y or self.height // 2
            return pyglet.text.Label(
                message, font_name=font, font_size=size,
                x=x, y=y, anchor_x='center', anchor_y='center')


    window = Missile()
    pyglet.app.run()

Much better, but it still retains several characteristics of the Basic program. The variable names are a disaster. By renaming the variables and improving the comments, the complete program looks like this:

    import pyglet
    from pyglet.gl import *
    import random


    def rgb_to_f(r, g, b):
        return(r / 255.0, g / 255.0, b / 255.0)

    # Colors of high-resolution mode HGR
    # Source: https://github.com/AppleWin/AppleWin/issues/254
    COLORS = [(0.0, 0.0, 0.0),  # Black 0
             rgb_to_f(20, 245, 60),  # Green 1
             rgb_to_f(255, 68, 253),  # Magenta 2
             rgb_to_f(255, 255, 255),  # White 3
             rgb_to_f(255, 106, 60),  # Orange 5
             rgb_to_f(20, 207, 253),  # Medium Blue 6
             rgb_to_f(255, 255, 255),  # White 7
             ]


    class Game():
        def __init__(self, parent):
            self.color = COLORS
            self.window = parent
            # Creates two vectors with 3 elements each
            self.Y = [0] * 3 # Bullet height
            self.F = [0] * 3 # Bullet state
            self.bullets_fired = 0  # bullets already fired
            self.bullet_speed = 5
            # Generates a random number between 0 and 6 + 4
            self.plane_speed = random.randint(0, 6) + 4
            # Generates a random number between 0 and 135 + 11
            self.plane_height = random.randint(0, 135) + 11
            self.state = "playing"
            self.plane_position = self.plane_speed
            self.pressed = False
            self.label = None

        def update(self):
            if self.state == "playing":
                self.play()
            else:
                if self.label:
                    glMatrixMode(gl.GL_PROJECTION)
                    glLoadIdentity()
                    glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                    glMatrixMode(gl.GL_MODELVIEW)
                    self.label.draw()
                if self.pressed:
                    pyglet.app.event_loop.exit()

        def play(self):
            self.draw_bases()
            if self.plane_position > 265:
                self.missed()
                return
            self.y = 159 - self.plane_height
            self.X = self.plane_position
            self.C = 3
            self.draw_plane()
            if self.pressed and self.bullets_fired < 3:
                self.F[self.bullets_fired] = 1
                self.bullets_fired += 1
            self.pressed = False
            self.process_bullets()
            self.plane_position += self.plane_speed

        def process_bullets(self):
            for self.J in range(0, 3):
                if self.F[self.J] == 0 or self.Y[self.J] > 145:
                    continue
                self.Y[self.J] += self.bullet_speed
                self.C = 3
                self.draw_bullet()
                self.X = (self.J + 1) * 70 - self.plane_position
                self.y = self.plane_height - self.Y[self.J]
                if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                    self.hit()

        def missed(self):
            self.print_message("MISSED")
            self.change_state("gameover")

        def hit(self):
            self.print_message("HIT!!!")
            self.change_state("gameover")

        def draw_plane(self):
            self.set_color(self.C)
            pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                                 ('v2i', (self.X, self.y,
                                          self.X, self.y - 8,
                                          self.X + 3, self.y - 2,
                                          self.X + 12, self.y - 2,
                                          self.X + 14, self.y,
                                          self.X, self.y)))

        def draw_bullet(self):
            self.set_color(self.C)
            J = self.J + 1
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J, 158 - self.Y[self.J],
                                          70 * J, 154 - self.Y[self.J])))

        def draw_bases(self):
            self.set_color(3)
            for J in range(1, 4):
                pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                     ('v2i', (70 * J - 5, 159,
                                              70 * J + 5, 159)))

        def set_color(self, color):
            glColor3f(*self.color[color])

        def change_state(self, state):
            print("State Change: {} --> {}".format(self.state, state))
            self.state = state

        def print_message(self, message):
            self.label = self.window.create_label(message)


    class Missile(pyglet.window.Window):
        def __init__(self):
            self.scale = 4    # Scales the graphics 4x
            self.frames = 8   # Frames per second.
                              # Defines how many times we will update the screen per second
            super(Missile, self).__init__(280 * self.scale,
                                          192 * self.scale,
                                          caption="Missiles")
            self.set_mouse_visible(False)
            self.game = Game(self)
            self.schedule = pyglet.clock.schedule_interval(
                func=self.update, interval=1.0 / self.frames)

        def update(self, interval):
            pass

        def on_draw(self):
            window.clear()
            self.game.update()

        def on_resize(self, width, height):
            # Initializes the view
            glViewport(0, 0, width, height)
            glMatrixMode(gl.GL_PROJECTION)
            glLoadIdentity()
            # Inverts the Y-axis coordinates
            glOrtho(0, width, height, 0, -1, 1)
            # Applies the scale
            glScalef(self.scale, self.scale, 1.0)
            glMatrixMode(gl.GL_MODELVIEW)

        def on_key_press(self, symbol, modifiers):
            self.game.pressed = True

        def create_label(self, message, x=None, y=None,
                      font='Times New Roman', size=36):
            """Prepares a text message to be displayed"""
            x = x or self.width // 2
            y = y or self.height // 2
            return pyglet.text.Label(
                message, font_name=font, font_size=size,
                x=x, y=y, anchor_x='center', anchor_y='center')


    window = Missile()
    pyglet.app.run()

In a future article, I will continue to refactor the code. Bullet and Plane are clearly classes. Animation leaves much to be desired. But that’s the fun of programming; you can always improve.