James Gray wrote:
> There are two levels of quiz difficulty to choose from. Given the above 
> rules
> for the validity of tickets and books, then:
> 
>   1. Write a Ruby program which generates random individual tickets
>   or
>   2. Write a Ruby program which generates random complete books of 
> tickets

Yay, I finally got time for this, my first try to submit one. It might 
be a little tricky, but it does what it has to :)

_________________________________________________________________

#First of all, let's look at the books.
#Assuming a symetrical distribution of tens, we have that for all nine 
rows,
#tickets can have one of the following distributions of numbers in a 
columnn:
def make_poss
  [  [1, 1, 1, 2, 2, 2, 2, 2, 2],
    [1, 1, 1, 1, 2, 2, 2, 2, 3],
    [1, 1, 1, 1, 1, 2, 2, 3, 3],
    [1, 1, 1, 1, 1, 1, 3, 3, 3]  ]
end

#Don't forget to put the numbers in the bag!
def make_container
  initial = (0..8).to_a.collect{|n| ((10*n)..(10*(n+1) - 1)).to_a }
  initial[0].delete(0); initial[-1].push(90)
  initial
end

class Array
  #Tricked so it adds only the non-nil elements
  def sum
    inject(0) {|sum, n| n.nil? ? sum : sum + n }
  end

  def column(n)
    collect{|row| row[n]}
  end
end

class Book < Array

  attr_accessor :tickets, :distribution

  def initialize
    #Lets's the fun begin. As stated before, a ticket can have on of 4 
possible
    #distribution of tickets (and they are ALL the possibilities).
    #So, we start defining randomly the distributions of our tickets.
    #Furthermore, we scramble the possibilities, so any row can have any 
distribution provided.
    super(Array.new(6).collect{|i| (poss = 
make_poss)[rand(poss.size)].sort_by{rand} })

    #Now we have to distribute our matrix. Because of the above, each 
row sums 15, but we have to make
    #each column sum 10.
    make_distribution

    #Now we adjust the numbers on each ten.
    balance
    @tickets = []

    #And we're ready
    make_tickets
  end

  #This method iterates each column, and accomodate the numbers until 
the sum of each column is 10.
  def make_distribution
    for i in 0...9
      s = column(i).sum
      remaining = (10 - s).abs
      if s != 10
      #If the sum is different than 10, decrement one to the greater
      #and increment it to the possible in the row, or viceversa.
      #If that's not possible, go onto the next row, and repeat if 
necessary.
        remaining.times do
          index = 0
          until gain(index, i, (s < 10 ? 1 : -1))
            index += 1
            index = 0 if index == 5
          end
        end
      end
    end
  end

  def display
    @tickets.each {|ticket| ticket.display}
  end

  #Knowing the distribution of the tickets, they are done almost 
automatically.
  def make_tickets
    container = make_container
    each{ |row|  @tickets << Ticket.new(row, container) }
  end

  #Returns the index where the increment occured, or nil if cannot be 
done
  def gain(row, column, num = 1)
    item = self[row][column]

    #We know a priori that the numbers must be between 1 and 3
    return nil if (item == 3 && num == 1) or (item == 1 && num == -1)

    #iterate over the array, starting from the right of the column
    for i in (column + 1)...(self[row].size)

      #Find the first element that can accept a loss of -num (or a gain)
      if (1..3) === (self[row][i] - num)
        #if so, increment and decrement the numbers.
        self[row][column] += num
        self[row][i] -= num
        return i
      end
    end
    return nil
  end

  #Balances the ticket distribution so the first row has 9 numbers and
  #the last one 11, without affecting the sum.
  def balance
    for row in self
      if row[0] > 1 and row[-1] < 3
        row[0] -= 1
        row[-1] += 1
        break
      end
    end
  end

end


class Ticket < Array

  def initialize(distribution = (poss = make_poss)[rand(poss.size)], 
container = make_container)
    #When initializing, we first make the ticket 'vertical' so its 
easier to keep track of the
    #numbers in each row.
    super(9); collect! {|row| []}
    distribution.each_with_index do |distr, index|
      choose = container[index]
      distr.times do
        #Exhausting possibilities
        self[index] << choose.slice!(rand(choose.size))
      end
    end
    collect! { |row| row.sort! }
    make_format
  end

  def make_format
    #Iterate over the colums
    for i in 0...3
      #Do this until we have 5 elements per column.
      until (s = column(i).compact.length) == 5
        #If the number of elements in the column is more than 5,
        #move one randomly to the next place
        if s > 5
          x = rand(9)
          self[x].insert(i, nil) unless self[x].length == 3
        else
        #If the number is less than four (that can only happen in the 
second column),
        #remove one nil
          for row in self
            if row[1] == nil and row[2] != nil
              row[1], row[2] = row[2], nil
              break
            end
          end
        end
      end
    end
    #Now we just transpose the ticket.
    replace((0...3).collect{ |i| column(i) })
  end

  def display
    print(top = "+----" * 9 + "+\n")
    each do |row|
      row.each{ |number|  print("|" + " " * (3 - number.to_s.length) + 
number.to_s + " ") }
      print "|\n", top
    end
    puts
  end

end

puts "Type B for a complete book, or T for a single ticket.\nType 
anything else to exit"
while (option = gets.chomp) =~ /\A[BbTt]/
  if option =~ /\A[Bb]/
    #Now all we have to do is to create a new book...
    book = Book.new
    #and display it...
    puts "BINGO!\n"
    book.display
  else
    #Or we can make single tickets... for cheating... you know
    puts "Today's my lucky day"
    x = Ticket.new
    x.display
  end
  puts "Play again?"
end

-- 
Posted via http://www.ruby-forum.com/.