Warning
This page is located in archive.

Final Exam Information

The final exam will be composed of two major parts:

  • Practical part (40 points)
  • Oral part (10 points; +-5 points from the practical part, see below)

Each exam term will start in the morning (typically at 9:00) with the practical part for which you will have 2 hours. Then, after some break, individual oral exam will take place.

When you register for an exam date in KOS, you will only register to the practical part. You will then be given a schedule for the oral part (typically starting at 12:00 or 13:00 and divided into 15 or 20 minute windows). If you have some strict time restrictions (for the oral part), please, send me an email when you are registering in KOS. I will try to accommodate the schedule if possible.

Practical part

Practical part will be similar to midterm. You will be given three different tasks, each with some sub-parts for you to solve and implement. Each task will have a different scoring, described in the corresponding file, so you can choose which tasks/parts to focus on. Even though this part will involve mainly programming, it is recommended that you bring a pen and paper. Some tasks might be easier to solve if you first draw a picture and “reason” about the solution, before you try to implement it.

You will have access to your solutions of the previous assignments in BRUTE and all the lecture materials. You will be provided with some cheatsheet of Python basics. For more conditions, see the course main page.

For examples of the practical part, see the 'Practical Exam Examples' section below.

For additional resources on tasks to solve, you can look at:

  • LeetCode page (look for easy and mid difficulty tasks).
  • PraticeProbs - look at only the Python/NumPy tasks. Here is a link with Python&NumPy specific filter.

For a quiz on Python programming knowledge, you can visit W3Schools page.

Code (with supplementary materials, like input files) for the example tasks for practical exam can be found at the course Gitlab: Exam Examples

Oral part

The oral part will take about 15 minutes per student and will be split into two parts:

  • Discussion about the your solution for the practical part
  • Theoretical question(s) from programming.

In the first part, we will shortly discuss some part of your solution or some 'missing' parts from the practical exam. You can gain or lose up to 5 points (from the practical part), depending on your answers. Losing points can happen if you will not be able to explain why/how some parts of your solution work. On the other hand, you can gain up to 5 points if you can describe a viable solution to some part of a task from the practical part.

In the second part, you will be asked a theoretical question about a concept from programming. The questions will be from the following topics:

  • Properties of basic data types - how are they represented (in memory), what operations they support, etc.
  • Data structures (ADTs and their implementations) - their properties and uses
  • Algorithmic complexity - some 'brief' analysis of an algorithm
  • 'Conceptual' solving of a programming problem - i.e., what approach/data structure would you use and why; what would be the properties (pros & cons) of the solution

Practical Exam Examples

Here, you can find some examples of tasks for the practical part of the final exam.

Practical Exam Structure

Each exam will be composed of three tasks, each from one of the following three topics:

  1. PyGame
  2. Data structures (stack, queue, tree, dictionary, etc.) and recursion
  3. Array and string manipulation

You are only expected to be able to use 'pure' Python and NumPy and all tasks are designed to be solvable using only these. Nonetheless, the task template scripts will likely use additional libraries for visualization, data loading, etc. (see the examples below). However, your solution is not limited to pure Python and NumPy. You can also use other common libraries. Here is the complete list of libraries that you can use in your solution:

These will be presented in the environment created for the exam.

PyGame

The task for this topic will be to finish an implementation of a simple game, created in the `PyGame` framework. Don't worry, all the PyGame stuff (visualization, input handling, 'game loop', etc.) is already implemented. Your task is to 'just' implement the game logic. This will typically involve some of the following computations and processing:

  • collision detection (typically distance between circles or overlap of rectangles),
  • equations to implement the “physics” (just very simple equations to adjust the position of the game objects),
  • boundary checks (e.g., “is the object's [X, Y] position out of the screen?”),
  • object “handling”: storing, iterating over and removal of objects (as in OOP objects) in the appropriate structures,
  • and, of course, score counting and checking for “game over” condition.

This task will typically involve implementation of several smaller functions, each worth a small part of the total points. To 'test' your solution during the exam, you can just run the code and see whether the game behaves as required. In addition to the description of the game, there will be a short video/GIF to show you how the end-product should look like. It is recommended to use printing of the desired values or debug breakpoints to halt the code during execution, to see what is happening.

You should familiarize yourself with the typical code structure of a PyGame game (you do not need to understand the syntax in-depth; this is to test your ability to understand and complement an existing code). Mainly, it is important to know that the objects have some positions (typically 2D), maybe an orientation and some other properties that will be of standard Python types (numbers, strings). By changing these, the object will change their appearance and interaction. These properties will always be explained, i.e., you do not need to 'decode' their purpose from the code.

To help you with the coordinate system in PyGame vs matrix (2D array) indexing, here is a useful graphic: The X/Y axis show the coordinate system used by PyGame. That is, objects are drawn in this coordinate system (mouse coordinates are also in this system). The row/col show the direction of row and column coordinates in “classical” matrices (e.g., when you iterate of it).

Below are examples of assignments:

Asteroids

Finish the implementation to create an adaptation of the classic game “Asteroids”. Here is a short preview of how the game should look like:  asteroids video

""" Asteroids Game
 
This is a simple game implemented using Pygame. Do not worry about the PyGame framework.
All needed to draw/handle inputs is already provided. Your task is to implement the "game logic"
using just pure Python and NumPy (you can use other libraries, if you want to).
 
Look for TODO and FIXME comments in the code to find where you should put your code.
The NOTE comments are there to provide additional information or hints.
 
Remember, in Pygame, the origin (0, 0) is in the top-left corner of the screen. The y-axis points
downwards, and the x-axis points to the right.
Going down means increasing the y-coordinate,
going right means increasing the x-coordinate,
going up means decreasing the y-coordinate,
going left means decreasing the x-coordinate.
 
Your tasks are:
 
1) Implement ship movement using WASD keys (`Ship.move` method)
    - on key press, move the ship in the corresponding direction
    - make sure the ship doesn't go out of the screen
 
2) Implement asteroid spawning at random edge positions (`spawn_asteroid` method)
    - spawn a new asteroid at a random screen edge position
    - compute the velocity of the asteroid towards the ship's current position
    - the asteroid must move at a constant speed (ASTEROID_SPEED)
 
3) Implement asteroid collision with the ship (`check_collisions` method)
    - check all asteroids for collision with the ship
    - remember, asteroids and the ship are circles (with some radius)
 
4) Implement shooting (`handle_shooting` method)
    - on mouse click, check if the mouse is close to an asteroid (less than SHOOT_RADIUS)
    - if so, remove the asteroid and increase the destroyed_count by 1
"""
import pygame
import sys
import numpy as np
import random
 
 
# Initialize Pygame
pygame.init()
 
# Screen dimensions
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Asteroids Game")
 
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
SHIP_COLOR = (0, 255, 0)
ASTEROID_COLOR = (255, 0, 0)
 
# Game settings
FPS = 60
SHIP_RADIUS = 15
ASTEROID_RADIUS_MIN = 10
ASTEROID_RADIUS_MAX = 70
ASTEROID_SPEED = 2
SPAWN_INTERVAL = 1000  # milliseconds
SHOOT_RADIUS = 50
SHOOT_COOLDOWN = 1000  # milliseconds
SHIP_SPEED = 5
 
# Initialize clock
clock = pygame.time.Clock()
 
 
# Ship class
class Ship:
    def __init__(self):
        self.x = SCREEN_WIDTH // 2  # initial x ship's position
        self.y = SCREEN_HEIGHT // 2  # initial y ship's position
 
    def draw(self):  # method to draw the ship
        pygame.draw.circle(screen, SHIP_COLOR, (int(self.x), int(self.y)), SHIP_RADIUS)
 
    def move(self, keys_pressed):
        # TODO: Update ship's position based on WASD keys
        # NOTE: make sure the ship doesn't go out of the screen
        # NOTE: the position of the ship is stored in self.x and self.y
        if keys_pressed[pygame.K_w] or keys_pressed[pygame.K_UP]:  # W/UP key
            pass # TODO: move the ship up by SHIP_SPEED
        if keys_pressed[pygame.K_s] or keys_pressed[pygame.K_DOWN]:  # S/DOWN key
            pass # TODO: move the ship down by SHIP_SPEED
        if keys_pressed[pygame.K_a] or keys_pressed[pygame.K_LEFT]:  # A/LEFT key
            pass # TODO: move the ship left by SHIP_SPEED
        if keys_pressed[pygame.K_d] or keys_pressed[pygame.K_RIGHT]:  # D/RIGHT key
            pass # TODO: move the ship right by SHIP_SPEED
 
 
# Asteroid class
class Asteroid:
    def __init__(self, x, y, dx, dy):
        self.x = x  # initial x position
        self.y = y  # initial y position
        self.dx = dx  # x velocity
        self.dy = dy  # y velocity
        self.radius = np.random.randint(ASTEROID_RADIUS_MIN, ASTEROID_RADIUS_MAX)
 
    def update(self):
        self.x += self.dx
        self.y += self.dy
 
    def draw(self):
        pygame.draw.circle(screen, ASTEROID_COLOR, (int(self.x), int(self.y)), self.radius)
 
    def is_off_screen(self):
        # TODO: return True if the asteroid is off the screen
        # NOTE: the position of the asteroid is stored in self.x and self.y
        # NOTE: screen dimensions are WIDTH and HEIGHT
        return False
 
 
class Game:
 
    def __init__(self):
        self.ship = Ship()
        self.asteroids = []
        self.shoot_cooldown = 0
        self.destroyed_count = 0
        self.last_spawn_time = pygame.time.get_ticks()
        self.running = True
 
    def spawn_asteroid(self):  # Function to spawn a new asteroid
        # TODO: Implement asteroid spawning at random edge positions
        # and set its velocity towards the ship's current position (dx, dy)
        # the speed can be computed as a vector from the ship to the asteroid
        # the length (norm) of the vector must be ASTEROID_SPEED (np.linalg.norm([dx, dy]) == ASTEROID_SPEED)
        # Hint:
        #   vector_from_A_to_B = [Ax - Bx, Ay - By]
        #   vector / sum(vector) = <unit vector>
        #   <unit vector> * x = <vector with length x>
        asteroid = Asteroid(0, 0, 0, 0)  # FIXME: change to the newly spawned asteroid
        self.asteroids.append(asteroid)
 
    # Function to handle shooting
    def handle_shooting(self):
        mouse_pos = pygame.mouse.get_pos()
        pygame.draw.circle(screen, (0, 0, 255), (mouse_pos[0], mouse_pos[1]), SHOOT_RADIUS)
        # TODO: Check for asteroids within SHOOT_RADIUS of mouse_pos
        # NOTE: If found/hit, remove the first such asteroid
        # NOTE: If hit, increase self.destroyed_count by 1
 
    # Function to check for collisions between ship and asteroids
    def check_collisions(self):
        # TODO: Detect collision between ship and any asteroid
        # Return True if collision occurs (i.e. game over)
        return False
 
    def move_ship(self):
        keys_pressed = pygame.key.get_pressed()
        self.ship.move(keys_pressed)
 
    def update(self):  # DO NOT MODIFY
        # Spawn asteroids at intervals
        current_time = pygame.time.get_ticks()
        if current_time - self.last_spawn_time >= SPAWN_INTERVAL - self.destroyed_count * 10:
            self.spawn_asteroid()
            self.last_spawn_time = current_time
 
        # Update and draw asteroids
        for asteroid in self.asteroids[:]:
            asteroid.update()
            asteroid.draw()
            if asteroid.is_off_screen():
                self.asteroids.remove(asteroid)
                self.spawn_asteroid()
 
        # Check for collisions
        if self.check_collisions():
            self.running = False
 
        # Draw ship
        self.ship.draw()
 
    @property
    def existing_count(self):
        return len(self.asteroids)
 
 
def draw_intro_screen():
    # Starfield settings
    NUM_STARS = 100
    stars = []
    for _ in range(NUM_STARS):
        stars.append({
            'x': random.randint(0, SCREEN_WIDTH),
            'y': random.randint(0, SCREEN_HEIGHT),
            'speed': random.uniform(0.5, 3.5),
            'size': random.randint(1, 3)
        })
 
    # Fonts
    title_font = pygame.font.Font(None, 72)
    instruction_font = pygame.font.Font(None, 36)
 
    def draw_text(text, font, color, y_offset=0):
        text_surface = font.render(text, True, color)
        text_rect = text_surface.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + y_offset))
        screen.blit(text_surface, text_rect)
 
    # Main intro loop
    running = True
    clock = pygame.time.Clock()
 
    while running:
        # Event handling
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type in (pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN):
                running = False
 
        # Update stars
        for star in stars:
            star['y'] += star['speed']
            if star['y'] > SCREEN_HEIGHT:
                star['y'] = 0
                star['x'] = random.randint(0, SCREEN_WIDTH)
 
        # Draw everything
        screen.fill(BLACK)
 
        # Draw starfield
        for star in stars:
            pygame.draw.circle(screen, WHITE, (star['x'], int(star['y'])), star['size'])
 
        # Draw text
        draw_text("ASTEROIDS", title_font, WHITE)
        draw_text("Press any key to continue", instruction_font, WHITE, 100)
 
        pygame.display.flip()
        clock.tick(30)
 
 
# Main game function
def main():
    draw_intro_screen()
    game = Game()
 
    last_shoot_time = 0
 
    while game.running:
        clock.tick(FPS)
        screen.fill(BLACK)
 
        # Event handling
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game.running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                current_time = pygame.time.get_ticks()
                if current_time - last_shoot_time >= SHOOT_COOLDOWN:
                    game.handle_shooting()
                    last_shoot_time = current_time
 
        game.move_ship()
        game.update()
 
        # Display destroyed asteroid count
        font = pygame.font.SysFont(None, 36)
        text_destroyed = font.render(f"Destroyed: {game.destroyed_count}", True, WHITE)
        font = pygame.font.SysFont(None, 24)
        text_asteroids = font.render(f"Current Asteroids: {game.existing_count}", True, WHITE)
        text_ship_pos = font.render(f"Ship pos: [{game.ship.x:.0f}, {game.ship.y:.0f}]", True, WHITE)
        screen.blit(text_asteroids, (10, 10))
        screen.blit(text_destroyed, (10, 30))
        screen.blit(text_ship_pos, (SCREEN_WIDTH - 200, 10))
 
        keys_pressed = pygame.key.get_pressed()
        if keys_pressed[pygame.K_q]:  # Q key
            game.running = False
 
        pygame.display.flip()
 
    pygame.quit()
    print(f"Collision detected! Game Over. Destroyed: {game.destroyed_count}")
    sys.exit()
 
 
if __name__ == "__main__":
    main()

ADT & recursion

In this topic, your task will be to implement an algorithm to create, modify or utilize some data structure, like stack, tree, etc. You might be provided with some data as and input to “fill-into” the structure. You might be also required to use the structure to solve some problem. In this case, recursion might be useful to solve the problem elegantly.

Containers

The JSON file for this task can be found at the course gitlab: scene.json

""" Counting objects in a scene description dictionary
 
Look for TODO and FIXME comments in the code to find where you should put your code.
The NOTE comments are there to provide additional information or hints.
Functions marked with 'DO NOT MODIFY' are there as helper functions
for visualization, data loading, etc. You can ignore them.
 
Input:
    A list of dictionaries with "scene description" loaded from a JSON file.
    At the 'top level', the data contains a list of 'objects'. Each object is a dictionary with the following fields:
        - name: object name
        - type: some class of household objects or a container
        - color: some basic color
    If the object type is a container, that is, the type is one of the following: 'box', 'drawer', 'cabinet',
    it also has a field 'contents' which is a list of contained objects.
    These can be, again, some objects or containers. In both cases, the object are again, dictionaries.
 
Output:
    Count of objects of the required color that are at least the the given "depth" (with top level being 0 depth).
 
Task:
    The task to solve is to create an algorithm that will count all objects of the required color
    that are at least at the given "depth" of nesting (with top level being 0 depth).
    That is, an object at depth 1 means that it is inside a 'standalone' container
    (i.e., a container that is NOT inside any other container).
    An object at depth 2 means that it is inside a container that is inside another container.
 
    It is probably a good idea to use recursion to solve this problem (but not required).
 
"""
 
import json
import os
 
 
def load_data(filename):
    # Load the scene data from the JSON file
    with open(filename, 'r') as f:
        scene_data = json.load(f)
    return scene_data
 
 
def count_objects_by_color(scene_data, target_color, min_depth) -> int:
    """
    Count objects of the required color that are at least the the given "depth" (with top level being 0 depth).
 
    Args:
        scene_data (list): A list of dictionaries with "scene description"
        target_color (str): The color to count
        min_depth (int): The minimum depth to count objects at
 
    Returns:
        int: The count of objects of the required color
    """
    # TODO: Implement the count_objects_by_color function
    # NOTE: scene_data a list of dictionaries.
    # NOTE: target_color is a string, min_depth is an integer
    # NOTE: Iterate through each top-level object
    # search for objects with the specified color
    # and count objects at depth >= min_depth
 
    total_count = 0
    # FIXME: compute the actual total_count
    return total_count
 
 
scene_dictionary = load_data(os.path.join(os.path.dirname(__file__), 'scene.json'))
# Define the target color and minimum depth
target_color = 'red'
min_depth = 3
 
# Iterate through each top-level object and count matching objects
total_count = count_objects_by_color(scene_dictionary, target_color, min_depth)
 
print(f"Total number of '{target_color}' objects at depth >= {min_depth}: {total_count}")
# Expected output: 'Total number of 'red' objects at depth >= 3: 237'

Arrays & Strings

In this topic, the tasks will be focused on 'iterables', like strings, arrays, lists, tuples, etc. Your task will be to manipulate these to achieve the desired output. Practice array & list creation, indexing and slicing, determining and modifying array shape, iteration, and basic array methods. For strings, you will typically need to extract some information from them or modify them in some way. You should practice basic string methods, iterating over strings, accessing parts of strings.

Image Downscaling

""" Image Downscaling via Block Averaging
 
Look for TODO and FIXME comments in the code to find where you should put your code.
The NOTE comments are there to provide additional information or hints.
Functions marked with 'DO NOT MODIFY' are there as helper functions
for visualization, data loading, etc. You can ignore them.
 
Input:
    An image represented as a 2D NumPy array.
 
Output:
    An image represented as a 2D NumPy array.
    Test your script by comparing it to the sample image.
 
Task:
    Divide the image into non-overlapping blocks of size k x k
    and replace each block with its average value to achieve "downscaling".
    The input will be an image (2D matrix) of size `M x N`
    and the output will be an image (2D matrix) of size `M/k x N/k`.
    You can assume the image dimensions will be divisible by `k`.
 
Hints:
    array.shape - will give you sizes of each dimension of the array
    array.reshape - will change the "shape" of the array
    array.mean(axis=...) - will compute the mean along the given axis
    array[start:stop:step] - will give you a slice of the array
 
    Multidimensional indexing of NumPy arrays is done in a single square brackets,
    where you specify the indices or slices for each dimension:
    array[indices_1, indices_2, ...]
"""
 
import numpy as np
import matplotlib.pyplot as plt
 
 
def block_average(img, block_size):
    """
    Downscale an image by averaging over non-overlapping blocks.
 
    Args:
        img: np.ndarray of shape (H, W, C); C is the number of color channels (1 for grayscale, 3 for RGB)
        block_size: int, size of the block
 
    Returns:
        compressed_img: np.ndarray of shape (H//block_size, W//block_size, C)
    """
    # TODO: Iterate of the image blocks and replace each block with its average
    # NOTE: you should probably get the image dimensions (height, width) and the number of channels
    # NOTE: then "slice" the image into non-overlapping blocks and compute their averages
 
    # FIXME: replace the following line with the actual code
    compressed_img = np.zeros((img.shape[0] // block_size, img.shape[1] // block_size, img.shape[2]))
    return compressed_img
 
 
def upscale_image(img, block_size):  # DO NOT MODIFY
    """
    Upscale the downscale image back to original size for visualization.
    """
    return np.kron(img, np.ones((block_size, block_size, 1)))
 
 
# Generate a synthetic test image (e.g., gradient)
def generate_test_image(height, width):  # DO NOT MODIFY
    """
    Generate a synthetic RGB image with horizontal and vertical gradients.
    """
    x = np.linspace(0, 1, width)
    y = np.linspace(0, 1, height)
    xv, yv = np.meshgrid(x, y)
    colors = np.clip(0.5 * np.ones_like(xv) + np.random.randn(*xv.shape) * 0.5, 0, 1)
    img = np.stack((xv, yv, colors), axis=2)
    return img
 
 
if __name__ == "__main__":
    # Parameters
    block_size = 32
    height, width = 256, 256
 
    # Generate test image
    original_img = generate_test_image(height, width)
 
    # Downscaled image
    compressed_img = block_average(original_img, block_size)
 
    # Upscale compressed image for visualization
    upscaled_img = upscale_image(compressed_img, block_size)
 
    # Plot original and downscale images
    fig, axes = plt.subplots(1, 2, figsize=(10, 5))
    axes[0].imshow(original_img)
    axes[0].set_title("Original Image")
    axes[0].axis('off')
 
    axes[1].imshow(upscaled_img)
    axes[1].set_title(f"Downscaled Image (Block Size: {block_size})")
    axes[1].axis('off')
 
    plt.tight_layout()
    plt.show()

courses/be5b33pge/exam/start.txt · Last modified: 2025/05/26 00:22 by skovirad