Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A basic substitution-cipher puzzle generator. Uses Prawn (0.5 or later) for PDF output. Sample output: http://www.jamisbuck.org/files/codes.pdf
# Generates a series of substitution cipher puzzles. The messages to
# "encrypt" are take from a text file passed on the command-line,
# where each message is on one line.
#
# The output is a PDF, "codes.pdf".
#
# This was written for my 7 year-old, who loves doing substitution
# ciphers.
require 'prawn'
ALPHABET = ('A'..'Z').to_a.freeze
# Generate the encrypted version of the given message. Returns a
# Hash object including the original message, the encoded message,
# and the key (another Hash describing the cipher used).
def code_for(message)
randomized = ALPHABET.sort_by { rand }
map = Hash[*ALPHABET.zip(randomized).to_a.flatten]
rmap = Hash[*randomized.zip(ALPHABET).to_a.flatten]
encoded = message.strip.upcase.split(//).map { |c| map[c] || c }
{ :message => message, :encoded => encoded, :key => rmap }
end
# Writes the "key" to the PDF in the form of a red line of letters
# and a corresponding green line of letters, showing how to decipher
# the code.
def write_key(doc, result)
from = result[:key].keys.sort
to = from.map { |char| result[:key][char] }
doc.font "Helvetica", :size => 8
y = doc.y - doc.font.ascender - doc.bounds.absolute_bottom
margin = 30
doc.fill_color "ff0000"
from.each_with_index do |char, index|
width = doc.width_of(char)
doc.text(char, :at => [margin + (index+1) * doc.font_size * 1.5 - width/2, y])
end
y -= doc.font.height
doc.fill_color "008000"
to.each_with_index do |char, index|
width = doc.width_of(char)
doc.text(char, :at => [margin + (index+1) * doc.font_size * 1.5 - width/2, y])
end
doc.y = y + doc.bounds.absolute_bottom - doc.font.height
end
# Writes the given encrypted message (index +number+ into the
# +result+ array of ciphers) to the PDF (+doc+). If +divider+
# is true, a horizontal rule is rendered above the puzzle.
#
# The message is word-wrapped, and the height calculated, so that
# a puzzle never spans two pages. (This means the messages need
# to be short, or the results are undefined.)
def write_puzzle(number, doc, result, divider)
doc.font_size = 10
# Calculate where the lines should be broken to wrap the puzzle
# onto multiple lines.
breaks = [result[:encoded].length]
character_width = doc.font_size * 2.5
line_width = doc.bounds.width - doc.font_size * 3
position = 0
length = 0
while position < result[:encoded].length
char = result[:encoded][position]
breaks[-1] = position if char == ' '
if char =~ /[A-Z]/
length += character_width
else
length += doc.font_size
end
position += 1
if length > line_width
position = breaks.last + 1
breaks << result[:encoded].length
length = 0
end
end
y = doc.y - doc.bounds.absolute_bottom
# The line-break at the end of the message can be discarded
breaks.pop
# If the cipher is longer than the remaining space on the page,
# start a new page.
if y - (breaks.length+1) * doc.font.height * 3 < doc.bounds.absolute_bottom
doc.start_new_page
y = doc.y - doc.bounds.absolute_bottom
divider = false
end
# Draw a horizontal rule if a divider line is needed
if divider
doc.move_to 0, y
doc.line_to bounds.width, y
doc.move_to 0, y - 2
doc.line_to bounds.width, y - 2
doc.stroke
y -= doc.font.height * 2
end
height = (breaks.length + 1) * doc.font.height * 3
margin = doc.font_size * 2
line = 0
x = doc.font_size * 3
# Draw the puzzle number as an outlined glyph (render mode 1)
doc.stroke_color "000000"
doc.fill_color "000000"
doc.font "Helvetica", :style => :bold, :size => 32
doc.add_content "1 Tr"
doc.text(number.to_s, :at => [0,y - doc.font.height/2])
doc.font "Helvetica", :size => 10
doc.add_content "0 Tr"
y -= doc.font.height
# Draw each line of the encrypted message as blank lines
# that can be filled in by pencil.
result[:encoded].each_with_index do |char, index|
# If we're at a line break position, start a new line
if index == breaks[line]
line += 1
x = doc.font_size * 3
y -= doc.font.height * 3
# If the puzzle is a letter at this position, draw a
# blank line with the encrypted letter below it.
elsif char =~ /[A-Z]/
length = character_width - doc.font_size / 2
width = doc.width_of(char)
doc.move_to x, y
doc.line_to x + length, y
doc.stroke
doc.text(char, :at => [x + length/2 - width/2, y - doc.font.height])
x += character_width
# Other characters we draw directly (punctuation, spaces, etc.)
else
doc.text(char, :at => [x,y])
x += doc.font_size
end
end
doc.y = y - doc.font.height * 2 + doc.bounds.absolute_bottom
end
abort "Please specify a text file containing the messages to encrypt" unless ARGV.any?
Prawn::Document.generate("codes.pdf", :compress => true) do
messages = File.readlines(ARGV.first)
messages.each_with_index do |message, index|
result = code_for(message)
write_puzzle(index+1, self, result, index > 0)
write_key(self, result)
self.y -= font.height
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment