--Apple-Mail-8--197501626
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
format=flowed
Class-oriented, but kinda long maybe. Hey, it has unit tests . . .
Reads in a file, deck.yaml, if it exists to specify the deck order. Zip
of the files is attached. If you run it out of the box it will use the
non-default deck in deck.yaml.
Thanks for the quiz, it was fun!
Moses
--Apple-Mail-8--197501626
Content-Transfer-Encoding: base64
Content-Type: application/zip;
x-unix-mode=0644;
name="solution.zip"
Content-Disposition: attachment;
filename=solution.zip
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
SwUGAAAAAAkACQBoAgAATRQAAAAA
--Apple-Mail-8--197501626
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed
########################################################################
########
# solitaire.rb
########################################################################
########
#!/usr/bin/env ruby
require 'cipher'
require 'yaml'
module Solitaire
text = ARGV.join(" ")
if FileTest::readable?("deck.yaml")
deck = Deck.new(YAML::load(File.open("deck.yaml")))
else
deck = Deck.new
end
cipher = Cipher.new(text, deck)
puts "#{cipher.mode}ed: #{cipher.crypt}"
end
########################################################################
########
# cipher.rb
########################################################################
########
#!/usr/bin/env ruby
require 'util'
require 'deck'
module Solitaire
ASCII_OFFSET = 64
ALPHABET_SIZE = 26
class Chunker
CHUNK_SIZE = 5
def initialize(text)
@legal_chars_only = text.gsub(/[^A-Za-z]/, "").upcase
@legal_chars_only <<= "X" * (- / legal_chars_only.size % CHUNK_SIZE)
raise "Nothing to chunk (non-alphabet characters removed): #{text}"
if @legal_chars_only.size==0
@chunks = []
@number_chunks = []
end
def chunks
@chunks if @chunks.size > 0
@chunks = @legal_chars_only.gsub(/(.{#{CHUNK_SIZE}})/, '\1
').rstrip.split(" ")
end
def number_chunks
@number_chunks if @number_chunks.size > 0
chunks.collect { |chunk| chunk.split("").collect { |char_string|
char_string[0]-ASCII_OFFSET } }
end
def Chunker.to_letters(number_chunks)
number_chunks.collect { |chunk| chunk.collect { |num|
(num+ASCII_OFFSET).chr }.join }
end
end
class Keystream
A_JOKER = Card.joker(?A)
B_JOKER = Card.joker(?B)
def initialize(deck=Deck.new)
@deck = deck
end
def keystream_letters(chunks)
chunks.collect { |chunk| (1..chunk.size).collect {
next_keystream_letter }.join }
end
def Keystream.card_to_letter(card)
return "" if card.is_joker?
(card.value.offset_mod(ALPHABET_SIZE)+ASCII_OFFSET).chr
end
private
def next_keystream_letter
process_deck
top_card = @deck[0]
keystream_card = @deck[top_card.value]
letter = Keystream.card_to_letter(keystream_card)
letter = next_keystream_letter if letter==""
letter
end
def process_deck
@deck.move_card!(A_JOKER, +1)
@deck.move_card!(B_JOKER, +2)
@deck.triple_cut!([A_JOKER, B_JOKER])
@deck.count_cut!
end
end
class Cipher
ENCRYPTED_TEXT_PATTERN = /\A[A-Z]{5}( [A-Z]{5})*\Z/
def initialize(text, deck=Deck.new)
@chunker = Chunker.new(text)
keystream = Keystream.new(deck)
@keystream_chunker =
Chunker.new(keystream.keystream_letters(@chunker.chunks).join)
if text =~ ENCRYPTED_TEXT_PATTERN
@mode = "decrypt"
@calc_number = proc { |num, keystream_num| num - keystream_num }
else
@mode = "encrypt"
@calc_number = proc { |num, keystream_num| num + keystream_num }
end
end
attr_reader :mode
def crypt
ciphered = [@chunker.number_chunks,
@keystream_chunker.number_chunks].collect_peel do |num_chunk,
keystream_num_chunk|
[num_chunk, keystream_num_chunk].collect_peel do |num,
keystream_num|
@calc_number.call(num,keystream_num).offset_mod(ALPHABET_SIZE)
end
end
Chunker.to_letters(ciphered).join(" ").rstrip
end
end
end
########################################################################
########
# deck.rb
########################################################################
########
require 'util'
module Solitaire
class Deck
NUM_CARDS = 54
def initialize(order=nil)
if order.nil?
@order = Array.new(NUM_CARDS) { |x| x+1 }
else
@order = order.collect { |val| val.instance_of?(Range) ? val.to_a :
val }.flatten
@order.collect! { |val| Card.parse(val).code }
end
end
attr_reader :order
protected :order
def [](index)
Card.parse(@order[index])
end
def move_card!(card, offset)
current_index = @order.index(card.code)
new_index = current_index+offset
if new_index >= NUM_CARDS
new_index -= NUM_CARDS - 1
elsif new_index < 0
new_index += NUM_CARDS - 1
end
@order.delete_at(current_index)
@order.insert(new_index, card.code)
end
def triple_cut!(cards)
raise "exactly two cards required for triple cut: #{cards}" unless
cards.size==2
indices = [@order.index(cards[0].code),
@order.index(cards[1].code)].sort
@order = @order[(indices[1]+1).. / order.size] +
@order[indices[0]..indices[1]] + @order[0..(indices[0]-1)]
end
def count_cut!
num_moved = Card.parse(@order.last).value
if num_moved!=53
@order = @order[num_moved, NUM_CARDS - num_moved - 1] +
@order[0..(num_moved-1)] + [@order.last]
end
self
end
def to_s
"<Deck: #{@order.inspect}>"
end
def ==(val)
@order == val.order
end
end
class Card
ACE = 1
JACK = 11
QUEEN = 12
KING = 13
def initialize(suit, value)
@code = suit.value + value
end
attr_reader :code
alias_method :value, :code
def Card.parse(code)
if (1..52).member?(code)
Card.new(Suit.by_value(code), code.offset_mod(13))
elsif (53..54).member?(code)
Card.joker(code+12)
elsif (65..66).member?(code)
Card.joker(code)
elsif code =~ /\A[AB]\Z/
Card.joker(code[0])
else
raise "Illegal class or value for parameter value, #{code.class}
#{code.inspect}"
end
end
def Card.joker(char)
JokerCard.new(char.chr)
end
def is_joker?
false
end
def ==(other)
code==other.code
end
def <=>(other)
code<=>other.code
end
def to_s
"Card: #{@code}"
end
end
class JokerCard < Card
JOKER_VALUE = 53
def initialize(which_one)
raise "No such joker: #{which_one}" unless which_one =~ /\A[AB]\Z/
@code = 52 + (which_one[0].to_i-64)
end
def is_joker?
true
end
def value
JOKER_VALUE
end
end
class Suit
@@byValue = {}
def Suit.by_value(val)
@@byValue[(val-1)/13*13]
end
def initialize(name, value)
@name = name
@value = value
@@byValue[value] = self
end
attr_reader :name, :value
CLUBS = Suit.new("clubs", 0)
DIAMONDS = Suit.new("diamonds", 13)
HEARTS = Suit.new("hearts", 26)
SPADES = Suit.new("spades", 39)
end
end
########################################################################
########
# util.rb
########################################################################
########
#!/usr/bin/env ruby
class Array
# "Peels" off a tuple of the elements at each successive index across
multiple arrays.
# Assumes self is an array of these multiple arrays. Stops when any of
the arrays is
# exhausted. I stole this from a ruby mailing list somewhere. I also
considered calling this each_tuple
def peel(&p)
collect { |a|
a.length
}.min.times { |i|
yield collect { |a| a[i] }
}
end
# syntactic sugar for Cipher
def collect_peel(&p)
collected = []
peel { |a,b| collected << p.call(a,b) }
collected
end
end
class Fixnum
def offset_mod(base)
((self-1)%base)+1
end
end
########################################################################
########
# deck.yaml
########################################################################
########
# Array of numbers (1 through 54), ranges of those numbers,
# and "A" or "B" that describes the order of the keyed deck
---
- 25
- !ruby/range 1..24
- !ruby/range 26..54
########################################################################
########
# testsuite.rb
########################################################################
########
#!/usr/bin/env ruby
require 'test/unit'
require 'test_cipher'
require 'test_deck'
require 'test_util'
########################################################################
########
# test_cipher.rb
########################################################################
########
#!/usr/bin/env ruby
require 'test/unit'
require 'cipher'
module Solitaire
class TestChunker < Test::Unit::TestCase
def test_chunks
chunker = Chunker.new("Code in Ruby, live longer!")
assert_equal(["CODEI","NRUBY","LIVEL","ONGER"], chunker.chunks)
end
def test_pads_with_Xs
chunker = Chunker.new("sty")
assert_equal(["STYXX"], chunker.chunks)
end
def test_number_chunks
chunker = Chunker.new("Code in Ruby, live longer!")
assert_equal([[3,15,4,5,9], [14,18,21,2,25], [12,9,22,5,12],
[15,14,7,5,18]], chunker.number_chunks)
end
def test_to_letters
assert_equal(["CODEI","NRUBY","LIVEL","ONGER"],
Chunker.to_letters([[3,15,4,5,9], [14,18,21,2,25], [12,9,22,5,12],
[15,14,7,5,18]]))
end
end
class TestKeystream < Test::Unit::TestCase
def setup
@keystream = Keystream.new
end
def test_keystream_letters
chunker = Chunker.new("Code in Ruby, live longer!")
assert_equal(["DWJXH","YRFDG","TMSHP","UURXJ"],
@keystream.keystream_letters(chunker.chunks))
end
def test_card_to_letter
assert_equal("", Keystream.card_to_letter(Card.joker(?A)), "A joker")
assert_equal("", Keystream.card_to_letter(Card.joker(?B)), "B joker")
assert_equal("A", Keystream.card_to_letter(Card.new(Suit::CLUBS,
Card::ACE)), "AC")
assert_equal("Z", Keystream.card_to_letter(Card.new(Suit::DIAMONDS,
Card::KING)), "KD")
assert_equal("A", Keystream.card_to_letter(Card.new(Suit::HEARTS,
Card::ACE)), "AH")
assert_equal("Z", Keystream.card_to_letter(Card.new(Suit::SPADES,
Card::KING)), "KS")
end
end
class TestCipher < Test::Unit::TestCase
def test_encrypt
cipher = Cipher.new("Code in Ruby, live longer!")
assert_equal("encrypt", cipher.mode)
assert_equal("GLNCQ MJAFF FVOMB JIYCB", cipher.crypt)
end
def test_decrypt
cipher = Cipher.new("GLNCQ MJAFF FVOMB JIYCB")
assert_equal("decrypt", cipher.mode)
assert_equal("CODEI NRUBY LIVEL ONGER", cipher.crypt)
end
def test_crypt_idempotent
cipher = Cipher.new("GLNCQ MJAFF FVOMB JIYCB")
assert_equal("decrypt", cipher.mode)
assert_equal("CODEI NRUBY LIVEL ONGER", cipher.crypt)
assert_equal("CODEI NRUBY LIVEL ONGER", cipher.crypt)
end
end
end
########################################################################
########
# test_deck.rb
########################################################################
########
#!/usr/bin/env ruby
require 'test/unit'
require 'deck'
module Solitaire
class TestDeck < Test::Unit::TestCase
def test_constructor_accepts_ranges_in_array
assert_equal(Deck.new, Deck.new([1, 2, 3, 4..52, 53..54]))
end
def test_construcor_accepts_chars_for_jokers
assert_equal(Deck.new, Deck.new((1..52).to_a + [?A, ?B]))
end
def test_move_card_lower
deck = Deck.new
deck.move_card!(Card.new(Suit::DIAMONDS, 6), +5)
assert_equal(Deck.new([1..18,20..24,19,25..54]).to_s, deck.to_s)
end
def test_move_card_one_lower
deck = Deck.new
deck.move_card!(Card.new(Suit::DIAMONDS, 6), +1)
assert_equal(Deck.new([1..18,20,19,21..54]).to_s, deck.to_s)
end
def test_move_card_higher
deck = Deck.new
deck.move_card!(Card.new(Suit::DIAMONDS, 6), -5)
assert_equal(Deck.new([1..13,19,14..18,20..54]), deck)
end
def test_move_card_is_cyclic_plus_1
deck = Deck.new
deck.move_card!(Card.joker(?A), +2)
assert_equal(Deck.new([1,53,2..52,54]), deck)
end
def test_move_card_to_end
deck = Deck.new
deck.move_card!(Card.joker(?A), +1)
assert_equal(Deck.new([1..52,54,53]), deck)
end
def test_move_card_to_one_before_end
deck = Deck.new([2..54,1])
deck.move_card!(Card.joker(?A), +1)
assert_equal(Deck.new([2..52,54,53,1]).to_s, deck.to_s)
end
def test_move_card_to_beginning
deck = Deck.new
deck.move_card!(Card.new(Suit::CLUBS, 2), -1)
assert_equal(Deck.new([2,1,3..54]), deck)
end
def test_move_card_is_cyclic_pass_end_forward
deck = Deck.new
deck.move_card!(Card.joker(?B), +1)
assert_equal(Deck.new([1,54,2..53]), deck)
end
def test_move_card_is_cyclic_pass_end_backward
deck = Deck.new
deck.move_card!(Card.new(Suit::CLUBS, Card::ACE), -1)
assert_equal(Deck.new([2..53,1,54]), deck)
end
def test_move_card_cyclic_backward_five
deck = Deck.new
deck.move_card!(Card.new(Suit::CLUBS, 5), -5)
assert_equal(Deck.new([1..4,6..53,5,54]), deck)
end
def test_triple_cut
deck = Deck.new
deck.triple_cut!([Card.new(Suit::CLUBS, Card::JACK),
Card.new(Suit::HEARTS, Card::KING)])
assert_equal(Deck.new([40..54,11..39,1..10]), deck)
end
def test_triple_cut_with_empty_side
deck = Deck.new([2..53,1,54])
deck.triple_cut!([Card.joker(?A), Card.joker(?B)])
assert_equal(Deck.new([53,1,54,2..52]), deck)
end
def test_count_cut
deck = Deck.new
deck.move_card!(Card.new(Suit::CLUBS, 5), 49)
deck.count_cut!
assert_equal(Deck.new([7..54,1..4,6,5]), deck)
end
end
class TestCard < Test::Unit::TestCase
def test_parse_joker_string
assert_equal(Card.joker(?A), Card.parse("A"))
end
def test_parse_joker_char
assert_equal(Card.joker(?A), Card.parse(?A))
end
def test_parse_normal_card
assert_equal(Card.new(Suit::CLUBS, 5), Card.parse(5))
end
def test_value
assert_equal(1, Card.new(Suit::CLUBS, Card::ACE).value, "AC")
assert_equal(7, Card.new(Suit::CLUBS, 7).value, "7C")
assert_equal(11, Card.new(Suit::CLUBS, Card::JACK).value, "JC")
assert_equal(13, Card.new(Suit::CLUBS, Card::KING).value, "KC")
assert_equal(14, Card.new(Suit::DIAMONDS, Card::ACE).value, "AD")
assert_equal(26, Card.new(Suit::DIAMONDS, Card::KING).value, "KD")
assert_equal(29, Card.new(Suit::HEARTS, 3).value, "3H")
assert_equal(39, Card.new(Suit::HEARTS, Card::KING).value, "KH")
assert_equal(40, Card.new(Suit::SPADES, Card::ACE).value, "AS")
assert_equal(52, Card.new(Suit::SPADES, Card::KING).value, "KS")
assert_equal(53, Card.joker(?A).value, "A Joker")
assert_equal(53, Card.joker(?B).value, "B Joker")
end
def test_is_joker_when_joker
assert(Card.joker(?A).is_joker?, "A joker not joker")
assert(Card.joker(?B).is_joker?, "B joker not joker")
end
def test_is_joker_when_not_joker
assert(!Card.new(Suit::SPADES, 7).is_joker?)
end
end
class TestSuit < Test::Unit::TestCase
def test_by_value_clubs
assert_equal("clubs", Suit.by_value(1).name)
end
def test_by_value_hearts
assert_equal("hearts", Suit.by_value(27).name)
end
def test_hearts
assert_equal("hearts", Suit::HEARTS.name)
end
end
end
########################################################################
########
# test_util.rb
########################################################################
########
#!/usr/bin/env ruby
require 'test/unit'
require 'util'
class TestArray < Test::Unit::TestCase
def test_peel_all_arrays_same_length
tuples = []
a1 = [1,3,5]
a2 = [2,4,6]
a3 = [1,4,9]
[a1, a2, a3].peel { |x,y,z| tuples << [x,y,z] }
assert_equal([[1,2,1],[3,4,4],[5,6,9]], tuples)
end
def test_peel_one_array_shorter
tuples = []
a1 = [1,3,5]
a2 = [2,4]
a3 = [1,4,9]
[a1, a2, a3].peel { |x,y,z| tuples << [x,y,z] }
assert_equal([[1,2,1],[3,4,4]], tuples)
end
def test_collect_peel
a1 = [1,3,5]
a2 = [2,4,6]
assert_equal([3,7,11], [a1, a2].collect_peel { |a,b| a+b })
end
end
class TestFixnum < Test::Unit::TestCase
def test_offset_mod
assert_equal(1, 27.offset_mod(26), "27 wrong")
assert_equal(26, 26.offset_mod(26), "26 wrong")
assert_equal(26, 0.offset_mod(26), "0 wrong")
assert_equal(25, -1.offset_mod(26), "-1 wrong")
end
end
--Apple-Mail-8--197501626--