用 C#.NET 編寫的一個完整字謎遊戲

介紹

字謎遊戲,可能你在許多益智書中都曾看到過。試着在電腦上用不同類別的內容寫字謎遊戲,並且有自定義字詞去玩也是很有意思的。

背景

我很早以前使用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 { getset; }    public string Word { getset; }}

我們有一些預設的類別和單詞如下。預設都是管道分隔的,其中每第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;        Categories.Add(Category);    }    else    {        WordEntity Word = new WordEntity();        Word.Category = Category;        Word.Word = DecryptedWords;        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 == nullcontinue;        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;        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 + "' 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)   // 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;    StoreWordPosition(Word, PlacementIndex_X, PlacementIndex_Y, OrientationDecision);    return true;}break;

垂直/左下/右下方向
相同的邏輯適用於爲這3個方向找到單詞的良好佈局。它們在矩陣位置和邊界檢查的增量/減量方面不同。
在所有的單詞被放置在矩陣中之後,FillInTheGaps()方法用隨機字母填充矩陣的其餘部分。此時窗體打開並觸發Paint()事件。在這個事件上,我們繪製最終顯示爲40×40像素矩形的線。然後我們將我們的字符矩陣映射到board上。

Pen pen = new Pen(Color.FromArgb(255000));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) * 4040, (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 == 1return// 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 { getset; }    public string Scorer { getset; }    public int Score { getset; }    public DateTime ScoreTime { getset; }............................

最多允許一個類別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事件而不是窗體。這是因爲在加載遊戲窗口後,列表框有焦點,而不是窗體。

環境

使用Visual Studio 2015 IDE編碼。這不是一個移動版本——需要PC電腦來玩這個遊戲。

免責聲明

這不是一個OOP項目,它遵循程序性編程,雖然有些地方應用了OOP並且將進程委託給類對象。項目是用RAD方法完成的,以便使事情進行。它需要重構。此外,我沒有遵循任何命名約定。我個人的偏好是名字能夠說明意圖,在將鼠標懸停在名字上面的時候,你可以自動地瞭解類型。我的意思是,變量’TheWordIntended’根據我沒有遵循的標準命名約定應該有一個像’strTheWordIntended’這樣的名字。這些都是可以被重構的。

未來的工作

有很多事情可以做——應用設計模式,OOP。作爲軟件開發的本質,重構是必須的。

興趣點

要強制重繪窗口,我們需要調用窗口的Invalidate()方法。也需要通過調整表單頂部和左側位置來校正鼠標座標。有趣的是,表單的座標定義爲:X爲距離屏幕頂部的距離,Y爲距離屏幕左側的距離。但是,鼠標座標用另一種方式定義:X爲距離窗口左邊的距離,Y作爲距離窗口頂部的距離。因此,爲了校準,我們需要仔細調整。

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        }    }瑕疵

我發現了一個小問題,如果一臺機器有多個監測的話。如果遊戲在一個窗口中加載,並且它被移動到另一個窗口的話,則鼠標拖動在第一個窗口上保持疤痕標記。不必恐慌,它在遊戲關閉後擦除。

bug

到目前爲止沒有發現任何bug。如果你發現bug的話,歡迎留言。也歡迎提出你的意見。

概要

這是一個字母拼圖遊戲,可預設單詞,自定義單詞詞,對單個詞類別計分。

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章