>  기사  >  백엔드 개발  >  C#을 사용하여 완전한 단어 퍼즐 게임을 작성하는 방법 샘플 코드 공유

C#을 사용하여 완전한 단어 퍼즐 게임을 작성하는 방법 샘플 코드 공유

黄舟
黄舟원래의
2017-07-18 10:21:462738검색

소개

단어 퍼즐 게임은 많은 퍼즐 책에서 본 적이 있을 것입니다. 다양한 카테고리의 콘텐츠와 사용자 정의 단어를 사용하여 컴퓨터에서 크로스워드 퍼즐을 작성해 보는 것도 재미있을 것입니다.

Background

오래전에 Turbo C를 사용하여 게임을 코딩했는데 코드를 잃어버렸습니다. C#.NET으로 부활시키면 정말 좋을 것 같아요. C언어는 메모리, GC, 그래픽 측면에서 많은 유연성을 제공하는데, C를 사용할 때 주의해야 할 점입니다. 그러나 C에 명시적으로 주의를 기울임으로써 우리는 많은 것을 배울 수 있습니다(이것이 C가 "신의 프로그래밍 언어"라고 불리는 이유입니다). 반면에 C#.NET에서는 이러한 부분을 처리해주기 때문에 단어 방향, 중복, 치트 코드, 채점, 암호화 등과 같은 다른 부분의 개선 사항에 집중할 수 있습니다. 따라서 두 언어를 감상할 때는 균형이 필요합니다.

제목에 '완료'라고 말한 이유는 다음과 같습니다.

1) 사전 설정된 단어 카테고리가 있습니다.

2) 누구도 파일을 함부로 조작할 수 없도록 단어와 점수를 암호화된 파일로 저장합니다. 변조된 경우 기본값으로 돌아가서 처음부터 채점을 시작합니다.

3) 치트 코드가 있지만 치트를 하면 점수에 해로울 수 있으며, 분명히 치트가 적용되면 점수가 0으로 재설정됩니다.

4) 채점 메커니즘이 있습니다.

코드 사용

이 게임은 다음 기능을 제공하며 이에 대해서는 다음 장에서 설명하겠습니다.

1) 카테고리 및 단어 로드: 프로그램에 하드 코딩된 사전 설정에서 단어를 로드합니다. 그러나 플레이어가 사용자 정의 단어를 제공하면 게임은 자동으로 이러한 모든 단어(사전 설정과 함께)를 파일에 저장하고 거기에서 읽습니다.

2) 그리드 위에 배치: 게임은 모든 단어를 18×18 매트릭스에 무작위로 배치합니다. 방향은 위 이미지와 같이 가로, 세로, 왼쪽 아래, 오른쪽 아래가 될 수 있습니다.

3) 채점: 카테고리별로 점수가 별도로 저장됩니다. 점수는 단어의 길이에 곱셈 인자(여기서는 10)를 곱하여 계산됩니다. 동시에 모든 단어를 찾은 후 남은 시간(곱셈 요소를 곱함)도 점수에 추가됩니다.

4) 숨겨진 단어 표시: 시간이 지난 후에도 플레이어가 여전히 모든 단어를 찾을 수 없는 경우, 게임에서는 찾을 수 없는 단어를 다른 색상으로 표시합니다.

5) 치트 코드: 게임은 게임 보드에 치트 코드(mambazamba)를 올립니다. 치트 코드는 단순히 하루(86,400초)에 대한 시간을 설정합니다. 그러나 치트 코드를 적용하면 실행 점수가 0이 되는 페널티도 적용됩니다.

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'는 전자가 후자에 포함되므로 두 단어가 있을 수 없습니다.

유효성 검사에 대해 간략하게 소개하겠습니다. 최대 길이, 최소 길이 및 SPACE 입력(공백은 허용되지 않음)에 대한 3가지 즉석 검사가 있습니다. 이는 단어 입력 그리드의 EditingControlShowing 이벤트에 사용자 정의 핸들러 Control_KeyPress를 추가하여 수행됩니다.

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(&#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事件而不是窗体。这是因为在加载游戏窗口后,列表框有焦点,而不是窗体。

위 내용은 C#을 사용하여 완전한 단어 퍼즐 게임을 작성하는 방법 샘플 코드 공유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.