The Limber Lambda

Cheating at Word Games, Part 2

Posted in Fun by Eric Smith on February 7, 2012

In a previous post, I detailed how to cheat at a popular Zynga game which is a play on the age-old hangman, but with cute balloons (the style of which you can upgrade with purchasable coins).  I covered the part of the game where you need to choose a word that your opponent needs to guess.  I mentioned that, of course, it’s also possible to cheat on the flip-side, that is, in having to guess the word that your opponent has set as a challenge for you. At that stage, I was resorting to grepping through all the words, with a regular expression, using the “any” regex token for letters that hadn’t yet been guessed (“.”).  i.e., if the word so far ended in at and consisted of four letters, finding possibilities was a case of:

grep –i “..at” /tmp/scowl/final/*

Of course, this isn’t particularly smart, because it will return words that aren’t candidates by virtue of the fact that they contain letters that I may have already incorrectly guessed.  Assuming that I have already guessed e and r, neither of which were correct, then a better grepwould be:

grep –iE “[^er]{2}at” /tmp/scowl/final/*

Enter Ruby

I’m a great fan of knocking Unixy bits together in impressive ways, but the poor mans word-guesser above needs an upgrade, and since I’m learning Ruby at the moment, I figured it was time to put in the effort to Ruby-fy what is essentially a simple need. In Part 1 of the cheating story I defined a class called EnglishWords which serves to encapsulate all possible words and operations on those words that I may be interested in.  I need to re-use this class for my word-guesser, so it’s time to move it out into its own source file. Using EnglishWords from a script then becomes:

require File.join(File.dirname($0), 'english_words.rb')

$0 is the path to the script being executed and File.dirname gives us the directory part of the path.  This require assumes that english_words.rb resides in the same folder as the top-level script.  Fortunately, I have a bit of background in Perl, from-which Ruby borrows many things, so the idioms are familiar.  Ruby also borrows a few things from Unix shell script, e.g., $0, $1 etc., and dirname/basename.  How do I get the directory of the currently executing script in Bash?

echo $( dirname $0 )

This is one of the reasons why this language is growing on me so quickly—an example of the principle of least surprise in action (provided, of course, you’re a Unix-head and not from a purely Windows background). I took the grep above and Rubyfied it without much translation at all:

words = EnglishWords.new ARGV.shift
word_so_far = ARGV.shift
wrong_letters = ARGV.shift

regexp_so_far = wrong_letters.empty? ?
                Regexp.new("^#{word_so_far}$") :
                Regexp.new("^" + word_so_far.gsub('.', "[^#{wrong_letters}]") + "$")

Line 7 is the important part; just replace all instances of “.” with a regular expression character class of the letters already guessed, and we’re ready to apply the regular expression.  Lines 1 to 3 are another example of a Unix idiom, namely, shift which “knocks” the first item of the array being acted on off and returns it.  Interestingly, shift is an example that I’ve come across in Ruby of a deviation from another convention—that of naming “dangerous” methods (that is, methods that alter the state of what’s being acted on) with a bang at the end e.g., gsub!(..).  I guess in this case, assuming you’re familiar with the Unix shift, the bang is redundant since shift by definition changes state. Searching for (and outputting) candidate words becomes:

words.each do |w|
    puts w if regexp_so_far.match(w)
end

But wait … EnglishWords has no definition for each yet.  Here it is:

  1: def each()
  2:     @words.keys.each { |w| yield w }
  3: end

Notice how each above takes no parameters, but I’m passing it a block (blocks are the same as lambdas in C#-speak).  Another feature of Ruby—the implicit block parameter which can be passed to a method and invoked with yield. image Without too much effort, and a little bit of Ruby, we’re up-and-running in the word guessing department Smile.

Follow

Get every new post delivered to your Inbox.