# \[QUIZ\] Hangman (#130)

**URL:** https://rubytalk.org/t/quiz-hangman-130/38893
**Category:** ruby-talk
**Created:** [6 July 2007 14:18 UTC](https://rubytalk.org/t/quiz-hangman-130/38893 "2007-07-06T14:18:17Z")
**Posts on this page:** 13
**Page:** 1

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [6 July 2007 14:18 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/1 "2007-07-06T14:18:17Z")

</div>

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until  
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

[http://www.rubyquiz.com/](http://www.rubyquiz.com/)

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone  
on Ruby Talk follow the discussion. Please reply to the original quiz message,  
if you can.

> **···**
>
> -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
> 
> by Brian Candler
> 
> Most people are probably familiar with the game of Hangman. The first player  
> picks a word or phrase, and the second player has to guess it a letter at a  
> time. If they make six wrong guesses (i.e. the target word does not contain the  
> guessed letter), they lose. If they guess the entire word before then, they win.
> 
> This quiz is to make a Hangman guessing player in Ruby. Play should proceed as  
> follows:
> 
> &nbsp;&nbsp;1. The program requests a word or phrase pattern, e.g. "-------".  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;2. The program suggests a letter, or may guess the entire word or phrase.  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;3. The user indicates which letter positions, if any, match that letter.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If none match, a life is lost. If six (or configurable) lives are lost,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the program loses.
> 
> The specification is otherwise open-ended to allow you to focus on whatever part  
> of the problem interests you. For example:
> 
> &nbsp;&nbsp;\* You may choose the form of user interface (e.g. terminal, GUI toolkit,  
> &nbsp;&nbsp;&nbsp;&nbsp;web).  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;\* You can just show the number of wrong guesses made, or you can actually  
> &nbsp;&nbsp;&nbsp;&nbsp;draw the hangman.  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;\* You may concentrate on improving the play, for example by using a  
> &nbsp;&nbsp;&nbsp;&nbsp;dictionary to improve the guesses made at each stage. A suitable file  
> &nbsp;&nbsp;&nbsp;&nbsp;is /usr/share/dict/words on many Linux systems.  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;\* A dynamic solution could start with an empty dictionary, and guess the  
> &nbsp;&nbsp;&nbsp;&nbsp;answer by chance. If it fails, it would prompt the user for the word  
> &nbsp;&nbsp;&nbsp;&nbsp;or phrase they were thinking of. It would add new words or phrases to  
> &nbsp;&nbsp;&nbsp;&nbsp;its dictionary so as to become a better player over time.  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;\* You could investigate ways of precomputing a hangman decision tree,  
> &nbsp;&nbsp;&nbsp;&nbsp;optimizing it for the minimum number of wrong guesses along each branch.  
> &nbsp;&nbsp;&nbsp;&nbsp;The aim is to produce an unbeatable guesser for a given dictionary.  
> &nbsp;&nbsp;  
> &nbsp;&nbsp;\* You may wish to consider how best to decouple the UI from the guessing  
> &nbsp;&nbsp;&nbsp;&nbsp;logic, to enable different UI's to work with the same guessing engine,  
> &nbsp;&nbsp;&nbsp;&nbsp;or vice versa.

---

<div class="post-metadata">

### Author: ![Jesse\_Merriman](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@Jesse\_Merriman](https://rubytalk.org/u/Jesse_Merriman)
#### Post date: [8 July 2007 20:18 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/2 "2007-07-08T20:18:19Z")

</div>

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,  
&nbsp;&nbsp;&nbsp;give it the right filename, and implement the appropriate methods. Then  
&nbsp;&nbsp;&nbsp;call hangman.rb with the correct --interface or --ai option.

- I've implemented a simple text interface and one based on Ncurses, just to  
&nbsp;&nbsp;&nbsp;try it out. My ncurses code is ugly, but it works ok. I almost tried out  
&nbsp;&nbsp;&nbsp;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,  
&nbsp;&nbsp;&nbsp;though it does not add new words to it. It greps through the dictionary  
&nbsp;&nbsp;&nbsp;file on each guess, but is still pretty quick with the 52848 word one I'm  
&nbsp;&nbsp;&nbsp;testing with.

Downsides:

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

&nbsp;&nbsp;- 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]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[--interface-arg | -j INTERFACE\_ARGUMENT]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[--ai | -a AI]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[--ai-arg | -b AI\_ARGUMENT]

Simple UI / Dictionary AI full example:

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

&nbsp;&nbsp;Woot! I win!

Ncurses UI / Random AI example end screen:

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

[ai\_core.rb](https://rubytalk.org/uploads/short-url/4MfGp7HtiN8g0P25lHFWWoLlMpr.rb) (639 Bytes)

[hangman.rb](https://rubytalk.org/uploads/short-url/wCF25LKwAEwRpDcRHmreSkXtoSC.rb) (1.47 KB)

[interface\_core.rb](https://rubytalk.org/uploads/short-url/bzxekFXOjUcJDgSrxRrJC21M269.rb) (312 Bytes)

[interface\_n.rb](https://rubytalk.org/uploads/short-url/vuSxIRlWdMOshDQdQ67oZFSqO8g.rb) (3.83 KB)

[interface\_text.rb](https://rubytalk.org/uploads/short-url/xlmbq6xXUa9PFI4CWcWwsgcodLo.rb) (694 Bytes)

[phrase.rb](https://rubytalk.org/uploads/short-url/gdNJK5tlxcIMhRaVTOGjEPgZzxY.rb) (529 Bytes)

[ai\_random.rb](https://rubytalk.org/uploads/short-url/5jqhDea11QAMnzkN2krIXmcmzae.rb) (275 Bytes)

[game.rb](https://rubytalk.org/uploads/short-url/1qkigMAflVTyyL1VFWZDHIZIjoC.rb) (753 Bytes)

[ai\_dictionary.rb](https://rubytalk.org/uploads/short-url/gYEpdzFdTbHoh1PxD2U3nBgwBPp.rb) (1.58 KB)

> **···**
>
> --  
> Jesse Merriman  
> [jessemerriman@warpmail.net](mailto:jessemerriman@warpmail.net)  
> [http://www.jessemerriman.com/](http://www.jessemerriman.com/)

---

<div class="post-metadata">

### Author: ![Andreas\_Launila](https://avatars.discourse-cdn.com/v4/letter/a/dbc845/32.png) [@Andreas\_Launila](https://rubytalk.org/u/Andreas_Launila)
#### Post date: [9 July 2007 09:26 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/3 "2007-07-09T09:26:48Z")

</div>

Ruby Quiz wrote:

> This quiz is to make a Hangman guessing player in Ruby. Play should proceed as  
> follows:

I focused on building a program that makes good guesses.

== Algorithm overview

The guesser reads a dictionary and then builds a database (which is  
reused) with the following information about each word:  
\* size  
\* positions of each character  
\* number of occurrences of each character

The basic algorithm is as follows:  
\* Remove all words that do not have a matching length.  
\* While the game has not been solved:  
\*\* Pick the character included in the most words still remaining.  
\*\* If the character is not in the word: remove all words with the character.  
\*\* If the character is in the word: remove all words that do not contain  
the character at exactly the revealed positions.

=== Weaknesses

The algorithm is not optimal. The character that's included in the most  
words is not necessarily the character which will give the largest  
reduction in number of potential words since position is not considered.  
Consider a dictionary with the following:

bdc  
ebc  
fcb

b and c are tied for number of occurrences, but b would be the better  
choice. If we pick b we will in all cases be left with one potential  
word. If we pick c and the word is one of the first two we get two  
potential words.

My guess is that it's a good enough heuristic in most cases though.

Also note that this program is built on the assumption that the word is  
picked randomly from the dictionary. More refined solutions could weigh  
in the relative frequency of different words in normal English text.

== Speed

It takes about 40 minutes to create the database for a dictionary with  
4\*10^5 words, but it only has to be created once.

Computing all guesses for a word (i.e. from being given the length to  
having the correct word) takes about 30 to 40 seconds for a dictionary  
with 4\*10^5 words. That time includes about 10 seconds to reset the  
database from previous uses, another 10 seconds for pruning based on  
word length and the rest for the remaining search.

=== Possible improvements

Much of the initial sorting could be precomputed (e.g. split words into  
different table based on length and then only work against the table  
with the specified length) to cut down on the time needed reset and do  
the initial pruning. The first (and possibly some additional steps)  
could also be precomputed.

== Dependencies

Requires a mysql database and the mysql-gem. You need to enter your  
username, passwords and database name in HangmanGuesser#db\_connection below.

== The code

#!/usr/bin/env ruby  
# == Synopsis

> **···**
>
> #  
> # automated\_hangman: plays a game of hangman with the word of your  
> # choice  
> #  
> # == Usage  
> #  
> # automated\_hangman [OPTION] ... WORD  
> #  
> # -h, --help:  
> # show help  
> #  
> # -d, --dictionary [dictionary location]:  
> # sets up the database to use the specified dictionary (defaults to  
> # /usr/share/dict/words), can take some time  
> #  
> # WORD: The word that the program should try to guess.
> 
> require 'getoptlong'  
> require 'rdoc/usage'  
> require 'mysql'
> 
> # Describes a game of hangman.  
> class Hangman  
> &nbsp;&nbsp;LIVES = 6
> 
> &nbsp;&nbsp;# Creates a new game of hangman where word is the target word.  
> &nbsp;&nbsp;def initialize(word)  
> &nbsp;&nbsp;&nbsp;&nbsp;@guesses =   
> &nbsp;&nbsp;&nbsp;&nbsp;@word\_characters = word.chomp.downcase.split(//)  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Returns an array containing the incorrect guessed characters.  
> &nbsp;&nbsp;def incorrect\_guesses  
> &nbsp;&nbsp;&nbsp;&nbsp;@guesses - @word\_characters  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Guesses a specified character. Returns an array of indices (possibly  
> &nbsp;&nbsp;# empty) where the character was found.  
> &nbsp;&nbsp;def guess(char\_guess)  
> &nbsp;&nbsp;&nbsp;&nbsp;@guesses \<\< char\_guess  
> &nbsp;&nbsp;&nbsp;&nbsp;indices =   
> &nbsp;&nbsp;&nbsp;&nbsp;@word\_characters.each\_with\_index do |character, index|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;indices \<\< index if character == char\_guess  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;return indices  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Returns a string representation of the current progress.  
> &nbsp;&nbsp;def to\_s  
> &nbsp;&nbsp;&nbsp;&nbsp;hidden\_characters = @word\_characters - @guesses  
> &nbsp;&nbsp;&nbsp;&nbsp;return @word\_characters.join(' ') if hidden\_characters.empty?  
> &nbsp;&nbsp;&nbsp;&nbsp;@word\_characters.join(' ').gsub(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/[#{hidden\_characters.uniq.join}]/, '\_')  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Checks whether the player has won.  
> &nbsp;&nbsp;def won?  
> &nbsp;&nbsp;&nbsp;&nbsp;(@word\_characters - @guesses).empty?  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Checks whether the player has lost.  
> &nbsp;&nbsp;def lost?  
> &nbsp;&nbsp;&nbsp;&nbsp;incorrect\_guesses.size \> LIVES  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Gets the number of characters in the word.  
> &nbsp;&nbsp;def character\_count  
> &nbsp;&nbsp;&nbsp;&nbsp;@word\_characters.size  
> &nbsp;&nbsp;end  
> end
> 
> # The guessing machine which picks the guesses.  
> class HangmanGuesser  
> &nbsp;&nbsp;# The location of the default dictionary to use.  
> &nbsp;&nbsp;DICTIONARY\_FILE = '/usr/share/dict/words'  
> &nbsp;&nbsp;# An array of the characters that should be considered.  
> &nbsp;&nbsp;CHARACTERS = ('a'..'z').to\_a  
> &nbsp;&nbsp;# Set this to true to see how the search progresses.  
> &nbsp;&nbsp;VERBOSE = true  
> &nbsp;&nbsp;# The maximum word length accepted.  
> &nbsp;&nbsp;MAX\_WORD\_LENGTH = 50
> 
> &nbsp;&nbsp;# The dictionary given should be the location of a file containing one  
> &nbsp;&nbsp;# word per line. The characters should be an array of all characters  
> &nbsp;&nbsp;# that should be considered (i.e. no words with other characters are  
> &nbsp;&nbsp;# included).  
> &nbsp;&nbsp;def initialize(hangman\_game, characters = CHARACTERS)  
> &nbsp;&nbsp;&nbsp;&nbsp;@con = self.class.db\_connection  
> &nbsp;&nbsp;&nbsp;&nbsp;@characters = characters  
> &nbsp;&nbsp;&nbsp;&nbsp;@hangman\_game = hangman\_game
> 
> &nbsp;&nbsp;&nbsp;&nbsp;reset\_tables  
> &nbsp;&nbsp;&nbsp;&nbsp;prune\_by\_word\_length @hangman\_game.character\_count  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Returns the guesses that the guesser would make.  
> &nbsp;&nbsp;def guesses  
> &nbsp;&nbsp;&nbsp;&nbsp;@guesses =   
> &nbsp;&nbsp;&nbsp;&nbsp;log{ "There are #{word\_count} potential words left." }  
> &nbsp;&nbsp;&nbsp;&nbsp;while not @hangman\_game.won?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess = next\_guess  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;raise 'The word is not in the dictionary.' if guess.nil?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@guesses \<\< guess  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log{ "Guessing #{guess}" }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_information(guess, @hangman\_game.guess(guess))  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log\_state  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log{ "\n" }  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;return @guesses  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;class \<\< self  
> &nbsp;&nbsp;&nbsp;&nbsp;# Creates the database and populates it with the dictionary file  
> &nbsp;&nbsp;&nbsp;&nbsp;# located at the specified location. Only considers the specified  
> &nbsp;&nbsp;&nbsp;&nbsp;# characters (array).  
> &nbsp;&nbsp;&nbsp;&nbsp;def create\_database(dictionary = DICTIONARY\_FILE,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;characters = CHARACTERS)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con = db\_connection  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@characters = characters  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@tables = ['words'] + @characters +  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@characters.map{ |c| c + '\_occurrences'}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;create\_tables  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;populate\_tables File.open(dictionary)  
> &nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;# Connects to the database that should store the tables.  
> &nbsp;&nbsp;&nbsp;&nbsp;def db\_connection  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Replace \<username\> and \<password\> with the database username and  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# password.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Mysql.real\_connect("localhost", \<username\>, \<password\>, "hangman")  
> &nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;private
> 
> &nbsp;&nbsp;&nbsp;&nbsp;# Creates the tables used to store words.  
> &nbsp;&nbsp;&nbsp;&nbsp;def create\_tables  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Drop old tables.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@tables.each do |table|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query "DROP TABLE IF EXISTS `#{table}`"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Words table.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query \<\<-"end\_sql"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CREATE TABLE `words` (  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`word_id` mediumint(8) unsigned NOT NULL AUTO\_INCREMENT,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`word` varchar(#{MAX\_WORD\_LENGTH}) NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`length` tinyint(3) unsigned NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`removed` tinyint(1) unsigned NOT NULL DEFAULT '0',  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PRIMARY KEY (`word_id`),  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INDEX (`removed`),  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INDEX (`length`)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;) ENGINE=MyISAM  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end\_sql
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Tables for the number of occurrences of each character.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;character\_occurrences\_table\_template =\<\<-'end\_template'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CREATE TABLE `%s_occurrences` (  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`word_id` mediumint(8) unsigned NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`occurrences` tinyint(3) unsigned NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PRIMARY KEY (`occurrences`, `word_id`),  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INDEX (`word_id`)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;) ENGINE=MyISAM  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end\_template
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Tables for the positions of each character.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;character\_table\_template =\<\<-'end\_template'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CREATE TABLE `%s` (  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`word_id` mediumint(8) unsigned NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`position` tinyint(3) unsigned NOT NULL,  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PRIMARY KEY (`position`, `word_id`),  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;INDEX (`word_id`)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;) ENGINE=MyISAM  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end\_template
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@characters.each do |character|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query character\_occurrences\_table\_template % character  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query character\_table\_template % character  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;# Loads a dictionary into the database.  
> &nbsp;&nbsp;&nbsp;&nbsp;def populate\_tables(dictionary\_file)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Disable the keys so that we don't update the indices while  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# adding.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@tables.each do |table|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query("ALTER TABLE #{table} DISABLE KEYS")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Prepare statements.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_word = @con.prepare(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"INSERT INTO words (word, length) VALUES (?, ?)")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character = {}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character\_occurrences = {}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@characters.each do |character|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character[character] = @con.prepare(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"INSERT INTO #{character} (word\_id, position) VALUES (?, ?)")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character\_occurrences[character] = @con.prepare(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"INSERT INTO #{character}\_occurrences " +  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"(word\_id, occurrences) VALUES (?, ?)")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Populate the database.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;previous\_word = nil  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dictionary\_file.each\_line do |line|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Only consider words that only contain characters a-z. Make  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# sure we don't get duplicates.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word = line.chomp.downcase  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if word == previous\_word or word =~ /[^a-z]/ or  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word.size \> MAX\_WORD\_LENGTH
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Add the word, its character positions and number of  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# occurrences.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_word.execute(word, word.size)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word\_id = @con.insert\_id  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;characters = word.split(//)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;characters.each\_with\_index do |character, position|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character[character].execute(word\_id, position)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@characters.each do |character|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;occurrences = characters.select{ |c| c == character }.size  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;add\_character\_occurrences[character].execute(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word\_id, occurrences)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;previous\_word = word  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Generate the indices.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@tables.each do |table|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@con.query("ALTER TABLE #{table} ENABLE KEYS")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;private
> 
> &nbsp;&nbsp;# Logs the current state of the guessing process.  
> &nbsp;&nbsp;def log\_state  
> &nbsp;&nbsp;&nbsp;&nbsp;log do  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messages =   
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messages \<\< @hangman\_game.to\_s  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;count = word\_count  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messages \<\< "There are #{count} potential words left."  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if count \<= 10  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res = @con.query('SELECT word FROM words WHERE removed = 0')  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res.each{ |row| messages \<\< row[0] }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res.free  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messages.join("\n")  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Logs the string produced by the block (may not be executed at all).  
> &nbsp;&nbsp;def log(&block)  
> &nbsp;&nbsp;&nbsp;&nbsp;puts yield() if VERBOSE  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Gets the number of potential words left.  
> &nbsp;&nbsp;def word\_count  
> &nbsp;&nbsp;&nbsp;&nbsp;res = @con.query('SELECT COUNT(\*) FROM words WHERE removed = 0')  
> &nbsp;&nbsp;&nbsp;&nbsp;count = res.fetch\_row[0].to\_i  
> &nbsp;&nbsp;&nbsp;&nbsp;res.free  
> &nbsp;&nbsp;&nbsp;&nbsp;return count  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Computes the next character that should be guessed. The next guess  
> &nbsp;&nbsp;# is the character (that has not yet been tried) that occurrs in the  
> &nbsp;&nbsp;# most words remaining.  
> &nbsp;&nbsp;def next\_guess  
> &nbsp;&nbsp;&nbsp;&nbsp;next\_character = nil  
> &nbsp;&nbsp;&nbsp;&nbsp;max\_count = 0  
> &nbsp;&nbsp;&nbsp;&nbsp;(@characters - @guesses).each do |character|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res = @con.query(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"SELECT COUNT(DISTINCT word\_id) FROM #{character} " +  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"NATURAL JOIN words WHERE removed = 0")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;count = res.fetch\_row[0].to\_i  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;res.free  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if count \> max\_count  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next\_character = character  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;max\_count = count  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;return next\_character  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Adds the information about at what indices in the word the specified  
> &nbsp;&nbsp;# character can be found to the guesser.  
> &nbsp;&nbsp;def add\_information(character, indices)  
> &nbsp;&nbsp;&nbsp;&nbsp;if indices.empty?  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# The character isn't in the word.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sql =\<\<-"end\_sql"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;UPDATE words SET removed = 1 WHERE removed = 0 AND word\_id IN (  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SELECT word\_id FROM #{character}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end\_sql  
> &nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Remove all words where the character isn't at the specified  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# places.  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sql =\<\<-"end\_sql"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;UPDATE words NATURAL JOIN #{character}\_occurrences  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SET removed = 1  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WHERE removed = 0  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AND (occurrences != #{indices.size}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OR word\_id IN (  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SELECT word\_id FROM #{character}  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WHERE position NOT IN (#{indices.join(', ')})  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end\_sql  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;&nbsp;&nbsp;@con.query(sql)  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Resets the table to start a new round of guesses.  
> &nbsp;&nbsp;def reset\_tables  
> &nbsp;&nbsp;&nbsp;&nbsp;@con.query('UPDATE words SET removed = 0')  
> &nbsp;&nbsp;end
> 
> &nbsp;&nbsp;# Prunes all words that do not have the specified length.  
> &nbsp;&nbsp;def prune\_by\_word\_length(expected\_length)  
> &nbsp;&nbsp;&nbsp;&nbsp;@con.query(  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"UPDATE words SET removed = 1 WHERE length != #{expected\_length}")  
> &nbsp;&nbsp;end  
> end
> 
> opts = GetoptLong.new(  
> &nbsp;&nbsp;['--help', '-h', GetoptLong::NO\_ARGUMENT],  
> &nbsp;&nbsp;['--dictionary', '-d', GetoptLong::OPTIONAL\_ARGUMENT])  
> opts.each do |opt, arg|  
> &nbsp;&nbsp;case opt  
> &nbsp;&nbsp;&nbsp;&nbsp;when '--help'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDoc::usage  
> &nbsp;&nbsp;&nbsp;&nbsp;when '--dictionary'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if arg != ''  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;HangmanGuesser.create\_database(arg)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;HangmanGuesser.create\_database  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;end  
> end
> 
> if ARGV.size != 1  
> &nbsp;&nbsp;abort "Incorrect usage, see --help"  
> end
> 
> game = Hangman.new(ARGV[0])  
> guesses = HangmanGuesser.new(game).guesses  
> if game.won?  
> &nbsp;&nbsp;puts 'Successfully guessed the word.'  
> else game.lost?  
> &nbsp;&nbsp;puts 'Failed guessing the word.'  
> end  
> puts "Made the following guesses: #{guesses.join(', ')}"  
> puts "Expended a total of #{game.incorrect\_guesses.size} lives."
> 
> --  
> Andreas Launila

---

<div class="post-metadata">

### Author: ![Yossef\_Mendelssohn](https://avatars.discourse-cdn.com/v4/letter/y/f19dbf/32.png) [@Yossef\_Mendelssohn](https://rubytalk.org/u/Yossef_Mendelssohn)
#### Post date: [11 July 2007 18:34 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/4 "2007-07-11T18:34:58Z")

</div>

Maybe people aren't trying this quiz because they're busy. I hope  
it's not because they find it uninteresting. I banged out something  
fairly quickly during the weekend and was waiting to find some time to  
clean it up. It seems I won't get that, so I'm going to post what I  
have now.

It's a fairly simple algorithm that uses a combination of the  
"established" frequency order of letters in the English language, as  
seen on Linotype machines, and a dictionary (/usr/dict/share/words, of  
course) to provide the next guess. What I find interesting about this  
is how many common words it cannot get before exhausting its allotment  
of wrong guesses. For instance, it won't get "book". Sometimes I'm  
surprised with the guesses it gives. Maybe the dictionary I'm using  
is \*too\* large.

> **···**
>
> -------------------------------------------------------------------------------  
> #!/usr/bin/env ruby
> 
> ALLOWED\_WRONG\_GUESSES = 6
> 
> WORD\_LIST\_FILENAME = '/usr/share/dict/words'
> 
> WORD\_LIST = {}
> 
> File.open(WORD\_LIST\_FILENAME) do |f|  
> &nbsp;&nbsp;f.each { |word| WORD\_LIST[word.strip.downcase] = true }  
> end
> 
> FREQUENCY\_ORDER = %w{etaoin shrdlu cmfwyp vbgkqj xz}.collect { |elem|  
> elem.split('') }.flatten
> 
> GUESSES\_MADE = {}
> 
> puts 'Enter a word pattern'  
> old\_pattern = pattern = gets.chomp
> 
> loop do  
> &nbsp;&nbsp;if pattern.match(/^[a-zA-Z\-]+$/)  
> &nbsp;&nbsp;&nbsp;&nbsp;if pattern.match(/-/)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if GUESSES\_MADE.values.select { |val| val == false }.length \>  
> ALLOWED\_WRONG\_GUESSES  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts 'crap I lost'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;regex = Regexp.new("^#{pattern.downcase.gsub(/-/, '.')}$")  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;possible\_words = WORD\_LIST.keys.select { |word|  
> word.match(regex) }  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;possible\_letters = possible\_words.collect { |word|  
> word.split('') }.flatten.uniq  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess = ((FREQUENCY\_ORDER - GUESSES\_MADE.keys) &  
> possible\_letters).first  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts guess.upcase  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pattern = gets.chomp
> 
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;GUESSES\_MADE[guess] = (pattern != old\_pattern)  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "wrong guesses made: #{GUESSES\_MADE.values.select { |val|  
> val == false }.length}"  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;old\_pattern = pattern  
> &nbsp;&nbsp;&nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts 'Yay I won'  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit  
> &nbsp;&nbsp;&nbsp;&nbsp;end  
> &nbsp;&nbsp;else  
> &nbsp;&nbsp;&nbsp;&nbsp;puts 'This pattern makes no sense.'  
> &nbsp;&nbsp;&nbsp;&nbsp;exit  
> &nbsp;&nbsp;end  
> end  
> -------------------------------------------------------------------------------
> 
> -yossef

---

<div class="post-metadata">

### Author: ![James\_Koppel](https://avatars.discourse-cdn.com/v4/letter/j/0ea827/32.png) [@James\_Koppel](https://rubytalk.org/u/James_Koppel)
#### Post date: [11 July 2007 21:41 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/5 "2007-07-11T21:41:57Z")

</div>

Sorry it's a little late; I'm on vacation and have spent limited time on a computer with Ruby installed.  
&nbsp;&nbsp;&nbsp;  
&nbsp;&nbsp;My solution reads a dictionary stored in "words.txt." It uses this to construct an array of arrays containing the possible words for each word in the pattern, which is constantly updated according to feedback.  
&nbsp;&nbsp;&nbsp;  
&nbsp;&nbsp;I thought about making something based on making sure each word has a vowel and hardcoding in the general English language letter frequencies, but I realized that that would be compeltely unnecessary, as I could constantly calculate the true letter frequency for the possible letters. This is highly effective and surprisingly simple, although it could definitely benefit from some basic knowledge of English grammar and parts of speech.  
&nbsp;&nbsp;&nbsp;  
&nbsp;&nbsp;Unfortunately, this program is definitely not unbeatqable. Likewise, with only 6 lives, probably none of the others are as well. As expected, when facing the word "cwm," the program guesses AEOIUY ["A" is more frequent than "E" for three-letter words, apparently], and loses before it has a chance to guess correctly.  
&nbsp;&nbsp;&nbsp;  
&nbsp;&nbsp;$Words = (file=File.new("words.txt")).read.upcase.split(/\n/)  
file.close  
&nbsp;&nbsp;def hangman\_start  
puts "Please enter word pattern."  
word\_pattern = gets.chomp  
possible\_words = []  
word\_pattern.split.length.times do |t|  
&nbsp;&nbsp;possible\_words \<\< $Words.select{ |word|  
&nbsp;&nbsp;&nbsp;word\_pattern.split[t].length == word.length}  
end

hangman\_round word\_pattern, possible\_words  
end  
&nbsp;&nbsp;$avail\_letters= ("A".."Z").to\_a  
&nbsp;&nbsp;def hangman\_round(word\_pattern, possible\_words, lives=6)  
guess(word\_pattern, possible\_words)  
puts word\_pattern  
puts "Are there any #{$guess}s?\t\tComputer lives=#{lives}"  
if gets.chomp=="y"  
&nbsp;&nbsp;puts "Please indicate all positions with a #{$guess}"  
&nbsp;&nbsp;puts "(0-indexed, comma-delimited)"  
&nbsp;&nbsp;gets.chomp.split(/,/).each{|pstr| word\_pattern[pstr.to\_i] = $guess}  
&nbsp;&nbsp;possible\_words.each\_index do |i|  
&nbsp;&nbsp;&nbsp;possible\_words[i] = possible\_words[i].select{|word|  
&nbsp;&nbsp;&nbsp;&nbsp;word.gsub(/[^#{$guess}]/, '\_') ==  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word\_pattern.split[i].gsub(/[^#{$guess}]/, '\_')}  
&nbsp;&nbsp;end  
else  
&nbsp;&nbsp;lives -= 1  
&nbsp;&nbsp;possible\_words.each {|words| words.reject! {|word| word.index $guess}}  
end  
if word\_pattern !~ /\_/  
&nbsp;&nbsp;puts word\_pattern  
&nbsp;&nbsp;puts "I win"  
elsif lives \> 0  
&nbsp;&nbsp;hangman\_round(word\_pattern, possible\_words, lives)  
else  
&nbsp;&nbsp;puts "You win"  
end  
end  
&nbsp;&nbsp;#Guesses by frequency analysis. If a letter appears in a possible word, it's a vote for  
#that letter. If a word is possible more than once, that's multiple votes, but not  
#if the letter appears multiple times in a possible word (it's still one possibility)  
#It then removes that letter from $avail\_letters and stores the guess into $guess  
#for convenience  
def guess(word\_pattern, possible\_words)  
all\_words = possible\_words.flatten  
guess = $avail\_letters.sort\_by {|c|  
&nbsp;&nbsp;all\_words.select{|w|w.index c}.length}.last  
$avail\_letters -= [guess]  
$guess = guess  
end  
&nbsp;&nbsp;hangman\_start

> **···**
>
> ---------------------------------  
> Boardwalk for $500? In 2007? Ha!  
> Play Monopoly Here and Now (it's updated for today's economy) at Yahoo! Games.

---

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [11 July 2007 02:41 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/6 "2007-07-11T02:41:23Z")

</div>

Not many people solved this one, so I wanted to see if it was too tough. Didn't seem so:

#!/usr/bin/env ruby -wKU

puts "One moment..."  
puts  
words = File.open(ARGV.shift || "/usr/share/dict/words") do |dict|  
&nbsp;&nbsp;&nbsp;dict.inject(Hash.new) do |all, word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;all.update(word.delete("^A-Za-z").downcase =\> true)  
&nbsp;&nbsp;&nbsp;end.keys  
end

guesses = Array.new

loop do  
&nbsp;&nbsp;&nbsp;puts guesses.empty? ?  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Please enter a word pattern (\_ \_ \_ \_ for example):" :  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Please update your pattern according to my guess (\_ i \_ \_ for example):"  
&nbsp;&nbsp;&nbsp;pattern = $stdin.gets.to\_s.delete("^A-Za-z\_")  
&nbsp;&nbsp;&nbsp;if pattern.include? "\_"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (guesses - pattern.delete("\_").split("")).size \> 6  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I'm out of guesses. You win."  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guessed your word. Pretty smart, huh?"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit  
&nbsp;&nbsp;&nbsp;end

&nbsp;&nbsp;&nbsp;choices = words.grep(/\A#{[pattern.tr](http://pattern.tr)("\_", ".")}\Z/i)  
&nbsp;&nbsp;&nbsp;odds = Hash.new(0)  
&nbsp;&nbsp;&nbsp;choices.each do |word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word.split("").each do |l|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if guesses.include? l  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;odds[l] += word.count(l)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;guess = odds.max { |(\_, c1), (\_, c2)| c1 \<=\> c2 }.first rescue nil

&nbsp;&nbsp;&nbsp;guesses \<\< guess  
&nbsp;&nbsp;&nbsp;puts "I guess the letter '#{guess}'."  
&nbsp;&nbsp;&nbsp;puts  
end

\_\_END\_\_

James Edward Gray II

---

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [11 July 2007 18:47 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/7 "2007-07-11T18:47:03Z")

</div>

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  
&nbsp;&nbsp;&nbsp;WORDS = File.open(WORDS\_CASH\_FILE) { |file| Marshal.load(file) }  
else  
&nbsp;&nbsp;&nbsp;WORDS = File.open( ARGV.find { |arg| arg =~ /\A[^-]/ } ||  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"/usr/share/dict/words" ) do |dict|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dict.inject(Hash.new) do |all, word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;all.update(word.delete("^A-Za-z").downcase =\> true)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end.keys  
&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;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  
&nbsp;&nbsp;&nbsp;puts guesses.empty? ?  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Please enter a word pattern (\_ \_ \_ \_ for example):" :  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Please update your pattern according to my guess (\_ i \_ \_ for example):"  
&nbsp;&nbsp;&nbsp;$stdout.flush  
&nbsp;&nbsp;&nbsp;pattern = $stdin.gets.to\_s.delete("^A-Za-z\_")

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

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guesses \<\< guess  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guess the letter '#{guess}'."  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next  
&nbsp;&nbsp;&nbsp;end

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

\_\_END\_\_

Finally, this is my test script:

#!/usr/bin/env ruby -wKU

require "words"

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

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

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

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hangman.puts pattern  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;end  
end

\_\_END\_\_

James Edward Gray II

---

<div class="post-metadata">

### Author: ![Thomas\_Wieczorek](https://avatars.discourse-cdn.com/v4/letter/t/e47c2d/32.png) [@Thomas\_Wieczorek](https://rubytalk.org/u/Thomas_Wieczorek)
#### Post date: [11 July 2007 19:10 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/8 "2007-07-11T19:10:18Z")

</div>

> Not many people solved this one, so I wanted to see if it was too  
> tough. Didn't seem so:

I was busy until today.

Here's my solution and my first quiz solution since I learn Ruby. It  
wasn't that hard. I learned a lot about Ruby with it.

> ruby hangman.rb [-w=word] [-l=lifes]

If you don't pass a word it will choose a random word from the  
dictionary. Lifes is set to 6 if you don't provide it.

\<code\>  
#Ruby Quiz 130  
#solution by Thomas Wieczorek \<wieczo.yo@googlemail.com\>  
#11.07.2007  
require 'yaml'  
$debug = true

class Player

public

&nbsp;&nbsp;def initialize(word)  
&nbsp;&nbsp;&nbsp;&nbsp;@word = word  
&nbsp;&nbsp;&nbsp;&nbsp;@dictionary = YAML.load\_file(DICTIONARYFILE)  
&nbsp;&nbsp;&nbsp;&nbsp;@letters = ('a'..'z').to\_a  
&nbsp;&nbsp;&nbsp;&nbsp;@guessed =   
&nbsp;&nbsp;&nbsp;&nbsp;scan\_dictionary(word)  
&nbsp;&nbsp;end

&nbsp;&nbsp;def guess()  
&nbsp;&nbsp;&nbsp;&nbsp;return @dictionary[0] if @dictionary.length == 1  
&nbsp;&nbsp;&nbsp;&nbsp;while (true)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;letter = @probabilities.pop  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if @guessed.include?(letter[0])  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@guessed \<\< letter[0]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break  
&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;return letter[0]  
&nbsp;&nbsp;end

&nbsp;&nbsp;def word=(value)

&nbsp;&nbsp;&nbsp;&nbsp;if not value.include?(".") then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#lost  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#unknown word  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if not @dictionary.include?(value) then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@dictionary = load\_dictionary()  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@dictionary \<\< value  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;File.open("dictionary.yaml", "w") { |f| YAML.dump(@dictionary, f) }  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if @word.eql?(value) then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@word = value  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;scan\_dictionary(value)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;end

private

&nbsp;&nbsp;DICTIONARYFILE = "dictionary.yaml"

&nbsp;&nbsp;def scan\_dictionary(masked)  
&nbsp;&nbsp;&nbsp;&nbsp;@dictionary = @dictionary or load\_dictionary()  
&nbsp;&nbsp;&nbsp;&nbsp;@dictionary = @dictionary.grep(Regexp.new("^#{@word}$"))  
&nbsp;&nbsp;&nbsp;&nbsp;set\_probability()  
&nbsp;&nbsp;end

&nbsp;&nbsp;def set\_probability  
&nbsp;&nbsp;&nbsp;&nbsp;alphabet = ('a'..'z').to\_a  
&nbsp;&nbsp;&nbsp;&nbsp;@probabilities = {}  
&nbsp;&nbsp;&nbsp;&nbsp;alphabet.each { |l| @probabilities[l] = 0 }  
&nbsp;&nbsp;&nbsp;&nbsp;@dictionary.each do |word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word.each\_byte do |letter|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#p letter  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;l = letter.chr  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@probabilities[l] += 1 if alphabet.include?(l)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;end

&nbsp;&nbsp;&nbsp;&nbsp;@probabilities = @probabilities.sort {|a,b| a[1]\<=\>b[1]}  
&nbsp;&nbsp;end

&nbsp;&nbsp;def load\_dictionary()  
&nbsp;&nbsp;&nbsp;&nbsp;return YAML.load\_file(DICTIONARYFILE)  
&nbsp;&nbsp;end  
end #of Player

def random\_word  
&nbsp;&nbsp;words = YAML.load\_file("dictionary.yaml")  
&nbsp;&nbsp;return words[rand(words.length)]  
end

def check\_for\_letters(word, guess, masked\_word)  
&nbsp;&nbsp;if word.include?(guess) then  
&nbsp;&nbsp;&nbsp;&nbsp;#puts "#{guess} is in #{word}"  
&nbsp;&nbsp;&nbsp;&nbsp;word.length.times do |i|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if word[i].chr == guess then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;masked\_word[i] = guess  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;end

&nbsp;&nbsp;return masked\_word  
end

def play\_game(word = "", lifes = 6, give\_output = false)  
&nbsp;&nbsp;#user given word  
&nbsp;&nbsp;word = random\_word if word == ""  
&nbsp;&nbsp;masked\_word = word.gsub(/\w/, ".")  
&nbsp;&nbsp;guess = ""

&nbsp;&nbsp;player = Player.new(masked\_word)

&nbsp;&nbsp;while(lifes \> 0)  
&nbsp;&nbsp;&nbsp;&nbsp;#AI guesses a letter or word  
&nbsp;&nbsp;&nbsp;&nbsp;puts "AI is looking for \>#{masked\_word}\<" if give\_output  
&nbsp;&nbsp;&nbsp;&nbsp;guess = player.guess()  
&nbsp;&nbsp;&nbsp;&nbsp;new\_word = ""  
&nbsp;&nbsp;&nbsp;&nbsp;won = false  
&nbsp;&nbsp;&nbsp;&nbsp;puts "AI guessed '#{guess}'" if give\_output  
&nbsp;&nbsp;&nbsp;&nbsp;if guess.length == 1 then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;masked\_word = check\_for\_letters(word, guess, masked\_word)

&nbsp;&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if guess.length \> 1 then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break if guess == word  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;lifes -= 1  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#nil  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;end

&nbsp;&nbsp;&nbsp;&nbsp;#wrong guess  
&nbsp;&nbsp;&nbsp;&nbsp;if not masked\_word.include?(guess) then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;lifes -= 1  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "AI lost a life. #{lifes} lifes left."  
&nbsp;&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#found word  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if masked\_word == word then  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#found a letter  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;player.word = masked\_word  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;end

&nbsp;&nbsp;if lifes \> 0 then  
&nbsp;&nbsp;&nbsp;&nbsp;won = true  
&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;#give word to player to extend dictionary  
&nbsp;&nbsp;&nbsp;&nbsp;player.word = word  
&nbsp;&nbsp;&nbsp;&nbsp;won = false  
&nbsp;&nbsp;end

&nbsp;&nbsp;return won, word, lifes  
end #of play\_game

won = false  
word = ""  
lifes = 6  
if ARGV.length \> 0  
&nbsp;&nbsp;ARGV.each do |arg|  
&nbsp;&nbsp;&nbsp;&nbsp;option = arg.split("=")  
&nbsp;&nbsp;&nbsp;&nbsp;case option[0]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;when "-w"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word = option[1]  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;when "-l"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;lifes = option[1].to\_i  
&nbsp;&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;end  
end

won, word, lifes = play\_game(word, lifes, true)

if won then  
&nbsp;&nbsp;puts "AI won! It guessed \"#{word}\" with #{lifes} lifes left."  
else  
&nbsp;&nbsp;puts "Awww! Lost! AI couldn't guess \"#{word}\"."  
end

\</code\>

[hangman.rb](https://rubytalk.org/uploads/short-url/h9b7XEtz53GYtmkmWWrJ84Ya4qD.rb) (3.79 KB)

> **···**
>
> 2007/7/11, James Edward Gray II \<james@grayproductions.net\>:

---

<div class="post-metadata">

### Author: ![Thomas\_Wieczorek](https://avatars.discourse-cdn.com/v4/letter/t/e47c2d/32.png) [@Thomas\_Wieczorek](https://rubytalk.org/u/Thomas_Wieczorek)
#### Post date: [11 July 2007 19:12 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/9 "2007-07-11T19:12:53Z")

</div>

A few words to my solution:  
- It saves the dictionary in a yaml file. If the AI doesn't know a  
word, it will add it to the dictionary  
- it guesses letters which have the highest possibility

It is a bit rough. I am happy about every suggestion to improve it.

> **···**
>
> 2007/7/11, Thomas Wieczorek \<wieczo.yo@googlemail.com\>:
> 
> > I was busy until today.
> > 
> > Here's my solution and my first quiz solution since I learn Ruby. It  
> > wasn't that hard. I learned a lot about Ruby with it.
> > 
> > \>ruby hangman.rb [-w=word] [-l=lifes]  
> > If you don't pass a word it will choose a random word from the  
> > dictionary. Lifes is set to 6 if you don't provide it.

---

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [11 July 2007 19:27 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/10 "2007-07-11T19:27:59Z")

</div>

> A second version of my code.

One last tiny tweak.

> 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  
> &nbsp;&nbsp;WORDS = File.open(WORDS\_CASH\_FILE) { |file| Marshal.load(file) }  
> else  
> &nbsp;&nbsp;WORDS = File.open( ARGV.find { |arg| arg =~ /\A[^-]/ } ||  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"/usr/share/dict/words" ) do |dict|  
> &nbsp;&nbsp;&nbsp;&nbsp;dict.inject(Hash.new) do |all, word|  
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;all.update(word.delete("^A-Za-z").downcase =\> true)  
> &nbsp;&nbsp;&nbsp;&nbsp;end.keys

Changing that line to:

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end.keys.sort\_by { |w| [w.length, w] }

helps me see where I'm at in the tests.

> &nbsp;&nbsp;end  
> &nbsp;&nbsp;File.open(WORDS\_CASH\_FILE, "w") { |file| Marshal.dump(WORDS, file) }  
> end
> 
> \_\_END\_\_

James Edward Gray II

> **···**
>
> On Jul 11, 2007, at 1:47 PM, James Edward Gray II wrote:

---

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [11 July 2007 20:42 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/11 "2007-07-11T20:42:19Z")

</div>

Last tweak, I promise!

Here's a new version of the guesser script that fairs a bit better. I found the improvement by trying some changes and running my testing script:

#!/usr/bin/env ruby -wKU

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

def frequency(words)  
&nbsp;&nbsp;&nbsp;freq = Hash.new(0)  
&nbsp;&nbsp;&nbsp;words.each do |word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word.split("").each { |letter| freq[letter] += word.count(letter) }  
&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;freq  
end  
FREQ = frequency(WORDS).sort\_by { |\_, count| -count }.map { |letter, \_| letter }

choices = WORDS  
guesses = Array.new

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

&nbsp;&nbsp;&nbsp;if (guesses - pattern.delete("\_").split("")).size \> 5 and pattern.include? "\_"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I'm out of guesses. You win."  
&nbsp;&nbsp;&nbsp;elsif not pattern.include? "\_"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guessed your word. Pretty smart, huh?"  
&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;choices = choices.grep(/\A#{[pattern.tr](http://pattern.tr)("\_", ".")}\Z/i)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess = frequency(choices).  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reject { |letter, \_| guesses.include? letter }.  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sort\_by { |letter, count| [-count, FREQ.index(letter)] }.  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;first.first rescue nil

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guesses \<\< guess  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guess the letter '#{guess}'."  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next  
&nbsp;&nbsp;&nbsp;end

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

\_\_END\_\_

James Edward Gray II

---

<div class="post-metadata">

### Author: ![James\_Edward\_Gray\_II](https://avatars.discourse-cdn.com/v4/letter/j/ea5d25/32.png) [@James\_Edward\_Gray\_II](https://rubytalk.org/u/James_Edward_Gray_II)
#### Post date: [11 July 2007 21:01 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/12 "2007-07-11T21:01:41Z")

</div>

This time I really mean last. 😉

I found another change that improves the algorithm:

#!/usr/bin/env ruby -wKU

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

def frequency(words)  
&nbsp;&nbsp;&nbsp;freq = Hash.new(0)  
&nbsp;&nbsp;&nbsp;words.each do |word|  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;word.split("").each { |letter| freq[letter] += word.count(letter) }  
&nbsp;&nbsp;&nbsp;end  
&nbsp;&nbsp;&nbsp;freq  
end  
FREQ = frequency(WORDS).sort\_by { |\_, count| -count }.map { |letter, \_| letter }

choices = WORDS  
guesses = Array.new

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

&nbsp;&nbsp;&nbsp;bad\_guesses = guesses - pattern.delete("\_").split("")  
&nbsp;&nbsp;&nbsp;if bad\_guesses.size \> 5 and pattern.include? "\_"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I'm out of guesses. You win."  
&nbsp;&nbsp;&nbsp;elsif not pattern.include? "\_"  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guessed your word. Pretty smart, huh?"  
&nbsp;&nbsp;&nbsp;else  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;choices = choices.grep(  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bad\_guesses.empty? ?  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/\A#{pattern.tr("\_", ".")}\Z/i :  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/\A(?!.\*[#{bad\_guesses.join}])#{pattern.tr("\_", ".")}\Z/i  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess = frequency(choices).  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reject { |letter, \_| guesses.include? letter }.  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sort\_by { |letter, count| [-count, FREQ.index(letter)] }.  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;first.first rescue nil

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guesses \<\< guess  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts "I guess the letter '#{guess}'."  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;puts  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next  
&nbsp;&nbsp;&nbsp;end

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

\_\_END\_\_

James Edward Gray II

> **···**
>
> On Jul 11, 2007, at 3:42 PM, James Edward Gray II wrote:
> 
> > Last tweak, I promise!

---

<div class="post-metadata">

### Author: ![Thomas\_Wieczorek](https://avatars.discourse-cdn.com/v4/letter/t/e47c2d/32.png) [@Thomas\_Wieczorek](https://rubytalk.org/u/Thomas_Wieczorek)
#### Post date: [11 July 2007 21:24 UTC](https://rubytalk.org/t/quiz-hangman-130/38893/13 "2007-07-11T21:24:47Z")

</div>

Sorry, it is late

Please replace

> word = "Salawzander"

with  
word = ""

[hangman.rb](https://rubytalk.org/uploads/short-url/6zBggE1RlgeRTqB3vRaHXaQIh3M.rb) (4.45 KB)
