From: Jesse Merriman Date: 2007-07-09T05:18:19+09:00 Subject: [QUIZ][SOLUTION] Hangman (#130) --Boundary-00=_LZUkGqSfYc67+su Content-Type: Multipart/Mixed; boundary="Boundary-00=_LZUkGqSfYc67+su" --Boundary-00=_LZUkGqSfYc67+su Content-Type: text/plain; charset="utf-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 tim= e. - Implemented a random AI and one that tries to match items in a dictionar= y, 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: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90 =E2=94=82 Hangman | = =E2=94=82 =E2=94=82---------+ = =E2=94=82 =E2=94=82 = =E2=94=82 =E2=94=82 Phrase: V-- = =E2=94=82 =E2=94=82 = =E2=94=82 =E2=94=82 Computer guess: M = =E2=94=82 =E2=94=82 Positions? = =E2=94=82 =E2=94=82 = =E2=94=82 =E2=94=82 = =E2=94=82 =E2=94=82 . I lost! = =E2=94=82 =E2=94=82 0 +--------+- = =E2=94=82 =E2=94=82 | | = =E2=94=82 =E2=94=82 _ | = =E2=94=82 =E2=94=82 | | | = =E2=94=82 =E2=94=82 + | = =E2=94=82 =E2=94=82 -|- | = =E2=94=82 =E2=94=82 / | \ | = =E2=94=82 =E2=94=82 ^ | = =E2=94=82 =E2=94=82 / \ | = =E2=94=82 =E2=94=82 / \ | = =E2=94=82 =E2=94=82 | = =E2=94=82 =E2=94=82 =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D = =E2=94=82 =E2=94=82 = =E2=94=82 =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2= =94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 =2D-=20 Jesse Merriman jessemerriman@warpmail.net http://www.jessemerriman.com/ --Boundary-00=_LZUkGqSfYc67+su Content-Type: application/x-ruby; name="ai_core.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="ai_core.rb" module Hangman module AI def AI.looks_ok? possible_ai possible_ai.respond_to? :guess end class Core DefaultMaxLives = 6 DefaultLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' attr_reader :lives, :max_lives, :letter_pool def initialize(lives = DefaultMaxLives, letters = DefaultLetters) @lives = lives.to_i @max_lives = lives.to_i @letter_pool = letters.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-00=_LZUkGqSfYc67+su Content-Type: application/x-ruby; name="hangman.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="hangman.rb" #!/usr/bin/env ruby require 'game' require 'getoptlong' class String # Taken & mildly modified from ActiveSupport. def camelize(first_letter_in_uppercase = true) 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 = GetoptLong.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 = arg when '--interface-arg'; iface_args << arg when '--ai'; ai = arg when '--ai-arg'; ai_args << arg end end begin require "interface_#{interface}" require "ai_#{ai}" iface_class = Hangman::Interface.const_get(interface.camelize) ai_class = Hangman::AI.const_get(ai.camelize) iface = iface_class.new(*iface_args) ai = ai_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-00=_LZUkGqSfYc67+su Content-Type: application/x-ruby; name="interface_core.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="interface_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-00=_LZUkGqSfYc67+su Content-Type: application/x-ruby; name="interface_n.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="interface_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 = body_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 = Ncurses.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 = 0 x, y = [], [] Ncurses.getyx @screen, y, x x, y = x.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 = line[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] = char.chr pos += 1 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 = lambda { |line| @screen.mvaddstr(y, x, line); y += 1 } item.each_line { |line| d[line.chomp] } end PhraseCoords = [4, 19] LivesCoords = [11, 2] PlatformCoords = [10, 2] Platform = < 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-00=_LZUkGqSfYc67+su-- --Boundary-00=_LZUkGqSfYc67+su--