Exemplo n.º 1
0
 def _high_score(self):
     high_score_file = HighScoreManager()
     file_name = high_score_file._filename
     high_score_file.load(file_name)
     score_root = Tk()
     score_root.title("High Score Board")
     high_score_board = Text(score_root, height=25, width=25)
     high_score_board.pack()
     high_score = high_score_file.get_entries()
Exemplo n.º 2
0
Arquivo: a3.py Projeto: mj221/CSSE1001
    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """
        self._won = won
        self.stop()
        self.click_next_wave.config(state='disabled')
        self.click_play.config(state='disabled')
        # Task 1.4 (Dialogs): show game over dialog here
        if self._won:
            messagebox.showinfo("Game Over!", "You Won")
        else:
            messagebox.showinfo("Game Over!", "You Lost")

        self._highscore = highscore = HighScoreManager()    # Call highscore manager
        if highscore.does_score_qualify(self._score):
            self._name = askstring('High Score Achieved!', 'Please tell us your name!')
            while self._name == None or self._name == '':
                # Asking for a valid name(if none or empty string)
                self._name = askstring('High Score Achieved!', 'Please Enter a valid name!')
            highscore.add_entry(self._name, self._score)
            highscore.save(filename = 'high_scores.json')
        else:
            return None
Exemplo n.º 3
0
    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """
        self._won = won
        self.stop()
        self._button_pause.config(state=DISABLED)
        if self._won == True:
            message_dialog = "Congratulation! You won!"
        else:
            message_dialog = "Oops, you lost, try again"
        high_score_file = HighScoreManager()
        file_name = high_score_file._filename
        high_score_file.load(file_name)
        # high_root = Tk()
        # if(high_score_file.does_score_qualify(self._score)==True):

        msg.showinfo("Tower", message_dialog)
Exemplo n.º 4
0
Arquivo: a3.py Projeto: mj221/CSSE1001
    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """
        self._master = master
        super().__init__(master, delay=delay)

        self._highscore = highscore = HighScoreManager()
        highscore.load(filename='high_scores.json')
        
        self._game = game = TowerGame()
        master.title("Tower of Defence by Minjae Lee")
        self.setup_menu(master)

        # create a game view and draw grid borders
        self._view = view = GameView(master, size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        # Task 1.3 (Status Bar): instantiate status bar

        self._status_bar = StatusBar(master)
        self._status_bar.pack(fill = tk.X)

        # Task 1.5 (Play Controls): instantiate widgets here
        
        self.play_control = tk.Frame(master, bg = 'white')
        self.play_control.pack(side = tk.BOTTOM, fill = tk.Y)

        self.click_next_wave = tk.Button(self.play_control, text = "Next Wave", command = self.next_wave)
        self.click_next_wave.pack(side = tk.LEFT)
        
        self.click_play = tk.Button(self.play_control, text = "Play", command =self._toggle_paused)
        self.click_play.pack(side = tk.LEFT)
        
        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        self._view.bind('<Button-1>', self._left_click)
        self._view.bind('<Motion>', self._move)
        self._view.bind('<Leave>', self._mouse_leave)

        self._view.bind('<Button-3>', self._right_click)

        # Level
        self._level = MyLevel()

        self.select_tower(SimpleTower)
        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        towers = [ ]
        for positions, tower in towers:
            for position in positions:
                self._game.place(position, tower_type=tower)

##        # Task 2.3 (ShopTowerView): instantiate gui here
        self.towers = [
            SimpleTower,
            MissileTower,
            EnergyTower,
            PulseTower,
            TurretTower,
            CoinTower
        ]

        shop = tk.Frame(master)
        shop.pack(fill=tk.X)
        
        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in self.towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            self._shop_view = view= ShopTowerView(shop, tower, bg=BACKGROUND_COLOUR, highlight="#4b3b4a",
                                 click_command=lambda class_=tower_class: self.select_tower(class_))
            view.pack(fill=tk.X)
            self._tower_views.append((tower, view))  # Can use to check if tower is affordable when refreshing view
Exemplo n.º 5
0
    def __init__(self, master: tk.Tk, delay: int = 30):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        master.title('Towers')
        super().__init__(master, delay=delay)

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, fill=tk.BOTH, expand=False)

        #        # Task 1.3 (Status Bar): instantiate status bar

        self._level = MyLevel()
        self._status_bar = StatusBar(master, self._level.get_max_wave())
        self._status_bar.pack(fill=tk.BOTH, side=tk.TOP, expand=False)

        self._play_and_upgrades = tk.Frame(master,
                                           bg='white',
                                           height=40,
                                           width=200)
        self._play_and_upgrades.pack(side=tk.BOTTOM, expand=True, fill=tk.BOTH)
        self._pause_button = tk.Button(self._play_and_upgrades,
                                       text='Pause',
                                       command=self._toggle_paused)
        self._pause_button.pack(side=tk.LEFT, anchor=tk.N, expand=False)
        self._pause_button.place(x=160, y=0)
        self._next_wave_button = tk.Button(self._play_and_upgrades,
                                           text='Next Wave',
                                           command=self.next_wave)
        self._next_wave_button.pack(side=tk.LEFT, anchor=tk.N, expand=False)
        self._next_wave_button.place(x=90, y=0)

        #highscores object
        self._high_scores = HighScoreManager()

        #Used for fixing bug where clicking New Game would make game faster
        try:
            if self._new_game_called:
                pass
        except AttributeError:  #raised when new_game hasn't been called
            self._new_game_count = 0
        #whether upgrades has been called before. Whether to clear upgrade buttons
        #or not.
        self._called_before = 0
        self._speed_upgrade = 0
        self._simple_upgrade = 0
        self._no_coins_ice = 0
        self._no_coins_speed = 0
        self._missile_upgrade = 0
        self._no_coins_simple = 0
        self._ice_upgrade = 0
        self._confirm_upgrades = 0

        #Shop Class instantiation
        self._towers = towers = [
            MissileTower, IceTower, PulseTower, EnergyTower
        ]

        self._shop = tk.Frame(master, bg='purple')
        self._shop.pack(fill=tk.BOTH, side=tk.BOTTOM, expand=False)
        #
        ##         Create views for each tower & store to update if availability changes
        #        self._tower_views = []
        #        col = 0
        #        for tower_class in towers:
        #            tower = tower_class(self._game.grid.cell_size)
        #            self._shop_view = ShopTowerView(self._shop, tower_class, #highlight="#4b3b4a",
        #                                 click_command=lambda class_=tower_class: self.select_tower(class_), bg=colours[col])
        #            self._shop_view.pack(fill=tk.BOTH, side=tk.TOP, expand=True)
        #            self._tower_views.append((tower, self._shop_view))
        #            Can use to check if tower is affordable when refreshing view
        #            col+=1

        self._tower_views = []
        col = 0
        for tower_class in towers:
            #            tower = tower_class(self._game.grid.cell_size // 2)
            tower = tower_class(30)
            view = ShopTowerView(
                self._shop,
                tower,
                bg='purple',  #, highlight="#4b3b4a",
                click_command=lambda class_=tower_class: self.select_tower(
                    class_))
            view.pack(fill=tk.X)
            self._tower_views.append((tower, view))
            col += 1

#         Task 1.5 (Play Controls): instantiate widgets here

# bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Motion>", self._move)
        self._view.bind("<Leave>", self._mouse_leave)
        self._view.bind("<Button-3>", self._right_click)
        # Level
        self._level = MyLevel()

        self.select_tower(MissileTower)

        self._view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()
Exemplo n.º 6
0
    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        super().__init__(master, delay=delay)

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master, size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        # Task 1.3 (Status Bar): instantiate status bar
        # ...
        self._player = tk.Frame(master, bg='pink')
        self._player.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        self._status = StatusBar(self._player)

        # Task 1.5 (Play Controls): instantiate widgets here
        # ...
        self._controlsArea = tk.Frame(self._player)
        self._controlsArea.pack(side=tk.BOTTOM)

        self._nextWave = tk.Button(self._controlsArea, text='Next wave', command=self.next_wave)
        self._nextWave.pack(side=tk.LEFT)

        self._pause = tk.Button(self._controlsArea, text='Play', command=self._toggle_paused)
        self._pause.pack(side=tk.LEFT)

        self._highscorefile = HighScoreManager()

        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        # ...
        self._view.bind("<Motion>", self._move)
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Leave>", self._mouse_leave)
        self._view.bind("<Button-3>",self._right_click)

        # Level
        self._level = MyLevel()

        self.select_tower(SimpleTower)

        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        # Remove the relevant lines while attempting the corresponding section
        # Hint: Comment them out to keep for reference

        # Task 1.2 (Tower Placement): remove these lines
        towers = [
            ([(2, 2), (3, 0), (4, 1), (4, 2), (4, 3)], MissileTower),
            ([(2, 5)], EnergyTower)
        ]

        for positions, tower in towers:
            for position in positions:
                game.place(position, tower_type=tower)

        # Task 1.5 (Tower Placement): remove these lines
        #game.queue_wave([], clear=True)
        #self.next_wave()

        # Task 1.5 (Play Controls): remove this line
        #self.start()
        #shop
        towers = [
        SimpleTower,
        MissileTower,
        EnergyTower
        ]

        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            # bg=BACKGROUND_COLOUR, highlight="#4b3b4a",
            shopView = ShopTowerView(self._player, tower, click_command=lambda class_=tower_class: self.select_tower(class_))
            shopView.pack(side=tk.TOP)

            # Can use to check if tower is affordable when refreshing view
            self._tower_views.append((tower, shopView))

        self.refresh_view()
Exemplo n.º 7
0
class TowerGameApp(Stepper):
    """Top-level GUI application for a simple tower defence game"""

    # All private attributes for ease of reading
    _current_tower = None
    _paused = False
    _won = None

    _level = None
    _wave = None
    _score = None
    _coins = None
    _lives = None

    _master = None
    _game = None
    _view = None

    # Task 3.3
    _selected_tower = None

    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        super().__init__(master, delay=delay)

        master.title('Towers')

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        self._right_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._right_frame.pack(side=tk.LEFT, fill=tk.Y)

        # Task 2.4 (High Scores): load files into a high score manager instance
        self._high_score_manager = HighScoreManager()
        self._high_score_manager.load(self._high_score_manager._filename)

        # Task 1.3 (Status Bar): instantiate status bar
        self._status = status = StatusBar(self._right_frame)
        status.pack(side=tk.TOP, fill=tk.X, anchor=tk.N, expand=False)

        # Task 2.3 (Shop): instantiation
        towers = [SimpleTower, MissileTower, EnergyTower, SlowTower]

        shop = tk.Frame(self._right_frame)
        shop.pack(side=tk.TOP, anchor=tk.N, fill=tk.X)

        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            v = ShopTowerView(shop,
                              tower,
                              bg=BACKGROUND_COLOUR,
                              highlight="#4b3b4a",
                              click_command=lambda class_=tower_class: self.
                              select_tower(class_))
            v.pack(fill=tk.X)
            self._tower_views.append(
                (tower, v)
            )  # Can use to check if tower is affordable when refreshing view

        # Task 1.5 (Play Controls): instantiate widgets here
        self._control = control = tk.Frame(self._right_frame)
        self._next_wave = tk.Button(control,
                                    text="Next Wave",
                                    command=self.next_wave,
                                    font=("TkDefaultFont", 12, "normal"))
        self._next_wave.pack(side=tk.LEFT)
        self._play = tk.Button(control,
                               text="Play",
                               command=self._toggle_paused,
                               font=("TkDefaultFont", 12, "normal"))
        self._play.pack(side=tk.LEFT)
        self._control.pack(side=tk.BOTTOM, anchor=tk.S)

        # Task 3.3 (Upgrade Tower): GUI
        self._upgrade_button = tk.Button(self._right_frame,
                                         text="Upgrade selected tower",
                                         state="disabled",
                                         command=self.upgrade_selected_tower,
                                         font=("TkDefaultFont", 12, "normal"))
        self._upgrade_button.pack(side=tk.BOTTOM, anchor=tk.S)
        self._selected_tower_level_up_cost = tk.Label(
            self._right_frame,
            text="Cost to upgrade: N/A",
            bg=BACKGROUND_COLOUR,
            fg="white",
            font=("TkDefaultFont", 12, "normal"))
        self._selected_tower_level_up_cost.pack(side=tk.BOTTOM, anchor=tk.S)
        self._selected_tower_level = tk.Label(
            self._right_frame,
            text="Selected tower level: None",
            bg=BACKGROUND_COLOUR,
            fg="white",
            font=("TkDefaultFont", 12, "normal"))
        self._selected_tower_level.pack(side=tk.BOTTOM, anchor=tk.S)

        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        self._view.bind("<Motion>", self._move)
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Leave>", self._mouse_leave)

        # Task 2.1: bind mouse event
        self._view.bind("<Button-2>", self._sell_tower)

        # Level
        self._level = MyLevel()

        self.select_tower(SimpleTower)

        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        self._check_availability()

    def setup_menu(self):
        """Sets up the application menu"""
        # Task 1.4: construct file menu here
        self._menubar = tk.Menu(self._master)
        self._master.config(menu=self._menubar)

        self._filemenu = tk.Menu(self._menubar)
        self._menubar.add_cascade(label="File", menu=self._filemenu)
        self._filemenu.add_command(label="New Game", command=self._new_game)
        self._filemenu.add_command(label="High Scores",
                                   command=self._show_high_scores)
        self._filemenu.add_command(label="Exit", command=self._exit)

    def _toggle_paused(self, paused=None):
        """Toggles or sets the paused state

        Parameters:
            paused (bool): Toggles/pauses/unpauses if None/True/False, respectively
        """
        if paused is None:
            paused = not self._paused

        # Task 1.5 (Play Controls): Reconfigure the pause button here
        if paused:
            self.pause()
            self._play.configure(text="Play")
        else:
            self.start()
            self._play.configure(text="Pause")

        self._paused = paused

    def _setup_game(self):
        """Sets up the game"""
        self._wave = 0
        self._score = 0
        self._coins = 5000
        self._lives = 20

        self._won = False

        # Task 1.3 (Status Bar): Update status here
        self.refresh_status()

        # Task 1.5 (Play Controls): Re-enable the play controls here (if they were ever disabled)
        self._next_wave.configure(state="normal")
        self._play.configure(state="normal")

        self._game.reset()

        # Auto-start the first wave
        self.next_wave()
        self._toggle_paused(paused=True)

    def _new_game(self):
        """Menu handler - restarts the game"""
        self._setup_game()
        self.refresh_view()

    def _exit(self):
        """Menu handler - exits the application"""
        confirm = messagebox.askyesno(
            "", "Are you sure you want to exit the game?")
        if confirm == True:
            self._master.destroy()

    def refresh_view(self):
        """Refreshes the game view"""
        if self._step_number % 2 == 0:
            self._view.draw_enemies(self._game.enemies)
        self._view.draw_towers(self._game.towers)
        self._view.draw_obstacles(self._game.obstacles)

        self._check_availability()

    def _step(self):
        """
        Perform a step every interval

        Triggers a game step and updates the view

        Returns:
            (bool) True if the game is still running
        """
        self._game.step()
        self.refresh_view()

        return not self._won

    # Task 1.2 (Tower Placement): Complete event handlers here (including docstrings!)
    def _move(self, event):
        """
        Handles the mouse moving over the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        if self._current_tower.get_value() > self._coins:
            return

        # move the shadow tower to mouse position
        position = event.x, event.y
        self._current_tower.position = position

        legal, grid_path = self._game.attempt_placement(position)

        # find the best path and covert positions to pixel positions
        path = [
            self._game.grid.cell_to_pixel_centre(position)
            for position in grid_path.get_shortest()
        ]

        # draw the path
        self._view.draw_path(path)

        # check if current position is a legal position to place a tower at
        legal = True
        cell_position = self._game.grid.pixel_to_cell(position)
        if not self._game.grid.is_cell_valid(cell_position):
            legal = False
        if cell_position in self._game.towers:
            legal = False
        try:
            self._game.generate_path(cell_position)
        except KeyError:
            legal = False

        # draw the shadow tower or cross if legal == False
        self._view.draw_preview(self._current_tower, legal)

    def _mouse_leave(self, event):
        """
        Handles the mouse leaving the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        # Task 1.2 (Tower placement)
        self._view.delete('shadow', 'range', 'path')

    def _left_click(self, event):
        """
        Handles the mouse left clicking on the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        self.reset_upgrade_gui()

        if self._current_tower is None:
            return

        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)

        try:
            # Task 3.3 (Upgrade Tower): update GUI when left clicked on a tower
            self._selected_tower = self._game.towers[cell_position]
            selected_tower_level = self._selected_tower.level
            selected_tower_upgrade_cost = self._selected_tower.level_cost
            self._selected_tower_level.configure(
                text="Selected tower level:  " + str(selected_tower_level))
            self._selected_tower_level_up_cost.configure(
                text="Cost to upgrade: " + str(selected_tower_upgrade_cost))

            if selected_tower_upgrade_cost > self._coins:
                self._upgrade_button.configure(text="Not enough coins!")
            else:
                self._upgrade_button.configure(state="normal")

        except KeyError:
            if self._current_tower.get_value() > self._coins:
                return
            elif self._game.place(cell_position,
                                  tower_type=self._current_tower.__class__):
                self._coins -= self._current_tower.get_value()
                self.refresh_status()
                self.refresh_view()

    def _sell_tower(self, event):
        """
        Handles the mouse right clicking on the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        self.reset_upgrade_gui()

        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)

        try:
            # refund 80% of the tower's cost to player (including upgrade costs)
            # math.floor is used to keep self._coins an integer
            tower_at_position = self._game.towers[cell_position]
            self._coins += math.floor(0.8 * tower_at_position.get_value())
            self.refresh_status()

            self._game.remove(cell_position)
            self.refresh_view()
        except KeyError:
            # do nothing if a tower does not exist at the point of right click
            return

    def next_wave(self):
        """Sends the next wave of enemies against the player"""
        if self._wave == self._level.get_max_wave():
            return

        self._wave += 1

        # Task 1.3 (Status Bar): Update the current wave display here
        self.refresh_status()

        # Task 1.5 (Play Controls): Disable the add wave button here (if this is the last wave)
        if self._wave == self._level.waves:
            self._next_wave.configure(state="disabled")

        # Generate wave and enqueue
        wave = self._level.get_wave(self._wave)
        for step, enemy in wave:
            enemy.set_cell_size(self._game.grid.cell_size)

        self._game.queue_wave(wave)

    def select_tower(self, tower):
        """
        Set 'tower' as the current tower

        Parameters:
            tower (AbstractTower): The new tower type
        """
        self._current_tower = tower(self._game.grid.cell_size)

    def _handle_death(self, enemies):
        """
        Handles enemies dying

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which died in a step
        """
        bonus = len(enemies)**.5
        for enemy in enemies:
            self._coins += enemy.points
            self._score += int(enemy.points * bonus)

        # Task 1.3 (Status Bar): Update coins & score displays here
        self.refresh_status()

    def _handle_escape(self, enemies):
        """
        Handles enemies escaping (not being killed before moving through the grid

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which escaped in a step
        """
        self._lives -= len(enemies)
        if self._lives < 0:
            self._lives = 0

        # Task 1.3 (Status Bar): Update lives display here
        self.refresh_status()

        # Handle game over
        if self._lives == 0:
            self._handle_game_over(won=False)

    def _handle_wave_clear(self):
        """Handles an entire wave being cleared (all enemies killed)"""
        if self._wave == self._level.get_max_wave():
            self._handle_game_over(won=True)

    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """
        self._won = won
        self.stop()

        # Task 1.4 (Dialogs): show game over dialog here
        result_message = ""
        if won:
            result_message = "You won the game."
        else:
            result_message = "You lost the game."

        messagebox.showinfo("Game over!", result_message)

        self._next_wave.configure(state="disabled")
        self._play.configure(state="disabled")

        # Task 2.4 (High Scores): asks user for name if score is high enough
        if self._high_score_manager.does_score_qualify(self._score):
            username = tk.simpledialog.askstring(
                "",
                "You are in the top 10! Enter your name below to go on the leaderboard!",
                parent=self._master)
            if username is not None:
                self._high_score_manager.add_entry(username, self._score)
                self._high_score_manager.save()

    def refresh_status(self):
        """Update the status bar (and others related)"""
        self._status.set_wave(self._wave, self._level.waves)
        self._status.set_score(self._score)
        self._status.set_coin(self._coins)
        self._status.set_lives(self._lives)

    def _check_availability(self):
        """Checks and sets availability of towers in the shop"""
        for towers in self._tower_views:
            tower_value = towers[0].get_value()
            if self._coins < tower_value:
                towers[1].set_available(False)
            else:
                towers[1].set_available(True)

    def _show_high_scores(self):
        """Handles displaying the highest scores and players."""
        high_score_root = tk.Tk()
        high_score_root.title("High Scores")
        high_score_root.resizable(False, False)

        high_score_data = self._high_score_manager.get_entries()

        # list of names and scores
        names_scores = []
        for element in high_score_data:
            name_score = element['name'], element['score']
            names_scores.append(name_score)

        title = tk.Label(high_score_root,
                         text="Top 10 Players",
                         font=("TkDefaultFont", 15, "bold"))
        title.pack(anchor=tk.N)

        # create GUI entries for each name/score
        for e in names_scores:
            name_score_frame = tk.Frame(high_score_root, width=250, height=26)
            name_score_frame.pack()
            name_score_frame.pack_propagate(False)
            name = tk.Label(name_score_frame, text=e[0])
            name.pack(side=tk.LEFT, padx=10, pady=5)
            score = tk.Label(name_score_frame, text=e[1])
            score.pack(side=tk.RIGHT, padx=10, pady=5)

        high_score_root.mainloop()

    def reset_upgrade_gui(self):
        """Resets the upgrade tower GUI in task 3.3 to initial state"""
        self._selected_tower_level_up_cost.configure(
            text="Cost to upgrade: N/A")
        self._selected_tower_level.configure(text="Selected tower level: None")
        self._selected_tower = None
        self._upgrade_button.configure(state="disabled",
                                       text="Upgrade selected tower")

    def upgrade_selected_tower(self):
        """Upgrade the currently selected tower"""
        if self._selected_tower is None:
            return
        else:
            self._coins -= self._selected_tower.level_cost
            self._selected_tower.level += 1
            self.reset_upgrade_gui()
            self.refresh_status()
Exemplo n.º 8
0
class TowerGameApp(Stepper):
    """Top-level GUI application for a simple tower defence game"""

    # pylint: disable=too-many-instance-attributes
    # I couldn't figure out how to fix certain bugs without defining a bunch of
    # variables at the start.

    # All private attributes for ease of reading
    _current_tower = None
    _paused = False
    _won = None

    _level = None
    _wave = None
    _score = None
    _coins = None
    _lives = None

    _master = None
    _game = None
    _view = None

    def __init__(self, master: tk.Tk, delay: int = 30):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        master.title('Towers')
        super().__init__(master, delay=delay)

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, fill=tk.BOTH, expand=False)

        #        # Task 1.3 (Status Bar): instantiate status bar

        self._level = MyLevel()
        self._status_bar = StatusBar(master, self._level.get_max_wave())
        self._status_bar.pack(fill=tk.BOTH, side=tk.TOP, expand=False)

        self._play_and_upgrades = tk.Frame(master,
                                           bg='white',
                                           height=40,
                                           width=200)
        self._play_and_upgrades.pack(side=tk.BOTTOM, expand=True, fill=tk.BOTH)
        self._pause_button = tk.Button(self._play_and_upgrades,
                                       text='Pause',
                                       command=self._toggle_paused)
        self._pause_button.pack(side=tk.LEFT, anchor=tk.N, expand=False)
        self._pause_button.place(x=160, y=0)
        self._next_wave_button = tk.Button(self._play_and_upgrades,
                                           text='Next Wave',
                                           command=self.next_wave)
        self._next_wave_button.pack(side=tk.LEFT, anchor=tk.N, expand=False)
        self._next_wave_button.place(x=90, y=0)

        #highscores object
        self._high_scores = HighScoreManager()

        #Used for fixing bug where clicking New Game would make game faster
        try:
            if self._new_game_called:
                pass
        except AttributeError:  #raised when new_game hasn't been called
            self._new_game_count = 0
        #whether upgrades has been called before. Whether to clear upgrade buttons
        #or not.
        self._called_before = 0
        self._speed_upgrade = 0
        self._simple_upgrade = 0
        self._no_coins_ice = 0
        self._no_coins_speed = 0
        self._missile_upgrade = 0
        self._no_coins_simple = 0
        self._ice_upgrade = 0
        self._confirm_upgrades = 0

        #Shop Class instantiation
        self._towers = towers = [
            MissileTower, IceTower, PulseTower, EnergyTower
        ]

        self._shop = tk.Frame(master, bg='purple')
        self._shop.pack(fill=tk.BOTH, side=tk.BOTTOM, expand=False)
        #
        ##         Create views for each tower & store to update if availability changes
        #        self._tower_views = []
        #        col = 0
        #        for tower_class in towers:
        #            tower = tower_class(self._game.grid.cell_size)
        #            self._shop_view = ShopTowerView(self._shop, tower_class, #highlight="#4b3b4a",
        #                                 click_command=lambda class_=tower_class: self.select_tower(class_), bg=colours[col])
        #            self._shop_view.pack(fill=tk.BOTH, side=tk.TOP, expand=True)
        #            self._tower_views.append((tower, self._shop_view))
        #            Can use to check if tower is affordable when refreshing view
        #            col+=1

        self._tower_views = []
        col = 0
        for tower_class in towers:
            #            tower = tower_class(self._game.grid.cell_size // 2)
            tower = tower_class(30)
            view = ShopTowerView(
                self._shop,
                tower,
                bg='purple',  #, highlight="#4b3b4a",
                click_command=lambda class_=tower_class: self.select_tower(
                    class_))
            view.pack(fill=tk.X)
            self._tower_views.append((tower, view))
            col += 1

#         Task 1.5 (Play Controls): instantiate widgets here

# bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Motion>", self._move)
        self._view.bind("<Leave>", self._mouse_leave)
        self._view.bind("<Button-3>", self._right_click)
        # Level
        self._level = MyLevel()

        self.select_tower(MissileTower)

        self._view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        # Remove the relevant lines while attempting the corresponding section
        # Hint: Comment them out to keep for reference

        # Task 1.2 (Tower Placement): remove these lines
#        towers = [
#            ([(2, 2), (3, 0), (4, 1), (4, 2), (4, 3)], SimpleTower),
#            ([(2, 5)], PulseTower)
#        ]
#
#        for positions, tower in towers:
#            for position in positions:
#                self._game.place(position, tower_type=tower)
#
##         Task 1.5 (Tower Placement): remove these lines
#        self._game.queue_wave([], clear=True)
#        self._wave = 4 - 1  # first (next) wave will be wave 4
#        self.next_wave()
##
##        # Task 1.5 (Play Controls): remove this line
#        self.start()

    def setup_menu(self):
        """Sets up the menu bar with New Game, Exit and High Scores commands"""
        self._menubar = tk.Menu(self._master)
        self._filemenu = tk.Menu(self._menubar, tearoff=0)
        self._filemenu.add_command(label='New Game', command=self._new_game)
        self._filemenu.add_command(label='Exit', command=self._exit)
        self._filemenu.add_command(label="High Scores",
                                   command=self._display_high_scores)
        self._menubar.add_cascade(label='File', menu=self._filemenu)
        self._master.config(menu=self._menubar)

    def _toggle_paused(self, paused=None):
        """Toggles or sets the paused state

        Parameters:
            paused (bool): Toggles/pauses/unpauses if None/True/False, respectively
        """
        if paused is None:
            paused = not self._paused
        if paused:
            self.pause()
        else:
            self.start()
        if paused:
            self._pause_button.configure(text='Play')
        else:
            self._pause_button.configure(text='Pause')

        self._paused = paused

    def _setup_game(self):
        """
        Sets up status_bar variables, pause/next wave buttons.
        Starts a game from wave 1.
        """
        self._wave = 0
        self._score = 0
        self._coins = 80
        self._lives = 20
        self._won = False

        #status_bar display setup
        self._status_bar.set_wave_display(self._wave,
                                          self._level.get_max_wave())
        self._status_bar.set_coin_display(self._coins)
        self._status_bar.set_lives_display(self._lives)
        self._status_bar.set_score_display(self._score)

        #activate the pause and next wave buttons
        self._next_wave_button.configure(state='active')
        self._pause_button.configure(state='active', text='Pause')

        self._game.reset()

        # Auto-start the first wave
        self.next_wave()
        self._toggle_paused(paused=False)

    # Task 1.4 (File Menu): Complete menu item handlers here (including docstrings!)
    #
    def _new_game(self):
        """
        Called by clicking "New Game" in menubar.
        Starts a new game.
        """
        #        self._view.forget()
        #        self._status_bar.forget()
        #        self._shop.forget()
        #        self._play_and_upgrades.forget()
        #        self._new_game_count += 1
        #        self._new_game_called = 1
        #        self.__init__(self._master, delay=(30 + self._new_game_count*30))
        self._setup_game()

    #
    def _exit(self):
        """Yes/No question confirming whether to close or not."""
        if messagebox.askyesno("Confirm Exit",
                               "Are you sure you want to exit?"):
            self._master.destroy()

    def refresh_view(self):
        """Refreshes the game view"""
        if self._step_number % 2 == 0:
            self._view.draw_enemies(self._game.enemies)
        self._view.draw_towers(self._game.towers)
        self._view.draw_obstacles(self._game.obstacles)

    def _step(self):
        """
        Perform a step every interval

        Triggers a game step and updates the view

        Returns:
            (bool) True if the game is still running
        """
        self._game.step()
        self.refresh_view()

        return not self._won

    def _move(self, event):
        """
        Handles the mouse moving over the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        if self._current_tower.get_value() > self._coins:
            self._view.delete('shadow', 'range', 'path')
            return

        # move the shadow tower to mouse position
        position = event.x, event.y
        self._current_tower.position = position

        legal, grid_path = self._game.attempt_placement(position)

        # find the best path and covert positions to pixel positions
        path = [
            self._game.grid.cell_to_pixel_centre(position)
            for position in grid_path.get_shortest()
        ]
        self._view.draw_preview(self._current_tower, legal)
        self._view.draw_path(path)

    def _mouse_leave(self, event):
        """Deletes the preview when cursor leaves game canvas."""
        self._view.delete('shadow', 'range', 'path')

    def _left_click(self, event):
        """
        Attempts to place self._current_tower on the clicked square.
        
        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        # retrieve position to place tower
        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)
        if self.is_paused():
            return

        if cell_position in self._game.towers.keys():
            self._get_upgrades(cell_position)
            return

        if self._current_tower is None:
            return

        if self._current_tower.get_value() > self._coins:
            return

        if self._game.place(cell_position,
                            tower_type=self._current_tower.__class__):
            # Task 1.2 (Tower placement): Attempt to place the tower being previewed
            self._coins -= int(self._current_tower.base_cost)
            self._status_bar.set_coin_display(self._coins)
            for tower_view in self._tower_views:
                if tower_view[0].base_cost > self._coins:
                    # pylint: disable=protected-access
                    # This only has effects intended
                    tower_view[1].update_price_colour('red')

    def _get_upgrades(self, cell_position):
        """
        Adds checkboxes to upgrade area according to the tower clicked, if 
        self._coins allows. Otherwise, shows message stating insufficient funds
        
        Parameters:
            cell_position (tuple): which cell was clicked
        """
        for label in [
                self._speed_upgrade, self._no_coins_ice, self._no_coins_speed,
                self._no_coins_simple, self._missile_upgrade,
                self._ice_upgrade, self._confirm_upgrades
        ]:
            try:
                label.destroy()
            except AttributeError:  #attribute error raised if that upgrade option
                continue  #hasn't been made previously

        self._called_before = 1

        #checks type of tower clicked, current coins and displays options accordnly
        self._tower_to_upgrade = tower_clicked = self._game.towers[
            cell_position]
        self._speed_clicked = tk.IntVar()
        self._ice_upgrade_clicked = tk.IntVar()
        self._range_upgrade_clicked = tk.IntVar()
        self._simple_upgrade_clicked = tk.IntVar()
        self._confirm_upgrades = tk.Button(self._play_and_upgrades,
                                           text='Confirm',
                                           command=self._upgrade_tower)
        self._confirm_upgrades.pack(side=tk.RIGHT)
        self._confirm_upgrades.place(x=238, y=55)

        if self._coins >= 100:
            self._speed_upgrade = tk.Checkbutton(
                self._play_and_upgrades,
                text='Speed Upgrade               100 coins',
                variable=self._speed_clicked,
                bg='white',
                offvalue=0,
                onvalue=1)
            self._speed_upgrade.pack(side=tk.LEFT, expand=False)
        else:
            self._no_coins_speed = tk.Label(
                self._play_and_upgrades,
                text='Not enough coins for speed upgrade')
            self._no_coins_speed.pack(side=tk.LEFT, expand=False)
        if tower_clicked.name == 'Ice Tower' and self._coins >= 250:
            self._ice_upgrade = tk.Checkbutton(
                self._play_and_upgrades,
                text='Ice Tower Upgrade        250 coins',
                variable=self._ice_upgrade_clicked,
                bg='white')
            self._ice_upgrade.pack(side=tk.LEFT, expand=False)
            self._ice_upgrade.place(x=0, y=70)
        elif tower_clicked.name == 'Ice Tower' and self._coins < 250:
            self._no_coins_ice = tk.Label(
                self._play_and_upgrades,
                text='Not enough coins for Ice Tower upgrade')
            self._no_coins_ice.pack(side=tk.LEFT, expand=False)
            self._no_coins_ice.place(x=0, y=70)
        if tower_clicked.name == 'Simple Tower' and self._coins >= 40:
            self._simple_upgrade = tk.Checkbutton(
                self._play_and_upgrades,
                text='Level Upgrade                40 coins',
                variable=self._simple_upgrade_clicked,
                bg='white')
            self._simple_upgrade.pack(side=tk.LEFT, expand=False)
            self._simple_upgrade.place(x=0, y=70)
        elif tower_clicked.name == 'Simple Tower' and self._coins < 40:
            self._no_coins_simple = tk.Label(
                self._play_and_upgrades,
                text='Not enough coins for Simple Tower upgrade')
            self._no_coins_simple.pack(side=tk.LEFT, expand=False)
            self._no_coins_simple.place(x=0, y=70)

    def _upgrade_tower(self):
        """
        Checks which checkboxes were ticked at time of confirming.
        Upgrades accordinly.
        """
        if self._speed_clicked.get() == 1:
            if self._tower_to_upgrade.cool_down_steps > 0:
                self._tower_to_upgrade.cool_down_steps -= 1
                self._tower_to_upgrade.cool_down = Countdown(
                    self._tower_to_upgrade.cool_down_steps)  #speeds up by 10%
                self._speed_clicked = 0
                self._coins -= 100
        if self._range_upgrade_clicked.get() == 1:
            if self._tower_to_upgrade.range.outer_radius != 5:
                self._tower_to_upgrade.range.outer_radius = 5
                self._coins -= 25
                self._range_upgrade_clicked = 0
        if self._ice_upgrade_clicked.get() == 1:
            self._tower_to_upgrade.upgraded = True
            self._tower_to_upgrade.level += 1
            self._tower_to_upgrade.colour = 'blue4'
            self._coins -= 250
        if self._simple_upgrade_clicked.get() == 1:
            self._tower_to_upgrade.level += 1
            self._coins -= 40
            self._simple_upgrade.destroy()

        for label in [
                self._speed_upgrade, self._no_coins_ice, self._no_coins_speed,
                self._no_coins_simple, self._simple_upgrade,
                self._missile_upgrade, self._ice_upgrade,
                self._confirm_upgrades
        ]:
            try:
                label.destroy()
                self._simple_upgrade.destroy()
            except AttributeError:
                pass

    def _right_click(self, event):
        """
        Attempts to sell tower in clicked grid
        Parameters:
            event (tk.Event): Tkinter mouse event
        """
        position = event.x, event.y
        position = self._game.grid.pixel_to_cell(position)
        if position in self._game.towers.keys():
            # Add 80% of tower's base_cost to player's wallet
            self._coins += int(0.8 * self._game.towers[position].base_cost)
            self._status_bar.set_coin_display(self._coins)
            del self._game.towers[position]
            for tower_view in self._tower_views:
                if tower_view[0].base_cost < self._coins:
                    # pylint: disable=protected-access
                    # This only has effects intended
                    tower_view[1].update_price_colour('white')

    def next_wave(self):
        """Sends the next wave of enemies against the player"""
        #checks whether it should do anything
        if self._wave == self._level.get_max_wave():
            self._next_wave_button.configure(state='disabled')
            return

        self._wave += 1
        self._pause_button.configure(text='Pause')

        #checks to disable button but still continues with wave.
        if self._wave == self._level.get_max_wave():
            self._next_wave_button.configure(state='disabled')


#        # Task 1.3 (Status Bar): Update the current wave display here
        self._status_bar.set_wave_display(self._wave,
                                          self._level.get_max_wave())

        # Generate wave and enqueue
        wave = self._level.get_wave(self._wave)
        for step, enemy in wave:
            enemy.set_cell_size(self._game.grid.cell_size)

        self._game.queue_wave(wave)
        self.start()

    def select_tower(self, tower):
        """
        Set 'tower' as the current tower

        Parameters:
            tower (AbstractTower): The new tower type
        """
        self._current_tower = tower(self._game.grid.cell_size)

    def _handle_death(self, enemies):
        """
        Handles enemies dying

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which died in a step
        """
        bonus = len(enemies)**.5
        for enemy in enemies:
            self._coins += enemy.points
            self._score += int(enemy.points * bonus)

        # Task 1.3 (Status Bar): Update coins & score displays here
        self._status_bar.set_coin_display(self._coins)
        self._status_bar.set_score_display(self._score)
        for tower_view in self._tower_views:
            if tower_view[0].base_cost <= self._coins:
                # pylint: disable=protected-access
                # This only has effects intended
                tower_view[1].update_price_colour('white')

    def _handle_escape(self, enemies):
        """
        Handles enemies escaping (not being killed before moving through the grid

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which escaped in a step
        """
        self._lives -= len(enemies)
        if self._lives < 0:
            self._lives = 0

        # Task 1.3 (Status Bar): Update lives display here
        self._status_bar.set_lives_display(self._lives)

        # Handle game over
        if self._lives == 0:
            self._handle_game_over(won=False)

    def _handle_wave_clear(self):
        """Handles an entire wave being cleared (all enemies killed)"""
        if self._wave == self._level.get_max_wave():
            self._handle_game_over(won=True)

        # Task 1.5 (Play Controls): remove this line
        #self.next_wave()

    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """

        if self._high_scores.does_score_qualify(self._score):
            high_score_name = simpledialog.askstring("Input",
                                                     "Your score qualified for "\
                                                     "the high scores, what is"\
                                                     " your name?")
            self._high_scores.add_entry(high_score_name, self._score)

        if won:
            win_or_lose = 'finished'
        else:
            win_or_lose = 'didn\'t finish'
        messagebox.showinfo(
            'Game Over',
            'You %s with a score of %s' % (win_or_lose, self._score))
        self._high_scores.save()

        self.stop()

    def _display_high_scores(self):
        """Displays the current high scores"""
        display = ''
        entries = self._high_scores.get_entries()
        for i in range(len(self._high_scores.get_entries())):
            try:
                display += str(entries[i]['name'] + ' - Score: ' +\
                               str(entries[i]['score']) + entries[i]['data'] + '\n')
            except TypeError:
                if entries[i][
                        'name']:  #accounts for people entering nothing for their name
                    display += str(str(entries[i]['name']) + ' - Score: ' +\
                                   str(entries[i]['score']) + '\n')
                else:
                    display += str('*NO NAME ENTERED*' + ' - Score: ' +\
                                   str(entries[i]['score']) + '\n')
        messagebox.showinfo("High Scores", str(display))
Exemplo n.º 9
0
    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        super().__init__(master, delay=delay)

        master.title('Towers')

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        self._right_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._right_frame.pack(side=tk.LEFT, fill=tk.Y)

        # Task 2.4 (High Scores): load files into a high score manager instance
        self._high_score_manager = HighScoreManager()
        self._high_score_manager.load(self._high_score_manager._filename)

        # Task 1.3 (Status Bar): instantiate status bar
        self._status = status = StatusBar(self._right_frame)
        status.pack(side=tk.TOP, fill=tk.X, anchor=tk.N, expand=False)

        # Task 2.3 (Shop): instantiation
        towers = [SimpleTower, MissileTower, EnergyTower, SlowTower]

        shop = tk.Frame(self._right_frame)
        shop.pack(side=tk.TOP, anchor=tk.N, fill=tk.X)

        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            v = ShopTowerView(shop,
                              tower,
                              bg=BACKGROUND_COLOUR,
                              highlight="#4b3b4a",
                              click_command=lambda class_=tower_class: self.
                              select_tower(class_))
            v.pack(fill=tk.X)
            self._tower_views.append(
                (tower, v)
            )  # Can use to check if tower is affordable when refreshing view

        # Task 1.5 (Play Controls): instantiate widgets here
        self._control = control = tk.Frame(self._right_frame)
        self._next_wave = tk.Button(control,
                                    text="Next Wave",
                                    command=self.next_wave,
                                    font=("TkDefaultFont", 12, "normal"))
        self._next_wave.pack(side=tk.LEFT)
        self._play = tk.Button(control,
                               text="Play",
                               command=self._toggle_paused,
                               font=("TkDefaultFont", 12, "normal"))
        self._play.pack(side=tk.LEFT)
        self._control.pack(side=tk.BOTTOM, anchor=tk.S)

        # Task 3.3 (Upgrade Tower): GUI
        self._upgrade_button = tk.Button(self._right_frame,
                                         text="Upgrade selected tower",
                                         state="disabled",
                                         command=self.upgrade_selected_tower,
                                         font=("TkDefaultFont", 12, "normal"))
        self._upgrade_button.pack(side=tk.BOTTOM, anchor=tk.S)
        self._selected_tower_level_up_cost = tk.Label(
            self._right_frame,
            text="Cost to upgrade: N/A",
            bg=BACKGROUND_COLOUR,
            fg="white",
            font=("TkDefaultFont", 12, "normal"))
        self._selected_tower_level_up_cost.pack(side=tk.BOTTOM, anchor=tk.S)
        self._selected_tower_level = tk.Label(
            self._right_frame,
            text="Selected tower level: None",
            bg=BACKGROUND_COLOUR,
            fg="white",
            font=("TkDefaultFont", 12, "normal"))
        self._selected_tower_level.pack(side=tk.BOTTOM, anchor=tk.S)

        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        self._view.bind("<Motion>", self._move)
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Leave>", self._mouse_leave)

        # Task 2.1: bind mouse event
        self._view.bind("<Button-2>", self._sell_tower)

        # Level
        self._level = MyLevel()

        self.select_tower(SimpleTower)

        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        self._check_availability()
Exemplo n.º 10
0
    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """
        #Setting title of the Window
        master.title("Towers")

        self._master = master
        super().__init__(master, delay=delay)

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        # Task 4.1 Postgraduate advanced feature. Initialise pygame and sounds
        pygame.init()
        mixer.init()
        self.build_sound = pygame.mixer.Sound("sound/build.wav")
        self.increase_coin = pygame.mixer.Sound("sound/coin.wav")
        self.decrease_life = pygame.mixer.Sound("sound/heart.wav")
        self.wave_m = pygame.mixer.Sound("sound/next_wave.wav")
        self.pause_play = pygame.mixer.Sound("sound/pause_play.wav")
        self.quit_m = pygame.mixer.Sound("sound/quit.wav")

        # Task 4.1 Postgraduate advanced feature. Play music
        pygame.mixer.music.load("sound/game_music.mp3")
        pygame.mixer.music.play(-1)

        # Task 1.3 (Status Bar): instantiate status bar

        # Put the status bar in a frame
        self._right_frame = tk.Frame(master, bg="white")
        self._right_frame.pack(fill=tk.BOTH, expand=True)

        self._status_bar = StatusBar(self._right_frame)
        self._status_bar.pack(side=tk.TOP)

        # Task 2.4 (High Score): instantiate highscore
        self._high_score = HighScoreManager()

        # Task 1.5 (Play Controls): instantiate widgets here
        # Create a frame for widgets
        self._bottom_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._bottom_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)

        #Create button for next wave
        self._next_wave_btn = tk.Button(self._bottom_frame,
                                        text="Next Wave",
                                        state=tk.NORMAL,
                                        command=self.next_wave)
        self._next_wave_btn.pack(side=tk.LEFT,
                                 ipadx=10,
                                 padx=10,
                                 ipady=10,
                                 pady=10)
        #Create button for play/pause
        self._play_btn = tk.Button(self._bottom_frame,
                                   text="Pause",
                                   state=tk.NORMAL,
                                   command=self._toggle_paused)
        self._play_btn.pack(side=tk.LEFT, ipadx=10, padx=10, ipady=10)

        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        # On leaving canvas hide mouse
        # On left click place tower
        # On right click sell tower
        self._view.bind('<Motion>', self._move)
        self._view.bind('<Leave>', self._mouse_leave)
        self._view.bind('<Button-1>', self._left_click)
        self._view.bind('<Button-3>', self._right_click)

        # Level
        self._level = MyLevel()

        #Choose tower on initialisation
        self.select_tower(SimpleTower)

        #Draw borders
        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        # Task 2.3 (Shop) Creating Shop

        #Store towers present in the game for Shop
        towers = [
            SimpleTower, EnergyTower, PulseTower, MissileTower, SlowTower
        ]

        #Create frmae for Shop window
        self._right_middle_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._right_middle_frame.pack(fill=tk.BOTH, expand=True)

        #Put shop in the frame created
        shop = tk.Frame(self._right_middle_frame, bg=BACKGROUND_COLOUR)
        shop.pack(side=tk.BOTTOM, fill=tk.X)

        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)
            view = ShopTowerView(shop,
                                 tower,
                                 bg=BACKGROUND_COLOUR,
                                 click_command=lambda class_=tower_class: self.
                                 select_tower(class_))
            view.pack(fill=tk.X)
            self._tower_views.append(
                (tower, view)
            )  # Can use to check if tower is affordable when refreshing view

        # Task 1.4 On pressing delete(X) button in window prompt user before exit

        def callback():
            """Prompt user on pressing delete(X)"""
            if messagebox.askokcancel("Quit", "Do you really wish to quit?"):
                self._high_score.save()
                pygame.mixer.Sound.play(self.quit_m)
                master.destroy()

        master.protocol("WM_DELETE_WINDOW", callback)
Exemplo n.º 11
0
class TowerGameApp(Stepper):
    """Top-level GUI application for a simple tower defence game"""

    # All private attributes for ease of reading
    _current_tower = None
    _paused = False
    _won = None

    _level = None
    _wave = None
    _score = None
    _coins = None
    _lives = None

    _master = None
    _game = None
    _view = None

    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """
        #Setting title of the Window
        master.title("Towers")

        self._master = master
        super().__init__(master, delay=delay)

        self._game = game = TowerGame()

        self.setup_menu()

        # create a game view and draw grid borders
        self._view = view = GameView(master,
                                     size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='antique white')
        view.pack(side=tk.LEFT, expand=True)

        # Task 4.1 Postgraduate advanced feature. Initialise pygame and sounds
        pygame.init()
        mixer.init()
        self.build_sound = pygame.mixer.Sound("sound/build.wav")
        self.increase_coin = pygame.mixer.Sound("sound/coin.wav")
        self.decrease_life = pygame.mixer.Sound("sound/heart.wav")
        self.wave_m = pygame.mixer.Sound("sound/next_wave.wav")
        self.pause_play = pygame.mixer.Sound("sound/pause_play.wav")
        self.quit_m = pygame.mixer.Sound("sound/quit.wav")

        # Task 4.1 Postgraduate advanced feature. Play music
        pygame.mixer.music.load("sound/game_music.mp3")
        pygame.mixer.music.play(-1)

        # Task 1.3 (Status Bar): instantiate status bar

        # Put the status bar in a frame
        self._right_frame = tk.Frame(master, bg="white")
        self._right_frame.pack(fill=tk.BOTH, expand=True)

        self._status_bar = StatusBar(self._right_frame)
        self._status_bar.pack(side=tk.TOP)

        # Task 2.4 (High Score): instantiate highscore
        self._high_score = HighScoreManager()

        # Task 1.5 (Play Controls): instantiate widgets here
        # Create a frame for widgets
        self._bottom_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._bottom_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)

        #Create button for next wave
        self._next_wave_btn = tk.Button(self._bottom_frame,
                                        text="Next Wave",
                                        state=tk.NORMAL,
                                        command=self.next_wave)
        self._next_wave_btn.pack(side=tk.LEFT,
                                 ipadx=10,
                                 padx=10,
                                 ipady=10,
                                 pady=10)
        #Create button for play/pause
        self._play_btn = tk.Button(self._bottom_frame,
                                   text="Pause",
                                   state=tk.NORMAL,
                                   command=self._toggle_paused)
        self._play_btn.pack(side=tk.LEFT, ipadx=10, padx=10, ipady=10)

        # bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        # Task 1.2 (Tower Placement): bind mouse events to canvas here
        # On leaving canvas hide mouse
        # On left click place tower
        # On right click sell tower
        self._view.bind('<Motion>', self._move)
        self._view.bind('<Leave>', self._mouse_leave)
        self._view.bind('<Button-1>', self._left_click)
        self._view.bind('<Button-3>', self._right_click)

        # Level
        self._level = MyLevel()

        #Choose tower on initialisation
        self.select_tower(SimpleTower)

        #Draw borders
        view.draw_borders(game.grid.get_border_coordinates())

        # Get ready for the game
        self._setup_game()

        # Task 2.3 (Shop) Creating Shop

        #Store towers present in the game for Shop
        towers = [
            SimpleTower, EnergyTower, PulseTower, MissileTower, SlowTower
        ]

        #Create frmae for Shop window
        self._right_middle_frame = tk.Frame(master, bg=BACKGROUND_COLOUR)
        self._right_middle_frame.pack(fill=tk.BOTH, expand=True)

        #Put shop in the frame created
        shop = tk.Frame(self._right_middle_frame, bg=BACKGROUND_COLOUR)
        shop.pack(side=tk.BOTTOM, fill=tk.X)

        # Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)
            view = ShopTowerView(shop,
                                 tower,
                                 bg=BACKGROUND_COLOUR,
                                 click_command=lambda class_=tower_class: self.
                                 select_tower(class_))
            view.pack(fill=tk.X)
            self._tower_views.append(
                (tower, view)
            )  # Can use to check if tower is affordable when refreshing view

        # Task 1.4 On pressing delete(X) button in window prompt user before exit

        def callback():
            """Prompt user on pressing delete(X)"""
            if messagebox.askokcancel("Quit", "Do you really wish to quit?"):
                self._high_score.save()
                pygame.mixer.Sound.play(self.quit_m)
                master.destroy()

        master.protocol("WM_DELETE_WINDOW", callback)

    def setup_menu(self):
        """Sets up the application menu"""
        # Task 1.4: construct file menu here
        # ...
        menubar = tk.Menu(self._master)
        self._master.config(menu=menubar)

        filemenu = tk.Menu(menubar)
        menubar.add_cascade(label="File", menu=filemenu)
        filemenu.add_command(label="New Game", command=self._new_game)
        filemenu.add_command(label="High Scores", command=self._high_scores)
        filemenu.add_command(label="Exit", command=self._exit)

    def _toggle_paused(self, paused=None):
        """Toggles or sets the paused state

        Parameters:
            paused (bool): Toggles/pauses/unpauses if None/True/False, respectively
        """
        if paused is None:
            paused = not self._paused

        # Task 1.5 (Play Controls): Reconfigure the pause button here

        # On pause stop music and change label text
        if paused:
            self.pause()
            pygame.mixer.Sound.play(self.pause_play)
            self._play_btn.config(text="Play")
            pygame.mixer.music.pause()
        else:
            # On start/unpause start music and change label text
            self.start()
            pygame.mixer.Sound.play(self.pause_play)
            self._play_btn.config(text="Pause")
            pygame.mixer.music.unpause()

        self._paused = paused

    def _setup_game(self):
        """Sets up the game"""
        self._wave = 0
        self._score = 0
        self._coins = 100
        self._lives = 20

        self._won = False

        # Task 1.3 (Status Bar): Update status here
        #Set values displayed in status bar
        self._status_bar.set_coins(self._coins)
        self._status_bar.set_wave(self._wave)
        self._status_bar.set_lives(self._lives)
        self._status_bar.set_score(self._score)

        # Task 1.5 (Play Controls): Re-enable the play controls here (if they were ever disabled)
        self._next_wave_btn.config(state=tk.NORMAL)
        self._play_btn.config(state=tk.NORMAL)
        self._game.reset()

        # Auto-start the first wave
        self.next_wave()
        self._toggle_paused(paused=True)

    # Task 1.4 (File Menu): Complete menu item handlers here (including docstrings!)
    #
    def _new_game(self):
        """Create a new message box file to check if player is sure and starts a new game"""
        reply = messagebox.askquestion(
            type=messagebox.YESNO,
            title="Exit Towers",
            message="Are you sure you want to start a new game?")
        if reply == messagebox.YES:
            self._game.reset()
            self._setup_game()
            self.refresh_view()
            self._view.draw_enemies(self._game.enemies)

    def _high_scores(self):
        """Enables user interaction when clicking on HighScores in File Menu"""

        #Creates a frame for highscore
        high_score_window = tk.Toplevel()
        high_score_window.title("High Scores")

        #Gets values stored in the file
        data_input = self._high_score.get_entries()
        Output = "Rank | Name | Score | Comment\n-----------------------------------\n"
        i = 1

        #Load values and shows on screen
        for dictionary in data_input:
            dictionary_values = str(dictionary.values())
            name, score, comment = dictionary_values.split(sep=",")
            Output = Output + str(i) + "       |" + name[
                14:-1] + "     |" + score + "   |" + comment[:-2] + "\n"
            i += 1

        #Button to returen to game
        msg = tk.Message(high_score_window, text=Output)
        msg.pack()

        button = tk.Button(high_score_window,
                           text="Return to Game",
                           command=high_score_window.destroy)
        button.pack()

    def _exit(self):
        """ Opens message box on player trying to exit"""
        reply = messagebox.askquestion(type=messagebox.YESNO,
                                       title="Quit Towers",
                                       message="Do you really wish to quit?")
        if reply == messagebox.YES:
            self._high_score.save()
            pygame.mixer.Sound.play(self.quit_m)
            self._master.destroy()

    def refresh_view(self):
        """Refreshes the game view"""

        #Draws towers and obstacles
        if self._step_number % 2 == 0:
            self._view.draw_enemies(self._game.enemies)
        self._view.draw_towers(self._game.towers)
        self._view.draw_obstacles(self._game.obstacles)

        #Sets available flag
        for tower_view in self._tower_views:
            if tower_view[0].get_value() > self._coins:
                tower_view[1].set_available(False)
            else:
                tower_view[1].set_available(True)

    def _step(self):
        """
        Perform a step every interval

        Triggers a game step and updates the view

        Returns:
            (bool) True if the game is still running
        """
        self._game.step()
        self.refresh_view()
        return not self._won

    # Task 1.2 (Tower Placement): Complete event handlers here (including docstrings!)
    # Event handlers: _move, _mouse_leave, _left_click
    def _move(self, event):
        """
        Handles the mouse moving over the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        if self._current_tower.get_value() > self._coins:
            return

        # move the shadow tower to mouse position
        position = event.x, event.y
        self._current_tower.position = position

        legal, grid_path = self._game.attempt_placement(position)

        # find the best path and covert positions to pixel positions
        path = [
            self._game.grid.cell_to_pixel_centre(position)
            for position in grid_path.get_shortest()
        ]

        # Task 1.2 (Tower placement): Draw the tower preview here
        # Draw tower path and preview
        self._view.draw_preview(self._current_tower, legal)
        self._view.draw_path(path)

    def _mouse_leave(self, event):
        """
        Handles the mouse leaving the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        # Task 1.2 (Tower placement): Delete the preview
        # Hint: Relevant canvas items are tagged with: 'path', 'range', 'shadow'
        #       See tk.Canvas.delete (delete all with tag)
        #Deletes mouse preview
        self._view.delete("path")
        self._view.delete("range")
        self._view.delete("shadow")

    def _left_click(self, event):
        """
        Handles the mouse left clicking on the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        # retrieve position to place tower
        if self._current_tower is None:
            return
        #If tower value is greater than current coins doesn't do anything
        if self._current_tower.get_value() > self._coins:
            return

        #Takes current position
        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)

        if self._game.place(cell_position,
                            tower_type=self._current_tower.__class__):
            #Task 2.1 (Buy and sell Towers) : On placing a tower deduct the value of tower from players wallet
            self._coins -= self._current_tower.get_value()

            #On placing tower plays sound
            pygame.mixer.Sound.play(self.build_sound)

    def _right_click(self, event):
        """
        Handles the mouse right clicking on the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        # retrieve position to sell tower
        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)

        self._game.remove(cell_position)
        """Task 2.1 (Buy and sell Towers) : player can sell a tower by right
            clicking on it. The tower should be removed from the grid and 80%
            of its value should be put back into the player's wallet."""
        self._coins += (self._current_tower.get_value() * 0.8)

    def next_wave(self):
        """Sends the next wave of enemies against the player"""
        # If is the last wave do nothing
        if self._wave == self._level.get_max_wave():
            return

        #else increase wave value
        self._wave += 1

        #Play sound on next wave
        pygame.mixer.Sound.play(self.wave_m)

        # Task 1.3 (Status Bar): Update the current wave display here
        # Update wave vlaue in Status Bar
        self._status_bar.set_wave(self._wave)

        # Task 1.5 (Play Controls): Disable the add wave button here (if this is the last wave)
        # Disable add wave button
        if self._wave == self._level.get_max_wave():
            self._next_wave_btn.config(state=tk.DISABLED)
            self._play_btn.config(state=tk.DISABLED)

        # Generate wave and enemy
        wave = self._level.get_wave(self._wave)
        for step, enemy in wave:
            enemy.set_cell_size(self._game.grid.cell_size)

        # Queue wave
        self._game.queue_wave(wave)

    def select_tower(self, tower):
        """
        Set 'tower' as the current tower

        Parameters:
            tower (AbstractTower): The new tower type
        """
        self._current_tower = tower(self._game.grid.cell_size)

    def _handle_death(self, enemies):
        """
        Handles enemies dying

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which died in a step
        """
        bonus = len(enemies)**.5
        for enemy in enemies:
            self._coins += enemy.points
            pygame.mixer.Sound.play(self.increase_coin)
            self._score += int(enemy.points * bonus)

        # Task 1.3 (Status Bar): Update coins & score displays here
        # Update coins and score in Status bar
        self._status_bar.set_coins(self._coins)
        self._status_bar.set_score(self._score)

    def _handle_escape(self, enemies):
        """
        Handles enemies escaping (not being killed before moving through the grid

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which escaped in a step
        """
        self._lives -= len(enemies)

        #Play sound on losing life
        pygame.mixer.Sound.play(self.decrease_life)

        if self._lives < 0:
            self._lives = 0

        # Task 1.3 (Status Bar): Update lives display here
        # Update lives value in Status BAr
        self._status_bar.set_lives(self._lives)

        # Handle game over
        if self._lives == 0:
            self._handle_game_over(won=False)

    def _handle_wave_clear(self):
        """Handles an entire wave being cleared (all enemies killed)"""
        if self._wave == self._level.get_max_wave():
            self._handle_game_over(won=True)

    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """
        self._won = won
        self.stop()
        self._next_wave_btn.config(state=tk.DISABLED)
        self._play_btn.config(state=tk.DISABLED)

        pygame.mixer.music.pause()

        # Task 1.4 (Dialogs): Show game over dialog here
        # Task 2.4 (High Scores) On users running out of lives, check and store score

        #Display message on winning or losing
        if self._won:
            messagebox.showinfo("Game Over", "Congratulations!!! You Won")
        else:
            messagebox.showinfo("Game Over", "Better luck next time. You Lost")

        #Store high score
        if self._high_score.does_score_qualify(self._score):

            #Create window to take input
            high_score_entry_window = tk.Toplevel()
            high_score_entry_window.title("High Score Entry")

            label = tk.Label(high_score_entry_window, text='Enter Name: ')
            label.pack(side=tk.LEFT)

            entry = tk.Entry(high_score_entry_window, width=20)
            entry.pack(side=tk.LEFT)

            #Function to add score & close window
            def add():
                name = entry.get()
                self._high_score.add_entry(name, self._score)
                pygame.mixer.Sound.play(self.quit_m)
                high_score_entry_window.destroy()

            calc = tk.Button(high_score_entry_window,
                             text="Enter",
                             command=add)
            calc.pack(side=tk.LEFT)
Exemplo n.º 12
0
    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        super().__init__(master, delay=delay)
        master.title("tkDefend")
        
        self._game = game = TowerGame()

        self._highscores = {}

        self.setup_menu()
        self._master.configure(menu=self._menu)

        #create a game view and draw grid borders
        self._view = view = GameView(master, size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='#000033') #was antique white
        view.pack(side=tk.LEFT, expand=True)

        #have a menu frame on the right
        self._right_frame = tk.Frame(self._master)
        self._right_frame.pack(side=tk.RIGHT, expand=True, fill=tk.Y)

        #Task 1.3 (Status Bar): instantiate status bar
        self._status_bar = StatusBar(self._right_frame)

        #shop goes between status bar and control frame
        shop = tk.Frame(self._right_frame, bg="#ff6600")
        shop.pack(side=tk.TOP, expand = True, anchor=tk.N, fill=tk.BOTH)


        self._towers = towers = [
            SimpleTower,
            SlowTower,
            InfernoTower,
            LaserTower,
            MissileTower,
            GunTower,
        ]

        self._towers.sort(key=lambda tower_class:tower_class.base_cost)

        #Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            #lambda class_=tower_class: self.select_tower(class_)

            view = ShopTowerView(shop, tower, 
                lambda class_=tower_class: 
                    self.select_tower(class_),
                bg="#ff6600", highlightbackground="#4b3b4a",bd=0,highlightthickness=0)


            view.pack(fill=tk.X)
            #Used to check if tower is affordable when refreshing view
            self._tower_views.append((tower, view)) 



        #Task 1.5 (Play Controls): instantiate widgets here
        self._control_frame = tk.Frame(self._right_frame)
        self._control_frame.pack(expand=True)
        self._wave_button = tk.Button(self._control_frame,
            text="next wave", command=self.next_wave)
        self._wave_button.pack(side=tk.LEFT)

        self._play_button_text = tk.StringVar()
        self._play_button_text.set("play")
        self._play_button = tk.Button(self._control_frame, textvariable=self._play_button_text,
            command=self._toggle_paused)
        self._play_button.pack(side=tk.RIGHT)

        #6.3 initiate upgrade dictionary to store upgrade controls for each tower
        self._upgrade_controls = {}


        #bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        #Task 1.2 (Tower Placement): bind mouse events to canvas here
        #Binds left click, mouse motion and mouse leave
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Motion>", self._move)
        #self._view.bind("<ButtonRelease-1>", self._mouse_leave)
        self._view.bind("<Leave>",self._mouse_leave)
        self._view.bind("<Button-2>", self._right_click)

        #high scores
        self._high_score_manager = HighScoreManager()



        #handling close window
        import sys
        if len(sys.argv) == 1:
            self._master.protocol("WM_DELETE_WINDOW", self._exit)
        #catching keyboard event Destroy
        #self._master.bind("<Destroy>", self._exit)

        #Level
        self._level = MyLevel()

        #self.select_tower(SimpleTower)
        self.select_tower(SimpleTower)

        self._view.draw_borders(game.grid.get_border_coordinates(), "#66ff66")

        #Get ready for the game
        self._setup_game()

        #laser count
        self._laser_count = 0
Exemplo n.º 13
0
class TowerGameApp(Stepper):
    """Top-level GUI application for a simple tower defence game"""

    #All private attributes for ease of reading
    _current_tower = None
    _paused = False
    _won = None

    _level = None
    _wave = None
    _score = None
    _coins = None
    _lives = None

    _master = None
    _game = None
    _view = None

    def __init__(self, master: tk.Tk, delay: int = 20):
        """Construct a tower defence game in a root window

        Parameters:
            master (tk.Tk): Window to place the game into
        """

        self._master = master
        super().__init__(master, delay=delay)
        master.title("tkDefend")
        
        self._game = game = TowerGame()

        self._highscores = {}

        self.setup_menu()
        self._master.configure(menu=self._menu)

        #create a game view and draw grid borders
        self._view = view = GameView(master, size=game.grid.cells,
                                     cell_size=game.grid.cell_size,
                                     bg='#000033') #was antique white
        view.pack(side=tk.LEFT, expand=True)

        #have a menu frame on the right
        self._right_frame = tk.Frame(self._master)
        self._right_frame.pack(side=tk.RIGHT, expand=True, fill=tk.Y)

        #Task 1.3 (Status Bar): instantiate status bar
        self._status_bar = StatusBar(self._right_frame)

        #shop goes between status bar and control frame
        shop = tk.Frame(self._right_frame, bg="#ff6600")
        shop.pack(side=tk.TOP, expand = True, anchor=tk.N, fill=tk.BOTH)


        self._towers = towers = [
            SimpleTower,
            SlowTower,
            InfernoTower,
            LaserTower,
            MissileTower,
            GunTower,
        ]

        self._towers.sort(key=lambda tower_class:tower_class.base_cost)

        #Create views for each tower & store to update if availability changes
        self._tower_views = []
        for tower_class in towers:
            tower = tower_class(self._game.grid.cell_size // 2)

            #lambda class_=tower_class: self.select_tower(class_)

            view = ShopTowerView(shop, tower, 
                lambda class_=tower_class: 
                    self.select_tower(class_),
                bg="#ff6600", highlightbackground="#4b3b4a",bd=0,highlightthickness=0)


            view.pack(fill=tk.X)
            #Used to check if tower is affordable when refreshing view
            self._tower_views.append((tower, view)) 



        #Task 1.5 (Play Controls): instantiate widgets here
        self._control_frame = tk.Frame(self._right_frame)
        self._control_frame.pack(expand=True)
        self._wave_button = tk.Button(self._control_frame,
            text="next wave", command=self.next_wave)
        self._wave_button.pack(side=tk.LEFT)

        self._play_button_text = tk.StringVar()
        self._play_button_text.set("play")
        self._play_button = tk.Button(self._control_frame, textvariable=self._play_button_text,
            command=self._toggle_paused)
        self._play_button.pack(side=tk.RIGHT)

        #6.3 initiate upgrade dictionary to store upgrade controls for each tower
        self._upgrade_controls = {}


        #bind game events
        game.on("enemy_death", self._handle_death)
        game.on("enemy_escape", self._handle_escape)
        game.on("cleared", self._handle_wave_clear)

        #Task 1.2 (Tower Placement): bind mouse events to canvas here
        #Binds left click, mouse motion and mouse leave
        self._view.bind("<Button-1>", self._left_click)
        self._view.bind("<Motion>", self._move)
        #self._view.bind("<ButtonRelease-1>", self._mouse_leave)
        self._view.bind("<Leave>",self._mouse_leave)
        self._view.bind("<Button-2>", self._right_click)

        #high scores
        self._high_score_manager = HighScoreManager()



        #handling close window
        import sys
        if len(sys.argv) == 1:
            self._master.protocol("WM_DELETE_WINDOW", self._exit)
        #catching keyboard event Destroy
        #self._master.bind("<Destroy>", self._exit)

        #Level
        self._level = MyLevel()

        #self.select_tower(SimpleTower)
        self.select_tower(SimpleTower)

        self._view.draw_borders(game.grid.get_border_coordinates(), "#66ff66")

        #Get ready for the game
        self._setup_game()

        #laser count
        self._laser_count = 0

    def setup_menu(self):
        '''
        Construct a file menu
        '''
        #Task 1.4: construct file menu here
        self._menu = tk.Menu(self._master)
        self._filemenu = tk.Menu(self._menu)

        self._filemenu.add_command(label="New Game", command=self._new_game)
        self._filemenu.add_command(label="High Scores", command=self._handle_highscores)
        self._filemenu.add_command(label="Exit", command=self._exit)

        self._menu.add_cascade(label="File", menu=self._filemenu)


    def _toggle_paused(self, paused=None):
        """Toggles or sets the paused state

        Parameters:
            paused (bool): Toggles/pauses/unpauses if None/True/False, respectively
        """
        #automatically start the first wave
        if self._wave == 0:
            self.next_wave()

        if paused is None:
            paused = not self._paused

        #Task 1.5 (Play Controls): Reconfigure the pause button here
        
        if paused:
            self.pause()
            self._play_button_text.set("play")
        else:
            self.start()
            self._play_button_text.set("pause")

        self._paused = paused

    def _setup_game(self):
        '''setup the game'''

        self._wave = 0
        self._score = 0
        self._coins = 100
        self._lives = 30

        self._won = False

        #Task 1.3 (Status Bar): Update status here
        self._status_bar.set_wave(self._wave)
        self._status_bar.set_score(self._score)
        self._status_bar.set_coins(self._coins)
        self._status_bar.set_lives(self._lives)

        #Task 1.5 (Play Controls): Re-enable the play controls here (if they were ever disabled)
        if self._wave_button.cget('state') != 'normal' or self._play_button.cget('state') != 'normal':
            self._wave_button.config(state=tk.NORMAL)
            self._play_button.config(state=tk.NORMAL)

        #set initial availability for tower views
        for tower, view in self._tower_views:
            if self._coins < tower.get_value():
                view.set_available(False)
            else: 
                view.set_available(True)

        self._game.reset()

        self._toggle_paused()

        #to store and retrive boss images
        self._view._boss_images = {}
        self._view.laser_counts = {}
        self._view.total_laser_count = 0

    #Task 1.4 (File Menu): Complete menu item handlers here (including docstrings!)
    
    def _new_game(self):
        '''
        restarts the game
        '''
        
        #clears the enemies
        self._view.delete("enemy","tower","obstacle","laser")
        self._game.queue_wave([], True) #clear all enemies
        self._setup_game()


    def _exit(self):
        '''
        exits the application
        '''
        confirm_window = tk.Toplevel(self._master)

        exit_label = tk.Label(confirm_window, text="Are you sure you want to exit?")
        exit_label.pack(pady=10)
        yes_button = tk.Button(confirm_window, text="Yes", command=self._master.destroy)
        yes_button.pack(side=tk.LEFT, padx=20)
        no_button = tk.Button(confirm_window, text="No", command=confirm_window.destroy)
        no_button.pack(side=tk.LEFT, padx=20)

        #quitting with keyboard
        def handle_return(event):
            '''handle quitting with keyboard'''
            self._master.destroy()

        confirm_window.bind("<Return>", handle_return)

    def _handle_highscores(self):
        '''displays highscores'''
        label_txt = "High scores:\n"
        high_score_entries = self._high_score_manager.get_entries()
        for entry in high_score_entries:
            label_txt += "%s: %s\n" %(entry['name'], entry['score'])

        highscore_window = tk.Toplevel(self._master)
        highscore_window.title("high scores")
        highscore_window.geometry('200x200')
        label = tk.Label(highscore_window, text=label_txt, padx=20)
        label.pack()


    def refresh_view(self):
        """Refreshes the game view"""
        if self._step_number % 2 == 0:
            self._view.draw_enemies(self._game.enemies)
        self._view.draw_towers(self._game.towers)
        self._view.draw_obstacles(self._game.obstacles)


    def _step(self):
        """
        Perform a step every interval

        Triggers a game step and updates the view

        Returns:
            (bool) True if the game is still running
        """
        
        self._game.step()

        self._view.delete('laser')


        self.refresh_view()

        return not self._won

    #Task 1.2 (Tower Placement): Complete event handlers here (including docstrings!)
    #Event handlers: _move, _mouse_leave, _left_click
    def _move(self, event):
        """
        Handles the mouse moving over the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        if self._current_tower.get_value() > self._coins:
            return

        #move the shadow tower to mouse position
        position = event.x, event.y
        self._current_tower.position = position

        legal, grid_path = self._game.attempt_placement(position)

        #find the best path and covert positions to pixel positions
        path = [self._game.grid.cell_to_pixel_centre(position)
                for position in grid_path.get_shortest()]

        #Task 1.2 (Tower placement): Draw the tower preview here
        self._view.draw_preview(self._current_tower, legal)
        self._view.draw_path(path)


    def _mouse_leave(self, event):
        """
        Handles the mouse leaving the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """

        #Task 1.2 (Tower placement): Delete the preview
        #Hint: Relevant canvas items are tagged with: 'path', 'range', 'shadow'
        #      See tk.Canvas.delete (delete all with tag)
        self._view.delete("shadow", "range", "path")

    def _left_click(self, event):
        """
        Handles the mouse left clicking in the game view canvas

        Parameter:
            event (tk.Event): Tkinter mouse event
        """
        #retrieve position to place tower
        if self._current_tower is None:
            return

        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)
        
        #if the event position already has a tower, show the upgrades for it
        if cell_position in self._game.towers:

            tower = self._game.towers[cell_position]


            #hide all upgrade_controls
            for t in self._upgrade_controls:
                self._upgrade_controls[t].pack_forget()

            
            #initiate the upgrade control if it doesn't already exist
            if tower not in self._upgrade_controls:
                upgrade_control = UpgradeControl(self._right_frame, tower, self)
                self._upgrade_controls[tower] = upgrade_control
                upgrade_control.pack(expand=True)

            else:
                #pack the one with the tower
                tower = self._game.towers[cell_position]
                upgrade_control = self._upgrade_controls[tower]
                upgrade_control.pack(expand=True)
                upgrade_control.check_status()


        #Task 1.2 (Tower placement): Attempt to place the tower being previewed
        legal, grid_path = self._game.attempt_placement(position)

        if legal and (self._current_tower.get_value() <= self._coins):
            self._coins -= self._current_tower.get_value()            
            self._status_bar.set_coins(self._coins)

            #refresh view upon placing a tower

            if self._game.place(cell_position, tower_type=self._current_tower.__class__):
                #delete preview after placing
                self._view.delete("shadow", "range", "path")
                for tower_type, shop_tower_view in self._tower_views:
                    if tower_type.base_cost > self._coins:
                        shop_tower_view.set_available(False)
                self.refresh_view()
                self._step()

    def _right_click(self, event):
        """
        Handles the mouse right clicking in the game view canvas.
        The tower in that cell position will be removed and 80% of its value 
        will be returned to the player.

        Parameter:
            event (tk.Event): Tkinter mouse event
        """

        position = event.x, event.y
        cell_position = self._game.grid.pixel_to_cell(position)

        removed_tower = self._game.remove(cell_position)
        self._coins += removed_tower.get_value() * 0.8

        #updates coins string var to display coins
        self._status_bar.set_coins(self._coins)

        #update availability for tower views
        for tower, view in self._tower_views:
            if self._coins < tower.get_value():
                view.set_available(False)
            else: 
                view.set_available(True)

    def next_wave(self):
        """Sends the next wave of enemies against the player"""
        if self._wave == self._level.get_max_wave():
            return

        self._wave += 1

        #Task 1.3 (Status Bar): Update the current wave display here
        self._status_bar.set_wave(self._wave)

        #Task 1.5 (Play Controls): Disable the add wave button here (if this is the last wave)
        if self._wave == 20:
            self._wave_button.config(state=tk.DISABLED)

        #Generate wave and enqueue
        wave = self._level.get_wave(self._wave, self._game)
        for step, enemy in wave:
            enemy.set_cell_size(self._game.grid.cell_size)

        self._game.queue_wave(wave)

    def select_tower(self, tower):
        """
        Set 'tower' as the current tower

        Parameters:
            tower (AbstractTower): The new tower type
        """
        for tower_type, shop_tower_view in self._tower_views:
            shop_tower_view.set_selected(False)
            if tower_type.__class__ == tower:
                shop_tower_view.set_selected(True)

        self._current_tower = tower(self._game.grid.cell_size)

    def _handle_death(self, enemies):
        """
        Handles enemies dying

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which died in a step
        """
        # for enemy in enemies:
        #     if type(enemy) == SuperRichardEnemy:
        #         time.sleep(0.2)
        bonus = len(enemies) ** .5
        for enemy in enemies:
            self._coins += enemy.points
            self._score += int(enemy.points * bonus)

        #Task 1.3 (Status Bar): Update coins & score displays here
        self._status_bar.set_coins(self._coins)
        self._status_bar.set_score(self._score)

        for tower, view in self._tower_views:
            if self._coins < tower.get_value():
                view.set_available(False)
            else: 
                view.set_available(True)


    def _handle_escape(self, enemies):
        """
        Handles enemies escaping (not being killed before moving through the grid

        Parameters:
            enemies (list<AbstractEnemy>): The enemies which escaped in a step
        """
        for enemy in enemies:
            self._lives -= enemy.live_damage
            if self._lives < 0:
                self._lives = 0

        #Task 1.3 (Status Bar): Update lives display here
        self._status_bar.set_lives(self._lives)

        #Handle game over
        if self._lives <= 0:
            self._handle_game_over(won=False)

    def _handle_wave_clear(self):
        """Handles an entire wave being cleared (all enemies killed)"""
        if self._wave == self._level.get_max_wave():
            self._handle_game_over(won=True)


    def record_high_score_from_game_over(self, entry_box):
        '''record the highscore of that player and add it to the highscore 
        list, then destroy the game over dialog box and show the highscore 
        box.

        parameter:
            player (string): name of the player
            highscore (string): the highscore to record
            entry_box (tk.Entry): the entry box to get the player's name and
            destroy its parent from.
        '''
        player = entry_box.get()
        self._high_score_manager.add_entry(player, self._score, '')
        self._high_score_manager.save()
        entry_box.master.destroy()

        #show the high scores
        self._handle_highscores()




    def _handle_game_over(self, won=False):
        """Handles game over
        
        Parameter:
            won (bool): If True, signals the game was won (otherwise lost)
        """
        self._won = won
        self.stop()

        #Task 1.4 (Dialogs): show game over dialog here
        dialog_box = tk.Toplevel(self._master)
        dialog_box.title("Game Over")

        if won:
            message = "You won! Enter your name:"
        else:
            message = "You lost! Enter your name:"

        label = tk.Label(dialog_box, text=message)
        label.pack(padx=50,pady=10)


        highscore_prompt = tk.Entry(dialog_box)
        highscore_prompt.pack(expand=True, pady=10)

        ok_button = tk.Button(dialog_box, text="ok",
         command=lambda entry_box=highscore_prompt: self.record_high_score_from_game_over(entry_box))
        ok_button.pack(pady=10)

        #disable buttons if game over
        self._wave_button.config(state=tk.DISABLED)
        self._play_button.config(state=tk.DISABLED)