ZX81 BASIC to Pygame Conversion of "Dropout" Game

Question:

I based the code below on this article: http://kevman3d.blogspot.com/2015/07/basic-games-in-python-1982-would-be.html

and on the ZX BASIC in this image:

code listing

 10 LET P=0
 20 LET T=P
 30 FOR Z=1 T0 10
 35 CLS
 37 PRINT AT 12,0;T
 40 LET R=INT (RND*17)
 50 FOR Y=0 TO 10
 60 PRINT AT Y,R;"O"
 70 LET N=P(INKEY$="4")-(INKEY$="1")
 80 IF N<0 OR N>15 THEN LET N=P
100 PRINT AT 11,P;"  ";AT 11,N;"┗┛";AT Y,R;" "
110 LET P=N
120 NEXT Y
130 LET T=T+(P=R OR P+1=R)
150 NEXT Z
160 PRINT AT 12,0;"YOU SCORED ";T;"/10"
170 PAUSE 4E4
180 RUN

I also shared it on Code Review Stack Exchange, and got a very helpful response refactoring it into high quality Python code complete with type hints.

However, for my purposes I’m wanting to keep the level of knowledge required to make this work a little less advanced, including avoiding the use of OOP. I basically want to maintain the "spirit of ZX BASIC" but make the code "not awful." The use of functions is fine, as we were allowed GOSUB back in the day.

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 curious to see how well the BASIC paradigm maps onto the more event driven approach of Pygame, so I’d welcome any comments on the pros and cons of this approach.

More specifically,

  • 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 a full listing implementing some of the ideas arising from my initial attempt if anyone is willing to provide one.

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

Asked By: Robin Andrews

||

Answers:

Here’s my reorganisation of your code:

import pygame
import random

# Define global constants
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()

# draw the images
player_icon = game_font.render("|__|", True, "black", "white")
# if we don't specify a background color, it'll be transparent
coin_text = game_font.render("O", True, "black")
msg_text = game_font.render("PRESS ANY KEY TO PLAY AGAIN", True, "black", "white")

# initialize some game variables
waiting = False  # start in game
player_pos = 0
score = 0
game_round = 0
coin_row = 0
coin_col = random.randint(0, 15)
running = True  # For program exit
# -------- Main Program Loop -----------
while running:
    # event handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if waiting:
                waiting = False
                score = 0  #  reset score
            elif event.key == pygame.K_LEFT:
                player_pos -= 1
            elif event.key == pygame.K_RIGHT:
                player_pos += 1

    # --- Game logic
    if waiting:
        # don't update the game state or redraw screen
        print_at_pos(5, 0, msg_text)
    else:
        coin_row += 1  # TODO: decouple from frame rate
        if -1 > player_pos:
            player_pos = -1 # so we can catch a coin at zero
        elif 15 < player_pos:
            player_pos = 15

        # coin is in scoring position
        if coin_row == 10:
            if player_pos + 1 == coin_col:
                score += 1
        elif coin_row > 10:  # round is over
            coin_col = random.randint(0, 15)
            coin_row = 0
            game_round+= 1
            if game_round >= NUM_ROUNDS:
                waiting = True
                game_round = 0  # reset round counter

        # --- Drawing code
        screen.fill("white")  # clear screen
        print_at_pos(10, player_pos, player_icon)
        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(6)
pygame.quit()

I’ve used a boolean waiting to allow for common event and game state handling that only moves during gameplay. For more complex interactions, you’ll want a state machine.

The coin movement is currently coupled to the frame rate, which is easy, but ideally you’d specify a rate/time interval, e.g. 200ms between row drops and then you could have a refresh rate similar to the monitor refresh rate.

Answered By: import random