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._item_grid = ItemGridView(master, input_size) # Task 2.2 Crafting: Create widgets here # ... input_frame = tk.Frame(master) input_frame.pack(side=tk.TOP) self._input = ItemGridView(input_frame, input_size) self._input.pack(side=tk.LEFT) self._craft_button = tk.Button(input_frame, text="craft") self._craft_button.pack(side=tk.LEFT) self._output = ItemGridView(input_frame, (1, 1)) self._output.pack(side=tk.LEFT) self.BORDER = 100 # BORDER//2 + |item grid| + BORDER//2 self.CELL_LENGTH = 64 # pixel width of grid cell self.CELL_SPACING = 5 # pixel spacing between grid cells 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.draw_cell((0, 0), stack, selected == key) pass else: # Task 2.2 Crafting: Draw input cells # ... self._input.draw_cell(key, stack, selected == key) pass 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.bind(event, lambda e: callback(self.xy_to_grid( (e.x, e.y)), e)) self._output.bind(event, lambda e: callback("output", e)) self._craft_button.bind(event, lambda e: callback("craft", e)) # 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) # # ... # Task 2.2 Crafting: You may add additional methods here # ... def set_button_method(self, newmethod): self._craft_button['command'] = newmethod def xy_to_grid(self, xy_position): """Returns the grid position of the cell that contains the 'xy_position' Parameters: xy_position (tuple<float, float>): (x, y) coordinates contained by some cell Return: (tuple<int, int>): The (row, column) grid position of the cell """ x, y = xy_position column = (x - self.BORDER // 2) // (self.CELL_LENGTH + self.CELL_SPACING) row = (y - self.BORDER // 2) // (self.CELL_LENGTH + self.CELL_SPACING) return row, column
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) # Task 2.2 Crafting: Create widgets here # ... # create input/output/button wedgets self._input_widget = ItemGridView(self, input_size) self._crafting_button = tk.Button(self, text=" => Craft => ") self._output_widget = ItemGridView(self, (1, 1)) self._input_widget.pack(side=tk.LEFT) self._crafting_button.pack(side=tk.LEFT) self._output_widget.pack(side=tk.TOP) 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") 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_widget.draw_cell((0, 0), stack, key == selected) else: # Task 2.2 Crafting: Draw input cells # ... self._input_widget.draw_cell(key, stack, key == selected) 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) # # ... self._input_widget.bind( event, lambda e: callback(self._input_widget.xy_to_grid((e.x, e.y)), e)) self._output_widget.bind(event, lambda e: callback("output", e)) self._crafting_button.bind(event, lambda e: callback("craft", e))
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
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'))
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
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
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
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))
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()
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
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