2
\$\begingroup\$

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/ nested for 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:

enter image description here

asked Sep 17, 2022 at 15:38
\$\endgroup\$
1
  • \$\begingroup\$ You just made me nostalgic for my old ZX Printer... \$\endgroup\$ Commented Sep 17, 2022 at 16:31

1 Answer 1

2
\$\begingroup\$

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()
answered Sep 18, 2022 at 14:46
\$\endgroup\$
1
  • 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\$ Commented Sep 20, 2022 at 17:36

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.