Home  >  Article  >  Backend Development  >  How to use C# to write a complete word puzzle game sample code sharing

How to use C# to write a complete word puzzle game sample code sharing

黄舟
黄舟Original
2017-07-18 10:21:462687browse

Introduction

Word puzzle game, you may have seen it in many puzzle books. It would also be fun to try writing crossword puzzles on your computer using different categories of content and having custom words to play with.

Background

I coded a game using Turbo C a long time ago, but I lost the code. I think it would be great to resurrect it with C#.NET. The language offers a lot of flexibility in terms of memory, GC, and graphics, which I have to be careful about when using C. But by paying explicit attention to C, we can learn a lot (this is why C is called "God's programming language"). On the other hand, because C#.NET takes care of these, I can focus on enhancements elsewhere, such as word orientation, overlap, cheat codes, scoring, encryption, etc. So there needs to be a balance when appreciating two languages.

The reason why I said it is "complete" in the title is as follows:

1) It has some categories of preset words.

2) It saves words and scores in encrypted files so that no one can tamper with the files. If it is tampered with, then it will revert to the default and start scoring from scratch.

3) It has cheat codes, but cheating will be detrimental to the score, and obviously once the cheat is applied it will reset the score to zero.

4) It has a scoring mechanism.

Using Code

The game provides the following functions, which I will discuss in subsequent chapters:

1) Loading categories and words: pre-coded from the program Load words in the device. However, if the player provides custom words, then the game will automatically store all of these (along with the presets) in a file and read them from there.

2) Place on the grid: The game randomly places all words in an 18×18 matrix. The direction can be horizontal, vertical, bottom left and bottom right as shown in the image above.

3) Scoring: For different categories, scores are stored separately. The score is calculated by multiplying the length of the word by a multiplication factor (here 10). At the same time, after all words have been found, the remaining time (multiplied by the multiplication factor) is also added to the score.

4) Show hidden words: If the player still cannot find all the words after time runs out, the game will display the unfound words in different colors.

5) Cheat codes: The game raises cheat codes (mambazamba) on the game board. The cheat code simply sets the time for a full day (86,400 seconds). However, applying a cheat code also applies a penalty that makes the run score zero.

1) Load categories and words:

Load preset

We have a simple class for holding categories and words :

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

We have 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 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|" +
...

We use encryption to write these words in the file. So no one can tamper with the file. For encryption I used a class borrowed from here. Simple to use - you need to pass a string and an encrypted password for encryption. For decryption, you need to pass the encrypted string and password.

If the file exists, then we read the categories and words from there, otherwise we save the preset (and player-defined words) and read from the preset. 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 where the words are stored. It first attempts to decrypt the string read from the file. If it fails (determined by the empty string returned), it will display a message about the problem and then reload from the built-in preset. Otherwise it reads from strings and separates them into categories and words and puts them in a word list. Every 15th word is a category, and subsequent words are 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);
    }
}

Save player’s custom words

The game can provide custom words provided by players. The device is located in the same loading window. Words should be a minimum of 3 characters long and a maximum of 10 characters long, and require 14 words - no more, no less. Instructions are in the label. Also a word cannot be a subpart of any other word. For example: there cannot be two words like 'JAPAN' and 'JAPANESE' because the former is included in the latter.

I'll give you a brief introduction to validity checking. There are 3 on-the-fly checks for maximum length, minimum length and SPACE input (no spaces allowed). This is done by adding our custom handler Control_KeyPress to the EditingControlShowingevent of the word 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 for validity. Complete as follows:

TextBox tb = sender as TextBox;
if (e.KeyChar == (char)Keys.Enter)
{
    if (tb.Text.Length <= MIN_LENGTH)   // Checking length
    {
        MessageBox.Show("Words should be at least " + MAX_LENGTH + " characters long.");
        e.Handled = true;
        return;
    }
}
if (tb.Text.Length >= MAX_LENGTH)   // Checking length
{
    MessageBox.Show("Word length cannot be more than " + MAX_LENGTH + ".");
    e.Handled = true;
    return;
}
if (e.KeyChar.Equals(&#39; &#39;))  // 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);

最后,在输入所有单词并且用户选择保存和使用自定义单词之后存在有效性检查。首先它检查是否输入了14个单词。然后它遍历所有的14个单词,并检查它们是否有无效字符。同时它也检查重复的单词。检查成功就把单词添加到列表中。最后,提交另一次迭代,以检查单词是否包含在另一个单词中(例如,不能有如’JAPAN’和’JAPANESE’这样的两个单词,因为前者包含在后者中)。通过下面的代码完成:

public bool CheckUserInputValidity(DataGridView WordsDataGridView, List<string> WordsByThePlayer)
{
    if (WordsDataGridView.Rows.Count != MAX_WORDS + 1)
    {
        MessageBox.Show("You need to have " + MAX_WORDS + " words in the list. Please add more.");
        return false;
    }

    char[] NoLettersList = { &#39;:&#39;, &#39;;&#39;, &#39;@&#39;, &#39;\&#39;&#39;, &#39;"&#39;, &#39;{&#39;, &#39;}&#39;, &#39;[&#39;, &#39;]&#39;, &#39;|&#39;, &#39;\\&#39;, &#39;<&#39;, &#39;>&#39;, &#39;?&#39;, &#39;,&#39;, &#39;.&#39;, &#39;/&#39;,
                            &#39;`&#39;, &#39;1&#39;, &#39;2&#39;, &#39;3&#39;, &#39;4&#39;, &#39;5&#39;, &#39;6&#39;, &#39;7&#39;, &#39;8&#39;, &#39;9&#39;, &#39;0&#39;, &#39;-&#39;, &#39;=&#39;, &#39;~&#39;, &#39;!&#39;, &#39;#&#39;, &#39;$&#39;,
                            &#39;%&#39;, &#39;^&#39;, &#39;&&#39;, &#39;*&#39;, &#39;(&#39;, &#39;)&#39;, &#39;_&#39;, &#39;+&#39;};   //&#39;
    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: &#39;" + Itm.Cells[0].Value.ToString() + "&#39;");
            return false;
        }
        if (WordsByThePlayer.IndexOf(Itm.Cells[0].Value.ToString()) != -1)
        {
            MessageBox.Show("Can&#39;t have duplicate word in the list. The duplicate word is: &#39;" + Itm.Cells[0].Value.ToString() + "&#39;");
            return false;
        }
        WordsByThePlayer.Add(Itm.Cells[0].Value.ToString());
    }
    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&#39;t have a word as a sub-part of another word. Such words are: &#39;" + WordsByThePlayer[i] + "&#39; and &#39;" + WordsByThePlayer[j] + "&#39;");
                return false;
            }
    }
    return true;
}

玩家的列表与现有单词一起保存,然后游戏板与该类别中的那些单词一起被打开。

2)放在网格上:

在网格上放置单词

单词通过InitializeBoard()方法被放置在网格上。我们在字符矩阵(二维字符数组)WORDS_IN_BOARD中先放置单词。然后我们在网格中映射这个矩阵。遍历所有的单词。每个单词获取随机方向(水平/垂直/左下/右下)下的随机位置。此时,如果我们可视化的话,单词矩阵看起来会有点像下面这样。

放置通过PlaceTheWords()方法完成,获得4个参数——单词方向,单词本身,X坐标和Y坐标。这是一个关键方法,所以我要逐个解释这四个方向。

水平方向

对于整个单词,逐个字符地运行循环。首先它检查这个词是否落在网格之外。如果这是真的,那么它返回到调用过程以生成新的随机位置和方向。

然后,它检查当前字符是否可能与网格上的现有字符重叠。如果发生这种情况,那么检查它是否是相同的字符。如果不是相同的字符,那就返回到调用方法,请求另一个随机位置和方向。

在这两个检查之后,如果放置是一种可能,那么就把单词放置在矩阵中,并且通过方法StoreWordPosition()将列表中的位置和方向存储在WordPositions中。

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] != &#39;\0&#39;)
        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;

垂直/左下/右下方向

相同的逻辑适用于为这3个方向找到单词的良好布局。它们在矩阵位置和边界检查的增量/减量方面不同。

在所有的单词被放置在矩阵中之后,FillInTheGaps()方法用随机字母填充矩阵的其余部分。此时窗体打开并触发Paint()事件。在这个事件上,我们绘制最终显示为40×40像素矩形的线。然后我们将我们的字符矩阵映射到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();

MapArrayToGameBoard()方法简单地把我们的字符矩阵放在board上。我们使用来自MSDN的绘图代码。这遍历矩阵中的所有字符,将它们放置在40×40矩形的中间,边距调整为10像素。

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

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

单词发现和有效性检查

鼠标点击位置和释放位置存储在点列表中。对鼠标按钮释放事件(GameBoard_MouseUp())调用CheckValidity()方法。同时,当用户在左键按下的同时拖动鼠标时,我们从起始位置绘制一条线到鼠标指针。这在GameBoard_MouseMove()事件中完成。

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

单词的有效性在CheckValidity()方法中检查。它通过抓取所有的字母来制定单词,字母通过使用鼠标查看相应的字符矩阵来绘制。然后检查是否真的匹配单词列表中的单词。如果匹配,则通过将单元格着色为浅蓝色并使单词列表中的单词变灰来更新单元格。

以下是抓取行开始和结束位置的代码片段。首先它检查行是否落在边界之外。然后它制定单词并且存储矩阵的坐标。类似地,它检查垂直,左下和右下单词,并尝试相应地匹配。如果这真的匹配,那么我们通过AddCoordinates()方法将临时矩形存储在我们的ColouredRectangles点列表中。

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

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

if (StartX > GridSize || EndX > GridSize || StartY > GridSize || EndY > GridSize || // Boundary checks.
    StartX <= 0 || EndX <= 0 || StartY <= 0 || EndY <= 0)
{
    StatusLabel.Text = "Nope!";
    StatusTimer.Start();
    return;
}

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

3)计分:

对于计分,我们有计分文件。如果缺少,则使用当前分数和类别创建一个。这里,再次,所有的分数被组合在一个大的管道分隔的字符串中,然后该字符串被加密并放入文件。我们有四个实体。

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

最多允许一个类别14个分数。首先加载分数列表中的所有分数,然后获得当前分类分数的排序子集。在该子集中,检查当前分数是否大于任何可用的分数。如果是,则插入当前分数。之后,检查子集数是否超过14,如果超过了,就消除最后一个。所以最后的得分消失了,列表总是有14个分数。这在CheckAndSaveIfTopScore()方法中完成。

这里,再次,如果有人篡改得分文件,那么它只会开始一个新的得分。不允许篡改。

4)显示隐藏的单词:

如果时间用完了,那么游戏用绿色显示单词。首先,获取玩家找不到的单词。可以是这样的

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

然后,遍历这些失败的单词位置并制定相应的失败的矩阵。最后,它通过无效来调用窗体的paint方法。

foreach (string Word in FailedWords)
{
    WordPosition Pos = WordPositions.Find(p => p.Word.Equals(Word));

    if (Pos.Direction == Direction.Horizontal) // Horizontal word.
        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 * 40, j * 40));
    else if (Pos.Direction == Direction.Vertical) // Vertical word.
        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 * 40, j * 40));
    else if (Pos.Direction == 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 * 40, j * 40));
    else if (Pos.Direction == 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 * 40, j * 40));
}
Invalidate();

5)作弊码:

这是一件小事了。这工作在keyup事件上,这个事件抓取所有的击键到CheatCode变量。实际上,我们合并玩家在游戏窗口上输入的击键,并看看代码是否与我们的CHEAT_CODE(mambazamba)匹配。例如,如果玩家按下“m”和“a”,那么我们在CheatCode变量中将它们保持为’ma’(因为,ma仍然匹配cheatcode模式)。类似地,如果它匹配CHEAT_CODE的模式,则添加连续变量。然而,一旦它不能匹配模式(例如,’mambi’),则重新开始。

最后,如果匹配,则激活作弊码(将剩余时间提高到完整一天,即86,400秒),并应用惩罚。

CheatCode += e.KeyCode.ToString().ToUpper();
if (CHEAT_CODE.IndexOf(CheatCode) == -1)    // Cheat code didn&#39;t match with any part of the cheat code.
    CheatCode = ("" + e.KeyCode).ToUpper();                         // Hence erase it to start over.
else if (CheatCode.Equals(CHEAT_CODE) && WORDS_FOUND.Count != MAX_WORDS)
{
    Clock.TimeLeft = 86400;                 // Cheat code applied, literally unlimited time. 86400 seconds equal 1 day.
    ScoreLabel.Text = "Score: 0";
    StatusLabel.Text = "Cheated! Penalty applied!!";
    StatusTimer.Start();
    CurrentScore = 0;
    Invalidate();

这里有趣的是,我们必须使用WordsListView的KeyUp事件而不是窗体。这是因为在加载游戏窗口后,列表框有焦点,而不是窗体。

The above is the detailed content of How to use C# to write a complete word puzzle game sample code sharing. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn