<reponame>crank17/UniProjects<filename>csse1001/a3_files/app.py
"""
Simple 2d world where the player can interact with the items in the world.
"""

__author__ = "<NAME>"
__date__ = "<EMAIL>"
__version__ = "1.1.0"
__copyright__ = "The University of Queensland, 2019"

import tkinter as tk
from tkinter import messagebox
import random
from collections import namedtuple
import pymunk
from block import Block, ResourceBlock, BREAK_TABLES, LeafBlock,\
    TrickCandleFlameBlock
from grid import Stack, Grid, SelectableGrid, ItemGridView
from item import Item, SimpleItem, HandItem, BlockItem, MATERIAL_TOOL_TYPES, \
    TOOL_DURABILITIES
from player import Player
from dropped_item import DroppedItem
from crafting import GridCrafter, CraftingWindow
from world import World
from core import positions_in_range
from game import GameView, WorldViewRouter, BoundaryWall
from mob import Bird
from sheep import Sheep
from furnace import FurnaceBlock, AdvancedCraftingWindow


BLOCK_SIZE = 2 ** 5
GRID_WIDTH = 2 ** 5
GRID_HEIGHT = 2 ** 4

# Task 3/Post-grad only:
# Class to hold game data that is passed to each thing's step function
# Normally, this class would be defined in a separate file
# so that type hinting could be used on PhysicalThing & its
# subclasses, but since it will likely need to be extended
# for these tasks, we have defined it here
GameData = namedtuple('GameData', ['world', 'player'])

class WorldViewRouter_new(WorldViewRouter):
    """Subclass of WorldViewRouter, used to change the routing table in order
    to draw additional mobs"""
    _routing_table = [
        # (class, method name)
        (Block, '_draw_block'),
        (TrickCandleFlameBlock, '_draw_mayhem_block'),
        (DroppedItem, '_draw_physical_item'),
        (Player, '_draw_player'),
        (Bird, '_draw_bird'),
        (Sheep, '_draw_sheep'),
        (BoundaryWall, '_draw_undefined'),
        (None.__class__, '_draw_undefined')
    ]

    def _draw_sheep(self, instance, shape, view):
        # draws a w white, rectangular sheep that routes to the Sheep class in
        # sheep.py
        return [view.create_rectangle(shape.bb.left, shape.bb.top,
                                      shape.bb.right, shape.bb.bottom,
                                 fill="white", tags=('mob', 'sheep'))]


def create_block(*block_id):
    """(Block) Creates a block (this function can be thought of as a block
     factory)

    Parameters:
        block_id (*tuple): N-length tuple to uniquely identify the block,
        often comprised of strings, but not necessarily (arguments are grouped
        into a single tuple)

    Examples:
        >>> create_block("leaf")
        LeafBlock()
        >>> create_block("stone")
        ResourceBlock('stone')
        >>> create_block("mayhem", 1)
        TrickCandleFlameBlock(1)
    """
    BREAK_TABLES_V2 = {
        "wool": {
            "hand": (1, True)
        },
        "wood_step": {
            "hand": (3, True)
        },
        "gold": {
            "hand": (70, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (7.5, False),
            "iron_pickaxe": (4, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
        "iron": {
            "hand": (50, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (5, True),
            "iron_pickaxe": (3, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
        "diamond": {
            "hand": (100, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (7.5, False),
            "iron_pickaxe": (5, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
    }
    if len(block_id) == 1:
        block_id = block_id[0]
        if block_id == "leaf":
            return LeafBlock()
        elif block_id == 'crafting_table':
            return CraftingTableBlock(block_id, block_id[1])
        elif block_id == "furnace":
            return FurnaceBlock(block_id,block_id[1])
        elif block_id in BREAK_TABLES:
            return ResourceBlock(block_id, BREAK_TABLES[block_id])
        elif block_id in BREAK_TABLES_V2:
            return ResourceBlock(block_id, BREAK_TABLES_V2[block_id])

    elif block_id[0] == 'mayhem':
        return TrickCandleFlameBlock(block_id[1])
    elif block_id[0] == 'crafting_table':
        return CraftingTableBlock(block_id[0], block_id[1])
    raise KeyError(f"No block defined for {block_id}")


def create_item(*item_id):
    """(Item) Creates an item (this function can be thought of as a item
    factory)

    Parameters:
        item_id (*tuple): N-length tuple to uniquely identify the item,
        often comprised of strings, but not necessarily (arguments are grouped
        into a single tuple)

    Examples:
        >>> create_item("dirt")
        BlockItem('dirt')
        >>> create_item("hands")
        HandItem('hands')
        >>> create_item("pickaxe", "stone")  # *without* Task 2.1.2 implemented
        Traceback (most recent call last):
        ...
        NotImplementedError: "Tool creation is not yet handled"
        >>> create_item("pickaxe", "stone")  # *with* Task 2.1.2 implemented
        ToolItem('stone_pickaxe')

    """
    BlockItems = ['wood', 'stone', "crafting_table", 'furnace', 'wool',
                  'wood_step']
    NormalItems = ['stick', 'big_stick', 'wool_vest', 'cloth', 'blanket',
                   'charcoal', 'heated_blanket', 'iron', 'gold', 'diamond']

    if len(item_id) == 2:

        if item_id[0] in MATERIAL_TOOL_TYPES and item_id[1] in TOOL_DURABILITIES:
            return ToolItem(item_id[0], item_id[1], TOOL_DURABILITIES[item_id[1]])

    elif len(item_id) == 1:

        item_type = item_id[0]
        # Task 1.4 Basic Items: Create wood & stone here
        if item_type == "hands":
            return HandItem("hands")

        elif item_type == "dirt":
            return BlockItem(item_type)
        if item_type in BlockItems:
            return BlockItem(item_type)
        if item_type in NormalItems:
            return Item(item_type)

        elif item_type == "apple":
            return FoodItem(item_type, 2)

        elif item_type == 'boomerang':
            return ToolItem('', "boomer\nrang",200)

        elif item_type == 'cooked_apple':
            return FoodItem(item_type, 5)

        elif item_type == "burnt_apple":
            return FoodItem(item_type, -2)

        elif item_type == "fire_stick":
            return ToolItem("", item_type, 999999)

    raise KeyError(f"No item defined for {item_id}")


# Task 1.3: Implement StatusView class here
class StatusView(tk.Frame):
    """Class inherited from Frame in order to display the user's health
    and food status"""
    def __init__(self, master, game):
        """Constructor:
        Parameters:
            master: the tkinter root widg  et
            game: the instance of the Ninedraft class"""
        super().__init__(master)
        self.health_icon = tk.PhotoImage(file="health.png")
        self.hunger_icon = tk.PhotoImage(file = "hunger.png")
        self.health_image = tk.Label(self, image=self.health_icon).\
            grid(row=0, column=1)
        self.hunger_image = tk.Label(self, image=self.hunger_icon).\
            grid(row=0, column=3)

        self._health_label = tk.Label(self, text="Health: {}  ".
                                      format(game._player.get_health()),
                                   font=("fixedsys", 10))
        self._health_label.grid(row=0, column=2)

        self._food_label = tk.Label(self, text="Food: {}".
                                    format(game._player.get_food()),
                                   font=("fixedsys", 10))
        self._food_label.grid(row=0, column=4)

    def set_health(self, health):
        """updates health based on input"""
        self._health_label.config(text="Health: {}  ".format(health))

    def set_food(self, food):
        """Updates food based on input"""
        self._food_label.config(text="Food: {}".format(food))


class FoodItem(Item):
    """Food item class inherited from superclass Item"""
    def __init__(self, item_id: str, strength: float):
        """Constructor:
        Parameters:
            item_id: the string of the id of the item, e.g. the items name ("apple")
            strength: the strength of the item when used, e.g. if the strength
            of an apple is 2, will increase the health or food by 2 units"""
        super().__init__(item_id)
        self._strength = strength
        self._item_id = item_id

    def get_strength(self):
        """get the strength of the given item"""
        return self._strength

    def place(self):
        """Use the selected item"""
        return [("effect", ('food', self._strength))]
    def can_attack(self):
        """Items are not able to attack"""
        return False


class ToolItem(Item):
    """Tool Item class inherited from superclass Item"""
    def __init__(self, item_id, tool_type, durability):
        """Constructor:
        Parameters:
            item_id (str): the identification of the tool (name)
            tool_type (str): the material that the tool is made out of
            durability (float): the durability of the material"""
        super().__init__(item_id)
        self._id = f'{tool_type}_{item_id}'
        self._tool_type = tool_type
        self._durability = durability
        self._max_durability = durability

    def get_type(self):
        """gets the type of material and type of tool"""
        return self._tool_type

    def get_durability(self):
        """returns the durability of the tool"""
        return self._durability

    def get_max_durability(self):
        """returns the maximum durability of the tool"""
        return self._max_durability

    def can_attack(self):
        """Returns if the tool has run out of durability or not"""
        #if the tool's durability has run out, it will no longer be able
        # to mine or attack
        if self._durability > 0:
            return True
        else:
            return False

    def attack(self, successful):
        """returns if the attack is successful, reduce the durability by one"""
        if not successful:
            self._durability -= 1

    def is_stackable(self):
        """Tools are not stackable"""
        return False


class CraftingTableBlock(ResourceBlock):
    """Crafting table class to create the main source of crafting"""
    def __init__(self, block_id, break_table):
        """Constructor
        Parameters:
            block_id (str): The string identification of the block
            break_table (str): The specific break table of the block"""
        super().__init__(block_id, break_table)
        self._block_id = block_id
        self._block_id = 'crafting_table'
        self._hitpoints = 1

        #break table for crafting table, as it is wood, most logical to only
        # break with an axe and hands
        self._break_table = {
            "hand": (20, True),
            "wood_axe": (18, True),
            "stone_axe": (17, True),
            "iron_axe": (15, True),
            "diamond_axe": (10, True),
            "golden_axe": (10, True),
            }

    def get_block_id(self):
        return self._block_id

    def get_drops(self, luck, correct_item_used):
        """Drops a itself in item form 5 times

        See Block.get_drops for parameters & return"""

        if correct_item_used:
            return [('item', (self._block_id,))]

    def use(self):
        """On right click, open the crafting window"""
        return ('crafting', 'crafting_table')


    def __repr__(self):
        return f"ResourceBlock({self._id!r})"

BLOCK_COLOURS = {
    'diamond': 'blue',
    'dirt': '#552015',
    'stone': 'grey',
    'gold': "#FAFD22",
    'iron': "#BCC7D2",
    'wood': '#723f1c',
    'leaves': 'green',
    'wood_step':'tan2',
    'crafting_table': 'pink',
    'furnace': 'black',
    'wool':'white',
}

ITEM_COLOURS = {
    'diamond': 'blue',
    'dirt': '#552015',
    'gold': "#FAFD22",
    'iron': "#BCC7D2",
    'stone': 'grey',
    'wood': '#723f1c',
    'wood_step':'tan2',
    'apple': '#ff0000',
    'leaves': 'green',
    'crafting_table': 'pink',
    'furnace': 'black',
    'cooked_apple': 'red4',
    'wool': 'white'
}


def load_simple_world(world):
    """Loads blocks into a world

    Parameters:
        world (World): The game world to load with blocks
    """
    block_weights = [
        (100, 'dirt'),
        (30, 'stone'),
        (10, "iron"),
        (3, "gold"),
        (1, "diamond")
    ]

    cells = {}

    ground = []

    width, height = world.get_grid_size()

    for x in range(width):
        for y in range(height):
            if x < 22:
                if y <= 8:
                    continue
            else:
                if x + y < 30:
                    continue

            ground.append((x, y))

    weights, blocks = zip(*block_weights)
    kinds = random.choices(blocks, weights=weights, k=len(ground))

    for cell, block_id in zip(ground, kinds):
        cells[cell] = create_block(block_id)

    trunks = [(3, 8), (3, 7), (3, 6), (3, 5)]

    for trunk in trunks:
        cells[trunk] = create_block('wood')

    leaves = [(4, 3), (3, 3), (2, 3), (4, 2), (3, 2), (2, 2), (4, 4), (3, 4), (2, 4)]

    for leaf in leaves:
        cells[leaf] = create_block('leaf')

    for cell, block in cells.items():
        # cell -> box
        i, j = cell

        world.add_block_to_grid(block, i, j)

    world.add_block_to_grid(create_block("mayhem", 0), 14, 8)

    world.add_mob(Bird("friendly_bird", (12, 12)), 400, 100)
    world.add_mob(Sheep("sheep", (50, 40)), 400, 100)


CRAFTING_RECIPES_2x2 = [
    (
        (
            (None, 'wood'),
            (None, 'wood')
        ),
        Stack(create_item('stick'), 4)
    ),
    (
        (
            ('stick', 'stick'),
            (None, 'stick')
        ),
        Stack(create_item('boomerang'), 1)
    ),
    (
        (
            ('stick', None),
            (None, 'stick')
        ),
        Stack(create_item('big_stick'), 1)
    ),
    (
        (
            ('wood', 'wood'),
            ('wood', 'wood')
        ),
        Stack(create_item('crafting_table'), 1)
    ),
    (
        (
            ('wool', None),
            (None, None)
        ),
        Stack(create_item('cloth'), 4)
    ),
    (
        (
            ('wood', None),
            ('wood', 'wood')
        ),
        Stack(create_item('wood_step'), 1)
    )


]

#3x3 Crafting Recipes
CRAFTING_RECIPES_3x3 = {
    (
        (
            (None, None, None),
            (None, 'wood', None),
            (None, 'wood', None)
        ),
        Stack(create_item('stick'), 16)
    ),
    (
        (
            ('wood', 'wood', 'wood'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'wood'), 1)
    ),
    (
        (
            ('stone', 'stone', 'stone'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'stone'), 1)
    ),
    (
        (
            ('iron', 'iron', 'iron'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'iron'), 1)
    ),
    (
        (
            ('diamond', 'diamond', 'diamond'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'diamond'), 1)
    ),
    (
        (
            ('gold', 'gold', 'gold'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'gold'), 1)
    ),
    (
        (
            ('wood', 'wood', None),
            ('wood', 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('axe', 'wood'), 1)
    ),
    (
        (
            (None, 'wood', None),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('shovel', 'wood'), 1)
    ),
    (
        (
            (None, 'stone', None),
            (None, 'stone', None),
            (None, 'stick', None)
        ),
        Stack(create_item('sword', 'wood'), 1)
    ),
    (

        (
            ('stone', 'stone', 'stone'),
            ('stone', None, 'stone'),
            ('stone', 'stone', 'stone')
        ),
        Stack(create_item('furnace'), 1)
    ),
    (

        (
            ('wool', None, 'wool'),
            ('wool', 'wool' , 'wool'),
            ('wool', 'wool', 'wool')
        ),
        Stack(create_item('wool_vest'), 1)
    ),
    (

        (
            ('cloth', "cloth", 'cloth'),
            ('cloth', 'cloth', 'cloth'),
            ('cloth', 'cloth', 'cloth')
        ),
        Stack(create_item('blanket'), 1)
    ),

}

CRAFTING_RECIPES_FURNACE = {
    (

        (

            ('apple',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('cooked_apple'), 1)
    ),
    (

        (

            ('cooked_apple',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('burnt_apple'), 1)
    ),
    (

        (

            ('wood',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('charcoal'), 4)
    ),
    (

        (

            ('charcoal',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('diamond'), 1)
    ),
    (

        (

            ('blanket',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('heated_blanket'), 1)
    ),
    (

        (

            ('stick',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('fire_stick'), 1)
    ),

}


class Ninedraft:
    """High-level app class for Ninedraft, a 2d sandbox game"""

    def __init__(self, master):
        """Constructor

        Parameters:
            master (tk.Tk): tkinter root widget
        """

        self._master = master
        #1.1 - Title
        master.title("Ninedraft")
        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)

        load_simple_world(self._world)

        self._player = Player()
        self._world.add_player(self._player, 250, 150)

        self._world.add_collision_handler("player", "item", on_begin=self.
                                          _handle_player_collide_item)

        self._hot_bar = SelectableGrid(rows=1, columns=10)
        self._hot_bar.select((0, 0))

        starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item("apple"), 4),
            Stack(create_item('crafting_table'),1),
            Stack(create_item('furnace'), 1),
            Stack(create_item('pickaxe', 'diamond'), 1),
            Stack(create_item("axe", "diamond"),1)

        ]

        for i, item in enumerate(starting_hotbar):
            self._hot_bar[0, i] = item

        self._hands = create_item('hands')

        starting_inventory = [
            ((1, 5), Stack(create_item('dirt'), 10)),
            ((0, 2), Stack(create_item('wood'), 10)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack

        self._crafting_window = None
        self._master.bind_all("e",
                          lambda e: self.run_effect(('crafting', 'basic')))

        self._view = GameView(master, self._world.get_pixel_size(),
                              WorldViewRouter_new(BLOCK_COLOURS, ITEM_COLOURS))
        self._view.pack()

        # Task 1.2 Mouse Controls: Bind mouse events here
        self._view.bind("<Motion>", self._mouse_move)
        self._view.bind("<Button-1>", self._left_click)
        #button-2 for functionailty with mac/other computers
        self._view.bind('<Button-2>', self._right_click)
        #button-3 is right click on Windows
        self._view.bind("<Button-3>", self._right_click)
        # tkinter <leave> handles when cursor leaves the widget
        self._view.bind("<Leave>", self._mouse_leave)


        # Task 1.3: Create instance of StatusView here
        self._status_view = StatusView(master, self)
        self._status_view.pack(side=tk.TOP)


        self._hot_bar_view = ItemGridView(master, self._hot_bar.get_size())
        self._hot_bar_view.pack(side=tk.TOP, fill=tk.X)

        # Task 1.5 Keyboard Controls: Bind to space bar for jumping here
        self._master.bind("<space>", lambda e: self._jump())

        self._master.bind("a", lambda e: self._move(-1, 0))
        self._master.bind("<Left>", lambda e: self._move(-1, 0))
        self._master.bind("d", lambda e: self._move(1, 0))
        self._master.bind("<Right>", lambda e: self._move(1, 0))
        self._master.bind("s", lambda e: self._move(0, 1))
        self._master.bind("<Down>", lambda e: self._move(0, 1))

        # Task 1.5 Keyboard Controls: Bind numbers to hotbar activation here
        for key in range(1, 10):
            self._master.bind(str(key), lambda e, key = key:
            self._activate_item(key-1))
        # 0 key gets confused with front of list, defined seperately.
        self._master.bind("0", lambda e: self._hot_bar.toggle_selection((0, 9)))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        #Utilizing the tkinter menubar
        self.menubar = tk.Menu(master)
        master.config(menu=self.menubar)
        #tearoff removes dashed line at top of dropdown box
        file_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Exit", command=self._popupmsg_exit)
        file_menu.add_command(label="New Game", command=self._popupmsg_restart)

        self._target_in_range = False

        self._target_position = 0, 0

        self.redraw()

        self.step()

    def _popupmsg_exit(self):
        """Display exit popup box on button click"""
        response = messagebox.askyesno("Exit?", "Do you want to exit?")
        if response:
            #Destroy widget
            self._master.destroy()

    def _popupmsg_restart(self):
        """Display restart popup box on button click"""
        response = messagebox.askyesno("Restart?", "Do you want to restart?")
        if response:
            #run restart
            self._restart()

    def _popupmsg_death(self):
        """Display restart popup box on player death"""
        response = messagebox.askyesno("You kicked the bucket!",
                                       "Do you want to respawn?")
        if response:
            #restart game
            self._restart()
        else:
            #close game
            self._master.destroy()

    def redraw(self):
        """Continuosly refresh and redraw all items in world in accordance with
        the step() function"""
        self._view.delete(tk.ALL)

        # physical things
        self._view.draw_physical(self._world.get_all_things())

        target_x, target_y = self._target_position
        target = self._world.get_block(target_x, target_y)
        cursor_position = self._world.grid_to_xy_centre(*self._world.xy_to_grid
        (target_x, target_y))

        # Task 1.2 Mouse Controls: Show/hide target here
        if self._target_in_range:
            self._view.show_target(self._player.get_position(), cursor_position)
        else:
            self._view.hide_target()

        # Task 1.3 StatusView: Update StatusView values here
        self._status_view.set_health(self._player.get_health())
        self._status_view.set_food(self._player.get_food())

        # hot bar
        self._hot_bar_view.render(self._hot_bar.items(), self._hot_bar.
                                  get_selected())

    def step(self):
        """Essentially the clock of the game, refreshes after every 15 ticks"""
        data = GameData(self._world, self._player)
        self._world.step(data)
        self.redraw()

        # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary
        # ...
        if self._player.get_health() <= 0:
            self._popupmsg_death()

        self._master.after(15, self.step)

    def _move(self, dx, dy):
        """Changes player's velocity based on the movement input"""
        self.check_target()
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))


    def _jump(self):
        """Makes the player move up if space bar is pressed"""
        self.check_target()
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        # X velocity is set to slow down the player, Y velocity jumps 200 units up
        self._player.set_velocity((velocity.x * 0.55, velocity.y + -200))


    def mine_block(self, block, x, y):
        """When a player left clicks on the block with the right tool, it will
        break, disappearing and dropping items of themselves"""
        luck = random.random()

        active_item, effective_item = self.get_holding()

        was_item_suitable, was_attack_successful = block.mine(effective_item,
                                                              active_item, luck)

        effective_item.attack(was_attack_successful)

        if block.is_mined():
            # Task 1.2 Mouse Controls: Reduce the player's food/health appropriately

            if self._player.get_food() > 0:
                self._player.change_food(-0.5)
                self._status_view.set_food(self._player.get_food())
            else:
                self._player.change_health(-0.5)
                self._status_view.set_health(self._player.get_health())

            # Task 1.2 Mouse Controls: Remove the block from the world &
            # get its drops
            drops = block.get_drops(luck, was_item_suitable)
            self._world.remove_block(block)

            if not drops:
                return

            x0, y0 = block.get_position()

            for i, (drop_category, drop_types) in enumerate(drops):
                print(f'Dropped {drop_category}, {drop_types}')

                if drop_category == "item":
                    physical = DroppedItem(create_item(*drop_types))

                    # this is so bleh
                    x = x0 - BLOCK_SIZE // 2 + 5 + (i % 3) * 11 + random.randint(0, 2)
                    y = y0 - BLOCK_SIZE // 2 + 5 + ((i // 3) % 3) * 11 + random.randint(0, 2)

                    self._world.add_item(physical, x, y)
                elif drop_category == "block":
                    self._world.add_block(create_block(*drop_types), x, y)
                else:
                    raise KeyError(f"Unknown drop category {drop_category}")

    def get_holding(self):
        """Returns the active item that the player is holding"""
        active_stack = self._hot_bar.get_selected_value()
        active_item = active_stack.get_item() if active_stack else self._hands

        effective_item = active_item if active_item.can_attack() else self._hands

        return active_item, effective_item

    def check_target(self):
        """Checks where the cursor is in the game window"""
        # select target block, if possible
        active_item, effective_item = self.get_holding()

        pixel_range = active_item.get_attack_range() * self._world.get_cell_expanse()

        self._target_in_range = positions_in_range(self._player.get_position(),
                                                   self._target_position,
                                                   pixel_range)

    def _mouse_move(self, event):
        """Tracks the mouse movement in the game window"""
        #trace the mouse movement
        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        """Checks if the mouse has left the widget, which then hides the target"""
        # if target is out of range, return false
        self._target_in_range = False

    def _restart(self):
        """When the player dies, or the restart button is pressed, run this
        function"""
        # Restart function, essentially resets all of the __init__ values of
        # Ninedraft

        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)

        load_simple_world(self._world)

        self._player = Player()
        self._world.add_player(self._player, 250, 150)
        self._status_view.set_health(self._player.get_health())
        self._status_view.set_food(self._player.get_food())

        self._hot_bar = SelectableGrid(rows=1, columns=10)
        self._hot_bar.select((0, 0))

        starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item('apple'), 4)
        ]

        for i, item in enumerate(starting_hotbar):
            self._hot_bar[0, i] = item

        self._hands = create_item('hands')

        starting_inventory = [
            ((1, 5), Stack(Item('dirt'), 10)),
            ((0, 2), Stack(Item('wood'), 10)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack
        self._world.add_collision_handler("player", "item", on_begin=self.
                                          _handle_player_collide_item)

    def _left_click(self, event):
        """Handles the events and effects when the left click button is pressed"""
        # Invariant: (event.x, event.y) == self._target_position
        #  => Due to mouse move setting target position to cursor
        x, y = self._target_position

        if self._target_in_range:
            block = self._world.get_block(x, y)

            if block:
                self.mine_block(block, x, y)
            else:
                mobs = self._world.get_mobs(x, y, max_distance=(2**5))
                for mob in mobs:
                    effect = mob.attack(0)
                    if not effect:
                        continue
                    x0, y0 = mob.get_position()

                    for i, (drop_category, drop_types) in enumerate(effect):
                        print(f'Dropped {drop_category}, {drop_types}')

                        if drop_category == "item":
                            physical = DroppedItem(create_item(*drop_types))
                            x = x0 - BLOCK_SIZE // 2 + 5 + (
                                       i % 3) * 11 + random.randint(0, 2)
                            y = y0 - BLOCK_SIZE // 2 + 5 + (
                                        (i // 3) % 3) * 11 + random.randint(
                                0, 2)

                            self._world.add_item(physical, x, y)

    def _trigger_crafting(self, craft_type):
        """Handles the event when a certain type of crafting is called by pressing
        'e' or right clicking on the furnace or the crafting table"""

        print(f"Crafting with {craft_type}")
        crafter = None
        title = None

        if craft_type == "basic":
            crafter = GridCrafter(CRAFTING_RECIPES_2x2)
            title = "Basic Crafting"
            self._crafting_window = CraftingWindow(self._master, title,
                                                   self._hot_bar,
                                                   self._inventory,
                                                   crafter)
        if craft_type == "crafting_table":
            crafter = GridCrafter(CRAFTING_RECIPES_3x3, rows=3, columns=3)
            title = "Crafting Table"
            self._crafting_window = CraftingWindow(self._master, title,
                                                   self._hot_bar,
                                                   self._inventory,
                                                   crafter)
        if craft_type == 'furnace':
            crafter = GridCrafter(CRAFTING_RECIPES_FURNACE, rows=3, columns=1)
            title = "Smelting"
            self._crafting_window = AdvancedCraftingWindow(self._master, title,
                                               self._hot_bar, self._inventory,
                                               crafter)

    def run_effect(self, effect):
        """Executes the effect given to the function
        i.e. run crafting, update health or food"""
        if len(effect) == 2:
            if effect[0] == "crafting":
                craft_type = effect[1]

                if craft_type == "basic":
                    print("Can't craft much on a 2x2 grid :/")

                elif craft_type == "crafting_table":
                    print("Let's get our kraft® on! King of the brands")
                elif craft_type == "furnace":
                    print("It's gettin hot in here...")
                # Allow crafting window to toggle off and on
                if self._crafting_window == None:
                    self._trigger_crafting(craft_type)
                else:
                    self._crafting_window.destroy()
                    self._crafting_window = None

                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                if self._player.get_food() >= self._player.get_max_food():
                    print(f"Gaining {strength} {'health'}!")
                    getattr(self._player, f"change_{'health'}")(strength)
                else:
                    print(f"Gaining {strength} {stat}!")
                    getattr(self._player, f"change_{stat}")(strength)
                return

        raise KeyError(f"No effect defined for {effect}")

    def _right_click(self, event):
        """Handles the event when the right mouse button is pressed"""
        print("Right click")

        x, y = self._target_position
        target = self._world.get_thing(x, y)

        if target:
            # use this thing
            print(f'using {target}')
            effect = target.use()
            print(f'used {target} and got {effect}')

            if effect:
                self.run_effect(effect)

        else:
            # place active item
            selected = self._hot_bar.get_selected()

            if not selected:
                return

            stack = self._hot_bar[selected]
            drops = stack.get_item().place()

            stack.subtract(1)
            if stack.get_quantity() == 0:
                # remove from hotbar
                self._hot_bar[selected] = None

            if not drops:
                return

            # handling multiple drops would be somewhat finicky, so prevent it
            if len(drops) > 1:
                raise NotImplementedError("Cannot handle dropping more than 1 thing")

            drop_category, drop_types = drops[0]

            x, y = event.x, event.y

            if drop_category == "block":
                existing_block = self._world.get_block(x, y)

                if not existing_block:
                    self._world.add_block(create_block(drop_types[0]), x, y)
                else:
                    raise NotImplementedError(
                        "Automatically placing a block nearby if the target cell is full is not yet implemented")

            elif drop_category == "effect":
                self.run_effect(drop_types)

            else:
                raise KeyError(f"Unknown drop category {drop_category}")

    def _activate_item(self, index):
        """Selects the desired item in the hotbar"""
        print(f"Activating {index}")

        self._hot_bar.toggle_selection((0, index))

    def _handle_player_collide_item(self, player: Player, dropped_item: DroppedItem, data,
                                    arbiter: pymunk.Arbiter):
        """Callback to handle collision between the player and a (dropped) item. If the player has sufficient space in
        their to pick up the item, the item will be removed from the game world.

        Parameters:
            player (Player): The player that was involved in the collision
            dropped_item (DroppedItem): The (dropped) item that the player collided with
            data (dict): data that was added with this collision handler (see data parameter in
                         World.add_collision_handler)
            arbiter (pymunk.Arbiter): Data about a collision
                                      (see http://www.pymunk.org/en/latest/pymunk.html#pymunk.Arbiter)
                                      NOTE: you probably won't need this
        Return:
             bool: False (always ignore this type of collision)
                   (more generally, collision callbacks return True iff the collision should be considered valid; i.e.
                   returning False makes the world ignore the collision)
        """

        item = dropped_item.get_item()

        if self._hot_bar.add_item(item):
            print(f"Added 1 {item!r} to the hotbar")
        elif self._inventory.add_item(item):
            print(f"Added 1 {item!r} to the inventory")
        else:
            print(f"Found 1 {item!r}, but both hotbar & inventory are full")
            return True

        self._world.remove_item(dropped_item)
        return False

# Task 1.1 App class: Add a main function to instantiate the GUI here


def main():
    """Executes the Ninedraft class and opens the game window"""
    root = tk.Tk()
    app = Ninedraft(root)
    # display pop up message if the close button was pressed
    root.protocol("WM_DELETE_WINDOW", app._popupmsg_exit)

    root.mainloop()


if __name__ == "__main__" :
    main()


"""
Simple 2d world where the player can interact with the items in the world.
"""

__author__ = "<NAME>"
__date__ = "<EMAIL>"
__version__ = "1.1.0"
__copyright__ = "The University of Queensland, 2019"

import tkinter as tk
from tkinter import messagebox
import random
from collections import namedtuple
import pymunk
from block import Block, ResourceBlock, BREAK_TABLES, LeafBlock,\
    TrickCandleFlameBlock
from grid import Stack, Grid, SelectableGrid, ItemGridView
from item import Item, SimpleItem, HandItem, BlockItem, MATERIAL_TOOL_TYPES, \
    TOOL_DURABILITIES
from player import Player
from dropped_item import DroppedItem
from crafting import GridCrafter, CraftingWindow
from world import World
from core import positions_in_range
from game import GameView, WorldViewRouter, BoundaryWall
from mob import Bird
from sheep import Sheep
from furnace import FurnaceBlock, AdvancedCraftingWindow


BLOCK_SIZE = 2 ** 5
GRID_WIDTH = 2 ** 5
GRID_HEIGHT = 2 ** 4

# Task 3/Post-grad only:
# Class to hold game data that is passed to each thing's step function
# Normally, this class would be defined in a separate file
# so that type hinting could be used on PhysicalThing & its
# subclasses, but since it will likely need to be extended
# for these tasks, we have defined it here
GameData = namedtuple('GameData', ['world', 'player'])

class WorldViewRouter_new(WorldViewRouter):
    """Subclass of WorldViewRouter, used to change the routing table in order
    to draw additional mobs"""
    _routing_table = [
        # (class, method name)
        (Block, '_draw_block'),
        (TrickCandleFlameBlock, '_draw_mayhem_block'),
        (DroppedItem, '_draw_physical_item'),
        (Player, '_draw_player'),
        (Bird, '_draw_bird'),
        (Sheep, '_draw_sheep'),
        (BoundaryWall, '_draw_undefined'),
        (None.__class__, '_draw_undefined')
    ]

    def _draw_sheep(self, instance, shape, view):
        # draws a w white, rectangular sheep that routes to the Sheep class in
        # sheep.py
        return [view.create_rectangle(shape.bb.left, shape.bb.top,
                                      shape.bb.right, shape.bb.bottom,
                                 fill="white", tags=('mob', 'sheep'))]


def create_block(*block_id):
    """(Block) Creates a block (this function can be thought of as a block
     factory)

    Parameters:
        block_id (*tuple): N-length tuple to uniquely identify the block,
        often comprised of strings, but not necessarily (arguments are grouped
        into a single tuple)

    Examples:
        >>> create_block("leaf")
        LeafBlock()
        >>> create_block("stone")
        ResourceBlock('stone')
        >>> create_block("mayhem", 1)
        TrickCandleFlameBlock(1)
    """
    BREAK_TABLES_V2 = {
        "wool": {
            "hand": (1, True)
        },
        "wood_step": {
            "hand": (3, True)
        },
        "gold": {
            "hand": (70, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (7.5, False),
            "iron_pickaxe": (4, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
        "iron": {
            "hand": (50, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (5, True),
            "iron_pickaxe": (3, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
        "diamond": {
            "hand": (100, False),
            "wood_pickaxe": (10, False),
            "stone_pickaxe": (7.5, False),
            "iron_pickaxe": (5, True),
            "diamond_pickaxe": (2, True),
            "golden_pickaxe": (1, True)
        },
    }
    if len(block_id) == 1:
        block_id = block_id[0]
        if block_id == "leaf":
            return LeafBlock()
        elif block_id == 'crafting_table':
            return CraftingTableBlock(block_id, block_id[1])
        elif block_id == "furnace":
            return FurnaceBlock(block_id,block_id[1])
        elif block_id in BREAK_TABLES:
            return ResourceBlock(block_id, BREAK_TABLES[block_id])
        elif block_id in BREAK_TABLES_V2:
            return ResourceBlock(block_id, BREAK_TABLES_V2[block_id])

    elif block_id[0] == 'mayhem':
        return TrickCandleFlameBlock(block_id[1])
    elif block_id[0] == 'crafting_table':
        return CraftingTableBlock(block_id[0], block_id[1])
    raise KeyError(f"No block defined for {block_id}")


def create_item(*item_id):
    """(Item) Creates an item (this function can be thought of as a item
    factory)

    Parameters:
        item_id (*tuple): N-length tuple to uniquely identify the item,
        often comprised of strings, but not necessarily (arguments are grouped
        into a single tuple)

    Examples:
        >>> create_item("dirt")
        BlockItem('dirt')
        >>> create_item("hands")
        HandItem('hands')
        >>> create_item("pickaxe", "stone")  # *without* Task 2.1.2 implemented
        Traceback (most recent call last):
        ...
        NotImplementedError: "Tool creation is not yet handled"
        >>> create_item("pickaxe", "stone")  # *with* Task 2.1.2 implemented
        ToolItem('stone_pickaxe')

    """
    BlockItems = ['wood', 'stone', "crafting_table", 'furnace', 'wool',
                  'wood_step']
    NormalItems = ['stick', 'big_stick', 'wool_vest', 'cloth', 'blanket',
                   'charcoal', 'heated_blanket', 'iron', 'gold', 'diamond']

    if len(item_id) == 2:

        if item_id[0] in MATERIAL_TOOL_TYPES and item_id[1] in TOOL_DURABILITIES:
            return ToolItem(item_id[0], item_id[1], TOOL_DURABILITIES[item_id[1]])

    elif len(item_id) == 1:

        item_type = item_id[0]
        # Task 1.4 Basic Items: Create wood & stone here
        if item_type == "hands":
            return HandItem("hands")

        elif item_type == "dirt":
            return BlockItem(item_type)
        if item_type in BlockItems:
            return BlockItem(item_type)
        if item_type in NormalItems:
            return Item(item_type)

        elif item_type == "apple":
            return FoodItem(item_type, 2)

        elif item_type == 'boomerang':
            return ToolItem('', "boomer\nrang",200)

        elif item_type == 'cooked_apple':
            return FoodItem(item_type, 5)

        elif item_type == "burnt_apple":
            return FoodItem(item_type, -2)

        elif item_type == "fire_stick":
            return ToolItem("", item_type, 999999)

    raise KeyError(f"No item defined for {item_id}")


# Task 1.3: Implement StatusView class here
class StatusView(tk.Frame):
    """Class inherited from Frame in order to display the user's health
    and food status"""
    def __init__(self, master, game):
        """Constructor:
        Parameters:
            master: the tkinter root widg  et
            game: the instance of the Ninedraft class"""
        super().__init__(master)
        self.health_icon = tk.PhotoImage(file="health.png")
        self.hunger_icon = tk.PhotoImage(file = "hunger.png")
        self.health_image = tk.Label(self, image=self.health_icon).\
            grid(row=0, column=1)
        self.hunger_image = tk.Label(self, image=self.hunger_icon).\
            grid(row=0, column=3)

        self._health_label = tk.Label(self, text="Health: {}  ".
                                      format(game._player.get_health()),
                                   font=("fixedsys", 10))
        self._health_label.grid(row=0, column=2)

        self._food_label = tk.Label(self, text="Food: {}".
                                    format(game._player.get_food()),
                                   font=("fixedsys", 10))
        self._food_label.grid(row=0, column=4)

    def set_health(self, health):
        """updates health based on input"""
        self._health_label.config(text="Health: {}  ".format(health))

    def set_food(self, food):
        """Updates food based on input"""
        self._food_label.config(text="Food: {}".format(food))


class FoodItem(Item):
    """Food item class inherited from superclass Item"""
    def __init__(self, item_id: str, strength: float):
        """Constructor:
        Parameters:
            item_id: the string of the id of the item, e.g. the items name ("apple")
            strength: the strength of the item when used, e.g. if the strength
            of an apple is 2, will increase the health or food by 2 units"""
        super().__init__(item_id)
        self._strength = strength
        self._item_id = item_id

    def get_strength(self):
        """get the strength of the given item"""
        return self._strength

    def place(self):
        """Use the selected item"""
        return [("effect", ('food', self._strength))]
    def can_attack(self):
        """Items are not able to attack"""
        return False


class ToolItem(Item):
    """Tool Item class inherited from superclass Item"""
    def __init__(self, item_id, tool_type, durability):
        """Constructor:
        Parameters:
            item_id (str): the identification of the tool (name)
            tool_type (str): the material that the tool is made out of
            durability (float): the durability of the material"""
        super().__init__(item_id)
        self._id = f'{tool_type}_{item_id}'
        self._tool_type = tool_type
        self._durability = durability
        self._max_durability = durability

    def get_type(self):
        """gets the type of material and type of tool"""
        return self._tool_type

    def get_durability(self):
        """returns the durability of the tool"""
        return self._durability

    def get_max_durability(self):
        """returns the maximum durability of the tool"""
        return self._max_durability

    def can_attack(self):
        """Returns if the tool has run out of durability or not"""
        #if the tool's durability has run out, it will no longer be able
        # to mine or attack
        if self._durability > 0:
            return True
        else:
            return False

    def attack(self, successful):
        """returns if the attack is successful, reduce the durability by one"""
        if not successful:
            self._durability -= 1

    def is_stackable(self):
        """Tools are not stackable"""
        return False


class CraftingTableBlock(ResourceBlock):
    """Crafting table class to create the main source of crafting"""
    def __init__(self, block_id, break_table):
        """Constructor
        Parameters:
            block_id (str): The string identification of the block
            break_table (str): The specific break table of the block"""
        super().__init__(block_id, break_table)
        self._block_id = block_id
        self._block_id = 'crafting_table'
        self._hitpoints = 1

        #break table for crafting table, as it is wood, most logical to only
        # break with an axe and hands
        self._break_table = {
            "hand": (20, True),
            "wood_axe": (18, True),
            "stone_axe": (17, True),
            "iron_axe": (15, True),
            "diamond_axe": (10, True),
            "golden_axe": (10, True),
            }

    def get_block_id(self):
        return self._block_id

    def get_drops(self, luck, correct_item_used):
        """Drops a itself in item form 5 times

        See Block.get_drops for parameters & return"""

        if correct_item_used:
            return [('item', (self._block_id,))]

    def use(self):
        """On right click, open the crafting window"""
        return ('crafting', 'crafting_table')


    def __repr__(self):
        return f"ResourceBlock({self._id!r})"

BLOCK_COLOURS = {
    'diamond': 'blue',
    'dirt': '#552015',
    'stone': 'grey',
    'gold': "#FAFD22",
    'iron': "#BCC7D2",
    'wood': '#723f1c',
    'leaves': 'green',
    'wood_step':'tan2',
    'crafting_table': 'pink',
    'furnace': 'black',
    'wool':'white',
}

ITEM_COLOURS = {
    'diamond': 'blue',
    'dirt': '#552015',
    'gold': "#FAFD22",
    'iron': "#BCC7D2",
    'stone': 'grey',
    'wood': '#723f1c',
    'wood_step':'tan2',
    'apple': '#ff0000',
    'leaves': 'green',
    'crafting_table': 'pink',
    'furnace': 'black',
    'cooked_apple': 'red4',
    'wool': 'white'
}


def load_simple_world(world):
    """Loads blocks into a world

    Parameters:
        world (World): The game world to load with blocks
    """
    block_weights = [
        (100, 'dirt'),
        (30, 'stone'),
        (10, "iron"),
        (3, "gold"),
        (1, "diamond")
    ]

    cells = {}

    ground = []

    width, height = world.get_grid_size()

    for x in range(width):
        for y in range(height):
            if x < 22:
                if y <= 8:
                    continue
            else:
                if x + y < 30:
                    continue

            ground.append((x, y))

    weights, blocks = zip(*block_weights)
    kinds = random.choices(blocks, weights=weights, k=len(ground))

    for cell, block_id in zip(ground, kinds):
        cells[cell] = create_block(block_id)

    trunks = [(3, 8), (3, 7), (3, 6), (3, 5)]

    for trunk in trunks:
        cells[trunk] = create_block('wood')

    leaves = [(4, 3), (3, 3), (2, 3), (4, 2), (3, 2), (2, 2), (4, 4), (3, 4), (2, 4)]

    for leaf in leaves:
        cells[leaf] = create_block('leaf')

    for cell, block in cells.items():
        # cell -> box
        i, j = cell

        world.add_block_to_grid(block, i, j)

    world.add_block_to_grid(create_block("mayhem", 0), 14, 8)

    world.add_mob(Bird("friendly_bird", (12, 12)), 400, 100)
    world.add_mob(Sheep("sheep", (50, 40)), 400, 100)


CRAFTING_RECIPES_2x2 = [
    (
        (
            (None, 'wood'),
            (None, 'wood')
        ),
        Stack(create_item('stick'), 4)
    ),
    (
        (
            ('stick', 'stick'),
            (None, 'stick')
        ),
        Stack(create_item('boomerang'), 1)
    ),
    (
        (
            ('stick', None),
            (None, 'stick')
        ),
        Stack(create_item('big_stick'), 1)
    ),
    (
        (
            ('wood', 'wood'),
            ('wood', 'wood')
        ),
        Stack(create_item('crafting_table'), 1)
    ),
    (
        (
            ('wool', None),
            (None, None)
        ),
        Stack(create_item('cloth'), 4)
    ),
    (
        (
            ('wood', None),
            ('wood', 'wood')
        ),
        Stack(create_item('wood_step'), 1)
    )


]

#3x3 Crafting Recipes
CRAFTING_RECIPES_3x3 = {
    (
        (
            (None, None, None),
            (None, 'wood', None),
            (None, 'wood', None)
        ),
        Stack(create_item('stick'), 16)
    ),
    (
        (
            ('wood', 'wood', 'wood'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'wood'), 1)
    ),
    (
        (
            ('stone', 'stone', 'stone'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'stone'), 1)
    ),
    (
        (
            ('iron', 'iron', 'iron'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'iron'), 1)
    ),
    (
        (
            ('diamond', 'diamond', 'diamond'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'diamond'), 1)
    ),
    (
        (
            ('gold', 'gold', 'gold'),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('pickaxe', 'gold'), 1)
    ),
    (
        (
            ('wood', 'wood', None),
            ('wood', 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('axe', 'wood'), 1)
    ),
    (
        (
            (None, 'wood', None),
            (None, 'stick', None),
            (None, 'stick', None)
        ),
        Stack(create_item('shovel', 'wood'), 1)
    ),
    (
        (
            (None, 'stone', None),
            (None, 'stone', None),
            (None, 'stick', None)
        ),
        Stack(create_item('sword', 'wood'), 1)
    ),
    (

        (
            ('stone', 'stone', 'stone'),
            ('stone', None, 'stone'),
            ('stone', 'stone', 'stone')
        ),
        Stack(create_item('furnace'), 1)
    ),
    (

        (
            ('wool', None, 'wool'),
            ('wool', 'wool' , 'wool'),
            ('wool', 'wool', 'wool')
        ),
        Stack(create_item('wool_vest'), 1)
    ),
    (

        (
            ('cloth', "cloth", 'cloth'),
            ('cloth', 'cloth', 'cloth'),
            ('cloth', 'cloth', 'cloth')
        ),
        Stack(create_item('blanket'), 1)
    ),

}

CRAFTING_RECIPES_FURNACE = {
    (

        (

            ('apple',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('cooked_apple'), 1)
    ),
    (

        (

            ('cooked_apple',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('burnt_apple'), 1)
    ),
    (

        (

            ('wood',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('charcoal'), 4)
    ),
    (

        (

            ('charcoal',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('diamond'), 1)
    ),
    (

        (

            ('blanket',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('heated_blanket'), 1)
    ),
    (

        (

            ('stick',),
            (None,),
            ('wood',)

        ),
        Stack(create_item('fire_stick'), 1)
    ),

}


class Ninedraft:
    """High-level app class for Ninedraft, a 2d sandbox game"""

    def __init__(self, master):
        """Constructor

        Parameters:
            master (tk.Tk): tkinter root widget
        """

        self._master = master
        #1.1 - Title
        master.title("Ninedraft")
        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)

        load_simple_world(self._world)

        self._player = Player()
        self._world.add_player(self._player, 250, 150)

        self._world.add_collision_handler("player", "item", on_begin=self.
                                          _handle_player_collide_item)

        self._hot_bar = SelectableGrid(rows=1, columns=10)
        self._hot_bar.select((0, 0))

        starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item("apple"), 4),
            Stack(create_item('crafting_table'),1),
            Stack(create_item('furnace'), 1),
            Stack(create_item('pickaxe', 'diamond'), 1),
            Stack(create_item("axe", "diamond"),1)

        ]

        for i, item in enumerate(starting_hotbar):
            self._hot_bar[0, i] = item

        self._hands = create_item('hands')

        starting_inventory = [
            ((1, 5), Stack(create_item('dirt'), 10)),
            ((0, 2), Stack(create_item('wood'), 10)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack

        self._crafting_window = None
        self._master.bind_all("e",
                          lambda e: self.run_effect(('crafting', 'basic')))

        self._view = GameView(master, self._world.get_pixel_size(),
                              WorldViewRouter_new(BLOCK_COLOURS, ITEM_COLOURS))
        self._view.pack()

        # Task 1.2 Mouse Controls: Bind mouse events here
        self._view.bind("<Motion>", self._mouse_move)
        self._view.bind("<Button-1>", self._left_click)
        #button-2 for functionailty with mac/other computers
        self._view.bind('<Button-2>', self._right_click)
        #button-3 is right click on Windows
        self._view.bind("<Button-3>", self._right_click)
        # tkinter <leave> handles when cursor leaves the widget
        self._view.bind("<Leave>", self._mouse_leave)


        # Task 1.3: Create instance of StatusView here
        self._status_view = StatusView(master, self)
        self._status_view.pack(side=tk.TOP)


        self._hot_bar_view = ItemGridView(master, self._hot_bar.get_size())
        self._hot_bar_view.pack(side=tk.TOP, fill=tk.X)

        # Task 1.5 Keyboard Controls: Bind to space bar for jumping here
        self._master.bind("<space>", lambda e: self._jump())

        self._master.bind("a", lambda e: self._move(-1, 0))
        self._master.bind("<Left>", lambda e: self._move(-1, 0))
        self._master.bind("d", lambda e: self._move(1, 0))
        self._master.bind("<Right>", lambda e: self._move(1, 0))
        self._master.bind("s", lambda e: self._move(0, 1))
        self._master.bind("<Down>", lambda e: self._move(0, 1))

        # Task 1.5 Keyboard Controls: Bind numbers to hotbar activation here
        for key in range(1, 10):
            self._master.bind(str(key), lambda e, key = key:
            self._activate_item(key-1))
        # 0 key gets confused with front of list, defined seperately.
        self._master.bind("0", lambda e: self._hot_bar.toggle_selection((0, 9)))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        #Utilizing the tkinter menubar
        self.menubar = tk.Menu(master)
        master.config(menu=self.menubar)
        #tearoff removes dashed line at top of dropdown box
        file_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Exit", command=self._popupmsg_exit)
        file_menu.add_command(label="New Game", command=self._popupmsg_restart)

        self._target_in_range = False

        self._target_position = 0, 0

        self.redraw()

        self.step()

    def _popupmsg_exit(self):
        """Display exit popup box on button click"""
        response = messagebox.askyesno("Exit?", "Do you want to exit?")
        if response:
            #Destroy widget
            self._master.destroy()

    def _popupmsg_restart(self):
        """Display restart popup box on button click"""
        response = messagebox.askyesno("Restart?", "Do you want to restart?")
        if response:
            #run restart
            self._restart()

    def _popupmsg_death(self):
        """Display restart popup box on player death"""
        response = messagebox.askyesno("You kicked the bucket!",
                                       "Do you want to respawn?")
        if response:
            #restart game
            self._restart()
        else:
            #close game
            self._master.destroy()

    def redraw(self):
        """Continuosly refresh and redraw all items in world in accordance with
        the step() function"""
        self._view.delete(tk.ALL)

        # physical things
        self._view.draw_physical(self._world.get_all_things())

        target_x, target_y = self._target_position
        target = self._world.get_block(target_x, target_y)
        cursor_position = self._world.grid_to_xy_centre(*self._world.xy_to_grid
        (target_x, target_y))

        # Task 1.2 Mouse Controls: Show/hide target here
        if self._target_in_range:
            self._view.show_target(self._player.get_position(), cursor_position)
        else:
            self._view.hide_target()

        # Task 1.3 StatusView: Update StatusView values here
        self._status_view.set_health(self._player.get_health())
        self._status_view.set_food(self._player.get_food())

        # hot bar
        self._hot_bar_view.render(self._hot_bar.items(), self._hot_bar.
                                  get_selected())

    def step(self):
        """Essentially the clock of the game, refreshes after every 15 ticks"""
        data = GameData(self._world, self._player)
        self._world.step(data)
        self.redraw()

        # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary
        # ...
        if self._player.get_health() <= 0:
            self._popupmsg_death()

        self._master.after(15, self.step)

    def _move(self, dx, dy):
        """Changes player's velocity based on the movement input"""
        self.check_target()
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))


    def _jump(self):
        """Makes the player move up if space bar is pressed"""
        self.check_target()
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        # X velocity is set to slow down the player, Y velocity jumps 200 units up
        self._player.set_velocity((velocity.x * 0.55, velocity.y + -200))


    def mine_block(self, block, x, y):
        """When a player left clicks on the block with the right tool, it will
        break, disappearing and dropping items of themselves"""
        luck = random.random()

        active_item, effective_item = self.get_holding()

        was_item_suitable, was_attack_successful = block.mine(effective_item,
                                                              active_item, luck)

        effective_item.attack(was_attack_successful)

        if block.is_mined():
            # Task 1.2 Mouse Controls: Reduce the player's food/health appropriately

            if self._player.get_food() > 0:
                self._player.change_food(-0.5)
                self._status_view.set_food(self._player.get_food())
            else:
                self._player.change_health(-0.5)
                self._status_view.set_health(self._player.get_health())

            # Task 1.2 Mouse Controls: Remove the block from the world &
            # get its drops
            drops = block.get_drops(luck, was_item_suitable)
            self._world.remove_block(block)

            if not drops:
                return

            x0, y0 = block.get_position()

            for i, (drop_category, drop_types) in enumerate(drops):
                print(f'Dropped {drop_category}, {drop_types}')

                if drop_category == "item":
                    physical = DroppedItem(create_item(*drop_types))

                    # this is so bleh
                    x = x0 - BLOCK_SIZE // 2 + 5 + (i % 3) * 11 + random.randint(0, 2)
                    y = y0 - BLOCK_SIZE // 2 + 5 + ((i // 3) % 3) * 11 + random.randint(0, 2)

                    self._world.add_item(physical, x, y)
                elif drop_category == "block":
                    self._world.add_block(create_block(*drop_types), x, y)
                else:
                    raise KeyError(f"Unknown drop category {drop_category}")

    def get_holding(self):
        """Returns the active item that the player is holding"""
        active_stack = self._hot_bar.get_selected_value()
        active_item = active_stack.get_item() if active_stack else self._hands

        effective_item = active_item if active_item.can_attack() else self._hands

        return active_item, effective_item

    def check_target(self):
        """Checks where the cursor is in the game window"""
        # select target block, if possible
        active_item, effective_item = self.get_holding()

        pixel_range = active_item.get_attack_range() * self._world.get_cell_expanse()

        self._target_in_range = positions_in_range(self._player.get_position(),
                                                   self._target_position,
                                                   pixel_range)

    def _mouse_move(self, event):
        """Tracks the mouse movement in the game window"""
        #trace the mouse movement
        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        """Checks if the mouse has left the widget, which then hides the target"""
        # if target is out of range, return false
        self._target_in_range = False

    def _restart(self):
        """When the player dies, or the restart button is pressed, run this
        function"""
        # Restart function, essentially resets all of the __init__ values of
        # Ninedraft

        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)

        load_simple_world(self._world)

        self._player = Player()
        self._world.add_player(self._player, 250, 150)
        self._status_view.set_health(self._player.get_health())
        self._status_view.set_food(self._player.get_food())

        self._hot_bar = SelectableGrid(rows=1, columns=10)
        self._hot_bar.select((0, 0))

        starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item('apple'), 4)
        ]

        for i, item in enumerate(starting_hotbar):
            self._hot_bar[0, i] = item

        self._hands = create_item('hands')

        starting_inventory = [
            ((1, 5), Stack(Item('dirt'), 10)),
            ((0, 2), Stack(Item('wood'), 10)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack
        self._world.add_collision_handler("player", "item", on_begin=self.
                                          _handle_player_collide_item)

    def _left_click(self, event):
        """Handles the events and effects when the left click button is pressed"""
        # Invariant: (event.x, event.y) == self._target_position
        #  => Due to mouse move setting target position to cursor
        x, y = self._target_position

        if self._target_in_range:
            block = self._world.get_block(x, y)

            if block:
                self.mine_block(block, x, y)
            else:
                mobs = self._world.get_mobs(x, y, max_distance=(2**5))
                for mob in mobs:
                    effect = mob.attack(0)
                    if not effect:
                        continue
                    x0, y0 = mob.get_position()

                    for i, (drop_category, drop_types) in enumerate(effect):
                        print(f'Dropped {drop_category}, {drop_types}')

                        if drop_category == "item":
                            physical = DroppedItem(create_item(*drop_types))
                            x = x0 - BLOCK_SIZE // 2 + 5 + (
                                       i % 3) * 11 + random.randint(0, 2)
                            y = y0 - BLOCK_SIZE // 2 + 5 + (
                                        (i // 3) % 3) * 11 + random.randint(
                                0, 2)

                            self._world.add_item(physical, x, y)

    def _trigger_crafting(self, craft_type):
        """Handles the event when a certain type of crafting is called by pressing
        'e' or right clicking on the furnace or the crafting table"""

        print(f"Crafting with {craft_type}")
        crafter = None
        title = None

        if craft_type == "basic":
            crafter = GridCrafter(CRAFTING_RECIPES_2x2)
            title = "Basic Crafting"
            self._crafting_window = CraftingWindow(self._master, title,
                                                   self._hot_bar,
                                                   self._inventory,
                                                   crafter)
        if craft_type == "crafting_table":
            crafter = GridCrafter(CRAFTING_RECIPES_3x3, rows=3, columns=3)
            title = "Crafting Table"
            self._crafting_window = CraftingWindow(self._master, title,
                                                   self._hot_bar,
                                                   self._inventory,
                                                   crafter)
        if craft_type == 'furnace':
            crafter = GridCrafter(CRAFTING_RECIPES_FURNACE, rows=3, columns=1)
            title = "Smelting"
            self._crafting_window = AdvancedCraftingWindow(self._master, title,
                                               self._hot_bar, self._inventory,
                                               crafter)

    def run_effect(self, effect):
        """Executes the effect given to the function
        i.e. run crafting, update health or food"""
        if len(effect) == 2:
            if effect[0] == "crafting":
                craft_type = effect[1]

                if craft_type == "basic":
                    print("Can't craft much on a 2x2 grid :/")

                elif craft_type == "crafting_table":
                    print("Let's get our kraft® on! King of the brands")
                elif craft_type == "furnace":
                    print("It's gettin hot in here...")
                # Allow crafting window to toggle off and on
                if self._crafting_window == None:
                    self._trigger_crafting(craft_type)
                else:
                    self._crafting_window.destroy()
                    self._crafting_window = None

                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                if self._player.get_food() >= self._player.get_max_food():
                    print(f"Gaining {strength} {'health'}!")
                    getattr(self._player, f"change_{'health'}")(strength)
                else:
                    print(f"Gaining {strength} {stat}!")
                    getattr(self._player, f"change_{stat}")(strength)
                return

        raise KeyError(f"No effect defined for {effect}")

    def _right_click(self, event):
        """Handles the event when the right mouse button is pressed"""
        print("Right click")

        x, y = self._target_position
        target = self._world.get_thing(x, y)

        if target:
            # use this thing
            print(f'using {target}')
            effect = target.use()
            print(f'used {target} and got {effect}')

            if effect:
                self.run_effect(effect)

        else:
            # place active item
            selected = self._hot_bar.get_selected()

            if not selected:
                return

            stack = self._hot_bar[selected]
            drops = stack.get_item().place()

            stack.subtract(1)
            if stack.get_quantity() == 0:
                # remove from hotbar
                self._hot_bar[selected] = None

            if not drops:
                return

            # handling multiple drops would be somewhat finicky, so prevent it
            if len(drops) > 1:
                raise NotImplementedError("Cannot handle dropping more than 1 thing")

            drop_category, drop_types = drops[0]

            x, y = event.x, event.y

            if drop_category == "block":
                existing_block = self._world.get_block(x, y)

                if not existing_block:
                    self._world.add_block(create_block(drop_types[0]), x, y)
                else:
                    raise NotImplementedError(
                        "Automatically placing a block nearby if the target cell is full is not yet implemented")

            elif drop_category == "effect":
                self.run_effect(drop_types)

            else:
                raise KeyError(f"Unknown drop category {drop_category}")

    def _activate_item(self, index):
        """Selects the desired item in the hotbar"""
        print(f"Activating {index}")

        self._hot_bar.toggle_selection((0, index))

    def _handle_player_collide_item(self, player: Player, dropped_item: DroppedItem, data,
                                    arbiter: pymunk.Arbiter):
        """Callback to handle collision between the player and a (dropped) item. If the player has sufficient space in
        their to pick up the item, the item will be removed from the game world.

        Parameters:
            player (Player): The player that was involved in the collision
            dropped_item (DroppedItem): The (dropped) item that the player collided with
            data (dict): data that was added with this collision handler (see data parameter in
                         World.add_collision_handler)
            arbiter (pymunk.Arbiter): Data about a collision
                                      (see http://www.pymunk.org/en/latest/pymunk.html#pymunk.Arbiter)
                                      NOTE: you probably won't need this
        Return:
             bool: False (always ignore this type of collision)
                   (more generally, collision callbacks return True iff the collision should be considered valid; i.e.
                   returning False makes the world ignore the collision)
        """

        item = dropped_item.get_item()

        if self._hot_bar.add_item(item):
            print(f"Added 1 {item!r} to the hotbar")
        elif self._inventory.add_item(item):
            print(f"Added 1 {item!r} to the inventory")
        else:
            print(f"Found 1 {item!r}, but both hotbar & inventory are full")
            return True

        self._world.remove_item(dropped_item)
        return False

# Task 1.1 App class: Add a main function to instantiate the GUI here


def main():
    """Executes the Ninedraft class and opens the game window"""
    root = tk.Tk()
    app = Ninedraft(root)
    # display pop up message if the close button was pressed
    root.protocol("WM_DELETE_WINDOW", app._popupmsg_exit)

    root.mainloop()


if __name__ == "__main__" :
    main()
Simple 2d world where the player can interact with the items in the world. # Task 3/Post-grad only: # Class to hold game data that is passed to each thing's step function # Normally, this class would be defined in a separate file # so that type hinting could be used on PhysicalThing & its # subclasses, but since it will likely need to be extended # for these tasks, we have defined it here Subclass of WorldViewRouter, used to change the routing table in order     to draw additional mobs # (class, method name) # draws a w white, rectangular sheep that routes to the Sheep class in # sheep.py (Block) Creates a block (this function can be thought of as a block      factory)      Parameters:         block_id (*tuple): N-length tuple to uniquely identify the block,         often comprised of strings, but not necessarily (arguments are grouped         into a single tuple)      Examples:         >>> create_block("leaf")         LeafBlock()         >>> create_block("stone")         ResourceBlock('stone')         >>> create_block("mayhem", 1)         TrickCandleFlameBlock(1) (Item) Creates an item (this function can be thought of as a item     factory)      Parameters:         item_id (*tuple): N-length tuple to uniquely identify the item,         often comprised of strings, but not necessarily (arguments are grouped         into a single tuple)      Examples:         >>> create_item("dirt")         BlockItem('dirt')         >>> create_item("hands")         HandItem('hands')         >>> create_item("pickaxe", "stone")  # *without* Task 2.1.2 implemented         Traceback (most recent call last):         ...         NotImplementedError: "Tool creation is not yet handled"         >>> create_item("pickaxe", "stone")  # *with* Task 2.1.2 implemented         ToolItem('stone_pickaxe') # Task 1.4 Basic Items: Create wood & stone here # Task 1.3: Implement StatusView class here Class inherited from Frame in order to display the user's health     and food status Constructor:         Parameters:             master: the tkinter root widg  et             game: the instance of the Ninedraft class updates health based on input Updates food based on input Food item class inherited from superclass Item Constructor:         Parameters:             item_id: the string of the id of the item, e.g. the items name ("apple")             strength: the strength of the item when used, e.g. if the strength             of an apple is 2, will increase the health or food by 2 units get the strength of the given item Use the selected item Items are not able to attack Tool Item class inherited from superclass Item Constructor:         Parameters:             item_id (str): the identification of the tool (name)             tool_type (str): the material that the tool is made out of             durability (float): the durability of the material gets the type of material and type of tool returns the durability of the tool returns the maximum durability of the tool Returns if the tool has run out of durability or not #if the tool's durability has run out, it will no longer be able # to mine or attack returns if the attack is successful, reduce the durability by one Tools are not stackable Crafting table class to create the main source of crafting Constructor         Parameters:             block_id (str): The string identification of the block             break_table (str): The specific break table of the block #break table for crafting table, as it is wood, most logical to only # break with an axe and hands Drops a itself in item form 5 times          See Block.get_drops for parameters & return On right click, open the crafting window Loads blocks into a world      Parameters:         world (World): The game world to load with blocks # cell -> box #3x3 Crafting Recipes High-level app class for Ninedraft, a 2d sandbox game Constructor          Parameters:             master (tk.Tk): tkinter root widget #1.1 - Title # Task 1.2 Mouse Controls: Bind mouse events here #button-2 for functionailty with mac/other computers #button-3 is right click on Windows # tkinter <leave> handles when cursor leaves the widget # Task 1.3: Create instance of StatusView here # Task 1.5 Keyboard Controls: Bind to space bar for jumping here # Task 1.5 Keyboard Controls: Bind numbers to hotbar activation here # 0 key gets confused with front of list, defined seperately. # Task 1.6 File Menu & Dialogs: Add file menu here #Utilizing the tkinter menubar #tearoff removes dashed line at top of dropdown box Display exit popup box on button click #Destroy widget Display restart popup box on button click #run restart Display restart popup box on player death #restart game #close game Continuosly refresh and redraw all items in world in accordance with         the step() function # physical things # Task 1.2 Mouse Controls: Show/hide target here # Task 1.3 StatusView: Update StatusView values here # hot bar Essentially the clock of the game, refreshes after every 15 ticks # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary # ... Changes player's velocity based on the movement input Makes the player move up if space bar is pressed # Task 1.2: Update the player's velocity here # X velocity is set to slow down the player, Y velocity jumps 200 units up When a player left clicks on the block with the right tool, it will         break, disappearing and dropping items of themselves # Task 1.2 Mouse Controls: Reduce the player's food/health appropriately # Task 1.2 Mouse Controls: Remove the block from the world & # get its drops # this is so bleh Returns the active item that the player is holding Checks where the cursor is in the game window # select target block, if possible Tracks the mouse movement in the game window #trace the mouse movement Checks if the mouse has left the widget, which then hides the target # if target is out of range, return false When the player dies, or the restart button is pressed, run this         function # Restart function, essentially resets all of the __init__ values of # Ninedraft Handles the events and effects when the left click button is pressed # Invariant: (event.x, event.y) == self._target_position #  => Due to mouse move setting target position to cursor Handles the event when a certain type of crafting is called by pressing         'e' or right clicking on the furnace or the crafting table Executes the effect given to the function         i.e. run crafting, update health or food # Allow crafting window to toggle off and on Handles the event when the right mouse button is pressed # use this thing # place active item # remove from hotbar # handling multiple drops would be somewhat finicky, so prevent it Selects the desired item in the hotbar Callback to handle collision between the player and a (dropped) item. If the player has sufficient space in         their to pick up the item, the item will be removed from the game world.          Parameters:             player (Player): The player that was involved in the collision             dropped_item (DroppedItem): The (dropped) item that the player collided with             data (dict): data that was added with this collision handler (see data parameter in                          World.add_collision_handler)             arbiter (pymunk.Arbiter): Data about a collision                                       (see http://www.pymunk.org/en/latest/pymunk.html#pymunk.Arbiter)                                       NOTE: you probably won't need this         Return:              bool: False (always ignore this type of collision)                    (more generally, collision callbacks return True iff the collision should be considered valid; i.e.                    returning False makes the world ignore the collision) # Task 1.1 App class: Add a main function to instantiate the GUI here Executes the Ninedraft class and opens the game window # display pop up message if the close button was pressed