--Boundary-00ZUkGqSfYc67+su
Content-Type: Multipart/Mixed;
  boundaryoundary-00ZUkGqSfYc67+su"

--Boundary-00ZUkGqSfYc67+su
Content-Type: text/plain;
  charsettf-8"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

Here's my solution. I tried to tackle many of the suggested ideas:

 - Extensible interface and AI. Just create a new file for the interface/ai,
   give it the right filename, and implement the appropriate methods. Then
   call hangman.rb with the correct --interface or --ai option.

 - I've implemented a simple text interface and one based on Ncurses, just to
   try it out. My ncurses code is ugly, but it works ok. I almost tried out
   some animation & color a la the rain.rb ncurses example, but have no time.

 - Implemented a random AI and one that tries to match items in a dictionary,
   though it does not add new words to it. It greps through the dictionary
   file on each guess, but is still pretty quick with the 52848 word one I'm
   testing with.

Downsides:

  - Not much error checking. Inputing illegal positions and such is an easy
    crash.

  - Very little documentation.

hangman.rb is the executable, which creates an interface (subclass of
Interface::Core), and an AI (subclass of AI::Core), and passes them to a
Game object which controls the basic game flow. (Heh, 9 source files for
a quiz submission, a personal record.. :)

Usage: hangman.rb [--interface | -i INTERFACE]
                  [--interface-arg | -j INTERFACE_ARGUMENT]
                  [--ai | -a AI]
                  [--ai-arg | -b AI_ARGUMENT]

Simple UI / Dictionary AI full example:

  $ cat dict.txt
  CAR
  CAT
  DOCK
  DOOR
  RUBY
  ACORN
  HINGE
  ZEBRA
  $ ./hangman.rb -a dictionary
  Enter a phrase pattern: ---
  ---  |  Computer lives: 6
  I guess A. What position(s) is it in? 2
  -A-  |  Computer lives: 6
  I guess C. What position(s) is it in? 1
  CA-  |  Computer lives: 6
  I guess R. What position(s) is it in?
  CA-  |  Computer lives: 5
  I guess T. What position(s) is it in? 3
  CAT  |  Computer lives: 5

  Woot! I win!

Ncurses UI / Random AI example end screen:

───────────────────────────────────────────────────────────────────────────┐
│ Hangman |                                                        │
│---------+                                                        │
│                                                                  │
│ Phrase:          V--                                             │
│                                                                  │
│ Computer guess: M                                                │
│ Positions?                                                       │
│                                                                  │
│                                                                  │
│                  .          I lost!                              │
│ 0       +--------+-                                              │
│         |        |                                               │
│         _        |                                               │
│        | |       |                                               │
│         +        |                                               │
│        -|-       |                                               │
│       / | \      |                                               │
│         ^        |                                               │
│        / \       |                                               │
│       /   \      |                                               │
│                  |                                               │
│ ====================     │
│                                                                  │
└──────────────────────────────────────────────────────────────────────────────┘

-- 
Jesse Merriman
jessemerriman / warpmail.net
http://www.jessemerriman.com/

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  name
i_core.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename
i_core.rb"

module Hangman
  module AI
    def AI.looks_ok? possible_ai
      possible_ai.respond_to? :guess
    end

    class Core
      DefaultMaxLives  
      DefaultLetters  ABCDEFGHIJKLMNOPQRSTUVWXYZ'

      attr_reader :lives, :max_lives, :letter_pool

      def initialize(lives  efaultMaxLives, letters  efaultLetters)
        @lives  ives.to_i
        @max_lives  ives.to_i
        @letter_pool  etters.split(//)
      end

      def lose_life; @lives  0, @lives-1].max; end

      def dead?; @lives.zero?; end

      private

      def random_letter
        @letter_pool[rand(@letter_pool.size)]
      end
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  nameangman.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenameangman.rb"

#!/usr/bin/env ruby

require 'game'
require 'getoptlong'

class String
  # Taken & mildly modified from ActiveSupport.
  def camelize(first_letter_in_uppercase  rue)
    if first_letter_in_uppercase
      gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
    else
      self[0].chr + self[1..-1].camelize
    end
  end
end

if __FILE__ $0
  Opts  etoptLong.new(
    [ '--interface',     '-i', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--interface-arg', '-j', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--ai',            '-a', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--ai-arg',        '-b', GetoptLong::REQUIRED_ARGUMENT ] )

  # defaults
  interface  text'
  ai  random'
  iface_args, ai_args  ], []

  Opts.each do |opt, arg|
    case opt
      when '--interface';     interface  rg
      when '--interface-arg'; iface_args << arg
      when '--ai';            ai  rg
      when '--ai-arg';        ai_args << arg
    end
  end

  begin
    require "interface_#{interface}"
    require "ai_#{ai}"

    iface_class  angman::Interface.const_get(interface.camelize)
    ai_class     angman::AI.const_get(ai.camelize)

    iface  face_class.new(*iface_args)
    ai  i_class.new(*ai_args)

    raise 'Bad interface' unless Hangman::Interface.looks_ok?(iface)
    raise 'Bad AI' unless Hangman::AI.looks_ok?(ai)

    Hangman::Game.new(iface, ai).run
  rescue LoadError le
    missing  \-\- (.*)$/.match(le.message)[1]
    $stderr.puts "Can't find #{missing}"
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  namenterface_core.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenamenterface_core.rb"

module Hangman
  module Interface
    def Interface.looks_ok? possible_iface
      possible_iface.respond_to?(:phrase_pattern) and
        possible_iface.respond_to?(:suggest) and
        possible_iface.respond_to?(:display) and
        possible_iface.respond_to?(:finish)
    end

    class Core; end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  namenterface_n.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenamenterface_n.rb"

require 'interface_core'
require 'ncurses'

module Hangman
  module Interface
    # An Ncurses-based interface. Originally I called this Ncurses instead of N,
    # but then to refer to the originaly Ncurses class you'd need to do
    # Object::Ncurses.
    class N < Core
      def initialize
        setup_screen
        self
      end

      def phrase_pattern
        @screen.mvaddstr 4, 2, 'Phrase:'
        @screen.move *PhraseCoords
        read_line
      end

      def suggest letter
        @screen.mvaddstr 6, 2, 'Computer guess:'
        @screen.mvaddstr 6, 18, letter
        @screen.mvaddstr 7, 2, 'Positions?      '
        read_line.chomp.split(' ').map { |x| x.to_i - 1}
      end

      def display phrase, lives, max_lives
        clear_fields
        body_i  ody_index lives, max_lives
        draw Bodies[body_i], *BodyCoords

        #@screen.mvaddstr *PhraseCoords, phrase # Damn, this doesn't work?
        @screen.mvaddstr PhraseCoords.first, PhraseCoords.last, phrase
        @screen.mvaddstr LivesCoords.first, LivesCoords.last, lives.to_s

        @screen.refresh
      end

      def finish user_won
        @screen.move 10, 30
        if user_won
          @screen.addstr 'I lost!'
        else
          @screen.addstr 'I won!'
        end

        read_line
        Ncurses.endwin
      end

      private

      def setup_screen
        Ncurses.initscr
        Ncurses.cbreak
        Ncurses.noecho
        @screen  curses.stdscr
        Ncurses.keypad @screen, true

        @screen.border(*([0]*8))
        @screen.mvaddstr 1, 1, ' Hangman |'
        @screen.mvaddstr 2, 1, '---------+'

        draw Platform, *PlatformCoords

        @screen.refresh
      end

      def clear_fields
        @screen.mvaddstr 7, 18, ' ' * 20
        @screen.mvaddstr LivesCoords.first, LivesCoords.last, ' ' * 6
      end

      # Modified from the ncurses-ruby read_line.rb example. Still fugly.
      def read_line
        line  '
        pos  
        x, y  ], []
        Ncurses.getyx @screen, y, x
        x, y  .first, y.first
        max_len  screen.getmaxx - x - 1

        loop do
          @screen.mvaddstr y, x, line
          @screen.move y, x + pos
          char  screen.getch
          case char
            when Ncurses::KEY_LEFT
              pos  0, pos - 1].max
            when Ncurses::KEY_RIGHT
              pos  line.length, pos + 1].min
            when Ncurses::KEY_ENTER, ?\n, ?\r
              return line
            when Ncurses::KEY_BACKSPACE, ?\177
               line  ine[0...([0, pos - 1].max)] + line[pos..-1]
               pos  0, pos - 1].max
               @screen.mvaddstr(y, x + line.length, '   ')
            when ' '[0]..255 # remaining printables
              if (pos < max_len)
                line[pos, 0]  har.chr
                pos + 
              else
                Ncurses.beep
              end
            else
              Ncurses.beep
          end
        end
      end

      def body_index lives, max_lives
        Bodies.size + (lives * (1 - Bodies.size) / max_lives).round - 1
      end

      def draw item, y, x
        d  ambda { |line| @screen.mvaddstr(y, x, line); y +  }
        item.each_line { |line| d[line.chomp] }
      end

      PhraseCoords  4, 19]
      LivesCoords  11, 2]

      PlatformCoords  10, 2]
      Platform  <EOF
                 .
        +--------+-
        |        |
                 |
                 |
                 |
                 |
                 |
                 |
                 |
                 |
                 |
EOF

      BodyCoords  13, 8]
      Bodies  '', <<EOF1, <<EOF2, <<EOF3, <<EOF4, <<EOF5, <<EOF6]
  _
 | |
  +
EOF1
  _
 | |
  +
  |
  |
EOF2
  _
 | |
  +
 -|
/ |
EOF3
  _
 | |
  +
 -|-
/ | \\
EOF4
  _
 | |
  +
 -|-
/ | \\
  ^
 /
/
EOF5
  _
 | |
  +
 -|-
/ | \\
  ^
 / \\
/   \\
EOF6
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  namenterface_text.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenamenterface_text.rb"

require 'interface_core'

module Hangman
  module Interface
    class Text < Core
      def phrase_pattern
        print 'Enter a phrase pattern: '
        $stdout.flush
        $stdin.gets.chomp
      end

      def suggest letter
        print "I guess #{letter}. What position(s) is it in? "
        $stdout.flush
        $stdin.gets.chomp.split(' ').map { |x| x.to_i - 1}
      end

      def display phrase, lives, max_lives
        puts "#{phrase}  |  Computer lives: #{lives}"
      end

      def finish user_won
        puts
        if user_won
          puts "I'm out of lives, so you won! Congrats."
        else
          puts "Woot! I win!"
        end
      end
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  namehrase.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenamehrase.rb"

module Hangman
  module Phrase
    BlankChar  -'

    # Return a regular expression to match the given phrase (case insensitive).
    def Phrase.regexp phrase
      blank_esc  egexp.escape BlankChar
      /^#{Regexp.escape(phrase).gsub(blank_esc, '.')}$/i
    end

    # Return an array of all the indices in phrase that have a blank character.
    def Phrase.blank_indices phrase
      is  ]
      phrase.split(//).each_with_index do |letter, i|
        is << i if letter BlankChar
      end
      is
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  name
i_random.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename
i_random.rb"

require 'ai_core'

module Hangman
  module AI
    class Random < Core
      def initialize lives  efaultMaxLives, letters  efaultLetters
        super(lives, letters)
      end

      def guess phrase
        @letter_pool.delete random_letter
      end
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  nameame.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filenameame.rb"

require 'phrase'

module Hangman
  class Game
    def initialize interface, ai
      @interface, @ai  nterface, ai
      self
    end

    def run
      @phrase  interface.phrase_pattern
      @interface.display @phrase, @ai.lives, @ai.max_lives

      while not done?
        guess
        @interface.display @phrase, @ai.lives, @ai.max_lives
      end

      finish
    end

    private

    def guess
      letter  ai.guess @phrase
      pos  interface.suggest letter

      if pos.empty?
        @ai.lose_life
      else
        pos.each { |pos| @phrase[pos]  etter }
      end
    end
 
    def done?
      @ai.dead? or (not @phrase.include?(Phrase::BlankChar))
    end

    def finish
      @interface.finish @ai.dead?
    end
  end
end

--Boundary-00ZUkGqSfYc67+su
Content-Type: application/x-ruby;
  name
i_dictionary.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename
i_dictionary.rb"

require 'ai_core'
require 'phrase'

module Hangman
  module AI
    class Dictionary < Core
      def initialize lives  efaultMaxLives, letters  efaultLetters,
                     dict_file  dict.txt'
        super(lives, letters)
        raise "#{dict_file} does not exist!" if not File.exists? dict_file
        @dict_file  ict_file
      end

      def guess phrase
        reg  hrase.regexp phrase
        file  ile.new @dict_file
        possible  ile.grep(reg).map { |x| x.chomp.upcase }
        file.close
        letter  hoose_letter possible, phrase
        @letter_pool.delete letter
      end

      private

      # Choose a letter to try to fill in the blanks in phrase. words is an
      # Enumerable of possible words. Letters that occur frequently in them
      # will be preferred.
      def choose_letter words, phrase
        # First, build up a hash of the counts of all letters in the blank
        # locations of the words.
        blank_indices  hrase.blank_indices phrase
        letter_to_count  ash.new { |h, k| h[k]   }

        words.each do |word|
          blank_indices.each do |i|
            letter_to_count[word[i..i]] + 
          end
        end

        # Removed previously-chosen letters.
        letter_to_count.delete_if { |k, v| not @letter_pool.include? k }

        # Find a maximum based on the values (which are the counts).
        best  etter_to_count.max { |x, y| x.last <y.last }

        # If there is a maximum, use it, else fall back on a random pick.
        best.nil? ? random_letter : best.first.upcase
      end
    end
  end
end

--Boundary-00ZUkGqSfYc67+su--
--Boundary-00ZUkGqSfYc67+su--