Step 5: Start and restart the game

Up to now, the game has started when you run it and closed when you either win or lose. In this step, we’ll add a new context: waiting for the game to start for the first time, or to restart after a game has completed.

This involves introducing the idea of a status for the game: either “Starting”: waiting for the game to begin; or “Running”: playing the game. Depending on which status is current, we will show different things on the screen and react differently to keypresses, mouse movements etc.

Step 5a: Add and display a Running status

The Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
WIDTH = 640
HEIGHT = 480

class Game(object): pass
game = Game()
game.score = 0
game.status = "Running"

#
# Create a status display, as wide as the screen and 60 pixels high.
# It's placed at the bottom of the screen
#
STATUS_DISPLAY = ZRect(0, HEIGHT - 60, WIDTH, 60)

#
# Create a game window which is as wide as the screen but allows
# a status display underneath
#
GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT - STATUS_DISPLAY.height - 1)
GAME_WINDOW.background_colour = "darkblue"
GAME_WINDOW.frame_colour = "white"

class Ball(ZRect): pass
#
# The ball is a red square halfway across the game window
#
ball = Ball(GAME_WINDOW.center, (30, 30))
ball.colour = "red"
#
# The ball moves one step right and one step down each tick
#
ball.direction = 1, 1
#
# The ball moves at a speed of 3 steps each tick
#
ball.speed = 3

class Bat(ZRect): pass
#
# The bat is a green oblong which starts just along the bottom
# of the game window and halfway across.
#
BAT_W = 150
BAT_H = 15
bat = Bat(GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H, BAT_W, BAT_H)
bat.colour = "green"

class Brick(ZRect): pass
#
# The brick is a rectangle one eight the width of the game window
# and one quarter high as it is wide.
#
N_BRICKS = 8
BRICK_W = GAME_WINDOW.width / N_BRICKS
BRICK_H = BRICK_W / 4
BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"]
#
# Create <N_BRICKS> blocks, filling the full width of the game window.
# Each brick is as high as a quarter of its width, so they remain
# proportional as the number of blocks or the screen size changes.
#
# The brick colours cycle through <BRICK_COLOURS>
#
bricks = []
for n_brick in range(N_BRICKS):
    brick = Brick(
        GAME_WINDOW.left + (n_brick * BRICK_W), GAME_WINDOW.top,
        BRICK_W, BRICK_H
    )
    brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)]
    bricks.append(brick)

def draw():
    #
    # Clear the screen, draw the game window and place the ball at its current position
    #
    screen.clear()
    #
    # Draw the game window and a frame around it
    #
    screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour)
    screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour)

    #
    # Show the current status, centred inside the status area
    #
    ## DELETE --> screen.draw.text("Score: %d" % game.score, center=STATUS_DISPLAY.center)
    screen.draw.text(
        "Score: %d" % game.score,
        left=STATUS_DISPLAY.left + 4,
        centery=STATUS_DISPLAY.centery
    )
    screen.draw.text(
        "Status: %s" % game.status,
        right=STATUS_DISPLAY.right - 4,
        centery=STATUS_DISPLAY.centery
    )

    screen.draw.filled_rect(ball, ball.colour)
    screen.draw.filled_rect(bat, bat.colour)
    for brick in bricks:
        screen.draw.filled_rect(brick, brick.colour)

def on_mouse_move(pos):
    #
    # Make the bat follow the horizontal movement of the mouse.
    # Ensure that the bat does not move outside the game window.
    #
    x, y = pos
    bat.centerx = x
    bat.clamp_ip(GAME_WINDOW)

def update():
    #
    # Move the ball along its current direction at its current speed
    #
    dx, dy = ball.direction
    ball.move_ip(ball.speed * dx, ball.speed * dy)

    #
    # Bounce the ball off the bat
    #
    if ball.colliderect(bat):
        ball.direction = dx, -dy

    #
    # If the ball hits a brick, kill that brick and
    # bounce the ball.
    #
    to_kill = ball.collidelist(bricks)
    if to_kill >= 0:
        bricks.pop(to_kill)
        game.score += 1
        ball.direction = dx, -dy

    #
    # Bounce the ball off the left or right walls
    #
    if ball.right >= GAME_WINDOW.right or ball.left <= GAME_WINDOW.left:
        ball.direction = -dx, dy

    #
    # If the ball hits the bottom wall, you lose
    #
    if ball.bottom >= GAME_WINDOW.bottom:
        exit()

    #
    # Bounce the ball off the top wall
    #
    if ball.top <= GAME_WINDOW.top:
        ball.direction = dx, -dy

    #
    # If there are no bricks left, you win
    #
    if not bricks:
        exit()

What’s happening?

  • We add a status attribute to the existing game object is initially set to “Running”.
  • We add the game status to the status display below the game, pushing the score to the left to make room.

Step 5b: Have the game pause before starting

The Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
WIDTH = 640
HEIGHT = 480

class Game(object): pass
game = Game()
game.score = 0
## DELETE --> game.status = "Running"
game.status = "Starting"

#
# Create a status display, as wide as the screen and 60 pixels high.
# It's placed at the bottom of the screen
#
STATUS_DISPLAY = ZRect(0, HEIGHT - 60, WIDTH, 60)

#
# Create a game window which is as wide as the screen but allows
# a status display underneath
#
GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT - STATUS_DISPLAY.height - 1)
GAME_WINDOW.background_colour = "darkblue"
GAME_WINDOW.frame_colour = "white"

class Ball(ZRect): pass
#
# The ball is a red square halfway across the game window
#
ball = Ball(GAME_WINDOW.center, (30, 30))
ball.colour = "red"
#
# The ball moves one step right and one step down each tick
#
ball.direction = 1, 1
#
# The ball moves at a speed of 3 steps each tick
#
ball.speed = 3

class Bat(ZRect): pass
#
# The bat is a green oblong which starts just along the bottom
# of the game window and halfway across.
#
BAT_W = 150
BAT_H = 15
bat = Bat(GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H, BAT_W, BAT_H)
bat.colour = "green"

class Brick(ZRect): pass
#
# The brick is a rectangle one eight the width of the game window
# and one quarter high as it is wide.
#
N_BRICKS = 8
BRICK_W = GAME_WINDOW.width / N_BRICKS
BRICK_H = BRICK_W / 4
BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"]
#
# Create <N_BRICKS> blocks, filling the full width of the game window.
# Each brick is as high as a quarter of its width, so they remain
# proportional as the number of blocks or the screen size changes.
#
# The brick colours cycle through <BRICK_COLOURS>
#
bricks = []
for n_brick in range(N_BRICKS):
    brick = Brick(
        GAME_WINDOW.left + (n_brick * BRICK_W), GAME_WINDOW.top,
        BRICK_W, BRICK_H
    )
    brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)]
    bricks.append(brick)

def draw():
    #
    # Clear the screen, draw the game window and place the ball at its current position
    #
    screen.clear()
    #
    # Draw the game window and a frame around it
    #
    screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour)
    screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour)

    #
    # Show the current status, centred inside the status area
    #
    screen.draw.text(
        "Score: %d" % game.score,
        left=STATUS_DISPLAY.left + 4,
        centery=STATUS_DISPLAY.centery
    )
    screen.draw.text(
        "Status: %s" % game.status,
        right=STATUS_DISPLAY.right - 4,
        centery=STATUS_DISPLAY.centery
    )

    if game.status == "Running":
►       screen.draw.filled_rect(ball, ball.colour)
►       screen.draw.filled_rect(bat, bat.colour)
►       for brick in bricks:
►           screen.draw.filled_rect(brick, brick.colour)

def on_mouse_move(pos):
    #
    # Make the bat follow the horizontal movement of the mouse.
    # Ensure that the bat does not move outside the game window.
    #
    if game.status == "Running":
►       x, y = pos
►       bat.centerx = x
►       bat.clamp_ip(GAME_WINDOW)

def on_key_down(key):
    if game.status == "Starting":
        if key == keys.SPACE:
            game.status = "Running"

def update():
    if game.status == "Running":
        #
        # Move the ball along its current direction at its current speed
        #
►       dx, dy = ball.direction
►       ball.move_ip(ball.speed * dx, ball.speed * dy)

        #
        # Bounce the ball off the bat
        #
►       if ball.colliderect(bat):
►           ball.direction = dx, -dy

        #
        # If the ball hits a brick, kill that brick and
        # bounce the ball.
        #
►       to_kill = ball.collidelist(bricks)
►       if to_kill >= 0:
►           bricks.pop(to_kill)
►           game.score += 1
►           ball.direction = dx, -dy

        #
        # Bounce the ball off the left or right walls
        #
►       if ball.right >= GAME_WINDOW.right or ball.left <= GAME_WINDOW.left:
►           ball.direction = -dx, dy

        #
        # If the ball hits the bottom wall, you lose
        #
►       if ball.bottom >= GAME_WINDOW.bottom:
►           exit()

        #
        # Bounce the ball off the top wall
        #
►       if ball.top <= GAME_WINDOW.top:
►           ball.direction = dx, -dy

        #
        # If there are no bricks left, you win
        #
►       if not bricks:
►           exit()

What’s happening?

  • We make sure that certain pieces of code only run when the game is Running, not when it’s waiting to start.
  • We add one new piece of functionality: if the game is Starting (ie waiting to start) we wait until the Space key is pressed and then change the status to Running, causing the other parts of the program to kick in.

Change it around

  • Choose a different key to make the game start
  • Display a message in the status window to indicate which key should be pressed.

Step 5c: Allow the game to restart once it’s complete

The Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
WIDTH = 640
HEIGHT = 480

class Game(object): pass
game = Game()
game.score = 0
game.status = "Starting"

#
# Create a status display, as wide as the screen and 60 pixels high.
# It's placed at the bottom of the screen
#
STATUS_DISPLAY = ZRect(0, HEIGHT - 60, WIDTH, 60)

#
# Create a game window which is as wide as the screen but allows
# a status display underneath
#
GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT - STATUS_DISPLAY.height - 1)
GAME_WINDOW.background_colour = "darkblue"
GAME_WINDOW.frame_colour = "white"

class Ball(ZRect): pass
#
# The ball is a red square halfway across the game window
#
ball = Ball(GAME_WINDOW.center, (30, 30))
ball.colour = "red"
#
# The ball moves one step right and one step down each tick
#
ball.direction = 1, 1
#
# The ball moves at a speed of 3 steps each tick
#
ball.speed = 3

class Bat(ZRect): pass
#
# The bat is a green oblong which starts just along the bottom
# of the game window and halfway across.
#
BAT_W = 150
BAT_H = 15
bat = Bat(GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H, BAT_W, BAT_H)
bat.colour = "green"

class Brick(ZRect): pass
#
# The brick is a rectangle one eight the width of the game window
# and one quarter high as it is wide.
#
N_BRICKS = 8
BRICK_W = GAME_WINDOW.width / N_BRICKS
BRICK_H = BRICK_W / 4
BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"]
#
# The brick colours cycle through <BRICK_COLOURS>
#
bricks = []

def reset_game():
    #
    # At the beginning of the game, centre the ball on the game window
    # and position the bat halfway across the game window and and sliding
    # along its bottom edge.
    #
    ball.center = GAME_WINDOW.center
    bat.center = (GAME_WINDOW.centerx, GAME_WINDOW.bottom - BAT_H)
    #
    # Create <N_BRICKS> blocks, filling the full width of the game window.
    # Each brick is as high as a quarter of its width, so they remain
    # proportional as the number of blocks or the screen size changes.
    #
    bricks.clear()
►   for n_brick in range(N_BRICKS):
►       brick = Brick(
►           GAME_WINDOW.left + (n_brick * BRICK_W), GAME_WINDOW.top,
►           BRICK_W, BRICK_H
►       )
►       brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)]
►       bricks.append(brick)

def draw():
    #
    # Clear the screen, draw the game window and place the ball at its current position
    #
    screen.clear()
    #
    # Draw the game window and a frame around it
    #
    screen.draw.filled_rect(GAME_WINDOW, GAME_WINDOW.background_colour)
    screen.draw.rect(GAME_WINDOW.inflate(+2, +2), GAME_WINDOW.frame_colour)

    #
    # Fill in the status window
    #
    if game.status == "Starting":
        #
        # If the game is waiting to start indicate how to start
        #
        screen.draw.text("Press SPACE to start", center=STATUS_DISPLAY.center)
    elif game.status == "Running":
        #
        # If the game is running show the current status, centred inside the status area
        #
►       screen.draw.text(
►           "Score: %d" % game.score,
►           left=STATUS_DISPLAY.left + 4,
►           centery=STATUS_DISPLAY.centery
►       )
►       screen.draw.text(
►           "Status: %s" % game.status,
►           right=STATUS_DISPLAY.right - 4,
►           centery=STATUS_DISPLAY.centery
►       )

    #
    # Fill in the gameplay window
    #
    if game.status == "Running":
        screen.draw.filled_rect(ball, ball.colour)
        screen.draw.filled_rect(bat, bat.colour)
        for brick in bricks:
            screen.draw.filled_rect(brick, brick.colour)

def on_mouse_move(pos):
    #
    # Make the bat follow the horizontal movement of the mouse.
    # Ensure that the bat does not move outside the game window.
    #
    if game.status == "Running":
        x, y = pos
        bat.centerx = x
        bat.clamp_ip(GAME_WINDOW)

def on_key_down(key):
    if game.status == "Starting":
        if key == keys.SPACE:
            reset_game()
            game.status = "Running"

def update():
    if game.status == "Running":
        #
        # Move the ball along its current direction at its current speed
        #
        dx, dy = ball.direction
        ball.move_ip(ball.speed * dx, ball.speed * dy)

        #
        # Bounce the ball off the bat
        #
        if ball.colliderect(bat):
            ball.direction = dx, -dy

        #
        # If the ball hits a brick, kill that brick and
        # bounce the ball.
        #
        to_kill = ball.collidelist(bricks)
        if to_kill >= 0:
            bricks.pop(to_kill)
            game.score += 1
            ball.direction = dx, -dy

        #
        # Bounce the ball off the left or right walls
        #
        if ball.right >= GAME_WINDOW.right or ball.left <= GAME_WINDOW.left:
            ball.direction = -dx, dy

        #
        # If the ball hits the bottom wall, you lose
        #
        if ball.bottom >= GAME_WINDOW.bottom:
            ## DELETE --> exit()
            game.status = "Starting"

        #
        # Bounce the ball off the top wall
        #
        if ball.top <= GAME_WINDOW.top:
            ball.direction = dx, -dy

        #
        # If there are no bricks left, you win
        #
        if not bricks:
            ## DELETE --> exit()
            game.status = "Starting"

What’s happening?

  • We move into its own function all the changes needed to restart the game.
  • When Space is pressed, we reset the game before switching to Running mode
  • When the game completes (win or lose) we switch back to Starting mode rather than dropping straight out.

Change it around

  • Add a “Kill” key which, when pressed, aborts the game and waits to restart.
  • In the status line, display the most recent result (win or lose) before restarting.