概述
介绍
字谜游戏,可能你在许多益智书中都曾看到过。试着在电脑上用不同类别的内容写字谜游戏,并且有自定义字词去玩也是很有意思的。
背景
我很早以前使用Turbo C编码游戏,但我丢失了代码。我觉得用C#.NET让它复活将是一件很伟大的事情。该语言在内存、GC、图形方面提供了很多灵活性,而这些是我在使用C语言的时候必须小心处理的。但是在C语言中的明确关注,会让我们学到很多(这就是为什么C语言被称为“上帝的编程语言”的原因)。另一方面,因为C#.NET照顾到了这些,所以我可以专注于其他地方的增强,例如字的方向,重叠,作弊码,计分,加密等。所以在欣赏两种语言的时候需要有一个平衡。
在题目中我之所以说它是“完整的”,原因如下:
1)它有一些类别的预设词。
2)它在加密文件中保存单词和分数,这样就没有人可以篡改文件。如果要篡改,那么它将恢复到预设并从头开始计分。
3)它有作弊码,但作弊会不利于得分,且显然作弊一旦应用会使分数归零。
4)它有一个计分机制。
使用代码
游戏提供以下功能,具体我将在随后的章节中讨论:
1)载入类别和单词:从程序中硬编码的预设中加载单词。然而,如果玩家提供自定义的单词,那么游戏将自动把所有这些(连同预设)存储在文件中并从那里读取。
2)放在网格上:游戏将所有的单词随机地放在18×18的矩阵中。方向可以是水平,垂直,左下和右下,如上图中所示。
3)计分:对于不同类别,分数单独存储。分数的计算方式是单词的长度乘以乘法因子(这里为10)。与此同时,在找到所有的单词之后,剩余时间(乘以乘法因子)也会加到分数中。
4)显示隐藏的单词:如果时间用完之后,玩家依然找不到所有的单词,那么游戏会用不同的颜色显示没找到的单词。
5)作弊码:游戏在游戏板上提作弊码(mambazamba)。作弊码只简单地设置了一整天的时间(86,400秒)。但是,应用作弊码也会应用让此次运行的计分为零的惩罚。
1)载入类别和单词:
载入预设
我们有一个简单的用于持有类别和单词的类:
class WordEntity
{
public string Category { get; set; }
public string Word { get; set; }
}
登录后复制
我们有一些预设的类别和单词如下。预设都是管道分隔的,其中每第15个单词是类别名称,后面的单词是该类别中的单词。
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|" +
...
登录后复制
我们使用加密在文件中写这些单词。所以没有人可以篡改文件。对于加密我使用了一个从这里借鉴的类。使用简单——你需要传递字符串和用于加密的加密密码。对于解密,你需要传递加密的字符串和密码。
如果文件存在,那么我们从那里读取类别和单词,否则我们保存预设(以及玩家自定义的单词)并从预设那里读取。这在下面的代码中完成:
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();
}
登录后复制
ReadFromFile()方法简单地从存储单词的文件中读取。它首先尝试解密从文件读取的字符串。如果失败(由返回的空白字符串确定),它将显示关于问题的一条消息,然后从内置预设重新加载。否则它从字符串读取并将它们分成类别和单词,并把它们放在单词列表中。每第15个词是类别,后续词是该类别下的单词。
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);
}
}
登录后复制
保存玩家的自定义词
游戏可供应由玩家提供的自定义词。设备位于相同的加载窗口。单词应该最少3个字符长,最多10个字符长,并且需要14个单词——不多也不能不少。指示在标签中。另外单词不能是任何其他词的子部分。例如:不能有如’JAPAN’和’JAPANESE’这样两个词,因为前者包含在后者中。
我将简要介绍一下有效性检查。有3个关于最大长度、最小长度和SPACE输入(不允许空格)的即时检查。这通过将我们自定义的处理程序Control_KeyPress添加到单词条目网格的EditingControlShowingevent中来完成。
private void WordsDataGridView_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
e.Control.KeyPress -= new KeyPressEventHandler(Control_KeyPress);
e.Control.KeyPress += new KeyPressEventHandler(Control_KeyPress);
}
登录后复制
每当用户输入东西时,处理程序就被调用并检查有效性。完成如下:
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(' ')) // 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 = { ':', ';', '@', ''', '"', '{', '}', '[', ']', '|', '\', '<', '>', '?', ',', '.', '/',
'`', '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.
{
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;
}
登录后复制
玩家的列表与现有单词一起保存,然后游戏板与该类别中的那些单词一起被打开。
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] != '