Hello!
Very nice quiz!!! :-)
Below is my solution.
Greetings,
Thomas
#!/usr/bin/env ruby
require 'optparse'
require 'ostruct'
# Handles the deck
class Deck
# Initializes the deck with the default values
def initialize
@deck = (1..52).to_a << 'A' << 'B'
end
# Operation "move a" (step 2)
def move_A
move_down( @deck.index( 'A' ) )
end
# Operation "move b" (step 3)
def move_B
2.times { move_down( @deck.index( 'B' ) ) }
end
# Operation "triple cut" (step 4)
def triple_cut
a = @deck.index( 'A' )
b = @deck.index( 'B' )
a, b = b, a if a > b
@deck.replace( [@deck[(b + 1)..-1], @deck[a..b], @deck[0...a]].flatten )
end
# Operation "count cut" (step 5)
def count_cut
temp = @deck[0..(@deck[-1] - 1)]
@deck[0..(@deck[-1] - 1)] = []
@deck[-1..-1] = [temp, @deck[-1]].flatten
end
# Operation "output the found letter" (step 6)
def output_letter
a = @deck.first
a = 53 if a.instance_of? String
output = @deck[a]
if output.instance_of? String
nil
else
output -= 26 if output > 26
(output + 64).chr
end
end
# Shuffle the deck using the initialization number +init+ and the method +method+.
# Currently there are only two methods: <tt>:fisher_yates</tt> and <tt>naive</tt>.
def shuffle( init, method = :fisher_yates )
srand( init )
self.send( method.id2name + "_shuffle", @deck )
end
private
# From pleac.sf.net
def fisher_yates_shuffle( a )
(a.size-1).downto(0) { |i|
j = rand(i+1)
a[i], a[j] = a[j], a[i] if i != j
}
end
# From pleac.sf.net
def naive_shuffle( a )
for i in 0...a.size
j = rand(a.size)
a[i], a[j] = a[j], a[i]
end
end
# Moves the index one place down while treating the used array as circular list.
def move_down( index )
if index == @deck.length - 1
@deck[1..1] = @deck[index], @deck[1]
@deck.pop
else
@deck[index], @deck[index + 1] = @deck[index + 1], @deck[index]
end
end
end
# Implements the Solitaire Cipher algorithm
class SolitaireCipher
# Initialize the deck
def initialize( init = -1, method = :fisher_yates )
@deck = Deck.new
@deck.shuffle( init, method ) unless init == -1
end
# Decodes the given +msg+ using the internal deck
def decode( msg )
msgNumbers = to_numbers( msg )
cipherNumbers = to_numbers( generate_keystream( msg.length ) )
resultNumbers = []
msgNumbers.each_with_index do |item, index|
item += 26 if item <= cipherNumbers[index]
temp = item - cipherNumbers[index]
resultNumbers << temp
end
return to_chars( resultNumbers )
end
# Encodes the given +msg+ using the internal deck
def encode( msg )
msg = msg.gsub(/[^A-Za-z]/, '').upcase
msg += "X"*(5 - (msg.length % 5)) unless msg.length % 5 == 0
msgNumbers = to_numbers( msg )
cipherNumbers = to_numbers( generate_keystream( msg.length ) )
resultNumbers = []
msgNumbers.each_with_index do |item, index|
temp = item + cipherNumbers[index]
temp = temp - 26 if temp > 26
resultNumbers << temp
end
return to_chars( resultNumbers )
end
private
# Converts the string of uppercase letters into numbers (A=1, B=2, ...)
def to_numbers( chars )
chars.unpack("C*").collect {|x| x - 64}
end
# Converts the array of numbers to a string (1=A, 2=B, ...)
def to_chars( numbers )
numbers.collect {|x| x + 64}.pack("C*")
end
# Generates a keystream for the given +length+.
def generate_keystream( length )
deck = @deck.dup
result = []
while result.length != length
deck.move_A
deck.move_B
deck.triple_cut
deck.count_cut
letter = deck.output_letter
result << letter unless letter.nil?
end
result.join
end
end
options = OpenStruct.new
options.key = -1
options.shuffle = :fisher_yates
options.call_method = :decode
opts = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename($0, '.*')} [options] message"
opts.separator ""
opts.separator "Options:"
opts.on( "-d", "--decode", "Decode the message" ) do
options.call_method = :decode
end
opts.on( "-e", "--encode", "Encode the message" ) do
options.call_method = :encode
end
opts.on( "-k", "--key KEY", Integer, "Key for shuffling the deck" ) do |key|
options.key = key
end
opts.on( "-m", "--method METHOD", [:fisher_yates, :naive],
"Select the shuffling method (fisher_yates, naive" ) do |method|
options.shuffle = method
end
opts.on( "-h", "--help", "Show help" ) do
puts opts
exit
end
end
if ARGV.length == 0
puts opts
exit
end
message = opts.permute!( ARGV )[0]
cipher = SolitaireCipher.new( options.key, options.shuffle )
puts cipher.send( options.call_method, message )