Step 4: Show a running score

This is a small step in concept: keep count of how many bricks have been knocked out and show that score underneath the game window.

But it involves two changes:

  • Keeping track of the state of the game – in this case, the score.
  • Putting the gameplay inside a window and other information outside it

First we’ll push the gameplay inside a window, in two steps. Then we’ll add a status line and keep track of the score.

Step 4a: Create a gameplay window

Note

At the end of this step, the game will still be showing across the entire window but the new gameplay window will show where it will be showing at the end of the next step.

../_images/step-s4a.gif

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
WIDTH = 640
HEIGHT = 480

#
# Create a game window which can be smaller than the
# screen with its own background & frame colours.
#
GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT)
GAME_WINDOW.inflate_ip(-50, -50)
GAME_WINDOW.background_colour = "darkblue"
GAME_WINDOW.frame_colour = "white"

class Ball(ZRect): pass
#
# The ball is a red square halfway across the game screen
#
ball = Ball(0, 0, 30, 30)
ball.center = WIDTH / 2, HEIGHT / 2
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 screen and halfway across.
#
BAT_W = 150
BAT_H = 15
bat = Bat(WIDTH / 2, HEIGHT - 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 screen
# and one quarter high as it is wide.
#
N_BRICKS = 8
BRICK_W = WIDTH / N_BRICKS
BRICK_H = BRICK_W / 4
BRICK_COLOURS = ["purple", "lightgreen", "lightblue", "orange"]
#
# Create <N_BRICKS> blocks, filling the full width of the screen.
# 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(n_brick * BRICK_W, 0, BRICK_W, BRICK_H)
    brick.colour = BRICK_COLOURS[n_brick % len(BRICK_COLOURS)]
    bricks.append(brick)

def draw():
    #
    # Clear the screen 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)

    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.
    #
    x, y = pos
    bat.centerx = x

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)
        ball.direction = dx, -dy

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

    #
    # If the ball hits the bottom of the screen, you lose
    #
    if ball.bottom >= HEIGHT:
        exit()

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

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

What’s happening?

  • We’re creating a rectangle to hold the gameplay window which is is 50 pixels smaller than the screen which holds the entire game.
  • To create a blue filling and a white border, we draw a filled-in rectangle in blue and then a slightly larger, unfilled rectangle in white which fits around it.

Change it around

  • Change the colours of the gameplay window

  • Make the gameplay window not be centred on the screen.

    Hint: The line which “inflates” the gameplay rectangle by 50 pixels in each direction is just a shortcut for playing with its width and height and x and y attributes. You can change them directly.

Step 4b: Move the gameplay inside the gameplay window

../_images/step-s4b.gif

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
WIDTH = 640
HEIGHT = 480

#
# Create a game window which can be smaller than the
# screen with its own background & frame colours.
#
GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT)
GAME_WINDOW.inflate_ip(-50, -50)
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
#
## DELETE --> ball = Ball(0, 0, 30, 30)
ball = Ball((0, 0), (30, 30))
## DELETE --> ball.center = WIDTH / 2, HEIGHT / 2
ball.center = GAME_WINDOW.center
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
## DELETE --> bat = Bat(WIDTH / 2, HEIGHT - BAT_H, BAT_W, BAT_H)
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
## DELETE --> BRICK_W = WIDTH / N_BRICKS
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):
    ## DELETE --> brick = Brick(n_brick * BRICK_W, 0, BRICK_W, BRICK_H)
    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)

    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)
        ball.direction = dx, -dy

    #
    # Bounce the ball off the left or right walls
    #
    ## DELETE --> if ball.right >= WIDTH or ball.left <= 0:
    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
    #
    ## DELETE --> if ball.bottom >= HEIGHT:
    if ball.bottom >= GAME_WINDOW.bottom:
        exit()

    #
    # Bounce the ball off the top wall
    #
    ## DELETE --> if ball.top <= 0:
    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?

This is quite a busy step, but a lot of the changes are mechanical substitutions.

  • In short, wherever we were previously assuming that we were playing within the whole width of the game screen, we now have to assume that we are playing only within the gameplay window. So, for example, checking whether the ball has hit the left-hand edge of the screen (ball.left <= 0) now has to check instead whether we’ve hit the left-hand edge of the gameplay window (ball.left <= GAME_WINDOW.left).
  • One particular change is to keep the bat within the gameplay window. By default, the mouse will stay within the game screen so we didn’t have to do anything to stop the bat going too far to the left or right. Now, though, if we don’t “clamp” the bat within the gameplay window, it would start to move outside its borders if the mouse is moved too far. You can see this change in the on_mouse_move code.

Change it around

  • Allow the bat to move outside the gameplay window to see what happens
  • Have the bricks occupy a smaller width than the entire width of the gameplay window.

Step 4c: Add a running score count

../_images/step-s4c.gif

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
WIDTH = 640
HEIGHT = 480

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

#
# Create a status display, as wide as the screen and 60 pixels high.
# It's placed at the bottom of the screen
#
## DELETE --> GAME_WINDOW = ZRect(0, 0, WIDTH, HEIGHT)
## DELETE --> GAME_WINDOW.inflate_ip(-50, -50)
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((0, 0), (30, 30))
ball.center = GAME_WINDOW.center
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, center=STATUS_DISPLAY.center)

    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’re introducing the concept of game state: a programming object which keeps track of various aspects of the game. In this step, we’re just tracking the score; in the next step, we’ll use this to introduce the idea of different screens (a “welcome” screen, a “scoreboard” screen etc.).

  • We create a global game object whose only attribute is a score, which we initialise to zero.
  • We add a status window: another on-screen rectangle at the bottom of the display. We adjust the size of the gameplay window to fit alongside it.
  • To keep track of the score, we simply add one to the score every time a brick is knocked out.
  • The screen.draw.text function is a very flexible way of getting text onto the screen.

Change it around

  • Have each brick colour give a different score

    Hint: bricks.pop returns the brick you’ve just popped. You can get a brick’s colour from its .colour attribute.

  • Play around with the text: make it left-aligned, a different colour, a different font, &c.