Dear monks,

In the latest issue of The Perl Review, Kevin Jackson-Mead presented a Perl implementation of the curious word game Jotto. You may read more about this in The Perl Review, but here is a simplified explanation:


Jotto is a two-player game where each player attempts to guess the other’s secret five-letter word. A player scores a guess based on the number of letters it has in common with the secret word. This is a sample game:

trios 1
false 3
slang 2
swell 2
passe 3
abase 2
pleat 4
paler 5
pearl correct

The sample game has the property that each guess could possibly be the secret word based on the previous guesses and scores. For example, ‘swell’ scores 1 with trios, 3 with ‘false’, and 2 with ‘slang’. Based on the information from the first three guesses, ‘swell’ could have been the secret word.


I decided this game is cool and started hacking away (as they say, "He who reinvents the wheel, understands how the wheel works"). Here is my version that plays the game, it is commented, short and efficient (or so I hope) relatively to the original version. It allows both modes of play - computer guessing and human guessing:

Note: Thanks to everyone in this node, and especially bart for helping me implement an efficient and correct score function.
#!/usr/local/bin/perl -w use strict; srand(time); my $words_equal = 999; # Compute the "score" of two words - how many characters # they have in common. # # Returns $words_equal if the given words are equal, otherwise # returns the number of common characters # sub score { my ($word1, $word2) = @_; my %bag; my $score = 0; return $words_equal if ($word1 eq $word2); foreach (split '', $word1) { $bag{$_}++; } foreach (split '', $word2) { if ($bag{$_}) { $bag{$_}--; $score++; } } return $score; } # Returns a random element from a given array # sub random_arr_elem { my @arr = @{$_[0]}; return $arr[rand() * ($#arr + 1)]; } # Given the name of a dictionary file, picks a random word from it # sub pick_random_word_from_file { my $filename = $_[0]; open(FH, $filename) or die "Can't open $filename: $!\n"; my @words = <FH>; my $the_word = random_arr_elem(\@words); chomp $the_word; return $the_word; } # "Refines" an array of words # Given an array of words, a guess, and the score of that guess, # removes all array elements that don't get the same score with # the guess # sub refine_words_array { my @arr = @{$_[0]}; my $guess = $_[1]; my $score = $_[2]; my @res_arr; foreach (@arr) { push(@res_arr, $_) if ($score == score($guess, $_)); } return \@res_arr; } # Play a human guess game - the human tries to guess a word # # Asks for a dictionary file. Picks a random word from this # file, and lets the human guess # sub human_guess_game { print "Specify dictionary file: "; my $dict_file = <>; chomp $dict_file; my $word = pick_random_word_from_file($dict_file); print "\n** $word **\n"; while (1) { print "\nEnter a guess: "; my $guess = <>; chomp $guess; if (score($word, $guess) == $words_equal) { print "\nCongrats, you guessed it !\n\n"; last; } else { print score($word, $guess); } } } # Play a computer guess game - the computer tries to guess a work # # Asks for a dictionary file and starts guessing words. The user # must supply the score for each guess # sub computer_guess_game { print "If I guess correctly, please enter $words_equal as the scor +e\n"; print "Specify dictionary file: "; my $dict_file = <>; chomp $dict_file; # Get a list of words from the dictionary file # open(FH, $dict_file) or die "Can't open $dict_file: $!\n"; my @words = <FH>; chomp(@words); my $guess = random_arr_elem(\@words); while (1) { print "My guess is: $guess\n"; print "Score: "; my $score = <>; chomp $score; if ($score == $words_equal) { print "\nYay, I won !!\n"; last; } my $ref = refine_words_array(\@words, $guess, $score); @words = @$ref; if (scalar(@words) == 0) { print "\nNo suitable word in the given dictionary !!\n"; last; } print "Legal words left: " . scalar(@words) . "\n"; $guess = random_arr_elem(\@words); } }

Update: The code, if run as it is, does nothing. You can call human_guess_game or computer_guess_game if you wish to play in one of the ways. Thanks to artist for the note.

In his article Kevin talked about the algorithm he uses to guess words (in computer-guess) mode. This seems to be the same algorithm I'm using, you may call it the "naive algorithm" - after each score given by a human for a guess, refine_words_array removes all unsuitable words.
It converges to a solution pretty quickly this way, and is optimal in some senses - on each iteration it takes a random guess from a list of ALL LEGAL words (in the sense that no word in this list is illegal), that are also the ONLY LEGAL WORDS (in the sense that there are no other legal words).
Better algorithms may be devised, but they will be heuristic - using our knowledge of the game to try and make better guesses. For example, trying to apply the "most constraining" heuristic - one that is likely to eliminate most words in the next refinement.
Thinking about better algorithms for this game is probably my next step. It does sound interesting !

Kind regards

Replies are listed 'Best First'.
Re: The Jotto word game
by queue (Beadle) on Jan 09, 2003 at 19:53 UTC
    I'd actually bookmarked that node for use when I rewrote the Jotto programs (soon, I must find time to do this soon).

    As I find myself saying a lot, I really do know a lot more know than I did when I wrote that code. I'm glad it proved useful in motivating you to improve it, though.

    I did some messing around with a "most constraining" heuristic. I did a bunch of tests, and then, after many days of running tests, I discovered that I had implemented the algorithm incorrectly. By that time, I didn't feel like going back to fix it. Rewriting the code might get me to try it again, and I think I can probably cut down on the time it takes to play a bunch of test games.

    One thing that I did, though, was only "turn on" the heuristic once the number of possible words dropped below a certain threshhold value. It just takes way too much time to think about it, otherwise, and, really, it doesn't matter too much for the first couple of words, anyway.

    I'd be interested to hear about what you come up with for improving the guessing algorithm.


    Queue/Kevin

    BTW, anyone up for an e-mail game of Jotto? I'm currently playing X-Jotto (secret word can be anywhere from 3-8 letters, score as normal), which I find much more interesting than regular Jotto, but I'm happy to play any variety.
Re: The Jotto word game
by spurperl (Priest) on Jan 14, 2003 at 09:55 UTC
    I coded a test_strategy sub, that takes a guessing function (a strategy) and runs many games with it, on the easy dictionary. Here it is:
    sub test_strategy { my $func_ref = $_[0]; my $dict_file = "easy.txt"; open(FH, $dict_file) or die "Can't open $dict_file: $!\n"; my @words = <FH>; chomp(@words); my $n_runs = 200; my $verbose = 0; my $total_count = 0; for (my $i = 0; $i < $n_runs; ++$i) { my @dup_words = @words; my $count = 1; # Pick the word that should be guessed my $the_word = random_arr_elem(\@dup_words); print "The word: $the_word\n" if ($verbose); # Here the guessing starts # my $guess = &$func_ref(\@dup_words); while (1) { my $score = score($the_word, $guess); print "$guess $score\n" if ($verbose); last if ($score == $words_equal); my $ref = refine_words_array(\@dup_words, $guess, $score); @dup_words = @$ref; die if (scalar(@words) == 0); $guess = &$func_ref(\@dup_words); ++$count; } print "Guessed after $count tries\n" if ($verbose); $total_count += $count; print "$i " if ($i % 10 == 1); } my $av_count = $total_count / $n_runs; print "\nRun $n_runs games. Average guess count: $av_count\n"; }
    When run like this:
    test_strategy(\&random_arr_elem);
    On average, this strategy guesses a word after 7.8 tries.

    This will serve as a framework for me to test more advanced heuristics, and see how they match against the simple random one.