I got the idea for this code from an article on converting ZX81 BASIC to Pygame. Although some code was provided in the article, I implemented this myself.
I'm pretty dubious about the approach of using nested for
loops inside the main game loop to make the game work, but at the same time I'm curios to see how well the BASIC paradigm maps onto the more event driven approach of Pygame, and would like to convert other games listed here for example: http://www.zx81stuff.org.uk/zx81/tape/10Games so I'd welcome any comments on the pros and cons of this approach.
Here's a few questions I have about my code:
Is there somewhere I can put the exit code
if event.type == pygame.QUIT
where it will work during game rounds, without having to repeat the code elsewhere?How would this game be implemented if I were to avoid the use of
for
loops/ nestedfor
loops?Are there any points of best practice for pygame/Python which I have violated?
What improvements can you suggest, bearing in mind my purpose is to write good Pygame code while maintaining the "spirit" of the ZX81 games.
Any input much appreciated. I'm also curious to see full listings implementing some of the ideas arising from my initial attempt.
Listing below:
import pygame
import random
import sys
# Define colors and other global constants
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
TEXT_SIZE = 16
SCREEN_SIZE = (16 * TEXT_SIZE, 13 * TEXT_SIZE)
NUM_ROUNDS = 5
def print_at_pos(row_num, col_num, item):
"""Blits text to row, col position."""
screen.blit(item, (col_num * TEXT_SIZE, row_num * TEXT_SIZE))
# Set up stuff
pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE)
pygame.display.set_caption("Dropout")
game_font = pygame.font.SysFont('consolas', TEXT_SIZE)
# Create clock to manage how fast the screen updates
clock = pygame.time.Clock()
# initialize some game variables
player_pos, new_player_pos, coin_row, score = 0, 0, 0, 0
# -------- Main Program Loop -----------
while True:
score = 0
# Each value of i represents 1 round
for i in range(NUM_ROUNDS):
coin_col = random.randint(0, 15)
# Each value of j represents one step in the coin's fall
for j in range(11):
pygame.event.get()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT]:
new_player_pos = player_pos + 1
elif pressed[pygame.K_LEFT]:
new_player_pos = player_pos - 1
if new_player_pos < 0 or new_player_pos > 15:
new_player_pos = player_pos
# --- Game logic
player_pos = new_player_pos
coin_row = j
if player_pos + 1 == coin_col and j == 10:
score += 1
# --- Drawing code
# First clear screen
screen.fill(WHITE)
player_icon = game_font.render("|__|", True, BLACK, WHITE)
print_at_pos(10, new_player_pos, player_icon)
coin_text = game_font.render("O", True, BLACK, WHITE)
print_at_pos(coin_row, coin_col, coin_text)
score_text = game_font.render(f"SCORE: {score}", True, BLACK, WHITE)
print_at_pos(12, 0, score_text)
# --- Update the screen.
pygame.display.flip()
# --- Limit to 6 frames/sec maximum. Adjust to taste.
clock.tick(8)
msg_text = game_font.render("PRESS ANY KEY TO PLAY AGAIN", True, BLACK, WHITE)
print_at_pos(5, 0, msg_text)
pygame.display.flip()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit(0)
if event.type == pygame.KEYDOWN:
waiting = False
Image of ZX81 listing:
-
\$\begingroup\$ You just made me nostalgic for my old ZX Printer... \$\endgroup\$Toby Speight– Toby Speight2022年09月17日 16:31:38 +00:00Commented Sep 17, 2022 at 16:31
1 Answer 1
Fun and interesting.
You have too many magic numbers. Things like 13 and 16 need constants. Better yet make those dimensions parametric.
Don't leave code laying around in the global namespace. Move code to functions, and variables at least to parameters if not to a class. A class in this case is easy and clean.
You have an important coordinate system error. TEXT_SIZE
is not the pixel dimensions of your character! You have to call .size()
on your font to get that.
print_at_pos
should take over responsibility for rendering via the font, since that's done in the exact same way every time.
Once you have clearer variable names and add more functions, all of your comments can go away.
Your if new_player_pos < 0 or new_player_pos > 15:
is better-expressed by a compound expression using min
/max
.
It's very important that you check for quit events during your main loop - currently your game cannot quit.
You have another coordinate system bug - you allow the player to move off the screen and miss some coin match events, both because you ignore the width of the player's symbol string.
Suggested
import pygame
from random import randrange
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
TEXT_SIZE = 16
TEXT_ANTIALIAS = True
TEXT_FOREGROUND = BLACK
TEXT_BACKGROUND = WHITE
PLAYER_SYMBOL = '|__|'
PLAYER_WIDTH = len(PLAYER_SYMBOL)
class DropoutGame:
def __init__(self, n_rows: int = 11, n_cols: int = 16, n_rounds: int = 5, fps: int = 6) -> None:
self.player_pos, self.coin_row, self.coin_col, self.score = 0, 0, 0, 0
self.n_rows, self.n_cols, self.n_rounds, self.fps = n_rows, n_cols, n_rounds, fps
self.n_display_cols = self.n_cols
self.n_display_rows = self.n_rows + 3
pygame.init()
pygame.display.set_caption('Dropout')
self.game_font = pygame.font.SysFont('Consolas', TEXT_SIZE)
self.char_w, self.char_h = self.game_font.size('W')
screen_size = (self.n_display_cols*self.char_w, self.n_display_rows*self.char_h)
self.screen = pygame.display.set_mode(screen_size)
self.clock = pygame.time.Clock()
def run(self) -> None:
while self.play_rounds():
if not self.should_play_again():
break
pygame.quit()
def play_rounds(self) -> bool:
self.score = 0
for i_round in range(self.n_rounds):
self.coin_col = randrange(self.n_cols)
for self.coin_row in range(self.n_rows):
self.move()
self.score += self.scored
self.draw()
self.clock.tick(self.fps)
if self.should_quit:
return False
return True
@property
def should_quit(self) -> bool:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return True
return False
def should_play_again(self) -> bool:
self.print(y=self.n_rows//2, x=0, text='PRESS ANY KEY TO PLAY AGAIN')
pygame.display.flip()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN:
return True
def move(self) -> None:
pressed = pygame.key.get_pressed()
if pressed[pygame.K_RIGHT]:
delta = 1
elif pressed[pygame.K_LEFT]:
delta = -1
else:
return
self.player_pos = max(0, min(self.n_cols - PLAYER_WIDTH, self.player_pos + delta))
@property
def scored(self) -> bool:
return (
self.player_pos <= self.coin_col <= self.player_pos+PLAYER_WIDTH
and self.coin_row == self.n_rows-1
)
def draw(self) -> None:
self.screen.fill(WHITE)
self.print(y=self.n_rows-1, x=self.player_pos, text=PLAYER_SYMBOL)
self.print(y=self.coin_row, x=self.coin_col, text='0')
self.print(y=self.n_display_rows-1, x=0, text=f'SCORE: {self.score}')
pygame.display.flip()
def print(self, y: int, x: int, text: str) -> None:
rendered = self.game_font.render(text, TEXT_ANTIALIAS, TEXT_FOREGROUND, TEXT_BACKGROUND)
self.screen.blit(rendered, (x*self.char_w, y*self.char_h))
if __name__ == '__main__':
DropoutGame().run()
-
1\$\begingroup\$ Thanks for this. I really appreciate the attention to detail and the quality of the code. I'm thinking that for my purposes, which are basically making coding fun for high school kids, I may have to simplify things a little. For example removing type hints and maybe not using a class, which although simple in concept could confuse less experienced learners. I think functions are fine in the "spirit of ZX Basic" as we were allowed
GOSUB
back in the day.GOTO
is probably well left in the past... \$\endgroup\$Robin Andrews– Robin Andrews2022年09月20日 17:36:02 +00:00Commented Sep 20, 2022 at 17:36