[Game Maker Adventure] Chương Check Point II: Boss Battle!

Real Boss Battle! Kiểm tra năng lực của bạn nào!

Tóm tắt chương trước

Bạn và Kurobo tiếp tục đi tìm kiếm trong di tích.

Hiện giờ, miễn không phải lũ goblin cao cấp thì chúng chẳng thể làm gì nổi chiến thuật du kích của bạn.

Chú ý

  • Chương check point là một chương rất quan trọng để bạn biết khả năng của mình tới đâu.
  • Độ khó của các chương Check Point sẽ khác hẳn so với các chương khác. Hãy dành thời gian suy nghĩ thấu đáo chúng.
  • Để vượt qua chương này, ngoài kiến thức từ các chương trước và các Act trước, bạn sẽ cần một chút sáng tạo nữa.

Cốt truyện

“Trời ạ, đường cụt rồi…”

“Chủ nhân, chủ nhân!! Hình như sau bức tường này là một căn phòng ẩn!” – Kurobo reo to.

“Gì cơ!? Liệu có phải phòng chứa bảo vật không nhỉ?”

Bạn lúc này mới háo hức quan sát bức tường phía trước.

Dây leo và rêu xanh đã phủ lên hầu hết bức tường, bạn sử dụng cây kiếm đã sứt mẻ của mình khéo léo cắt hết chúng.

Phía sau, một bệ đá hiện ra, cảnh này rất quen thuộc.

“Lại phải sáng tạo ma thuật để mở cừa à?”

Bạn thở dài.

“Kurobo không quen thuộc với thứ này cho lắm… nhưng có một cách…” – Kurobo tươi tỉnh nói.

“Này, l-làm gì vậy!?”

Bạn nhìn Kurobo đang tụ năng lượng.

“^o^”

*Bùm*

Một tiếng nổ lớn vang lên kèm theo tiếng cơ hệ nào đó chạy dọc trên trần nhà. Kèm với đó, một cánh cửa đột ngột xuất hiện phía sau, nhốt bạn và Kurobo lại.

Và nó… bắt đầu di chuyển.

“…” – Kurobo.

“…” – Bạn.

Check point: Mê cung ảo

“Gyaaaaaaa@aaa#!@#*(^$!@$”

Bạn và Kurobo hét toáng. Hai người cố gắng đẩy lại bức tường đang ép lại nhưng dường như không có hiệu quả.

“Chủ nhân, em xin lỗi…” – Kurobo nằm ra sàn.

“…”

Bạn bình tĩnh lại, nhìn chằm chằm vào bệ đá trước mặt.

Phải giải nó thì mới có cơ hội sống sót!

Quest: Mê cung ảo* ✦✦✦✦✦

Quest có độ khó ✦✦✦✦✦ rất khó. Hãy dành thời gian cho nó, cho dù bạn không nghĩ ra nhưng chính việc bạn ngồi nghĩ sẽ giúp bạn tư duy sau này rất nhiều!

Đằng sau bức tường là một mê cung luôn biến đổi. Bạn chỉ có thể đi tiếp nếu có thể tự tạo ra con đường của mình.

Hãy viết chương trình thoả mãn (xem thêm hình mô tả bên dưới):

  1. Tạo ra một mê cung bằng seed. Mỗi seed khác nhau sẽ tạo ra một mê cung khác nhau. Cùng 1 seed sẽ luôn tạo ra 1 mê cung giống nhau. (Hình 1).
  2. Mê cung được chia thành các phòng. Mỗi phòng sẽ có Rương hoặc Quái Vật. Các căn phòng phải thông với nhau (Hình 2).
  3. Bạn biết điểm xuất phát của mình, điểm này đánh dấu màu xanh. Kí hiệu là “S”.
  4. Rương là “R”. Điểm này đánh dấu màu vàng.
  5. Quái vật sẽ điền số. Số càng lớn quái càng mạnh.
  6. Ngoài ra, những nơi không được đi (tường) sẽ kí hiệu là #.
  7. Xem thêm mô tả bằng hình và video bên dưới.
  8. Cho những người học lại: Chỉ được dùng kiến thức đã học từ chương 1 tới giờ.

Video mô tả nội dung Mê cung ảo. Liệu bạn có thể làm được!?
Hình 1: Mỗi seed (dạng số) sẽ cho ra một loại kết cấu mê cung và nội dung bên trong khác nhau, cùng 1 seed sẽ cho ra kết quả giống nhau.
Hình 2: Kí hiệu của từng phần trong mê cung. Mỗi seed sẽ tạo ra một mê cung khác nhau.

Chủ nhân, em xin lỗi…

Gợi ý của Kurobo?????

Xem đáp án (độ khó càng lớn cost càng lớn)

[mycred_sell_this]

Chia nhỏ đề bài

Với những dạng bài rắc rối như thế này, việc đầu tiên cần làm là chia nhỏ đề bài.

Dùng seed để tạo ra một mê cung.

Đề bài gồm 2 phần:

  1. Seed
  2. Mê cung

Tiếp tục chia nhỏ các phần trên thành những phần nhỏ hơn.

1. Seed

Với seed, đặc tính của nó là với mỗi seed khác nhau sẽ tạo ra một kết quả khác nhau. Cùng một seed sẽ cho ra kết quả giống nhau.

Seed có thể là dạng số, dạng chữ, dạng hình… tuỳ chương trình. Trong hầu hết các game, seed đều dùng một dãy số. Ta cũng chọn theo như vậy luôn.

Seed để tạo ra các biến số ngẫu nhiên nhưng có qui luật theo cách mà ta đặt.

Thứ biến số trong bài toán là vị trí các phòngnội dung trong căn phòng.

Vậy, ta sử dụng seed để điều khiển 2 thứ này.

1.1. Vị trí các phòng

Các phòng được tạo trên một không gian 2 chiều, không có đường chéo. Bài toán lúc này chỉ đơn giản là tạo phòng trong lưới 2D.

Ta hoàn toàn có thể rải vị trí các căn phòng một cách ngẫu nhiên trên lưới 2D trên, nhưng điểm khó của bài toán là phải làm sao cho các căn phòng thông với nhau.

Lúc này, bắt buộc việc tạo phòng phải có qui luật thoả mãn đề bài.

Qui tắc thoả mãn đề bài được chọn ra như sau: đầu tiên, chọn 1 toạ độ bất kì. Sau đó di chuyển từ toạ độ đó Lên, Xuống, Trái hoặc Phải 1 đơn vị.

Console.WriteLine("Enter your seed: ");
double seed = Convert.ToDouble(Console.ReadLine());

int eventNumber = 9;

double[] wallEvent = new double[eventNumber];
for (int i = 0; i < eventNumber; i++)
{
    wallEvent[i] = (seed + seed % (10 * i + 1)) % 4;
}

eventNumber là số căn phòng lớn nhất có thể tồn tại trong mê cung.

Bằng cách tạo ra array wallEvent được biến đổi từ seed ban đầu, ta có một dãy qui định cách di chuyển toạ độ phòng. Với 0 là đi lên, 1 là sang phải, 2 là xuống, 3 là sang trái.

Ta sửa thêm một số chỗ để tránh trường hợp quay đi quay lại chỗ cũ liên tục. Đoạn code như sau:

Console.WriteLine("Enter your seed: ");
double seed = Convert.ToDouble(Console.ReadLine());

int eventNumber = 9;

double[] wallEvent = new double[eventNumber];
for (int i = 0; i < eventNumber; i++)
{
    wallEvent[i] = (seed + seed % (10 * i + 1)) % 4;
    if (i > 0 && wallEvent[i] == 2 && wallEvent[i - 1] == 0)
    {
        wallEvent[i] = 1;
    }
    else if (i > 0 && wallEvent[i] == 0 && wallEvent[i - 1] == 2)
    {
        wallEvent[i] = 1;
    }
    else if (i > 0 && wallEvent[i] == 3 && wallEvent[i - 1] == 1)
    {
        wallEvent[i] = 0;
    }
    else if (i > 0 && wallEvent[i] == 1 && wallEvent[i - 1] == 3)
    {
        wallEvent[i] = 0;
    }
}

int genMazeX = 0;
int genMazeY = 0;

int rangeMazeXRight = 0;
int rangeMazeXLeft = 0;
int rangeMazeYTop = 0;
int rangeMazeYBot = 0;

int[] genMazeXList = new int[eventNumber + 1];
int[] genMazeYList = new int[eventNumber + 1];
genMazeXList[0] = 0;
genMazeYList[0] = 0;

for (int i = 0; i < wallEvent.Length; i++)
{
    switch (wallEvent[i])
    {
        case 0:
            Console.WriteLine("Up");
            genMazeY++;
            if (genMazeY > 0 && genMazeY > rangeMazeYTop)
            {
                rangeMazeYTop = genMazeY;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 1:
            Console.WriteLine("Right");
            genMazeX++;
            if (genMazeX > 0 && genMazeX > rangeMazeXRight)
            {
                rangeMazeXRight = genMazeX;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 2:
            Console.WriteLine("Down");
            genMazeY--;
            if (genMazeY < 0 && genMazeY < rangeMazeYBot)
            {
                rangeMazeYBot = genMazeY;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 3:
            Console.WriteLine("Left");
            genMazeX--;
            if (genMazeX < 0 && genMazeX < rangeMazeXLeft)
            {
                rangeMazeXLeft = genMazeX;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        default:
            break;
    }
}

void UpdateGenMaze(int genMazeX, int genMazeY, int[] genMazeXList, int[] genMazeYList, int i)
{
    genMazeXList[i] = genMazeX;
    genMazeYList[i] = genMazeY;

    //debug
    Console.WriteLine("X: " + genMazeXList[i]);
    Console.WriteLine("Y: " + genMazeYList[i]);
}

Ở đoạn code bên trên, ta đã tách từng phần tử của wallEvent sẽ tương ứng với 1 toạ độ X và Y trong genMazeXList và genMazeYList.

Đoạn code lúc này sẽ cho ra kết quả một danh sách vị trí căn phòng:

Enter your seed:
23238582
Down
X: 0
Y: -1
Down
X: 0
Y: -2
Right
X: 1
Y: -2
Up
X: 1
Y: -1
Right
X: 2
Y: -1
Down
X: 2
Y: -2
Right
X: 3
Y: -2
Up
X: 3
Y: -1
Up
X: 3
Y: 0

Việc tiếp theo là cộng toạ độ để tránh những trường hợp toạ độ âm.

//... them vao ben duoi doan code tren

for (int i = 0; i < genMazeXList.Length; i++)
{
    if (rangeMazeXLeft < 0)
    {
        genMazeXList[i] += rangeMazeXLeft * -1;
    }
}
for (int i = 0; i < genMazeYList.Length; i++)
{
    if (rangeMazeYBot < 0)
        genMazeYList[i] += rangeMazeYBot * -1;
}

1.2. Nội dung căn phòng

Để đơn giản, ta sử dụng luôn các con số định hướng để quyết định nội dung căn phòng.

Array wallEvent chính là key của đoạn code, wallEvent[0] là điểm xuất phát. Rương là phần tử có giá trị bằng 0. Còn lại 3 giá trị 1, 2, 3 sẽ là vị trí của kẻ địch.

Như vậy, wallEvent chứa 2 thông tin quan trọng: cách khởi tạo danh sách phòng và nội dung căn phòng.

2. Mê cung

Ta phải vẽ mê cung trên console app. Có rất nhiều kiểu vẽ khác nhau, bạn có thể tự sáng tạo riêng cho mình.

Đoạn code để vẽ như sau:

string wallTop = "+---";
string wallBot = "+---";
string wallTopEnd = "+";
string wallBotEnd = "+";
string wallEnd = "|";

int mazeX = 5;
int mazeY = 3;

//top wall
for (int i = 0; i < mazeX; i++)
{
    Console.Write(wallTop);
}
Console.Write(wallTopEnd);

//for wall
for (int i = 0; i < mazeY; i++)
{
    Console.WriteLine();
    for (int j = 0; j < mazeX; j++)
    {
        Console.Write($"| # ");
    }
    Console.Write(wallEnd);
    Console.WriteLine();
    for (int j = 0; j < mazeX; j++)
    {
        Console.Write(wallBot);
    }
    Console.Write(wallBotEnd);
}

Chỉ cần nhập độ rộng mazeX và độ cao mazeY của mê cung thì kết quả sẽ được một mê cung rất gọn gàng như thế này:

+---+---+---+---+---+
| # | # | # | # | # |
+---+---+---+---+---+
| # | # | # | # | # |
+---+---+---+---+---+
| # | # | # | # | # |
+---+---+---+---+---+

Hoàn thiện

Bây giờ, nhiệm vụ cuối cùng chỉ là kết nối 2 mục lại với nhau: seed và mê cung. Đoạn code hoàn thiện như sau:

Console.WriteLine("Enter your seed: ");
double seed = Convert.ToDouble(Console.ReadLine());

int eventNumber = 9;

double[] wallEvent = new double[eventNumber];
for (int i = 0; i < eventNumber; i++)
{
    wallEvent[i] = (seed + seed % (10 * i + 1)) % 4;
    if (i > 0 && wallEvent[i] == 2 && wallEvent[i - 1] == 0)
    {
        wallEvent[i] = 1;
    }
    else if (i > 0 && wallEvent[i] == 0 && wallEvent[i - 1] == 2)
    {
        wallEvent[i] = 1;
    }
    else if (i > 0 && wallEvent[i] == 3 && wallEvent[i - 1] == 1)
    {
        wallEvent[i] = 0;
    }
    else if (i > 0 && wallEvent[i] == 1 && wallEvent[i - 1] == 3)
    {
        wallEvent[i] = 0;
    }
}

int genMazeX = 0;
int genMazeY = 0;

int rangeMazeXRight = 0;
int rangeMazeXLeft = 0;
int rangeMazeYTop = 0;
int rangeMazeYBot = 0;

int[] genMazeXList = new int[eventNumber + 1];
int[] genMazeYList = new int[eventNumber + 1];
genMazeXList[0] = 0;
genMazeYList[0] = 0;

for (int i = 0; i < wallEvent.Length; i++)
{
    switch (wallEvent[i])
    {
        case 0:
            //Console.WriteLine("Up");
            genMazeY++;
            if (genMazeY > 0 && genMazeY > rangeMazeYTop)
            {
                rangeMazeYTop = genMazeY;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 1:
            //Console.WriteLine("Right");
            genMazeX++;
            if (genMazeX > 0 && genMazeX > rangeMazeXRight)
            {
                rangeMazeXRight = genMazeX;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 2:
            //Console.WriteLine("Down");
            genMazeY--;
            if (genMazeY < 0 && genMazeY < rangeMazeYBot)
            {
                rangeMazeYBot = genMazeY;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        case 3:
            //Console.WriteLine("Left");
            genMazeX--;
            if (genMazeX < 0 && genMazeX < rangeMazeXLeft)
            {
                rangeMazeXLeft = genMazeX;
            }
            UpdateGenMaze(genMazeX, genMazeY, genMazeXList, genMazeYList, i);
            break;
        default:
            break;
    }
}

for (int i = 0; i < genMazeXList.Length; i++)
{
    if (rangeMazeXLeft < 0)
    {
        genMazeXList[i] += rangeMazeXLeft * -1;
    }
}
for (int i = 0; i < genMazeYList.Length; i++)
{
    if (rangeMazeYBot < 0)
        genMazeYList[i] += rangeMazeYBot * -1;
}

int mazeX, mazeY;
mazeX = rangeMazeXLeft * -1 + rangeMazeXRight + 1;
mazeY = rangeMazeYBot * -1 + rangeMazeYTop + 1;

//debug
//Console.WriteLine("MazeX Left" + rangeMazeXLeft);
//Console.WriteLine("MazeX Right" + rangeMazeXRight);
//Console.WriteLine("MazeY Bot" + rangeMazeYBot);
//Console.WriteLine("MazeY Top" + rangeMazeYTop);

//for (int i = 0; i < wallBEvent.Length; i++)
//{
//    Console.WriteLine("---");
//    Console.WriteLine(i);
//    Console.WriteLine(genMazeXList[i]);
//    Console.WriteLine(genMazeYList[i]);
//}

string wallTop = "+---";
string wallBot = "+---";
string wallTopEnd = "+";
string wallBotEnd = "+";
string wallEnd = "|";

//top wall
for (int i = 0; i < mazeX; i++)
{
    Console.Write(wallTop);
}
Console.Write(wallTopEnd);

//for wall
for (int i = 0; i < mazeY; i++)
{
    Console.WriteLine();
    for (int j = 0; j < mazeX; j++)
    {
        bool isEvent = false;
        for (int k = 0; k < wallEvent.Length; k++)
        {
            if (genMazeXList[k] == j && genMazeYList[k] == i && !isEvent)
            {
                if (k == 0)
                {
                    Console.Write("| ");
                    Console.BackgroundColor = ConsoleColor.Green;
                    Console.ForegroundColor = ConsoleColor.Black;
                    Console.Write("S");
                    Console.BackgroundColor = ConsoleColor.Black;
                    Console.ForegroundColor = ConsoleColor.Gray;
                    Console.Write(" ");
                }
                else
                {
                    switch (wallEvent[k])
                    {
                        case 0:
                            Console.Write("| ");
                            Console.BackgroundColor = ConsoleColor.Yellow;
                            Console.ForegroundColor = ConsoleColor.Black;
                            Console.Write("R");
                            Console.BackgroundColor = ConsoleColor.Black;
                            Console.ForegroundColor = ConsoleColor.Gray;
                            Console.Write(" ");
                            break;
                        default:
                            Console.Write($"| {wallEvent[k]} ");
                            break;
                    }
                }
                isEvent = true;
            }
        }
        if (!isEvent)
        {
            Console.Write($"| # ");
        }
    }
    Console.Write(wallEnd);
    Console.WriteLine();
    for (int j = 0; j < mazeX; j++)
    {
        Console.Write(wallBot);
    }
    Console.Write(wallBotEnd);
}






void UpdateGenMaze(int genMazeX, int genMazeY, int[] genMazeXList, int[] genMazeYList, int i)
{
    genMazeXList[i] = genMazeX;
    genMazeYList[i] = genMazeY;

    //debug
    //Console.WriteLine("X: " + genMazeXList[i]);
    //Console.WriteLine("Y: " + genMazeYList[i]);
}

Cách giải của mình chỉ để tham khảo. Trong lập trình, để giải quyết 1 vấn đề có rất nhiều cách khác nhau, vì thế nếu cách giải của bạn không giống mẫu thì cũng đừng lo lắng quá.

Nếu thấy có thắc mắc, xin hãy comment ngay xuống bên dưới hoặc báo bên discord nhé.

[/mycred_sell_this]

Chương Trước | Tổng quan | Chương Kế

Kurrwo

Công nhân👷gõ 💬 văn bản ⌨️ trò chơi 🎮 điện tử 📱 👏👏👏

Post navigation