diff --git a/exercise/ps4b.py b/exercise/ps4b.py index 4e5e101..8533aca 100644 --- a/exercise/ps4b.py +++ b/exercise/ps4b.py @@ -18,13 +18,17 @@ def load_words(file_name): ''' print("Loading word list from file...") # inFile: file - inFile = open(file_name, 'r') - # wordlist: list of strings - wordlist = [] - for line in inFile: - wordlist.extend([word.lower() for word in line.split(' ')]) - print(" ", len(wordlist), "words loaded.") - return wordlist + try: + inFile = open(file_name, 'r') + # wordlist: list of strings + wordlist = [] + for line in inFile: + wordlist.extend([word.lower() for word in line.split(' ')]) + print(" ", len(wordlist), "words loaded.") + return wordlist + except FileNotFoundError: + print(f"Warning: File {file_name} not found!") + return [] def is_word(word_list, word): ''' @@ -35,12 +39,6 @@ def is_word(word_list, word): word (string): a possible word. Returns: True if word is in word_list, False otherwise - - Example: - >>> is_word(word_list, 'bat') returns - True - >>> is_word(word_list, 'asdf') returns - False ''' word = word.lower() word = word.strip(" !@#$%^&*()-_+={}[]|\:;'<>?,./\"") @@ -50,10 +48,14 @@ def get_story_string(): """ Returns: a story in encrypted text. """ - f = open("story.txt", "r") - story = str(f.read()) - f.close() - return story + try: + f = open("story.txt", "r") + story = str(f.read()) + f.close() + return story + except FileNotFoundError: + print("Warning: story.txt not found!") + return "" ### END HELPER CODE ### @@ -70,7 +72,8 @@ def __init__(self, text): self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) def get_message_text(self): ''' @@ -78,7 +81,7 @@ def get_message_text(self): Returns: self.message_text ''' - pass #delete this line and replace with your code here + return self.message_text def get_valid_words(self): ''' @@ -87,7 +90,7 @@ def get_valid_words(self): Returns: a COPY of self.valid_words ''' - pass #delete this line and replace with your code here + return self.valid_words[:] def build_shift_dict(self, shift): ''' @@ -103,7 +106,17 @@ def build_shift_dict(self, shift): Returns: a dictionary mapping a letter (string) to another letter (string). ''' - pass #delete this line and replace with your code here + lower_keys = string.ascii_lowercase + upper_keys = string.ascii_uppercase + shift_dict = {} + + for i in range(26): + # Map lowercase + shift_dict[lower_keys[i]] = lower_keys[(i + shift) % 26] + # Map uppercase + shift_dict[upper_keys[i]] = upper_keys[(i + shift) % 26] + + return shift_dict def apply_shift(self, shift): ''' @@ -117,7 +130,14 @@ def apply_shift(self, shift): Returns: the message text (string) in which every character is shifted down the alphabet by the input shift ''' - pass #delete this line and replace with your code here + mapping_dict = self.build_shift_dict(shift) + encrypted_text = [] + for char in self.message_text: + if char in mapping_dict: + encrypted_text.append(mapping_dict[char]) + else: + encrypted_text.append(char) + return "".join(encrypted_text) class PlaintextMessage(Message): def __init__(self, text, shift): @@ -135,7 +155,10 @@ def __init__(self, text, shift): self.message_text_encrypted (string, created using shift) ''' - pass #delete this line and replace with your code here + Message.__init__(self, text) + self.shift = shift + self.encryption_dict = self.build_shift_dict(shift) + self.message_text_encrypted = self.apply_shift(shift) def get_shift(self): ''' @@ -143,7 +166,7 @@ def get_shift(self): Returns: self.shift ''' - pass #delete this line and replace with your code here + return self.shift def get_encryption_dict(self): ''' @@ -151,7 +174,7 @@ def get_encryption_dict(self): Returns: a COPY of self.encryption_dict ''' - pass #delete this line and replace with your code here + return self.encryption_dict.copy() def get_message_text_encrypted(self): ''' @@ -159,7 +182,7 @@ def get_message_text_encrypted(self): Returns: self.message_text_encrypted ''' - pass #delete this line and replace with your code here + return self.message_text_encrypted def change_shift(self, shift): ''' @@ -171,7 +194,9 @@ def change_shift(self, shift): Returns: nothing ''' - pass #delete this line and replace with your code here + self.shift = shift + self.encryption_dict = self.build_shift_dict(shift) + self.message_text_encrypted = self.apply_shift(shift) class CiphertextMessage(Message): @@ -185,7 +210,8 @@ def __init__(self, text): self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + # --- ĐÃ SỬA LỖI TẠI DÒNG DƯỚI ĐÂY --- + Message.__init__(self, text) def decrypt_message(self): ''' @@ -196,29 +222,85 @@ def decrypt_message(self): the message, then we would expect 26 - s to be the best shift value for decrypting it. - Note: if multiple shifts are equally good such that they all create - the maximum number of valid words, you may choose any of those shifts - (and their corresponding decrypted messages) to return - Returns: a tuple of the best shift value used to decrypt the message and the decrypted message text using that shift value ''' - pass #delete this line and replace with your code here + best_shift = 0 + max_valid_words = 0 + best_message = "" + + # Try all possible shifts from 0 to 25 + for s in range(26): + # Apply shift to the current ciphertext + decrypted_try = self.apply_shift(s) + + # Split into words to check validity + words = decrypted_try.split(' ') + valid_count = 0 + + # Count valid words + for word in words: + if is_word(self.valid_words, word): + valid_count += 1 + + # Update best shift if we found more valid words + if valid_count > max_valid_words: + max_valid_words = valid_count + best_shift = s + best_message = decrypted_try + + return (best_shift, best_message) if __name__ == '__main__': -# #Example test case (PlaintextMessage) -# plaintext = PlaintextMessage('hello', 2) -# print('Expected Output: jgnnq') -# print('Actual Output:', plaintext.get_message_text_encrypted()) -# -# #Example test case (CiphertextMessage) -# ciphertext = CiphertextMessage('jgnnq') -# print('Expected Output:', (24, 'hello')) -# print('Actual Output:', ciphertext.decrypt_message()) + # Example test case (PlaintextMessage) + plaintext = PlaintextMessage('hello', 2) + print('Expected Output: jgnnq') + print('Actual Output:', plaintext.get_message_text_encrypted()) - #TODO: WRITE YOUR TEST CASES HERE + # Example test case (CiphertextMessage) + ciphertext = CiphertextMessage('jgnnq') + print('Expected Output:', (24, 'hello')) + print('Actual Output:', ciphertext.decrypt_message()) - #TODO: best shift value and unencrypted story + # TODO: WRITE YOUR TEST CASES HERE + print("\n------------------------------") + print("MY CUSTOM TEST CASES:") + + # 1. Test PlaintextMessage + print("\n1. Testing PlaintextMessage:") + my_msg = PlaintextMessage("Python is awesome!", 4) + print("Original:", my_msg.get_message_text()) + print("Shift:", my_msg.get_shift()) + print("Encrypted:", my_msg.get_message_text_encrypted()) + + # Thay đổi shift + print("-> Changing shift to 1...") + my_msg.change_shift(1) + print("New Encrypted:", my_msg.get_message_text_encrypted()) + + # 2. Test CiphertextMessage + print("\n2. Testing CiphertextMessage:") + # Giả sử ta có chuỗi đã mã hóa: "Python is awesome!" -> shift 1 -> "Qzuipo jt bxftpnf!" + encrypted_str = "Qzuipo jt bxftpnf!" + cipher_msg = CiphertextMessage(encrypted_str) + + # Giải mã + result = cipher_msg.decrypt_message() + print(f"Input Encrypted: {encrypted_str}") + print(f"Best Shift found: {result[0]}") + print(f"Decrypted Text: {result[1]}") + + # TODO: best shift value and unencrypted story + print("\n------------------------------") + print("DECODING STORY.TXT:") - pass #delete this line and replace with your code here + story_text = get_story_string() + if story_text: + story_cipher = CiphertextMessage(story_text) + decoded_story = story_cipher.decrypt_message() + print(f"Best Shift: {decoded_story[0]}") + print("Story Content:\n") + print(decoded_story[1]) + else: + print("Could not load story.txt to decrypt.") \ No newline at end of file diff --git a/exercise/ps4c.py b/exercise/ps4c.py index 1ceb239..a53ee3b 100644 --- a/exercise/ps4c.py +++ b/exercise/ps4c.py @@ -4,8 +4,38 @@ # Time Spent: x:xx import string -from ps4a import get_permutations +def get_permutations(sequence): + ''' + Enumerate all permutations of a given string + + sequence (string): an arbitrary string to permute. Assume that it is a + non-empty string. + + You must use recursion for this part. + + Returns: a list of all permutations of sequence + + Example: + >>> get_permutations('abc') + ['abc', 'acb', 'bac', 'bca', 'cab', 'cba'] + + Note: depending on your implementation, you may return the permutations in + a different order than what is listed here. + ''' + if len(sequence) <= 1: + return [sequence] + else: + perms = [] + first_char = sequence[0] + next_chars = sequence[1:] + # Recursive call + perms_of_rest = get_permutations(next_chars) + for p in perms_of_rest: + for i in range(len(p) + 1): + # Insert the first character into all possible positions + perms.append(p[:i] + first_char + p[i:]) + return sorted(list(set(perms))) # Return unique sorted permutations ### HELPER CODE ### def load_words(file_name): ''' @@ -70,7 +100,8 @@ def __init__(self, text): self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + self.message_text = text + self.valid_words = load_words(WORDLIST_FILENAME) def get_message_text(self): ''' @@ -78,7 +109,7 @@ def get_message_text(self): Returns: self.message_text ''' - pass #delete this line and replace with your code here + return self.message_text def get_valid_words(self): ''' @@ -87,7 +118,7 @@ def get_valid_words(self): Returns: a COPY of self.valid_words ''' - pass #delete this line and replace with your code here + return self.valid_words[:] def build_transpose_dict(self, vowels_permutation): ''' @@ -109,7 +140,23 @@ def build_transpose_dict(self, vowels_permutation): another letter (string). ''' - pass #delete this line and replace with your code here + transpose_dict = {} + + # Map consonants to themselves + for char in CONSONANTS_LOWER: + transpose_dict[char] = char + for char in CONSONANTS_UPPER: + transpose_dict[char] = char + + # Map vowels based on permutation + # VOWELS_LOWER is 'aeiou' + for i in range(5): + # Mapping lowercase + transpose_dict[VOWELS_LOWER[i]] = vowels_permutation[i] + # Mapping uppercase + transpose_dict[VOWELS_UPPER[i]] = vowels_permutation[i].upper() + + return transpose_dict def apply_transpose(self, transpose_dict): ''' @@ -119,7 +166,14 @@ def apply_transpose(self, transpose_dict): on the dictionary ''' - pass #delete this line and replace with your code here + encrypted_text = [] + for char in self.message_text: + if char in transpose_dict: + encrypted_text.append(transpose_dict[char]) + else: + # Keep punctuation, spaces, numbers, etc. as is + encrypted_text.append(char) + return "".join(encrypted_text) class EncryptedSubMessage(SubMessage): def __init__(self, text): @@ -132,7 +186,7 @@ def __init__(self, text): self.message_text (string, determined by input text) self.valid_words (list, determined using helper function load_words) ''' - pass #delete this line and replace with your code here + SubMessage.__init__(self, text) def decrypt_message(self): ''' @@ -152,12 +206,35 @@ def decrypt_message(self): Hint: use your function from Part 4A ''' - pass #delete this line and replace with your code here + vowel_perms = get_permutations(VOWELS_LOWER) + max_valid_words = 0 + best_message = self.message_text # Default return original if no words found + + for p in vowel_perms: + # Build transpose dict for this permutation attempt + # Note: We treat the permutation as the key to transform current text + temp_dict = self.build_transpose_dict(p) + decrypted_attempt = self.apply_transpose(temp_dict) + + # Count valid words in this attempt + valid_count = 0 + words = decrypted_attempt.split() + for w in words: + if is_word(self.valid_words, w): + valid_count += 1 + + # Update best message if we found more valid words + if valid_count > max_valid_words: + max_valid_words = valid_count + best_message = decrypted_attempt + + return best_message if __name__ == '__main__': # Example test case + print("--- Example Test Case ---") message = SubMessage("Hello World!") permutation = "eaiuo" enc_dict = message.build_transpose_dict(permutation) @@ -168,3 +245,41 @@ def decrypt_message(self): print("Decrypted message:", enc_message.decrypt_message()) #TODO: WRITE YOUR TEST CASES HERE + print("\n--- Student Test Case 1: SubMessage ---") + # Test case: Simple encryption with a different permutation + msg1 = SubMessage("Python is fun") + perm1 = "uoeia" # a->u, e->o, i->e, o->i, u->a + dict1 = msg1.build_transpose_dict(perm1) + print("Input: 'Python is fun', Permutation: 'uoeia'") + # P(y)th(o->i)n (i->e)s f(u->a)n => Pythin es fan + print("Expected: Pythin es fan") + print("Actual:", msg1.apply_transpose(dict1)) + + print("\n--- Student Test Case 2: SubMessage ---") + # Test case: Check uppercase handling and punctuation + msg2 = SubMessage("Eat? No, Tea!") + perm2 = "iouea" # a->i, e->o, i->u, o->e, u->a + dict2 = msg2.build_transpose_dict(perm2) + print("Input: 'Eat? No, Tea!', Permutation: 'iouea'") + # E(->O)at(a->i)? N(o->e), T(e->o)a(a->i)! => Oit? Ne, Tei! + print("Expected: Oit? Ne, Tei!") + print("Actual:", msg2.apply_transpose(dict2)) + + print("\n--- Student Test Case 3: EncryptedSubMessage ---") + # Test case: Decrypting a known string + # "Like a rolling stone" -> encrypted with 'uoeia' (i->e, e->o, a->u, o->i) + # L(i->e)k(e->o) (a->u) r(o->i)ll(i->e)ng st(o->i)n(e->o) -> Leko u rilleng stino + encrypted_text1 = "Leko u rilleng stino" + enc_msg_obj1 = EncryptedSubMessage(encrypted_text1) + print(f"Input Encrypted: '{encrypted_text1}'") + print("Expected Decrypted: Like a rolling stone") + print("Actual Decrypted:", enc_msg_obj1.decrypt_message()) + + print("\n--- Student Test Case 4: EncryptedSubMessage ---") + # Test case: String with no valid decryption (should return original) + # Or a string with numbers/symbols heavily involved + nonsense_text = "Xyz 123" + enc_msg_obj2 = EncryptedSubMessage(nonsense_text) + print(f"Input Encrypted: '{nonsense_text}'") + print("Expected Decrypted: Xyz 123 (Or unchanged since no vowels/words found)") + print("Actual Decrypted:", enc_msg_obj2.decrypt_message())