# 6.00 Problem Set 4 # # Caesar Cipher Skeleton # import string import random WORDLIST_FILENAME = "words.txt" # ----------------------------------- # Helper code # (you don't need to understand this helper code) def load_words(): """ Returns a list of valid words. Words are strings of lowercase letters. Depending on the size of the word list, this function may take a while to finish. """ print "Loading word list from file..." # inFile: file inFile = open(WORDLIST_FILENAME, 'r', 0) # line: string line = inFile.readline() # wordlist: list of strings wordlist = line.split() print " ", len(wordlist), "words loaded." return wordlist wordlist = load_words() def is_word(wordlist, word): """ Determines if word is a valid word. wordlist: list of words in the dictionary. word: a possible word. returns True if word is in wordlist. Example: >>> is_word(wordlist, 'bat') returns True >>> is_word(wordlist, 'asdf') returns False """ word = word.lower() word = word.strip(" !@#$%^&*()-_+={}[]|\:;'<>?,./\"") return word in wordlist def random_word(wordlist): """ Returns a random word. wordlist: list of words returns: a word from wordlist at random """ return random.choice(wordlist) def random_string(wordlist, n): """ Returns a string containing n random words from wordlist wordlist: list of words returns: a string of random words separated by spaces. """ return " ".join([random_word(wordlist) for _ in range(n)]) def random_scrambled(wordlist, n): """ Generates a test string by generating an n-word random string and encrypting it with a sequence of random shifts. wordlist: list of words n: number of random words to generate and scamble returns: a scrambled string of n random words NOTE: This function will ONLY work once you have completed your implementation of apply_shifts! """ s = random_string(wordlist, n) + " " shifts = [(i, random.randint(0, 26)) for i in range(len(s)) if s[i-1] == ' '] return apply_shifts(s, shifts)[:-1] def get_fable_string(): """ Returns a fable in encrypted text. """ f = open("fable.txt", "r") fable = str(f.read()) f.close() return fable # (end of helper code) # ----------------------------------- # # Problem 1: Encryption # def build_coder(shift): """ Returns a dict that can apply a Caesar cipher to a letter. The cipher is defined by the shift value. Ignores non-letter characters like punctuation and numbers. shift: -27 < int < 27 returns: dict Example: >>> build_coder(3) {' ': 'c', 'A': 'D', 'C': 'F', 'B': 'E', 'E': 'H', 'D': 'G', 'G': 'J', 'F': 'I', 'I': 'L', 'H': 'K', 'K': 'N', 'J': 'M', 'M': 'P', 'L': 'O', 'O': 'R', 'N': 'Q', 'Q': 'T', 'P': 'S', 'S': 'V', 'R': 'U', 'U': 'X', 'T': 'W', 'W': 'Z', 'V': 'Y', 'Y': 'A', 'X': ' ', 'Z': 'B', 'a': 'd', 'c': 'f', 'b': 'e', 'e': 'h', 'd': 'g', 'g': 'j', 'f': 'i', 'i': 'l', 'h': 'k', 'k': 'n', 'j': 'm', 'm': 'p', 'l': 'o', 'o': 'r', 'n': 'q', 'q': 't', 'p': 's', 's': 'v', 'r': 'u', 'u': 'x', 't': 'w', 'w': 'z', 'v': 'y', 'y': 'a', 'x': ' ', 'z': 'b'} (The order of the key-value pairs may be different.) """ ### TODO. assert shift >= 0 and shift < 27, 'shift %s is not between 0 and 27' % shift # #numbers.Integral used in case of long integers assert isinstance(shift, int), 'shift is not an integer' coder = {} lowercase_and_space = string.ascii_lowercase + ' ' uppercase_and_space = string.ascii_uppercase + ' ' # Shift letters over shift places shifted_lowercase_and_space = lowercase_and_space[shift:] + lowercase_and_space[:shift] shifted_uppercase_and_space = uppercase_and_space[shift:] + uppercase_and_space[:shift] # Construct Caesar cipher dictionary # Add uppercase letters first so ' ' will be overwritten to point to lowercase letter for i in range(len(uppercase_and_space)): coder[uppercase_and_space[i]] = shifted_uppercase_and_space[i] for i in range(len(lowercase_and_space)): coder[lowercase_and_space[i]] = shifted_lowercase_and_space[i] return coder print build_coder(3) def build_encoder(shift): """ Returns a dict that can be used to encode a plain text. For example, you could encrypt the plain text by calling the following commands >>>encoder = build_encoder(shift) >>>encrypted_text = apply_coder(plain_text, encoder) The cipher is defined by the shift value. Ignores non-letter characters like punctuation and numbers. shift: 0 <= int < 27 returns: dict Example: >>> build_encoder(3) {' ': 'c', 'A': 'D', 'C': 'F', 'B': 'E', 'E': 'H', 'D': 'G', 'G': 'J', 'F': 'I', 'I': 'L', 'H': 'K', 'K': 'N', 'J': 'M', 'M': 'P', 'L': 'O', 'O': 'R', 'N': 'Q', 'Q': 'T', 'P': 'S', 'S': 'V', 'R': 'U', 'U': 'X', 'T': 'W', 'W': 'Z', 'V': 'Y', 'Y': 'A', 'X': ' ', 'Z': 'B', 'a': 'd', 'c': 'f', 'b': 'e', 'e': 'h', 'd': 'g', 'g': 'j', 'f': 'i', 'i': 'l', 'h': 'k', 'k': 'n', 'j': 'm', 'm': 'p', 'l': 'o', 'o': 'r', 'n': 'q', 'q': 't', 'p': 's', 's': 'v', 'r': 'u', 'u': 'x', 't': 'w', 'w': 'z', 'v': 'y', 'y': 'a', 'x': ' ', 'z': 'b'} (The order of the key-value pairs may be different.) HINT : Use build_coder. """ ### TODO. def build_decoder(shift): """ Returns a dict that can be used to decode an encrypted text. For example, you could decrypt an encrypted text by calling the following commands >>>encoder = build_encoder(shift) >>>encrypted_text = apply_coder(plain_text, encoder) >>>decrypted_text = apply_coder(plain_text, decoder) The cipher is defined by the shift value. Ignores non-letter characters like punctuation and numbers. shift: 0 <= int < 27 returns: dict Example: >>> build_decoder(3) {' ': 'x', 'A': 'Y', 'C': ' ', 'B': 'Z', 'E': 'B', 'D': 'A', 'G': 'D', 'F': 'C', 'I': 'F', 'H': 'E', 'K': 'H', 'J': 'G', 'M': 'J', 'L': 'I', 'O': 'L', 'N': 'K', 'Q': 'N', 'P': 'M', 'S': 'P', 'R': 'O', 'U': 'R', 'T': 'Q', 'W': 'T', 'V': 'S', 'Y': 'V', 'X': 'U', 'Z': 'W', 'a': 'y', 'c': ' ', 'b': 'z', 'e': 'b', 'd': 'a', 'g': 'd', 'f': 'c', 'i': 'f', 'h': 'e', 'k': 'h', 'j': 'g', 'm': 'j', 'l': 'i', 'o': 'l', 'n': 'k', 'q': 'n', 'p': 'm', 's': 'p', 'r': 'o', 'u': 'r', 't': 'q', 'w': 't', 'v': 's', 'y': 'v', 'x': 'u', 'z': 'w'} (The order of the key-value pairs may be different.) HINT : Use build_coder. """ ### TODO. def apply_coder(text, coder): """ Applies the coder to the text. Returns the encoded text. text: string coder: dict with mappings of characters to shifted characters returns: text after mapping coder chars to original text Example: >>> apply_coder("Hello, world!", build_encoder(3)) 'Khoor,czruog!' >>> apply_coder("Khoor,czruog!", build_decoder(3)) 'Hello, world!' """ encoded_word = '' for i in range (0, len(text)): if text[i] not in coder: encoded_word = encoded_word + text[i] else: encoded_word = encoded_word + coder[text[i]] return encoded_word print apply_coder('Hello, world!', build_coder(3)) ### TODO. def apply_shift(text, shift): """ Given a text, returns a new text Caesar shifted by the given shift offset. The empty space counts as the 27th letter of the alphabet, so spaces should be replaced by a lowercase letter as appropriate. Otherwise, lower case letters should remain lower case, upper case letters should remain upper case, and all other punctuation should stay as it is. text: string to apply the shift to shift: amount to shift the text returns: text after being shifted by specified amount. Example: >>> apply_shift('This is a test.', 8) 'Apq hq hiham a.' """ ### TODO. return apply_coder(text, build_coder(shift)) print apply_shift('This is a test.', 8) # # Problem 2: Codebreaking. # def find_best_shift(wordlist, text): """ Decrypts the encoded text and returns the plaintext. text: string returns: 0 <= int 27 Example: >>> s = apply_coder('Hello, world!', build_encoder(8)) >>> s 'Pmttw,hdwztl!' >>> find_best_shift(wordlist, s) returns 8 >>> apply_coder(s, build_decoder(8)) returns 'Hello, world!' """ ### TODO max_num_valid_words = 0 best_shift = 0 for shift in range(27): shifted_words = apply_shift(text, shift) potential_words = shifted_words.split() num_valid_words = 0 for word in potential_words: if is_word(wordlist, word): num_valid_words += 1 if num_valid_words > max_num_valid_words: max_num_valid_words = num_valid_words best_shift = shift return best_shift print find_best_shift(wordlist, "Pmttw,hdwztl!") # # Problem 3: Multi-level encryption. # def apply_shifts(text, shifts): """ Applies a sequence of shifts to an input text. text: A string to apply the Ceasar shifts to shifts: A list of tuples containing the location each shift should begin and the shift offset. Each tuple is of the form (location, shift) The shifts are layered: each one is applied from its starting position all the way through the end of the string. returns: text after applying the shifts to the appropriate positions Example: >>> apply_shifts("Do Androids Dream of Electric Sheep?", [(0,6), (3, 18), (12, 16)]) 'JufYkaolfapxQdrnzmasmRyrpfdvpmEurrb?' """ ### TODO. encoded_word = "" a = dict(shifts) for key in a: if key == 0: initial_shift = apply_shift(text, a[key]) encoded_word = encoded_word + initial_shift print encoded_word else: new_shift = apply_shift(encoded_word[key:], a[key]) print new_shift encoded_word = encoded_word[0:key] + new_shift print encoded_word return encoded_word # print apply_shifts("JufYkaolfapxQdrnzmasmRyrpfdvpmEurrb?", [(0, 21), (3, 9), (12, 11), (18, 0), (21, 0), (30, 0)]) # # Problem 4: Multi-level decryption. # def find_best_shifts_rec(wordlist, text, start): """ Given a scrambled string and a starting position from which to decode, returns a shift key that will decode the text to words in wordlist, or None if there is no such key. Hint: You will find this function much easier to implement if you use recursion. wordlist: list of words text: scambled text to try to find the words for start: where to start looking at shifts returns: list of tuples. each tuple is (position in text, amount of shift) """ ### TODO. new_string = text for shift in xrange(0, 27): new_string = text[0:start] + apply_shift(text[start:len(text)], shift) for i in xrange(start, len(new_string)+1): if is_word(wordlist, new_string[start:i]): if i == len(new_string): return [(start, shift)] elif new_string[i] == ' ': recShifts = find_best_shifts_rec(wordlist, new_string, i+1) if recShifts != None: return [(start, shift)] + recShifts print find_best_shifts_rec(wordlist, "JufYkaolfapxQdrnzmasmRyrpfdvpmEurrb?", 0) def decrypt_fable(): """ Using the methods you created in this problem set, decrypt the fable given by the function get_fable_string(). Once you decrypt the message, be sure to include as a comment at the end of this problem set how the fable relates to your education at MIT. returns: string - fable in plain text """ fable = get_fable_string() shifts = find_best_shifts_rec(wordlist, fable, 0) decoded_fable = apply_shifts(fable, shifts) return decoded_fable print decrypt_fable()