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))")
}
@Cellule
Copy link

Cellule commented Dec 22, 2017

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
const outer = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
const inner = ["A","O","E","P","C","T","Q","I","H","J","G","F","K","B","R","Y","L","V","D","Z","N","X","U","W","M","S"];

const sentences = [{
  hint: 10, // September 10th
  sentence: "NVWMVO. KJUYBA. WU EAJVN. VR MAWGNBXO OBB VXLWRR JE GBELNBLBX."
}, {
  hint: 19, // September 19th
  sentence: "GIBRIV. APXSLP. QIEPBZD FSW AP JKBXK. IC RLBAGPF TB DB VPNPK KPXOBFPK."
}, {
  // At Holmes' house
  hint: 19, // September 19th
  sentence: "QBGFPV"
}];

for (const {hint, sentence} of sentences) {
  const toAlign = String.fromCharCode('A'.charCodeAt(0) + hint - 1);
  const offset = 26 - inner.indexOf(toAlign);
  const rotatedOuter = outer.slice(offset, 26).concat(outer.slice(0, offset));
  let s = "";
  for (let c of sentence) {
    const index = inner.indexOf(c);
    if (index === -1) {
      s += c;
    } else {
      s += rotatedOuter[index];
    }
  }
  console.log(s);
}

@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