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, 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:
- It has preset words with some categories.
- 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.
- It has two cheat codes, however cheating penalizes the scoring by deducting 50 from the current score.
- It has a scoring mechanism.
- 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:
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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
:
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.
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))
ReadFromFile();
else
{
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(""))
{
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();
return;
}
string Category = "";
for (int i = 0; i <= DecryptedWords.GetUpperBound(0); i++)
{
if (i % (MAX_WORDS + 1) == 0)
{
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.
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.
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)
{
MessageBox.Show("Word length cannot be more than " + MAX_LENGTH + ".");
e.Handled = true;
return;
}
if (e.KeyChar.Equals(' '))
{
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++)
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++)
{
string str = WordsByThePlayer[i];
for (int j = i + 1; j < WordsByThePlayer.Count; j++)
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
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 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()
.
case Direction.Right:
for (int i = 0, j = PlacementIndex_X; i < Word.Length; i++, j++)
{
if (j >= GridSize) return false;
if (WORDS_IN_BOARD[j, PlacementIndex_Y] != '\0')
if (WORDS_IN_BOARD[j, PlacementIndex_Y] != Word[i])
{
PlaceAvailable = false;
break;
}
}
if (PlaceAvailable)
{
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);
for (int i = 0; i <= GridSize; i++)
e.Graphics.DrawLine(pen, 40, (i + 1) * 40, GridSize * 40 + 40, (i + 1) * 40);
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];
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);
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);
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;
int StartX = Points.ToArray()[1].X / SizeFactor;
int StartY = Points.ToArray()[1].Y / SizeFactor;
int EndX = Points.ToArray()[0].X / SizeFactor;
int EndY = Points.ToArray()[0].Y / SizeFactor;
if (StartX > GridSize || EndX > GridSize || StartY > GridSize ||
EndY > GridSize ||
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)
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)
.................................
.................................
.................................
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 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.
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)
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)
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)
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)
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)
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)
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)
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)
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 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.
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.
public enum CHEAT_TYPE { INCREASE_TIME, FLASH_WORDS, NONE};
CheatType = CHEAT_TYPE.NONE;
CheatCodeForIncreasingTime += CheatCode;
if (CHEAT_CODE_FOR_INCREASING_TIME.IndexOf(CheatCodeForIncreasingTime) == -1)
CheatCodeForIncreasingTime = (CheatCode);
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)
CheatCodeForFlashUndiscoveredWords = (CheatCode);
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.
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);
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);
}
}
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.