关键词 > CSSE1001/CSSE7030

CSSE1001/CSSE7030 Maze Runner Once Again Assignment 3 Semester 1, 2022

发布时间:2022-05-26

Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

Maze Runner Once Again

Assignment 3

Semester 1, 2022

CSSE1001/CSSE7030

1 Introduction

In assignment 2 you implemented a game of MazeRunner with a text-based interface. The Apple MVC design pattern used in this game allows modelling logic and view classes to be modified fairly independently. In this assignment, you’ll exchange the text-based interface of MazeRunner with a more sophisticated tkinter-based Graphical User Interface (GUI).

The game view initially contains 3 components:

● A level view on which the tiles and entities are drawn using either coloured shapes or images;

● An inventory view, which appears to the right of the level view and allows users to apply collected items (other than coins) by left-clicking those items in their inventory; and

● A stats view, which shows the player’s stats, as well as the number coins they have collected.

As opposed to assignment 2, where users controlled the game by inputting text at a prompt, in assignment 3 users control the game through key-presses and mouse clicks.  An example of the final game is shown in Figure 1.

You can find the tkinter documentation on effbot1 and New Mexico Tech2.

2 Tips and hints

This assignment is split into two tasks for undergraduate students, and an additional third task for postgraduate task.  The number of marks associated with each task is not an indication of difficulty. Task 1 may take less effort than task 2, yet is worth significantly more marks. A fully functional attempt at task 1 will likely earn more marks than attempts at both task 1 and task 2 that have many errors throughout. Likewise, a fully functional attempt at a single part of task 1 will likely earn more marks than an attempt at all of task 1 that has many errors through- out.  You should be testing regularly throughout the coding process.  Test your GUI manually and regularly upload to Gradescope to ensure the components you have implemented pass the Gradescope tests. At the minimum you should not move on to task 2 until you pass the task 1 tests.

Except where specified, minor differences in the look (e.g.  colours, fonts, etc.)  of the GUI are acceptable. Except where specified, you are only required to do enough error handling such that

Figure 1: Example fully functional game at the end of Task 2.

regular game play does not cause your program to crash or error.  If an attempt at a feature causes your program to crash or behave in a way that testing other functionality becomes difficult without your marker modifying your code, comment it out before submitting your assignment. If your solution contains code that prevents it from being run, you will receive a mark of 0.

You must only make use of libraries listed in Appendix A. You must not import anything that is not on this list; doing so will result in a deduction of up to 100% of your mark.

You may use any course provided code in your assignment.  This includes any code from the support files or sample solutions for previous assignments from this semester only, as well as any lecture or tutorial code provided to you by course staff. However, it is your responsibility to ensure that this code is styled appropriately, and is an appropriate and correct approach to the problem you are addressing.

3 Task 1: Basic Gameplay - 10 marks

Task 1 requires you to implement a functional GUI-based version of MazeRunner. At the end of this task your game should look like Figure 2.  A heading label should appear at the top of the window at all times.  Below this, the three components should appear as described in Section 1 and as depicted below.

Figure 2: Game at end of task 1.

Certain events should cause behaviour as per Table 1.

Event

Behaviour

Key Press: w

Player attempts to move up one square.

Key Press: a

Player attempts to move left one square.

Key Press: s

Player attempts to move down one square.

Key Press: d

Player attempts to move right one square.

Left click on an item in inven- tory

One item of the kind clicked should be applied to the player and removed from the inventory.  If no instances of the item remain in the inventory, the label showing the item should be removed from view, and all other item labels below it should move up to ll the space.

Table 1: Events and their corresponding behaviours.

In task 1, tiles are represented by coloured rectangles and entities are represented by coloured circles.  You must also annotate the circles of entities with their id (as per Fig. 2).  The colours representing each tile and entity can be found in constants.py. You must use the colours speci- fied in constants.py.

When the player wins or loses the game they should be informed of the outcome via a messagebox. The messagebox must display the text of WIN MESSAGE or LOSS MESSAGE in constants.py.  For task 1, there is no required behaviour after the messagebox is closed; the program can terminate or remain open.  However, whatever behaviour you choose must be reasonable (i.e.  closing the messagebox should not cause your program to crash, or an error to show).

To complete this task you will need to implement various view classes, as well as a graphical controller class which extends the existing MazeRunner controller class. While it is not necessary in order to complete task 1, you are permitted to add additional modelling classes if they improve your solution.

The following sub-sections outline the required structure for your code.  You will benefit from writing these classes in parallel, but you should still test individual methods as you write them.

3.1 Provided Code

The following code is provided in a2 solution.py and a3 support.py for you to use when im- plementing your solution.

3.1.1 Model classes

Model classes should be defined as per Assignment 2. You may add more modelling classes if they improve the code. You may not modify the supplied model classes.

3.1.2 AbstractGrid

AbstractGrid is an abstract view class which inherits from tk.Canvas and provides base func- tionality for multiple view classes. An AbstractGrid can be thought of as a grid with a set number of rows and columns, which supports creation of text and shapes at specific (row, column) po- sition.  Note that the number of rows may differ from the number of columns, and may change after the construction of the AbstractGrid.  If the dimensions change, the size of the cells will be be updated to allow the AbstractGrid to retain its original size. AbstractGrid consists of the following interface:

init (self, master:   Union[tk.Tk,  tk.Frame],  dimensions:   tuple[int,  int], size:   tuple[int,  int],  **kwargs)  ->  None:

Sets up a new AbstractGrid in the master frame. dimensions is the initial number of rows and number of columns, and size is the width in pixels, and the height in pixels. **kwargs is used to allow AbstractGrid to support any named arguments supported by tk.Canvas.

● set dimensions(self,  dimensions:   tuple[int,  int])  ->  None : Sets the dimensions of the grid to dimensions.

● get bbox(self, position:   tuple[int,  int])  ->  tuple[int,  int,  int,  int] :        Returns the bounding box for the (row, col) position, in the form (x min, y min, x max, y max).

● get midpoint(self, position:   tuple[int,  int])  ->  tuple[int,  int] :

Gets the graphics coordinates for the center of the cell at the given (row, col) position.

● annotate position(self, position:   tuple[int,  int],  text:    str)  ->  None : Annotates the cell at the given (row, col) position with the provided text.

● clear(self)  ->  None: Clears the canvas.

3.2 View classes

You must implement the view classes for the level map, inventory, and player stats.  Because the level map and player stats can both be represented by grids, you are required to use the AbstractGrid class to factor out the shared functionality.  This section outlines the classes and methods you are required to write. While you do not need to, you are permitted to add additional methods or classes provided they improve your solution.

3.2.1 LevelView

LevelView is a view class which inherits from AbstractGrid and displays the maze (tiles) along with the entities.  Tiles are drawn on the map using coloured rectangles at their (row, column) postitions, and entities are drawn over the tiles using coloured, annotated ovals at their (row, column) positions (as per Fig. 2). The colours representing each tile and entity can be found in constants.py. You must use these colours.

Your  program  should work  for  reasonable  map  sizes.   You  must  not  assume that the  num- ber of rows will always be equal to the number of columns.  You must use the create oval, create rectangle, and create text methods for tk.Canvas to achieve this task. The LevelView class should be instantiated as LevelView(master,  dimensions,  size,  **kwargs) where       **kwargs should be passed to the super class as with AbstractGrid. The width and height of the maze shown in the level (and consequently, the level view) are given in constants.py. While it is recommended to create your own helper methods in this class, the only method you are required to create is:

● draw(self,  tiles:    list[list[Tile]],  items:   dict[tuple[int,  int],  Item], player pos:   tuple[int,  int])  ->  None:

Clears and redraws the entire level (maze and entities).

3.2.2 StatsView

StatsView is a view class which inherits from AbstractGrid and displays the player’s stats (HP, health, thirst), along with the number of coins collected (as per Fig. 2). A StatsView always has four columns and two rows.  The first row contains the headings for the types of stats and the second row contains the values for the stat in that column.  You are required to implement the following methods in this class:

init (self, master:   Union[tk.Tk,  tk.Frame], width:    int,  **kwargs)  ->  None : Sets  up  a  new  StatsView  in the master  frame with the  given width.   The  height  of   the StatsView can be found in constants.py.  The background colour should be set to   THEME COLOUR from constants.py via the **kwargs.

● draw stats(self, player stats:   tuple[int,  int,  int])  ->  None: Draws the player’s stats (hp, hunger, thirst).

● draw coins(self, num coins:    int)  ->  None: Draws the number of coins. Again, it may be beneficial to create your own helper methods in this class.

3.2.3 InventoryView

InventoryView is a view class which inherits from tk.Frame, and displays the items the player has in their inventory via tk.Labels, under a title label.  This class also provides a mechanism through which the user can apply items. You must implement the following methods in this class:

init (self, master:   Union[tk.Tk,  tk.Frame],  **kwargs)  ->  None : Creates a new InventoryView within master.

● set click callback(self,  callback:   Callable[[str],  None])  ->  None :

Sets the function to be called when an item is clicked.  The provided callback function should take one argument; the string name of the item.

● clear(self)  ->  None: Clears all child widgets from this InventoryView.

●   draw item(self, name:    str, num:    int,  colour:    str)  ->  None :

Creates and binds (if a callback exists) a single tk.Label in the InventoryView frame. name is the name of the item, num is the quantity currently in the users inventory, and colour is the background colour for this item label (determined by the type of item).

● draw inventory(self,  inventory:    Inventory)  ->  None :

Draws any non-coin inventory items with their quantities and binds the callback for each, if a click callback has been set. Hint: loop over each item in the inventory and call draw item for each to create and bind the item label.

3.2.4 GraphicalInterface

GraphicalInterface inherits from UserInterface and must implement the methods described by UserInterface.  The GraphicalInterface manages the overall view (i.e.  the title banner and the three major widgets), and enables event handling.  You must implement the following methods:

init (self, master:   tk.Tk)  ->  None: Creates a new GraphicalInterface with mas- ter frame master.  Sets the title of the window to MazeRunner’ and creates a title label. Note: this method is not responsible for instantiating the three major components, as you may not know the dimensions of the maze when the GraphicalInterface is instantiated.

● create interface(self,  dimensions:   tuple[int,  int])  ->  None :

Creates the components (level view, inventory view, and stats view) in the master frame for this interface.  dimensions represent the (row, column) dimensions of the maze in the current level.

● clear all(self)  ->  None: Clears each of the three major components (do not delete the component instances, just clear them).

● set maze dimensions(self,  dimensions:   tuple[int,  int])  ->  None : Updates the dimensions of the maze in the level to dimensions.

● bind keypress(self,  command:   Callable[[tk.Event],  None])  ->  None :

Binds the given command to the general keypress event. The command should be a function which takes in the keypress event, and performs different actions depending on what char- acter was pressed. Any character other than ‘w’, ‘a’, ‘s’, or ‘d’ should be ignored (including all uppercase letters).

● set inventory callback(self,  callback:   Callable[[str],  None])  ->  None :

Sets the function to be called when an item is clicked in the inventory view to be callback. The callback function will need to be constructed in the controller class (see Section 3.3), and must take one argument (the string name of an Item), and apply the first instance of that item in the inventory to the player. This function should then remove that item from the player’s inventory.

● draw inventory(self,  inventory:    Inventory)  ->  None :

Draws any non-coin inventory items with their quantities and binds the callback for each, if a click callback has been set.

● draw(self, maze:   Maze,  items:   dict[tuple[int,  int],  Item], player position: tuple[int,  int],  inventory:    Inventory, player stats:   tuple[int,  int,  int])  ->  None:  Must implement the draw method as per the docstring in the UserInterface  class.  This should just involve clearing the three major components and redrawing them  with the new state (provided as arguments to this method).

●   draw inventory(self,  inventory:    Inventory)  ->  None :

Must implement the draw inventory method as per the docstring in the UserInterface class.  Note: this method will need to draw both the non-coin items on the inventory view (see public draw inventory method), and also draw the coins on the stats view.

●   draw level(self, maze:   Maze,  items:   dict[tuple[int,  int],  Item], player position:   tuple[int,  int])  ->  None:

Must implement the draw level method as per the docstring in the UserInterface class.

●   draw player stats(self, player stats:   tuple[int,  int,  int])  ->  None:

Must implement the draw player stats method as per the docstring in the UserInterface class.

3.3 Controller class

3.3.1 GraphicalMazeRunner

GraphicalMazeRunner should inherit from MazeRunner and overwrite / add methods where re- quired in order to enable the game to use a GraphicalInterface instead of a TextInterface, and to be based on events generated by the user (keypresses and mouse clicks), rather than based on user input to the shell. In particular, you will need to implement the following methods:

init (self,  game file:    str,  root:   tk.Tk)  ->  None:  Creates a new Graphical- MazeRunner game, with the view inside the given root widget.

●   handle keypress(self,  e:   tk.Event)  ->  None: Handles a keypress. If the key pressed was one of ‘w’, ‘a’, ‘s’, or ‘d’ a move is attempted. If the player wins or loses the game with this move, they are informed of their result via a messagebox.

●   apply item(self,  item name:    str)  ->  None:   Attempts  to  apply  an  item  with  the given name to the player.

● play(self)  ->  None:  Called to cause gameplay to occur.  This method should first cre- ate the widgets on the GraphicalInterface, bind the keypress handler, set the inventory callback, and then redraw to allow the game to begin.


3.4 play game(root:   tk.Tk) function The play game function should be fairly short. You should:

1.  Construct the controller instance using the file in the GAME FILE constant and the root tk.Tk parameter.

2.  Cause gameplay to commence.

3. Ensure the root window stays opening listening for events (using mainloop).

3.5 main function

The main function should:

1.  Construct the root tk.Tk instance.

2.  Call the play game function passing in the newly created root tk.Tk instance.

4 Task 2: Images, Buttons, and File Menu

Task 2 requires you to add additional features to enhance the games look and functionality. Fig- ure 1 gives an example of the game at the end of task 2.  Another example is shown below in Figure 3. Note: Your task 1 functionality must still be testable. When your program is run with the TASK constant in constants.py set to 1, the game should display only task 1 features. When your program is run with the TASK constant set to 2, the game should display all attempted task 2 features. There should be no task 2 features visible when running the game in task 1 mode.

As an advanced task, the structure of your code is largely up to you.  However, if you complete this task, you will be marked on how well-designed your code is.   Some aspects of design are compulsory  (e.g.   implementing the subclasses described in subsections below).   However, the methods and internal design of these classes are not prescribed.  When designing your solution, you should consider ways to effectively utilize the inheritance structure to avoid duplicating code.