8. Red Rain game

8.1. Game

from microbit import *

# adapted from Red Rain by Matt Land, June 2016
# A micro:bit game where the player blocks raindrops with an 'umbrella' paddle.
# Move the paddle using buttons A and B. Block drops to score, miss five to lose!

from microbit import *
import random

# Paddle sprites
paddle_sprites = [
    "66000",  # Leftmost
    "06600",  # Center-left
    "00660",  # Center-right
    "00066",  # Rightmost
]
paddle_position = 0  # Start paddle at the left

def run_paddle():
    """Update paddle position based on button input."""
    global paddle_position
    if button_a.is_pressed() and paddle_position > 0:
        paddle_position -= 1
    if button_b.is_pressed() and paddle_position < 3:
        paddle_position += 1

# Ball sprites
ball_sprites = [
    "90000:",  # Left
    "09000:",  # Left-center
    "00900:",  # Center
    "00090:",  # Right-center
    "00009:",  # Right
]

class ScoreState:
    """Handles scoring and level progression."""
    def __init__(self):
        self._hits = 0
        self._misses = 0
        self._level = 1
        self.time = 200  # Time delay for ball movement
        self.score = 0
        self._streak = 0

    def hit(self):
        """Register a hit and increase the score."""
        self._hits += 1
        self._streak += 1
        self.score += self._streak * self._level * 10
        display.show(Image.SQUARE)
        sleep(25)

    def miss(self):
        """Register a miss and reset the streak."""
        self._misses += 1
        self._streak = 0
        display.show(Image.NO)
        sleep(250)

    def level_complete(self):
        """Advance to the next level if the current one is complete."""
        if self._hits < 10:
            return
        display.show(Image.YES)
        sleep(300)
        display.clear()
        sleep(500)
        display.show(Image("00000:00000:00000:00000:00000"))  # Blank screen
        sleep(600)
        self._level += 1
        self.time = max(50, self.time - 2)  # Reduce time, with a minimum limit
        self._hits = self._misses = 0  # Reset for the new level
        display.scroll("Level {} Score {}".format(self._level, self.score), delay=75)

    def game_is_over(self):
        """Check if the game is over."""
        return self._misses >= 5

class BallState:
    """Manages ball behavior and collisions."""
    def __init__(self, score):
        self._spawned = False
        self._direction = False
        self._column = 0
        self._row = 0
        self.score = score  # Assign the score object during initialization

    def _spawn(self):
        """Spawn a new ball at a random column."""
        if not self._spawned:
            self._column = random.randint(0, 4)
            self._row = -1  # Start above the display
            self._spawned = True
            self._direction = False  # Ball starts falling

    def _move(self):
        """Move the ball down or up based on its direction."""
        if not self._direction and self._row < 4:
            self._row += 1  # Move down
        elif self._direction and self._row >= 0:
            self._row -= 1  # Move up
        sleep(100)  # Add sleep to slow down each ball movement

    def _test_collision(self):
        """Check for collisions with the paddle."""
        if self._row == 4:  # Ball reaches the paddle row
            if self._column in [paddle_position, paddle_position + 1]:
                self._hit()
            else:
                self._miss()

    def _despawn(self):
        """Remove the ball if it leaves the screen."""
        if (not self._direction and self._row == 4) or (self._direction and self._row == -1):
            self._spawned = False

    def _hit(self):
        """Handle a successful hit."""
        self._direction = True  # Change direction to rising
        self.score.hit()

    def _miss(self):
        """Handle a missed ball."""
        self._despawn()
        self.score.miss()

    def run(self):
        """Main ball logic for movement and collision checks."""
        self._spawn()
        self._move()
        self._test_collision()
        self._despawn()

    def draw_field(self, row):
        """Draw the current row of the field."""
        if not self._spawned or row != self._row:
            return "00000:"
        return ball_sprites[self._column]

# Initialize game state
my_score = ScoreState()
my_ball = BallState(my_score)  # Link the ball state with the score state

# Main game loop
while True:
    run_paddle()
    my_ball.run()
    field = [my_ball.draw_field(n) for n in range(4)]  # Create field rows
    field = "".join(field) + paddle_sprites[paddle_position]  # Add paddle
    image = Image(field)  # Generate image from the field
    display.show(image)
    my_score.level_complete()  # Check for level completion
    if my_score.game_is_over():  # End game if over
        break
    sleep(my_score.time)  # Adjust game speed
    # sleep(my_score.time + 50)  # Add an extra delay to slow things further

# Display final score
display.scroll("Game Over Score {}".format(my_score.score), loop=True, delay=100)

8.2. Red Rain Game Explanation

This is a game for the microbit where the player moves an ‘umbrella’ paddle at the bottom of the LED matrix using the A and B buttons. Raindrops fall from the top of the LED matrix and can be blocked by the paddle. The game increases in speed after each level. The player must block ten raindrops to complete a level and avoid missing five drops, which ends the game. Scoring is based on successful blocks, with points awarded for streaks of consecutive blocks.

8.3. Imports and Initialization

from microbit import *
import random

8.4. Paddle Sprites and Position

paddle_sprites = [
    "66000",  # Leftmost
    "06600",  # Center-left
    "00660",  # Center-right
    "00066",  # Rightmost
]
paddle_position = 0  # Start paddle at the left
  • Paddle Sprites: Defines the visual representation of the paddle in different positions.

  • Paddle Position: Initializes the paddle’s starting position.


8.5. Paddle Movement Function

def run_paddle():
    """Update paddle position based on button input."""
    global paddle_position
    if button_a.is_pressed() and paddle_position > 0:
        paddle_position -= 1
    if button_b.is_pressed() and paddle_position < 3:
        paddle_position += 1
  • run_paddle: Updates the paddle’s position based on button A and B inputs.


8.6. Ball Sprites

ball_sprites = [
    "90000:",  # Left
    "09000:",  # Left-center
    "00900:",  # Center
    "00090:",  # Right-center
    "00009:",  # Right
]
  • Ball Sprites: Defines the visual representation of the ball in different columns.


8.7. ScoreState Class

class ScoreState:
    """Handles scoring and level progression."""
    def __init__(self):
        self._hits = 0
        self._misses = 0
        self._level = 1
        self.time = 200  # Time delay for ball movement
        self.score = 0
        self._streak = 0

    def hit(self):
        """Register a hit and increase the score."""
        self._hits += 1
        self._streak += 1
        self.score += self._streak * self._level * 10
        display.show(Image.SQUARE)
        sleep(25)

    def miss(self):
        """Register a miss and reset the streak."""
        self._misses += 1
        self._streak = 0
        display.show(Image.NO)
        sleep(250)

    def level_complete(self):
        """Advance to the next level if the current one is complete."""
        if self._hits < 10:
            return
        display.show(Image.YES)
        sleep(300)
        display.clear()
        sleep(500)
        display.show(Image("00000:00000:00000:00000:00000"))  # Blank screen
        sleep(600)
        self._level += 1
        self.time = max(50, self.time - 2)  # Reduce time, with a minimum limit
        self._hits = self._misses = 0  # Reset for the new level
        display.scroll("Level {} Score {}".format(self._level, self.score), delay=75)

    def game_is_over(self):
        """Check if the game is over."""
        return self._misses >= 5
  • ScoreState: Manages the game’s scoring, level progression, and game-over conditions. - hit: Increments hits and score. - miss: Increments misses and resets the streak. - level_complete: Advances to the next level if the player has enough hits. - game_is_over: Checks if the game should end based on the number of misses.


8.8. BallState Class

class BallState:
    """Manages ball behavior and collisions."""
    def __init__(self, score):
        self._spawned = False
        self._direction = False
        self._column = 0
        self._row = 0
        self.score = score  # Assign the score object during initialization

    def _spawn(self):
        """Spawn a new ball at a random column."""
        if not self._spawned:
            self._column = random.randint(0, 4)
            self._row = -1  # Start above the display
            self._spawned = True
            self._direction = False  # Ball starts falling

    def _move(self):
        """Move the ball down or up based on its direction."""
        if not self._direction and self._row < 4:
            self._row += 1  # Move down
        elif self._direction and self._row >= 0:
            self._row -= 1  # Move up
        sleep(100)  # Add sleep to slow down each ball movement

    def _test_collision(self):
        """Check for collisions with the paddle."""
        if self._row == 4:  # Ball reaches the paddle row
            if self._column in [paddle_position, paddle_position + 1]:
                self._hit()
            else:
                self._miss()

    def _despawn(self):
        """Remove the ball if it leaves the screen."""
        if (not self._direction and self._row == 4) or (self._direction and self._row == -1):
            self._spawned = False

    def _hit(self):
        """Handle a successful hit."""
        self._direction = True  # Change direction to rising
        self.score.hit()

    def _miss(self):
        """Handle a missed ball."""
        self._despawn()
        self.score.miss()

    def run(self):
        """Main ball logic for movement and collision checks."""
        self._spawn()
        self._move()
        self._test_collision()
        self._despawn()

    def draw_field(self, row):
        """Draw the current row of the field."""
        if not self._spawned or row != self._row:
            return "00000:"
        return ball_sprites[self._column]
  • BallState: Manages the ball’s spawning, movement, collision detection, and despawning. - _spawn: Spawns a new ball at a random column. - _move: Moves the ball down or up based on its direction. - _test_collision: Checks if the ball collides with the paddle. - _despawn: Removes the ball if it leaves the screen. - _hit: Handles a successful hit. - _miss: Handles a missed ball. - run: Executes the main ball logic. - draw_field: Draws the current row of the field.


8.9. Game Initialization and Main Loop

# Initialize game state
my_score = ScoreState()
my_ball = BallState(my_score)  # Link the ball state with the score state

# Main game loop
while True:
    run_paddle()
    my_ball.run()
    field = [my_ball.draw_field(n) for n in range(4)]  # Create field rows
    field = "".join(field) + paddle_sprites[paddle_position]  # Add paddle
    image = Image(field)  # Generate image from the field
    display.show(image)
    my_score.level_complete()  # Check for level completion
    if my_score.game_is_over():  # End game if over
        break
    sleep(my_score.time)  # Adjust game speed
    # sleep(my_score.time + 50)  # Add an extra delay to slow things further

# Display final score
display.scroll("Game Over Score {}".format(my_score.score), loop=True, delay=100)
  • Initialization: Creates instances of ScoreState and BallState, linking them together.

  • Main Loop: Runs the game loop, updating the paddle position, ball state, and display. Checks for level completion and game-over conditions, adjusting the game speed accordingly.

  • Final Score Display: Displays the final score when the game ends.