My solution follows. I didn't do the extra credit for checking to see if the same UserID/IP was spamming the system. I also didn't do the extra credit for passing an argument for the difficulty of the question. Instead, I created a framework where you categorize types of captchas in an hierarchy, and you can ask for a specific type of captcha by using the desired subclass. For example, in my code below, I have: class Captcha::Zoology < Captcha ... end class Captcha::Math < Captcha class Basic < Math ... end class Algebra < Math ... end end This allows you to do: Captcha.create_question # a question from any framework, while Captcha::Zoology.create_question # only questions in this class Captcha::Math.create_question # any question in Math or its subclasses Captcha::Math::Basic.create_question # only Basic math questions I'm not wild about the fact that I re-create the Marshal file after every question creation or remove-retrieval, but it seemed the safest way. I have no idea how this will work (or fail) in a multi-threaded environment. I do like that I have the marshal file yank out questions after a certain time limit, and (optionally) after the answer has been checked. This keeps the marshal file quite tiny. The persistence for AnswerStore could easily be abstracted out to use a DB instead, if available. I'm not wild about some of the specific captcha questions I created; some of them seem to be annoyingly hard at times or (rarely) confusing. But this framework makes it pretty easy to modify the question generation, and add your own. I'm most proud of the String#variation method (except the name). Using regexp-like notation, it performs a sort of reverse-regexp, building a random string based on some criteria. (I'm not a golfer, but I also like how terse it turned out.) Without further explanation, the code: class Captcha # Invalidate an answer as soon as it has been checked for? REMOVE_ON_CHECK = true # Returns a hash with two values: # _question_:: A string with the question that the user should answer # _answer_id_:: A unique ID for this question that should be passed to # #check_answer or #get_answers def self.create_question question, answers = factories.random.call answer_id = AnswerStore.instance.store( answers ) return { :question => question, :answer_id => answer_id } end # _answer_id_:: The unique ID returned by #create_question # _answer_:: The user's string or numeric answer to the question def self.check_answer( info ) #TODO - implement userid persistence and checks answer_id = info[ :answer_id ] answer = info[ :answer ].to_s.downcase store = AnswerStore.instance valid_answers = if REMOVE_ON_CHECK store.remove( answer_id ) else store.retrieve( answer_id ) end valid_answers = valid_answers.map{ |a| a.to_s.downcase } valid_answers.include?( answer ) end def self.get_answers( id ) warn "Hey, that's cheating!" AnswerStore.instance.retrieve( id ) end # Add the block to my store of question factories def self.add_factory( &block ) ( @factories ||= [] ) << block end # Keep track of the classes that inherit from me def self.inherited( subklass ) ( @subclasses ||= [] ) << subklass end # All the question factories in myself and subclasses def self.factories @factories ||= [] @subclasses ||= [] @factories + @subclasses.map{ |sub| sub.factories }.flatten end class AnswerStore require 'singleton' include Singleton FILENAME = 'captcha_answers.marshal' MINUTES_TO_STORE = 10 def initialize if File.exists?( FILENAME ) @all_answers = File.open( FILENAME ){ |f| Marshal.load( f ) } else @all_answers = { :lastid=>0 } end # Purge any answers that are too old, both for security and # to keep a small log size @all_answers.delete_if { |id,answer| next if id == :lastid ( Time.now - answer.time ) > MINUTES_TO_STORE * 60 } warn "#{@all_answers.length} answers previously stored" if $DEBUG end # Serialize the answer(s), and return a unique ID for it def store( *answers ) idx = @all_answers[ :lastid ] += 1 @all_answers[ idx ] = Answer.new( *answers ) serialize idx end # Retrieve the correct answer(s) def retrieve( answer_id ) answers = @all_answers[ answer_id ] ( answers && answers.possibilities ) || [] end # Manually clear out a stored answer # # Returns the answer if it exists in the store, an empty array otherwise def remove( answer_id ) answers = retrieve( answer_id ) @all_answers.delete( answer_id ) serialize answers end private # Shove the current store state to disk def serialize File.open( FILENAME, 'wb' ){ |f| f << Marshal.dump ( @all_answers ) } end class Answer attr_reader :possibilities, :time def initialize( *possibilities ) @possibilities = possibilities.flatten @time = Time.now end end end end class String def variation( values={} ) out = self.dup while out.gsub!( /\(([^())?]+)\)(\?)?/ ){ ( $2 && ( rand > 0.5 ) ) ? '' : $1.split( '|' ).random }; end out.gsub!( /:(#{values.keys.join('|')})\b/ ){ values[$1.intern] } out.gsub!( /\s{2,}/, ' ' ) out end end class Array def random self[ rand( self.length ) ] end end class Integer ONES = %w[ zero one two three four five six seven eight nine ] TEENS = %w[ ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen ] TENS = %w[ zero ten twenty thirty forty fifty sixty seventy eighty ninety ] MEGAS = %w[ none thousand million billion ] # code by Glenn Parker; # see http://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-talk/135449 def to_english places = to_s.split(//).collect {|s| s.to_i}.reverse name = [] ((places.length + 2) / 3).times do |p| strings = Integer.trio(places[p * 3, 3]) name.push(MEGAS[p]) if strings.length > 0 and p > 0 name += strings end name.push(ONES[0]) unless name.length > 0 name.reverse.join(" ") end def to_digits self.to_s.split('').collect{ |digit| digit.to_i.to_english }.join ('-') end def to_rand_english rand < 0.5 ? to_english : to_digits end private # code by Glenn Parker; # see http://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-talk/135449 def Integer.trio(places) strings = [] if places[1] == 1 strings.push(TEENS[places[0]]) elsif places[1] and places[1] > 0 strings.push(places[0] == 0 ? TENS[places[1]] : "#{TENS[places[1]]}-#{ONES[places[0]]}") elsif places[0] > 0 strings.push(ONES[places[0]]) end if places[2] and places[2] > 0 strings.push("hundred", ONES[places[2]]) end strings end end # Specific captchas follow, showing off categorization class Captcha::Zoology < Captcha add_factory { q = "How many (wings|exhaust pipes|titanium teeth|TVs|wooden knobs) " q << "does a (standard|normal|regular) " q << "(giraffe|cat|bear|dog|frog|cow|elephant) have?" [ q.variation, '0', 'zero', 'none' ] } add_factory { q = "How many (wings|legs|eyes) does a (standard|normal|regular) " q << "(goose|bird|chicken|rooster|duck|swan) have?" [ q.variation, 2, 'two' ] } end class Captcha::Math < Captcha class Basic < Math add_factory { q = "(How (much|many)|What) is (the (value|result) of)? " q << ":num1 :op :num2?" num1 = rand( 90 ) + 9 num2 = rand( 30 ) + 2 plus = 'plus:added to:more than'.split(':') minus = 'minus:less:taking away'.split(':') times = 'times:multiplied by:x'.split(':') op = [plus,minus,times].flatten.random case true when plus.include?( op ) answer = num1 + num2 when minus.include?( op ) answer = num1 - num2 when times.include?( op ) answer = num1 * num2 end num1 = num1.to_rand_english num2 = num2.to_rand_english [ q.variation( :num1 => num1, :op => op, :num2 => num2 ), answer ] } add_factory { num1 = rand( 990000 ) + 1000 num2 = rand( 990000 ) + 1000 answer = num1 + num2 num1 = num1.to_rand_english num2 = num2.to_rand_english [ "Add #{num1} (and|to) #{num2}.".variation, answer ] } end class Algebra < Math add_factory { q = "Calculate :n1:x :op :n2:y, (for|if (I say )?) " q << ":x( is (set to )?|=):xV(,| and) :y( is (set to )?|=):yV." n1 = rand( 20 ) + 9 n2 = rand( 10 ) + 2 x = %w|a x z r q t|.random y = %w|c i y s m|.random xV = rand( 5 ) yV = rand( 6 ) plus = 'plus:added to:more than'.split(':') minus = 'minus:less:taking away'.split(':') times = 'times:multiplied by:x'.split(':') op = [plus,minus,times].flatten.random case true when plus.include?( op ) answer = n1*xV + n2*yV when minus.include?( op ) answer = n1*xV - n2*yV when times.include?( op ) answer = n1*xV * n2*yV end xV = xV.to_rand_english yV = yV.to_rand_english vars = { :n1=>n1,:op=>op,:n2=>n2,:x=>x,:y=>y,:xV=>xV,:yV=>yV } [ q.variation( vars ), answer ] } end end if __FILE__ == $0 if ARGV.empty? q = Captcha::Math.create_question puts "#{q[ :answer_id ]} : #{q[ :question ]}" else pieces = {} nextarg = nil ARGV.each{ |arg| case arg when /-i|--id/i then nextarg = :id when /-a|--answer/i then nextarg = :answer else pieces[ nextarg ] = arg end } pieces = { :answer_id => pieces[:id], :answer => pieces[:answer] } puts Captcha.check_answer( pieces ) end end