# If the window is too tall to fit on the screen, check your operating system display settings and reduce display
# scaling if it is enabled.
import pgzeropgzrunpygamesys
from random import *
from enum import Enum

# Check Python version number. sys.version_info gives version as a tuple, e.g. if (3,7,2,'final',0) for version 3.7.2.
# Unlike many languages, Python can compare two tuples in the same way that you can compare numbers.
if sys.version_info < (3,5):
    print("This game requires at least version 3.5 of Python. Please download it from www.python.org")
    sys.exit()

# Check Pygame Zero version. This is a bit trickier because Pygame Zero only lets us get its version number as a string.
# So we have to split the string into a list, using '.' as the character to split on. We convert each element of the
# version number into an integer - but only if the string contains numbers and nothing else, because it's possible for
# a component of the version to contain letters as well as numbers (e.g. '2.0.dev0')
# We're using a Python feature called list comprehension - this is explained in the Bubble Bobble/Cavern chapter.
pgzero_version = [int(sif s.isnumeric() else for in pgzero.__version__.split('.')]
if pgzero_version < [1,2]:
    print("This game requires at least version 1.2 of Pygame Zero. You have version {0}. Please upgrade using the command 'pip3 install --upgrade pgzero'".format(pgzero.__version__))
    sys.exit()

WIDTH 480 
HEIGHT 800
TITLE "Infinite Bunner"

ROW_HEIGHT 40

# See what happens when you change this to True
DEBUG_SHOW_ROW_BOUNDARIES False

# The MyActor class extends Pygame Zero's Actor class by allowing an object to have a list of child objects,
# which are drawn relative to the parent object.
class MyActor(Actor):
    def __init__(selfimageposanchor=("center""bottom")):
        super().__init__(imageposanchor)

        self.children = []

    def draw(selfoffset_xoffset_y):
        self.+= offset_x
        self.+= offset_y

        super().draw()
        for child_obj in self.children:
            child_obj.draw(self.xself.y)

        self.-= offset_x
        self.-= offset_y

    def update(self):
        for child_obj in self.children:
            child_obj.update()

# The eagle catches the rabbit if it goes off the bottom of the screen
class Eagle(MyActor):
    def __init__(selfpos):
        super().__init__("eagles"pos)

        self.children.append(MyActor("eagle", (0, -32)))

    def update(self):
        self.+= 12

class PlayerState(Enum):
    ALIVE 0
    SPLAT 1
    SPLASH 2
    EAGLE 3

# Constants representing directions
DIRECTION_UP 0
DIRECTION_RIGHT 1
DIRECTION_DOWN 2
DIRECTION_LEFT 3

direction_keys = [keys.UPkeys.RIGHTkeys.DOWNkeys.LEFT]

# X and Y directions indexed into by in_edge and out_edge in Segment
# The indices correspond to the direction numbers above, i.e. 0 = up, 1 = right, 2 = down, 3 = left
# Numbers 0 to 3 correspond to up, right, down, left
DX = [0,4,0,-4]
DY = [-4,0,4,0]

class Bunner(MyActor):
    MOVE_DISTANCE 10

    def __init__(selfpos):
        super().__init__("blank"pos)

        self.state PlayerState.ALIVE

        self.direction 2
        self.timer 0

        # If a control input is pressed while the rabbit is in the middle of jumping, it's added to the input queue
        self.input_queue = []

        # Keeps track of the furthest distance we've reached so far in the level, for scoring
        # (Level Y coordinates decrease as the screen scrolls)
        self.min_y self.y

    def handle_input(selfdir):
        # Find row that player is trying to move to. This may or may not be the row they're currently standing on,
        # depending on whether the proposed movement would take them onto a different row
        for row in game.rows:
            if row.== self.Bunner.MOVE_DISTANCE DY[dir]:
                # Found the target row
                # Can the player move to the new location? Can't move if there's something in the way
                # (or if the new location is off the screen)
                if row.allow_movement(self.Bunner.MOVE_DISTANCE DX[dir]):
                    # It's okay to move here, so set direction and timer. Player will move one pixel per frame
                    # for the specified number of frames
                    self.direction dir
                    self.timer Bunner.MOVE_DISTANCE
                    game.play_sound("jump"1)

                # No need to continue searching
                return

    def update(self):
        # Check each control direction
        for direction in range(4):
            if key_just_pressed(direction_keys[direction]):
                self.input_queue.append(direction)

        if self.state == PlayerState.ALIVE:
            # While the player is alive, the timer variable is used for movement. If it's zero, the player is on
            # the ground. If it's above zero, they're currently jumping to a new location.

            # Are we on the ground, and are there inputs to process?
            if self.timer == and len(self.input_queue) &gt0:
                # Take the next input off the queue and process it
                self.handle_input(self.input_queue.pop(0))

            land False
            if self.timer &gt0:
                # Apply movement
                self.+= DX[self.direction]
                self.+= DY[self.direction]
                self.timer -= 1
                land self.timer == 0      # If timer reaches zero, we've just landed

            current_row None
            for row in game.rows:
                if row.== self.y:
                    current_row row
                    break

            if current_row:
                # Row.check receives the player's X coordinate and returns the new state the player should be in
                # (normally ALIVE, but SPLAT or SPLASH if they've collided with a vehicle or if they've fallen in
                # the water). It also returns a second result which is only used if there was a collision, and even
                # then only for certain collisions. When the new state is SPLAT, we will add a new child object to the
                # current row, with the appropriate 'splat' image. In this case, the second result returned from
                # check_collision is a Y offset which affects the position of this new child object. If the player is
                # hit by a car the Y offset is zero, but if they are hit by a train the returned offset is 8 as this
                # positioning looks a little better.
                self.statedead_obj_y_offset current_row.check_collision(self.x)
                if self.state == PlayerState.ALIVE:
                    # Water rows move the player along the X axis, if standing on a log
                    self.+= current_row.push()

                    if land:
                        # Just landed - play sound effect appropriate to the current row
                        current_row.play_sound()
                else:
                    if self.state == PlayerState.SPLAT:
                        # Add 'splat' graphic to current row with the specified position and Y offset
                        current_row.children.insert(0MyActor("splat" str(self.direction), (self.xdead_obj_y_offset)))
                    self.timer 100
            else:
                # There's no current row - either because player is currently changing row, or the row they were on
                # has been deleted. Has the player gone off the bottom of the screen?
                if self.&gtgame.scroll_pos HEIGHT 80:
                    # Create eagle
                    game.eagle Eagle((self.xgame.scroll_pos))
                    self.state PlayerState.EAGLE
                    self.timer 150
                    game.play_sound("eagle")

            # Limit x position so player doesn't go off the screen. The player movement code doesn't allow jumping off
            # the screen, but without this line, the player could be carried off the screen by a log
            self.max(16min(WIDTH 16self.x))
        else:
            # Not alive - timer now counts down prior to game over screen
            self.timer -= 1

        # Keep track of the furthest we've got in the level
        self.min_y min(self.min_yself.y)

        # Choose sprite image
        self.image "blank"
        if self.state == PlayerState.ALIVE:
            if self.timer &gt0:
                self.image "jump" str(self.direction)
            else:
                self.image "sit" str(self.direction)
        elif self.state == PlayerState.SPLASH and self.timer &gt84:
            # Display appropriate 'splash' animation frame. Note that we use a different technique to display the
            # 'splat' image – see: comments earlier in this method. The reason two different techniques are used is
            # that the splash image should be drawn on top of other objects, whereas the splat image must be drawn
            # underneath other objects. Since the player is always drawn on top of other objects, changing the player
            # sprite is a suitable method of displaying the splash image.
            self.image "splash" str(int((100 self.timer) / 2))

# Mover is the base class for Car, Log and Train
# The thing they all have in common, besides inheriting from MyActor, is that they need to store whether they're
# moving left or right and update their X position each frame
class Mover(MyActor):
    def __init__(selfdximagepos):
        super().__init__(imagepos)

        self.dx dx

    def update(self):
        self.+= self.dx

class Car(Mover):
    # These correspond to the indicies of the lists self.sounds and self.played. Used in Car.update to trigger
    # playing of the corresponding sound effects.
    SOUND_ZOOM 0
    SOUND_HONK 1

    def __init__(selfdxpos):
        image "car" str(randint(03)) + ("0" if dx &ltelse "1")
        super().__init__(dximagepos)

        # Cars have two sound effects. Each can only play once. We use this
        # list to keep track of which has already played.
        self.played = [FalseFalse]
        self.sounds = [("zoom"6), ("honk"4)]

    def play_sound(selfnum):
        if not self.played[num]:
            # Select a sound and pass the name and count to Game.play_sound.
            # The asterisk operator unpacks the two items and passes them to play_sound as separate arguments
            game.play_sound(*self.sounds[num])
            self.played[num] = True

class Log(Mover):
    def __init__(selfdxpos):
        image "log" str(randint(01))
        super().__init__(dximagepos)

class Train(Mover):
    def __init__(selfdxpos):
        image "train"  +str(randint(02)) + ("0" if dx &ltelse "1")
        super().__init__(dximagepos)

# Row is the base class for Pavement, Grass, Dirt, Rail and ActiveRow
# Each row corresponds to one of the 40 pixel high images which make up sections of grass, road, etc.
# The last row of each section is 60 pixels high and overlaps with the row above
class Row(MyActor):
    def __init__(selfbase_imageindexy):
        # base_image and index form the name of the image file to use
        # Last argument is the anchor point to use
        super().__init__(base_image str(index), (0y), ("left""bottom"))

        self.index index

        # X direction of moving elements on this row
        # Zero by default - only ActiveRows (see below) and Rail have moving elements
        self.dx 0

    def next(self):
        # Overridden in child classes. See comments in Game.update
        return

    def collide(selfxmargin=0):
        # Check to see if the given X coordinate is in contact with any of this row's child objects (e.g. logs, cars,
        # hedges). A negative margin makes the collideable area narrower than the child object's sprite, while a
        # positive margin makes the collideable area wider.
        for child_obj in self.children:
            if >= child_obj.- (child_obj.width 2) - margin and &ltchild_obj.+ (child_obj.width 2) + margin:
                return child_obj

        return None

    def push(self):
        return 0

    def check_collision(selfx):
        # Returns the new state the player should be in, based on whether or not the player collided with anything on
        # this road. As this class is the base class for other types of row, this method defines the default behaviour
        # – i.e. unless a subclass overrides this method, the player can walk around on a row without dying.
        return PlayerState.ALIVE0

    def allow_movement(selfx):
        # Ensure the player can't walk off the left or right sides of the screen
        return >= 16 and <= WIDTH-16

class ActiveRow(Row):
    def __init__(selfchild_typedxsbase_imageindexy):
        super().__init__(base_imageindexy)

        self.child_type child_type    # Class to be used for child objects (e.g. Car)
        self.timer 0
        self.dx choice(dxs)   # Randomly choose a direction for cars/logs to move

        # Populate the row with child objects (cars or logs). Without this, the row would initially be empty.
        = -WIDTH 70
        while &ltWIDTH 70:
            += randint(240480)
            pos = (WIDTH + (if self.dx &gtelse -x), 0)
            self.children.append(self.child_type(self.dxpos))

    def update(self):
        super().update()

        # Recreate the children list, excluding any which are too far off the edge of the screen to be visible
        self.children = [for in self.children if c.> -70 and c.&ltWIDTH 70]

        self.timer -= 1

        # Create new child objects on a random interval
        if self.timer &lt0:
            pos = (WIDTH 70 if self.dx &ltelse -700)
            self.children.append(self.child_type(self.dxpos))
            # 240 is minimum distance between the start of one child object and the start of the next, assuming its
            # speed is 1. If the speed is 2, they can occur twice as frequently without risk of overlapping with
            # each other. The maximum distance is double the minimum distance (1 + random value of 1)
            self.timer = (random()) * (240 abs(self.dx))

# Grass rows sometimes contain hedges
class Hedge(MyActor):
    def __init__(selfxypos):
        super().__init__("bush"+str(x)+str(y), pos)

def generate_hedge_mask():
    # In this context, a mask is a series of boolean values which allow or prevent parts of an underlying image from showing through.
    # This function creates a mask representing the presence or absence of hedges in a Grass row. False means a hedge
    # is present, True represents a gap. Initially we create a list of 12 elements. For each element there is a small
    # chance of a gap, but normally all element will be False, representing a hedge. We then randomly set one item to
    # True, to ensure that there is always at least one gap that the player can get through
    mask = [random() &lt0.01 for in range(12)]
    mask[randint(011)] = True # force there to be one gap

    # We then widen gaps to a minimum of 3 tiles. This happens in two steps.
    # First, we recreate the mask list, except this time whether a gap is present is based on whether there was a gap
    # in either the original element or its neighbouring elements. When using Python's built-in sum function, a value
    # of True is treated as 1 and False as 0. We must use the min/max functions to ensure that we don't try to look
    # at a neighbouring element which doesn't exist (e.g. there is no neighbour to the right of the last element)
    mask = [sum(mask[max(0i-1):min(12i+2)]) &gtfor in range(12)]

    # We want to ensure gaps are a minimum of 3 tiles wide, but the previous line only ensures a minimum gap of 2 tiles
    # at the edges. The last step is to return a new list consisting of the old list with the first and last elements duplicated
    return [mask[0]] + mask * [mask[-1]]

def classify_hedge_segment(maskprevious_mid_segment):
    # This function helps determine which sprite should be used by a particular hedge segment. Hedge sprites are numbered
    # 00, 01, 10, 11, 20, 21 - up to 51. The second number indicates whether it's a bottom (0) or top (1) segment,
    # but this method is concerned only with the first number. 0 represents a single-tile-width hedge. 1 and 2 represent
    # the left-most or right-most sprites in a multi-tile-width hedge. 3, 4 and 5 all represent middle pieces in hedges
    # which are 3 or more tiles wide.

    # mask is a list of 4 boolean values - a slice from the list generated by generate_hedge_mask. True represents a gap
    # and False represents a hedge. mask[1] is the item we're currently looking at.
    if mask[1]:
        # mask[1] == True represents a gap, so there will be no hedge sprite at this location
        sprite_x None
    else:
        # There's a hedge here - need to check either side of it to see if it's a single-width, left-most, right-most
        # or middle piece. The calculation generates a number from 0 to 3 accordingly. Note that when boolean values
        # are used in arithmetic in Python, False is treated as being 0 and True as 1.
        sprite_x mask[0] - mask[2]

    if sprite_x == 3:
        # If this is a middle piece, to ensure the piece tiles correctly, we alternate between sprites 3 and 4.
        # If the next piece is going to be the last of this hedge section (sprite 2), we need to make sure that sprite 3
        # does not precede it, as the two do not tile together correctly. In this case we should use sprite 5.
        # mask[3] tells us whether there's a gap 2 tiles to the right - which means the next tile will be sprite 2
        if previous_mid_segment == and mask[3]:
            return 5None
        else:
            # Alternate between 3 and 4
            if previous_mid_segment == None or previous_mid_segment == 4:
                sprite_x 3
            elif previous_mid_segment == 3:
                sprite_x 4
            return sprite_xsprite_x
    else:
        # Not a middle piece
        return sprite_xNone

class Grass(Row):
    def __init__(selfpredecessorindexy):
        super().__init__("grass"indexy)

        # In computer graphics, a mask is a series of boolean (true or false) values indicating which parts of an image
        # will be transparent. Grass rows may contain hedges which block the player's movement, and we use a similar
        # mechanism here. In our hedge mask, values of False mean a hedge is present, while True means there is a gap
        # in the hedges. Hedges are two rows high - once hedges have been created on a row, the pattern will be
        # duplicated on the next row (although the sprites will be different - e.g. there are separate sprites
        # for the top-left and bottom-left corners of a hedge). Note that the upper sprites overlap with the row above.
        self.hedge_row_index None     # 0 or 1, or None if no hedges on this row
        self.hedge_mask None

        if not isinstance(predecessorGrassor predecessor.hedge_row_index == None:
            # Create a brand-new set of hedges? We will only create hedges if the previous row didn't have any.
            # We also only want hedges to appear on certain types of grass row, and on only a random selection
            # of rows
            if random() &lt0.5 and index &gtand index &lt14:
                self.hedge_mask generate_hedge_mask()
                self.hedge_row_index 0
        elif predecessor.hedge_row_index == 0:
            self.hedge_mask predecessor.hedge_mask
            self.hedge_row_index 1

        if self.hedge_row_index != None:
            # See comments in classify_hedge_segment for explanation of previous_mid_segment
            previous_mid_segment None
            for in range(113):
                sprite_xprevious_mid_segment classify_hedge_segment(self.hedge_mask[1:3], previous_mid_segment)
                if sprite_x != None:
                    self.children.append(Hedge(sprite_xself.hedge_row_index, (40 200)))

    def allow_movement(selfx):
        # allow_movement in the base class ensures that the player can't walk off the left and right sides of the
        # screen. The call to our own collide method ensures that the player can't walk through hedges. The margin of
        # 8 prevents the player sprite from overlapping with the edge of a hedge.
        return super().allow_movement(xand not self.collide(x8)

    def play_sound(self):
        game.play_sound("grass"1)

    def next(self):
        if self.index <= 5:
            row_classindex Grassself.index 8
        elif self.index == 6:
            row_classindex Grass7
        elif self.index == 7:
            row_classindex Grass15
        elif self.index >= and self.index <= 14:
            row_classindex Grassself.index 1
        else:
            row_classindex choice((RoadWater)), 0

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)

class Dirt(Row):
    def __init__(selfpredecessorindexy):
        super().__init__("dirt"indexy)

    def play_sound(self):
        game.play_sound("dirt"1)

    def next(self):
        if self.index <= 5:
            row_classindex Dirtself.index 8
        elif self.index == 6:
            row_classindex Dirt7
        elif self.index == 7:
            row_classindex Dirt15
        elif self.index >= and self.index <= 14:
            row_classindex Dirtself.index 1
        else:
            row_classindex choice((RoadWater)), 0

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)
    
class Water(ActiveRow):
    def __init__(selfpredecessorindexy):
        # dxs contains a list of possible directions (and speeds) in which child objects (in this case, logs) on this
        # row could move. We pass the lists to the constructor of the base class, which randomly chooses one of the
        # directions. We want logs on alternate rows to move in opposite directions, so we take advantage of the fact
        # that that in Python, multiplying a list by True or False results in either the same list, or an empty list.
        # So by looking at the direction of child objects on the previous row (predecessor.dx), we can decide whether
        # child objects on this row should move left or right. If this is the first of a series of Water rows,
        # predecessor.dx will be zero, so child objects could move in either direction.
        dxs = [-2,-1]*(predecessor.dx >= 0) + [1,2]*(predecessor.dx <= 0)
        super().__init__(Logdxs"water"indexy)

    def update(self):
        super().update()

        for log in self.children:
            # Child (log) object positions are relative to the parent row. If the player exists, and the player is at the
            # same Y position, and is colliding with the current log, make the log dip down into the water slightly
            if game.bunner and self.== game.bunner.and log == self.collide(game.bunner.x, -4):
                log.2
            else:
                log.0

    def push(self):
        # Called when the player is standing on a log on this row, so player object can be moved at the same speed and
        # in the same direction as the log
        return self.dx

    def check_collision(selfx):
        # If we're colliding with a log, that's a good thing!
        # margin of -4 ensures we can't stand right on the edge of a log
        if self.collide(x, -4):
            return PlayerState.ALIVE0
        else:
            game.play_sound("splash")
            return PlayerState.SPLASH0

    def play_sound(self):
        game.play_sound("log"1)

    def next(self):
        # After 2 water rows, there's a 50-50 chance of the next row being either another water row, or a dirt row
        if self.index == or (self.index >= and random() &lt0.5):
            row_classindex Dirtrandint(4,6)
        else:
            row_classindex Waterself.index 1

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)

class Road(ActiveRow):
    def __init__(selfpredecessorindexy):
        # Specify the possible directions and speeds from which the movement of cars on this row will be chosen
        # We use Python's set data structure to specify that the car velocities on this row will be any of the numbers
        # from -5 to 5, except for zero or the velocity of the cars on the previous row
        dxs list(set(range(-56)) - set([0predecessor.dx]))
        super().__init__(Cardxs"road"indexy)

    def update(self):
        super().update()

        # Trigger car sound effects. The zoom effect should play when the player is on the row above or below the car,
        # the honk effect should play when the player is on the same row.
        for y_offsetcar_sound_num in [(-ROW_HEIGHTCar.SOUND_ZOOM), (0Car.SOUND_HONK), (ROW_HEIGHTCar.SOUND_ZOOM)]:
            # Is the player on the appropriate row?
            if game.bunner and game.bunner.== self.y_offset:
                for child_obj in self.children:
                    # The child object must be a car
                    if isinstance(child_objCar):
                        # The car must be within 100 pixels of the player on the x-axis, and moving towards the player
                        # child_obj.dx < 0 is True or False depending on whether the car is moving left or right, and
                        # dx < 0 is True or False depending on whether the player is to the left or right of the car.
                        # If the results of these two comparisons are different, the car is moving towards the player.
                        # Also, for the zoom sound, the car must be travelling faster than one pixel per frame
                        dx child_obj.game.bunner.x
                        if abs(dx) &lt100 and ((child_obj.dx &lt0) != (dx &lt0)) and (y_offset == or abs(child_obj.dx) &gt1):
                            child_obj.play_sound(car_sound_num)

    def check_collision(selfx):
        if self.collide(x):
            game.play_sound("splat"1)
            return PlayerState.SPLAT0
        else:
            return PlayerState.ALIVE0

    def play_sound(self):
        game.play_sound("road"1)

    def next(self):
        if self.index == 0:
            row_classindex Road1
        elif self.index &lt5:
            # 80% chance of another road
            random()
            if &lt0.8:
                row_classindex Roadself.index 1
            elif &lt0.88:
                row_classindex Grassrandint(0,6)
            elif &lt0.94:
                row_classindex Rail0
            else:
                row_classindex Pavement0
        else:
            # We've reached maximum of 5 roads in a row, so choose something else
            random()
            if &lt0.6:
                row_classindex Grassrandint(0,6)
            elif &lt0.9:
                row_classindex Rail0
            else:
                row_classindex Pavement0

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)

class Pavement(Row):
    def __init__(selfpredecessorindexy):
        super().__init__("side"indexy)

    def play_sound(self):
        game.play_sound("sidewalk"1)

    def next(self):
        if self.index &lt2:
            row_classindex Pavementself.index 1
        else:
            row_classindex Road0

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)

# Note that Rail does not inherit from ActiveRow
class Rail(Row):
    def __init__(selfpredecessorindexy):
        super().__init__("rail"indexy)

        self.predecessor predecessor

    def update(self):
        super().update()

        # Only Rail rows with index 1 have trains on them
        if self.index == 1:
            # Recreate the children list, excluding any which are too far off the edge of the screen to be visible
            self.children = [for in self.children if c.> -1000 and c.&ltWIDTH 1000]

            # If on-screen, and there is currently no train, and with a 1% chance every frame, create a train
            if self.&ltgame.scroll_pos+HEIGHT and len(self.children) == and random() &lt0.01:
                # Randomly choose a direction for trains to move. This can be different for each train created
                dx choice([-2020])
                self.children.append(Train(dx, (WIDTH 1000 if dx &ltelse -1000, -13)))
                game.play_sound("bell")
                game.play_sound("train"2)

    def check_collision(selfx):
        if self.index == and self.predecessor.collide(x):
            game.play_sound("splat"1)
            return PlayerState.SPLAT8     # For the meaning of the second return value, see comments in Bunner.update
        else:
            return PlayerState.ALIVE0

    def play_sound(self):
        game.play_sound("grass"1)

    def next(self):
        if self.index &lt3:
            row_classindex Railself.index 1
        else:
            item choice( ((Road0), (Water0)) )
            row_classindex item[0], item[1]

        # Create an object of the chosen row class
        return row_class(selfindexself.ROW_HEIGHT)

class Game:
    def __init__(selfbunner=None):
        self.bunner bunner
        self.looped_sounds = {}

        try:
            if bunner:
                music.set_volume(0.4)
            else:
                music.play("theme")
                music.set_volume(1)
        except:
            pass

        self.eagle None
        self.frame 0

        # First (bottom) row is always grass
        self.rows = [Grass(None00)]

        self.scroll_pos = -HEIGHT

    def update(self):
        if self.bunner:
            # Scroll faster if the player is close to the top of the screen. Limit scroll speed to
            # between 1 and 3 pixels per frame.
            self.scroll_pos -= max(1min(3float(self.scroll_pos HEIGHT self.bunner.y) / (HEIGHT // 4)))
        else:
            self.scroll_pos -= 1

        # Recreate the list of rows, excluding any which have scrolled off the bottom of the screen
        self.rows = [row for row in self.rows if row.&ltint(self.scroll_pos) + HEIGHT ROW_HEIGHT 2]

        # In Python, a negative index into a list gives you items in reverse order, e.g. my_list[-1] gives you the
        # last element of a list. Here, we look at the last row in the list - which is the top row - and check to see
        # if it has scrolled sufficiently far down that we need to add a new row above it. This may need to be done
        # multiple times - particularly when the game starts, as only one row is added to begin with.
        while self.rows[-1].&gtint(self.scroll_pos)+ROW_HEIGHT:
            new_row self.rows[-1].next()
            self.rows.append(new_row)

        # Update all rows, and the player and eagle (if present)
        for obj in self.rows + [self.bunnerself.eagle]:
            if obj:
                obj.update()

        # Play river and traffic sound effects, and adjust volume each frame based on the player's proximity to rows
        # of the appropriate types. For each such row, a number is generated representing how much the row should
        # contribute to the volume of the sound effect. These numbers are added together by Python's sum function.
        # On the following line we ensure that the volume can never be above 40% of the maximum possible volume.
        if self.bunner:
            for namecountrow_class in [("river"2Water), ("traffic"3Road)]:
                # The first line uses a list comprehension to get each row of the appropriate type, e.g. Water rows
                # if we're currently updating the "river" sound effect.
                volume sum([16.0 max(16.0abs(r.self.bunner.y)) for in self.rows if isinstance(rrow_class)]) - 0.2
                volume min(0.4volume)
                self.loop_sound(namecountvolume)

        return self

    def draw(self):
        # Create a list of all objects which need to be drawn. This includes all rows, plus the player
        # Using list(s.rows) means we're creating a copy of that list to use - we don't want to create a reference
        # to it as that would mean we're modifying the original list's contents
        all_objs list(self.rows)

        if self.bunner:
            all_objs.append(self.bunner)

        # We want to draw objects in order based on their Y position. In general, objects further down the screen should be drawn
        # after (and therefore in front of) objects higher up the screen. We can use Python's built-in sort function
        # to put the items in the desired order, before we draw the  The following function specifies the criteria
        # used to decide how the objects are sorted.
        def sort_key(obj):
            # Adding 39 and then doing an integer divide by 40 (the height of each row) deals with the situation where
            # the player sprite would otherwise be drawn underneath the row below. This could happen when the player
            # is moving up or down. If you assume that it occupies a 40x40 box which can be at an arbitrary y offset,
            # it generates the row number of the bottom row that that box overlaps. If the player happens to be
            # perfectly aligned to a row, adding 39 and dividing by 40 has no effect on the result. If it isn't, even
            # by a single pixel, the +39 causes it to be drawn one row later.
            return (obj.39) // ROW_HEIGHT

        # Sort list using the above function to determine order
        all_objs.sort(key=sort_key)

        # Always draw eagle on top of everything
        all_objs.append(self.eagle)
        
        for obj in all_objs:
            if obj:
                # Draw the object, taking the scroll position into account
                obj.draw(0, -int(self.scroll_pos))

        if DEBUG_SHOW_ROW_BOUNDARIES:
            for obj in all_objs:
                if obj and isinstance(objRow):
                    pygame.draw.rect(screen.surface, (255255255), pygame.Rect(obj.xobj.int(self.scroll_pos), screen.surface.get_width(), ROW_HEIGHT), 1)
                    screen.draw.text(str(obj.index), (obj.xobj.int(self.scroll_pos) - ROW_HEIGHT))

    def score(self):
        return int(-320 game.bunner.min_y) // 40

    def play_sound(selfnamecount=1):
        try:
            # Some sounds have multiple varieties. If count > 1, we'll randomly choose one from those
            # We don't play any sounds if there is no player (e.g. if we're on the menu)
            if self.bunner:
                # Pygame Zero allows you to write things like 'sounds.explosion.play()'
                # This automatically loads and plays a file named 'explosion.wav' (or .ogg) from the sounds folder (if
                # such a file exists)
                # But what if you have files named 'explosion0.ogg' to 'explosion5.ogg' and want to randomly choose
                # one of them to play? You can generate a string such as 'explosion3', but to use such a string
                # to access an attribute of Pygame Zero's sounds object, we must use Python's built-in function getattr
                sound getattr(soundsname str(randint(0count 1)))
                sound.play()
        except:
            # If a sound fails to play, ignore the error
            pass

    def loop_sound(selfnamecountvolume):
        try:
            # Similar to play_sound above, but for looped sounds we need to keep a reference to the sound so that we can
            # later modify its volume or turn it off. We use the dictionary self.looped_sounds for this - the sound
            # effect name is the key, and the value is the corresponding sound reference.
            if volume &gtand not name in self.looped_sounds:
                full_name name str(randint(0count 1))
                sound getattr(soundsfull_name)      # see play_sound method above for explanation
                sound.play(-1)  # -1 means sound will loop indefinitely
                self.looped_sounds[name] = sound

            if name in self.looped_sounds:
                sound self.looped_sounds[name]
                if volume &gt0:
                    sound.set_volume(volume)
                else:
                    sound.stop()
                    del self.looped_sounds[name]
        except:
            # If a sound fails to play, ignore the error
            pass


    def stop_looped_sounds(self):
        try:
            for sound in self.looped_sounds.values():
                sound.stop()
            self.looped_sounds.clear()
        except:
            # If sound system is not working/present, ignore the error
            pass

# Dictionary to keep track of which keys are currently being held down
key_status = {}

# Was the given key just pressed? (i.e. is it currently down, but wasn't down on the previous frame?)
def key_just_pressed(key):
    result False

    # Get key's previous status from the key_status dictionary. The dictionary.get method allows us to check for a given
    # entry without giving an error if that entry is not present in the dictionary. False is the default value returned
    # when the key is not present.
    prev_status key_status.get(keyFalse)

    # If the key wasn't previously being pressed, but it is now, we're going to return True
    if not prev_status and keyboard[key]:
        result True

    # Before we return, we need to update the key's entry in the key_status dictionary (or create an entry if there
    # wasn't one already
    key_status[key] = keyboard[key]

    return result

def display_number(ncolourxalign):
    # align: 0 for left, 1 for right
    str(n)  # Convert number to string
    for in range(len(n)):
        screen.blit("digit" str(colour) + n[i], (+ (len(n) * align) * 250))


# Pygame Zero calls the update and draw functions each frame

class State(Enum):
    MENU 1
    PLAY 2
    GAME_OVER 3

def update():
    global stategamehigh_score

    if state == State.MENU:
        if key_just_pressed(keys.SPACE):
            state State.PLAY
            game Game(Bunner((240, -320)))
        else:
            game.update()

    elif state == State.PLAY:
        # Is it game over?
        if game.bunner.state != PlayerState.ALIVE and game.bunner.timer &lt0:
            # Update high score
            high_score max(high_scoregame.score())

            # Write high score file
            try:
                with open("high.txt""w"as file:
                    file.write(str(high_score))
            except:
                # If an error occurs writing the file, just ignore it and carry on, rather than crashing
                pass

            state State.GAME_OVER
        else:
            game.update()

    elif state == State.GAME_OVER:
        # Switch to menu state, and create a new game object without a player
        if key_just_pressed(keys.SPACE):
            game.stop_looped_sounds()
            state State.MENU
            game Game()

def draw():
    game.draw()

    if state == State.MENU:
        screen.blit("title", (00))
        screen.blit("start" str([0121][game.scroll_pos // 4]), ((WIDTH 270) // 2HEIGHT 240))

    elif state == State.PLAY:
        # Display score and high score
        display_number(game.score(), 000)
        display_number(high_score1WIDTH 101)

    elif state == State.GAME_OVER:
        # Display "Game Over" image
        screen.blit("gameover", (00))

# Set up sound system
try:
    pygame.mixer.quit()
    pygame.mixer.init(44100, -162512)
    pygame.mixer.set_num_channels(16)
except:
    # If an error occurs, just ignore it
    pass

# Load high score from file
try:
    with open("high.txt""r"as f:
        high_score int(f.read())
except:
    # If opening the file fails (likely because it hasn't yet been created), set high score to 0
    high_score 0

# Set the initial game state
state State.MENU

# Create a new Game object, without a Player object
game Game()

pgzrun.go()