On Sun, 26 Sep 2004 23:24:26 +0900, Florian Gross <flgr / ccan.de> wrote: > Moin! > > Here's my solution for the Solitaire Cipher quiz. It's fairly > class-oriented and longish. and another one gets added to the pile... also class oriented, just under 200 lines with comments. # bail if you have nothing to do. unless ARGV[0] print <<-STOP_PRINT Encrypts/decrypts a string of text. Usage: Give it some text! STOP_PRINT exit end # the obligatory deck of cards. ############################################################################### class Deck attr_reader :cards, :order def initialize @cards = [] @order = [] build_deck end def to_s return @cards.to_s end def build_deck [:Clubs, :Diamonds, :Hearts, :Spades].each do |suit| rank = 0 'A23456789TJQK'.each_byte do |name| # 'real' cards have a rank value rank += 1 add_card(name.chr, suit, rank) end end # Jokers have no rank value 'AB'.each_byte {|name| add_card(name.chr, :Joker, 0)} end # build order while adding. def add_card(name, suit, rank) card = Card.new(name, suit, rank) @cards << card @order << card.to_s end # Uses order to hunt for cards (Joker searches). def find_card(name, suit) return @order.index(Card.to_s(name, suit)) end # does as many cuts as you give it. def cut_cards(cuts) cards = [] loc = 0 [cuts].flatten.each_with_index do |cut, idx| cards[idx] = @cards[loc...cut] loc = cut end cards << @cards[loc... / cards.length] end def cards=(cards) # flatten to handle cut results. @cards = cards.flatten # rebuild @order each time the deck changes. update_order end # simple, but not very efficient. def update_order @order = @cards.collect {|card| card.to_s} end end # the above deck is made up of... ############################################################################### class Card @@SUITS = { :Clubs => 0, :Diamonds => 13, :Hearts => 26, :Spades => 39, :Joker => 53 } def self.to_s(name, suit) return name + ' ' + suit.to_s + "\n" end attr_reader :name, :suit, :rank def initialize(name, suit, rank) @name = name @suit = suit @rank = rank + @@SUITS[suit] end def to_s Card.to_s(@name, @suit) end end ############################################################################### class Solitaire attr_reader :deck def initialize(text) @deck = Deck.new @text = text.to_s end def process # does it look encrypted? 5 letter blocks all uppercase? looks_encrypted = @text.gsub(/[A-Z]{5}\s?/, '').empty? results = '' # prep the text for parsing. if looks_encrypted # strip off the blanks for consistency text = @text.gsub(/\s/, '') else # Discard any non A to Z characters, and uppercase all remaining text = @text.upcase.gsub!(/[^A-Z]/, '') # Split the message into five character groups, words, padding = word_count(text, 5) # using Xs to pad the last group text += padding end # parse it, and build up results. text.each_byte do |char| if looks_encrypted char -= next_key char += 26 if char < 65 else char += next_key char -= 26 if char > 90 end results += char.chr end return space_text(results, 5) end # counts words as 5 char blocks def word_count(text, len) words, strays = text.length.divmod(len) words += 1 if strays > 0 pad = "X" * (len - strays) return [words, pad] end def space_text(text, len) # adds a space every 5 letters. # not sure how efficient this is. return text.unpack(('A' + len.to_s) * word_count(text, len)[0]).join(' ') end def shift_card(name, suit, count) # find the card idx = @deck.find_card(name, suit) # remove it from the deck. card = @deck.cards.slice!(idx) # calculate new placement. idx += count # the slice above makes length 'look' zero-based idx -= @deck.cards.length if idx > @deck.cards.length # glue the deck together as cards before, card, cards after. @deck.cards = @deck.cards[0...idx] + [card] + @deck.cards[idx... / deck.cards.length] end def next_key shift_card('A', :Joker, 1) shift_card('B', :Joker, 2) # find the 2 jokers, and sort them for the cut. jokers = [@deck.find_card('A', :Joker), @deck.find_card('B', :Joker)].sort # increment the 2nd joker pos -- cut uses 'up to, but not including' jokers[1] += 1 # reverse works nicely for the triple cut. @deck.cards = @deck.cut_cards(jokers).reverse # get the value from the last card, and cut up to it. cuts = @deck.cut_cards([@deck.cards.last.rank, @deck.cards.length - 1]) @deck.cards = cuts[1] + cuts[0] + cuts[2] # read top card value, count down that many cards + 1 key = @deck.cards[@deck.cards[0].rank].rank # convert it to a letter, adjust if needed. key -= 26 if key > 26 # if key is still > 26, then it's a joker! return (key) unless key > 26 # try again if it's a joker! next_key end end test = Solitaire.new(ARGV[0]) puts test.process puts test.deck -- Bill Guindon (aka aGorilla)