Dot & Flip

Gulp Catch

The game from the Dot & Flip Python cartoons. Flip tosses pancakes. Gulp catches them. You change the rules.

The whole game is one Python file: gulp_catch.py.

Kids: ask a grown-up to help with step 1. Installing a program needs their password.

On this page: What you need · 1. Install Python · 2. Get the game · 3. Play · Change the rules · Build it step by step · If something goes wrong · For grown-ups

What you need

Already have Python and IDLE? Jump to step 2.

Step 1: Install Python

Watch it first: the film Lesson 0: installing Python at home shows every click, on Windows and on a Mac.

Windows

  1. Go to python.org/downloads.
  2. Skip the big button. Click the small link under it: the installer.
  3. Open the downloaded file. Tick the box at the bottom, Add python.exe to PATH. Then click Install Now.
  4. If Windows asks whether this app may make changes, a grown-up clicks Yes.
  5. When it says Setup was successful, click Close.
  6. Open the Start menu, type IDLE, and click it.

Mac

  1. Go to python.org/downloads and click the download button. You get a package file.
  2. Open it. Click Continue until you see Agree. Click Agree, then Install. A grown-up types their name and password.
  3. When it is done, a Python folder opens in Applications. Inside is IDLE. Double-click it.

Use the Python from python.org. Some other ways of installing Python on a Mac leave out the part that draws windows, and the game needs it.

Linux

Most Linux computers already have Python. Add IDLE and the part that draws windows:

Then open IDLE from the menu.

Check that it works

IDLE opens a window with >>> in it. That is the Shell. Type this after the arrows and press Enter:

print("Hello, Gulp")

If you see Hello, Gulp, Python is ready.

Step 2: Get the game

There are two ways. Way B always works, so if Way A gives you trouble, use Way B.

Way A: download the file

  1. Click here: download gulp_catch.py. If a page full of code opens instead, use Way B below.
  2. Your browser may ask whether to keep the file. It is plain text that you can read, so choose Keep.
  3. The file is now in your Downloads folder. Move it to Documents, so you can find it next time.

Way B: copy and paste

  1. Click Copy the game under the code below. If the button does nothing, click inside the code, press Ctrl+A and then Ctrl+C. On a Mac: Cmd+A, then Cmd+C.
  2. In IDLE, click File, then New File. An empty window opens.
  3. Paste: Ctrl+V. On a Mac: Cmd+V.
  4. Click File, then Save As. Name it gulp_catch.py and save it in Documents.
Show the whole game (about 300 lines)
# Gulp Catch
#
# Flip tosses pancakes. Gulp catches them.
# Left and Right arrows move Gulp. Space starts the game.
# Catch the golden pancakes. Do not catch the burnt ones.
#
# To play: open this file in IDLE and press F5.
# If the arrows do nothing, click the game window first.
#
# Everything after a # is a note for people. Python skips it.
# The numbers below are the rules. Change one and play again.

from tkinter import Tk, Canvas
from random import randint
import time

lives_at_start = 3        # lose this many pancakes and the game is over
gulp_speed = 7            # how far Gulp moves in one step
gravity_at_start = 0.15   # how hard the pancakes are pulled down
faster = 0.02             # more pull for every five pancakes caught
fastest_fall = 12         # top falling speed. Much faster, and a pancake can jump past the mouth
toss_every_at_start = 80  # steps between two pancakes at the start
burnt_chance = 5          # about one pancake in five is burnt

width = 480
height = 640
counter_top = 600         # pancakes land here


def touching(a, b):
    # a and b are boxes: [left, top, right, bottom]
    return a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]


class Flip:
    def __init__(self, canvas):
        self.canvas = canvas
        self.x = 120
        self.speed = 2
        x = self.x
        self.parts = [
            canvas.create_line(x, 58, x, 40, width=3, fill="gray30"),
            canvas.create_oval(x - 6, 32, x + 6, 44, fill="orange red", outline=""),
            canvas.create_rectangle(x - 22, 58, x + 22, 90, fill="light sea green",
                                    outline="dark slate gray", width=2),
            canvas.create_oval(x - 13, 68, x - 5, 76, fill="black"),
            canvas.create_oval(x + 5, 68, x + 13, 76, fill="black"),
            canvas.create_rectangle(x - 28, 92, x + 28, 140, fill="light sea green",
                                    outline="dark slate gray", width=2),
            canvas.create_line(x + 28, 108, x + 50, 108, width=4, fill="gray30"),
            canvas.create_oval(x + 46, 100, x + 86, 114, fill="gray20", outline=""),
        ]

    def step(self):
        # Flip walks along the top and turns around at the walls.
        self.x = self.x + self.speed
        for part in self.parts:
            self.canvas.move(part, self.speed, 0)
        if self.x <= 40 or self.x >= width - 90:
            self.speed = -self.speed


class Pancake:
    def __init__(self, canvas, x, y, burnt):
        self.canvas = canvas
        self.x = x
        self.y = y
        self.speed_x = randint(-3, 3)
        self.speed_y = -randint(2, 5)    # tossed up out of the pan
        self.burnt = burnt
        self.landed = False
        if burnt:
            self.shape = canvas.create_oval(x - 20, y - 7, x + 20, y + 7,
                                            fill="saddle brown", outline="black", width=2)
        else:
            self.shape = canvas.create_oval(x - 20, y - 7, x + 20, y + 7,
                                            fill="goldenrod", outline="dark goldenrod", width=2)

    def step(self, gravity):
        # Gravity: every step, the pancake falls a little faster.
        self.speed_y = self.speed_y + gravity
        if self.speed_y > fastest_fall:
            self.speed_y = fastest_fall
        # Bounce off the side walls.
        if self.x + self.speed_x < 20 or self.x + self.speed_x > width - 20:
            self.speed_x = -self.speed_x
        # Land on the counter.
        if self.y + self.speed_y > counter_top - 7:
            self.speed_y = counter_top - 7 - self.y
            self.speed_x = 0
            self.landed = True
        self.x = self.x + self.speed_x
        self.y = self.y + self.speed_y
        self.canvas.move(self.shape, self.speed_x, self.speed_y)

    def box(self):
        return [self.x - 20, self.y - 7, self.x + 20, self.y + 7]

    def hide(self):
        self.canvas.itemconfig(self.shape, state="hidden")


class Gulp:
    def __init__(self, canvas):
        self.canvas = canvas
        self.x = width / 2
        self.speed = 0
        x = self.x
        c = canvas
        body = c.create_oval(x - 100, 470, x + 100, 730, fill="plum4", outline="")
        dots = [
            c.create_oval(x - 88, 548, x - 70, 566, fill="thistle", outline=""),
            c.create_oval(x + 66, 534, x + 84, 552, fill="thistle", outline=""),
            c.create_oval(x - 62, 584, x - 46, 600, fill="thistle", outline=""),
            c.create_oval(x + 44, 582, x + 60, 598, fill="thistle", outline=""),
            c.create_oval(x - 52, 484, x - 38, 498, fill="thistle", outline=""),
        ]
        eyes = [
            c.create_oval(x - 28, 490, x - 14, 504, fill="white", outline=""),
            c.create_oval(x - 24, 494, x - 18, 500, fill="black"),
            c.create_oval(x + 14, 490, x + 28, 504, fill="white", outline=""),
            c.create_oval(x + 18, 494, x + 24, 500, fill="black"),
        ]
        open_mouth = c.create_oval(x - 60, 520, x + 60, 580, fill="gray10", outline="")
        half_mouth = c.create_oval(x - 60, 536, x + 60, 564, fill="gray10", outline="",
                                   state="hidden")
        shut_mouth = c.create_oval(x - 56, 547, x + 56, 553, fill="gray10", outline="",
                                   state="hidden")
        self.burp_text = c.create_text(x, 446, text="BURP!", fill="plum4",
                                       font="Helvetica 22 bold", state="hidden")
        self.mouths = [open_mouth, half_mouth, shut_mouth]
        # Chewing is a list of pictures, shown one after another.
        self.frames = [shut_mouth, half_mouth, shut_mouth, half_mouth, shut_mouth, open_mouth]
        self.frame = 0
        self.frame_timer = 0
        self.chewing = False
        self.burp_timer = 0
        self.parts = [body, open_mouth, half_mouth, shut_mouth, self.burp_text]
        for part in dots:
            self.parts.append(part)
        for part in eyes:
            self.parts.append(part)

    def go_left(self, event):
        self.speed = -gulp_speed

    def go_right(self, event):
        self.speed = gulp_speed

    def stop_left(self, event):
        if self.speed < 0:
            self.speed = 0

    def stop_right(self, event):
        if self.speed > 0:
            self.speed = 0

    def step(self):
        new_x = self.x + self.speed
        if new_x > 100 and new_x < width - 100:
            self.x = new_x
            for part in self.parts:
                self.canvas.move(part, self.speed, 0)
        if self.chewing:
            self.frame_timer = self.frame_timer + 1
            if self.frame_timer == 5:
                self.frame_timer = 0
                self.show_mouth(self.frames[self.frame])
                self.frame = self.frame + 1
                if self.frame == len(self.frames):
                    self.chewing = False
        if self.burp_timer > 0:
            self.burp_timer = self.burp_timer - 1
            if self.burp_timer == 0:
                self.canvas.itemconfig(self.burp_text, state="hidden")

    def show_mouth(self, mouth):
        for other in self.mouths:
            self.canvas.itemconfig(other, state="hidden")
        self.canvas.itemconfig(mouth, state="normal")

    def chew(self):
        self.chewing = True
        self.frame = 0
        self.frame_timer = 4

    def burp(self):
        self.canvas.itemconfig(self.burp_text, state="normal")
        self.burp_timer = 40

    def mouth_box(self):
        return [self.x - 60, 520, self.x + 60, 560]


class Game:
    def __init__(self):
        self.window = Tk()
        self.window.title("Gulp Catch")
        self.window.protocol("WM_DELETE_WINDOW", self.close)
        self.canvas = Canvas(self.window, width=width, height=height, bg="powder blue")
        self.canvas.pack()
        self.flip = Flip(self.canvas)
        self.gulp = Gulp(self.canvas)
        # The counter: missed pancakes land on it, the score is written on its front.
        self.canvas.create_rectangle(0, counter_top, width, height, fill="slate gray", outline="")
        self.canvas.create_line(0, counter_top, width, counter_top, width=3, fill="light slate gray")
        self.status = self.canvas.create_text(width / 2, (counter_top + height) / 2, text="",
                                              fill="white", font="Helvetica 16 bold")
        self.message = self.canvas.create_text(width / 2, 300, justify="center",
                                               font="Helvetica 18 bold",
                                               text="GULP CATCH\n\n"
                                                    "Left and Right move Gulp.\n"
                                                    "Catch the golden pancakes.\n"
                                                    "Not the burnt ones.\n\n"
                                                    "Space: start")
        self.canvas.bind_all("<KeyPress-Left>", self.gulp.go_left)
        self.canvas.bind_all("<KeyPress-Right>", self.gulp.go_right)
        self.canvas.bind_all("<KeyRelease-Left>", self.gulp.stop_left)
        self.canvas.bind_all("<KeyRelease-Right>", self.gulp.stop_right)
        self.canvas.bind_all("<KeyPress-space>", self.start)
        self.pancakes = []
        self.landed = []
        self.score = 0
        self.lives = lives_at_start
        self.show_status()
        self.playing = False
        self.window_open = True

    def start(self, event):
        if not self.playing:
            for pancake in self.pancakes:
                pancake.hide()
            for pancake in self.landed:
                pancake.hide()
            self.pancakes = []
            self.landed = []
            self.score = 0
            self.lives = lives_at_start
            self.gravity = gravity_at_start
            self.toss_every = toss_every_at_start
            self.toss_timer = 30
            self.canvas.itemconfig(self.message, text="")
            self.show_status()
            self.playing = True

    def show_status(self):
        self.canvas.itemconfig(self.status, text=f"Score {self.score}      Lives {self.lives}")

    def toss(self):
        burnt = randint(1, burnt_chance) == 1
        pancake = Pancake(self.canvas, self.flip.x + 66, 107, burnt)
        self.pancakes.append(pancake)

    def caught(self, pancake):
        pancake.hide()
        self.pancakes.remove(pancake)
        if pancake.burnt:
            self.gulp.burp()
            self.lose_life()
        else:
            self.gulp.chew()
            self.score = self.score + 1
            if self.score % 5 == 0:
                self.gravity = self.gravity + faster
                if self.toss_every > 30:
                    self.toss_every = self.toss_every - 5
            self.show_status()

    def dropped(self, pancake):
        self.pancakes.remove(pancake)
        self.landed.append(pancake)
        if not pancake.burnt:
            self.lose_life()

    def lose_life(self):
        self.lives = self.lives - 1
        self.show_status()
        if self.lives == 0:
            self.playing = False
            self.canvas.itemconfig(self.message,
                                   text=f"GAME OVER\n\nScore {self.score}\n\nSpace: play again")

    def step(self):
        self.flip.step()
        self.gulp.step()
        if self.playing:
            self.toss_timer = self.toss_timer - 1
            if self.toss_timer == 0:
                self.toss()
                self.toss_timer = self.toss_every
            # Walk over a copy of the list: removing from the list
            # you are walking over skips pancakes.
            for pancake in self.pancakes[:]:
                pancake.step(self.gravity)
                if touching(pancake.box(), self.gulp.mouth_box()):
                    self.caught(pancake)
                elif pancake.landed:
                    self.dropped(pancake)
                if not self.playing:
                    break

    def close(self):
        self.window_open = False

    def run(self):
        while self.window_open:
            self.step()
            self.window.update()
            time.sleep(0.02)
        self.window.destroy()


game = Game()
game.run()

Step 3: Play

  1. Open IDLE.
  2. Click File, then Open. Find gulp_catch.py and open it. A window with the code appears. Do not double-click the file itself: then it may run without IDLE, and you cannot change the rules.
  3. Press F5, or click Run, then Run Module. On many laptops and Macs, hold fn and press F5.
  4. The game window opens. Click it once, so it hears the keys.
  5. Press Space. Flip starts tossing.
Key What it does
← Left arrow Gulp goes left
→ Right arrow Gulp goes right
Space Start, and play again after the game is over
Close the window Stop the game

The rules of the game:

Change the rules

At the top of the file, the rules are just numbers. Change one, and play again.

Rule Starts at What it means
lives_at_start 3 how many pancakes you can lose before the game is over
gulp_speed 7 how far Gulp moves in one step
gravity_at_start 0.15 how hard the pancakes are pulled down
faster 0.02 more pull for every five pancakes caught
fastest_fall 12 the top falling speed
toss_every_at_start 80 steps between two pancakes at the start. Smaller means more pancakes
burnt_chance 5 about one pancake in this many is burnt

How to change one:

  1. In the window with the code, find the line, for example gulp_speed = 7.
  2. Change only the number: gulp_speed = 20.
  3. Save: Ctrl+S. On a Mac: Cmd+S.
  4. Press F5 and play. Gulp zooms.

Ideas to try:

Something will break. Something always does. Then you fix it, just like Dot:

Below the rules are width, height and counter_top: the size of the window and where the counter is. You can change them too, but Flip and Gulp are drawn for this size.

Build it step by step

The films build the game in six parts. Each part is a whole program: open it in IDLE and press F5.

Part File Film What is new
1 part1_loop.py Lesson 46: A Game Is a Loop Flip walks, a pancake flies from wall to wall. A game is a loop
2 part2_drive.py Lesson 47: Gulp Drives The arrows drive Gulp, and he stops at the edges
3 part3_catch.py Lesson 48: Through the Mouth A pancake that touches Gulp's mouth is caught
4 part4_rules.py Lesson 49: Game Over, Eventually Points, lives, game over. Space starts again
5 part5_many.py Lesson 50: One Loop, Many Pancakes Many pancakes at once, and burnt ones
6 part6_gravity.py Lesson 51: Gravity Goes the Wrong Way Gravity, landing, chewing. The whole game

Part 6 is the same game as gulp_catch.py. In parts 1 to 3, closing the window ends with a red message in the Shell. That is expected: part 4 shows how a game closes its window properly.

If something goes wrong

What you see What to do
The arrows do nothing Click the game window once
F5 does nothing Click the window with the code first, not the Shell. On a laptop or a Mac, hold fn and press F5. Or click Run, then Run Module
No game window It may be hiding behind IDLE. Look in the taskbar (Windows) or the Dock (Mac)
IDLE asks Source Must Be Saved Click OK. IDLE saves the file and runs it
No module named 'tkinter' Linux: install python3-tk (step 1). Mac: install Python from python.org
SyntaxError after a change Something besides a number changed. Press Ctrl+Z (Cmd+Z on a Mac), or get a fresh copy
ValueError that says empty range burnt_chance is below 1. Make it 1 or more
TypeError that mentions tuple A number has a comma: write 0.15, not 0,15
A red message when you close the window In parts 1 to 3, that is expected

For grown-ups

Licence

MIT: free to use, copy, change and share. The full text is in LICENSE.