Click here to Skip to main content
13,146,742 members (79,717 online)
Click here to Skip to main content
Add your own
alternative version

Stats

24.4K views
1.4K downloads
39 bookmarked
Posted 9 Oct 2016

A Complete Word Puzzle Game in C#.NET

, 14 Nov 2016
Rate this:
Please Sign up or sign in to vote.
An eye-teaser to tease your eyes, see if you can beat it!

Introduction

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.

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, so 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 elvel. 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 catories and words there is the class WordEntity:

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

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

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:

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.

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 later.

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

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:

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:

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 word in that category.

2) Choosing Directions:

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.
 
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.

Placement is done in PlaceTheWords() method which obtains 4 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().

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:

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.

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.

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.

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.

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.

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.

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:
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 elimiates 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.
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.
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:

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.
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 summarise, 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 ot interest here is, the tabs (\t) didn't work; perhaps it doesn't work in label texts.

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 amalgumated 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.

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 claibrate 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.

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 work around soon.

Glitches

I found a minor glitch if there are multiple montors 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 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

Thanking Member 10014441 for reporting the ‘CalibrationFactor’ bug.

Thanking jrobb229 for reporting the top score bug, and the datagridview ENTER key anomaly bug. Also thanks for feature improvement suggestions.

Thanking sx2008 for suggestions on size reduction of the project.

Thanks to everybody else for playing and commenting :).

Future Works

Remaining time should be adjusted according to difficulty levels. At this moment a fixed 720 seconds 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 saught 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 baypass, 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

Drawing letter on a form

Drawing a straight line on a form - Link 1

Drawing a straight line on a form - Link 2

Colour a rectangle

Datagrid font sizing

Datagrid column width setup

Datagrid column header setup

Datagrid key press event handler

Bind datagrid columns with predefined columns

String ciphering

Fixing maximum number of allowed rows in a DataGridView

Finding items of a list which are absent in another list

Capturing Screen - Link 1

Capturing Screen - Link 2

A Responsive Design Technique for Winforms

Arrows Clip-Art

History

10 Oct 2016: First Release

17/18 Oct 2016: Bug fixing, responsive design, re-formatted code in CP.

20 Oct 2016: Removed installer from downloadable, resized downloadable project and stored the executable in that downlaodable zip file, added one more reference.

15 Nov 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)

Share

About the Author

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

You may also be interested in...

Pro
Pro

Comments and Discussions

 
Questionyou are my god Pin
Member 1337462623-Aug-17 2:04
memberMember 1337462623-Aug-17 2:04 
QuestionFinally, something I have been looking for. Pin
James Lonero2-Dec-16 5:39
memberJames Lonero2-Dec-16 5:39 
AnswerRe: Finally, something I have been looking for. Pin
Mehedi Shams3-Dec-16 0:48
memberMehedi Shams3-Dec-16 0:48 
GeneralRe: Finally, something I have been looking for. Pin
James Lonero5-Dec-16 5:49
memberJames Lonero5-Dec-16 5:49 
GeneralRe: Finally, something I have been looking for. Pin
Mehedi Shams5-Dec-16 11:10
memberMehedi Shams5-Dec-16 11:10 
GeneralRe: Finally, something I have been looking for. Pin
Mehedi Shams7-Dec-16 17:56
memberMehedi Shams7-Dec-16 17:56 
QuestionNice! Pin
rx7man16-Nov-16 20:04
memberrx7man16-Nov-16 20:04 
AnswerRe: Nice! Pin
Mehedi Shams17-Nov-16 16:26
memberMehedi Shams17-Nov-16 16:26 
GeneralMy vote of 5 Pin
jrobb22916-Nov-16 9:11
memberjrobb22916-Nov-16 9:11 
GeneralRe: My vote of 5 Pin
Mehedi Shams17-Nov-16 16:17
memberMehedi Shams17-Nov-16 16:17 
GeneralMy vote of 5 Pin
Mohammad Shuvo12-Nov-16 6:06
memberMohammad Shuvo12-Nov-16 6:06 
GeneralRe: My vote of 5 Pin
Mehedi Shams12-Nov-16 12:15
memberMehedi Shams12-Nov-16 12:15 
GeneralRe: My vote of 5 Pin
Mohammad Shuvo13-Nov-16 1:28
memberMohammad Shuvo13-Nov-16 1:28 
GeneralMy vote of 5 Pin
jrobb22911-Nov-16 12:10
memberjrobb22911-Nov-16 12:10 
GeneralRe: My vote of 5 Pin
Mehedi Shams11-Nov-16 16:08
memberMehedi Shams11-Nov-16 16:08 
GeneralRe: My vote of 5 Pin
Mehedi Shams13-Nov-16 15:33
memberMehedi Shams13-Nov-16 15:33 
GeneralRe: My vote of 5 Pin
jrobb22913-Nov-16 15:58
memberjrobb22913-Nov-16 15:58 
GeneralRe: My vote of 5 Pin
Mehedi Shams14-Nov-16 23:57
memberMehedi Shams14-Nov-16 23:57 
GeneralRe: My vote of 5 Pin
jrobb22915-Nov-16 5:59
memberjrobb22915-Nov-16 5:59 
GeneralRe: My vote of 5 Pin
Mehedi Shams17-Nov-16 16:23
memberMehedi Shams17-Nov-16 16:23 
GeneralRe: My vote of 5 Pin
jrobb22917-Nov-16 18:44
memberjrobb22917-Nov-16 18:44 
BugDisplay Failed Words Pin
Member 1001444124-Oct-16 0:11
memberMember 1001444124-Oct-16 0:11 
GeneralRe: Display Failed Words Pin
Mehedi Shams24-Oct-16 12:16
memberMehedi Shams24-Oct-16 12:16 
QuestionCan't Open Project in VS 2012 Pin
rspercy6521-Oct-16 2:26
memberrspercy6521-Oct-16 2:26 
AnswerRe: Can't Open Project in VS 2012 Pin
Mehedi Shams21-Oct-16 11:24
memberMehedi Shams21-Oct-16 11:24 

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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.170915.1 | Last Updated 15 Nov 2016
Article Copyright 2016 by Mehedi Shams
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid