[SOLUTION] LSRC Name Picker (#129)

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 <<EOD

.+
+h:
-shh shhhs
........./hhhhy---:--::::. :shhhhyyyyyssyhhhhhhhys/ :shhhhyo+oyhhhhhy+.
/syyyhhhhyyy: ohhhhhyssoy:

/yhhhdhhhysyh:
.shhyo- :oyhhh
oho- -+ys
-:` -.
EOD
}.grid

content_frame = TkFrame.new(root){
background ‘#000000
grid{rowspan 60; colspan ‘100’; sticky “ew”}}

##Holds a pseudo-console
console_frame = TkFrame.new(content_frame) {
background ‘#000000
width 100
grid{rowspan 100; colspan ‘100’; sticky “ew”}}
console_var = TkVariable.new " "*100
console = TkLabel.new(console_frame){
background ‘#000000
foreground ‘#C0C0C0
justify
‘left’
font TkFont.new(‘Courier’){size 40}
grid{rowspan 100; colspan ‘100’; sticky “ew”}
height 7
}.textvariable(console_var)

##Holds the list that scrolls all the attendees names
list_frame = TkFrame.new(content_frame) {
background ‘#000000
grid{rowspan 100; colspan ‘100’; sticky “ew”}}
list = TkListbox.new(list_frame){
background ‘#000000
foreground ‘#C0C0C0
borderwidth 0
selectforeground ‘#000000
selectbackground ‘#C0C0C0
highlightthickness 0
width 75
font TkFont.new(‘Courier’){size 40}
listvariable [’ ']*10
height 10
}

#Displays the word “scanning” when the list of attendees scrolled by
#There is significant flicker involved with this method, as everything
is drawn as soon
#as I programmatically make the change.
#I was unable to remove the flicker (perhaps by suspending drawng
routines, but
#could not find the class responsible in the docs). I had signicant
trouble getting updating
#the value of #the listvariable to work; replacing the listvariable
worked but was very slow.
#This approach is the best I came up with.
scanning_display = TkListbox.new(list_frame){
foreground ‘#000000
background ‘#000000
highlightthickness 0
borderwidth 0
width 25
font TkFont.new(‘Courier’){size 40}
listvariable [’ ']*10
height 10}

flying_text_frame = TkFrame.new(content_frame) {
background ‘#000000
width 100}

flying_textboxes = ([nil]*10).map {
[(v=TkVariable.new(" "*100)),
TkEntry.new(flying_text_frame){
background ‘#000000
foreground ‘#C0C0C0
borderwidth 0

font TkFont.new(‘Courier’){size 40}
width 100
grid{rowspan 100; colspan ‘100’; sticky “ew”}
}.textvariable(v)]}.map{|arr|arr[0]}

TkGrid.grid(list, scanning_display)

##The main procedure of the program
run_picker = proc do
if $ran
return
else
$ran = true
end
$attendees = $attendees.sort_by {|n| n.split.reverse.join(’ ')}
type(console_var,“\n”) do
sleep 2
console_var.value += “Why do you wake me, mortal?\n>”
sleep 2; type(console_var, “I seek your wisdom and guidance.\n”) do
sleep 2; console_var.value += “What perplexes you?\n>”; sleep 2
type(console_var, "Tell me the one most worthy of
"+
“receiving this prize.\n”) do
sleep 2
console_var.value += "Very well; "
sleep 0.5
console_var.value += “the time is right for that decision.”
lvar = TkVariable.new $attendees
sleep 1
list.listvariable lvar
scanning_display.listvariable(TkVariable.new([“scanning”]))
scanning_display.itemconfigure(0, “background”=> “#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}