from math import pi
from traceback import print_exc
from browser import doc, html
from browser.timer import set_timeout, clear_timeout

levels = {
    1: '''.........
          .1.1.1.1.
          .........
          .1.1.1.1.
          .........
          .1.1.1.1.
          .........''',
    2: '''123123123123
          312312312312
          231231231231
          123123123123
          312312312312
          231231231231
          123123123123''',
    3: '''
          11112221111
          11111111111
          111..3..111
          111XXXXX111
          11111111111
          11111111111
          322X2X2X223'''
}


class MakeLevel(object):
    def __init__(self, ctx, area, send_msg=None):
        self.ctx = ctx
        self.area = area
        self.colors = ["grey", "green", "blue", "brown"]
        self.send_message = send_msg

    def new_level(self, n):
        bricks = []
        level = levels[n].split("\n")
        nb_rows = len(level)
        nb_cols = len(level[0].strip())
        for row in level:
            assert len(row.strip()) == nb_cols
        brick_width = (self.area["max_x"] - self.area["min_x"])/nb_cols
        brick_height = (self.area["max_y"] - self.area["min_y"])/nb_rows

        y = self.area["min_y"] - brick_height
        for row in level:
            row = row.strip()
            y += brick_height
            x = self.area["min_x"] - brick_width
            for brick in row:
                x += brick_width
                if brick == ".":
                    continue
                elif brick == "X":
                    bricks.append(Brick(x, y, width=brick_width,
                        height=brick_height, color=self.colors[0],
                        breakable=False, send_msg=self.send_message,
                        ctx=self.ctx))
                else:
                    bricks.append(Brick(x, y, width=brick_width,
                        height=brick_height, color=self.colors[int(brick)],
                        nb_hits=int(brick), send_msg=self.send_message,
                        ctx=self.ctx))
        return bricks


class Player(object):
    '''Simple class to keep track of player's lives'''
    def __init__(self, lives):
        self.lives = lives
        self.lost_life = False

    def get_lives(self):
        return self.lives

    def lose_ball(self):
        '''Determines what to do when ball is lost'''
        self.lives -= 1
        self.lost_life = True


class GameObject(object):
    '''A basic game object can draw and update itself'''
    def __init__(self):
        pass

    def draw(self):
        pass

    def update(self):
        pass


class Display(GameObject):
    '''Container for the various canvases used to draw the game'''
    def __init__(self):
        self.game_canvas = doc["game-canvas"]
        self.game_canvas_ctx = self.game_canvas.getContext('2d')
        self.width = self.game_canvas.width
        self.height = self.game_canvas.height

        main_canvas = doc["main-canvas"]
        self.main_canvas_ctx = main_canvas.getContext("2d")

        background_canvas = doc["background-canvas"]
        self.background_canvas_ctx = background_canvas.getContext("2d")

        far_background_canvas = doc["far-background-canvas"]
        self.far_background_canvas_ctx = far_background_canvas.getContext("2d")
        self.far_background_canvas_ctx.globalAlpha = 0.5  # making transparent
        self.far_background_image = html.IMG(src="src/images/code_background.png")

        # html canvas image loading workaround to ensure that image is available
        def on_load(*arg):
            self.draw_far_background()
        self.far_background_image.onload = on_load

    def clear_main(self):
        '''Clears main canvas: the one with moving objects'''
        self.main_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_game(self):
        '''Clears the game canvas: the one that is "clickable" and where
           the textual presentation is displayed as an overlay.'''
        self.game_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_background(self):
        '''Clears the background canvas: where the bricks are drawn'''
        self.background_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_far_background(self):
        '''Clears the far background: where the background image is drawn.'''
        self.far_background_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_all(self):
        '''Clears all the drawing canvases'''
        self.clear_main()
        self.clear_game()
        self.clear_background()
        self.clear_far_background()

    def draw(self):
        self.clear_main()

    def draw_far_background(self):
        '''Draws background image'''
        self.far_background_canvas_ctx.drawImage(self.far_background_image, 0, 0)


class GameInfo(GameObject):
    '''General information about the game shown to the player'''
    def __init__(self, ctx, width, height):
        self.ctx = ctx
        self.width = width
        self.height = height

    def show_game_over(self):
        self.ctx.font = "100px sans-serif"
        self.ctx.fillStyle = "red"
        self.ctx.fillText("Game over!", 50, 300)

    def show_game_win(self):
        self.ctx.font = "100px sans-serif"
        self.ctx.fillStyle = "green"
        self.ctx.fillText("You win!", 50, 300)

    def write_help(self):
        self.ctx.font = "30px sans-serif"
        self.ctx.fillStyle = "rgba(100, 100, 100, 0.6)"
        self.ctx.fillRect(0, 0, self.width, self.height)
        self.ctx.fillStyle = "blue"
        self.ctx.fillText("S to start the animation", 50, 100)
        self.ctx.fillText("P to pause the animation", 50, 150)
        self.ctx.fillText("Q to quit: click BEFORE editing!", 50, 200)


class Score(GameObject):
    '''Game score updated and drawn from this class'''
    def __init__(self, world=None, ctx=None, ball_color="red", get_lives=None):
        self.score = 0
        self.world = world
        self.ctx = ctx
        self.score_x = self.world["max_x"] - 50
        self.lives_x = 30
        self.get_lives = get_lives
        self.ball_color = ball_color
        self.ball_radius = 10
        self.baseline = 15

    def update(self, info=None):
        '''Updates the score based on information received'''
        if info is None:
            return

        if info == "ball hit paddle":
            self.score += 10
        elif info == "ball hit wall":
            self.score -= 10   # penalize slow play
        elif info == "ball hit brick":
            self.score += 100
        elif info == "break brick":
            self.score += 1000
        elif info == "win":
            self.score += 10000
        elif info == "lose ball":
            self.score -= 1000
        else:
            pass

    def draw(self):
        '''Draws the score on the screen, including graphical information about lives'''
        # prepare area for drawing
        self.ctx.clearRect(0, 0, self.world["max_x"], self.world["max_y"])
        self.ctx.fillStyle = "black"
        self.ctx.fillRect(0, 0, self.world["max_x"], self.world["max_y"])
        # draw score
        self.ctx.font = "15px sans-serif"
        self.ctx.fillStyle = "white"
        self.ctx.fillText(str(self.score), self.score_x, self.baseline)
        # draw lives
        self.ctx.fillStyle = self.ball_color
        for life in range(self.get_lives()):
            self.ctx.beginPath()
            self.ctx.arc(self.lives_x + life*4*self.ball_radius, self.baseline,
                         self.ball_radius, 0, pi*2)
            self.ctx.closePath()
            self.ctx.fill()


class Ball(GameObject):
    '''The basic object bounced by the player using a paddle.'''
    def __init__(self, x, y, radius=10, color='red', dx=5, dy=5,
                 send_msg=None, world=None, ctx=None):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
        self.dx = dx
        self.dy = dy
        self.send_message = send_msg
        self.world = world
        self.ctx = ctx

        self.speed = (self.dx**2 + self.dy**2)**0.5
        self.max_speed = 2*self.speed
        self.calculate_bounding_box()

    def draw(self):
        '''Displays on the screen'''
        self.ctx.fillStyle = self.color
        self.ctx.beginPath()
        self.ctx.arc(self.x+self.radius, self.y+self.radius, self.radius, 0, pi*2)
        self.ctx.closePath()
        self.ctx.fill()

    def update(self):
        self.move()
        self.stay_in_world()

    def move(self):
        self.x += self.dx
        self.y += self.dy

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + 2*self.radius
        self.y_max = self.y + 2*self.radius

    def handle_hit_with(self, other):
        '''Determine what must be done to a ball when it hits either
           the self.paddle or a brick'''
        if isinstance(other, Paddle) and self.dy > 0:
            self.handle_hit_with_paddle(other)
        elif isinstance(other, Brick):
            self.handle_hit_with_brick(other)

    def handle_hit_with_paddle(self, paddle):
        self.reverse_vertical_move()
        offset = self.x - (paddle.x + paddle.width/2)
        self.dx = 10*offset/paddle.width
        speed = (self.dx**2 + self.dy**2)**0.5
        self.dx *= (self.speed/speed)
        self.dy *= (self.speed/speed)
        self.send_message("ball hit paddle")

    def reverse_vertical_move(self):
        self.y -= self.dy
        self.dy = -self.dy
        self.calculate_bounding_box()

    def reverse_horizontal_move(self):
        self.x -= self.dx
        self.dx = -self.dx
        self.calculate_bounding_box()

    def handle_hit_with_brick(self, brick):
        self.reverse_vertical_move()
        if self.overlaps_with(brick):  # horizontal hit?
            self.reverse_vertical_move()
            self.reverse_horizontal_move()
            if self.overlaps_with(brick):
                self.reverse_vertical_move()
        if self.speed < self.max_speed:
            self.speed *= 1.05
            self.dx *= 1.05
            self.dy *= 1.05
        self.send_message("ball hit brick")

    def overlaps_with(self, other):
        '''Determines if a Ball instance overlaps (collides) with another object'''
        return (((self.x_min < other.x_min < self.x_max)
                or (other.x_min < self.x_min < other.x_max))
                and ((self.y_min < other.y_min < self.y_max)
                or (other.y_min < self.y_min < other.y_max)))

    def stay_in_world(self):
        '''Ensures that the ball stays within the proper boundaries'''
        if self.x < self.world["min_x"] and self.dx < 0:
            self.dx = -self.dx
            self.x = 2*self.world["min_x"] - self.x
            self.send_message("hit wall")
        elif self.x > self.world["max_x"] - 2*self.radius and self.dx > 0:
            self.dx = -self.dx
            self.x = 2*(self.world["max_x"] - 2*self.radius) - self.x
            self.send_message("hit wall")
        if self.y < self.world["min_y"] and self.dy < 0:
            self.dy = -self.dy
            self.y = 2*self.world["min_y"] - self.y
            self.send_message("hit wall")
        elif self.y > self.world["max_y"] - 2*self.radius and self.dy > 0:
            if DEBUG:
                self.dy = -self.dy
                self.y = 2*(self.world["max_y"] - 2*self.radius) - self.y
            else:
                self.send_message("lose ball")
        self.calculate_bounding_box()


class Paddle(GameObject):

    def __init__(self, x, y, width=80, height=10, color="blue", dx=7,
                get_mouse_position=None, world=None, ctx=None):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.dx = dx
        self.calculate_bounding_box()
        self.get_mouse_position = get_mouse_position
        self.world = world
        self.ctx = ctx

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + self.width
        self.y_max = self.y + self.height

    def draw(self):
        self.ctx.fillStyle = self.color
        self.ctx.fillRect(self.x, self.y, self.width, self.height)

    def move(self, direction):
        if direction == "left":
            self.dx = - abs(self.dx)
        elif direction == "right":
            self.dx = abs(self.dx)
        self.x += self.dx
        self.stay_in_world()
        return False

    def mouse_move(self, ev):
        x, y = self.get_mouse_position(ev)
        self.x = x
        self.stay_in_world()

    def stay_in_world(self):
        if self.x < self.world["min_x"]:
            self.x = self.world["min_x"]
        elif self.x + self.width > self.world["max_x"]:
            self.x = self.world["max_x"] - self.width
        self.calculate_bounding_box()


class Brick(GameObject):

    def __init__(self, x, y, width=60, height=30, color="green",
            breakable=True, nb_hits=1,
            send_msg=None, ctx=None):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.is_breakable = breakable
        self.is_hittable = True
        self.is_visible = True
        self.nb_hits = nb_hits
        self.send_message = send_msg
        self.ctx = ctx
        self.calculate_bounding_box()

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + self.width
        self.y_max = self.y + self.height

    def draw(self):
        if self.is_visible:
            self.ctx.fillStyle = self.color
            self.ctx.fillRect(self.x, self.y, self.width, self.height)

    def handle_hit_with(self, other):
        if isinstance(other, Ball) and self.is_breakable and self.is_hittable:
            self.nb_hits -= 1
            if self.nb_hits == 0:
                self.is_visible = False
                self.is_hittable = False
                self.ctx.clearRect(self.x-1, self.y-1, self.width+2, self.height+2)
                self.send_message("break brick")


class Game(object):

    def __init__(self):
        self.pause = True
        self.fps = 30
        self.time_between_frames = 1000/self.fps   # time between frames in ms
        self.frame_id = None
        self.display = Display()

        score_height = 30
        self.top_display = {"min_x": 0, "min_y": 0,
            "max_x": self.display.width, "max_y": score_height}
        self.bottom_display = {"min_x": 0, "min_y": score_height,
            "max_x": self.display.width, "max_y": self.display.height}

        brick_area = {"min_x": 0, "min_y": score_height+100,
            "max_x": self.display.width, "max_y": self.display.height-200}
        self.make_level = MakeLevel(ctx=self.display.background_canvas_ctx,
                          area=brick_area, send_msg=self.receive_message)
        self.level = 1

        self.player = Player(lives=3)
        self.add_game_objects()
        #
        doc.bind("mousemove", self.paddle.mouse_move)
        doc.bind("keydown", self.handle_keydown_events)
        #
        self.display.clear_all()
        self.display.draw_far_background()
        self.info.write_help()

    def add_game_objects(self):
        self.objects = []
        self.ball = Ball(10, self.display.height-30, dy=-5,
                        send_msg=self.receive_message,
                        ctx=self.display.main_canvas_ctx,
                        world=self.bottom_display)
        self.objects.append(self.ball)
        if self.level == 1:
            self.paddle = Paddle(100, self.display.height-20,
                get_mouse_position=self.get_mouse_position,
                world=self.bottom_display, ctx=self.display.main_canvas_ctx)
        self.objects.append(self.paddle)
        self.bricks = self.make_level.new_level(self.level)
        self.objects.extend(self.bricks)
        if self.level == 1:
            self.info = GameInfo(self.display.game_canvas_ctx, self.display.width,
                             self.display.height)
        self.objects.append(self.info)
        if self.level == 1:
            self.score = Score(world=self.top_display,
                           ctx=self.display.main_canvas_ctx,
                           ball_color=self.ball.color,
                           get_lives=self.player.get_lives)
        self.objects.append(self.score)

    def receive_message(self, info):
        self.score.update(info)
        if info == "lose ball":
            self.player.lose_ball()
            self.play_sound("problem-sound")
        elif info == "ball hit paddle":
            self.play_sound("paddle-sound")
        elif info == "ball hit brick":
            self.play_sound("break-sound")

    def handle_collisions(self):
        self.next_level = True
        if self.ball.overlaps_with(self.paddle):
            self.ball.handle_hit_with(self.paddle)
        for brick in self.bricks:
            if brick.is_hittable:
                if brick.is_breakable:
                    self.next_level = False
                if self.ball.overlaps_with(brick):
                    self.ball.handle_hit_with(brick)
                    brick.handle_hit_with(self.ball)

    def next_frame(self):
        if self.next_level:
            self.pause = True
            self.level += 1
            if self.level > len(levels):
                self.info.show_game_win()
                self.play_sound("win-sound")
                return False
            self.add_game_objects()
            self.info.write_help()
            return False
        elif self.pause:
            self.info.write_help()
            if self.frame_id is not None:
                clear_timeout(self.frame_id)
            return False
        elif self.player.lost_life:
            if self.player.lives == 0:
                self.pause = True
                self.info.show_game_over()
                return False
            else:
                self.pause = True
                self.player.lost_life = False
                self.ball = Ball(10, self.display.height-30, dy=-5,
                    send_msg=self.receive_message,
                    ctx=self.display.main_canvas_ctx, world=self.bottom_display)
                self.objects[0] = self.ball
                return True
        else:
            return True

    def loop(self):
        '''The game loop is the heart of the program'''
        try:
            self.display.clear_main()
            for obj in self.objects:
                obj.update()
            self.handle_collisions()
            if self.next_frame():
                for obj in self.objects:
                    obj.draw()
                self.show_fps()
                self.frame_id = set_timeout(self.loop, self.time_between_frames)
        except:
            print_exc()
            notify("blue")

    def show_fps(self):
        ctx = self.display.main_canvas_ctx
        ctx.font = "15px sans-serif"
        ctx.fillStyle = "yellow"
        ctx.fillText("FPS: %d" % self.fps, 300, 15)

    def start(self):
        self.pause = False
        self.display.game_canvas.style.cursor = "none"
        self.display.clear_game()
        self.loop()

    def quit(self):
        doc.unbind("keydown")
        doc.unbind("mousemove")
        self.display.game_canvas.style.cursor = "default"
        self.display.clear_all()
        if self.frame_id is not None:
            clear_timeout(self.frame_id)

    def change_fps(self, increment):
        self.fps += increment
        if self.fps < 1:
            self.fps = 1
        self.time_between_frames = 1000/self.fps

    def handle_keydown_events(self, ev):
        remind = True
        if ev.keyCode == 37:   # left arrow
            remind = self.paddle.move("left")
        if ev.keyCode == 38:   # up arrow
            self.change_fps(3)
            remind = False
        if ev.keyCode == 39:   # right arrow
            remind = self.paddle.move("right")
        if ev.keyCode == 40:   # down arrow
            self.change_fps(-5)
            remind = False
        if ev.keyCode == 80:  # p or P for Pause
            self.pause = True
            remind = False
        elif ev.keyCode == 81:  # q or Q  for Quit
            self.quit()
            remind = False
        elif ev.keyCode == 83 and self.pause:  # s or S for Start
            self.start()
            remind = False
        ev.preventDefault()
        if remind:
            notify("red")

    def get_mouse_position(self, ev):
        bound = self.display.game_canvas.getBoundingClientRect()
        x = ev.clientX - bound.left
        y = ev.clientY - bound.top
        return x, y

    def play_sound(self, sound_id):
        current_sound = doc[sound_id]
        current_sound.load()
        current_sound.play()


if __name__ == "__main__":
    DEBUG = False
    game = Game()

from math import pi
from traceback import print_exc
from browser import doc, html
from browser.timer import set_timeout, clear_timeout

levels = {
    1: '''.........
          .1.1.1.1.
          .........
          .1.1.1.1.
          .........
          .1.1.1.1.
          .........''',
    2: '''123123123123
          312312312312
          231231231231
          123123123123
          312312312312
          231231231231
          123123123123''',
    3: '''
          11112221111
          11111111111
          111..3..111
          111XXXXX111
          11111111111
          11111111111
          322X2X2X223'''
}


class MakeLevel(object):
    def __init__(self, ctx, area, send_msg=None):
        self.ctx = ctx
        self.area = area
        self.colors = ["grey", "green", "blue", "brown"]
        self.send_message = send_msg

    def new_level(self, n):
        bricks = []
        level = levels[n].split("\n")
        nb_rows = len(level)
        nb_cols = len(level[0].strip())
        for row in level:
            assert len(row.strip()) == nb_cols
        brick_width = (self.area["max_x"] - self.area["min_x"])/nb_cols
        brick_height = (self.area["max_y"] - self.area["min_y"])/nb_rows

        y = self.area["min_y"] - brick_height
        for row in level:
            row = row.strip()
            y += brick_height
            x = self.area["min_x"] - brick_width
            for brick in row:
                x += brick_width
                if brick == ".":
                    continue
                elif brick == "X":
                    bricks.append(Brick(x, y, width=brick_width,
                        height=brick_height, color=self.colors[0],
                        breakable=False, send_msg=self.send_message,
                        ctx=self.ctx))
                else:
                    bricks.append(Brick(x, y, width=brick_width,
                        height=brick_height, color=self.colors[int(brick)],
                        nb_hits=int(brick), send_msg=self.send_message,
                        ctx=self.ctx))
        return bricks


class Player(object):
    '''Simple class to keep track of player's lives'''
    def __init__(self, lives):
        self.lives = lives
        self.lost_life = False

    def get_lives(self):
        return self.lives

    def lose_ball(self):
        '''Determines what to do when ball is lost'''
        self.lives -= 1
        self.lost_life = True


class GameObject(object):
    '''A basic game object can draw and update itself'''
    def __init__(self):
        pass

    def draw(self):
        pass

    def update(self):
        pass


class Display(GameObject):
    '''Container for the various canvases used to draw the game'''
    def __init__(self):
        self.game_canvas = doc["game-canvas"]
        self.game_canvas_ctx = self.game_canvas.getContext('2d')
        self.width = self.game_canvas.width
        self.height = self.game_canvas.height

        main_canvas = doc["main-canvas"]
        self.main_canvas_ctx = main_canvas.getContext("2d")

        background_canvas = doc["background-canvas"]
        self.background_canvas_ctx = background_canvas.getContext("2d")

        far_background_canvas = doc["far-background-canvas"]
        self.far_background_canvas_ctx = far_background_canvas.getContext("2d")
        self.far_background_canvas_ctx.globalAlpha = 0.5  # making transparent
        self.far_background_image = html.IMG(src="src/images/code_background.png")

        # html canvas image loading workaround to ensure that image is available
        def on_load(*arg):
            self.draw_far_background()
        self.far_background_image.onload = on_load

    def clear_main(self):
        '''Clears main canvas: the one with moving objects'''
        self.main_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_game(self):
        '''Clears the game canvas: the one that is "clickable" and where
           the textual presentation is displayed as an overlay.'''
        self.game_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_background(self):
        '''Clears the background canvas: where the bricks are drawn'''
        self.background_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_far_background(self):
        '''Clears the far background: where the background image is drawn.'''
        self.far_background_canvas_ctx.clearRect(0, 0, self.width, self.height)

    def clear_all(self):
        '''Clears all the drawing canvases'''
        self.clear_main()
        self.clear_game()
        self.clear_background()
        self.clear_far_background()

    def draw(self):
        self.clear_main()

    def draw_far_background(self):
        '''Draws background image'''
        self.far_background_canvas_ctx.drawImage(self.far_background_image, 0, 0)


class GameInfo(GameObject):
    '''General information about the game shown to the player'''
    def __init__(self, ctx, width, height):
        self.ctx = ctx
        self.width = width
        self.height = height

    def show_game_over(self):
        self.ctx.font = "100px sans-serif"
        self.ctx.fillStyle = "red"
        self.ctx.fillText("Game over!", 50, 300)

    def show_game_win(self):
        self.ctx.font = "100px sans-serif"
        self.ctx.fillStyle = "green"
        self.ctx.fillText("You win!", 50, 300)

    def write_help(self):
        self.ctx.font = "30px sans-serif"
        self.ctx.fillStyle = "rgba(100, 100, 100, 0.6)"
        self.ctx.fillRect(0, 0, self.width, self.height)
        self.ctx.fillStyle = "blue"
        self.ctx.fillText("S to start the animation", 50, 100)
        self.ctx.fillText("P to pause the animation", 50, 150)
        self.ctx.fillText("Q to quit: click BEFORE editing!", 50, 200)


class Score(GameObject):
    '''Game score updated and drawn from this class'''
    def __init__(self, world=None, ctx=None, ball_color="red", get_lives=None):
        self.score = 0
        self.world = world
        self.ctx = ctx
        self.score_x = self.world["max_x"] - 50
        self.lives_x = 30
        self.get_lives = get_lives
        self.ball_color = ball_color
        self.ball_radius = 10
        self.baseline = 15

    def update(self, info=None):
        '''Updates the score based on information received'''
        if info is None:
            return

        if info == "ball hit paddle":
            self.score += 10
        elif info == "ball hit wall":
            self.score -= 10   # penalize slow play
        elif info == "ball hit brick":
            self.score += 100
        elif info == "break brick":
            self.score += 1000
        elif info == "win":
            self.score += 10000
        elif info == "lose ball":
            self.score -= 1000
        else:
            pass

    def draw(self):
        '''Draws the score on the screen, including graphical information about lives'''
        # prepare area for drawing
        self.ctx.clearRect(0, 0, self.world["max_x"], self.world["max_y"])
        self.ctx.fillStyle = "black"
        self.ctx.fillRect(0, 0, self.world["max_x"], self.world["max_y"])
        # draw score
        self.ctx.font = "15px sans-serif"
        self.ctx.fillStyle = "white"
        self.ctx.fillText(str(self.score), self.score_x, self.baseline)
        # draw lives
        self.ctx.fillStyle = self.ball_color
        for life in range(self.get_lives()):
            self.ctx.beginPath()
            self.ctx.arc(self.lives_x + life*4*self.ball_radius, self.baseline,
                         self.ball_radius, 0, pi*2)
            self.ctx.closePath()
            self.ctx.fill()


class Ball(GameObject):
    '''The basic object bounced by the player using a paddle.'''
    def __init__(self, x, y, radius=10, color='red', dx=5, dy=5,
                 send_msg=None, world=None, ctx=None):
        self.x = x
        self.y = y
        self.radius = radius
        self.color = color
        self.dx = dx
        self.dy = dy
        self.send_message = send_msg
        self.world = world
        self.ctx = ctx

        self.speed = (self.dx**2 + self.dy**2)**0.5
        self.max_speed = 2*self.speed
        self.calculate_bounding_box()

    def draw(self):
        '''Displays on the screen'''
        self.ctx.fillStyle = self.color
        self.ctx.beginPath()
        self.ctx.arc(self.x+self.radius, self.y+self.radius, self.radius, 0, pi*2)
        self.ctx.closePath()
        self.ctx.fill()

    def update(self):
        self.move()
        self.stay_in_world()

    def move(self):
        self.x += self.dx
        self.y += self.dy

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + 2*self.radius
        self.y_max = self.y + 2*self.radius

    def handle_hit_with(self, other):
        '''Determine what must be done to a ball when it hits either
           the self.paddle or a brick'''
        if isinstance(other, Paddle) and self.dy > 0:
            self.handle_hit_with_paddle(other)
        elif isinstance(other, Brick):
            self.handle_hit_with_brick(other)

    def handle_hit_with_paddle(self, paddle):
        self.reverse_vertical_move()
        offset = self.x - (paddle.x + paddle.width/2)
        self.dx = 10*offset/paddle.width
        speed = (self.dx**2 + self.dy**2)**0.5
        self.dx *= (self.speed/speed)
        self.dy *= (self.speed/speed)
        self.send_message("ball hit paddle")

    def reverse_vertical_move(self):
        self.y -= self.dy
        self.dy = -self.dy
        self.calculate_bounding_box()

    def reverse_horizontal_move(self):
        self.x -= self.dx
        self.dx = -self.dx
        self.calculate_bounding_box()

    def handle_hit_with_brick(self, brick):
        self.reverse_vertical_move()
        if self.overlaps_with(brick):  # horizontal hit?
            self.reverse_vertical_move()
            self.reverse_horizontal_move()
            if self.overlaps_with(brick):
                self.reverse_vertical_move()
        if self.speed < self.max_speed:
            self.speed *= 1.05
            self.dx *= 1.05
            self.dy *= 1.05
        self.send_message("ball hit brick")

    def overlaps_with(self, other):
        '''Determines if a Ball instance overlaps (collides) with another object'''
        return (((self.x_min < other.x_min < self.x_max)
                or (other.x_min < self.x_min < other.x_max))
                and ((self.y_min < other.y_min < self.y_max)
                or (other.y_min < self.y_min < other.y_max)))

    def stay_in_world(self):
        '''Ensures that the ball stays within the proper boundaries'''
        if self.x < self.world["min_x"] and self.dx < 0:
            self.dx = -self.dx
            self.x = 2*self.world["min_x"] - self.x
            self.send_message("hit wall")
        elif self.x > self.world["max_x"] - 2*self.radius and self.dx > 0:
            self.dx = -self.dx
            self.x = 2*(self.world["max_x"] - 2*self.radius) - self.x
            self.send_message("hit wall")
        if self.y < self.world["min_y"] and self.dy < 0:
            self.dy = -self.dy
            self.y = 2*self.world["min_y"] - self.y
            self.send_message("hit wall")
        elif self.y > self.world["max_y"] - 2*self.radius and self.dy > 0:
            if DEBUG:
                self.dy = -self.dy
                self.y = 2*(self.world["max_y"] - 2*self.radius) - self.y
            else:
                self.send_message("lose ball")
        self.calculate_bounding_box()


class Paddle(GameObject):

    def __init__(self, x, y, width=80, height=10, color="blue", dx=7,
                get_mouse_position=None, world=None, ctx=None):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.dx = dx
        self.calculate_bounding_box()
        self.get_mouse_position = get_mouse_position
        self.world = world
        self.ctx = ctx

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + self.width
        self.y_max = self.y + self.height

    def draw(self):
        self.ctx.fillStyle = self.color
        self.ctx.fillRect(self.x, self.y, self.width, self.height)

    def move(self, direction):
        if direction == "left":
            self.dx = - abs(self.dx)
        elif direction == "right":
            self.dx = abs(self.dx)
        self.x += self.dx
        self.stay_in_world()
        return False

    def mouse_move(self, ev):
        x, y = self.get_mouse_position(ev)
        self.x = x
        self.stay_in_world()

    def stay_in_world(self):
        if self.x < self.world["min_x"]:
            self.x = self.world["min_x"]
        elif self.x + self.width > self.world["max_x"]:
            self.x = self.world["max_x"] - self.width
        self.calculate_bounding_box()


class Brick(GameObject):

    def __init__(self, x, y, width=60, height=30, color="green",
            breakable=True, nb_hits=1,
            send_msg=None, ctx=None):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = color
        self.is_breakable = breakable
        self.is_hittable = True
        self.is_visible = True
        self.nb_hits = nb_hits
        self.send_message = send_msg
        self.ctx = ctx
        self.calculate_bounding_box()

    def calculate_bounding_box(self):
        self.x_min = self.x
        self.y_min = self.y
        self.x_max = self.x + self.width
        self.y_max = self.y + self.height

    def draw(self):
        if self.is_visible:
            self.ctx.fillStyle = self.color
            self.ctx.fillRect(self.x, self.y, self.width, self.height)

    def handle_hit_with(self, other):
        if isinstance(other, Ball) and self.is_breakable and self.is_hittable:
            self.nb_hits -= 1
            if self.nb_hits == 0:
                self.is_visible = False
                self.is_hittable = False
                self.ctx.clearRect(self.x-1, self.y-1, self.width+2, self.height+2)
                self.send_message("break brick")


class Game(object):

    def __init__(self):
        self.pause = True
        self.fps = 30
        self.time_between_frames = 1000/self.fps   # time between frames in ms
        self.frame_id = None
        self.display = Display()

        score_height = 30
        self.top_display = {"min_x": 0, "min_y": 0,
            "max_x": self.display.width, "max_y": score_height}
        self.bottom_display = {"min_x": 0, "min_y": score_height,
            "max_x": self.display.width, "max_y": self.display.height}

        brick_area = {"min_x": 0, "min_y": score_height+100,
            "max_x": self.display.width, "max_y": self.display.height-200}
        self.make_level = MakeLevel(ctx=self.display.background_canvas_ctx,
                          area=brick_area, send_msg=self.receive_message)
        self.level = 1

        self.player = Player(lives=3)
        self.add_game_objects()
        #
        doc.bind("mousemove", self.paddle.mouse_move)
        doc.bind("keydown", self.handle_keydown_events)
        #
        self.display.clear_all()
        self.display.draw_far_background()
        self.info.write_help()

    def add_game_objects(self):
        self.objects = []
        self.ball = Ball(10, self.display.height-30, dy=-5,
                        send_msg=self.receive_message,
                        ctx=self.display.main_canvas_ctx,
                        world=self.bottom_display)
        self.objects.append(self.ball)
        if self.level == 1:
            self.paddle = Paddle(100, self.display.height-20,
                get_mouse_position=self.get_mouse_position,
                world=self.bottom_display, ctx=self.display.main_canvas_ctx)
        self.objects.append(self.paddle)
        self.bricks = self.make_level.new_level(self.level)
        self.objects.extend(self.bricks)
        if self.level == 1:
            self.info = GameInfo(self.display.game_canvas_ctx, self.display.width,
                             self.display.height)
        self.objects.append(self.info)
        if self.level == 1:
            self.score = Score(world=self.top_display,
                           ctx=self.display.main_canvas_ctx,
                           ball_color=self.ball.color,
                           get_lives=self.player.get_lives)
        self.objects.append(self.score)

    def receive_message(self, info):
        self.score.update(info)
        if info == "lose ball":
            self.player.lose_ball()
            self.play_sound("problem-sound")
        elif info == "ball hit paddle":
            self.play_sound("paddle-sound")
        elif info == "ball hit brick":
            self.play_sound("break-sound")

    def handle_collisions(self):
        self.next_level = True
        if self.ball.overlaps_with(self.paddle):
            self.ball.handle_hit_with(self.paddle)
        for brick in self.bricks:
            if brick.is_hittable:
                if brick.is_breakable:
                    self.next_level = False
                if self.ball.overlaps_with(brick):
                    self.ball.handle_hit_with(brick)
                    brick.handle_hit_with(self.ball)

    def next_frame(self):
        if self.next_level:
            self.pause = True
            self.level += 1
            if self.level > len(levels):
                self.info.show_game_win()
                self.play_sound("win-sound")
                return False
            self.add_game_objects()
            self.info.write_help()
            return False
        elif self.pause:
            self.info.write_help()
            if self.frame_id is not None:
                clear_timeout(self.frame_id)
            return False
        elif self.player.lost_life:
            if self.player.lives == 0:
                self.pause = True
                self.info.show_game_over()
                return False
            else:
                self.pause = True
                self.player.lost_life = False
                self.ball = Ball(10, self.display.height-30, dy=-5,
                    send_msg=self.receive_message,
                    ctx=self.display.main_canvas_ctx, world=self.bottom_display)
                self.objects[0] = self.ball
                return True
        else:
            return True

    def loop(self):
        '''The game loop is the heart of the program'''
        try:
            self.display.clear_main()
            for obj in self.objects:
                obj.update()
            self.handle_collisions()
            if self.next_frame():
                for obj in self.objects:
                    obj.draw()
                self.show_fps()
                self.frame_id = set_timeout(self.loop, self.time_between_frames)
        except:
            print_exc()
            notify("blue")

    def show_fps(self):
        ctx = self.display.main_canvas_ctx
        ctx.font = "15px sans-serif"
        ctx.fillStyle = "yellow"
        ctx.fillText("FPS: %d" % self.fps, 300, 15)

    def start(self):
        self.pause = False
        self.display.game_canvas.style.cursor = "none"
        self.display.clear_game()
        self.loop()

    def quit(self):
        doc.unbind("keydown")
        doc.unbind("mousemove")
        self.display.game_canvas.style.cursor = "default"
        self.display.clear_all()
        if self.frame_id is not None:
            clear_timeout(self.frame_id)

    def change_fps(self, increment):
        self.fps += increment
        if self.fps < 1:
            self.fps = 1
        self.time_between_frames = 1000/self.fps

    def handle_keydown_events(self, ev):
        remind = True
        if ev.keyCode == 37:   # left arrow
            remind = self.paddle.move("left")
        if ev.keyCode == 38:   # up arrow
            self.change_fps(3)
            remind = False
        if ev.keyCode == 39:   # right arrow
            remind = self.paddle.move("right")
        if ev.keyCode == 40:   # down arrow
            self.change_fps(-5)
            remind = False
        if ev.keyCode == 80:  # p or P for Pause
            self.pause = True
            remind = False
        elif ev.keyCode == 81:  # q or Q  for Quit
            self.quit()
            remind = False
        elif ev.keyCode == 83 and self.pause:  # s or S for Start
            self.start()
            remind = False
        ev.preventDefault()
        if remind:
            notify("red")

    def get_mouse_position(self, ev):
        bound = self.display.game_canvas.getBoundingClientRect()
        x = ev.clientX - bound.left
        y = ev.clientY - bound.top
        return x, y

    def play_sound(self, sound_id):
        current_sound = doc[sound_id]
        current_sound.load()
        current_sound.play()


if __name__ == "__main__":
    DEBUG = False
    game = Game()

.........           .1.1.1.1.           .........           .1.1.1.1.           .........           .1.1.1.1.           ......... 123123123123           312312312312           231231231231           123123123123           312312312312           231231231231           123123123123 11112221111           11111111111           111..3..111           111XXXXX111           11111111111           11111111111           322X2X2X223 Simple class to keep track of player's lives Determines what to do when ball is lost A basic game object can draw and update itself Container for the various canvases used to draw the game # making transparent # html canvas image loading workaround to ensure that image is available Clears main canvas: the one with moving objects Clears the game canvas: the one that is "clickable" and where            the textual presentation is displayed as an overlay. Clears the background canvas: where the bricks are drawn Clears the far background: where the background image is drawn. Clears all the drawing canvases Draws background image General information about the game shown to the player Game score updated and drawn from this class Updates the score based on information received # penalize slow play Draws the score on the screen, including graphical information about lives # prepare area for drawing # draw score # draw lives The basic object bounced by the player using a paddle. Displays on the screen Determine what must be done to a ball when it hits either            the self.paddle or a brick # horizontal hit? Determines if a Ball instance overlaps (collides) with another object Ensures that the ball stays within the proper boundaries # time between frames in ms # # The game loop is the heart of the program # left arrow # up arrow # right arrow # down arrow # p or P for Pause # q or Q  for Quit # s or S for Start