引见
字谜游戏,能够你在许多益智书中都曾看到过。试着在电脑上用差别种别的内容写字谜游戏,而且有自定义字词去玩也是很有意义的。
背景
我很早以前运用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] != '\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;
垂直/左下/右下方向
雷同的逻辑适用于为这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] != '\0') { 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'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事宜而不是窗体。这是由于在加载游戏窗口后,列表框有核心,而不是窗体。
以上就是怎样运用C#来编写的一个完全字谜游戏的示例代码分享的细致内容,更多请关注ki4网别的相干文章!