pythonpygamegame-developmentbasicretro-computing

ZX81 BASIC to Pygame Conversion of "Dropout" Game


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,

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


Solution

  • 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.