예제 #1
0
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
        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("food"), 4),
            Stack(create_item("pickaxe", "stone"), 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("e", lambda e: self.run_effect(
            ('crafting', 'basic')))

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

        # Task 1.2 Mouse Controls: Bind mouse events here
        self._master.bind('<Motion>', self._mouse_move)
        self._master.bind('<Button-1>', self._left_click)
        self._master.bind('<Button-3>', self._right_click)

        # Task 1.3: Create instance of StatusView here
        self._stats_bar = StatusView(self._player.get_health(),
                                     self._player.get_food())

        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>", 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(10):
            print("Bound", key, "to", key)
            self._master.bind(str(key),
                              lambda e, key=key: self._activate_item(key - 1))
        self._master.bind("0", lambda e, key=key: self._activate_item(9))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        game_menu = tk.Menu(root)
        file_menu = tk.Menu(game_menu)
        file_menu.add_command(label="New Game", command=self._restart)
        file_menu.add_command(label="Exit", command=self._exit)
        game_menu.add_cascade(label="File", menu=file_menu)

        root.protocol("WM_DELETE_WINDOW", self._exit)

        root.config(menu=game_menu)

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self._crafting_open = False

        self.step()

    def _exit(self):
        """Checks if the user really wants to exit, and shuts down the game if true"""

        if tk.messagebox.askquestion(
                title="Exit game",
                message="Are you sure you want to exit Ninedraft?") == "yes":
            print("Shutting down...")
            root.destroy()

    def _restart(self):
        """Restarts the game"""

        print("Restarting")
        for thing in self._world.get_all_things():
            self._world.remove_thing(thing)
        load_simple_world(self._world)
        self._player = Player()
        self._world.add_player(self._player, 250, 150)

        for position, item in self._hot_bar.items():
            self._hot_bar.pop(position)

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

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

        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

    def redraw(self):
        """Redraws the game and UI every 'tick' (step) of the game"""
        self._view.delete(tk.ALL)

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

        # target
        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
        self.check_target()
        if self._target_in_range:
            self._view.show_target(self._player.get_position(),
                                   cursor_position)

        # Task 1.3 StatusView: Update StatusView values here
        self._stats_bar.set_health(self._player.get_health())
        self._stats_bar.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):
        """Steps game time forwards by one 'tick'"""
        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:
            death_message = tk.messagebox.askquestion(
                title="You died!", message="You died! Do you want to restart?")
            if death_message == "yes":
                self._restart()
            else:
                self._exit()

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

    def _move(self, dx, dy):
        """Moves the player"""

        self.check_target()
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self, event):
        """Causes the player to jump upwards"""

        self.check_target()
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        x_velocity, y_velocity = velocity
        self._player.set_velocity((x_velocity * 1.2, y_velocity - 180))

    def mine_block(self, block, x, y):
        """Mines a block, removing it from the game and dropping one or more items"""

        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(-1)
                print("Food now at", self._player.get_food())
            else:
                self._player.change_health(-0.5)
                print("Health now at", 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):
        """Gets the item in the player's 'hand', the active item
        
        Returns (tuple): The item active, and if it is effective against the targeted block"""

        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 if the target is in range"""

        # 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):
        """Updates the target whenever the mouse moves"""

        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        """Triggers when mouse leaves the game window, and makes the target out of range"""

        self._target_in_range = False

    def _left_click(self, event):
        """Processes a left click"""

        # 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)

    def _trigger_crafting(self, craft_type):
        """Activates the crafting window for a grid of size"""

        print(f"Crafting with {craft_type}")
        # self._crafting_window.protocol("WM_DELETE_WINDOW", self._exit)
        if craft_type == "basic":
            if self._crafting_open:
                self._crafting_window.destroy()
                self._crafting_open = False
            else:
                self.crafter = GridCrafter(CRAFTING_RECIPES_2x2)
                self._crafting_window = CraftingWindow(root,
                                                       "Inventory & Crafting",
                                                       self._hot_bar,
                                                       self._inventory,
                                                       self.crafter)
                self._crafting_window.bind(
                    "e", lambda e: self.run_effect(('crafting', 'basic')))

                self._crafting_open = True
        else:
            if self._crafting_open:
                print("open")
                self._crafting_window.destroy()
                self._crafting_open = False
            else:
                self.crafter == GridCrafter(CRAFTING_RECIPES_3x3,
                                            rows=3,
                                            columns=3)
                self._crafting_window = CraftingWindow(
                    root, "Crafting Table", self._hot_bar, self._inventory,
                    GridCrafter(CRAFTING_RECIPES_3x3, rows=3, columns=3))
                self._crafting_window.bind(
                    "e", lambda e: self.run_effect(('crafting', 'basic')))
                self._crafting_open = True

    def run_effect(self, effect):
        """Applies the effect given
        
        Parameters:
            Effect (str): The effect to apply"""

        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")

                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                if self._player.get_health() < self._player.get_max_health():
                    print("Health")
                    stat = "health"

                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):
        """Processes a right click"""

        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:
            if self._target_in_range:
                # 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):
        """Activates the item at index
        
        Parameters:
            index (int): The index of the item to activate"""

        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
예제 #2
0
class GridCrafterView(tk.Frame):
    """A tkinter widget used to display crafting with a grid as input and a single cell as output"""
    def __init__(self, master, input_size):
        """Constructor

        Parameters:
            master (tk.Frame | tk.Toplevel | tk.Tk): Tkinter parent widget
            input_size (tuple<int, int>):
                    The (row, column) size of the grid crafter's input grid
        """
        super().__init__(master)
        self._master = master
        self._crafting_type = 'basic'

        # Task 2.2 Crafting: Create widgets here
        # ...
        print(input_size)
        if input_size == (2, 1):
            self._crafting_type = 'furnace'

            # Frame for the smelting item, fuel and fire image
            self._furnace_frame = tk.Frame(self)

            # Create one grid for the item to be smelted
            self._smelt_grid = Grid(rows=1, columns=1)
            self._smelt_grid_view = ItemGridView(self._furnace_frame, (1, 1))
            self._smelt_grid_view.pack(side=tk.TOP, expand=True)

            # Flame
            self._flame_img = ImageTk.PhotoImage(Image.open('flame.png'))
            self._flame_label = tk.Label(self._furnace_frame,
                                         image=self._flame_img)
            self._flame_label.pack(side=tk.TOP)

            # Create on grid below the fire for the fuel item
            self._fuel_grid = Grid(rows=1, columns=1)
            self._fuel_grid_view = ItemGridView(self._furnace_frame, (1, 1))
            self._fuel_grid_view.pack(side=tk.TOP, expand=True)

            # Render and pack
            self._smelt_grid_view.render(self._smelt_grid.items(), None)
            self._fuel_grid_view.render(self._fuel_grid.items(), None)
            self._furnace_frame.pack(side=tk.LEFT, expand=True)
        else:  # Basic crafting grid for 2x2 or crafting table
            self._input_grid = Grid(rows=input_size[0], columns=input_size[1])
            self._input_grid_view = ItemGridView(self,
                                                 self._input_grid.get_size())
            self._input_grid_view.pack(side=tk.LEFT, expand=True)
            self._input_grid_view.render(self._input_grid.items(), None)

        # Create output cell and craft button, the same for all cases
        self._output_cell = Grid(rows=1, columns=1)
        self._output_cell_view = ItemGridView(self,
                                              self._output_cell.get_size())
        self._output_cell_view.pack(side=tk.RIGHT, expand=True)

        self._craft_button = tk.Button(self, text='=>Craft=>')
        self._craft_button.pack(side=tk.LEFT)

        self._output_cell_view.render(self._output_cell.items(), None)

    def render(self, key_stack_pairs, selected):
        """Renders the stacks at appropriate cells, as determined by 'key_stack_pairs'

        Parameters:
            key_stack_pairs (tuple<*, Stack>):
                    (key, stack) pairs, where each stack should be drawn at the cell
                    corresponding to key
            selected (*): The key that is currently selected, or None if no key is selected
        """
        # Task 2.2 Crafting: Render widgets here
        # ...
        #print(f"{selected} is selected")
        #print(key_stack_pairs)
        for key, stack in key_stack_pairs:
            #print(f"Redrawing {stack} at {key}")
            if key == "output":
                # Task 2.2 Crafting: Draw output cell
                # ...
                self._output_cell_view.draw_cell((0, 0),
                                                 stack,
                                                 active=selected == key)
            elif self._crafting_type == 'furnace' and key == (0, 0):
                self._smelt_grid_view.draw_cell((0, 0),
                                                stack,
                                                active=selected == key)
            elif self._crafting_type == 'furnace' and key == (1, 0):
                self._fuel_grid_view.draw_cell((0, 0),
                                               stack,
                                               active=selected == key)
            elif self._crafting_type == 'basic':
                # Task 2.2 Crafting: Draw input cells
                # ...
                self._input_grid_view.draw_cell(key,
                                                stack,
                                                active=selected == key)

    def bind_for_id(self, event, callback):
        """Binds callback to tkinter mouse event

        Callback accept parameters: callback(key, event), where
          - key (*) is the key of the cell clicked, etc.
          - mouse_event (tk.MouseEvent) is the original mouse event from tkinter
        """
        if event not in TK_MOUSE_EVENTS:
            return

        # Task 2.2 Crafting: Bind to tkinter widgets here
        # When a cell is clicked, we need to call the callback. Tkinter's bind does
        # this for us, but not exactly how we want. Tkinter bound callbacks have a single
        # parameter, the mouse event containing useful information about the click (i.e.
        # the x & y coordinates)
        #
        # However, x & y coordinates aren't that useful. The class controlling this widget
        # (i.e. CraftingWindow) only needs to know which cell was clicked. It's not
        # concerned with where it was clicked, just that it was. This is so it can easily
        # interact with the crafter model (i.e. GridCrafter) and move stacks around or
        # select/deselect them.
        #
        # To integrate with CraftingWindow, you will need to transform the callback
        # provided to tk.bind, exactly as is done in ItemGridView.bind_for_id, except
        # the first argument may not necessarily be a (row, column) position, but
        # simply an arbitrary key (for basic 2x2 crafting, the 5 keys are:
        #    "output", (0, 0), (0, 1), (1, 0), (1, 1)
        #
        # ...
        if self._crafting_type == 'basic':
            self._input_grid_view.bind(
                event, lambda e: callback(
                    self._input_grid_view.xy_to_grid((e.x, e.y)), e))
        elif self._crafting_type == 'furnace':
            self._smelt_grid_view.bind(event, lambda e: callback((0, 0), e))
            self._fuel_grid_view.bind(event, lambda e: callback((1, 0), e))
        self._output_cell_view.bind(event, lambda e: callback("output", e))
        self._craft_button.bind(event, lambda e: callback('craft', 'craft'))
예제 #3
0
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
        self._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("pickaxe", "stone"), 1)
        ]

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

        self._hands = create_item("hands")
        self._weapon = create_item("pickaxe", "stone")

        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._crafting_window = None
        self._master.bind("e", lambda e: self.run_effect(
            ('crafting', 'basic')))

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

        self._master.bind("<Motion>", self._mouse_move)
        self._master.bind("<Button-1>", self._left_click)
        self._master.bind("<Button-2>", self._right_click)

        # Task 1.3: Create instance of StatusView here
        # ...

        self._status_view = StatusView(master)
        self._status_view.pack()
        self._status_view.set_food_value(self._player.get_food())
        self._status_view.set_health_value(self._player.get_health())

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

        self._master.bind("<space>", lambda e: self._jump())  #2019.5.22改
        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))

        self._master.bind("1", lambda e: self._activate_item(0))
        self._master.bind("2", lambda e: self._activate_item(1))
        self._master.bind("3", lambda e: self._activate_item(2))
        self._master.bind("4", lambda e: self._activate_item(3))
        self._master.bind("5", lambda e: self._activate_item(4))
        self._master.bind("6", lambda e: self._activate_item(5))
        self._master.bind("7", lambda e: self._activate_item(6))
        self._master.bind("8", lambda e: self._activate_item(7))
        self._master.bind("9", lambda e: self._activate_item(8))
        self._master.bind("0", lambda e: self._activate_item(9))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        # ...
        menu = MenuBar(master, [("File", {
            "New Game": self._reset,
            "Exit": self._close
        })])

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def _reset(self):
        self._master.destroy()
        main()

    def _close(self):
        """ Exit the drawing application """
        result = tk.messagebox.askquestion(title="Quiz Window",
                                           message="Do you really wanna quiz?")
        if (result == "yes"):
            self._master.destroy()

    def redraw(self):
        self._view.delete(tk.ALL)

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

        # target
        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))

        #2019.5.22
        if self._target_in_range:
            self._target_position = cursor_position

            self._view.show_target(self._player.get_position(),
                                   self._target_position, cursor_position)
        else:
            self._view.hide_target()

        # Task 1.3 StatusView: Update StatusView values here
        # ...

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

    def step(self):
        data = GameData(self._world, self._player)
        self._world.step(data)
        self.check_target()
        self.redraw()

        # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary
        # ...

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

    def _move(self, dx, dy):
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self):
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + 10, velocity.y - 100))

    def mine_block(self, block, x, y):
        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_health(0)
                self._player.change_food(-2)
                self._status_view.set_food_value(self._player.get_food())

            elif self._player.get_food() == 0:
                self._player.change_food(0)
                self._player.change_health(-2)
                self._status_view.set_health_value(self._player.get_health())

            if self._player.is_dead():
                result = tk.messagebox.askquestion(
                    title="You are dead!", message="Do you want to try again?")
                if (result == "yes"):
                    self._reset()
                else:
                    self._master.destroy()

            # ...

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

            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):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _left_click(self, event):
        # 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)

    def _trigger_crafting(self, craft_type):
        CRAFTING_RECIPES_2x2 = [
            (((None, 'wood'), (None, 'wood')), Stack(create_item("stick"), 4)),
            (((None, 'dirt'), (None, 'wood')), Stack(create_item("stone"), 4)),
            (((None, 'dirt'), (None, 'dirt')), Stack(create_item("wood"), 4)),
            ((("wood", 'dirt'), ("wood", 'dirt')),
             Stack(create_item("apple"), 4))
        ]

        print(f"Crafting with {craft_type}")
        crafter = GridCrafter(CRAFTING_RECIPES_2x2)

        show_crafter = CraftingWindow(self._master, "crafting", self._hot_bar,
                                      self._inventory, crafter)

    def run_effect(self, effect):
        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")

                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                print(f"Gaining {strength} {stat}!")

                getattr(self._player, f"change_{stat}")(strength)
                self._status_view.set_food_value(self._player.get_food())
                self._status_view.set_health_value(self._player.get_health())
                if self._player.get_food(
                ) == 20 and self._player.get_health() < 20:
                    self._player.change_health(strength)
                    self._status_view.set_food_value(self._player.get_food())
                    self._status_view.set_health_value(
                        self._player.get_health())

                return

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

    def _right_click(self, event):
        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):
        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
예제 #4
0
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
        self._world = WorldReporter((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))

        self._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("honey"), 1),
            Stack(create_item("pickaxe", "diamond"), 1),
        ]

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

        self._hands = create_item('hands')

        self._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 self._starting_inventory:
            self._inventory[position] = stack

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

        self._view = GameView(master, self._world.get_pixel_size(),
                              CreateWorld(BLOCK_COLOURS, ITEM_COLOURS))
        # Change WorldViewRouter to my subclass here

        self._view.pack()

        # Task 1.2 Mouse Controls: Bind mouse events here
        # ...
        self._view.bind("<Motion>", self._mouse_move)
        self._view.bind("<Leave>", self._mouse_leave)
        self._master.bind("<Button-1>", self._left_click)
        self._master.bind("<Button-3>", self._right_click)

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

        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 - 1: self._activate_item(key))
        self._master.bind('0', lambda e: self._activate_item(9))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        # ...
        menubar = tk.Menu(self._master)
        self._master.config(menu=menubar)

        file_menu = tk.Menu(menubar)
        menubar.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="New Game", command=self.new_game)
        file_menu.add_command(label="Exit", command=self.exit)

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def new_game(self):
        """Restart the game by reseting the world, player, hotbar and inventory"""
        self._world = WorldReporter((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)

        for i in self._hot_bar:
            self._hot_bar[i] = None

        self._starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item("apple"), 4)
        ]

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

        for i in self._inventory:
            self._inventory[i] = None

        self._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 self._starting_inventory:
            self._inventory[position] = stack

    def exit(self):
        """Exit the application"""
        if messagebox.askyesno("Exit", "Would you like to exit the game?"):
            self._master.destroy()

    def redraw(self):
        self._view.delete(tk.ALL)

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

        # target
        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)

        # 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):
        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.is_dead():
            if messagebox.askyesno("You died",
                                   "Would you like to start again?"):
                self.new_game()
            else:
                self.exit()

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

    def _move(self, dx, dy):
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))
        self.check_target()

    def _jump(self):
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        # ...
        self._player.set_velocity((velocity.x * 0.80, velocity.y - 300))
        self.check_target()

    def mine_block(self, block, x, y):
        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.9)
            else:
                self._player.change_health(-0.5)

            self._world.remove_block(block)

            if block.get_id() == 'hive':
                block_x, block_y = block.get_position()
                for i in range(5):
                    self._world.add_mob(Bee(f"killer_bee{i}", (10, 10)),
                                        block_x, block_y)

            # Task 1.2 Mouse Controls: Get what the block drops.
            # ...
            drops = block.get_drops(luck, was_item_suitable)

            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 attack(self, mob, x, y):
        """Method to attack mobs encountered in the world"""
        luck = random.random()

        active_item, effective_item = self.get_holding()

        was_attack_successful = mob.attacked(effective_item, luck)

        if was_attack_successful:

            # Get damage from TooLItem
            damage = effective_item.attack(True)
            if not damage:  # HandItem returns None, so make that 0 damage
                damage = 0

            # Deal damage to the mob, will return any dropped items
            drops = mob.take_damage(luck, damage)

            if not drops:
                return

            x0, y0 = mob.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):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        self._target_in_range = False

    def _left_click(self, event):
        # 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)
            mobs = self._world.get_mobs(x, y, 15)
            if block:
                self.mine_block(block, x, y)
            elif mobs:
                self.attack(mobs[0], x, y)

    def _trigger_crafting(self, craft_type):
        print(f"Crafting with {craft_type}")
        if craft_type == "crafting_table":
            crafter = GridCrafter(CRAFTING_RECIPES_3x3, rows=3, columns=3)
        elif craft_type == "furnace":
            crafter = GridCrafter(FURNACE_RECIPES_2x1, rows=2, columns=1)
        else:
            crafter = GridCrafter(CRAFTING_RECIPES_2x2)

        self._craft_window = CraftingWindow(self._master, "Crafting",
                                            self._hot_bar, self._inventory,
                                            crafter)
        self._craft_window.bind('e', lambda e: self._craft_window.destroy())

    def run_effect(self, effect):
        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")

                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                if stat == 'food':
                    food_needed = self._player.get_max_food(
                    ) - self._player.get_food()
                    # Food can be increased by total food strength
                    if strength < food_needed:
                        print(f"Gaining {strength} {stat}!")
                        getattr(self._player, f"change_{stat}")(strength)
                    # Food strength is greater than the required food
                    else:
                        # check how much food is needed add it to food to max
                        # food stat
                        health_gain = strength - food_needed
                        print(f"Gaining {food_needed} {stat}!")
                        getattr(self._player, f"change_{stat}")(strength)
                        # put the rest into health
                        stat = 'health'
                        print(f"Gaining {health_gain} {stat}!")
                        getattr(self._player, f"change_{stat}")(strength)
                return

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

    def _right_click(self, event):
        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):
        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
예제 #5
0
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
        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)
        master.title('Ninedraft')
        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"), 20),
            Stack(create_item("pickaxe", "stone"), 1),
            Stack(create_item("diamond"), 20),
            Stack(create_item("wool"), 20),
            Stack(create_item("furnace"), 1),
            Stack(create_item("honey"), 1),
            Stack(create_item("hive"), 1),
            Stack(create_item("bow"), 1),
            Stack(create_item("arrow"), 20)
        ]

        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._crafting_window = None
        self._master.bind("e", lambda e: self.run_effect(
            ('crafting', 'basic')))

        self._view = GameView(master, self._world.get_pixel_size(),
                              NewWorldViewRouter(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("<Leave>", self._mouse_leave)
        self._master.bind("<Button-1>", self._left_click)
        self._view.bind("<Button-3>", self._right_click)

        # Task 1.3: Create instance of StatusView here
        # ...
        self._statusview = StatusView(master, self._player.get_health(),
                                      self._player.get_food())
        self._statusview.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
        # ...
        self._master.bind("1", lambda e: self._hot_bar.select((0, 0)))
        self._master.bind("2", lambda e: self._hot_bar.select((0, 1)))
        self._master.bind("3", lambda e: self._hot_bar.select((0, 2)))
        self._master.bind("4", lambda e: self._hot_bar.select((0, 3)))
        self._master.bind("5", lambda e: self._hot_bar.select((0, 4)))
        self._master.bind("6", lambda e: self._hot_bar.select((0, 5)))
        self._master.bind("7", lambda e: self._hot_bar.select((0, 6)))
        self._master.bind("8", lambda e: self._hot_bar.select((0, 7)))
        self._master.bind("9", lambda e: self._hot_bar.select((0, 8)))
        self._master.bind("0", lambda e: self._hot_bar.select((0, 9)))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        # ...
        menu_bar = tk.Menu(self._master)
        master.config(menu=menu_bar)
        file_menu = tk.Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label='File', menu=file_menu)
        file_menu.add_command(label='New Game', command=self.restart)
        file_menu.add_command(label='Exit', command=self.exit)
        master.protocol("WM_DELETE_WINDOW", self.exit)

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def restart(self):
        """Restarts the game"""

        answer = messagebox.askyesno(
            title='New Game?',
            message='Are you sure you would like to start a new game?')
        if answer:
            for thing in self._world.get_all_things():
                self._world.remove_thing(thing)
            load_simple_world(self._world)
            self._player = Player()
            self._world.add_player(self._player, 250, 150)
            self._hot_bar.select((0, 0))

            starting_hotbar = [
                Stack(create_item("dirt"), 20),
                Stack(create_item("apple"), 20),
                Stack(create_item("pickaxe", "stone"), 1),
                Stack(create_item("diamond"), 20),
                Stack(create_item("wool"), 20),
                Stack(create_item("furnace"), 1),
                Stack(create_item("honey"), 1),
                Stack(create_item("hive"), 1),
                Stack(create_item("bow"), 1),
                Stack(create_item("arrow"), 20)
            ]
            for position, cell in self._hot_bar.items():
                self._hot_bar[position] = None

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

            for position, cell in self._inventory.items():
                self._inventory[position] = None
            starting_inventory = [
                ((1, 5), Stack(Item('dirt'), 10)),
                ((0, 2), Stack(Item('wood'), 10)),
            ]

            for position, stack in starting_inventory:
                self._inventory[position] = stack

    def exit(self):
        """Exits the application"""

        answer = messagebox.askyesno(
            title='Exit', message='Are you sure you want to quit Ninedraft?')
        if answer:
            self._master.destroy()

    def redraw(self):
        self._view.delete(tk.ALL)

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

        # target
        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
        # ...
        self._view.show_target(self._player.get_position(),
                               self._target_position)
        if not self._target_in_range:
            self._view.hide_target()

        # Task 1.3 StatusView: Update StatusView values here
        # ...
        self._statusview.set_health(self._player.get_health())

        self._statusview.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):
        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.is_dead():
            self.restart()

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

    def _move(self, dx, dy):
        self.check_target()
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self):
        self.check_target()
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        # ...
        self._player.set_velocity((velocity.x / 1.5, velocity.y - 150))

    def mine_block(self, block, x, y):
        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 the block has been mined

        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)
            else:
                self._player.change_health(-2.5)

            # Task 1.2 Mouse Controls: Remove the block from the world & get its drops
            # ...
            self._world.remove_item(block)
            if luck < 1:
                drops = block.get_drops(luck, was_item_suitable)
            # Have a look at the World class for removing
            # Have a look at the Block class for getting the drops

            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):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        self._target_in_range = False

    def _left_click(self, event):
        # 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)
            elif "Sheep('sheep')":
                block = WoolBlock("wool", "wood")
                block.get_drops(1, True)

    def _trigger_crafting(self, craft_type):
        print(f"Crafting with {craft_type}")
        CRAFTING_RECIPES_2x2 = [
            (((None, 'wood'), (None, 'wood')), Stack(create_item('stick'), 4)),
            ((('wood', 'wood'), ('wood', 'wood')),
             Stack(create_item('crafting_table'), 1)),
            ((('dirt', 'dirt'), ('dirt', 'dirt')),
             Stack(create_item('wood'), 1)),
            ((('stone', 'stone'), ('stone', 'stone')),
             Stack(create_item('diamond'), 1)),
            ((('apple', 'apple'), ('apple', 'apple')),
             Stack(create_item('honey'), 1)),
        ]

        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)),
            ((('diamond', 'diamond', 'diamond'), (None, 'stick', None),
              (None, 'stick', None)),
             Stack(create_item('pickaxe', 'diamond'), 1)),
            ((('wood', 'wood', None), ('wood', 'stick', None),
              (None, 'stick', None)), Stack(create_item('axe', 'wood'), 1)),
            ((('stone', 'stone', None), ('wood', 'stick', None),
              (None, 'stick', None)), Stack(create_item('axe', 'stone'), 1)),
            (((None, 'wood', None), (None, 'stick', None),
              (None, 'stick', None)), Stack(create_item('shovel', 'wood'), 1)),
            (((None, 'stone', None), (None, 'stick', None),
              (None, 'stick', None)), Stack(create_item('shovel', 'stone'),
                                            1)),
            (((None, 'wood', None), (None, 'wood', None),
              (None, 'stick', None)), Stack(create_item('sword', 'wood'), 1)),
            (((None, 'stone', None), (None, 'stone', None),
              (None, 'stick', None)), Stack(create_item('sword', 'stone'), 1)),
            (((None, None, None), ('wool', 'wool', 'wool'),
              ('wood', 'wood', 'wood')), Stack(create_item('bed'), 1)),
            ((('stone', 'stone', 'stone'), ('stone', None, 'stone'),
              ('stone', 'stone', 'stone')), Stack(create_item('furnace'), 1))
        }

        if craft_type == "basic":
            crafter = GridCrafter(CRAFTING_RECIPES_2x2, 2, 2)
        else:
            crafter = GridCrafter(CRAFTING_RECIPES_3x3, 3, 3)

        self._crafting_window = CraftingWindow(self._master,
                                               craft_type,
                                               hot_bar=self._hot_bar,
                                               inventory=self._inventory,
                                               crafter=crafter)

    def run_effect(self, effect):
        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")

                self._trigger_crafting(craft_type)
                return

            elif effect[0] in ("food", "health"):
                stat, strength = effect

                if self._player.get_food() < self._player._max_food:
                    stat = "food"
                else:
                    stat = "health"

                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):
        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]
            if not stack:
                return
            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):
        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
예제 #6
0
class GridCrafterView(tk.Frame):
    """A tkinter widget used to display crafting with a grid as input and a single cell as output"""
    def __init__(self, master, input_size, mode="normal"):
        """Constructor

        Parameters:
            master (tk.Frame | tk.Toplevel | tk.Tk): Tkinter parent widget
            input_size (tuple<int, int>):
                    The (row, column) size of the grid crafter's input grid
        """
        super().__init__(master)
        # check mode
        if mode not in ["normal", "smelting"]:
            raise NotImplementedError("Not supported mode")
        # Task 2.2 Crafting: Create widgets here
        self._input = SelectableGrid(rows=input_size[0], columns=input_size[1])
        self._input_view = ItemGridView(self, self._input.get_size())
        self._output = SelectableGrid(rows=1, columns=1)
        if mode == "smelting":
            self._button = tk.Button(self, text="=>Smelt=>", height=1)
            im = Image.open('fire.jpg')
            self.img = ImageTk.PhotoImage(im)
            # img = tk.PhotoImage(file="fire.gif")
            self._label = tk.Label(self, image=self.img)
            self._label.pack(side=tk.LEFT)
        else:
            self._button = tk.Button(self, text="=>Craft=>", height=1)
        self._input_view.pack(side=tk.LEFT)
        self._button.pack(side=tk.LEFT)
        self._output_view = ItemGridView(self, (1, 1))
        self._output_view.pack(side=tk.LEFT)
        self._input_view.render(self._input.items(),
                                self._input.get_selected())
        self._output_view.render(self._output.items(),
                                 self._output.get_selected())

    def render(self, key_stack_pairs, selected):
        """Renders the stacks at appropriate cells, as determined by 'key_stack_pairs'

        Parameters:
            key_stack_pairs (tuple<*, Stack>):
                    (key, stack) pairs, where each stack should be drawn at the cell
                    corresponding to key
            selected (*): The key that is currently selected, or None if no key is selected
        """
        # Task 2.2 Crafting: Create widgets here
        print(f"{selected} is selected")
        for key, stack in key_stack_pairs:
            print(f"Redrawing {stack} at {key}")
            if key == "output":
                # Task 2.2 Crafting: Draw output cell
                self._output_view.draw_cell((0, 0), stack,
                                            selected == "output")
            else:
                # Task 2.2 Crafting: Draw input cells
                self._input_view.draw_cell(key, stack,
                                           True if selected == key else False)

    def bind_for_id(self, event, callback):
        """Binds callback to tkinter mouse event

        Callback accept parameters: callback(key, event), where
          - key (*) is the key of the cell clicked, etc.
          - mouse_event (tk.MouseEvent) is the original mouse event from tkinter
        """
        if event not in TK_MOUSE_EVENTS:
            return
        self._input_view.bind_for_id(event, callback)
        self._output_view.bind(event, lambda e: callback("output", e))
        self._button.bind(event, lambda e: callback("craft", e))
예제 #7
0
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
        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)
        self._master.title("Ninedraft")
        load_simple_world(self._world)

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

        self._world.add_collision_handler(
            "player", "item", on_begin=self._handle_player_collide_item)
        self._world.add_collision_handler(
            "player", "mob", on_post_solve=self._handle_player_collide_mob)
        self._hot_bar = SelectableGrid(rows=1, columns=10)
        self._hot_bar.select((0, 0))

        starting_hotbar = [
            Stack(create_item("dirt"), 20),
            Stack(create_item("crafting_table"), 1),
            Stack(create_item("furnace"), 1),
            Stack(create_item('axe', 'wood'), 1),
            Stack(create_item('pickaxe', 'golden'), 1),
        ]

        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)),
            ((0, 4), Stack(Item('stone'), 20)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack

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

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

        # Task 1.2 Mouse Controls: Bind mouse events here
        self._master.bind("<Motion>", self._mouse_move)
        self._master.bind("<1>", self._left_click)
        self._master.bind("<3>", self._right_click)

        # Task 1.3: Create instance of StatusView here
        self.status_view = StatusView(self._master)
        self.status_view.pack()
        self.status_view.set_food(self._player.get_food())
        self.status_view.set_health(self._player.get_health())

        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 x: 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))
        self._master.bind("w", lambda e: self._move(0, -1))
        self._master.bind("<Up>", lambda e: self._move(0, -1))
        # Task 1.5 Keyboard Controls: Bind numbers to hotbar activation here
        for i in range(10):
            self._master.bind(str((i + 1) % 10),
                              (lambda x: lambda e: self._activate_item(x))(i))

        # Task 1.6 File Menu & Dialogs: Add file menu here
        self.menu = tk.Menu(self._master)
        file_bar = tk.Menu(self.menu)
        file_bar.add_command(label="New Game", command=self._restart)
        file_bar.add_command(label="Exit", command=self._quit)

        self.menu.add_cascade(label='File', menu=file_bar)
        self._master.config(menu=self.menu)

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def _quit(self):
        result = simpledialog.messagebox.askyesno(
            title='quit', message='Do you really want to quit?')
        if result:
            self._master.quit()

    def _restart(self):
        result = simpledialog.messagebox.askyesno(
            title='restart', message='Do you really want to restart?')
        if result:
            self._master.destroy()
            root = tk.Tk()
            self.__init__(root)
            root.mainloop()

    def redraw(self):
        self._view.delete(tk.ALL)

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

        # target
        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 target and 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_food(self._player.get_food())
        self.status_view.set_health(self._player.get_health())

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

    def step(self):
        data = GameData(self._world, self._player)
        self._world.step(data)
        self.check_target()
        self.redraw()

        # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary
        # ...

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

    def _move(self, dx, dy):
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self):
        velocity = self._player.get_velocity()
        # Task 1.4: Update the player's velocity here
        self._player.set_velocity((velocity[0] * 0.8, velocity[1] - 200))

    def mine_block(self, block, x, y):
        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():
            print(block)
            # Task 1.2 Mouse Controls: Reduce the player's food/health appropriately
            if self._player.get_food() > 0:
                self._player.change_food(-1)
            else:
                self._player.change_health(-1)
                if self._player.get_health() <= 0:
                    self._restart()

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

            drops = block.get_drops(random.random(), True)
            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)
                elif drop_category == "mob":
                    self._world.add_mob(Bee("foe_bee", (8, 8)), x, y)
                else:
                    raise KeyError(f"Unknown drop category {drop_category}")

    def get_holding(self):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _left_click(self, event):
        # Invariant: (event.x, event.y) == self._target_position
        #  => Due to mouse move setting target position to cursor
        x, y = self._target_position
        mobs = self._world.get_mobs(x, y, 2.0)
        for mob in mobs:
            if mob.get_id() is "friendly_sheep":
                print('Dropped block, wool')
                physical = DroppedItem(create_item("wool"))

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

                self._world.add_item(physical, x0, y0)
            elif mob.get_id() is "foe_bee":
                print(f"{self._player} attack a bee,damage 1 hit")
                mob.attack(True)
                self._player.change_health(-1)
                if self._player.get_health() <= 0:
                    self._restart()
                if mob.is_dead:
                    print("A bee is deaded")
                    self._world.remove_mob(mob)

        target = self._world.get_thing(x, y)
        if not target:
            return

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

    def _trigger_crafting(self, craft_type):
        print(f"Crafting with {craft_type}")
        if craft_type in ("basic", "crafting_table", "furnace"):
            if craft_type == "basic":
                crafter = GridCrafter(CRAFTING_RECIPES_2x2)
            elif craft_type == "crafting_table":
                crafter = GridCrafter(CRAFTING_RECIPES_3x3, rows=3, columns=3)
            elif craft_type == "furnace":
                crafter = GridCrafter(SMELTING_RECIPES_1x2, rows=1, columns=2)
            self._crafting_window = CraftingWindow(
                self._master,
                "Smelt",
                self._hot_bar,
                self._inventory,
                crafter,
                mode="smelting" if craft_type == "furnace" else "normal")

    def run_effect(self, effect):
        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("Let's smelting by yourself")
                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                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):
        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):
        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

    def _handle_player_collide_mob(self, player: Player, mob: Mob, data,
                                   arbiter: pymunk.Arbiter):
        """Callback to handle collision between the player and a mob. If the player meet a bee,
        the player will get 1 hit.

        Parameters:
            player (Player): The player that was involved in the collision
            mob (Mob): The mob 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)
        """
        if mob.get_id() == "foe_bee":
            print(f"{self._player} touch a bee,get 1 damage")
            self._player.change_health(-1)
            if self._player.get_health() <= 0:
                self._restart()
예제 #8
0
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
        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("pickaxe","diamond"),1),
            Stack(create_item("axe","iron"),1),
            Stack(create_item("crafting_table"),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)),
            ((2, 5), Stack(create_item('stick'), 4)),
            ((0, 0), Stack(create_item('stone'), 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("e",
                          lambda e: self.run_effect(('crafting', 'basic')))



        self._view = GameView(master, self._world.get_pixel_size(), WorldViewRouter(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('<Leave>', self._mouse_leave)
        self._view.bind('<Button-1>', self._left_click)
        # For a Macbook, without a mouse to do a right click
        self._view.bind('<Button-2>', self._right_click)
        # For normal devices, with a mouse to do a right click
        self._view.bind('<Button-3>', self._right_click)
        # Task 1.3: Create instance of StatusView here
        # ...
        self._status_view = StatusView(self._master)
        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
        # ...
        # Do selection or deselection with keys from 1 to 9.
        for key_number in range(1,10):
            self._master.bind(str(key_number),lambda e ,key=key_number: self._activate_item(key-1))
        # 0 controls the last cell of the hotbar
        self._master.bind("0",lambda e:self._activate_item((9)))



        # Task 1.6 File Menu & Dialogs: Add file menu here
        # ...
        self._menubar = tk.Menu(master)
        master.config(menu=self._menubar)
        self._filemenu = tk.Menu(self._menubar)
        # restart or exit the game through the menu
        self._filemenu.add_command(label="New Game",command = self.message_box_restart)
        self._filemenu.add_command(label="Exit",command = self.message_box_exit)
        self._menubar.add_cascade(label="File", menu = self._filemenu)
        # exit the game when trying to close the window
        self._master.protocol("WM_DELETE_WINDOW", self.message_box_exit)

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def message_box_exit(self):
        """Exit the game if you choose yes and do nothing if you choose no."""
        exit = messagebox.askyesno("Exit","Would you like to exit the game?")
        if exit == True:
            self._master.destroy()


    def restart_the_game(self):
        """Reset the game"""
        # reset the world
        self._world = World((GRID_WIDTH, GRID_HEIGHT), BLOCK_SIZE)
        load_simple_world(self._world)
        # reset the player
        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)
        # reset the hotbar
        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("pickaxe", "diamond"), 1),
            Stack(create_item("axe", "iron"), 1),
            Stack(create_item("crafting_table"), 1)
            
        ]

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

        self._hands = create_item('hands')
        # reset the inventory
        starting_inventory = [
            ((1, 5), Stack(create_item('dirt'), 10)),
            ((0, 2), Stack(create_item('wood'), 10)),
            ((2, 5), Stack(create_item('stick'), 4)),
            ((0, 0), Stack(create_item('stone'),10)),
        ]
        self._inventory = Grid(rows=3, columns=10)
        for position, stack in starting_inventory:
            self._inventory[position] = stack

    def message_box_restart(self):
        """Restart the game if you choose yes and do nothing if you choose no."""
        restart = messagebox.askyesno("New Game", "Would you like to start a new game?")
        if restart == True:
            self.restart_the_game()


    def message_box_restart_for_death(self):
        """If you die, restart the game if you choose yes and exit the game if you choose no."""
        restart = messagebox.askyesno("New Game", "You die!!!! Would you like to start a new game?")
        if restart == True:
            self.restart_the_game()
        else:
            self._master.destroy()



    def redraw(self):
        self._view.delete(tk.ALL)

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

        # target
        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
        # ...
        self._view.show_target(self._player.get_position(), cursor_position)
        if not self._target_in_range:
            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):
        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.message_box_restart_for_death()


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

    def _move(self, dx, dy):
        self.check_target()
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self):
        self.check_target()
        velocity = self._player.get_velocity()
        # Task 1.2: Update the player's velocity here
        # ...
        self._player.set_velocity((velocity.x * 0.8, velocity.y -200))



    def mine_block(self, block, x, y):
        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(-1.0)
            else:
                self._player.change_health(-1.0)
            # Task 1.2 Mouse Controls: Remove the block from the world & get its drops
            # ...
            self._world.remove_block(block)
            drops = block.get_drops(luck, was_item_suitable)

            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):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _mouse_leave(self, event):
        self._target_in_range = False

    def _left_click(self, event):
        # 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)

    def _trigger_crafting(self, craft_type):
        print(f"Crafting with {craft_type}")
        # Initialise the crafting window
        if craft_type == "basic":
            crafter = GridCrafter(CRAFTING_RECIPES_2x2)

            self.crafter_window = CraftingWindow(self._master, "Basic Crafter", self._hot_bar, self._inventory,crafter)
            # close the crafting window by pressing "e"
            self.crafter_window.bind("e",
                              lambda e: self.crafter_window.destroy())



        # Initialise the crafting table
        elif craft_type == "crafting_table":
            crafter = GridCrafter(CRAFTING_RECIPES_3x3,rows=3,columns=3)
            self.crafter_window = CraftingWindow(self._master,"Advanced Crafter", self._hot_bar, self._inventory, crafter)
            # close the crafting window by pressing "e"
            self.crafter_window.bind("e",
                                     lambda e: self.crafter_window.destroy())


    def run_effect(self, effect):
        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")

                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                if self._player.get_food() < self._player.get_max_food():
                    stat, strength = effect
                    print(f"Gaining {strength} {stat}!")
                    getattr(self._player, f"change_{stat}")(strength)
                elif self._player.get_food() == self._player.get_max_food() and self._player.get_health() < self._player.get_max_food():
                    stat, strength = effect
                    stat = 'health'
                    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):
        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):
        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
예제 #9
0
class Ninedraft:
    def __init__(self, master):

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

        load_simple_world(self._world)
        self._master = master
        self._master.title(gameTitle)

        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)]

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

        self._hands = create_item('hands')

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

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

        self._view = GameView(master, self._world.get_pixel_size(),
                              WorldViewRouter())
        self._view.pack()

        # Task 1.2 Mouse Controls: Bind mouse events here
        self._view.bind("<Button-1>", self._left_click)
        # Note that <Button-2> symbolises middle click.
        self._view.bind("<Button-3>", self._right_click)
        self._view.bind("<Motion>", self._mouse_move)

        # Task 1.3: Create instance of StatusView here
        # ...

        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("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
        # ...

        # Task 1.6 File Menu & Dialogs: Add file menu here
        # ...

        self._target_in_range = False
        self._target_position = 0, 0

        self.redraw()

        self.step()

    def redraw(self):
        # TODO: cache items internally to improve efficiency
        self._view.delete(tk.ALL)

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

        # target
        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
        # GameView.show_target(player_position, cursor_position)

        # Task 1.3 StatusView: Update StatusView values here
        # ...

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

    def step(self):
        # TODO: pass effects...
        self._world.step()
        self.check_target()
        self.redraw()

        # Task 1.6 File Menu & Dialogs: Handle the player's death if necessary
        self._master.after(15, self.step)

    def _move(self, dx, dy):
        velocity = self._player.get_velocity()
        self._player.set_velocity((velocity.x + dx * 80, velocity.y + dy * 80))

    def _jump(self):
        velocity = self._player.get_velocity()
        # Task 1.4 Keyboard Controls: Update the player's velocity here
        # ...

    def mine_block(self, block, x, y):
        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
            #          You may select what you believe is an appropriate amount by
            #          which to reduce the food or health.
            # ...

            self._world.remove_block(block)
            # Task 1.2 Mouse Controls: Get what the block drops.
            # ...

            if not block.get_drops(random.randrange(0, 10, 1) / 10, True):
                # luck, break_result
                return

            x0, y0 = block.get_position()

            for i, (drop_category, drop_types) in enumerate(
                    block.get_drops(random.randrange(0, 10, 1) / 10, True)):
                print(f'Dropped {drop_category}, {drop_types}')

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

                    # TODO: 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):
        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):
        # 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):
        self._target_position = event.x, event.y
        self.check_target()

    def _left_click(self, event):
        print("Left click")
        # 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:
                print("t")
                self.mine_block(block, x, y)

    def _trigger_crafting(self, craft_type):
        print(f"Crafting with {craft_type}")

    def run_effect(self, effect):
        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")

                self._trigger_crafting(craft_type)
                return
            elif effect[0] in ("food", "health"):
                stat, strength = effect
                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):
        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
            item = self._hot_bar.get_selected_value().get_item()

            if not item:
                return

            drops = item.place()

            if not drops:
                return

            # TODO: handle multiple drops
            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):
        print(f"Activating {index}")

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

    def _handle_player_collide_item(self, player: Player, 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
            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)
        """

        if self._inventory.add_item(item.get_item()):
            print(f"Picked up a {item!r}")
            self._world.remove_item(item)

        return False