# Gulp Catch, part 6: gravity, landing, chewing. The whole game.
#
# Flip tosses pancakes up out of his pan. Gravity pulls them down a little faster every step,
# so they fly in an arc; the counter stops them. Gulp chews: a list of mouth pictures, one after another.
# 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()
