"""CSC110 Fall 2021 Prep 10: Programming Exercises Instructions (READ THIS FIRST!) =============================== This Python module contains a new *class definition* with attributes and representation invariants already defined. We have started a few different methods in the class body, and your task is to implement EACH method based on the method header and description. There are some helper functions we have provided near the bottom of this file; please do not modify either of them. We have marked each place you need to write code with the word "TODO". As you complete your work in this file, delete each TODO comment. You do not need to add additional doctests. However, you should test your work carefully before submitting it! Copyright and Usage Information =============================== This file is provided solely for the personal and private use of students taking CSC110 at the University of Toronto St. George campus. All forms of distribution of this code, whether as given or with any changes, are expressly prohibited. For more information on copyright for CSC110 materials, please consult our Course Syllabus. This file is Copyright (c) 2021 David Liu, Mario Badr, and Tom Fairgrieve. """ import random import tkinter as tk PREDEFINED_COLOURS = [ 'AntiqueWhite1', 'CadetBlue1', 'DarkGoldenrod1', 'DarkOliveGreen1', 'DarkOrange1', 'DarkOrchid1', 'DarkSeaGreen1', 'DarkSlateGray1', 'HotPink1', 'IndianRed1', 'LightBlue1', 'LightGoldenrod1', 'LightPink1', 'LightSkyBlue1', 'LightSteelBlue1', 'MediumOrchid1', 'MediumPurple1', 'OliveDrab1', 'PaleGreen1', 'PaleTurquoise1', 'PaleVioletRed1', 'RosyBrown1', 'RoyalBlue1', 'SeaGreen1', 'SkyBlue1', 'SlateBlue1', 'SlateGray1', 'SteelBlue1', 'VioletRed1', 'alice blue', 'antique white', 'aquamarine', 'azure', 'bisque', 'blanched almond', 'blue violet', 'brown1', 'burlywood1', 'cadet blue', 'chocolate1', 'coral', 'coral1', 'cornflower blue', 'cyan', 'dark goldenrod', 'dark green', 'dark khaki', 'dark olive green', 'dark orange', 'dark orchid', 'dark salmon', 'dark sea green', 'dark slate blue', 'dark turquoise', 'dark violet', 'deep pink', 'deep sky blue', 'dodger blue', 'firebrick1', 'floral white', 'forest green', 'ghost white', 'gold', 'goldenrod', 'goldenrod1', 'green yellow', 'hot pink', 'indian red', 'khaki', 'khaki1', 'lavender', 'lavender blush', 'lawn green', 'lemon chiffon', 'light blue', 'light coral', 'light cyan', 'light goldenrod', 'light goldenrod yellow', 'light grey', 'light pink', 'light salmon', 'light sea green', 'light sky blue', 'light slate blue', 'light steel blue', 'light yellow', 'lime green', 'linen', 'maroon', 'maroon1', 'medium aquamarine', 'medium blue', 'medium orchid', 'medium purple', 'medium sea green', 'medium slate blue', 'medium spring green', 'medium turquoise', 'medium violet red', 'midnight blue', 'mint cream', 'misty rose', 'navajo white', 'navy', 'old lace', 'olive drab', 'orange', 'orange red', 'orchid1', 'pale goldenrod', 'pale green', 'pale turquoise', 'pale violet red', 'papaya whip', 'peach puff', 'pink', 'pink1', 'plum1', 'powder blue', 'purple', 'purple1', 'rosy brown', 'royal blue', 'saddle brown', 'salmon', 'salmon1', 'sandy brown', 'sea green', 'sienna1', 'sky blue', 'slate blue', 'snow', 'spring green', 'steel blue', 'tan1', 'thistle', 'thistle1', 'tomato', 'turquoise', 'turquoise1', 'violet red', 'wheat1', 'white smoke', 'yellow green' ] class Spinner: """A spinner for a board game. A spinner has a certain number of slots, numbered starting at 0 and increasing by 1 each slot. For example, if the spinner has 6 slots, they are numbered 0 through 5, inclusive. A spinner also has an arrow that points to one of these slots. Instance Attributes: - slots: The number of slots in this spinner. - position: The slot number that the spinner's arrow is currently pointing to. Representation Invariants: - 0 <= self.position < self.slots Sample Usage: >>> s = Spinner(8) # Create a spinner with 8 slots >>> s.position # A spinner initially points to slot 0 0 >>> s.spin(4) >>> s.position 4 >>> s.spin(2) >>> s.position 6 >>> s.spin(2) >>> s.position 0 """ slots: int position: int def __init__(self, size: int) -> None: """Initialize a new spinner with the given number of slots. A spinner's position always starts at 0 (so there is no "position" argument for the initializer). Preconditions: - size >= 1 """ self.slots = size self.position = 0 def spin(self, force: int) -> None: """Spin this spinner, advancing the arrow slots. The spinner wraps around once it reaches its maximum slot, starting back at 0. See the class docstring for an example of this. Preconditions: - force >= 0 """ self.position = (self.position + force) % self.slots def spin_randomly(self) -> None: """Spin this spinner randomly. This modifies the spinner's position to a random slot on the spinner. Each slot has an equal chance of being pointed to. >>> s = Spinner(8) >>> s.spin_randomly() >>> 0 <= s.position < 8 True """ self.position = random.randint(0, self.slots - 1) def draw(self, canvas: tk.Canvas) -> None: """Draw this spinner onto the given canvas. (See starter file images for some examples.) Here is the algorithm you should follow for drawing the slots: The circle is filled by equal sectors, one for each slot. The first sector (corresponding to slot 0) should start on the radius of the circle extending horizontally to the right of the circle's centre, and extend counter-clockwise. The remaining sectors are numbered in counter-clockwise order starting from this first sector. For example, if self.slots == 6, each sector spans 60 degrees, starting at the "positive x direction" relative to the circle's centre. Each slot has a colour chosen by the provided get_colour function (don't change this function!). See below for some implementation notes. Note: This method will display an empty circle if the number of slots in the spinner is too high for a small screen size. This is normal, and you don't need to fix this. """ height = canvas.winfo_reqheight() width = canvas.winfo_reqwidth() x, y, radius = width // 2, height // 2, width // 4 # Draw each the sector for slot i using the draw_arc function and get_colour method. # You will need to calculate the start and stop angles by doing some math. As a hint, # slot 0's sector has a start angle of 0. angle = 360 / self.slots for i in range(self.slots): draw_arc(canvas, x, y, radius, angle * i, angle * (i + 1), self.get_colour(canvas, i)) def get_colour(self, w: tk.Widget, slot: int) -> str: """Return a unique colour to use for the slot in spinner. The returned colour is grayscale when the slot is not currently selected. Preconditions: - 0 <= slot < spinner.slots - spinner.slots <= len(PREDEFINED_COLOURS) You should not modify this function. """ normalize = len(PREDEFINED_COLOURS) // self.slots selected_colour = PREDEFINED_COLOURS[normalize * slot] if self.position == slot: return selected_colour else: # Convert to RGB tuple selected_colour = w.winfo_rgb(selected_colour) # Convert to grayscale gray_avg = sum(selected_colour[0:3]) // 3 return rgb_to_hex((gray_avg, gray_avg, gray_avg)) #################################################################################################### # Helper functions - DO NOT CHANGE THESE #################################################################################################### def rgb_to_hex(colour: tuple[int, int, int]) -> str: """Return the hex format of the RGB colour.""" r, g, b = colour return f'#{r:02x}{g:02x}{b:02x}' def draw_arc(canvas: tk.Canvas, x: int, y: int, r: int, start_angle: float, stop_angle: float, colour: str) -> None: """Draw a filled (with colour) part of a circle (with radius r) at location (x, y) starting at start_angle and ending at stop_angle (all angles are in degrees). """ canvas.create_arc(x - r, y - r, x + r, y + r, start=start_angle, extent=stop_angle - start_angle, fill=colour) def spin_button_callback(canvas: tk.Canvas, label: tk.Label, spinner: Spinner) -> None: """Spin the spinner and update the canvas to show the change. This function is called when you click the button in the GUI. These types of functions are called "callbacks". """ spinner.spin_randomly() label.configure(text=f'Position: {spinner.position}') spinner.draw(canvas) canvas.update() def run_example(spinner: Spinner) -> None: """Show a window that visualizes a spinner. You can use this function to test your other functions. When you call this function, the spinner will appear in a window. You can spin the spinner randomly by clicking the button. """ window = tk.Tk() window.title('Visualizing the spinner') # Use a frame for organizing the different widgets frame = tk.Frame(window) frame.pack() # Create a canvas and "initialize it" with a drawing of the spinner canvas = tk.Canvas(frame, bg='white', height=500, width=500) spinner.draw(canvas) # Create a label to tell us which position the spinner is at (helps with debugging) label = tk.Label(frame, text=f'Position: {spinner.position}') # Create a button to control the spinner spin_button = tk.Button(frame, text='Spin', command=lambda: spin_button_callback(canvas, label, spinner)) # Organize our widgets canvas.pack(side=tk.BOTTOM) spin_button.pack(side=tk.TOP) label.pack(side=tk.TOP) # Give control of our program over to tkinter window.mainloop() if __name__ == '__main__': import python_ta python_ta.check_all(config={ 'max-line-length': 100, 'extra-imports': ['python_ta.contracts', 'random', 'tkinter'], 'disable': ['R1705', 'C0200'], 'max-args': 7 }) import python_ta.contracts python_ta.contracts.DEBUG_CONTRACTS = False python_ta.contracts.check_all_contracts() import doctest doctest.testmod()