CS61A Project 2
easterling

Project 2: CS 61A Autocorrected Typing Software cats.zip

image

Programmers dream of
Abstraction, recursion, and
Typing really fast.

Introduction

Important submission note: For full credit:

  • Submit with Phase 1 complete by Tuesday, September 27, worth 1 pt.
  • Submit with all phases complete by Friday, September 30.

Try to attempt the problems in order, as some later problems will depend on earlier problems in their implementation and therefore also when running ok tests.

The entire project can be completed with a partner.

You can get 1 bonus point by submitting the entire project by Thursday, September 29.

In this project, you will write a program that measures typing speed. Additionally, you will implement typing autocorrect, which is a feature that attempts to correct the spelling of a word after a user types it. This project is inspired by typeracer.

When students in the past have tried to implement the functions without thoroughly reading the problem description, they’ve often run into issues. 😱 Read each description thoroughly before starting to code.

Final Product

Our staff solution to the project can be interacted with at cats.cs61a.org. If you’d like, feel free to try it out now. When you finish the project, you’ll have implemented a significant part of this match yourself!

Phase 1: Typing

When students in the past have tried to implement the functions without thoroughly reading the problem description, they’ve often run into issues. 😱 Read each description thoroughly before starting to code.

Problem 1 (1 pt)

Throughout the project, we will be making changes to functions in cats.py.

Implement pick. This function selects which paragraph the user will type. It takes three parameters:

  • a list of paragraphs (strings)
  • a select function, which returns True for paragraphs that can be selected
  • a non-negative index k

The pick function returns the kth paragraph for which select returns True. If no such paragraph exists (because k is too large), then pick returns the empty string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def pick(paragraphs, select, k):
"""Return the Kth paragraph from PARAGRAPHS for which SELECT called on the
paragraph returns True. If there are fewer than K such paragraphs, return
the empty string.

Arguments:
paragraphs: a list of strings
select: a function that returns True for paragraphs that can be selected
k: an integer

>>> ps = ['hi', 'how are you', 'fine']
>>> s = lambda p: len(p) <= 4
>>> pick(ps, s, 0)
'hi'
>>> pick(ps, s, 1)
'fine'
>>> pick(ps, s, 2)
''
"""
# BEGIN PROBLEM 1
paragraphs = [i for i in paragraphs if select(i)]
if len(paragraphs)>k:
return paragraphs[k]
else:
return ''
# END PROBLEM 1

Problem 2 (1 pt)

Implement about, which takes a list of topic words. It returns a function which takes a paragraph and returns a boolean indicating whether that paragraph contains any of the words in topic.

Once we’ve implemented about, we’ll be able to pass the returned function to pick as the select argument, which will be useful as we continue to implement our typing test.

To be able to make this comparison accurately, you will need to ignore case (that is, assume that uppercase and lowercase letters don’t change what word it is) and punctuation in the paragraph. Additionally, only check for exact matches of the words in topic in the paragraph, not substrings. For example, “dogs” is not a match for the word “dog”.

Hint: You may use the string utility functions in utils.py. You can reference the docstrings of the utility functions to see how they are being used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def about(topic):
"""Return a select function that returns whether
a paragraph contains one of the words in TOPIC.

Arguments:
topic: a list of words related to a subject

>>> about_dogs = about(['dog', 'dogs', 'pup', 'puppy'])
>>> pick(['Cute Dog!', 'That is a cat.', 'Nice pup!'], about_dogs, 0)
'Cute Dog!'
>>> pick(['Cute Dog!', 'That is a cat.', 'Nice pup.'], about_dogs, 1)
'Nice pup.'
"""
assert all([lower(x) == x for x in topic]), 'topics should be lowercase.'
# BEGIN PROBLEM 2
def uu(somewords):
list=split(somewords)
for i in list:
if lower(remove_punctuation(i)) in topic:
return True
return False
return uu
# END PROBLEM 2

Problem 3 (2 pts)

Implement accuracy, which takes a typed paragraph and a source paragraph. It returns the percentage of words in typed that exactly match the corresponding words in source. Case and punctuation must match as well. “Corresponding” here means that two words must occur at the same indices in typed and source—the first words of both must match, the second words of both must match, and so on.

A word in this context is any sequence of characters separated from other words by whitespace, so treat “dog;” as a single word.

If typed is longer than source, then the extra words in typed that have no corresponding word in source are all incorrect.

If both typed and source are empty, then the accuracy is 100.0. If typed is empty but source is not empty, then the accuracy is zero. If typed is not empty but source is empty, then the accuracy is zero.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def accuracy(typed, source):
"""Return the accuracy (percentage of words typed correctly) of TYPED
when compared to the prefix of SOURCE that was typed.

Arguments:
typed: a string that may contain typos
source: a string without errors

>>> accuracy('Cute Dog!', 'Cute Dog.')
50.0
>>> accuracy('A Cute Dog!', 'Cute Dog.')
0.0
>>> accuracy('cute Dog.', 'Cute Dog.')
50.0
>>> accuracy('Cute Dog. I say!', 'Cute Dog.')
50.0
>>> accuracy('Cute', 'Cute Dog.')
100.0
>>> accuracy('', 'Cute Dog.')
0.0
>>> accuracy('', '')
100.0
"""
# BEGIN PROBLEM 3
typed_words = split(typed)
source_words = split(source)
len1 = len(typed_words)
len2 = len(source_words)
i=0
count = 0
if len1 ==0 and len2 ==0:
return 100.0
elif len1 ==0 or len2 ==0:
return 0.0
else:
min_len = min(len1,len2)
for i in range(min_len):
if source_words[i] == typed_words[i]:
count += 1
return 100.0*count/len1
# END PROBLEM 3

Problem 4 (1 pt)

Implement wpm, which computes the words per minute, a measure of typing speed, given a string typed and the amount of elapsed time in seconds. Despite its name, words per minute is not based on the number of words typed, but instead the number of groups of 5 characters, so that a typing test is not biased by the length of words. The formula for words per minute is the ratio of the number of characters (including spaces) typed divided by 5 (a typical word length) to the elapsed time in minutes.

For example, the string "I am glad!" contains three words and ten characters (not including the quotation marks). The words per minute calculation uses 2 as the number of words typed (because 10 / 5 = 2). If someone typed this string in 30 seconds (half a minute), their speed would be 4 words per minute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def wpm(typed, elapsed):
"""Return the words-per-minute (WPM) of the TYPED string.

Arguments:
typed: an entered string
elapsed: an amount of time in seconds

>>> wpm('hello friend hello buddy hello', 15)
24.0
>>> wpm('0123456789',60)
2.0
"""
assert elapsed > 0, 'Elapsed time must be positive'
# BEGIN PROBLEM 4
return (len(typed)/5)/(elapsed/60)
# END PROBLEM 4

Phase 2: Autocorrect

When students in the past have tried to implement the functions without thoroughly reading the problem description, they’ve often run into issues. 😱 Read each description thoroughly before starting to code.

In the web-based GUI, there is an autocorrect button, but right now it doesn’t do anything. Let’s implement automatic correction of typos. Whenever the user presses the space bar, if the last word they typed doesn’t match a word in the dictionary but is close to one, then that similar word will be substituted for what they typed.

Problem 5 (2 pts)

Implement autocorrect, which takes a typed_word, a word_list, a diff_function, and a limit.

If the typed_word is contained inside the word_list, autocorrect returns that word.

Otherwise, autocorrect returns the word from word_list that has the lowest difference from the provided typed_word based on the diff_function. However, if the lowest difference between typed_word and any of the words in word_list is greater than limit, then typed_word is returned instead.

Important: If typed_word is not contained inside word_list, and multiple strings have the same lowest difference from typed_word according to the diff_function, autocorrect should return the string that appears first in word_list.

A diff function takes in three arguments. The first is the typed_word, the second is the source word (in this case, a word from word_list), and the third argument is the limit. The output of the diff function, which is a number, represents the amount of difference between the two strings.

Note: Some diff functions may be asymmetric (meaning flipping the first two parameters may yield a different output), so make sure to pass in the arguments to diff_function in the correct order. We will see an example of an asymmetric diff function in problem 7.

Here is an example of a diff function that computes the minimum of 1 + limit and the difference in length between the two input strings:

1
2
3
4
5
6
>>> def length_diff(w1, w2, limit):
... return min(limit + 1, abs(len(w2) - len(w1)))
>>> length_diff('mellow', 'cello', 10)
1
>>> length_diff('hippo', 'hippopotamus', 5)
6

Assume that typed_word and all elements of word_list are lowercase and have no punctuation.

Hint: Try using max or min with the optional key argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def autocorrect(typed_word, word_list, diff_function, limit):
"""Returns the element of WORD_LIST that has the smallest difference
from TYPED_WORD. Instead returns TYPED_WORD if that difference is greater
than LIMIT.

Arguments:
typed_word: a string representing a word that may contain typos
word_list: a list of strings representing source words
diff_function: a function quantifying the difference between two words
limit: a number

>>> ten_diff = lambda w1, w2, limit: 10 # Always returns 10
>>> autocorrect("hwllo", ["butter", "hello", "potato"], ten_diff, 20)
'butter'
>>> first_diff = lambda w1, w2, limit: (1 if w1[0] != w2[0] else 0) # Checks for matching first char
>>> autocorrect("tosting", ["testing", "asking", "fasting"], first_diff, 10)
'testing'
"""
# BEGIN PROBLEM 5
theword=typed_word
min = limit
if typed_word in word_list:
return typed_word
else:
for word in word_list:
distence = diff_function(typed_word,word,limit)
if distence <= min:
if theword == typed_word or distence < min:
min = distence
theword = word
return theword
# END PROBLEM 5

Problem 6 (3 pts)

Implement feline_fixes, which is a diff function that takes two strings. It returns the minimum number of characters that must be changed in the typed word in order to transform it into the source word. If the strings are not of equal length, the difference in lengths is added to the total.

Important: You may not use while, for, or list comprehensions in your implementation. Use recursion.

Here are some examples:

1
2
3
4
5
6
7
8
9
10
11
>>> big_limit = 10
>>> feline_fixes("nice", "rice", big_limit) # Substitute: n -> r
1
>>> feline_fixes("range", "rungs", big_limit) # Substitute: a -> u, e -> s
2
>>> feline_fixes("pill", "pillage", big_limit) # Don't substitute anything, length difference of 3.
3
>>> feline_fixes("roses", "arose", big_limit) # Substitute: r -> a, o -> r, s -> o, e -> s, s -> e
5
>>> feline_fixes("rose", "hello", big_limit) # Substitute: r->h, o->e, s->l, e->l, length difference of 1.
5

Important: If the number of characters that must change is greater than limit, then feline_fixes should return any number larger than limit and should minimize the amount of computation needed to do so.

These two calls to feline_fixes should take about the same amount of time to evaluate:

1
2
3
4
5
>>> limit = 4
>>> feline_fixes("roses", "arose", limit) > limit
True
>>> feline_fixes("rosesabcdefghijklm", "arosenopqrstuvwxyz", limit) > limit
True
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def feline_fixes(typed, source, limit):
"""A diff function for autocorrect that determines how many letters
in TYPED need to be substituted to create SOURCE, then adds the difference in
their lengths and returns the result.

Arguments:
typed: a starting word
source: a string representing a desired goal word
limit: a number representing an upper bound on the number of chars that must change

>>> big_limit = 10
>>> feline_fixes("nice", "rice", big_limit) # Substitute: n -> r
1
>>> feline_fixes("range", "rungs", big_limit) # Substitute: a -> u, e -> s
2
>>> feline_fixes("pill", "pillage", big_limit) # Don't substitute anything, length difference of 3.
3
>>> feline_fixes("roses", "arose", big_limit) # Substitute: r -> a, o -> r, s -> o, e -> s, s -> e
5
>>> feline_fixes("rose", "hello", big_limit) # Substitute: r->h, o->e, s->l, e->l, length difference of 1.
5
"""
# BEGIN PROBLEM 6
if limit<0:
return 1
if len(typed) == 0 or len(source) == 0:
return max(len(source),len(typed))
else:
if source[0] != typed[0]:
return feline_fixes(typed[1:], source[1:], limit-1)+1
else:
return feline_fixes(typed[1:], source[1:], limit)
# END PROBLEM 6

Problem 7 (3 pts)

Implement minimum_mewtations, which is a diff function that returns the minimum number of edit operations needed to transform the start word into the goal word.

There are three kinds of edit operations, with some examples:

  1. Add a letter to start.
    • Adding "k" to "itten" gives us "kitten".
  2. Remove a letter from start.
    • Removing "s" from "scat" givs us "cat".
  3. Substitute a letter in start for another.
    • Substituting "z" with "j" in "zaguar" gives us "jaguar".

Each edit operation contributes 1 to the difference between two words.

1
2
3
4
5
6
7
>>> big_limit = 10
>>> minimum_mewtations("cats", "scat", big_limit) # cats -> scats -> scat
2
>>> minimum_mewtations("purng", "purring", big_limit) # purng -> purrng -> purring
2
>>> minimum_mewtations("ckiteus", "kittens", big_limit) # ckiteus -> kiteus -> kitteus -> kittens
3

We have provided a template of an implementation in cats.py.

Hint: This is a recursive function with three recursive calls. One of these recursive calls will be similar to the recursive call in feline_fixes.

You may modify the template however you want or delete it entirely.

Important: If the number of edits required is greater than limit, then minimum_mewtations should return any number larger than limit and should minimize the amount of computation needed to do so.

These two calls to minimum_mewtations should take about the same amount of time to evaluate:

1
2
3
4
5
>>> limit = 2
>>> minimum_mewtations("ckiteus", "kittens", limit) > limit
True
>>> minimum_mewtations("ckiteusabcdefghijklm", "kittensnopqrstuvwxyz", limit) > limit
True
1
python3 ok -q 07✂️

(Optional) Extension: final diff (0pt)

You may optionally design your own diff function called final_diff. Here are some ideas for making even more accurate corrections:

  • Take into account which additions and deletions are more likely than others. For example, it’s much more likely that you’ll accidentally leave out a letter if it appears twice in a row.
  • Treat two adjacent letters that have swapped positions as one change, not two.
  • Try to incorporate common misspellings.

You can also set the limit you’d like your diff function to use by changing the value of the variable FINAL_DIFF_LIMIT in cats.py.

You can check your final_diff‘s success rate by running:

1
python3 score.py

If you don’t know where to start, try copy-pasting your code for feline_fixes and minimum_mewtations into final_diff and scoring them. Looking at the typos they accidentally fixed might give you some ideas!

Phase 3: Multiplayer

When students in the past have tried to implement the functions without thoroughly reading the problem description, they’ve often run into issues. 😱 Read each description thoroughly before starting to code.

Typing is more fun with friends! You’ll now implement multiplayer functionality, so that when you run cats_gui.py on your computer, it connects to the course server at cats.cs61a.org and looks for someone else to race against.

To race against a friend, 5 different programs will be running:

  • Your GUI, which is a program that handles all the text coloring and display in your web browser.
  • Your cats_gui.py, which is a web server that communicates with your GUI using the code you wrote in cats.py.
  • Your opponent’s cats_gui.py.
  • Your opponent’s GUI.
  • The CS 61A multiplayer server, which matches players together and passes messages around.

When you type, your GUI uploads what you have typed to your cats_gui.py server, which computes how much progress you have made and returns a progress update. It also uploads a progress update to the multiplayer server, so that your opponent’s GUI can display it.

Meanwhile, your GUI display is always trying to keep current by asking for progress updates from cats_gui.py, which in turn requests that info from the multiplayer server.

Each player has an id number that is used by the server to track typing progress.

Problem 8 (2 pts)

Implement report_progress, which is called every time the user finishes typing a word. It takes a list of the words typed, a list of the words in the prompt, the user’s user_id, and a upload function that is used to upload a progress report to the multiplayer server. There will never be more words in typed than in prompt.

Your progress is a ratio of the words in the prompt that you have typed correctly, up to the first incorrect word, divided by the number of prompt words. For example, this example has a progress of 0.25:

1
report_progress(["Hello", "ths", "is"], ["Hello", "this", "is", "wrong"], ...)

Your report_progress function should do two things: upload a message to the multiplayer server and return the progress of the player with user_id.

You can upload a message to the multiplayer server by calling the upload function on a two-element dictionary containing the keys 'id' and 'progress'. You should then return the player’s progress, which is the ratio of words you computed.

Hint: See the dictionary below for an example of a potential input into the upload function. This dictionary represents a player with user_id 1 and progress 0.6.

1
{'id': 1, 'progress': 0.6}

Before writing any code, unlock the tests to verify your understanding of the question:

1
python3 ok -q 08 -u✂️

Once you are done unlocking, begin implementing your solution. You can check your correctness with:

1
python3 ok -q 08✂️

Problem 9 (2 pts)

Implement time_per_word, which takes in a list words and times_per_player, a list of lists for each player with timestamps indicating when each player finished typing every individual word in words. It returns a match with the given information.

A match is a dictionary that stores words and times. The times are stored as a list of lists of how long it took each player to type every word in words. Specifically, times[i][j] indicates how long it took player i to type words[j].

For example, say words = ['Hello', 'world'] and times = [[5, 1], [4, 2]], then [5, 1] corresponds to the list of times for player 0, and [4, 2] corresponds to the list of times for player 1. Thus, player 0 took 5 units of time to write the word 'Hello'.

Important: Be sure to use the match constructor when returning a match. The tests will check that you are using the match dictionary rather than assuming a particular data format.

Read the definitions for the match constructor in cats.py to learn more about how the dictionary is implemented.

Timestamps are cumulative and always increasing, while the values in times are differences between consecutive timestamps for each player.

Here’s an example: If times_per_player = [[1, 3, 5], [2, 5, 6]], the corresponding times attribute of the match would be [[2,2], [3, 1]]. This is because the differences in timestamps are (3-1), (5-3) for the first player and (5-2), (6-5) for the second player. The first value of each list within times_per_player represents the initial starting time for each player.

Before writing any code, unlock the tests to verify your understanding of the question:

1
python3 ok -q 09 -u✂️

Once you are done unlocking, begin implementing your solution. You can check your correctness with:

1
python3 ok -q 09✂️

👩🏽‍💻👨🏿‍💻 Pair programming? We suggest switching roles now, if you haven’t recently. Almost done!

Problem 10 (2 pts)

Implement fastest_words, which returns which words each player typed fastest. This function is called once both players have finished typing. It takes in a match.

Specifically, the fastest_words function returns a list of lists of words, one list for each player, and within each list the words they typed the fastest (against all the other players). In the case of a tie, consider the earliest player in the list (the smallest player index) to be the one who typed it the fastest.

For example consider the following match with the words ‘Just’, ‘have’, and ‘fun’. Player 0 typed ‘fun’ the fastest (3 seconds), Player 1 typed ‘Just’ the fastest (4 seconds), and they tied on the word ‘have’ (both took 1 second) so we consider to Player 0 to be the fastest, because they are the earliest player in the list.

1
2
3
4
>>> player_0 = [5, 1, 3]
>>> player_1 = [4, 1, 6]
>>> fastest_words(match(['Just', 'have', 'fun'], [player_0, player_1]))
[['have', 'fun'], ['Just']]

The match argument is a match dictionary, like the one returned in Problem 9. You can access words in the match with the selector get_word, which takes in a match and the word_index (an integer). With get_word you can access the time it took any player to type any word using time.

Important: Be sure to use the match constructor when returning a match. The tests will check that you are using the match dictionary rather than assuming a particular data format.

Make sure your implementation does not mutate the given player input lists. For the example above, calling fastest_words on [player_0, player_1] should not mutate player_0 or player_1.

Before writing any code, unlock the tests to verify your understanding of the question:

1
python3 ok -q 10 -u✂️

Once you are done unlocking, begin implementing your solution. You can check your correctness with:

1
python3 ok -q 10✂️

Congratulations! Now you can play against other students in the course. Set enable_multiplayer to True near the bottom of cats.py and type swiftly!

1
python3 cats_gui.py

At this point, run the entire autograder to see if there are any tests that don’t pass.

1
python3 ok

Once you are satisfied, submit to Ok to complete the project.

1
python3 ok --submit

If you have a partner, make sure to add them to the submission on okpy.

Check to make sure that you did all the problems by running:

1
python3 ok --score
 Comments
Comment plugin failed to load
Loading comment plugin