Skip to content

Instantly share code, notes, and snippets.

@thirdratecyberpunk
Last active December 14, 2022 18:02
Show Gist options
  • Save thirdratecyberpunk/55c91f1be163bc2bea03ec7617c2a54b to your computer and use it in GitHub Desktop.
Save thirdratecyberpunk/55c91f1be163bc2bea03ec7617c2a54b to your computer and use it in GitHub Desktop.
Quick and dirty code for the automatable puzzles in the gchq Christmas Card puzzle 2022
import nltk
from nltk.corpus import words
class Gear:
def __init__(self, word, direction):
# letters on the teeth, assuming the first character in the string is the initial gear setting
# and you read the letters in a clockwise sequence
self.word = word
# whether the gear rotates anti-clockwise (1) or clockwise (-1)
self.direction = direction
def rotate(self, num_steps):
"""
Returns the character that the gear should "rotate" to given a number of steps
"""
increments = (num_steps % len(self.word)) * self.direction
return self.word[increments]
def analysis_problem():
"""
Finds all the even length words in the sentence
"""
message = "The Director has written his usual Christmas message but now uncover the odd one out amongst the words forming these two sentences Its odd because its not odd"
tokens = message.split()
even = []
for token in tokens:
if len(token) % 2 == 0:
even.append(token)
return even
def coding_problem():
"""
Brute forces all the possible solutions to the grid problem
Checks if all three rows are valid 3 letter word rows and then if the final state creates a 9 letter word
"""
# get all words
nltk.download('words')
word_list = words.words()
# initial problem constraints
initial_state = [["f", "o", "r"], ["m", "a", "t"], ["i", "o", "n"]]
blue_word = "part"
green_word = "eyes"
gold_word = "uncurl"
# finds all the valid words for row 0
print("checking row 0...")
row_0_valid_words = []
for gold_character in gold_word:
for blue_character in blue_word:
row_0 = initial_state[0]
row_0[0] = gold_character
row_0[1] = blue_character
result = ''.join(row_0)
if result in words.words():
row_0_valid_words.append(result)
unique_row_0 = set(row_0_valid_words)
print(f"{len(unique_row_0)} unique solutions found: {unique_row_0}")
# finds all the valid words for row 1
print("checking row 1...")
row_1_valid_words = []
for gold_character in gold_word:
for blue_character in blue_word:
for green_character in green_word:
row_1 = initial_state[1]
row_1[0] = blue_character
row_1[1] = green_character
row_1[2] = gold_character
result = ''.join(row_1)
if result in words.words():
row_1_valid_words.append(result)
unique_row_1 = set(row_1_valid_words)
print(f"{len(unique_row_1)} unique solutions found: {unique_row_1}")
# finds all the valid words for row 2
print("checking row 2...")
row_2_valid_words = []
for gold_character in gold_word:
for blue_character in blue_word:
for green_character in green_word:
row_2 = initial_state[2]
row_2[0] = blue_character
row_2[1] = gold_character
row_2[2] = green_character
result = ''.join(row_2)
if result in words.words():
row_2_valid_words.append(result)
unique_row_2 = set(row_2_valid_words)
print(f"{len(unique_row_2)} solutions found: {unique_row_2}")
# finds a combination of valid words from the rows that creates a valid 9 letter word
print("finding valid 9 letter word(s)...")
solutions = []
for row_0_valid in unique_row_0:
for row_1_valid in unique_row_1:
for row_2_valid in unique_row_2:
candidate = row_0_valid + row_1_valid + row_2_valid
if candidate in words.words():
solutions.append(candidate)
return solutions
def engineering_problem():
"""
Returns the word given a series of gears objects
Could probably make it so you can create a GearPuzzle class
which specifies an original direction and if any Gears are connected
by a chain to calculate rotation direction to make this scale to any size
puzzle but I'll leave that for another time
"""
gear_0 = Gear("RMNOPQ", -1)
gear_1 = Gear("EFGHIJKL", 1)
gear_2 = Gear("JBCDEFGHI", 1)
gear_3 = Gear("KLMNO", -1)
gear_4 = Gear("XYZTUVW", -1)
gears = [gear_0, gear_1, gear_2, gear_3, gear_4]
word = []
for gear in gears:
word.append(gear.rotate(20))
return ''.join(word)
def next_alpha(s, increment):
"""
Returns the letter in the alphabet that is x away from the given letter
"""
return chr((ord(s.upper()) + increment - 65) % 26 + 65)
def codebreaking_problem():
"""
Brute forces the Caesar Shift
This doesn't actually WORK because NLTK doesn't recognise plurals
A bit of sanitisation could handle it or you could just read the final output
"""
# only handles the obvious what.3.words address
coded_string = "pvucpbse.hsje.sfkpjot"
# removes full stop to make checking if it's all words easier
coded_string_tokens = coded_string.split('.')
for i in range (1, 26):
result = []
# increments all characters in string
for token in coded_string_tokens:
result.append(''.join([next_alpha(char, i) for char in token]).lower())
# checks if all resultant words are valid english words
is_word = True
print(result)
for potential_word in result:
print(potential_word)
if potential_word not in words.words():
is_word = False
else:
print(f"{potential_word} is a valid english word!")
# if they are, assume that this is the answer
if is_word:
return result
# would be interesting to see if there are any sentences that are
# Caesar Shift hash collisions
return ("No valid Caesar Shift found")
print(analysis_problem())
print(coding_problem())
print(engineering_problem())
print(codebreaking_problem())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment