Converting a Game Written in Basic to Python - Part I
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.