找洞鑽

\\\景資成發///

一仁徐以潔 一平廖子綺 一誠吳怡宣

Process

遊戲發想

徐以潔 廖子綺 吳怡宣

IDEATION

During the ideation phase, expect to discuss the project in depth to clearly understand the goals and requirements.

IDEATION

During the ideation phase, expect to discuss the project in depth to clearly understand the goals and requirements.

IDEATION

During the ideation phase, expect to discuss the project in depth to clearly understand the goals and requirements.

分工表

遊戲發想:徐以潔 廖子綺 吳怡宣

程式設計:徐以潔

簡報:廖子綺

報告:徐以潔 廖子綺 吳怡宣

遊戲介紹

遊戲試玩

完整程式碼

程式碼(完整版):

#include <iostream>
#include <cstdio>    // For puts
#include <conio.h>   // For _kbhit and _getch
#include <windows.h> // For Sleep and console functions (gotoxy, SetConsoleCursorPosition)

#include <ctime>     // For time(nullptr) in srand
#include <cstdlib>   // For srand, rand
#include <vector>    // Using vector<string> for easier map manipulation
#include <string>    // Required for std::string
#include <thread>    // For std::this_thread::sleep_for
#include <chrono>    // For std::chrono::milliseconds
#include <io.h>      // For _setmode
#include <fcntl.h>   // For _O_U8TEXT

using namespace std;

// 使用 vector<string> 比 char[][] 更方便且安全地操作地圖。
// 定義地圖尺寸
const int MAP_H = 8; // 地圖高度,從 0 到 7
const int MAP_W = 22; // 地圖寬度,從 0 到 21

// 初始地圖設定 - 注意:這裡將玩家 'o' 移除,會在 resetGame 中放置
vector<wstring> play_map = {
    L"######################",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #", // 原本的玩家位置現在也初始化為空白
    L"######################"  // 最後一行保持為牆壁
};

struct Player {// 玩家
    int x;
    int y;
} player;

bool game_over = false;// 控制遊戲狀態
int final_score = 0; // 用於儲存最終分數

void welcome()
{
	wcout << L"\n\n\t\t\t找  洞  鑽";
	wcout << L"\n\n\t\t      請按任意鍵開始";
	wcout << L"\n\n\t\t      按F可結束遊戲";
	getch();//得到任意鍵
	system("cls");//清空螢幕
	SetConsoleTitleW(L"找洞鑽");//設置窗口標題
}

void gotoxy(int x, int y) {// 設定游標位置,讓畫面更新更流暢
    COORD coord;
    coord.X = x;
    coord.Y = y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);// 將游標移動到指定的位置
}

void drawMap() {
    gotoxy(0, 0); // 將游標移動到左上角,以便重新繪製整個地圖
    for (int i = 0; i < MAP_H; ++i) {
        wcout << play_map[i] << endl;
    }
}

void reset() {// 重置遊戲地圖和玩家位置
    play_map = {
        L"######################",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"######################"
    };
    player.x = 11; // 玩家 X 座標重置到中心
    player.y = 6;  // 玩家總是在y=6上
    play_map[player.y][player.x] = L'o'; // 只在這裡放置玩家 'o'
    game_over = false; // 重置遊戲時,將遊戲結束狀態設為 false
    final_score = 0;   // 重置分數
}

void xxxyyy() {// 生成新的障礙物

    for (int i = 1; i < MAP_W - 1; ++i) {// 先都設定為障礙物 'x'
        play_map[1][i] = L'x';
    }

    int hole_w = rand() % 2 + 3; // 隨機生成 3 或 4 格寬的洞,因為%2

    // 計算洞的起始位置範圍
    int max_hole_start_x = (MAP_W - 1) - hole_w;// 確保出現在遊戲範圍裡
    int hole_start_x = rand() % max_hole_start_x + 1;// +1是因為從1開始

    for (int i = 0; i < hole_w; ++i) {// 挖出第一個洞
        play_map[1][hole_start_x + i] = L' ';
    }

    if (rand() % 10 < 3) { // 30% 機率生成另外的洞
        int an_hole_w = rand() % 2 + 1; // 1 或 2 格寬
        int an_hole_start_x;
        bool v_found = false;// 還沒找到適合的地方

        // 嘗試找一個不重疊的位置
        for(int attempt = 0; attempt < 10; ++attempt) { // 嘗試幾次
            an_hole_start_x = rand() % (MAP_W - 1 - an_hole_w) + 1;
            bool overlap = false; // 沒有重疊到
            for(int i = 0; i < an_hole_w; ++i) {
                if (play_map[1][an_hole_start_x + i] == ' ') { // 如果新洞的位置已經是空格,說明重疊了
                    overlap = true; //有重疊
                    break;
                }
            }
            if (!overlap) { // 沒有重疊
                v_found = true; // 找到地方了
                break;
            }
        }
        if (v_found) {
            for (int i = 0; i < an_hole_w; ++i) {
                play_map[1][an_hole_start_x + i] = L' ';
            }
        }
    }
}

// 移動所有障礙物向下,並計算分數
bool moveDown(int& current_score) { // 傳入分數引用(&=傳入分數本身不是分身)
    bool ya = false; // 沒有撞到玩家
    bool scored_this_frame = false; // 標記這一幀是否有分數增加

    // 從底部往上遍歷所有可移動的行(不包括邊界)
    for (int r = MAP_H - 2; r >= 1; --r) { // 從玩家上方一行開始往上檢查(8-2=6=r)
        for (int c = 1; c < MAP_W - 1; ++c) { // 遍歷所有可移動的列
            if (play_map[r][c] == L'x') { // 如果當前位置有障礙物
                if (r + 1 == player.y && c == player.x) {// 檢查下方是否為玩家
                    ya = true; // 撞到了
                }

                // 將障礙物移動到下一行,只有當下一行不是最底部的牆壁時才移動
                if (r + 1 < MAP_H - 1) { // 確保障礙物不會覆蓋到最底下那行
                    play_map[r + 1][c] = L'x';
                    // 如果障礙物從玩家上方一行成功移動到最後一行,並且沒有撞到 (沒有 ya),則加分
                    if (r == player.y - 1 && !ya) {
                        if (!scored_this_frame) { // 確保次只加一次分數
                            current_score += 1; // 增加分數
                            scored_this_frame = true;
                        }
                    }
                }
                play_map[r][c] = L' ';// 清除原位置
            }
        }
    }
    return ya; // 返回是否發生碰撞
}

void movement(char move_letter) {// 處理玩家移動
    play_map[player.y][player.x] = L' '; // 先將玩家目前位置清空

    switch (move_letter) {
        case 'a': // 向左移動
            // 檢查是否超出左邊界以及目標位置是否為空(非牆壁且非障礙物)
            if (player.x > 1 && play_map[player.y][player.x - 1] == L' ') {
                player.x--;
            }
            break;
        case 'd': // 向右移動
            // 檢查是否超出右邊界以及目標位置是否為空(非牆壁且非障礙物)
            if (player.x < MAP_W - 2 && play_map[player.y][player.x + 1] == L' ') {
                player.x++;
            }
            break;
        case 'f': // 重置遊戲
            // 這裡不再直接調用 resetGame,因為還有遊戲結束和或重置的選擇
            // 如果在遊戲結束後按 'f' 應該觸發重啟
            if (game_over) {
                // 在 main 迴圈外處理重置
            }
            break;
    }
    play_map[player.y][player.x] = L'o'; // 在新位置放置玩家
}

int main() {

    // START: 確保控制台能正確顯示 UTF-8 中文
    // 設置標準輸出流模式為 UTF-8 文本
    _setmode(_fileno(stdout), _O_U8TEXT);
    // 設置標準輸入流模式為 UTF-8 文本 (如果需要處理中文輸入的話)
    _setmode(_fileno(stdin), _O_U8TEXT);
    // 設置控制台輸出代碼頁為 UTF-8 (65001)
    SetConsoleOutputCP(CP_UTF8);
    // 設置控制台輸入代碼頁為 UTF-8 (65001) (如果需要處理中文輸入的話)
    SetConsoleCP(CP_UTF8);
    // END: 確保控制台能正確顯示 UTF-8 中文

    welcome();
    srand(static_cast<unsigned int>(time(nullptr))); // 利用時間設定亂數種子

    bool play_again = true;

    while (play_again) { // 外部迴圈控制遊戲是否重啟
        reset(); // 遊戲開始前先重置地圖和玩家位置
        final_score = 0; // 每次重置遊戲時,分數也要重置

        long long last_spawn_time = GetTickCount(); // 記錄上次生成障礙物的時間
        long long last_fall_time = GetTickCount(); // 記錄上次障礙物掉落的時間
        // 每 1800 毫秒 (1.8秒) 生成一次障礙物
        const int SPAWN_T= 1800;
        // 每 600 毫秒 (0.6秒) 障礙物掉落一次
        const int FALL_T = 600;

        drawMap(); // 初始繪製地圖

        while (!game_over) { // 當 game_over 為 true 時跳出迴圈
            long long current_time = GetTickCount(); // 取得當前時間

            if (_kbhit()) { // 檢查是否有按鍵被按下
                char key = _getch(); // 取得按下的鍵
                if (key == L'f') { // 在遊戲中按 'f' 可以選擇立即結束本局遊戲
                    game_over = true;
                    // 如果需要在遊戲中途結束,可以考慮在這裡設置一個特殊的分數或狀態
                    break; // 跳出當前遊戲迴圈
                }
                movement(key);  // 玩家移動
            }

            if (current_time - last_spawn_time > SPAWN_T) {
                xxxyyy();
                last_spawn_time = current_time; // 更新上次生成時間
            }

            if (current_time - last_fall_time > FALL_T) {
                // 將 score 變數的引用傳遞給 moveDown 函式
                bool collision_detected = moveDown(final_score); // 使用 final_score
                if (collision_detected) {
                    game_over = true; // 設為遊戲結束
                }
                last_fall_time = current_time; // 更新上次掉落時間
            }

            // --- 最終碰撞檢查 (玩家與新落下的或現有障礙物的碰撞) ---
            // 確保玩家移動到一個有 'x' 的位置時也能判定死亡
            if (play_map[player.y][player.x] == L'x') {
                game_over = true; // 設為遊戲結束
            }

            // --- 遊戲狀態更新與繪製 ---
            drawMap(); // 重新繪製地圖以顯示所有更新

            gotoxy(0, MAP_H + 1); // 將游標移動到地圖下方
            wcout << L"你的分數: " << final_score << "               " << endl; // 清空舊分數,避免殘影

            // 稍微延遲一下,控制遊戲速度,避免 CPU 使用率過高
            std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 讓迴圈每 20 毫秒執行一次,讓遊戲更流暢
        }

        // --- 遊戲結束後的訊息 ---
        gotoxy(0, MAP_H + 3); // 移動游標到地圖下方更遠處
        wcout << L"哇哇 死掉了!你被障礙物砸中了!最終分數: " << final_score << endl;
        wcout << L"遊戲結束。按 'f' 重新開始,按 'q' 退出遊戲。" << endl;

        // 等待玩家輸入重新開始或退出
        char choice = ' ';
        while (true) {
            if (_kbhit()) {
                choice = _getch();
                if (choice == 'f' || choice == 'F') {
                    play_again = true; // 玩家選擇重新開始
                    break;
                } else if (choice == 'q' || choice == 'Q') {
                    play_again = false; // 玩家選擇退出
                    break;
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暫延遲,避免佔用CPU
        }
        system("cls"); // 清空螢幕
    }
    return 0;
}

程式介紹

程式碼:

// 使用 vector<string> 比 char[][] 更方便且安全地操作地圖。
// 定義地圖尺寸
const int MAP_H = 8; // 地圖高度,從 0 到 7
const int MAP_W = 22; // 地圖寬度,從 0 到 21

// 初始地圖設定 - 這裡將玩家 ('o') 移除,會在 reset 中放置
vector<wstring> play_map = {
    L"######################",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #",
    L"#                    #", // 原本的玩家位置現在也初始化為空白
    L"######################"  // 最後一行保持為牆壁
};

struct Player {// 玩家
    int x;
    int y;
} player;

bool game_over = false;// 控制遊戲狀態
int final_score = 0; // 用於儲存最終分數

程式碼:

void welcome()
{
	wcout << L"\n\n\t\t\t找  洞  鑽";
	wcout << L"\n\n\t\t      請按任意鍵開始";
	wcout << L"\n\n\t\t      按F可結束遊戲";
	getch();//得到任意鍵
	system("cls");//清空螢幕
	SetConsoleTitleW(L"找洞鑽");//設置窗口標題
}

void gotoxy(int x, int y) {// 設定游標位置,讓畫面更新更流暢
    COORD coord;
    coord.X = x;
    coord.Y = y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);// 將游標移動到指定的位置
}

void drawMap() {
    gotoxy(0, 0); // 將游標移動到左上角,以便重新繪製整個地圖
    for (int i = 0; i < MAP_H; ++i) {
        wcout << play_map[i] << endl;
    }
}

程式碼:

void reset() {// 重置遊戲地圖和玩家位置
    play_map = {
        L"######################",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"#                    #",
        L"######################"
    };
    player.x = 11; // 玩家 X 座標重置到中心
    player.y = 6;  // 玩家總是在y=6上
    play_map[player.y][player.x] = L'o'; // 只在這裡放置玩家 'o'
    game_over = false; // 重置遊戲時,將遊戲結束狀態設為 false
    final_score = 0;   // 重置分數
}

程式碼:

void xxxyyy() {// 生成新的障礙物

    for (int i = 1; i < MAP_W - 1; ++i) {// 先都設定為障礙物 'x'
        play_map[1][i] = L'x';
    }

    int hole_w = rand() % 2 + 3; // 隨機生成 3 或 4 格寬的洞,因為%2

    // 計算洞的起始位置範圍
    int max_hole_start_x = (MAP_W - 1) - hole_w;// 確保出現在遊戲範圍裡
    int hole_start_x = rand() % max_hole_start_x + 1;// +1是因為從1開始

    for (int i = 0; i < hole_w; ++i) {// 挖出第一個洞
        play_map[1][hole_start_x + i] = L' ';
    }

    if (rand() % 10 < 3) { // 30% 機率生成另外的洞
        int an_hole_w = rand() % 2 + 1; // 1 或 2 格寬
        int an_hole_start_x;
        bool v_found = false;// 還沒找到適合的地方

        // 嘗試找一個不重疊的位置
        for(int attempt = 0; attempt < 10; ++attempt) { // 嘗試幾次
            an_hole_start_x = rand() % (MAP_W - 1 - an_hole_w) + 1;
            bool overlap = false; // 沒有重疊到
            for(int i = 0; i < an_hole_w; ++i) {
                if (play_map[1][an_hole_start_x + i] == ' ') { // 如果新洞的位置已經是空格,說明重疊了
                    overlap = true; //有重疊
                    break;
                }
            }
            if (!overlap) { // 沒有重疊
                v_found = true; // 找到地方了
                break;
            }
        }
        if (v_found) {
            for (int i = 0; i < an_hole_w; ++i) {
                play_map[1][an_hole_start_x + i] = L' ';
            }
        }
    }
}

程式碼:

// 移動所有障礙物向下,並計算分數
bool moveDown(int& current_score) { // 傳入分數引用(&=傳入分數本身不是分身)
    bool ya = false; // 沒有撞到玩家
    bool scored_this_frame = false; // 標記這一幀是否有分數增加

    // 從底部往上遍歷所有可移動的行(不包括邊界)
    for (int r = MAP_H - 2; r >= 1; --r) { // 從玩家上方一行開始往上檢查(8-2=6=r)
        for (int c = 1; c < MAP_W - 1; ++c) { // 遍歷所有可移動的列
            if (play_map[r][c] == L'x') { // 如果當前位置有障礙物
                if (r + 1 == player.y && c == player.x) {// 檢查下方是否為玩家
                    ya = true; // 撞到了
                }

                // 將障礙物移動到下一行,只有當下一行不是最底部的牆壁時才移動
                if (r + 1 < MAP_H - 1) { // 確保障礙物不會覆蓋到最底下那行
                    play_map[r + 1][c] = L'x';
                    // 如果障礙物從玩家上方一行成功移動到最後一行,並且沒有撞到 (沒有 ya),則加分
                    if (r == player.y - 1 && !ya) {
                        if (!scored_this_frame) { // 確保次只加一次分數
                            current_score += 1; // 增加分數
                            scored_this_frame = true;
                        }
                    }
                }
                play_map[r][c] = L' ';// 清除原位置
            }
        }
    }
    return ya; // 返回是否發生碰撞
}

程式碼:

void movement(char move_letter) {// 處理玩家移動
    play_map[player.y][player.x] = L' '; // 先將玩家目前位置清空

    switch (move_letter) {
        case 'a': // 向左移動
            // 檢查是否超出左邊界以及目標位置是否為空(非牆壁且非障礙物)
            if (player.x > 1 && play_map[player.y][player.x - 1] == L' ') {
                player.x--;
            }
            break;
        case 'd': // 向右移動
            // 檢查是否超出右邊界以及目標位置是否為空(非牆壁且非障礙物)
            if (player.x < MAP_W - 2 && play_map[player.y][player.x + 1] == L' ') {
                player.x++;
            }
            break;
        case 'f': // 重置遊戲
            // 這裡不再直接調用 resetGame,因為還有遊戲結束和或重置的選擇
            // 如果在遊戲結束後按 'f' 應該觸發重啟
            if (game_over) {
                // 在 main 迴圈外處理重置
            }
            break;
    }
    play_map[player.y][player.x] = L'o'; // 在新位置放置玩家
}

程式碼:

int main() {

    // START: 確保控制台能正確顯示 UTF-8 中文
    // 設置標準輸出流模式為 UTF-8 文本
    _setmode(_fileno(stdout), _O_U8TEXT);
    // 設置標準輸入流模式為 UTF-8 文本 (如果需要處理中文輸入的話)
    _setmode(_fileno(stdin), _O_U8TEXT);
    // 設置控制台輸出代碼頁為 UTF-8 (65001)
    SetConsoleOutputCP(CP_UTF8);
    // 設置控制台輸入代碼頁為 UTF-8 (65001) (如果需要處理中文輸入的話)
    SetConsoleCP(CP_UTF8);
    // END: 確保控制台能正確顯示 UTF-8 中文

    welcome();
    srand(static_cast<unsigned int>(time(nullptr))); // 利用時間設定亂數種子

    bool play_again = true;

程式碼:

    while (play_again) { // 外部迴圈控制遊戲是否重啟
        reset(); // 遊戲開始前先重置地圖和玩家位置
        final_score = 0; // 每次重置遊戲時,分數也要重置

        long long last_spawn_time = GetTickCount(); // 記錄上次生成障礙物的時間
        long long last_fall_time = GetTickCount(); // 記錄上次障礙物掉落的時間
        // 每 1800 毫秒 (1.8秒) 生成一次障礙物
        const int SPAWN_T= 1800;
        // 每 600 毫秒 (0.6秒) 障礙物掉落一次
        const int FALL_T = 600;

        drawMap(); // 初始繪製地圖

        while (!game_over) { // 當 game_over 為 true 時跳出迴圈
            long long current_time = GetTickCount(); // 取得當前時間

            if (_kbhit()) { // 檢查是否有按鍵被按下
                char key = _getch(); // 取得按下的鍵
                if (key == L'f') { // 在遊戲中按 'f' 可以選擇立即結束本局遊戲
                    game_over = true;
                    break; // 跳出當前遊戲迴圈
                }
                movement(key);  // 玩家移動
            }

            if (current_time - last_spawn_time > SPAWN_T) {
                xxxyyy();
                last_spawn_time = current_time; // 更新上次生成時間
            }

            if (current_time - last_fall_time > FALL_T) {
                // 將 score 變數的引用傳遞給 moveDown 函式
                bool collision_detected = moveDown(final_score); // 使用 final_score
                if (collision_detected) {
                    game_over = true; // 設為遊戲結束
                }
                last_fall_time = current_time; // 更新上次掉落時間
            }

            // --- 最終碰撞檢查 (玩家與新落下的或現有障礙物的碰撞) ---
            // 確保玩家移動到一個有 'x' 的位置時也能判定死亡
            if (play_map[player.y][player.x] == L'x') {
                game_over = true; // 設為遊戲結束
            }

            // --- 遊戲狀態更新與繪製 ---
            drawMap(); // 重新繪製地圖以顯示所有更新

            gotoxy(0, MAP_H + 1); // 將游標移動到地圖下方
            wcout << L"你的分數: " << final_score << "               " << endl; // 清空舊分數,避免殘影

            // 稍微延遲一下,控制遊戲速度,避免 CPU 使用率過高
            std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 讓迴圈每 20 毫秒執行一次,讓遊戲更流暢
        }

程式碼:

        // --- 遊戲結束後的訊息 ---
        gotoxy(0, MAP_H + 3); // 移動游標到地圖下方更遠處
        wcout << L"哇哇 死掉了!你被障礙物砸中了!最終分數: " << final_score << endl;
        wcout << L"遊戲結束。按 'f' 重新開始,按 'q' 退出遊戲。" << endl;

        // 等待玩家輸入重新開始或退出
        char choice = ' ';
        while (true) {
            if (_kbhit()) {
                choice = _getch();
                if (choice == 'f' || choice == 'F') {
                    play_again = true; // 玩家選擇重新開始
                    break;
                } else if (choice == 'q' || choice == 'Q') {
                    play_again = false; // 玩家選擇退出
                    break;
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暫延遲,避免佔用CPU
        }
        system("cls"); // 清空螢幕
    }
    return 0;
}

反思與展望

心得與反思

  • 在這次製作過程,遇到很多麻煩,有適時的運用到AI協助,而在經過AI調整後。都有慢慢去看每個部分,可以很快速的達成我原本想很久的問題。
  • 整個遊戲其實整體改變不少,原先是想製作成炸彈掉下來,要閃躲的感覺,但後來覺得現在這種方式比較簡單。

遇到的問題

一開始遇到的問題是障礙物無法正常掉落,於是我想到運用自訂義涵式。後來又遇到中文無法正常顯示的問題,於是去找Gemini幫忙,它建議使用UTF-8。

但在最後遊玩的時候都會覺得太快,加上延遲及時間,讓它下落的速度與出現速度得到控制

成果反思

這次製作遇到很多問題,因為學期末的關係導致較晚開始,再加上對於自定義函式不算特別熟悉,導致整體製作時間拉長

這次成發讓我們學到了如何把課堂知識應用在實際操作中,也增進了團隊合作的默契。看到成果順利完成,我們都很開心,感謝學姐們的耐心指導。

未來展望

  1. 可以在遊戲裡面添加計時器的功能,讓玩家知道自己玩了多長時間
  2. 添加歷史最高分數,這樣可以讓玩家為了打破紀錄玩更久
  3. 以關卡數作為判斷標準,加快掉落速度及數量

謝謝大家🩶

Copy of Minimal

By xuyijie

Copy of Minimal

  • 17