Skip to content

Instantly share code, notes, and snippets.

@kevboh
Last active January 23, 2024 03:00
Show Gist options
  • Save kevboh/47eeb6d6602a9dffd962346828d539b6 to your computer and use it in GitHub Desktop.
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
//: 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))")
}
@pevsfreedom
Copy link

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