I wrote earlier:
# Version 1 is a function, and requires serial input.

Version 2 is a class, and handles random input, which means you can easily 
use it to power your new graphical interface.

Cheers,
Dave

class MadLib


 # You can enumerate the placeholders... not too useful, though.
 include Enumerable

 def initialize(string)

  # the original madlib string is kept
  @s = string

  # a placeholder for each user entry required
  # maps a tag name or number onto a [question, answer] array.
  @placeholders = {}

  # an index for each set of brackets in @s
  # the key into @placeholders for each respective set of brackets
  @index = []

  # the initial scan gets tag names and assigns numbers for tags with no 
name
  i = '0'
  @s.scan /\(\(([^:)]*):?(.*?)\)\)/ do |a, b|
   if @placeholders.has_key? a
    @index << a
   elsif b.size > 0
    @index << a
    @placeholders.update a => [b, nil]
   else
    @index << i
    @placeholders.update i => [a, nil]
    i = i.succ
   end
  end
 end

 def []=(index, answer)
  @placeholders[index][1] = answer
  @placeholders[index]
 end

 def [](index)
  @placeholders[index]
 end

 def outstanding
  @index.select {|i| @placeholders[i][1].nil? }.uniq
 end
 def outstanding_questions
  outstanding.map {|i| @placeholders[i][0] }
 end
 def each_outstanding
  outstanding.each do |i|
   yield @placeholders[i]
  end
 end

 def all
  @index.uniq
 end
 def all_questions
  all.map {|i| @placeholders[i][0] }
 end
 def each
  all.each do |i|
   yield @placeholders[i]
  end
 end

 def done?
  @placeholders.all? {|k, v| v[1] }
 end

 def collect!
  all.each do |i|
   self[i] = yield @placeholders[i]
  end
  self
 end

 def to_s
  if done?
   story
  else
   ""
  end
 end

 private
  def story
   i = 0
   @s.gsub /\(\(.*?\)\)/ do |token|
    i = i.succ
    @placeholders[@index[i - 1]][1]
   end
  end
end

if $0 == __FILE__
 m = MadLib.new(ARGF.read)
 m.collect! do |question, answer|
  answer or begin
   print "#{question}? "
   gets.chomp
  end
 end
 puts m
end