Comp 110 Mystery on the Hogwarts Express

The entire COMP110 teaching team took a ride on the Hogwarts Express. Everyone was having a splendid time until A HEINOUS CRIME WAS COMMITTED.

The trolley witch was pushing the Honeydukes Express food cart, full of delicious treats like Cauldron Cakes, Chocolate Frogs, and Jelly Slugs.

Then, suddenly, out of nowhere, a member of our team STOLE all of the Bertie Bott's Every Flavour Beans from the trolley cart.

Officers from the Department of Magical Law Enforcement meticulously recorded data from the crime scene. Every suspicious UTA was isolated in an individual train car and each train car's properties were recorded in a CSV data file. This is where you come in as the wizard crime scene data investigator.

The officers were only left with a single anonymous tip:

The note in the 5th yellow train car may contain vital information.

And so, your search to find the thief begins...

Department of Magical Law Enforcement's Notes About the Data File

Each Car on the train has the following properties:

  • carriage - the identifying train car number which you can think of as a serial number
  • person - the name of the person in the train car
  • color - the name of the color of the train car
  • topHatch - TRUE or FALSE: does this train car have a top hatch a person can enter or exit from?
  • weapon - any weaponry the person in the train car is armed with
  • note - any note the person in the train car had on their body

Learning Objectives

  1. Gain comfort and familiarity with writing recursive functions
  2. Practice the patterns learned in class for building lists recursively that serve as filtering or mapping functions
  3. Practice generalizing functions through the use of parameters
  4. Compose more sophisticated, specific functions by making use of simpler, more generic functions

Part 0. Starting the Dev Environment

As with in lecture, begin by opening up VSCode, ensuring your project is still open, and then running the following two commands in the VSCode Integrated Terminal:

  1. npm run pull
  2. npm start

The first command "pulls" the latest files needed for COMP110 into your repo, if any. The second command starts the development environment and will open your web browser to the local server. 

Part 1. Setting up an App

In the same directory used in lecture:

  1. Right click on the "src" folder
  2. Select "New Folder"
  3. Name the new folder: ps02-hogwarts-express
    1. Note: capitalization and punctuation are important!
  4. Right click on the folder you just created
  5. Select "New File"
  6. Name the new file: index-app.ts
    1. Note: capitalization and punctuation are important!
    2. Note: the i in index is lowercase!
  7. You should see a CSV data file in the data/ folder of the project named ps02-train-data.csv. This will be the data file you use to test your code.

Part 2. Starting your Code

2.0 Honor Code Header

All problem sets begin with an Honor Code pledge. Notice this header is contained within block comment tags discussed in Lecture 1. You can copy paste the pledge below and fill in your name and ONYEN.

/**
 * Author:
 *
 * ONYEN:
 *
 * UNC Honor Pledge: I certify that no unauthorized assistance has been received
 * or given in the completion of this work. I certify that I understand and
 * could now rewrite on my own, without assistance from course staff,
 * the problem set code I am submitting.
 */

2.1 Imports

The next step after ensuring the honor code header is to import the functions we'll use from the introcs library (i.e. printimage, and so on) and its List library. The first line of code to add to your app is the following:

import { print, csvToList } from "introcs";
import { List, cons, first, rest, listify } from "introcs/list";

2.2 The Car class and main Function

Each Car of our train's data file will be an object modeled using the Car class below. You should review classes and properties in Lecture 8 for more on classes and objects.

class Car {
    carriage: number = 0;
    person: string = "";
    color: string = "";
    topHatch: boolean = false;
    weapon: string = "";
    note: string = "";
}
 
export let main = async () => {
   let data: List<Car> = await csvToList("Hogwarts Crime Data", Car);
   print(data); // TODO: Remove this line once your data is printed
   // TODO: Your function calls go inside of this block
};
 
// TODO: Define your functions here
main();

Notice that we are establishing a variable named data that is a List of Car objects loaded from a CSV. You can find the ps02-train-data.csv in the data folder.

Clue 1 - Walk

The initial clue 0 comes from an anonymous tip: The note in the 5th yellow train car may contain vital information.

The way we will approach this problem is to break it down into simple functions for each step of this process, then use those simpler functions to solve for the complete clue.

1.1 filterYellow - Declare and export a function named filterYellow. Given a List of Car objects as an argument, return a List of all Car objects whose color property is yellow. Test by calling this function from the main function and printing its results:

let filtered: List<Car> = filterYellow(data);
print(filtered);

1.2 nth - Declare and export a function named nth. The purpose of this function is to return the n'th Car of the List using 0 as the first index. The nth function is given two arguments: 1) a List of Car objects, 2) an index number. The nth function must return a Car object. For example, nth(data, 0) should return the first Car in the data List, nth(data, 1) should return the second Car in the data List.

The base case for this recursive function is when the List parameter is an empty List. This means the index you are looking for does not exist. In this case, throw an Error with message "index does not exist" as shown in Lecture 8.

The recursive case for this function is when the List parameter is not empty. In this case, make the recursive call with the rest of the List and the current index minus one.

The special base case for this function is when the index is 0. In this case, return the first Car in the List.

Check for understanding: once you have this function implemented, convince yourself how the recursive steps of this function work.

1.3 findClue1 - Declare and export a function named findClue1. The purpose of this function is to find the first clue. This function should be given a List of Car objects as a parameter and return a string. The string this function returns should be the contents of the note property of the 5th yellow train Car in the input data. (Note: remember, the 5th car will have index 4 because we start indexing from 0!) Your findClue1 function should make use of both your filterYellow function and the nth function by calling each. An example usage of this function from your main function:

let clue1: string = findClue1(data);
print("Clue 1: " + clue1);

Hint: Once you've found the 5th yellow Car in findClue1, you will need to return its note property. How can you access its note property?

You have solved the Walk segment of this problem set when your findClue1 function returns a note containing the word "average". Once you have this working, submit for grading to check to be sure you have all edge cases completed before continuing to finding the next clue.

Clue 2 - Jog

Now you know the color of the Car to look for in the next clue. You also know that it has an above average carriage # property (compared to all other Cars). This means, if the average carriage number of all Car objects is 20, the Car you are looking for's carriage # property is greater than 20.

Similar to the Walk exercise, in Jog you will write smaller functions that perform single tasks and then compose them together into more advanced functions. In the process of solving for this clue, we will need to establish some arithmetic reducers for finding an average of a List of numbers. Then we'll build some more advanced filters.

2.1 sum - Declare and export a function named sum. The purpose of this function is to find the summation of a List of numbers. Given a List of numbers, this function should return the result of adding all numbers in the List together. An empty List should return 0. Example usage:

print(sum(listify(1, 2, 3))); // Prints 6

2.2 count - Declare and export a function named count. The purpose of this function is to count the number of numbers in a List. Given a List of numbers, it should return the List's length. An empty List should return 0. Example usage:

print(count(listify(1, 2, 3))); // Prints 3

2.3 average - Declare and export a function named average. Given a List of numbers, the purpose of this function is to return the average value of the List. Example usage:

print(average(listify(1, 2, 3))); // Prints 2

2.4 filterAboveCarriage - Declare and export a function named filterAboveCarriage. Given a List of Car objects as a first argument, and a carriage number as a second argument, return a List of all Car objects whose carriage property is greater than the given carriage number. Example usage:

let filtered: List<Car> = filterAboveCarriage(data, 20);
print(filtered); // Only displays cars whose carriage property is greater than 20

2.5 filterByColor - Declare and export a function named filterByColor. Given a List of Car objects as a first argument, and a color string as a second argument, return a List of all Car objects whose color property is equal to the given color parameter. Notice this is exactly like the filterByYellow function you wrote in walk. This function, however, will be more generally useful because it uses a parameter to specify the color you want to filter by.

let filtered: List<Car> = filterByColor(data, "blue");
print(filtered); // Only displays cars whose color property is blue

2.6 mapToCarriage - Declare and export a function named mapToCarriage. Given a List of Car objects as an argument, this function should return a List of numbers, and more specifically the carriage numbers, of each Car in the original List.

let carriageNumbers: List<number> = mapToCarriage(data);
print(carriageNumbers); // Only displays the carriage numbers of each Car

2.7 findClue2 - Now that you have all of the functions above, you're ready to solve for clue 2! Define and export a function named findClue2. Given a List of Cars, it should return the note property of the first Car in the List that is blue and has a carriage number property above the average of all of the Cars in the input list. You should be able to complete this function by making use of the functions above. You can assume there will always be a Car in the input List that meets these criteria. Hint: break this function down into a sequence of steps by initializing and using multiple intermediate variables step-by-step. To test your progress, print your intermediate variable values and return an empty string until your final result is computed. Example usage:

let clue2: string = findClue2(data);
print("Clue 2:" + clue2);

You have solved the Jog segment of this problem set when your findClue2 function returns a note containing the word "escaped". Print it out to reveal the clue. Once you have this working, submit for grading to check to be sure you have all edge cases completed before continuing to the final step of finding THE THIEF.

Catching the Thief - Sprint

3.1 - findThief - Declare and export a function named findThief. Given a List of train Cars, this function should return the name of the person in the Car with a top hatch property who is armed with the weapon found in clue 2. For this final segment of the problem set, you will need to write your own helper function(s) like we did for the first two clues to filter down the list of suspects to 1. We will not grade you on these helper functions, so you are free to experiment, we will only grade based on the correctness of the findThief function. Example usage:

let thief: string = findThief(data);
print("Thief: " + thief);

Hint #: to filter by weapon, define a filter function that evaluates the weapon property using the string method includes as shown lecture 4.

Once you've found the culprit, submit for final grading. Case closed? Great work!