Click here to Skip to main content
15,867,939 members
Articles / Game Development

A Complete Word Puzzle Game in C#.NET

Rate me:
Please Sign up or sign in to vote.
4.93/5 (26 votes)
14 Jul 2021CPOL18 min read 111.1K   3.7K   56   55
An eye-teaser to tease your eyes, see if you can beat it!
This is a word puzzle game which you might find in many puzzle books. Just good to have it on the machine with different words of different categories, and also to be able to play with custom words.

Image 1

Background

I coded the game a long time ago using Turbo C. But I lost the code. I thought it would be great to revive it once more with C#.NET. The language offers a lot of flexibilities in terms of memory, GC, graphics, which I had to explicitly take care of when using C. But with explicit care in C, it offered a lot of learning (that's why it is called 'God's programming language'). On the other hand, since C# .NET takes care of these, I could focus on other enhancements like word directions, overlaps, cheat codes, scoring, encryption, etc. So there is a balance that we need to appreciate for both languages.

I am calling it complete for the following reasons:

  1. It has preset words with some categories.
  2. It keeps the words and scores in encrypted files so that nobody can tamper with the files. If there is a tamper, then it would revert back to presets and start scoring from the beginning.
  3. It has two cheat codes, however cheating penalizes the scoring by deducting 50 from the current score.
  4. It has a scoring mechanism.
  5. It has a scoring summary so the player can check the scoring mechanism.

Using the Code

The game offers the following features that will be discussed in subsequent sections:

  1. Loading categories and words: Words are loaded from presets hard-coded in the program. However if the player provides in custom words, then the game automatically stores all of them (along with the presets) in a file and reads from there.
  2. Choosing directions: The game is made omni-directional in v3.0 release. That means words might be placed in any of the 8 possible directions.
  3. Placement on grid: The game places all the words in the 18x18 matrix in random locations and in random directions. There are 8 possible directions are right, down, down-left, and down-right, left, up, up-left and up-right as seen in the snap above.
  4. Scoring: Scores are stored individually for individual categories. Score is calculated as length of the word multiplied by a factor. Multiplication factors are set to different values according to different difficulty levels as shown below. These are called ‘augmenters’ hereby. Augmenters are chosen according to difficulty level. E.g., left direction has a multiplier of 20, whereas right direction has a multiplier of 10 as finding out a left-directional word is more difficult than finding out a right-directional word.
    Along with this, after all the words are found, the remaining time is multiplied by the multiplication factor ( = 10 at this release) is added with the score.
  5. Display hidden words: If time runs out and the player could not find all the words, then the game displays the words in a different colour. The same method is used to flash the words when the cheat code ‘FLASH’ is applied.
  6. Summary display: At the end of the game, a summary is displayed along with the snap of the game board so as to provide the player the details of scoring.
  7. Cheat code: The game offers two cheat codes (mambazamba, flash) on the game board. The first one raises the time by 100 more seconds. The second one cheat code flashes the words for a second and then hides them again. Both of the cheat codes penalize the score by deducting 50 points from the current score.

1) Loading Categories and Words

Loading Presets

For holding categories and words, there is the class WordEntity:

C#
class WordEntity
{
    public string Category { get; set; }
    public string Word { get; set; }
}

There are some preset categories and words as follows. The presets are all pipe-delimited where every 15th word is the category name and the following words are the words in that category.

C#
private string PRESET_WORDS =
"COUNTRIES|BANGLADESH|GAMBIA|AUSTRALIA|ENGLAND|NEPAL|INDIA|PAKISTAN|
 TANZANIA|SRILANKA|CHINA|CANADA|JAPAN|BRAZIL|ARGENTINA|" +
"MUSIC|PINKFLOYD|METALLICA|IRONMAIDEN|NOVA|ARTCELL|FEEDBACK|ORTHOHIN|
 DEFLEPPARD|BEATLES|ADAMS|JACKSON|PARTON|HOUSTON|SHAKIRA|" +
...

Encryption is used to write these words in file so that nobody can tamper with the file. If any tampering is found, the game reloads from the hard-coded categories. For encryption, a class is borrowed from here. This is simple to use – just the string and an encryption password are passed to the method. For decryption, the encrypted string and the password are passed.

If the words file exists, then the categories and words are read from there, otherwise the presets (along with player's custom words) are saved and read from there. This is done in the following code:

C#
if (File.Exists(FILE_NAME_FOR_STORING_WORDS))   // If words file exists, then read it.
    ReadFromFile();
else
{   // Otherwise, create the file and populate from there.
    string EncryptedWords = StringCipher.Encrypt(PRESET_WORDS, ENCRYPTION_PASSWORD);
    using (StreamWriter OutputFile = new StreamWriter(FILE_NAME_FOR_STORING_WORDS))
        OutputFile.Write(EncryptedWords);
    ReadFromFile();
}

The ReadFromFile() method simply reads from the file which stores the words. It first tries to decrypt the string read from file. If fails (determined by blank string returned), then it displays a message about the problem and then reload from the built-in presets. Else it reads through the strings and separates them into categories and words, and puts them in a word list. Every 15th word is the category, and subsequent words are the words under that category.

C#
string Str = File.ReadAllText(FILE_NAME_FOR_STORING_WORDS);
string[] DecryptedWords = StringCipher.Decrypt(Str, ENCRYPTION_PASSWORD).Split('|');
if (DecryptedWords[0].Equals(""))  // This means the file was tampered.
{
    MessageBox.Show("The words file was tampered. 
                     Any Categories/Words saved by the player will be lost.");
    File.Delete(FILE_NAME_FOR_STORING_WORDS);
    PopulateCategoriesAndWords();   // Circular reference.
    return;
}

string Category = "";

for (int i = 0; i <= DecryptedWords.GetUpperBound(0); i++)
{
    if (i % (MAX_WORDS + 1) == 0)   // Every 15th word is the category name.
    {
        Category = DecryptedWords[i];
        Categories.Add(Category);
    }
    else
    {
        WordEntity Word = new WordEntity();
        Word.Category = Category;
        Word.Word = DecryptedWords[i];
        WordsList.Add(Word);
    }
}

Saving Player's Customized Words

The game offers to play with customized words provided by the player. The facility is available on the same loading window. The words should be minimum 3 characters long, max 10 characters long, and there needs to be exactly 14 words - no more or no less. This is instructed in the label. Also a word cannot be sub-part of any other words. E.g.: There cannot be two words like 'JAPAN' and 'JAPANESE' as the former is contained in the latter.

Image 2

There are some validity checks on the words. There are two instant checks on max length and SPACE entry (no space allowed). This is done by adding the custom handler Control_KeyPress to the EditingControlShowing event of the words entry grid.

C#
private void WordsDataGridView_EditingControlShowing
(object sender, DataGridViewEditingControlShowingEventArgs e)
{    
    e.Control.KeyPress -= new KeyPressEventHandler(Control_KeyPress);
    e.Control.KeyPress += new KeyPressEventHandler(Control_KeyPress);
}

Whenever the user enters something, the handler is called and checks the validity. This is done as follows:

C#
TextBox tb = sender as TextBox;
if (tb.Text.Length >= MAX_LENGTH)   // Checking max length
{
    MessageBox.Show("Word length cannot be more than " + MAX_LENGTH + ".");
    e.Handled = true;
    return;
}
if (e.KeyChar.Equals(' '))          // Checking space; no space allowed. 
                                    // Other invalid characters check can be put here 
                                    // instead of the final check on save button click.
{
    MessageBox.Show("No space, please.");
    e.Handled = true;
    return;
}
e.KeyChar = char.ToUpper(e.KeyChar);

Another validity check occurs after all the words are entered and the user chooses to save and play with the custom words. First, it checks if 14 words were entered or not. Then it iterates through all of the 14 words and checks if they have invalid characters. At the same time, it also checks duplicate words. After the check succeeds, the word is added in a list.

Then it iterates over the list and checks if any word has length of less than 3. If any such word is encountered, it pops a message.

Finally, another iteration is committed with the words in the list to check if a word is contained in another word (E.g., There cannot be two words like 'JAPAN' and 'JAPANESE' as the former is contained in the later). This is done in the CheckUserInputValidity() method as below:

C#
if (WordsDataGridView.Rows.Count != MAX_WORDS)
{
    MessageBox.Show("You need to have " + MAX_WORDS + " words in the list. Please add more.");
    return false;
}

char[] NoLettersList = { ':', ';', '@', '\'', '"', '{', '}', 
                        '[', ']', '|', '\\', '<', '>', '?', ',', '.', '/',
                        '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 
                        '-', '=', '~', '!', '#', '$',
                        '%', '^', '&', '*', '(', ')', '_', '+'};   //'
foreach (DataGridViewRow Itm in WordsDataGridView.Rows)
{
    if (Itm.Cells[0].Value == null) continue;
    if (Itm.Cells[0].Value.ToString().IndexOfAny(NoLettersList) >= 0)
    {
        MessageBox.Show("Should only contain letters. 
                         The word that contains something else other than letters is: 
                         '" + Itm.Cells[0].Value.ToString() + "'");
        return false;
    }
    if (WordsByThePlayer.IndexOf(Itm.Cells[0].Value.ToString()) != -1)
    {
        MessageBox.Show("Can't have duplicate word in the list. 
                         The duplicate word is: '" + Itm.Cells[0].Value.ToString() + "'");
        return false;
    }
    WordsByThePlayer.Add(Itm.Cells[0].Value.ToString());
}

for (int i = 0; i < WordsByThePlayer.Count - 1; i++) // For every word in the list 
                                                     // check the minimum length; 
                                                     // it should be at least 
                                                     // 3 characters long.
    if (WordsByThePlayer[i].Length <3)
        {
            MessageBox.Show("Words must be at least 3 characters long. 
                             A word '" + WordsByThePlayer[i]  + 
                             "' is encountered having less than the acceptable length.'");
            return false;
        }

for (int i = 0; i < WordsByThePlayer.Count - 1; i++)     // For every word in the list.
{
    string str = WordsByThePlayer[i];
    for (int j = i + 1; j < WordsByThePlayer.Count; j++) // Check existence with every other
                                                         // word starting from the next word
        if (str.IndexOf(WordsByThePlayer[j]) != -1)
        {
            MessageBox.Show("Can't have a word as a sub-part of another word. 
                             Such words are: '" + WordsByThePlayer[i] + "' and 
                             '" + WordsByThePlayer[j] + "'");
            return false;
        }
}
return true;

The player's list is saved along with the existing words and then the gameboard is opened up with those words in that category.

2) Choosing Directions

Image 3

The game is omni-directional; meaning it offers flexibility for words placement at any directions. It at least requires 2 directions. The chosen directions would impose a scoring augmenter, which is actually a multiplication factor. This factor is chosen according to difficulty. For example, the top-right, and top-left directions seem to be the hardest, hence they have the augmenters 30, compared to easier directions of right (which has augmenter 10). After choosing directions, the choice is passed to the game engine which deals with these directions.

C#
private void PlayButton_Click(object sender, EventArgs e)
{
    try
    {
        List<GameEngine.Direction> ChosenDirections = new List<GameEngine.Direction>();
        if (!ListedDirectionsSuccessfully(ref ChosenDirections))
        {
            MessageBox.Show("Please choose at least two directions.");
            return;
        }

        GameBoard Board = new GameBoard(CurrentWords, CurrentCategory, ChosenDirections);
        Board.MdiParent = Parent.FindForm();
        Board.Show();
        Close();
    }
    catch (Exception Ex)
    {
        MessageBox.Show("An error occurred in 'PlayButton_Click' 
        method of 'ChooseDirections' form. Error msg: " + Ex.Message);
    }
}

private bool ListedDirectionsSuccessfully(ref List<GameEngine.Direction> Directions)
{
    foreach (Control Ctl in Controls)
        if (Ctl is CheckBox)
            if ((Ctl as CheckBox).Checked)
                Directions.Add((GameEngine.Direction)Enum.Parse
                (typeof(GameEngine.Direction), Ctl.Tag.ToString()));
    return Directions.Count >= 2;
}

This is to note that the GameEngine.Direction is borrowed from the game engine class which is the actual host of the Direction enum.

3) Placement on Grid

The main codes and logics are in the class GameEngine.

Placing Words on the Grid

The words are placed on the grid in the InitializeBoard() method. There is a character matrix (2D char array) WORDS_IN_BOARD where the words are placed first. Then this matrix is mapped to the grid. All the words are iterated one by one. For each word, a random location is obtained along with random direction (from 8 directions). At this point, the word matrix looks somewhat like the following:

Image 4

Placement is done in PlaceTheWords() method which obtains four parameters - direction of word, the X and Y-coordinates, and the word itself. This is a key method, so this needs a clear explanation for all the 8 directions.

Right Direction

A loop is run character by character for the entire word. First, it checks if the word is falling outside the grid. If that is true, then it returns to the calling procedure asking to generate a new random location and direction.

If it passes the boundary check above, then it checks if the current character is likely to overlap with an existing character on the grid. If that happens, then it checks if it is the same character or not. If not the same character, then it returns to the calling method requesting another random location and direction.

After these two checks, if the placement is a possibility, then it places the word in the matrix and also stores the location and direction in a list WordPositions through the method StoreWordPosition().

C#
case Direction.Right:
    for (int i = 0, j = PlacementIndex_X; i < Word.Length; i++, j++) // First we check 
                                                                     // if the word can be 
                                                                     // placed in the array. 
                                                                     // For this it needs 
                                                                     // blanks there.
    {
        if (j >= GridSize) return false; // Falling outside the grid. 
                                         // Hence placement unavailable.
        if (WORDS_IN_BOARD[j, PlacementIndex_Y] != '\0')
            if (WORDS_IN_BOARD[j, PlacementIndex_Y] != Word[i])   // If there is an overlap, 
                                                                  // then we see if the 
                                                                  // characters match. 
                                                                  // If matches, then it 
                                                                  // can still go there.
            {
                PlaceAvailable = false;
                break;
            }
    }
    if (PlaceAvailable)
    {   // If all the cells are blank, or a non-conflicting overlap is available, 
        // then this word can be placed there. So place it.
        for (int i = 0, j = PlacementIndex_X; i < Word.Length; i++, j++)
            WORDS_IN_BOARD[j, PlacementIndex_Y] = Word[i];
        StoreWordPosition(Word, PlacementIndex_X, PlacementIndex_Y, OrientationDecision);
        return true;
    }
    break;
Word Positions

The WordPosition class plays a vital role in holding the word map, pixel information, direction, and scoring augmenter. The class is as follows:

C#
public class WordPosition
{
    public string Word { get; set; }
    public int PlacementIndex_X { get; set; }
    public int PlacementIndex_Y { get; set; }
    public GameEngine.Direction Direction { get; set; }
    public int ScoreAugmenter { get; set; }
}

And the method that keeps the positions of the words is as follows. It obtains four parameters – the word itself, the X and Y-coordinates of the word, and the direction. It instantiates the above class, stores information, and puts the augmenter factor according to directions.

C#
private void StoreWordPosition(string Word, int PlacementIndex_X, 
                               int PlacementIndex_Y, Direction OrientationDecision)
{
    WordPosition Pos = new WordPosition();
    Pos.Word = Word;
    Pos.PlacementIndex_X = PlacementIndex_X;
    Pos.PlacementIndex_Y = PlacementIndex_Y;
    Pos.Direction = OrientationDecision;

    switch (OrientationDecision)
    {
        case Direction.Down: Pos.ScoreAugmenter = 10; break;
        case Direction.Up: Pos.ScoreAugmenter = 20; break;
        case Direction.Right: Pos.ScoreAugmenter = 10; break;
        case Direction.Left: Pos.ScoreAugmenter = 20; break;
        case Direction.DownLeft: Pos.ScoreAugmenter = 20; break;
        case Direction.DownRight: Pos.ScoreAugmenter = 20; break;
        case Direction.UpLeft: Pos.ScoreAugmenter = 30; break;
        case Direction.UpRight: Pos.ScoreAugmenter = 30; break;
        case Direction.None: Pos.ScoreAugmenter = 0; break;
    }
    WordPositions.Add(Pos);
}
Other Directions

The same logic applies for finding a good placement for the word for these other 7 directions. They differ in the increment/decrement of the matrix positions and boundary checks.

After all the words are placed in the matrix, the FillInTheGaps() method fills in the rest of the matrix with random letters. For every NULL cells (\0), it generates a random uppercase letter and puts it there.

C#
for (int i = 0; i < GridSize; i++)
    for (int j = 0; j < GridSize; j++)
        if (WORDS_IN_BOARD[i, j] == '\0')
            WORDS_IN_BOARD[i, j] = (char)(65 + GetRandomNumber(Rnd, 26));

At this point, the form opens up and fires the Paint() event. On this event, first we draw the lines which ultimately display as 40x40 pixels rectangles. Then we map our character matrix to the board.

C#
Pen pen = new Pen(Color.FromArgb(255, 0, 0, 0));

ColourCells(ColouredRectangles, Color.LightBlue);
if (FailedRectangles.Count > 0) ColourCells(FailedRectangles, Color.ForestGreen);

// Draw horizontal lines.
for (int i = 0; i <= GridSize; i++)
    e.Graphics.DrawLine(pen, 40, (i + 1) * 40, GridSize * 40 + 40, (i + 1) * 40);

// Draw vertical lines.
for (int i = 0; i <= GridSize; i++)
    e.Graphics.DrawLine(pen, (i + 1) * 40, 40, (i + 1) * 40, GridSize * 40 + 40);

MapArrayToGameBoard();

The MapArrayToGameBoard() method simply puts the character matrix on the board. The drawing code from MSDN is used here. This iterates through all the characters in the matrix places them in the middle of the 40x40 pixels rectangles with margin calibration of 10 pixels.

C#
Graphics formGraphics = CreateGraphics();
Font drawFont = new Font("Arial", ResponsiveObj.GetMetrics(16));
SolidBrush drawBrush = new SolidBrush(Color.Black);
string CharacterToMap;

try
{
    for (int i = 0; i < GridSize; i++)
        for (int j = 0; j < GridSize; j++)
        {
            if (TheGameEngine.WORDS_IN_BOARD[i, j] != '\0')
            {
                CharacterToMap = "" + TheGameEngine.WORDS_IN_BOARD[i, j]; // "" is needed 
                                                                          // as a means for 
                                                                          // conversion of 
                                                                          // character to 
                                                                          // string.
                formGraphics.DrawString(CharacterToMap, drawFont, drawBrush, 
                                       (i + 1) * SizeFactor + CalibrationFactor, 
                                       (j + 1) * SizeFactor + CalibrationFactor);
            }
        }
}

Word Finding and Validity Checks

Mouse click and release positions are stored in Points list. The CheckValidityAndUpdateScore() method is called on mouse button release event (GameBoard_MouseUp()). In the meantime, while the user drags the mouse while left button down, a line is drawn from the starting position to the mouse pointer. This is done in the GameBoard_MouseMove() event.

C#
if (Points.Count > 1)
    Points.Pop();
if (Points.Count > 0)
    Points.Push(e.Location);

// Form top = X = Distance from top, left = Y = Distance from left.
// However mouse location X = Distance from left, Y = Distance from top.

// Need an adjustment to exact the location.
Point TopLeft = new Point(Top, Left);
Point DrawFrom = new Point(TopLeft.Y + Points.ToArray()[0].X + CalibrationFactor, 
                 TopLeft.X + Points.ToArray()[0].Y + MouseDrawYCalibrationFactor);
Point DrawTo = new Point(TopLeft.Y + Points.ToArray()[1].X + CalibrationFactor, 
               TopLeft.X + Points.ToArray()[1].Y + MouseDrawYCalibrationFactor);

ControlPaint.DrawReversibleLine(DrawFrom, DrawTo, Color.Black); // draw new line

The validity of a word is checked in CheckValidity() method. It formulates the word by grabbing all the letters drawn using the mouse by looking at the corresponding character matrix. Then it checks if this really matches a word in our word list. If matched, then it updates the cells by colouring them to light blue and graying the word in the word list.

Following is a snippet of code that grabs the line start and end positions. First, it checks if the line falls outside the boundary. Then it formulates the word and also stores the co-ordinates of the rectangles. Similarly, it checks for vertical, down-left and down-right words and tries to match accordingly. If this is really a match, then we store the temporary rectangles in our ColouredRectangles points list through AddCoordinates() method.

C#
if (Points.Count == 1) return; // This was a doble click, no dragging, hence return.
int StartX = Points.ToArray()[1].X / SizeFactor;    // Retrieve the starting position 
                                                    // of the line.
int StartY = Points.ToArray()[1].Y / SizeFactor;

int EndX = Points.ToArray()[0].X / SizeFactor;      // Retrieve the ending position 
                                                    // of the line.
int EndY = Points.ToArray()[0].Y / SizeFactor;

if (StartX > GridSize || EndX > GridSize || StartY > GridSize || 
                                            EndY > GridSize || // Boundary checks.
    StartX <= 0 || EndX <= 0 || StartY <= 0 || EndY <= 0)
    StatusForDisplay ="Nope!";

StringBuilder TheWordIntended = new StringBuilder();
List<Point> TempRectangles = new List<Point>();
TheWordIntended.Clear();
if (StartX < EndX && StartY == EndY) // Right line drawn.
    for (int i = StartX; i <= EndX; i++)
    {
        TheWordIntended.Append(WORDS_IN_BOARD[i - 1, StartY - 1].ToString());
        TempRectangles.Add(new Point(i * SizeFactor, StartY * SizeFactor));
    }
else if (StartX > EndX && StartY == EndY) // Left line drawn.
.................................
.................................
.................................

In the similar way, it checks for all other directions. Please notice this is an IF-ELSE IF block; once a direction is matched, the word in that direction only is added and other blocks are not entered.

After a word is formulated, it check if the word is in the words list. If it is there and not already found, then it adds the word in the WORDS_FOUND LIST and updates the score.

4) Scoring

For scoring, there is a score file. If it is missing, then it creates one with the current score and category. Here again, all the scores are combined in a big pipe-delimited string, then that string is encrypted and put in file. There are four attributes for a score:

C#
class ScoreEntity
{
    public string Category { get; set; }
    public string Scorer { get; set; }
    public int Score { get; set; }
    public DateTime ScoreTime { get; set; }
..............
..............

It allows a maximum of MAX_SCORES (= 14 at this article) scores for a category. First, it loads all the scores in the scores list, then it obtains a sorted subset of scores for the current category (highest score on top). In that subset it checks if the current score greater than or equal to (>=) any available score. If it is, then it inserts the current score. After that, it checks if the subset count exceeds MAX_SCORES, if it does, then it eliminates the last one. So the last score is gone and the list always has MAX_SCORES scores. This is done in CheckAndSaveIfTopScore() method.

Here again, if somebody tampers the score file, then it simply starts a new scoring. No tampering allowed.

5) Displaying Hidden Words

If time runs out (or if cheat applied), then the game displays the words in green. First, it obtains the words that the player was not able to find. This is done here.

C#
List<string> FailedWords = new List<string>();
foreach (string Word in WORD_ARRAY)
    if (WORDS_FOUND.IndexOf(Word) == -1)
        FailedWords.Add(Word);

Then it iterates through these failed word locations and formulates the corresponding failed rectangles. Finally, it calls the form's paint method by invalidating.

C#
foreach (string Word in FailedWords)
{
    WordPosition Pos = TheGameEngine.ObtainFailedWordPosition(Word);

    if (Pos.Direction == GameEngine.Direction.Right) // Right.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; i++, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.Left) // Left.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; i--, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.Down) // Down.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; j++, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.Up) // Up.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; j--, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.DownLeft) // Down left word.
        for (int i = Pos.PlacementIndex_Y + 1, j = Pos.PlacementIndex_X + 1, 
             k = 0; k < Pos.Word.Length; i--, j++, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.UpLeft) // Up left word.
        for (int i = Pos.PlacementIndex_Y + 1, j = Pos.PlacementIndex_X + 1, 
             k = 0; k < Pos.Word.Length; i--, j--, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.DownRight) // Down right word.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; i++, j++, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
    else if (Pos.Direction == GameEngine.Direction.UpRight) // Up Right word.
        for (int i = Pos.PlacementIndex_X + 1, j = Pos.PlacementIndex_Y + 1, 
             k = 0; k < Pos.Word.Length; i++, j--, k++)
            FailedRectangles.Add(new Point(i * SizeFactor, j * SizeFactor));
}
Invalidate();

6) Summary Display

Image 5

The idea is to display a summary of scoring to the player at the end of the game (whether successful in finding out of the words, or failed to find all). This is done in the DisplayScoreDetails() method of the GameBoard form's code file. This, on the other hand, captures a snap of the word grid area of the board (at the current situation – with colours for success and fails) and passes it as a memory stream to the ScoreDetails form.

C#
private void DisplayScoreDetails()
{
    MemoryStream MS = new MemoryStream();
    CaptureGameScreen(ref MS);

    ScoreDetails ScoreDetailsObj = new ScoreDetails(TheGameEngine.WordPositions, 
    GameEngine.REMAINING_TIME_BONUS_FACTOR, TheGameEngine.WORDS_FOUND, Words, 
    Clock.TimeLeft, TheGameEngine.CurrentScore, ref MS);
    ScoreDetailsObj.MdiParent = Parent.FindForm();
    ScoreDetailsObj.Show();
}

private void CaptureGameScreen(ref MemoryStream MS)
{
    using (Bitmap bitmap = new Bitmap(GridSize * SizeFactor + 2, GridSize * SizeFactor + 2))
    {
        using (Graphics g = Graphics.FromImage(bitmap))
        {
            if (Screen.PrimaryScreen.Bounds.Width >= 1600)
                g.CopyFromScreen(new Point(Bounds.Left + SizeFactor + 
                ResponsiveObj.GetMetrics(10), Convert.ToInt16(Bounds.Top + 
                (SizeFactor * 3.25))), Point.Empty, Bounds.Size);
            else if (Screen.PrimaryScreen.Bounds.Width > 1200)
                g.CopyFromScreen(new Point(Bounds.Left + SizeFactor + 
                ResponsiveObj.GetMetrics(10), Convert.ToInt16(Bounds.Top + 
                (SizeFactor * 3.85))), Point.Empty, Bounds.Size);
            else if (Screen.PrimaryScreen.Bounds.Width > 1100)
                g.CopyFromScreen(new Point(Bounds.Left + SizeFactor + 
                ResponsiveObj.GetMetrics(10), Convert.ToInt16(Bounds.Top + 
                (SizeFactor * 4.2))), Point.Empty, Bounds.Size);
            else
                g.CopyFromScreen(new Point(Bounds.Left + SizeFactor + 
                ResponsiveObj.GetMetrics(10), Convert.ToInt16(Bounds.Top + 
                (SizeFactor * 4.65))), Point.Empty, Bounds.Size);
        }
        bitmap.Save(MS, ImageFormat.Bmp);
    }
}

The purpose of the responsive object can be found in the references section of this article; this is not discussed here. Just to summarize, it provides a clever approach to scale the controls according to different resolutions – sort of ‘Shrink Ray’ as seen in the movie ‘Despicable Me’ :).

I have failed to find a generic approach to exact the grid area for different resolutions. As an alternative approach, different resolutions were tried to find a good capture of the words grid and then it was passed to the details form. The details form then regenerates the image and displays the score summary accordingly. This is to assist the player in understanding the calculations the game made for scoring. A point of interest here is, the tabs (\t) didn't work; perhaps it doesn't work in label texts.

C#
private void LoadScoreDetails()
{
    StringBuilder SBuilder = new StringBuilder();
    SBuilder.Append("Score for found words:\n");
    SBuilder.Append("======================\n");
    int Augmenter, Len;
    foreach(string Wrd in WORDS_FOUND)
    {
        Augmenter = WordPositions.Find(p => p.Word.Equals(Wrd)).ScoreAugmenter;
        Len = Wrd.Length;
        SBuilder.Append(Wrd + ", Score:\t\t" + Len.ToString() + " x " + 
        WordPositions.Find(p => p.Word.Equals(Wrd)).ScoreAugmenter.ToString() + 
        " = " + (Len * Augmenter).ToString() + "\n");
    }

    SBuilder.Append("\nFailed Words:\n");
    SBuilder.Append("======================\n");

    string[] FailedWords = WORD_ARRAY.Where(p => !WORDS_FOUND.Any
                           (p2 => p2.Equals(p))).ToArray();
    if (FailedWords.GetUpperBound(0) < 0)
        SBuilder.Append("None\n");
    else
        foreach(string Word in FailedWords)
            SBuilder.Append(Word + "\n");
    SBuilder.Append("\nTimer bonus:\t\t");
    SBuilder.Append("======================\n");
    if (RemainingTime == 0)
        SBuilder.Append("None\n");
    else SBuilder.Append(RemainingTime.ToString() + " x " + 
         REMAINING_TIME_MULTIPLIER.ToString() + " = " + 
         (RemainingTime * REMAINING_TIME_MULTIPLIER).ToString() + "\n");

    SBuilder.Append("======================\n");
    SBuilder.Append("Total score:\t\t" + TotalScore.ToString());

    ScoreDetailslabel.Text = SBuilder.ToString();
}

Saving the snap is not provided at this point. Of course, the same approach of snapping the game board can be applied here as well.

7) Cheat Code

This is a minor thing to describe. This works on the keyup event where any keystroke is grabbed into two intermediary variables - CheatCodeForIncreasingTime and CheatCodeForFlashUndiscoveredWords. Actually, the keystrokes are amalgamated as entered by the player on the game window. Then it checks if the code matches any available cheat codes (‘mambazamba’, or ‘flash’). For example, if the player presses 'm' and 'a', then they are kept as 'ma' in the CheatCodeForIncreasingTime variable (because, 'ma' still matches the cheatcode pattern). Similarly, we add consecutive variables to it if it matches the pattern of the CHEAT_CODE. However, once it fails to match a pattern (e.g., 'mambi'), then it starts over.

Because the game has 2 cheat codes at the moment, so care needs to be taken for both of them explicitly. That is why the keystroke is kept in two separate variables and a match is checked separately. Whichever matches, it triggers the corresponding cheat action.

Finally, if there is a match with ‘mambazamba’ then the first cheat is activated (literally, it raises the remaining time by 100 more seconds), and applies the penalty (deducts 50 points from the current score).

On the other hand, if it matches with ‘flash’, then the second cheat is activated (this would flash all the undiscovered words on the board for 1 second and then hide them back), and applies the same penalty.

C#
public enum CHEAT_TYPE { INCREASE_TIME, FLASH_WORDS, NONE};

CheatType = CHEAT_TYPE.NONE;
CheatCodeForIncreasingTime += CheatCode;

if (CHEAT_CODE_FOR_INCREASING_TIME.IndexOf(CheatCodeForIncreasingTime) == -1)  // Cheat code 
          // didn't match with any part of the cheat code starting from the first letter.
    CheatCodeForIncreasingTime = (CheatCode);          // Hence, erase it to start over.
else if (CheatCodeForIncreasingTime.Equals(CHEAT_CODE_FOR_INCREASING_TIME) && 
         WordsFound != MAX_WORDS)
{
    CheatType = CHEAT_TYPE.INCREASE_TIME;
    return true;
}

CheatCodeForFlashUndiscoveredWords += CheatCode;
if (CHEAT_CODE_FOR_UNDISCOVERED_WORDS.IndexOf
    (CheatCodeForFlashUndiscoveredWords) == -1)        // Cheat code didn't match with 
                                                       // any part of the cheat code.
    CheatCodeForFlashUndiscoveredWords = (CheatCode);  // Hence, erase it to start over.
else if (CheatCodeForFlashUndiscoveredWords.Equals(CHEAT_CODE_FOR_UNDISCOVERED_WORDS) && 
                                                   WordsFound != MAX_WORDS)
{
    CheatType = CHEAT_TYPE.FLASH_WORDS;
    return true;
}
return false;

The interesting thing to note here is we have to use the KeyUp event of the WordsListView instead of the form. This is because after loading the game window, the list box has the focus, not the form.

Environment

Coded using Visual Studio 2015 IDE, with .NET Framework of 4.5. This is not a mobile version - a machine is required to play.

Points of Interest

To force a redraw of the window, we need to call the Invalidate() method of the window. There was also a need to calibrate the mouse co-ordinates by adjusting with the forms top and left positions. The interesting thing is, a form's co-ordinates are defined as: X to be the distance from top of the screen, Y to be the distance from left of the screen. However, mouse co-ordinates are defined as the other way: X as the distance from left of the window, Y as the distance from top of the window. Hence, for calibration, we needed to adjust carefully.

C#
private void GameBoard_MouseMove(object sender, MouseEventArgs e)
{
    try
    {
        if (e.Button == MouseButtons.Left)
        {
            if (Points.Count > 1)
                Points.Pop();
            if (Points.Count > 0)
                Points.Push(e.Location);

            // Form top = X = Distance from top, left = Y = Distance from left.
            // However mouse location X = Distance from left, Y = Distance from top.

            // Need an adjustment to exact the location.
            Point TopLeft = new Point(Top, Left);
            Point DrawFrom = new Point(TopLeft.Y + Points.ToArray()[0].X + 10, 
                                       TopLeft.X + Points.ToArray()[0].Y + 80);
            Point DrawTo = new Point(TopLeft.Y + Points.ToArray()[1].X + 10, 
                                     TopLeft.X + Points.ToArray()[1].Y + 80);

            ControlPaint.DrawReversibleLine(DrawFrom, DrawTo, Color.Black); // draw new line
        }
    }

Another important and interesting thing was discovered through the message of jrobb229 about the ENTER key behaviour. The initial release offered instant checks on the datagrid where the player wants to enter words less than 3 characters long. It actually processed the logic check of length, but there was no way to stop the cursor from moving to the next cell. This happened on the way it was implemented.

I still couldn’t find a way to counter this behaviour. So I added the alternate approach of doing the length check in the later validation. I am not quite happy with the bypass; anyway just provided an alternative to the current bug, and hope to find the perfect workaround soon.

Glitches

I found a minor glitch if there are multiple monitors in a machine. If the game is loaded in one window, and it is moved to the other window, then mouse dragging keeps scar marks on the first window. But no panic, it erases after the game is closed.

Another glitch will be observed if the game board is moved to another window from the primary window and the snapping code tries to snap the given area of the primary screen. The reason is the same as the current release opts for screen capturing for the primary screen. A check is not provided as to where the game board has moved at the point of capturing.

Disclaimer

Aside from the initial release, the game is refactored to a more Object-Oriented approach. However, as it is a never-ending process, so there might be more ways to improvise.

I didn't follow any naming convention. My personal preference is to have a name that might be able to tell the intention, while hovering over the name, we can easily understand the type; so why making a variable heavier with names like 'strStatusLabel'? There might be controversies, however that is not what this article is intended for.

Acknowledgements

Thanks to:

  • Member 10014441 for reporting the ‘CalibrationFactor’ bug
  • jrobb229 for reporting the top score bug, and the datagridview ENTER key anomaly bug. Also thanks for feature improvement suggestions
  • sx2008 for suggestions on size reduction of the project
  • everybody else for playing and commenting :)

Future Works

Remaining time should be adjusted according to difficulty levels. At this moment, a fixed 720 seconds does not really justify difficulty levels as easier directions and harder directions both have the same time limit. On another note, this might be considered okay as the player opts for a difficult game and hence time should remain constant.

The details screen can be captured and saved as an image for a future reference. The code for capturing a screen is already there.

A generic approach for snapping the game board in different resolutions might be sought for. At the moment, it is rather a crude approach with some IF-ELSE conditions.

The ENTER key press for the datagridview is actually not fired. This is an odd behaviour and difficult to deal with when we want to see what is happening (e.g., checking word length for less than 3 characters) at ENTER key press. ‘e.Handled’ is not applicable in this case. At this release, this problem was bypassed with an alternative approach. I am not very happy with the bypass, but just resorted to that to get it going at the moment. This is a genuine programming optimization and can be looked after.

Summary

This is a word puzzle game featuring preset words, custom words, scoring on individual word categories.

References

Finding items of a list which are absent in another list:

History

  • 10th October, 2016: First release
  • 17th/18th October, 2016: Bug fixing, responsive design, re-formatted code in CodeProject
  • 20th October, 2016: Removed installer from downloadable, resized downloadable project and stored the executable in that downloadable zip file, added one more reference.
  • 15th November, 2016: Made it omni-directional (8 directions), provided two cheat codes, and refactored the whole project with a better OOP approach, better scoring. Scoring is now available if not all the words are found by the time limit, but is a top score anyway. Better scoring summary for scoring reference.

License

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


Written By
Software Developer
Bangladesh Bangladesh
A software developer mainly in .NET technologies and SQL Server. Love to code and learn.

Comments and Discussions

 
QuestionMessage Closed Pin
14-Jan-22 6:51
Arhan Tapduk14-Jan-22 6:51 
QuestionWord pre_set Pin
Member 1404088020-Mar-19 12:08
Member 1404088020-Mar-19 12:08 
AnswerRe: Word pre_set Pin
Mehedi Shams11-Aug-19 12:52
Mehedi Shams11-Aug-19 12:52 
Questionyou are my god Pin
Member 1337462623-Aug-17 2:04
Member 1337462623-Aug-17 2:04 
AnswerRe: you are my god Pin
Mehedi Shams5-Nov-17 19:16
Mehedi Shams5-Nov-17 19:16 
QuestionFinally, something I have been looking for. Pin
James Lonero2-Dec-16 5:39
James Lonero2-Dec-16 5:39 
AnswerRe: Finally, something I have been looking for. Pin
Mehedi Shams3-Dec-16 0:48
Mehedi Shams3-Dec-16 0:48 
GeneralRe: Finally, something I have been looking for. Pin
James Lonero5-Dec-16 5:49
James Lonero5-Dec-16 5:49 
GeneralRe: Finally, something I have been looking for. Pin
Mehedi Shams5-Dec-16 11:10
Mehedi Shams5-Dec-16 11:10 
GeneralRe: Finally, something I have been looking for. Pin
Mehedi Shams7-Dec-16 17:56
Mehedi Shams7-Dec-16 17:56 
QuestionNice! Pin
rx7man16-Nov-16 20:04
rx7man16-Nov-16 20:04 
AnswerRe: Nice! Pin
Mehedi Shams17-Nov-16 16:26
Mehedi Shams17-Nov-16 16:26 
GeneralMy vote of 5 Pin
jrobb22916-Nov-16 9:11
jrobb22916-Nov-16 9:11 
GeneralRe: My vote of 5 Pin
Mehedi Shams17-Nov-16 16:17
Mehedi Shams17-Nov-16 16:17 
GeneralMy vote of 5 Pin
Farhad Reza12-Nov-16 6:06
Farhad Reza12-Nov-16 6:06 
GeneralRe: My vote of 5 Pin
Mehedi Shams12-Nov-16 12:15
Mehedi Shams12-Nov-16 12:15 
GeneralRe: My vote of 5 Pin
Farhad Reza13-Nov-16 1:28
Farhad Reza13-Nov-16 1:28 
GeneralMy vote of 5 Pin
jrobb22911-Nov-16 12:10
jrobb22911-Nov-16 12:10 
GeneralRe: My vote of 5 Pin
Mehedi Shams11-Nov-16 16:08
Mehedi Shams11-Nov-16 16:08 
GeneralRe: My vote of 5 Pin
Mehedi Shams13-Nov-16 15:33
Mehedi Shams13-Nov-16 15:33 
GeneralRe: My vote of 5 Pin
jrobb22913-Nov-16 15:58
jrobb22913-Nov-16 15:58 
GeneralRe: My vote of 5 Pin
Mehedi Shams14-Nov-16 23:57
Mehedi Shams14-Nov-16 23:57 
GeneralRe: My vote of 5 Pin
jrobb22915-Nov-16 5:59
jrobb22915-Nov-16 5:59 
GeneralRe: My vote of 5 Pin
Mehedi Shams17-Nov-16 16:23
Mehedi Shams17-Nov-16 16:23 
GeneralRe: My vote of 5 Pin
jrobb22917-Nov-16 18:44
jrobb22917-Nov-16 18:44 

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.