ワードパズルゲーム。多くのパズル本で見たことがあるかもしれません。さまざまなカテゴリのコンテンツを使用して、カスタムの単語を使用して、コンピューター上でクロスワード パズルを作成してみるのも楽しいでしょう。
昔、Turbo C を使用してゲームをコーディングしましたが、コードを紛失してしまいました。 C#.NETで復活したらいいのにと思います。この言語はメモリ、GC、グラフィックスの点で非常に柔軟性があり、C を使用する場合には注意が必要です。しかし、C に明確に注意を払うことで、多くのことを学ぶことができます (これが、C が「神のプログラミング言語」と呼ばれる理由です)。一方、C#.NET がこれらを処理するため、単語の方向、オーバーラップ、チート コード、スコアリング、暗号化など、他の場所の機能強化に集中できます。したがって、2 つの言語を鑑賞するときはバランスを取る必要があります。
タイトルで「完成」と書いた理由は次のとおりです:
1) いくつかのカテゴリーのプリセット単語があります。
2) 単語とスコアを暗号化されたファイルに保存し、誰もファイルを改ざんできないようにします。改ざんされると、デフォルトに戻り、最初からスコアリングが開始されます。
3) チートコードが入っていますが、チートを行うとスコアに悪影響があり、当然チートを適用するとスコアはゼロにリセットされます。
4) 採点メカニズムがあります。
このゲームには次の機能があり、これについては後続の章で説明します:
1) カテゴリと単語のロード: プログラムにハードコーディングされたプリセットから単語をロードします。ただし、プレーヤーがカスタム単語を提供した場合、ゲームはこれらすべてを (プリセットとともに) 自動的にファイルに保存し、そこから読み取ります。
2) グリッド上に配置: ゲームはすべての単語を 18×18 の行列にランダムに配置します。上の図に示すように、方向は水平、垂直、左下、右下です。
3) スコアリング: 異なるカテゴリのスコアは個別に保存されます。スコアは、単語の長さに乗算係数 (ここでは 10) を乗算して計算されます。同時に、すべての単語が見つかった後、残り時間 (倍率を掛けたもの) もスコアに加算されます。
4) 隠された単語を表示: 時間が経過してもプレーヤーがすべての単語を見つけることができない場合、ゲームは見つからない単語を異なる色で表示します。
5) チートコード: ゲームはゲームボード上にチートコード (マンバザンバ) を発行します。チートコードは、単純に丸 1 日の時間を設定します (86,400 秒)。ただし、チートコードを適用するとランスコアが0になるペナルティも適用されます。
プリセットをロードします
カテゴリと単語を保持するための簡単なクラスがあります:
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」のような 2 つの単語は存在できません。前者は後者に含まれるためです。
妥当性チェックについて簡単に説明します。最大長、最小長、および SPACE 入力 (スペースは許可されません) について 3 つのオンザフライ チェックがあります。これを行うには、カスタム ハンドラー Control_KeyPress を単語入力グリッドの EditingControlShowing イベントに追加します。
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; }
玩家的列表与现有单词一起保存,然后游戏板与该类别中的那些单词一起被打开。
在网格上放置单词
单词通过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)); }
对于计分,我们有计分文件。如果缺少,则使用当前分数和类别创建一个。这里,再次,所有的分数被组合在一个大的管道分隔的字符串中,然后该字符串被加密并放入文件。我们有四个实体。
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()方法中完成。
这里,再次,如果有人篡改得分文件,那么它只会开始一个新的得分。不允许篡改。
如果时间用完了,那么游戏用绿色显示单词。首先,获取玩家找不到的单词。可以是这样的
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();
这是一件小事了。这工作在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# を使用して完全なワード パズル ゲームを作成する方法のサンプル コードの共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。