CSC110 / assignments / a3 / a3_part3.py
a3_part3.py
Raw
"""CSC110 Fall 2022 Assignment 3, Part 3: Chaos, Fractals, Point Sequences

Instructions (READ THIS FIRST!)
===============================

This Python module contains the functions you should complete for Part 3.
Note that this module imports a3_helpers.py, which you will need to read
through to understand how to use the functions we've provided for you.

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) 2022 David Liu and Tom Fairgrieve
"""
import random

from pygame import Surface    # According to FAQ I'm allowed to do this.

import a3_helpers


###############################################################################
# Question 1
###############################################################################
def part3_warmup() -> None:
    """Draw a Pygame window and some pixels.

    Specifically:
    - The Pygame window should be 800-by-800 pixels in size
    - You should draw 100 pixels in distinct locations, *using a for loop in some way*
        - For example, a horizontal, vertical, or diagonal line of pixels
    - You can pick whatever colours you like for the pixels; the colours can be
      the same for all pixels, or can be different for different pixels (e.g., a gradient).
      However, the points must all be visible on the screen (so avoid white or very light colours).

    You MUST use the three Pygame helper functions in a3_helpers.py in the "Questions 1-3" section.
    You MAY use the float_to_colour helper function in a3_helpers.py found at the bottom of the file.

    Read through those functions carefully to understand how to use them in your implementation here!
    """
    # 1. Create the pygame screen (initialize_pygame_window)
    w = a3_helpers.initialize_pygame_window(800, 800)

    # 2. Draw your points (using a for loop and the draw_pixel function)
    p = [120, 230, 340, 450, 560, 190, 200, 310, 420, 530]
    # I tried my best to pick colour that is not so bright!
    c = [0.77, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0]
    # 100 pixels
    # I could've used Cartesian product instead :D
    for x in p:
        for y in range(0, len(p)):
            a3_helpers.draw_pixel(w, (x + 50, p[y]), a3_helpers.float_to_colour(c[y]))

    # 3. Wait for the user to close the pygame window (using wait_for_pygame_exit)
    a3_helpers.wait_for_pygame_exit()


###############################################################################
# Question 2
###############################################################################
def generate_point_sequence1(vertex_points: list[tuple[int, int]],
                             initial_point: tuple[int, int],
                             num_points: int) -> list[tuple[int, int]]:
    """Return a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 1" definition.

    Note that initial_point is always INCLUDED in the returned point sequence (as the first point in the sequence).

    Preconditions:
    - len(vertex_points) >= 3
    - vertex_points does not contain duplicates
    - num_points >= 1

    Use the random.randint function to randomly choose an INDEX between 0 and len(vertex_points) - 1, inclusive.
    Here's an example of using this function (note that unlike range, both inputs to randint are INCLUSIVE):

    >>> n = random.randint(0, 5)
    >>> n in range(0, 6)
    True

    # >>> vertex_points = [(100, 100), (700, 100), (700, 700), (100, 700)]
    # >>> initial_point = (500, 500)
    # >>> generate_point_sequence1(vertex_points, initial_point, 4)
    """
    return_l = [initial_point]
    for i in range(0, num_points - 1):
        n = random.randint(0, len(vertex_points) - 1)
        n = vertex_points[n]
        list.append(return_l, (((return_l[i][0] + n[0]) // 2), ((return_l[i][1] + n[1]) // 2)))
    return return_l


def draw_point_sequence1(
        screen_width: int,
        screen_height: int,
        vertex_points: list[tuple[int, int]],
        initial_point: tuple[int, int],
        num_points: int) -> None:
    """
    #############################################################################################################
    # **IMPORTANT**
    # I MADE helper function "h_f_draw_point_sequence1", code taken from this "draw_point_sequence1".
    # I will be using "h_f_draw_point_sequence1" function in user_pattern.
    # For more information, please read "user_pattern".
    #############################################################################################################

    Draw a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 1" definition.

    The Pygame window used to draw the points has dimensions specified by screen_width and screen_height.

    Note that initial_point is always INCLUDED in the drawn points (and is always the first point to be drawn).

    Preconditions:
    - screen_width >= 2
    - screen_height >= 2
    - len(vertex_points) >= 3
    - vertex_points does not contain duplicates
    - all({0 <= vertex[0] < screen_width for vertex in vertex_points})
    - all({0 <= vertex[1] < screen_height for vertex in vertex_points})
    - 0 <= initial_point[0] < screen_width
    - 0 <= initial_point[1] < screen_height
    - num_points >= 1

    NOTES:
    1. You may *not* call generate_point_sequence1 in this function. This is because we don't want you to
       accumulate a list of points, but instead immediately draw each point to the screen.
       This will allow you to draw a very large number of points (e.g., one million) without needing to
       store all of them in computer memory. Plus, good practice with for loop patterns!
    2. That said, your implementation should be very similar to generate_point_sequence1, and in particular use
       random.randint in the same way.
    3. Your implementation should also have the same structure as part3_warmup (particuarly the pygame parts).
    4. Like part3_warmup, you can choose any colours you want, but the points must all be visible on the screen
       (so avoid white or very light colours).

    # >>> vertex_points = [(700, 400), (250, 660), (250, 140)]
    # >>> initial_point = (500, 500)
    # >>> draw_point_sequence1(800, 800, vertex_points, initial_point, 1000)
    To myself in the future: try use a3_helpers.regular_polygon_vertices :D
    """
    w = a3_helpers.initialize_pygame_window(screen_width, screen_height)
    return_l = initial_point
    # This is to include initial_point as well for pygame.
    a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    for _ in range(0, num_points - 1):
        n = random.randint(0, len(vertex_points) - 1)
        n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    a3_helpers.wait_for_pygame_exit()


###############################################################################
# Question 3
###############################################################################
def generate_point_sequence2(vertex_points: list[tuple[int, int]],
                             initial_point: tuple[int, int],
                             num_points: int) -> list[tuple[int, int]]:
    """Return a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 2" definition.

    Note that initial_point is always INCLUDED in the returned point sequence (as the first point in the sequence).

    Preconditions:
    - len(vertex_points) >= 4
    - vertex_points does not contain duplicates
    - num_points >= 1

    HINTS:
    - Use one or more accumulator variables to keep track of the *indexes* of the vertex points
      that have been selected so far in the sequence. (Or at a minimum, the two most recent indexes.)
    - You can elegantly avoid choosing a vertex or its neighbours by generating a *random offset*
      to add to that vertex's index.

      For example, if we have five vertices [v_0, v_1, v_2, v_3, v_4], and the current vertex is v_1,
      we can add either 2 or 3 to the index to obtain v_3 or v_4.
    """
    #################################################################################################################
    # Note: I came up with 3 different ways to solve this question. One were simplified way.
    # One were more effiective way using % remainder. But I wanted this answer to be intuitive as much as possible.
    # So that when I look back, I can easily understand what I did. So I chosed the longer, easy one to answer here!
    #################################################################################################################
    return_l = [initial_point]
    save_vertex = []
    for i in range(0, 3):
        # Just like point sequence 1.
        n = random.randint(0, len(vertex_points) - 1)
        list.append(save_vertex, n)
        n = vertex_points[n]
        list.append(return_l, (((return_l[i][0] + n[0]) // 2), ((return_l[i][1] + n[1]) // 2)))
    for i in range(3, num_points - 1):
        # These codes are to make sure that
        # if same vertex was choosen in pn-1 and pn-2,
        # then we don't choose same vertices of vn-1 and neighbour of vn-1.
        if save_vertex[i - 1] == save_vertex[i - 2] and save_vertex[i - 1] != 0 and save_vertex[i - 1] !=\
                len(vertex_points) - 1:
            # Non-special case.
            n = random.randint(0, len(vertex_points) - 1)
            # According to Professor Liu, we're allowed to use while loop!
            while n in (save_vertex[i - 1] - 1, save_vertex[i - 1], save_vertex[i - 1] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            list.append(save_vertex, n)
            n = vertex_points[n]
            # This is for testing purpose, so that I can use later in the future, please ignore.
            # print('\nTest', save_vertex[i - 1], n, vertex_points)
        elif save_vertex[i - 1] == save_vertex[i - 2] and save_vertex[i - 1] == 0:
            n = random.randint(0, len(vertex_points) - 1)
            # Special case for first vertex.
            while n in (len(vertex_points) - 1, save_vertex[i - 1], save_vertex[i - 1] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            list.append(save_vertex, n)
            n = vertex_points[n]
            # print('\nTest', save_vertex[i - 1], n, vertex_points)        # This is for testing purpose, please ignore.
        elif save_vertex[i - 1] == save_vertex[i - 2] and save_vertex[i - 1] == len(vertex_points) - 1:
            # Special case for last vertex.
            n = random.randint(0, len(vertex_points) - 1)
            while n in (len(vertex_points) - 2, save_vertex[i - 1], 0):
                n = random.randint(0, len(vertex_points) - 1)
            list.append(save_vertex, n)
            n = vertex_points[n]
            # print('\nTest', save_vertex[i - 1], n, vertex_points)        # This is for testing purpose, please ignore.
        else:
            # Keep it like point_sequence1 when condition is not met.
            n = random.randint(0, len(vertex_points) - 1)
            list.append(save_vertex, n)
            n = vertex_points[n]
        list.append(return_l, (((return_l[i][0] + n[0]) // 2), ((return_l[i][1] + n[1]) // 2)))
    return return_l


def draw_point_sequence2(
        screen_width: int,
        screen_height: int,
        vertex_points: list[tuple[int, int]],
        initial_point: tuple[int, int],
        num_points: int) -> None:
    """
    #############################################################################################################
    # **IMPORTANT**
    # I MADE helper function "h_f_draw_point_sequence2", code taken from this "draw_point_sequence2".
    # I will be using "h_f_draw_point_sequence2" function in user_pattern.
    # For more information, please read "user_pattern".
    #############################################################################################################

    Draw a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 2" definition.

    The Pygame window used to draw the points has dimensions specified by screen_width and screen_height.

    Note that initial_point is always INCLUDED in the drawn points (and is always the first point to be drawn).

    Preconditions:
    - screen_width >= 2
    - screen_height >= 2
    - len(vertex_points) >= 4
    - vertex_points does not contain duplicates
    - all({0 <= vertex[0] < screen_width for vertex in vertex_points})
    - all({0 <= vertex[1] < screen_height for vertex in vertex_points})
    - 0 <= initial_point[0] < screen_width
    - 0 <= initial_point[1] < screen_height
    - num_points >= 1

    NOTES:
    1. You may *not* call generate_point_sequence2 in this function. This is because we don't want you to
       accumulate a list of points, but instead immediately draw each point to the screen.
       This will allow you to draw a very large number of points (e.g., one million) without needing to
       store all of them. Plus, good practice with for loop patterns!
    2. That said, your implementation should be similar to generate_point_sequence2, and in particular use
       random.randint in the same way.
    3. Your implementation should also have the same structure as part3_warmup (particuarly the pygame parts).
    4. Like part3_warmup, you can choose any colours you want, but the points must all be visible on the screen
       (so avoid white or very light colours).

    To myself in the future: try use a3_helpers.regular_polygon_vertices :D
    Try running:
    # >>> draw_point_sequence2(800, 800, a3_helpers.regular_polygon_vertices(800, 800, 200, 5), (500, 500), 1000000)
    """
    w = a3_helpers.initialize_pygame_window(screen_width, screen_height)
    return_l = initial_point
    a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    # I will only save up to 2 elements!
    # This is to save memory as much as possible.
    save_vertex = [0, 0]
    for _ in range(0, 3):
        n = random.randint(0, len(vertex_points) - 1)
        save_vertex[0] = save_vertex[1]
        save_vertex[1] = n
        n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    for _ in range(3, num_points - 1):
        if save_vertex[0] == save_vertex[1] and save_vertex[0] != 0 and save_vertex[0] != \
                len(vertex_points) - 1:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (save_vertex[0] - 1, save_vertex[0], save_vertex[0] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        elif save_vertex[0] == save_vertex[1] and save_vertex[0] == 0:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (len(vertex_points) - 1, save_vertex[0], save_vertex[0] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        elif save_vertex[0] == save_vertex[1] and save_vertex[0] == len(vertex_points) - 1:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (len(vertex_points) - 2, save_vertex[0], 0):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        else:
            n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    a3_helpers.wait_for_pygame_exit()


###############################################################################
# Question 4
###############################################################################
def verify_point_sequence1(vertex_points: list[tuple[int, int]],
                           initial_point: tuple[int, int],
                           points: list[tuple[int, int]]) -> bool:
    """Return whether the given points could have been generated as "Point Sequence 1" for the
    given vertex_points and initial_point.

    This should follow the same approach as the manual testing strategy described in Question 2
    on the assignment handout.

    Preconditions:
    - len(vertex_points) >= 3
    - vertex_points does not contain duplicates
    - len(points) >= 1

    NOTE: You may use ANY COMBINATION of for loops and comprehensions (including just one of them)
    to implement this function.
    """
    score = 0
    check_possibilties = [initial_point]
    if points[0] == initial_point:
        for i in points:
            # Calc. possible midpoints.
            possibilties = []
            for j in vertex_points:
                list.append(possibilties,
                            (((i[0] + j[0]) / 2), ((i[1] + j[1]) / 2)))
            # Check the possibilties.
            if points[i] in check_possibilties:
                score += 1
            # I don't need this necessarily, but why not. Let's add this when points[i] is not in check_possibilties.
            else:
                score -= 1
            check_possibilties = possibilties
    return len(points) == score


###############################################################################
# Question 5
###############################################################################
def h_f_draw_point_sequence1(
        w: Surface,
        vertex_points: list[tuple[int, int]],
        initial_point: tuple[int, int],
        num_points: int) -> None:
    """
    #############################################################################################################
    # **IMPORTANT**
    # I MADE helper function "h_f_draw_point_sequence1", code taken from "draw_point_sequence1".
    # I will be using "h_f_draw_point_sequence1" function in user_pattern.
    # For more information, please read "user_pattern".
    #############################################################################################################

    Draw a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 1" definition.

    The Pygame window used to draw the points has dimensions specified by screen_width and screen_height.

    Note that initial_point is always INCLUDED in the drawn points (and is always the first point to be drawn).

    Preconditions:
    - screen_width >= 2
    - screen_height >= 2
    - len(vertex_points) >= 3
    - vertex_points does not contain duplicates
    - all({0 <= vertex[0] < screen_width for vertex in vertex_points})
    - all({0 <= vertex[1] < screen_height for vertex in vertex_points})
    - 0 <= initial_point[0] < screen_width
    - 0 <= initial_point[1] < screen_height
    - num_points >= 1
    """
    return_l = initial_point
    a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    for _ in range(0, num_points - 1):
        n = random.randint(0, len(vertex_points) - 1)
        n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))


def h_f_draw_point_sequence2(
        w: Surface,
        vertex_points: list[tuple[int, int]],
        initial_point: tuple[int, int],
        num_points: int) -> None:
    """
    #############################################################################################################
    # **IMPORTANT**
    # I MADE helper function "h_f_draw_point_sequence2", code taken from "draw_point_sequence2".
    # I will be using "h_f_draw_point_sequence2" function in user_pattern.
    # For more information, please read "user_pattern".
    #############################################################################################################

    Draw a point sequence of length num_points generated by the given vertex_points and initial_point,
    using the "Point Sequence 2" definition.

    The Pygame window used to draw the points has dimensions specified by screen_width and screen_height.

    Note that initial_point is always INCLUDED in the drawn points (and is always the first point to be drawn).

    Preconditions:
    - screen_width >= 2
    - screen_height >= 2
    - len(vertex_points) >= 4
    - vertex_points does not contain duplicates
    - all({0 <= vertex[0] < screen_width for vertex in vertex_points})
    - all({0 <= vertex[1] < screen_height for vertex in vertex_points})
    - 0 <= initial_point[0] < screen_width
    - 0 <= initial_point[1] < screen_height
    - num_points >= 1
    """
    return_l = initial_point
    a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    save_vertex = [0, 0]
    for _ in range(0, 3):
        n = random.randint(0, len(vertex_points) - 1)
        save_vertex[0] = save_vertex[1]
        save_vertex[1] = n
        n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))
    for _ in range(3, num_points - 1):
        if save_vertex[0] == save_vertex[1] and save_vertex[0] != 0 and save_vertex[0] != \
                len(vertex_points) - 1:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (save_vertex[0] - 1, save_vertex[0], save_vertex[0] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        elif save_vertex[0] == save_vertex[1] and save_vertex[0] == 0:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (len(vertex_points) - 1, save_vertex[0], save_vertex[0] + 1):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        elif save_vertex[0] == save_vertex[1] and save_vertex[0] == len(vertex_points) - 1:
            n = random.randint(0, len(vertex_points) - 1)
            while n in (len(vertex_points) - 2, save_vertex[0], 0):
                n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        else:
            n = random.randint(0, len(vertex_points) - 1)
            save_vertex[0] = save_vertex[1]
            save_vertex[1] = n
            n = vertex_points[n]
        return_l = (((return_l[0] + n[0]) // 2), ((return_l[1] + n[1]) // 2))
        a3_helpers.draw_pixel(w, (return_l[0], return_l[1]), (0, 0, 0))


def user_pattern(screen_width: int, screen_height: int, num_vertices: int, sequence_type: int, num_points: int) -> None:
    """
    #############################################################################################################
    # **IMPORTANT**
    # I MADE helper function "h_f_draw_point_sequence1" and "h_f_draw_point_sequence2",
    # code taken from draw_point_sequence1 and draw_point_sequence2.
    # I will be using those 2 helper function in user_pattern.
    # This is to avoid re-initializing when just calling draw_point_sequence1 and
    # draw_point_sequence2 according to Professor Liu on Campuswire: https://campuswire.com/c/GB134F106/feed/260
    #############################################################################################################

    Display an interactive Pygame window that lets a user create their own point sequence patterns.

    Specifically, this function:

    1. Creates a Pygame window with the given screen width and height.
    2. Waits for the user to click on the window (num_vertices + 1) times.
    3. Draws a point sequence where:
        - the vertex points are the locations of the first <num_vertices> user clicks
            - YOU MAY ASSUME the user clicks <num_vertices> unique points
        - the initial point is the location of the last user click
        - the sequence type is either "Point Sequence 1" or "Point Sequence 2", depending on whether
          sequence_type == 1 or sequence_type == 2.
        - the sequence length is num_points
    4. After the drawing is complete, waits for the user to close the Pygame window.

    Preconditions:
    - screen_width >= 100
    - screen_height >= 100
    - sequence_type in {1, 2}
    - if sequence_type == 1 then num_vertices >= 3
    - if sequence_type == 2 then num_vertices >= 4
    - num_points >= 1

    HINTS:
        - Use the helper function input_mouse_pygame found in a3_helpers.py.
          This is the same function you were introduced to in Tutorial 5.
    """
    w = a3_helpers.initialize_pygame_window(screen_width, screen_height)
    vertexes = []
    for _ in range(0, num_vertices + 1):
        list.append(vertexes, a3_helpers.input_mouse_pygame())
    inital_point = vertexes[len(vertexes) - 1]
    list.pop(vertexes, len(vertexes) - 1)
    if sequence_type == 1:
        h_f_draw_point_sequence1(w, vertexes, inital_point, num_points)
    elif sequence_type == 2:
        h_f_draw_point_sequence2(w, vertexes, inital_point, num_points)
    a3_helpers.wait_for_pygame_exit()


# When you are ready to check your work with python_ta, uncomment the following lines.
# (In PyCharm, select the lines below and press Ctrl/Cmd + / to toggle comments.)
if __name__ == '__main__':
    import python_ta
    python_ta.check_all(config={
        'max-line-length': 120,
        'extra-imports': ['random', 'a3_helpers', 'pygame'],
    })