HW 7:
Enchanted Garden
Submission Details
Due: 4/19 - Submit via Gradescope:
4 files in total
EnchantedMushroom.py
EnchantedLog.py
enchantedGarden.py
A PDF that contains your self-assessment & reflection
Homeworks are an opportunity to engage with the material and practice your programming skills. To support you, solutions will be available on moodle. It is your responsibility to engage with them in a way that strategically moves your learning forward. You should:
Attempt the homework without looking at the solutions.
Review the solutions; if you were stuck, you may revise your homework. You may find yourself iteratively reviewing and revising to best support your learning.
When you submit your homework, you will be required to complete a self-assessment and reflection:
Give yourself an assessment mark on the initial attempt:
✘ if you did not complete it
✓- if you completed it, but were very far from the solution
✓ if you completed it and essentially had the solution
✓+ if you completed it correctly, including precise communication
? if you are unsure as to whether or not you completed it
Provide a short reflection as to why you earned that mark, such as:
I did not understand how to get started, and it was too close to the deadline to visit support hours.
I got stumped by a bug where my program entered an infinite loop.
If you used the solutions to revise your work, describe what you needed to change, as in:
I modified the style of my conditional when I saw a simpler approach in the solutions.
I was so stuck that the majority of my submission is based on the solutions.
Ask specific questions on portions that are still confusing, such as:
Why does it cause an error when I returned "1" instead of 1?
[Optional] Provide one hint for the homework (to your past self or future student).
Learning goals
practice with OOP
gain comfort with the discomfort of working with and adapting sample code
Be strategic!
Start early!
Ask questions!
Leave time for questions by... starting early :)
When you're stuck, pause and evaluate what could be going on. Remember the self-regulated learning cycle!
Do you have a plan?
Are you evaluating your plan? How will you test your code as you go?
How will you revise? What will you do when you're confused? Stuck?
Overview
You've probably noticed that a crucial developer skill to practice is learning and adapting to new technologies and platforms. As developers, we do this regularly, often working with code that we won’t fully understand. It can be overwhelming at first, but there is a way to be systematic. First, start with simple sample code. Ideally it has behavior similar to our goal behavior. Then work to systematically adapt the code, changing only a small amount at a time. This often means not understanding every line of code and being comfortable with some discomfort.
For this homework, you’ll be creating a "GUI" for the first time. Probably, most of your previous programs have used the “command line” interface, allowing for text-only input and output. A GUI or Graphical User Interface is what we are more familiar with when we interact with devices – we expect to click different positions on the screen and have the interface (visually) respond, sometimes by changing the color or text of what we see.
Working with and programming GUIs in most languages takes quite a bit of getting used to. The code can sometimes feel clunky and mysterious, and that is part of what we’re practicing in this homework. First you’ll walk through some sample code. Then, based on what you've seen in the sample code and the introduction to defining your own classes, you'll create GUI application using OOP.
Part 1: sample GUI with Tkinter
Step 1: Set up the sample program
Create a folder to organize your code called LightRoom
Create a file for each of the following and copy/paste the code from below:
LightBulb.py
room.py
Download the two image asset files from below by right-clicking them and saving/moving them to your LightRoom folder
Confirm the directory and its contents by comparing with the image to the right
room.py
""" This is an example GUI using Tkinter that creates a "room"
that LightBulbs can be added to. It relies on the LightBulb class. """
from tkinter import *
from LightBulb import LightBulb
def main():
""" This function sets up the Tkinter GUI. """
# create the main tkinter window
window:Tk = Tk()
# set the background color
window.configure(background='black')
# this sets the title that shows at the top of the window
window.title( "Light Bulbs!" )
# this sets the width and height of the window (in pixels)
window.geometry("600x400")
# now that we have a window, we can initialize the PhotoImage elements
# that we want the LightBulb class to share
# to be in line with the assumptions of the LightBulb class
# we will first add the off image (at index 0)
LightBulb.displayElts.append( PhotoImage(file = r"LightBulbOff.png") )
# and then the on image (at index 1)
LightBulb.displayElts.append( PhotoImage(file = r"LightBulbOn.png" ) )
# add one light at the coordinates (100, 200)
# note that the upper left of the window is (0,0)
# x coordinates behave "as usual" with larger x values to the right
# y coordinates behave "flipped" with larger y values down
addLight( window, 100, 200 )
# ask the window to launch
window.mainloop()
def addLight( window:Tk, posX:int, posY:int ) -> None:
""" Create a LightBulb instance and initialize/add a display element for it. """
# create a Tkinter Label instance to display the LightBulb instance
# it requires the window for attachment and an optional background color
bulbDisplay:Label = Label( window, bg="black" )
# ask the Label instance to place itself on the window at the
# specified (posX,posY) coordinates by using the optional arguments x and y
bulbDisplay.place(x=posX,y=posY)
# create the LightBulb instance and pass it the GUI label for displaying
lightBulb:LightBulb = LightBulb( bulbDisplay )
# print out info about the LightBulb instance
print( f"Just added a LightBulb: {lightBulb}" )
if __name__ == "__main__":
main()
LightBulb.py
from tkinter import Label
class LightBulb( object ):
""" Represents a light bulb that can be on or off and maintains
a GUI element for display. """
# list to hold display elements
# because of how Tkinter is structured, this will need to be updated
# by an external class that has access to the main Tkinter window
# we expect that index 0 will hold the "off" image and index 1 the "on" image
displayElts:list = []
def __init__( self, guiElt:Label ):
""" Construct a new light bulb, initially off, that should use the
passed guiElt for display. """
# initialize the instance properties
self.__on:bool = False # to track if the light is on/off
self.__display:Label = guiElt # to maintain the GUI display
# set up the GUI to start by displaying the off image
# on the left, we access the Tkinter Label object's
# instance property named display
# it is a dictionary, and we want to update the entry for the "image" key
# on the right, we assign the "off" image which we expect to be stored at index 0
self.__display["image"] = LightBulb.displayElts[0]
# set up the GUI to respond to clicks by "binding" the click to the
# instance method processClick
# this works with Tkinter's "event-driven" infrastructure and
# is similar to signing up for an email list
# when we sign up for a mailing list, we have to give it the "address"
# for how to contact us
# here, we have the label sign up for click events using the argument "<Button>"
# and we give the "address" for how to contact us by passing the
# instance method "address" self.processClick
# OBSERVE that we are NOT invoking the instance method
self.__display.bind( "<Button>", self.processClick )
def processClick( self, event ) -> None:
""" This instance property exists as a way of allowing the Tkinter GUI
to "contact" us when a click has occurred. It requires the event parameter,
which we won't use (but contains information) -- this is like the body of
an email for a mailing list. """
# print info so we know a click occurred
print( f"Clicking: {self}" )
# change the state of the light bulb by invoking the instance methods setOn and isOn
self.setOn( not self.isOn() )
# print info now that the state has changed
print( f"... and now: {self}." )
def isOn( self ) -> bool:
""" A getter for accessing the state of the light bulb
returns True if it's on and False otherwise. """
return self.__on
def setOn( self, turnTheLightOn:bool ) -> None:
""" A setter for changing the state of the light bulb to
be on (True) or off (False). """
# update the instance property
self.__on = turnTheLightOn
# refresh the display
self.refreshDisplay()
def refreshDisplay( self ) -> None:
""" An instance method that refreshes the display to reflect
the current state of the light bulb."""
# invoke the instance property isOn to check if the bulb is on
if self.isOn():
# set the Tkinter Label data to display the on image (at index 1)
self.__display["image"] = LightBulb.displayElts[1]
else:
# set the Tkinter Label data to display the off image (at index 0)
self.__display["image"] = LightBulb.displayElts[0]
def __str__( self ) -> str:
""" Return a string representation of this light bulb. """
if self.isOn():
return "A light bulb that is on."
else:
return "A light bulb that is off."
Image asset files
LightBulbOff.png
LightBulbOn.png
Step 2: Run the sample program
In VSCode, make sure that you have the file room.py selected, as it contains the main code for launching the program.
Run the sample program. You should see a window with a black background and light bulb. Try clicking the light bulb -- it should "turn on" and show a different image.
click the bulb (it's a little hard to see, but is on the left side)
-->
Step 3: Investigate the code
When you get sample code, it's always good to try a few experiments to start to gain understanding of how it works. Here are a couple questions you might have when you first play with it.
How did the light bulb get added to the window? How did its position get set?
How would we add another light bulb? Will clicking it impact all the light bulbs or just the one that is clicked?
How does the light bulb change its display when a click occurs?
Let's take each question and walk through "experiments" to gain understanding.
Question 1: How did the light bulb get added to the window? How did its position get set?
Look through the code and see if you can find the line(s) of code for adding the light bulb.
Run an experiment by commenting it out and re-running the program -- does the light bulb disappear?
Once you've tried, check out the solutions below.
Debrief
The GUI is mainly set up in room.py. There are a couple candidate lines that seem to add the light bulb.
line 31: addLight( window, 100, 200 )
line 45: bulbDisplay.place(x=posX,y=posY)
Run the experiment for each of these lines separately. They should both make the light bulb disappear!
As you consider the difference between the two lines, consider the roles of abstraction and reusability. Since we ultimately may want to add more light bulbs, line 31 seems more helpful. It invokes a function that encapsulates the behavior required to create and add the light bulb and is thus reusable. At this point, we may not care how that happens (though line 45 is certainly part of it), so let's abstract away the "implementation details."
Question 2: How would we add another light bulb? Will clicking it impact both light bulbs or just the one that is clicked?
Based on the experiments from Question 1, add another line of code (by copy/pasting) to add another light bulb at the location (300, 200). Where do you expect that light bulb to show up?
Run the program to see if you are correct.
Do you think that clicking the new light bulb will impact both light bulbs or just the one that is clicked?
Click the light bulb to see if you are correct.
Once you've tried, check out the debrief below.
Debrief
After line 31, add a copy, then modify the second parameter to have the value 300:
addLight( window, 300, 200 )When you run the program, you should see a second light bulb slightly the right of the center of the window.
When you looked through the code, you should have noticed that an instance of the LightBulb class is created in line 48 of room.py.
# create the LightBulb instance and pass it the GUI label for displaying
lightBulb:LightBulb = LightBulb( bulbDisplay )
This should prompt you to expect that each light bulb (through the power of OOP) will maintain its own state. Clicking one light bulb should not impact the other one.
When you run the program, you should be able to click the light bulbs on/off individually!
Question 3: How does the light bulb change its display when a click occurs?
Look through the code to see if you can find which parts are related to the light bulb's display.
Then, see if you could add a third image asset using the file to the right and modify the code to randomly display the original "on" asset or the newly added one if the light bulb is on.
Once you've tried, check out the debrief below.
LightBulbPink.png
Debrief
As you look through the code, you should see a couple candidate lines that seem related to the light bulb's display:
in LightBulb.py, lines 7-11 seem to create a variable (this actually belongs to the LightBulb class and can be shared by all the instances) for maintaining the image assets:
# list to hold display elements
# because of how Tkinter is structured, this will need to be updated
# by an external class that has access to the main Tkinter window
# we expect that index 0 will hold the "off" image and index 1 the "on" image
displayElts:list = []
in LightBulb.py, lines 21-26 of the constructor seem to set the display to start with the off image:
# set up the GUI to start by displaying the off image
# on the left, we access the Tkinter Label object's
# instance property named display
# it is a dictionary, and we want to update the entry for the "image" key
# on the right, we assign the "off" image which we expect to be stored at index 0
self.__display["image"] = LightBulb.displayElts[0]
in LightBulb.py, lines 70-79 define the instance method for refreshing the display and contain code that is analogous to what is in the constructor -- depending on whether to display the on or off image, the code accesses a different index of the displayElts list:
def refreshDisplay( self ) -> None:
""" An instance method that refreshes the display to reflect
the current state of the light bulb."""
# invoke the instance property isOn to check if the bulb is on
if self.isOn():
# set the Tkinter Label data to display the on image (at index 1)
self.__display["image"] = LightBulb.displayElts[1]
else:
# set the Tkinter Label data to display the off image (at index 0)
self.__display["image"] = LightBulb.displayElts[0]
in room.py, lines 22-28 seem to set up the image assets for the LightBulb to use
# now that we have a window, we can initialize the PhotoImage elements
# that we want the LightBulb class to share
# to be in line with the assumptions of the LightBulb class
# we will first add the off image (at index 0)
LightBulb.displayElts.append( PhotoImage(file = r"LightBulbOff.png") )
# and then the on image (at index 1)
LightBulb.displayElts.append( PhotoImage(file = r"LightBulbOn.png" ) )
To add an additional asset and randomly display it for a light bulb that is on, we can:
add the asset by appending it to the displayElts list -- insert this code after line 28 of room.py
# add a third image asset: an alternative pink image (at index 2)
LightBulb.displayElts.append( PhotoImage(file = r"LightBulbPink.png" ) )
modify the refreshDisplay instance method to randomly select index 1 or 2, as in:
def refreshDisplay( self ) -> None:
""" An instance method that refreshes the display to reflect
the current state of the light bulb."""
# invoke the instance property isOn to check if the bulb is on
if self.isOn():
# randomly choose a lit bulb to display (index 1 or 2)
litBulbIndex:int = random.randint(1,2)
# set the Tkinter Label data to display that image
self.__display["image"] = LightBulb.displayElts[litBulbIndex]
else:
# set the Tkinter Label data to display the off image (at index 0)
self.__display["image"] = LightBulb.displayElts[0]
run the program and see if the light bulb is pink sometimes!
Part 2: Enchanted Garden
Now you'll take what you've learned in Part 1 and synthesize it further by creating your own "Enchanted Garden" program!
Watch the video for a preview of how the final program should work.
Requirements
To create your Enchanted Garden, you must:
Define an EnchantedMushroom class in a file named EnchantedMushroom.py
Define an EnchantedLog class in a file named EnchantedLog.py
Define a set of functions, including a main function, in a file named enchantedGarden.py
The specifications for each file are outlined below.
Developer tips
Develop a little, test a little...
Be thoughtful of your process; you may want to think about the design vs implementation phases
Resist the temptation to write the entire program all at once! Otherwise, the debugging process becomes so much more difficult and time-consuming!
First, create the skeleton of your program files by writing "stubs" (declarations of the classes, instance methods and functions with "placeholder" implementations).
Then implement one part at a time and test as you go.
Remember you have different types of debugging strategies:
Print statements help you peek at memory during program execution.
Tracing your program on paper helps you understand when the execution doesn't match your intended logic.
The Debugger can help you do both!
Be strategic!
Be intentional around your approach to working on the project:
Start early!
Ask questions!
Leave time for questions by... starting early :)
When you're stuck, pause and evaluate what could be going on. Consider the self-regulated learning cycle!
Do you have a plan?
Are you evaluating your plan? How will you test your code as you go?
How will you revise? What will you do when you're confused? Stuck?
EnchantedMushroom class in EnchantedMushroom.py
Define a class that represents an enchanted mushroom. Write your class definition in a file named EnchantedMushroom.py, adhering to the following specification:
The class should be named EnchantedMushroom.
Like the LightBulb class, there should be a shared "class variable" called displayImages. For the mushroom, this will be a list that is initialized to the empty list and will ultimately hold 4 images.
(Due to how Tkinter handles image files, this will need to populated by the main program functions.)There should be a two instance properties:
An int to track the enchantment level of the mushroom, with 0 indicating no enchantment
A Label to maintain the display for the mushroom (much like the LightBulb class)
There should be a constructor that:
Requires a single argument when invoked for the Label instance used as a display
Should initialize the instance properties
Should set up the display to show the image at index 0 of displayImages
Should "bind" mouse click events to the instance method processClick
There should be a getter for the enchantment level instance property called getEnchantment that:
Requires no arguments when invoked
Should return an int that is 0 for the initial enchantment level (not enchanted), 1 for the next level of enchantment, etc.
There should be a setter to change changing the level of enchantment called setEnchantment that:
Requires a single argument when invoked of type int
Checks if the given value is valid (between 0 and the # of levels)
If an invalid value is passed, it should assign the closest valid value. For a value < 0, set the enchantment to the initial level. For a value > the final level value, set the enchantment to the final level.
There should be an instance method called enchant that:
Requires no arguments when invoked
Should "advance" the level of enchantment by invoking the getEnchantment and setEnchantment methods
Like the LightBulb class, there should be an instance method for handling click events called processClick that:
Requires a single argument when invoked (we'll skip the type for this as it would require a little more GUI explanation than is within the scope of this course).
Should invoke the instance method enchant
Like the LightBulb class, there should be an instance method for refreshing the display called refreshDisplay that:
Requires no arguments when invoked
Should update the displayed image to be the corresponding image in the displayImages list.
There should be an instance method called __str__ that:
Requires no arguments when invoked
Should return a string representation of the mushroom. If the mushroom is enchanted at level 0, for example, it should return
"A mushroom enchanted at level 0."
EnchantedLog class in EnchantedLog.py
Define a class that represents an enchanted log. Write your class definition in a file named EnchantedLog.py, adhering to the following specification:
The class should be named EnchantedLog.
Like the EnchantedMushroom class, there should be a shared "class variable" called displayImages. For the log, this will be a list that is initialized to the empty list and will ultimately hold 2 images.
(Due to how Tkinter handles image files, this will need to populated by the main program functions.)There should be a three instance properties:
A bool to track whether the log is enchanted (True) or not (False); it should initially not be enchanted
A list to track the EnchantedMushroom instances associated to the log; it should be initialized to the empty list
A Label to maintain the display for the log (much like the EnchantedMushroom class)
There should be a constructor that:
Requires a single argument when invoked for the Label instance used as a display
Should initialize the instance properties
Should set up the display to show the image at index 0 of displayImages
Should "bind" mouse click events to the instance method processClick
There should be a getter for the enchantment level instance property called isEnchanted that:
Requires no arguments when invoked
Should return an bool
There should be an instance method called enchant that:
Requires no arguments when invoked
Should have a return type of None
Should check if all the mushrooms associated to this log have some level of enchantment (>0). If so, should update the instance property to set the enchanted state to True and refresh the display.
There should be an instance method called addMushroom that:
Requires a single argument when invoked of type EnchantedMushroom
Should have a return type of None
Should add the passed mushroom to the list of mushrooms (maintained as an instance property)
Like the EnchantedMushroom class, there should be an instance method for handling click events called processClick that:
Requires a single argument when invoked (we'll skip the type for this as it would require a little more GUI explanation than is within the scope of this course).
Should invoke the instance method enchant
Like the EnchantedMushroom class, there should be an instance method for refreshing the display called refreshDisplay that:
Requires no arguments when invoked
Should update the displayed image to be the corresponding image in the displayImages list:
If the log is not enchanted, display the image at index 0
Otherwise, display the image at index 1
There should be an instance method called __str__ that:
Requires no arguments when invoked
Should return a string representation of the log, such as
"A log that is not enchanted with 3 mushrooms."
or
"A log that is enchanted with 4 mushrooms."
main program functions in enchantedGarden.py
In a file enchantedGarden.py, using room.py as a guide, define the following functions:
A main function that:
Requires no parameters
Sets up the Tkinter window:
The background color should be navy
The dimensions should be 800 x 600
Initializes the art assets:
The image files are all in this directory
You'll notice they are named with the patter mushroomNUM.png and logNUM.py. This means you can use for loops to create the PhotoImage elements that the EnchantedMushroom and EnchantedLog classes each share.
Create and add two logs by invoking the addLogWithMushrooms (described next):
One log should be at coordinates (0,100) with 3 mushrooms
The other log should be at coordinates (300,350) with 4 mushrooms
Launch the Tkinter window
A addLogWithMushrooms function that:
Requires 4 parameters:
The Tkinter window
An int for the x coordinate
An int for the y coordinate
An int for the number of mushrooms
Should create a Tkinter Label instance to display the log
Should ask the created Label instance to place itself on the window at the specified x and y coordinates
Should create the EnchantedLog instance and pass it the GUI label for displaying
Should create the mushrooms using the createMushroom function (described next) and add them to the log using the log's instance method addMushroom
The coordinates of the mushroom mushroomNum (assuming mushroomNum takes on the values 0, 1, ...) should be:
(posX + 40 + mushroomNum*100, posY-100)
A createMushroom function that:
(Is almost the same as the addLightBulb function from Part 1)
Requires 3 parameters:
The Tkinter window
An int for the x coordinate
An int for the y coordinate
Should create a Tkinter Label instance to display the mushroom
Should ask the created Label instance to place itself on the window at the specified x and y coordinates
Should create the EnchantedMushroom instance and pass it the GUI label for displaying
Should return the EnchantedMushroom instance (so the log can add it)
IMPORTANT
Do NOT invoke the main function directly in your script, or you will fail the Gradescope autograder tests.
Remember to use this pattern to ensure it is only invoked when executed directly as a Python program:
if __name__ == "__main__":
main()