Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Anagrams2 - A Simple WPF Game Application

Rate me:
Please Sign up or sign in to vote.
4.97/5 (16 votes)
4 Nov 2012CPOL21 min read 70.6K   1.4K   40   33
My Anagrams game ported to WPF.

Introduction

Back in April 2008, I posted my first .Net article here on CodeProject:

       Anagrams - A Word Game in C#[^]

It's a simple word game that allowed the user to find anagrams within a scrambled word, awarding Scrabble-based points and extra time based on the word found. I reveled in my cleverocity (I know - that's not really a word, which is ironic when you consider the nature of the code this article describes), and congratulated myself for a job well-done. Since my current job requires coding within WPF, I got the urge to revisit Anagrams, and give it a much-needed facelift.

I must admit that while I tried to use certain patterns and WPF practices, I certainly didn't make heroic efforts to stay rigidly within their implied guidelines. Everything in programming is a trade-off, and rigid application of those kinds of rules has absolutely no place in software development. Given the nature of and size of this application, that idea is even more applicable.

Featured Technologies

The following technologies and code elements are present in this application:

  • Visual Studio 12
  • WPF
  • MVVM pattern
  • Dispatch Timers
  • Data Binding
  • Value Converters
  • Linq
  • Bacon

IMPORTANT NOTICE! *I THINK* you have to install .Net 4.5 before running this application, or change the set method on the GameDictionary.PercentRemaining property to public by removing the private accessor).

General Architecture

This version of the game contains a lot less code than the original. I'm not sure if that's due mostly to a combination of my increased knowledge of .Net (combined with its latest features), or due to the use MVVM/WPF. Whatever the cause, it's a good thing.

Words are loaded from a text file on disk (in the old version there was a text file for each word size), and each time user starts a game, a list of words is created for use by the interface. This list of words contains ALL of the words that can be found within the currently selected game word. This pattern is probably the biggest reason there is less code in the application. In the old version, all words were available to the game all the time, and I had to maintain several indicators, indexes, and other crap because I (falsely) assumed that .Net's performance was lacking, and I "coded around" that falsely perceived problem.

The Model

The model is driven by a single comma-delimited text file that contains all of the words that can be used in the game (currently, the word count is over 125,000). When the program runs, the data file is loaded, and points are calculated for each word. A word is represented by the AWord class:

C#
public class AWord
{
    public string Text   { get; private set; }
    public int    Points { get; private set; }
	public bool   Used   { get; protected set; }

    public AWord(string text)
    {
        this.Text   = text;
        this.Used = false;
        this.Points = Globals.CalcWordScore(this.Text);
    }

    public AWord(AWord word)
    {
        this.Text   = word.Text;
        this.Used   = word.Used;
        this.Points = word.Points;
    }
}

In order to have a lower impact on memory and increase the performance, I included the Points and Used properties in the model. The primary reason is that the words used in a given game are extracted from the model only when they're needed by the current game. I didn't want to have to recalculate word points for every game (although there are rarely any instances where you'd get more than 300-400 possible words in a given scramble). The Used property is required to keep track of words used as game words in the current app session, and the only way to maintain tracking is to put this property into the model. What this should tell you is that MVVM is only a guideline, and not a rigid set of requirements. Sometimes, you gotta bend the rules to make the code and data workable within the context of the application in which it is used.

And the list of words is represented by the MainDictionaryClass:

C#
//////////////////////////////////////////////////////////////////////////////////////
public class MainDictionary : List<AWord>
{
    public bool CanPlay          { get; set;         }
    public int  ShortestWordSize { get; private set; }
    public int  LongestWordSize  { get; private set; }

    //---------------------------------------------------------------------------------
    public MainDictionary()
    {
        this.ShortestWordSize = 65535;
        this.LongestWordSize  = 0;
        this.CanPlay          = LoadFile(Path.Combine
                                         (Path.GetDirectoryName
                                          (System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), 
                                         "Anagrams2Words.txt"));
    }

    //---------------------------------------------------------------------------------
    protected bool LoadFile(string fileName)
    {
        bool success = false;
        try
        {
            using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    string words;
                    while (!reader.EndOfStream)
                    {
                        words = reader.ReadLine();
                        if (words.Length > 0)
                        {
                            string[] wordsplit = words.Split(' ');
                            for (int i = 0; i < wordsplit.Length; i++)
                            {
                                string text      = wordsplit[i].ToUpper();
                                ShortestWordSize = Math.Min(ShortestWordSize, text.Length);
                                LongestWordSize  = Math.Max(LongestWordSize, text.Length);
                                AWord item       = new AWord(text);
                                Add(item);
                            }
                        }
                    }
                    Globals.LongestWord  = LongestWordSize;
                    Globals.ShortestWord = ShortestWordSize;
                    success              = this.Count > 0;
                }
            }
        }
        catch (Exception e)
        {
            if (e != null) { }
        }
        return success;
    }

    //---------------------------------------------------------------------------------
    public List<AWord> GetWordsByLength(int length)
    {
        var list = (from item in this 
                    where item.Text.Length == length
                    select item).ToList<AWord>();
        return list;
    }
}

As you can see, there's really not much going on here. The user can't change the list of words from within the program, so there's no reason to be able to save them back to the hard drive. Other than loading the words and calculating their point values, what else is there to do/say?

The View Model

The view model is where most of the work is done during game play. To keep the article size to a reasonable minimum, I didn't include comments that actually exist in the code. Generally speaking, I uses the standard WPF objects to allow the interface to reflect changes in the view model, namely, INotifyPropertyChanged and ObservableCollection. Given the amount of documentation available on the internet for these objects, and the fact that I'm not doing anything beyond normal usage, this is the only thing I'm going to say about these objects.

The DisplayWord Class

This class represents a word in the current game, and is created for each word used in the current game. It is derived from AWord. The properties are as follows:

  • string Foreground - This is the foreground color to be used when displaying the word in the ListBox on the main window.
  • string Scramble - This is the scrambled version of the word. This string will be empty unless this is the current game word.
  • bool Found - This property indicates whether or not the user has found this word during game play. When this property is true, the color of the word changes to either blue (a normal found word), or red (if this word is the original word).
  • bool IsOriginalWord - This property indicates that this word is the current game word.

The only code of any real interest in this class is the ScrambleIt method. It's responsible (as you might guess) for scrambling the word.

C#
//--------------------------------------------------------------------------------
public void ScrambleIt()
{
    StringBuilder scramble = new StringBuilder();
    do 
    {
        string temp = this.Text;
        do 
        {
            if (temp.Length > 1)
            {
                int index = (temp.Length > 1) ? Globals.RandomNumber(0, temp.Length-1) : 0;
                scramble.Append(temp[index]);
                temp = temp.Remove(index, 1);
            }
            else
            {
                scramble.Append(temp);
                temp = "";
            }
        } while (!string.IsNullOrEmpty(temp));
    } while (scramble.ToString() == this.Text);
    this.Scramble       = scramble.ToString();
    this.IsOriginalWord = true;
}

Since there's a remote possibility that the resulting scrambled word could end up being the original word, the word is re-scrambled until it is not the same as the original word.

The GameStatistics Class

This class maintains statistics relevant to the current game-in-progress, such as how many words of a certain length have been found, how many points have been earned and similar data. there's a little math happening here, but nothing worth further note. I was tempted to include my current warp anti-gravity calculations here, but I didn't want to confuse anyone, or upset those who think it's not possible).

The Settings Class

This class represents the settings that the user can change in the Setup window, and is a view model on those properties. It contains nothing but set/get and a Save method. Nothing fancy, and certainly not worth discussing any further.

The WordCounts Class

This class represents a list of WordCounItem objects, and is used within (and exposed from) the GameStatistics class. A WordCountItem contains two properties:

  • LetterCount - The size of the words being tracked by this item
  • WordCount - How many words of this size have been found in the current game

The GameDictionary Class

This class manages game play and is created for each game, which relieves us of having to reset properties in words and the statistics between games. To start things off, we need to determine how many letters the game word will have. This is determined by the game settings, and the default is that a random number of letters will be used

C#
//--------------------------------------------------------------------------------
private int SetLetterCount()
{
	int letterCount = 0;
	int shortest = Math.Max(Globals.MainDictionary.ShortestWordSize, 6);
	int longest  = Globals.MainDictionary.LongestWordSize;
	switch (this.Settings.LetterPoolMode)
	{
		case LetterPoolMode.Random : 
			letterCount = Globals.RandomNumber(shortest, longest);
			break;

		case LetterPoolMode.Static :
			letterCount = Settings.LetterPoolCount;
			break;
	}
	return letterCount;
}

Now that we have a word size, we can choose a word at random from our main dictionary.

C#
//--------------------------------------------------------------------------------
private AWord SelectNewWord(int count)
{
    AWord selectedWord = null;
    List<AWord> words = Globals.MainDictionary.GetWordsByLength(count);
    if (this.Settings.TrackUsedWords)
    {
        if (words.Count == 0)
        {
            words.Clear();
            if (this.Settings.LetterPoolMode == LetterPoolMode.Random)
            {
                count = SetLetterCount();
                selectedWord = SelectNewWord(count);
            }
            else
            {
                words = Globals.MainDictionary.GetWordsByLength(count);
                selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
            }
        }
        else // otherwise we have words to pick from that haven't yet been used
        {
            selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
        }
    }
    else // we're not tracking used words, so just pick one
    {
        selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
    }
    if (selectedWord != null)
    {
        selectedWord.Used = true;
    }
    return selectedWord;
}

Next, we need to find all words that can be derived from the game word.

C#
//--------------------------------------------------------------------------------
public void FindPossibleWords(AWord selectedWord)
{
    this.Clear();
    if (selectedWord != null)
    {
        var possibleWords = (from item in Globals.MainDictionary
                             where Globals.Contains(selectedWord.Text, item.Text)
                             select item).ToList<AWord>();
        foreach(AWord word in possibleWords)
        {
            DisplayWord displayWord = new DisplayWord(word, (word.Text == selectedWord.Text));
            if (displayWord.IsOriginalWord)
            {
                this.GameWord = displayWord;
            }
            this.Add(displayWord);
            Debug.WriteLine("{0} words", this.Count);
        }
    }
}

Finally, we start the game.

C#
//--------------------------------------------------------------------------------
public void ResetGame()
{
    this.WordCount = this.Count;
    this.Statistics.Reset();
    InitTimer();
    StartTimer();
    this.IsPlaying = true;
}

Once game play has started, this object handles the housekeeping when a word is submitted, or when the game is stopped. When a word is submitted, validity must be established, and points awarded (or deducted).

C#
//--------------------------------------------------------------------------------
public bool ValidAndScoreWord(string text)
{
    text       = text.ToUpper();
    int points = 0;
    bool valid = (!string.IsNullOrEmpty(text));
    DisplayWord foundWord = null;
    if (valid)
    {
        foundWord = (from item in this 
        where (item.Text == text && !item.Found) 
        select item).FirstOrDefault();
        valid = (foundWord != null);
    }
    if (valid)
    {
        foundWord.Found = true;
        foundWord.SetFoundColor();
        points += foundWord.Points;
        // the player can bonus points if he specifies the original word
        if (foundWord.IsOriginalWord)
        {
            // bonus points
            points += ORIGINAL_WORD_BONUS;
        }
        // OR he can get bonus points for specifying a word using all of the 
        // letters that isn't the original word.
        else
        {
            if (foundWord.Text.Length == GameWord.Text.Length)
            {
                // bonus points
                points += ALL_LETTERS_BONUS;
            }
        }
        this.Statistics.Update(foundWord.Text.Length, points);
        // determine bonus time
        if (Settings.TimerMode != TimerMode.NoTimer)
        {
            int wordRemainder;
            Math.DivRem(this.Statistics.WordCount.WordsFound, this.Settings.BonusWords, out wordRemainder);
            if (wordRemainder == 0)
            {
                this.SecondsRemaining += this.Settings.BonusTime;
            }
            if (foundWord.IsOriginalWord)
            {
                this.SecondsRemaining += 60;
            }
            else 
            {
                if (foundWord.Text.Length == GameWord.Text.Length)
                {
                    // bonus points
                    this.SecondsRemaining += 30;
                }
            }
            this.SecondsAtStart = this.SecondsRemaining;
        }
    }
    else
    {
        // deduct a point because the word was invalid (or already used)
        points--;
        this.Statistics.Update(0, points);
    }
    if (this.IsWinner)
    {
        StopTimer();
    }
    return (foundWord != null);
}

If the timer is used (configurable in the Setup form), the following method handles the ticks:

C#
//--------------------------------------------------------------------------------
public void StartTimer()
{
    if (this.Settings.TimerMode != TimerMode.NoTimer)
    {
        m_timer.Tick += new EventHandler 
        (
            delegate(object s, EventArgs a) 
            { 
                this.SecondsRemaining--;
                this.PercentRemaining = ((double)this.SecondsRemaining / 
                                          Math.Max((double)this.SecondsAtStart, 1)) * 100d;
                if (Settings.PlayTickSound)
                {
                    m_soundPlayer.Play();
                }
                if (this.SecondsRemaining == 0)
                {
                    StopTimer();
                }
            }
        );
        m_timer.Start();
    }
    this.IsPlaying = true;
}

All other methods are for starting, stopping, and solving the game

The Views

As with most WPF apps, the most interesting stuff (which also happens to be the stuff that's the biggest pain in the ass for people like me that haven't drank the WPF kool-aid) happens in the XAML. The code-behind is minimal (as it probably should be), with the main window having just a little under 120 lines of code in it (excluding comments).

The Main Window

Image 1

This is where the game is actually played. There were several relatively challenging aspects of this form.

The ProgressBar

The ProgressBar is used to show time remaining in the form of both a text display of minutes/seconds remaining, and a graphic representation of the percentage of time remaining. Since the ProgressBar already showed the graphical stuff, my task was to add the text display, which is illustrated below:

<Border BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
    <Label x:Name="PART_TextDisplay"
           Content="{Binding Path=SecondsRemaining, Converter={StaticResource SecondsRemaining}}"
           FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>

In order to display the time remaining, I created the TimeRemainingConverter converter. The ProgressBar.Value property is bound to the GameDictionary.SecondsRemaining property.

The Game Word

The game word is comprised of two controls overlaying each other. If a game is underway, the game word is displayed. Otherwise, the Game Over control is displayed. I did this to give a more visually alarming indication that the game had expired (for whatever reason.

<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,5" x:Name="textboxGameWord"
         CharacterCasing="Upper" Text="" IsReadOnly="true" Focusable="False"
         removed="Silver" FontWeight="Bold" BorderBrush="Black" />
<Border Grid.Row="1" Grid.Column="1" Focusable="False" removed="Red" BorderBrush="Black"
        BorderThickness="1" Margin="0,0,0,5"
        Visibility="{Binding Path=IsPlaying, Converter={StaticResource isPlayingConverter}}" >
    <TextBlock Focusable="False" removed="Red" Foreground="Yellow" FontWeight="Bold"
               FontStyle="Italic" Text="GAME OVER!!" VerticalAlignment="Center"
               HorizontalAlignment="Center"/>
</Border>

The most interesting part of that is the use of the IsPlayingVisibilityConverter.

The User Word

The user word control actually helps the filter mechanism work. As the user types, the list of possible words that have been found are chcked to see if the word starts with the text that's been typed so far. This is done to help the user to avoid submitting duplicate words (which, if done, causes a one point deduction for the game. The code for this is handled in code-behind via the TextChanged event which then causes the view model to set the SatisfiesFilter property. This is one of those places that a more WPF-sih approach is available by way of the ICollectionView, but that I chose to approach in a more contextual way (within the implementation already performed in the GameDictionary view model object).

The ListBox

I needed the ListBox to be able to show items in a custom fashion, namely certain items were to be certain colors, and those items would only be shown if the user found them (or when the user clicked the Solve button. So, I simply replaced the ContentPresenter in the ListBoxItem style with the following:

<Grid >
    <StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
        <TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
        <TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic" 
                   Visibility="{Binding Path=IsOriginalWord, Converter={StaticResource BoolToVisibility}}" />
    </StackPanel>
    <StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,0,5,0">
        <TextBlock x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" />
    </StackPanel>
</Grid>

In general the item is displayed in italic, but word would be adorned with a doubleasterisk ("**") if it represented toe original game word. I also added the word points to the item.

Next, a given items would only be visible if the word was "found". I used the built-in WPF converter for this

XAML
<Setter Property="Visibility" Value="{Binding Path=Found, Converter={StaticResource BoolToVisibility}}" />

Finally, the item's foreground color would depend on the status of the word. If the word is not found, it would be gray (after the puzzle was solved), blue if it was found, or red if it was found and was also the original game word.

XAML
<Setter Property="Foreground" Value="{Binding Path=Foreground}" />

The End-of-Game Statisics GroupBox

Most of you should be aware of this by now, but the standard WPF GroupBox control is (IMHO) designed incorrectly. The border of the groupbox, when placed over a container whose background is not transparent, displays a non-transparent inner and outer border. Since I'm using a Light Steel Blue background, it was painfully evident. This is what it looked like:

Image 2

In order to correct it, I had to edit the standard template to change these borders to transparent.

<Border BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}" 
        removed="{TemplateBinding Background}" Grid.ColumnSpan="4" Grid.Column="0" 
        CornerRadius="4" Grid.Row="1" Grid.RowSpan="3"/>
    <Border x:Name="Header" Grid.Column="1" Padding="3,1,3,0" Grid.Row="0" Grid.RowSpan="2">
        <ContentPresenter ContentSource="Header" RecognizesAccessKey="True" 
                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    </Border>
    <ContentPresenter Grid.ColumnSpan="2" Grid.Column="1" Margin="{TemplateBinding Padding}" 
                      Grid.Row="2" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    <Border BorderBrush="Transparent" 
            BorderThickness="{TemplateBinding BorderThickness}" 
            Grid.ColumnSpan="4" CornerRadius="4" Grid.Row="1" Grid.RowSpan="3">
        <Border.OpacityMask>
            <MultiBinding ConverterParameter="7" 
                          Converter="{StaticResource BorderGapMaskConverter}">
                <Binding ElementName="Header" Path="ActualWidth"/>
                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
            </MultiBinding>
        </Border.OpacityMask>
        <Border BorderBrush="{TemplateBinding BorderBrush}" 
                BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
            <Border BorderBrush="Transparent" 
                    BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
        </Border>
    </Border>
</Border>

Problem solved.

The Statistics Themselves

The count of words found of given sizes required the use of a ConverterParameter (and is the reason behind the existince of the WordCounts/WordCountItem classes. I needed to be able to specify the size of the words to retrieve the count for, but you can only have one converter per binding. Happily, I knew the desired word size, so I was able to use the ConverterParameter to help the converter do "the right thing".

<TextBlock Grid.Column="1" Grid.Row="2">
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="3" />
    </TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="4" />
    </TextBlock.Text>
</TextBlock>
    ...
    ...
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="10" />
    </TextBlock.Text>
</TextBlock>

Doing it this way solves the immediate problem, but is not sufficient for the design of the model itself. the model is adaptive where the largest number of letters is concerned, and this design does NOT take that into consideration. The view model itself does (there is one word cound item from 3 to "longest word size"), so one day, I might address this appropriately.

The Setup Window

Image 3

This view allows the user to changing settings in order to alter gameplay. The only problem I encountered here was with radio buttons due to their nature of by checking one, another is unchecked. I didn't feel like resolving the issue, so all RadioButton setting/getting is performed in the code behind. All of the "solutions" I saw on the internet were simply hacks around the issue, and I didn't feel like messing with it.

The Winner Window

In the event that the user actually wins a game (finding all words within the current game word), this window is displayed. I don't have a screen shot of this because I've never actually won a game. Smile | <img src= " src="http://www.codeproject.com/script/Forums/Images/smiley_smile.gif" />

Other Aspects of the Code

The Globals Object

This object is a convenient container for stuff that needs to be accessible duiring the entire application session.

The Contains Method

When a game is started, this method is called for all words in the main dictionary that are smaller than or equal to the size of the game word. I do this when the game starts to avoid any delays during actual game play.

First I created a sorted list of characters for the container (the game word), and the text (the possible word). I realize that it's technically unnecessary to sort the characters, but in reality, it sames a small amount of time in the do/while loop because the loop can potentially break out a little sooner. Needless optimization? Maybe.

C#
//--------------------------------------------------------------------------------
public static bool Contains(string container, string inText)
{
    // The idea is to simply run through the two strings and delete matching 
    // characters as they're found.  If the inText string ends up as an empty 
    // string, the word did in fact contain the specified text.
    bool   contains = false;
    List<char> containerList = new List<char>();
    containerList.AddRange(container);
    containerList.Sort();

    List<char> testList = new List<char>();
    testList.AddRange(inText);
    testList.Sort();

Once I have the two lists, I can iterate through each one, testing the current character, and if found, deleting it from each list. If the test list is empty at the end of the iteration, the word is a valid possible word so we return true.

C#
    bool found = false;
    do
    {
        found = false;
        for (int i = 0; i < containerList.Count; i++)
        {
            if (testList[0] == containerList[i])
            {
                testList.RemoveAt(0);
                containerList.RemoveAt(i);
                found = true;
                break;
            }
        }
    } while (found && testList.Count > 0);
		
    contains = (testList.Count == 0);
    return contains;
}

Final Note - this method could probably have gone into the GameDictionary class, but I was simply too damn lazy to put it there.

The CalcWordScore Method

This method calculates the number of points the specified string is worth (in a game of Scrabble). It utilizes a static array of points and can determine the score regardless of the case of the latter

C#
//--------------------------------------------------------------------------------
public static int CalcWordScore(string text)
{
    text = text.ToUpper();
    int points = 0;
    foreach(char character in text)
    {
        int charInt = Convert.ToInt16(character);
        points += m_wordPoints[charInt - ((charInt >= 97) ? 97 : 65)];
    }
    return points;
}

Final Note - this method could probably have gone into the AWord class, but I was simply too damn lazy to put it there. Are you starting to see a pattern here?

The RandomNumber Method

Since words (and sometimes word sizes) are selected at random, I needed to use the .Net Random class. In order for it to be more likely that a more random number is generated each time, I had to instantiate the Random object whenever I needed to use it. So, I created this method to do the dirty work.

C#
//--------------------------------------------------------------------------------
public static int RandomNumber(int min, int max)
{
    return new Random().Next(min, max);
}

Again, this method could have gone into the GameDictionary class, but blah, blah, blah....

The IntToEnum Method

I came up with this method several years ago to protect my code from exceptions caused by manually edited data and its potential for causing problems. I even wrote a tip about it - Setting Enumerators From Questionable Data Sources (for C# and VB)[^]. If you're interest in what it does, you can go read the tip.

How to Play the Game

Game play is quite simple. Simply start the app. The foirst thing you'll see is this message box:

Image 5

If everything is okay, you'll be told to "gird your loins". If it can't find the dictionary file, you will be informed, and the game will not proceed beyond this point. (The dictionary file has to be in the application folder.)

Assuming your own personal electronic world hasn't fallen into anarchy (the dictionary file was found), click okay, and you'll see this:

Image 6

Click the New Game button to start a game. At that point, a randomly selected word will be presented in its scrambled form, and you can immediately start typing in the User Word field.

Image 7

Just type a word, and press the Return key. If the word is valid, it will be displayed in the list along with the points earned for that word (not counting any bonus points that also may have been earned). As you enter words, the area at the top/right part of the window will show you how many words are possible, how many you've found, and how many points you've earned. Beneath the New Game and Solve buttons, you'll see a few statistics that are also updated as you play.

When you've found all of the words you can, click the Solve button, and the list will be updated to show ALL of the words that were possible. Here's what the form will look like when you hit the solve button. Notice the last word in the list that's followed by two asterisks - this is the original word that was scrambled for the game. Ironically, the word turned out to be COMPILING.

Image 8

And here's and example of some words that were found during the game.

Image 9

Some details:

  • The words you found will be displayed in blue.
  • The words you did NOT find will be displayed in gray.
  • If you found the original word, it will be displayed in red. It is also displayed with two asterisks (whether you found it or not) to indicate that it's the word that was originally scrambled for the current game.

If you're playing a timed game, the timer at the top of the window will count down to zero, and at that point the game is over. Bonus time can be configured in the Setup window, and can be awarded every time the player finds a specified number of words.

Final Comments

As I said at the top of this article, this is a simple game. The original version was my first solely owned/designed .Net app, and was written with Windows Forms. This version contains a lot less code, and a simpler model/view model from which to work.

After playing the game a bit, I've decided that it really needs some sort of audible indication that the game has expired. Maybe one day...

Changes - 18 Oct 2012

Image 10

While playing the game, I was annoyed by some of the things I was encountering. Granted, these are very minor things, but they annoyed me none the less.

What Word Did I Just Submit?

Some of the scrambled words allow a large number of words to be found, and when the listbox starts to fill with words, you start to wonder if the word you typed was accepted. The problem is that it takes time for you to scroll the list to find it (and that timer is ticking down while you're looking for the word - tick, tock).

This change had a moderate impact on the view model and the XAML. First, I had to add a property to the DisplayWord class to indicate that the word was the last one found

:

C#
//................................................................................
public bool IsLastWordFound
{
    get { return m_isLastWordFound; }
    set
    {
        m_isLastWordFound = value;
        RaisePropertyChanged("IsLastWordFound");
    }
}

Next, I added a new property to the GameDictionary class to retain a reference to the last word found. This is just so I don't have to search the list every time a word is submitted to find the word that's marked as the last word found. As you can see, setting the last word found also unsets the last word status on the previously found last word:

C#
//................................................................................
public DisplayWord LastWordFound 
{ 
    get { return m_lastWordFound; }
    set
    {
        if (m_lastWordFound != null)
        {
            m_lastWordFound.IsLastWordFound = false;
        }
        m_lastWordFound = value;
        if (m_lastWordFound != null)
        {
            m_lastWordFound.IsLastWordFound = true;
        }
        // we don't need to be notified that the last word  has changed here.
    }
}

Finally, I added the a pair of Path elements to serve as last-word-found indicators in the ListBoxItem style in the XAML.

<Path Stretch="Uniform"  Stroke="DarkGreen" Fill="DarkGreen" Data="M 0,0 7.5,7.5 0,15 0,0" Grid.Column="0" 
      Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Grid.Column="1" HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
    <TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
    <TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic" Visibility="{Binding Path=IsOriginalWord, 
                Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<TextBlock Grid.Column="2" x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" Margin="0,0,5,0" />
<Path Stretch="Uniform" Stroke="DarkGreen" Fill="DarkGreen" Data="M 15,0 15,15 7.5,7.5 15,0" Grid.Column="3" 
      Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />

The final part of this feature change is the act of scrolling the last-found-word into view. I simply added the following line to the indicated method:

C#
//--------------------------------------------------------------------------------
private void buttonSubmit_Click(object sender, RoutedEventArgs e)
{
    ...
    ...
	this.wordList.ScrollIntoView(CurrentGameDictionary.LastWordFound);
}

Game Word Background Color

I sometimes play without the timer, and this makes the background color of the to similar to the color of the ProgressBar control just above the Game Word. So, I changed the background color of the Game Word control to be LightSteelBlue

Game Word Double-Spacing

I was having a small problem with determining the letters available to me in a given game, so I decided i wanted to see about changing the kerning of the string. If you were hoping for a definitive way to do this, I'm sorry to disappoint you, but I wimped out and simply wrote a new converter class (see the DoubleSpaceConverter in the source code) that puts a space between the letters.

New Game Button

During game play, I was accidentally clicking the New game button when I actually wanted to click the Solve button first. To solve this problem, I added the following line to the indicated method in MainWindow.Xaml.cs:

C#
//--------------------------------------------------------------------------------
private void UpdateButtons()
{
    this.buttonNewGame.IsEnabled = ((CurrentGameDictionary == null) || (CurrentGameDictionary != null && !CurrentGameDictionary.IsPlaying));
    ...
    ...
}

Final Note

The screen shot provided in this section also illustrates the 1 / NN thing I did a few days ago.

04 Nov 2012 - Fixes and Features

Most programmers simply can't leave well enough alone, and that goes for me, too. I play this game a lot, and I wanted to see some cumulative statistics. I also wanted to fix the scroll bar issue.

Fixes

  • List box scroll bar - When I originally posted this article, I was having a problem with the scroll bar in the list box. The problem wasn't noticed until the user had found enough words for the scroll bar to be needed, it would appear as expected, but the scroller thumb wouldn't change size until you started dragging it.

    It finally bothered me enough that I went looking for a fix. I'd never seen this problem before, so i figured it had something to do with the fact that all of the words in the game dictionary are added to the list box, and they are collapsed until the user "finds" them. Why this confuses the ScrollViewer in the list is anybody's guess. In any case, the fix was to add the following property to the XAML that describes the list box:
     
    XML
    ScrollViewer.CanContentScroll="False"
    Once I did that, the ScrollViewer stopped acting like a drunken sailor.
     
  • User Word Focus - If you did something else on your system that cause the app's window to lose focus, the caret would not be correctly displayed sometimes in the User Word text box.
     

New Statistics Feature

Since I play the game a lot, I wanted a way to save cumulative statistics so I can see how poorly I do in the game over time. Since there were quite a few metrics to retain, I decided to change the Statistics display to a TabControl. The Current game tab shows the same stats we had before, and the Cumulative tab shows the - well - the cumulative stats. Here are some screen shots to give you an idea.

Image 11

 

 

Image 12

 

 

 

Lastly, some changes were made to program flow and functionality to accommodate the new statistics.

 

  • When you start a new game, the tab control automatically switches the Current Game page.
     
  • When you win or solve a running game, or if time expires on a running game, the tab control automatically select the Cumulative page.
     
  • Cumulative statistics can be reset either from the Cumulative tab, or from the Setup window.
     
  • Cumulative statistics are tuned on by default, but can be toggled on/off in the Setup window.
     

The other major change was the inclusion of .Net class names into the dictionary. I wrote a WinForms app that references all of the .Net 4.5 assemblies, and extracts all of class names from them. Then, it removes non-numeric characters from these discovered class names, and creates a text file that contains any class name that is 3-10 characters long. The file is created in the appropriate folder, and is loaded by default by the Anangrams2 application. There are a few hundred class names that are already covered by the standard scrabble dictionary, so the class name version of the word is not included in the resulting overall dictionary. The act of loading this additional file increases the initialization time for the Anagrams2 app by a few seconds because of the filtering being done. If you're not interested in loading the extra file, simply delete it. In the Setup window, you can elect to include class names in your games.

The interesting bit of the code involved in discovering the class names is as follows:

C#
//--------------------------------------------------------------------------------
private void BuildWordListFromClassNames()
{
    Assembly[] assemblies = null;
    try
    {
        // I tried to do this in a single Linq statement, but the first from was 
        // throwing exceptions about not being able to access some of the 
        // assemblies, so I broke it out into separate statements
        assemblies = (from assembly in AppDomain.CurrentDomain.GetAssemblies().AsParallel() 
        select assembly).ToArray<Assembly>();
    }
    catch (Exception)
    {
        // we don't care about exceptions (we take what reflect deigns to give us) 
    }
    try
    {
        if (assemblies != null)
        {
            foreach (Assembly assembly in assemblies)
            {
                var types = (from type in assembly.GetTypes().AsParallel()
                where CheckWordlength(MassageName(type.Name.ToUpper()).Length)
                select MassageName(type.Name.ToUpper())).ToArray<string>();

                if (types.Length > 0)
                {
                    foreach(string word in types)
                    {
                        if (!classnames.Contains(word))
                        {
                            classnames.Add(word);
                        }
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Oops!");
    }
    classnames.Sort();
    this.label2.Text = classnames.Count.ToString();
    this.listBox1.Items.AddRange(classnames.ToArray());
}

History

  • 14 Oct 2012 - Original version
  • 15 Oct 2012 - While writing the article, I commented out the GroupBox style used to eliminate the non-transparent border borders so I could get a screenshot of it. I was playing the game this morning when I noticed that it was still commented out. I uncommented the xaml, recompiled, and uploaded the source again.
  • 15 Oct 2012 - Change #2 - I added the number of possible words for each letter count in the Game Statistics group box. This required a change to a couple the GameStatistics, WordCounts, and WordCountItem constructors, as well as the converter that processes the data. the result is that the info is shown like this - 3-letter words: 0 / NN, where NN is the possible number of 3-letter words. I also changed the ListBox font to Consolas. The ZIP file has been re-uploaded - again.
  • 15 Oct 2012 - IMPORTANT NOTICE! *I THINK* you have to install .Net 4.5 before running this application, or change the set method on the GameDictionary.PercentRemaining property to public by removing the private accessor).
  • 18 Oct 2012 - Added some usability changes (detailed above).
  • 04 Nov 2012 - Added a couple of features and fixed a couple of bugs (detailed above).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
Questionhow about... Pin
bryce27-Apr-14 15:15
bryce27-Apr-14 15:15 
AnswerRe: how about... Pin
#realJSOP11-Jun-14 9:29
mve#realJSOP11-Jun-14 9:29 
QuestionQuick question Pin
Member 971583928-Dec-12 4:52
Member 971583928-Dec-12 4:52 
AnswerRe: Quick question Pin
#realJSOP29-Dec-12 4:47
mve#realJSOP29-Dec-12 4:47 
GeneralGo forth and code - and so you did ... Pin
Espen Harlinn5-Nov-12 11:11
professionalEspen Harlinn5-Nov-12 11:11 
QuestionMy vote of 5 Pin
Akinmade Bond22-Oct-12 2:10
professionalAkinmade Bond22-Oct-12 2:10 
AnswerRe: My vote of 5 Pin
#realJSOP23-Oct-12 1:47
mve#realJSOP23-Oct-12 1:47 
GeneralRe: My vote of 5 Pin
Akinmade Bond23-Oct-12 3:32
professionalAkinmade Bond23-Oct-12 3:32 
GeneralMy vote of 5 Pin
fredatcodeproject18-Oct-12 7:11
professionalfredatcodeproject18-Oct-12 7:11 
very good
QuestionNew Stuff Pin
#realJSOP17-Oct-12 1:57
mve#realJSOP17-Oct-12 1:57 
QuestionATTENTION Pin
#realJSOP15-Oct-12 8:34
mve#realJSOP15-Oct-12 8:34 
GeneralMy vote of 5 Pin
linuxjr15-Oct-12 5:25
professionallinuxjr15-Oct-12 5:25 
GeneralMy vote of 5 Pin
Akinmade Bond15-Oct-12 3:38
professionalAkinmade Bond15-Oct-12 3:38 
GeneralI like the bacon mention Pin
Slacker00715-Oct-12 0:55
professionalSlacker00715-Oct-12 0:55 
GeneralRe: I like the bacon mention Pin
#realJSOP15-Oct-12 1:55
mve#realJSOP15-Oct-12 1:55 
GeneralRe: I like the bacon mention Pin
Slacker00715-Oct-12 2:20
professionalSlacker00715-Oct-12 2:20 
GeneralMy vote of 4 Pin
fredatcodeproject14-Oct-12 7:38
professionalfredatcodeproject14-Oct-12 7:38 
GeneralRe: My vote of 4 Pin
#realJSOP14-Oct-12 7:49
mve#realJSOP14-Oct-12 7:49 
AnswerRe: My vote of 4 Pin
Nish Nishant14-Oct-12 12:21
sitebuilderNish Nishant14-Oct-12 12:21 
GeneralRe: My vote of 4 Pin
#realJSOP14-Oct-12 12:39
mve#realJSOP14-Oct-12 12:39 
GeneralRe: My vote of 4 Pin
Nish Nishant15-Oct-12 4:28
sitebuilderNish Nishant15-Oct-12 4:28 
GeneralRe: My vote of 4 Pin
#realJSOP15-Oct-12 4:34
mve#realJSOP15-Oct-12 4:34 
GeneralRe: My vote of 4 Pin
Nish Nishant15-Oct-12 4:35
sitebuilderNish Nishant15-Oct-12 4:35 
GeneralRe: My vote of 4 Pin
#realJSOP15-Oct-12 8:26
mve#realJSOP15-Oct-12 8:26 
GeneralRe: My vote of 4 Pin
fredatcodeproject18-Oct-12 7:12
professionalfredatcodeproject18-Oct-12 7:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.