Wordle Solver

After starting this blog using Perl and remembering how much I love Perl, 
I felt like tinkering with it and so I created a Wordle helper.
It's a delight to see it work, sometimes surprisingly well:

from STEAM to THRUM in one crack.

It isn't perfect, because it's difficult to convey a clue like ERROR
on the command line.  Telling the script, say, that the word has 2 R's but not
3 and both number are in the wrong position.  Or that you know in a particular
position, it's not X,Y or Z characters.  I've skipped that logic for now.
But even with those edge cases, the script can filter all the choices down
to just a few that you can quickly see the answer.

The dictionary came from word file in Linux that I filtered down to just
the ones containing 5 letters.  I've noticed sometimes the NY Times Games
app contains words that are not in my dictionary and those will get missed.

Below is the source code.  To use it:


-i means show all words that include any of the letters.  Example:  -i xyz
   would return all words which contain X, Y, and Z
-e is the opposite.  It means exclude.  If a word contains anything 
   sent via -e, it is filtered out.
-k is for the known positions.  If you know the word ends with X, you
   would send -k ????x, where the question marks are placeholders
-u is for the unknown positions.  For example, if you know you have a X and
   it is not in position 2, you would send -u ?x???



#!/usr/bin/perl -w

use strict;
use Data::Dumper;
use Getopt::Std;
our ($opt_i, $opt_e, $opt_k, $opt_u);

# usage:  i = include characters
# usage:  e = exclude characters
# usage:  k = known positions. ? for unknown.  Example: words that start and end with 'r' r???r
getopt('ieku');

my @words;

load_dictionary();

if (defined $opt_i){
  @words = filter_by_includes ();
}

if (defined $opt_e){
  @words = filter_by_excludes ();
}

if (defined $opt_k){
  @words = filter_by_known_positions ();
}

if (defined $opt_u ){
  @words = filter_by_unknown_positions ();
}

sub load_dictionary {
  # five character words derived from: /usr/share/dict/words
  my $dict = "wordledict.txt";

  open (FILE, "<", $dict) or die "Cant open dictionary: $!";

  foreach my $word () {
    chomp $word;
    push @words, $word;
  }
  close FILE;
}

# Loops through all words in a dictionary and filters
# out those that do not match every`characther from opt_i
sub filter_by_includes {

  my @match_chars = split(//, $opt_i); 

  my @filtered;

  WORD: foreach my $word (@words){

    CHAR: foreach my $i (@match_chars) {
             next WORD unless $word =~ /[$i]/;
           }

           push @filtered, $word;
         }

  return @filtered;
}

# Loops through every word in the dictionary
# and filters out any word that contains a
# character defined in opt_e
sub filter_by_excludes {

  my $chars = $opt_e; 
  my @filtered;

  foreach my $word (@words){
    next if $word =~ /[$chars]/;
    push @filtered, $word;
  }

  return @filtered;
}

#------------------------------------------------------------
# foreach word in the dictionary, loop through the five known
# postionional characters entered by user.  If the char is a
# quesion mark, skip and go to next known positional character
# If the positional character matches the current word's character in 
# the same position, set flag to true and go to next postional
# character.  If any word chacter does not match the current
# positional character, skip that word(flag = false) and do not 
# add it to the matching filtered set of words 
sub filter_by_known_positions {

  # User entered example ?a??e
  my @known_positions = split (//, $opt_k); 

  unless (@known_positions == 5) {
    print "Usage: -k must be 5 characters, ?\'s for unknowns\n";
    exit;
  }

  my @filtered;

  WORD: foreach my $word (@words){

          my @word_chars = split (//, $word);
          
	  my $flag = "false";

	  CHAR: foreach my $i (0..4){

	    next CHAR if ($known_positions[$i] eq '?');
            
	    if ($known_positions[$i] eq $word_chars[$i]){
              $flag = "true";
	      next CHAR;
	    }
            $flag = "false";
            next WORD if ($flag eq "false");
          }

          push @filtered, $word;
        }
  return @filtered;
}





# Similar to filter by known, goes through each letter in word and -u arg,
# example ?a?e?, skips any word where a is in position 2
# and e is in position 4.  This increases accuracy but
# it's not perfect.  words with double letters don't do well
# for example
sub filter_by_unknown_positions {
  my @unknown_positions = split (//, $opt_u); 

  unless (@unknown_positions == 5) {
    print "Usage: -u must be 5 characters, ?\'s for unknowns\n";
    exit;
  }

  my @filtered;

  WORD: foreach my $word (@words){

          my @word_chars = split (//, $word);
          
	  CHAR: foreach my $i (0..4){

	    next CHAR if ($unknown_positions[$i] eq '?');
            
	    if ($unknown_positions[$i] eq $word_chars[$i]){
	      next WORD;
	    }
          }

          push @filtered, $word;
        }
  return @filtered;

}


# PRINT RESULTS
foreach (@words) { 
  print "$_\n";
}