#!/usr/bin/env python3
#
# Copyright 2020 Thomer Gil [https://thomer.com/games/brickbreaker]
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
#
# On Mac:
#
# brew install python@3.8
# export PATH=/usr/local/opt/python@3.8/bin:$PATH
# export PATH=$HOME/Library/Python/3.8/bin:$PATH
# pip3.8 install --user pipenv
# pipenv install pygame==2.0.0.dev6
# pipenv install cx_freeze
# pipenv run python3.8 ./brickbreaker.py
#
# To recreate setup.py:
# pipenv run cxfreeze-quickstart
#
# On Mac, to build an installer:
# pipenv run python3.8 setup.py bdist_dmg
#
# On Windows:
#
# Install Python using the Windows installer
# Install Cygwin (without python)
# pip install --user pipenv
# export PATH=$PATH:/cygdrive/c/Users/Thomer\ Gil/AppData/Roaming/Python/Python38/Scripts/
# pipenv install pygame
# pipenv run py brickbreaker.py
#
# On Windows, to build an installer:
#
# cd /cygdrive/z/nobackup/brickbreaker/
# # only needed the first time
# # pipenv install cx_freeze
# pipenv run py setup.py bdist_msi
#


# {{{ imports, consts, globals
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import pygame
import random
import threading
import argparse

BGCOLOR = (0,0,0)
FGCOLOR = (255,255,255)
NFRAMES = 60

# wall properties
NBRICKS = 13
NROWS = 8
ABOVE_WALL_EMPTY_SPACE = 60
BELOW_WALL_EMPTY_SPACE = 260
BRICK_WIDTH = 56
BRICK_HEIGHT = 15
BRICK_HSPACE = 2 # pixels; ideally an even number; it's divided by 2 later
BRICK_VSPACE = 2
BRICK_COLORS = [
    # https://www.oreilly.com/library/view/raspberry-pi-for/9781118554234/a5_20_9781118554234-ch13.html
    # https://www.rapidtables.com/web/color/RGB_Color.html
    (255,100,100),  # red
    (0,255,0),      # lime
    (128,0,128),    # purple
    (255,255,0),    # yellow
    (0, 0, 255),    # blue
    (0, 128, 0),    # green
    (255,165,0),    # orange
    (128,128,128),  # gray
    (0,255,255),    # cyan/aqua
    (0,128,128),    # teal
    (128,128,0),    # olive
    (138,43,226),   # blue violet
    (245,222,179),  # wheat
    (230,230,250),  # lavender
    (0,0,128),      # navy
    (127,255,212),  # aqua marine
    (169,169,169),  # dark grey
    (105,105,105),  # dim grey
    (0,0,0),        # black
    (0,0,0),        # black
    (0,0,0),        # black
]

# ball properties
NBALLS = 1
BALL_COLOR = (255,255,255)
BALL_RADIUS = 4
BELOW_WALL_SPACE_TO_BALL = 60 # where ball is released
BALL_Y_SPEED = 2.5
BALL_X_SPEED_SLOW = 2
BALL_X_SPEED_FAST = 4

# lives and levels
NLIVES = 3
LEVEL_START = 1 # start at level

# where to draw levels
LEVEL_X_FROM_RIGHT = 35
LEVEL_Y = 5
LEVEL_SIZE = 16

# where to draw the lives
LIVES_X = 10
LIVES_Y = 10
LIVES_SPACE = BALL_RADIUS*3

# level up and death dynamics
INIT_SLEEP = 0.6
DEATH_SLEEP = 0.3
NLIVES_LEVEL_UP = 1
LEVEL_UP_MULT = 1.15

# paddle properties
PADDLE_HEIGHT = 12
PADDLE_WIDTH = 80
PADDLE_MOVE = 5
MAX_PADDLE_MOVE = 10
PADDLE_COLOR = (255,255,255)
BELOW_PADDLE_EMPTY_SPACE = 2

# paddle bounce zones
PADDLE_ZONES = {
    BALL_X_SPEED_SLOW: [
        # bounce back
        (15, "MULT", -1),

        # symmetric bounce
        (65, "SET", BALL_X_SPEED_SLOW),

        # accelerate to high speed
        (20, "SET", BALL_X_SPEED_FAST),
    ],

    BALL_X_SPEED_FAST: [
        # bounce back
        (15, "MULT", -1),

        # dampen to low speed
        (50, "SET", BALL_X_SPEED_SLOW),

        # continue at high speed
        (35, "SET", BALL_X_SPEED_FAST),
    ],
}
# }}}

# {{{ Wall
class Wall():
    # draw a wall that alternates
    #
    # [....] [....] [....] [....]  <-- odd rows
    # ..] [....] [....] [....] [.  <-- even rows
    # [....] [....] [....] [....]
    # ..] [....] [....] [....] [.
    #
    # even rows have 1 more brick than odd rows, but the first and last brick
    # are only half bricks.
    #
    def __init__(self, nrows, nbricks, level):
        self.bricks = pygame.sprite.Group()
        self.nrows = nrows
        self.nbricks = nbricks

        y = ABOVE_WALL_EMPTY_SPACE
        for i in range(nrows):
            x = 0
            for j in range(nbricks+1):
                brick_width = BRICK_WIDTH

                # on even rows, first and last brick is a half brick. because the
                # even rows have one more BRICK_HSPACE than the odd rows, subtract
                # half a BRICK_HSPACE from the width of the first and last brick.
                # (Otherwise the last brick would (invisibly) stick out to the right.
                if (i % 2) == 0 and (j == 0 or j >= nbricks):
                    brick_width = (brick_width/2) - (BRICK_HSPACE/2)

                # on odd rows, skip the last brick, because it'll be drawn
                # outside the screen. phrased differently: the for loop goes
                # from j to nbricks+1 -- the +1 is to accommodate bricks on
                # even rows; for odd rows, it's one too many.
                if (i % 2) == 1 and (j >= nbricks):
                    continue

                brick = Brick(x, y, brick_width, BRICK_HEIGHT, level)
                self.bricks.add(brick)

                x += brick_width + BRICK_HSPACE
            y += BRICK_HEIGHT + BRICK_VSPACE

    def draw(self, screen):
        for brick in self.bricks:
            brick.draw(screen)

    def top_y(self):
        return ABOVE_WALL_EMPTY_SPACE

    def bottom_y(self):
        return self.top_y() \
               + self.nrows*BRICK_HEIGHT \
               + (self.nrows-1)*BRICK_VSPACE

    def kill(self, brick):
        self.bricks.remove(brick)

    def is_empty(self):
        return len(self.bricks.sprites()) == 0
# }}}
# {{{ Brick
class Brick(pygame.sprite.Sprite):
    def __init__(self, x, y, width, height, level):
        super(Brick, self).__init__()
        self.image = pygame.Surface((int(width), int(height)))
        self.image.fill(BGCOLOR)
        pygame.draw.rect(self.image, BRICK_COLORS[level-1], [0, 0, int(width), int(height)])
        self.rect = self.image.get_rect()
        self.rect.x = int(x)
        self.rect.y = int(y)
        self.mask = pygame.mask.from_surface(self.image)
# }}}
# {{{ Ball
class Ball(pygame.sprite.Sprite):
    def __init__(self, loc, speed, v_mult):
        super(Ball, self).__init__()
        self.x, self.y = float(loc[0]), float(loc[1])

        self.image = pygame.Surface([BALL_RADIUS*2, BALL_RADIUS*2])
        self.image.set_colorkey(BGCOLOR) # see https://riptutorial.com/pygame/example/23788/transparency
        self.image.fill(BGCOLOR)
        pygame.draw.circle(self.image, BALL_COLOR, (BALL_RADIUS, BALL_RADIUS), BALL_RADIUS, BALL_RADIUS)
        self.rect = self.image.get_rect()
        self.rect.x = self.getx()
        self.rect.y = self.gety()

        # decide left/direction randomly. also, note that the ball can't
        # ever go faster than 75% of the brick or paddle height (which
        # ever is thinner). this is to prevent a whole bunch of problems
        # where the ball "flies through" the paddle or through bricks.
        x_velocity = BALL_X_SPEED_SLOW
        if random.randint(0,1) == 0:
            x_velocity *= -1

        min_brick_paddle = min(BRICK_HEIGHT, PADDLE_HEIGHT)
        y_velocity = min((float(speed) * v_mult), 0.75*min_brick_paddle)
        self.velocity = [float(x_velocity), float(y_velocity)]

        # improves performance of collision detection
        self.mask = pygame.mask.from_surface(self.image)


    def getx(self):
        return int(self.x)

    def gety(self):
        return int(self.y)

    def xvelocity(self):
        return self.velocity[0]

    def yvelocity(self):
        return self.velocity[1]

    def set_xvelocity(self, v):
        self.velocity[0] = float(v)

    def set_yvelocity(self, v):
        self.velocity[1] = float(v)

    def setx(self, x):
        self.x = float(x)

    def sety(self, y):
        self.y = float(y)

    def hbounce(self):
        self.velocity[0] *= float(-1)

    def vbounce(self):
        self.velocity[1] *= float(-1)

    def update(self, screen):
        global screen_width, screen_height
        self.x += self.velocity[0]
        self.y += self.velocity[1]
        self.rect.x = int(self.x)
        self.rect.y = int(self.y)

    # returns True if ball is travelling from left to right
    def left_to_right(self):
        return self.velocity[0] > 0

    # returns True if ball is travelling from right to left
    def right_to_left(self):
        return self.velocity[0] < 0

# }}}
# {{{ Paddle
class Paddle(pygame.sprite.Sprite):
    def __init__(self, paddle_width, move, v_mult):
        global screen_width, screen_height
        super(Paddle, self).__init__()
        self.paddle_width = paddle_width
        self.move = min(move * v_mult, MAX_PADDLE_MOVE)
        self.image = pygame.Surface((self.paddle_width, PADDLE_HEIGHT))
        self.image.fill(PADDLE_COLOR)
        pygame.draw.rect(self.image, PADDLE_COLOR, [0, 0, self.paddle_width, PADDLE_HEIGHT])
        self.rect = self.image.get_rect()
        self.keypress = None
        self.reset()
        self.mask = pygame.mask.from_surface(self.image)

    def reset(self):
        self.rect.x = int(screen_width/2 - (self.paddle_width/2))
        self.rect.y = int(screen_height - BELOW_PADDLE_EMPTY_SPACE - PADDLE_HEIGHT)

    def set_keypress(self, keypress):
        self.keypress = keypress

    def edge_correct(self):
        if self.rect.x < 0:
            self.rect.x = 0
        elif self.rect.x > (screen_width-self.paddle_width):
            self.rect.x = (screen_width-self.paddle_width)

    # moves the paddle according to the user's keypresses
    def update(self, screen):
        if self.keypress == None:
            return

        global screen_width

        move = self.move
        if pygame.key.get_mods() & pygame.KMOD_SHIFT:
            move = 1

        if self.keypress[pygame.K_LEFT] or self.keypress[pygame.K_COMMA]:
            self.rect.x -= int(move)
        elif self.keypress[pygame.K_RIGHT] or self.keypress[pygame.K_PERIOD]:
            self.rect.x += int(move)
        self.edge_correct()
        self.keypress = None

    # given an x coordinate and direction, compute how far from the
    # paddle's edge the ball hit (in %).
    def edge_dist(self, x, from_right_edge = True):
        perc_from_edge = ((x - self.rect.x) / self.paddle_width) * 100
        if perc_from_edge < 0:
            perc_from_edge = 0
        elif perc_from_edge > 100:
            perc_from_edge = 100

        # if we're asked to calculate from the right edge, invert it
        if from_right_edge:
            perc_from_edge = 100 - perc_from_edge

        return perc_from_edge

    # determine which zone the paddle dropped into
    def dist2action(self, perc, v):
        zones = PADDLE_ZONES[abs(v)]
        aggr_perc = 0
        for zone in zones:
            if aggr_perc <= perc <= (aggr_perc + zone[0]):
                return zone
            aggr_perc += zone[0]
# }}}
# {{{ Game
class Game:
    def __init__(self, args):
        global screen_width, screen_height
        self.nbricks = args.bricks
        self.nballs = args.nballs
        self.nballs_play = 0
        self.nballs_lost = 0
        self.nrows = args.rows
        self.speed = args.speed
        self.move = args.move
        self.level = args.level
        self.infinite = args.infinite
        self.paddle_width = args.paddle_width
        self.init_sleep = None
        self.auto = args.auto
        self.auto_aim_perc = None
        screen_width = (self.nbricks * BRICK_WIDTH) + (BRICK_HSPACE * (self.nbricks-1))
        screen_height = ABOVE_WALL_EMPTY_SPACE \
                        + self.nrows*BRICK_HEIGHT \
                        + (self.nrows-1)*BRICK_VSPACE \
                        + BELOW_WALL_EMPTY_SPACE \
                        + PADDLE_HEIGHT \
                        + BELOW_PADDLE_EMPTY_SPACE

        self.screen = pygame.display.set_mode([screen_width, screen_height])
        self.v_mult = LEVEL_UP_MULT ** (self.level-1)
        self.nlives_left = args.lives-1
        self.sprites = pygame.sprite.Group()

        # start a thread to load the font
        self.font = None
        self.font_loader_thread = threading.Thread(target=self.load_font)
        self.font_loader_thread.daemon = True
        self.font_loader_thread.start()

        self.create_sprites()

    def create_sprites(self):
        self.balls = []
        self.sprites.empty()

        self.wall = Wall(self.nrows, self.nbricks, self.level)
        self.sprites.add(self.wall.bricks)

        self.paddle = Paddle(self.paddle_width, self.move, self.v_mult)
        self.sprites.add(self.paddle)
        self.spawn_ball()

    def spawn_ball(self):
        ball_init_xy = (int((screen_width/2)-BALL_RADIUS), self.wall.bottom_y() + BELOW_WALL_SPACE_TO_BALL)
        ball = Ball(ball_init_xy, self.speed, self.v_mult)
        self.balls.append(ball)
        self.sprites.add(ball)
        self.nballs_play += 1

    def load_font(self):
        self.font = pygame.font.SysFont(None, LEVEL_SIZE)

    def draw_base_screen(self):
        self.screen.fill(BGCOLOR)
        for i in range(self.nlives_left):
            pygame.draw.circle(self.screen, BALL_COLOR, (LIVES_X + i*LIVES_SPACE, LIVES_Y), BALL_RADIUS, BALL_RADIUS)

        if self.font:
            self.font_loader_thread.join()
            img = self.font.render("L: " + str(self.level), True, FGCOLOR)
            self.screen.blit(img, (screen_width-LEVEL_X_FROM_RIGHT, LEVEL_Y))

    def draw(self):
        self.draw_base_screen()
        self.sprites.draw(self.screen)
        pygame.display.flip()

    # returns the balls that dropped
    def balls_dropped(self):
        balls = []
        for ball in self.balls:
            if ball.gety() > (screen_height - PADDLE_HEIGHT - BELOW_PADDLE_EMPTY_SPACE):
                balls.append(ball)
        return balls


    def balls_edge_bounce(self):
        for ball in self.balls:
            # right edge bounce
            if ball.getx() >= (screen_width-(2*BALL_RADIUS)):
                ball.setx(screen_width-(2*BALL_RADIUS)-1)
                ball.hbounce()

            # left edge bounce
            elif ball.x <= 0:
                ball.setx(1)
                ball.hbounce()

            # top edge bounce: can only happen if ball is heading up; this
            # prevents a bug where occassionally the ball goes a bit outside
            # the edge and gets stuck to the top for a bit
            elif ball.yvelocity() < 0 and ball.gety() <= 0:
                ball.vbounce()

    # returns the set of brick balls bounced against, or None
    def bounced_bricks(self):
        bricks = []
        for ball in self.balls:
            brick = pygame.sprite.spritecollideany(ball, self.wall.bricks, ball_brick_collided)
            if brick:
                bricks.append(brick)
        return bricks

    # returns balls that bounce off the paddle
    def ball_paddle_bounce(self):
        balls = []
        for ball in self.balls:
            # surely, if the ball is going up, it can't bounce against the
            # paddle; this is to prevent a bug that sometimes seems to occur
            # on the first bounce in the game, where the ball "sinks" into
            # the paddle. (you can even observe it.) this is a workaround
            if ball.yvelocity() < 0:
                continue
            mask = pygame.sprite.collide_mask(ball, self.paddle)
            if mask:
                balls.append(ball)
        return balls

    #
    # determines the ball's behavior depending on:
    #
    # 1) what direction the ball is coming from
    # 2) what speed the the ball has
    # 3) where on the paddle it landed
    #
    #                      \
    #                       \
    #                        \
    #                         O
    # ---------------------------------------------------
    # | zone 1 |        zone 2          |    zone 3     |
    # ---------------------------------------------------
    #
    # 0%        25%        50%           75%         100%
    #
    # In the above picture, the ball is coming from left to right
    # (ball.left_to_right() == True) with a horizontal velocity of
    # ball.xvelocity() and it lands on zone 2 of the paddle.
    #
    # The paddle's zones are defined in PADDLE_ZONES. PADDLE_ZONES is a
    # 2-tier hash table, first keyed by the velocity of the incoming
    # ball, and secondly by the percentage away from the left edge. (Or,
    # when the ball is coming from the right, the percentage away from
    # the right edge.)
    #
    # Determine outgoing horizontal velocity as follows.
    #
    # 1. Look up PADDLE_ZONES[ball.xvelocity()]. (That is, look up
    # the behavior given that ball's current horizontal velocity.)
    #
    # 2. Determine which zone of the paddle the ball landed on by
    # going through the paddle's zones (as defined in the table that
    # emerged from the previous step), from left to right.
    #
    # 3. Either set ("SET" in PADDLE_ZONES) the ball's new horizontal
    # velocity, or multiply ("MULT" in PADDLE_ZONES) the current ball's
    # horizontal velocity.
    #
    def handle_paddle_bounce(self, ball):
        # compute the distance (in %'s) from the paddle's edge
        perc_from_edge = self.paddle.edge_dist(ball.getx(), ball.right_to_left())
        # print("perc_from_edge = " + str(perc_from_edge))

        # find the paddle zone the ball fell into and retrieve the
        # action we are supposed to take
        action = self.paddle.dist2action(perc_from_edge, ball.xvelocity())
        # print("action = " + str(action))

        # handle vertical bounce; this always happens when bouncing
        # against the paddle
        ball.vbounce()

        # make sure that the ball is firmly above the paddle and didn't
        # accidentally "sink into" the paddle due to a rounding error
        # self.ball.rect.y = self.paddle.rect.y - 2*BALL_RADIUS

        # handle horizontal bounce based on the action
        if action[1] == "MULT":
            ball.set_xvelocity(ball.xvelocity() * action[2])

        elif action[1] == "SET":
            r2l = ball.right_to_left()
            ball.set_xvelocity(action[2])
            if r2l:
                ball.hbounce() # invert direction, because action[2] was a left-to-right number

        else:
            print("undefined action = " + action[1])

        # print("ball velocity after paddle bounce: " + str(self.ball.velocity))

    #
    # auto-moves the paddle
    #
    # Decides randomly where on the paddle to let the ball bounce
    # (self.auto_aim_perc) and then smoothly moves the paddle along while adjusting
    #
    #
    def auto_move(self, ball):
        #
        # choose a new auto_aim_perc; this is right after a ball/paddle bounce
        #
        # auto_aim_perc is where on the paddle (in % from the left edge) we're aiming the ball
        # pre_perc is where on the paddle the ball was aimed when we chose a new auto_aim_perc.
        # auto_diff_perc is the difference between those two
        # auto_diff_perc_counter is used to slowly go from 0 to auto_diff_perc
        #
        if not self.auto_aim_perc:
            self.auto_aim_perc = random.randint(2, 98)
            self.auto_prev_perc = ((ball.getx() - self.paddle.rect.x) / self.paddle_width) * 100
            self.auto_diff_perc = self.auto_aim_perc - self.auto_prev_perc
            self.auto_diff_perc_counter = 0.0

        # compute where the paddle should move based on auto_prev_perc
        # compute where the paddle should move based on auto_aim_perc
        # compute where the paddle should move to slowly get from auto_prev_perc to auto_aim_perc
        prev_goto_x = ball.getx() - ((self.paddle_width/100) * (self.auto_prev_perc))
        aim_goto_x = ball.getx() - ((self.paddle_width/100) * (self.auto_aim_perc))
        now_goto_x = prev_goto_x - ((self.paddle_width/100) * self.auto_diff_perc_counter)

        # get auto_diff_perc_counter a bit closer to auto_diff_perc
        if self.auto_diff_perc < 0   and self.auto_diff_perc_counter > self.auto_diff_perc:
            self.auto_diff_perc_counter -= 0.25
        elif self.auto_diff_perc > 0 and self.auto_diff_perc_counter < self.auto_diff_perc:
            self.auto_diff_perc_counter += 0.25

        # move paddle
        self.paddle.rect.x = int(now_goto_x)
        self.paddle.edge_correct()

    #
    # returns value of running
    #
    def update(self, key_presses):
        if self.auto:
            self.auto_move(self.balls[0])
        else:
            self.paddle.set_keypress(key_presses)

        # update model
        self.sprites.update(self.screen)

        # rest a bit second before the game begins
        if not self.init_sleep:
            pygame.time.delay(int(INIT_SLEEP * 1000))
            self.init_sleep = True


        # if ball dropped below paddle, remove it
        balls = self.balls_dropped()
        for ball in balls:
            self.sprites.remove(ball)
            self.balls.remove(ball)
            self.nballs_play -= 1
            self.nballs_lost += 1

        # if no more balls in game, lose
        if len(self.balls) == 0:
            pygame.time.delay(int(DEATH_SLEEP * 1000))
            if not self.infinite:
                self.nlives_left -= 1
                if self.nlives_left < 0:
                    return False
            self.paddle.reset()
            self.nballs_lost = 0
            self.nballs_play = 0
            self.spawn_ball()
            self.auto_aim_perc = None
            self.init_sleep = False

            return True

        # handle edge bounces
        self.balls_edge_bounce()

        # collision between paddle and ball
        balls = self.ball_paddle_bounce()
        if balls:
            # if playing with multiple balls, release each consecutive ball on a bounce
            if self.nballs_play + self.nballs_lost < self.nballs:
                self.spawn_ball()

            for ball in balls:
                self.handle_paddle_bounce(ball)
                self.auto_aim_perc = None

        # collision between ball and bricks
        bricks = self.bounced_bricks()
        for brick in bricks:
            self.wall.kill(brick)
            self.sprites.remove(brick)

        # all bricks gone: level up
        if self.wall.is_empty():
            self.draw() # satisfying to see the last brick disappear
            pygame.time.delay(int(DEATH_SLEEP * 1000))
            self.nlives_left += 1
            self.level += 1
            if self.level > len(BRICK_COLORS):
                print("You are Brick Breaker God.")
                return False
            self.v_mult = LEVEL_UP_MULT ** (self.level-1)
            self.auto_aim_perc = None
            self.create_sprites()

        return True

# }}}

# {{{ ball_wall_collided
def ball_brick_collided(ball, brick):
    bounce_point = pygame.sprite.collide_mask(ball, brick)
    if bounce_point != None:
        # if y coordinate is 0 or greater than 2*RADIUS, vertical bounce
        if bounce_point[1] == 0 or bounce_point[1] > BALL_RADIUS:
            ball.vbounce()
        # if x coordinate is 0 or greater than 2*RADIUS, horizontal
        # bounce
        elif bounce_point[0] == 0 or bounce_point[0] > BALL_RADIUS:
            ball.hbounce()

        # this is an edge case: guess: most likely a vertical bounce is
        # the way to go
        else:
            ball.vbounce()
    return bounce_point
# }}}

# {{{ main
def main():
    parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog="""\
in-game keys:
  left and right   : move paddle left and right
  < and >          : move paddle left and right
  shift + <move>   : move paddle a little bit
  space or p       : (un)pause game
  esc or q         : quit game
""")
    parser.add_argument("--nballs", help="# of balls", type=int, default=NBALLS)
    parser.add_argument("--lives", help="# of lives", type=int, default=NLIVES)
    parser.add_argument("--level", help="level to start at", type=int, default=LEVEL_START)
    parser.add_argument("--rows", help="number of rows", type=int, default=NROWS)
    parser.add_argument("--bricks", help="number of bricks in a row", type=int, default=NBRICKS)
    parser.add_argument("--paddle-width", help="width of a paddle in pixels", type=int, default=PADDLE_WIDTH)
    parser.add_argument("--speed", help="ball speed", type=float, default=BALL_Y_SPEED)
    parser.add_argument("--move", help="paddle move speed", type=float, default=PADDLE_MOVE)
    parser.add_argument("--infinite", help="infinite lives", action="store_true")
    parser.add_argument("--auto", help="self play", action="store_true")
    args = parser.parse_args()

    pygame.init()
    pygame.display.set_caption('Brick Breaker')

    game = Game(args)

    clock = pygame.time.Clock()

    running, paused = True, False
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN:
                if event.key in [pygame.K_SPACE, pygame.K_p]:
                    paused = not paused
                elif event.key in [pygame.K_ESCAPE, pygame.K_q]:
                    running = False

        if not running:
            break

        if paused:
            continue

        game.draw()
        clock.tick(NFRAMES)

        running = game.update(pygame.key.get_pressed())

    pygame.quit()

if __name__ == "__main__":
    main()
# }}}
