A second version of my code.  Minor changes mainly to support me  
testing it.  I tried different guessing algorithms, but nothing beat  
the original strategy with my dictionary.

First, this is a tiny words.rb lib used by both the guesser and test  
scripts:

#!/usr/bin/env ruby -wKU

WORDS_CASH_FILE = "words.cache"

if File.exist? WORDS_CASH_FILE
   WORDS = File.open(WORDS_CASH_FILE) { |file| Marshal.load(file) }
else
   WORDS = File.open( ARGV.find { |arg| arg =~ /\A[^-]/ } ||
                      "/usr/share/dict/words" ) do |dict|
     dict.inject(Hash.new) do |all, word|
       all.update(word.delete("^A-Za-z").downcase => true)
     end.keys
   end
   File.open(WORDS_CASH_FILE, "w") { |file| Marshal.dump(WORDS, file) }
end

__END__

Next, my guesser script which works the same as yesterday's version  
but is optimized here and there for testing:

#!/usr/bin/env ruby -wKU

puts "One moment..."
puts
require "words"

choices = WORDS
guesses = Array.new

loop do
   puts guesses.empty?                                       ?
        "Please enter a word pattern (_ _ _ _ for example):" :
        "Please update your pattern according to my guess (_ i _ _  
for example):"
   $stdout.flush
   pattern = $stdin.gets.to_s.delete("^A-Za-z_")

   if (guesses - pattern.delete("_").split("")).size > 5 and  
pattern.include? "_"
     puts "I'm out of guesses.  You win."
   elsif not pattern.include? "_"
     puts "I guessed your word.  Pretty smart, huh?"
   else
     choices = choices.grep(/\A#{pattern.tr("_", ".")}\Z/i)
     odds    = Hash.new(0)
     choices.each do |word|
       word.split("").each do |l|
         next if guesses.include? l
         odds[l] += word.count(l)
       end
     end
     guess = odds.max { |(_, c1), (_, c2)| c1 <=> c2 }.first rescue nil

     guesses << guess
     puts "I guess the letter '#{guess}'."
     puts
     next
   end

   puts
   if ARGV.include? "--loop"
     choices = WORDS
     guesses = Array.new
   else
     break
   end
end

__END__

Finally, this is my test script:

#!/usr/bin/env ruby -wKU

require "words"

results = Hash.new(0)
at_exit do
   results[:total] = results[:right] + results[:wrong]
   puts
   puts   "Words:     #{results[:total]}"
   puts   "Guessed:   #{results[:right]}"
   puts   "Missed:    #{results[:wrong]}"
   printf "Accuracy:  %.2f%%\n", results[:right] / results 
[:total].to_f * 100
   puts
end
trap("INT") { exit }

IO.popen( File.join(File.dirname(__FILE__), "hangman.rb --loop"),
           "r+" ) do |hangman|
   WORDS.each do |word|
     pattern = word.tr("a-z", "_")
     loop do
       input = String.new
       hangman.each do |line|
         input << line
         break if input =~ /^(?:I'm out|I guessed)|:\Z/
       end

       if input =~ /^I'm out/
         puts "It missed '#{word}'."
         results[:wrong] += 1
         break
       elsif input =~ /^I guessed/
         puts "It guessed '#{word}'."
         results[:right] += 1
         break
       elsif input =~ /^I guess the letter '(.)'/
         guess = $1
         word.split("").each_with_index do |letter, i|
           pattern[i, 1] = letter if letter == guess
         end
       end

       hangman.puts pattern
     end
   end
end

__END__

James Edward Gray II