From: James Koppel Date: 2007-06-29T09:06:56+09:00 Subject: [QUIZ][SOLUTION] LSRC Name Picker (#129) --0-700630361-1183075616=:70820 Content-Type: text/plain; charset=ascii Here is my solution to this week's Ruby quiz. It is my first program to use Ruby/Tk, and, after using it, will probably be my last. The picker's main procedure as follows: Upon startup, the program creates a pseudo terminal command line displaying a short "conversation" between the program and the user, with a picture of a star above it the whole time (ASCII-art, as I was loathe to include, other non-plain-text files in the solution). It then quickly scrolls through a listbox with a terminal-ish feel, saying that it is "scanning" for one worthy of receiving the prize.It then switches to a view with the scrambled name of the recipient at the bottom, and suspensefully has the characters fly up towards the top, placing themselves in the correct order. If known, the picker then displays the recipient's hometown and organization below the name. To implement the persistence - well, what could be sexier than having the program modify its own source code? If it detects the "-a" command-line argument, rather than running the picker, the program will instead append source code to itself that adds the attendee's name to an array (with -o and -t options for organization and hometown, respectively). Similarly, after being run, the program modifies itself to add the winner to the $previous_winners array. Originally, the add_code method implemented a "Schlemiel the Painter" algorithm, as all appended code had to come before Tk.mainloop; by using at_exit, I have managed to avoid that so that the program can just do a plain 'ol append. To seed the program with test data, I just grabbed the list of last names from http://www.census.gov/genealogy/names/dist.all.last, saved it as "names.dat" and used the following script (LSRCPicker.rb was the name of my program's file): File.open("names.dat") do |f| names = f.readlines.map{|l|l.split[0].capitalize} 300.times do name = names[rand(names.length)] + " " + name = names[rand(names.length)] org = nil if rand(2)==1 org = names[rand(names.length)] + " and " + names[rand(names.length)] + " Consulting, LLC" end home = names[rand(names.length)] + "ville, " + %w{MA CA NY MI FL MO AZ TX AR IL}.sort_by{rand}.first if org `ruby LSRCPicker.rb -a #{name} -o #{org} -t #{home}` else `ruby LSRCPicker.rb -a #{name} -t #{home}` end end end Here is my actual solution: ####Door prize picekr for LSRC. The program creates persistance by modifying its ####own source code. ####Command line usage: Without arguments, it runs the name picker ####With the -a argument, combined with the optional -o and -t arguments, ####it adds an attendee's name, organization, and hometown respectively to the program require 'tk' require 'enumerator' $attendees = [] $organization = {} $hometown = {} $previous_winners = [nil] ###Adds code_str to this source file right before Tk::mainloop is invoked def add_code(code_str) File.open($0, "a") do |f| f.puts code_str end end class Array def first_satisfies_i each_with_index {|el, i| return i if yield el} nil end def map_with_index mapped = [] each_with_index {|el, i| mapped[i] = yield(el,i)} mapped end end def get_arg(name) arg_proc = proc {|arg| arg =~ /^-/} if (i = ARGV.index name) ARGV[(i+1)..((j=ARGV[(i+1)..-1].first_satisfies_i(&arg_proc)) ? i+j : -1) ].join(' ') end end ###Code to check and process people being added from the command line unless ARGV==[] name, org, town = ['-a','-o','-t'].map{|n| get_arg n} add_code <<-EOC $attendees << #{name.inspect} $organization[#{name.inspect}] = #{org.inspect} $hometown[#{name.inspect}] = #{town.inspect} EOC exit end class TkVariable ###Makes updating values as easy as it should be def []=(*args) v = self.value v[*args[0...-1]] = args.last self.value = v end end ###Simulates a user typing text into a console ###The only way I could get it to wait for the typing to finish before ###continuing was to have it yield when done def type(tkvar, text, sleep_t=0.05) Thread.new(tkvar, text) do |tkvar, text| until text.empty? sleep sleep_t tkvar.value, text[0,1] = tkvar.value+text[0,1], "" end yield end end def char_fly(tkvars, char_pos, dest_pos) incr =(dest_pos.to_f-char_pos)/(tkvars.length-1) c = tkvars.last.value[char_pos, 1] return if " "==c Thread.new(tkvars, char_pos, incr, c) do |tkvars, char_in, incr, c| tkvars.reverse.each_cons(2) do |tkvar_prev, tkvar| tkvar_prev[char_in.round, 1] = ' ' char_in += incr tkvar[char_in.round, 1] = c sleep 0.1 end end end root = TkRoot.new { title 'Lone Star Ruby Conf Door Prize Picker' background '#000000'} TkMessage.new(root){ background '#000000' borderwidth 0 justify 'center' font 'courier' foreground '#C0C0C0' text < "#C0C0C0") $attendees[0..-10].each_with_index do |el, i| list.yview(i) list.selection_set(i) sleep 0.01 end list_frame.ungrid sleep 0.2 console_var.value += "\nI have found your worthy candidate." + " Watch and let the mystery reveal itself." sleep 3 console_frame.ungrid chosen = nil chosen = $attendees[rand($attendees.length) ] while $previous_winners.include? chosen scrambled = chosen.split(//).map_with_index{|el, i| [rand,el,i]}.sort.map{|arr|arr[1..2]} scrambled_str = scrambled.map{|el| el[0]}.join flying_textboxes.last.value = " "*(50-scrambled_str.length/2)+ scrambled_str flying_text_frame.grid sleep 1 until flying_textboxes.first.value.include? chosen ci=rand(scrambled_str.length) char_fly(flying_textboxes, (50-scrambled_str.length/2)+ci, (50-scrambled_str.length/2)+scrambled[ci][1]) sleep 0.1 end t = $hometown[chosen] o = $organization[chosen] flying_textboxes[1][50-t.length/2,t.length] = t if t flying_textboxes[2][50-o.length/2,o.length] = o if o add_code <<-EOC $previous_winners << #{chosen.inspect} EOC end end end end root.bind('FocusIn', &run_picker) at_exit {Tk.mainloop} ____________________________________________________________________________________ Get the free Yahoo! toolbar and rest assured with the added security of spyware protection. http://new.toolbar.yahoo.com/toolbar/features/norton/index.php --0-700630361-1183075616=:70820--