Skip to content

Instantly share code, notes, and snippets.

@ecarnevale
Forked from jamis/codes.rb
Created February 13, 2009 13:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ecarnevale/63892 to your computer and use it in GitHub Desktop.
Save ecarnevale/63892 to your computer and use it in GitHub Desktop.
# 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".
#
# Forked from Jamis Buck's http://gist.github.com/58141
# changed to show code as numbers instead of letters and a hint of solution instead of the key.
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'prawnlink', 'lib'))
require 'prawn'
ALPHABET = ('A'..'Z').to_a.freeze
CHARTEST = /[A-Z1234567890]/
# 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, hintPos=0)
#randomized = ALPHABET.sort_by { rand }
randomized = (1..ALPHABET.length).sort_by { rand }
map = Hash[*ALPHABET.zip(randomized).to_a.flatten]
rmap = Hash[*randomized.zip(ALPHABET).to_a.flatten]
messageUp = message.strip.upcase
if hintPos > 0 then
hintStart = 1 + messageUp.split(" ")[0,hintPos-1].join(" ").length
hintStop = messageUp.split(" ")[hintPos-1].length + hintStart
else
hintStart = 0
hintStop = 0
end
encoded = messageUp.split(//).map { |c| map[c] || c }
{ :message => messageUp.split(//), :encoded => encoded, :key => rmap, :hintStart => hintStart, :hintStop => hintStop }
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|
char = char.to_s
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.to_s =~ CHARTEST
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.
puts result[:hintStart]
puts result[:hintStop]
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.to_s =~ CHARTEST
length = character_width - doc.font_size / 2
width = doc.width_of(char.to_s)
if ((result[:hintStart]..result[:hintStop]).include?(index+1)) then
doc.text(result[:message][index], :at => [x + length/2 - width/2, y])
else
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])
end
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, 5)
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