Last active
January 23, 2024 03:00
-
-
Save kevboh/47eeb6d6602a9dffd962346828d539b6 to your computer and use it in GitHub Desktop.
Decoder for coded messages in Sherlock Holmes Consulting Detective (Jack the Ripper & West End) case 7, A Question of Identity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Decoder for coded messages in Sherlock Holmes Consulting Detective (Jack the Ripper & West End) | |
// case 7, A Question of Identity | |
// RUNNING THIS IS FOR SURE SPOILERS so maybe don't, you're probably smarter than I am and can do it without this <3 | |
// To run in Xcode Playgrounds, dowload the file and remove the .swift extension. | |
import Foundation | |
let outer = "abcdefghijklmnopqrstuvwxyz".uppercased() | |
let inner = "aoepctqihjgfkbrylvdznxuwms".uppercased() | |
/// Given a string's character view, "rotates" that view the given number of places by moving that many | |
/// characters from the end of the view to the start. | |
/// | |
/// - Parameters: | |
/// - characters: The character view (`.characters`) of a string to rotate. | |
/// - places: The number of places to rotate that string. | |
/// - Returns: The rotated character view. Use `String.init()` to cast it back to a string, if necessary. | |
func rotate(characters: String.CharacterView, places: Int) -> String.CharacterView { | |
let suffix = characters.dropLast(places) | |
var prefix = characters.suffix(from: characters.index(characters.endIndex, offsetBy: -places)) | |
prefix.append(contentsOf: suffix) | |
return String(prefix).characters | |
} | |
/// Given a character in `input`, return the corresponding character in `key`. | |
/// | |
/// - Parameters: | |
/// - character: The character to search for. | |
/// - key: The key, probably a rotated character view. | |
/// - input: The static "ring" string. | |
/// - Returns: The corresponding character in `key`, or `nil` if none is found. | |
func decode(character: Character, key: String.CharacterView, input: String) -> Character? { | |
guard let index = input.characters.index(of: character) else { return nil } | |
return key[index] | |
} | |
/// A wrapper around `NSLinguisticTagger` to do some lightweight, janky analysis on a string to try | |
/// and guess if it's readable English. | |
struct Analyzer { | |
/// The input string. | |
let string: String | |
private let tagger = NSLinguisticTagger( | |
tagSchemes: [NSLinguisticTagSchemeLanguage, NSLinguisticTagSchemeLemma], | |
options: 0 | |
) | |
/// Create an analyzer to guess at the given string. | |
/// | |
/// - Parameter string: An attempt at a decoded message. | |
init(string: String) { | |
self.string = string | |
self.tagger.string = string | |
} | |
/// `true` if the sentence appears to be an English-like language. | |
var isEnglishish: Bool { | |
var mayBeEnglish = false | |
tagger.enumerateTags( | |
in: NSMakeRange(0, string.characters.count), | |
scheme: NSLinguisticTagSchemeLanguage, | |
options: [] | |
) { (tag, _, _, stop) in | |
if tag == "en" { | |
mayBeEnglish = true | |
stop[0] = true | |
} | |
} | |
return mayBeEnglish | |
} | |
/// `true` if the majority of words in the string have valid-seeming roots. | |
var isMajorityLemmable: Bool { | |
var lemmaCount = 0 | |
let wordCount = string.components(separatedBy: " ").count | |
tagger.enumerateTags( | |
in: NSMakeRange(0, string.characters.count), | |
scheme: NSLinguisticTagSchemeLemma, | |
options: [] | |
) { tag, _, _, _ in | |
if !tag.isEmpty { | |
lemmaCount = lemmaCount + 1 | |
} | |
} | |
return lemmaCount >= wordCount / 2 | |
} | |
} | |
/// Attempt to turn a message encoded with the Sherlock rotating cipher into a decoded English sentence. | |
/// | |
/// - Parameter message: The message to decode. | |
/// - Returns: The decoded result, if decoding was deemed successful by the analyzer. | |
func decode(message: String) -> (String, Int)? { | |
for offset in 0..<26 { | |
let key = rotate(characters: outer.characters, places: offset) | |
let decoded = String(message.characters.map({decode(character: $0, key: key, input: inner) ?? $0})) | |
let analyzer = Analyzer(string: decoded) | |
if analyzer.isEnglishish && analyzer.isMajorityLemmable { | |
return (decoded, offset) | |
} | |
} | |
return nil | |
} | |
// clue in asylum | |
let asylumMessage = "NVWMVO. KJUYBA. WU EAJVN. VR MAWGNBXO OBB VXLWRR JE GBELNBLBX." | |
print("Decoding asylum message: \(asylumMessage)") | |
if let (result, offset) = decode(message: asylumMessage) { | |
print("\(result) (with ring offset \(offset))") | |
} | |
print("---") | |
// mesage in newspaper | |
let newspaperMessage = "GIBRIV. APXSLP. QIEPBZD FSW AP JKBXK. IC RLBAGPF TB DB VPNPK KPXOBFPK." | |
print("Decoding newspaper message \(newspaperMessage)") | |
if let (result, offset) = decode(message: newspaperMessage) { | |
print("\(result) (with ring offset \(offset))") | |
} |
Thank you -- we spent 45 minutes trying to do this with pen and paper, and I don't even know how to code but figured out how to run that html just for this - so yeah, lol.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I don't have what it needs to run swift, so instead I decided write a Javascript version.
At first, I copied your algorithm, then I used the solution in the book to get a better version.
SPOILERS: Click to expand