# 2024 交大電機營 程式遊戲實作 -- 踩地雷
---
聽完前面兩個講師的課,相信大家也都變超強的啦~
現在就來挑戰自己,完成下面的踩地雷小遊戲吧! :muscle: :face_with_monocle:
___
## 被挖空的程式碼
- #### 踩地雷課程簡報: [EECamp_程式遊戲實作_踩地雷](https://drive.google.com/file/d/1mPWIF-KMT6YXswGQYe-MYgEb_gTtbFdF/view?usp=sharing)
#### 將下面程式碼的 11題空格填入正確的答案
```C++=
#include <assert.h>
#include <string.h>
#include <conio.h>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <queue>
#include <iomanip>
using namespace std;
void CleanScreen(); //清空螢幕
void output(int graph[8][8], int vis[8][8], int, int, int, int); //輸出
int check_position(int graph[8][8], int, int); //檢查該區塊情形
int bomb_around(int graph[8][8], int, int); //數九宮格內地雷個數
// 不用看懂這個函數
// Clean up the Screen
void CleanScreen() {
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
}
//輸出踩地雷地圖 (每個區塊有 地雷 / 旗標 / 已經點開 三種情形)
void output(int graph[8][8], int vis[8][8], int mined_cnt, int flag_cnt,
int cursor_row, int cursor_col) {
cout << "\033[1;32;45m ";
for (int j = 0; j < 8; j++) cout << setw(3) << j; //輸出直行編號
cout << "\033[0m\n";
for (int i = 0; i < 8; i++) { //i 標註 row 編號
cout << "\033[1;32;45m" << setw(3) << i << "\033[0m";//輸出橫列編號
for (int j = 0; j < 8; j++) { //j 標註 col 編號
if (___1___) //判斷是否為游標所在區塊
cout << "\033[5;44m"; //文字閃爍
if (vis[i][j] == 0) cout << " - " << "\033[0m"; //未知區塊
else if (vis[i][j] == 1) { //已點開的區塊(可顯示實際值)
if (___2___) cout << "\033[1;31m" << " * " << "\033[0m"; //是地雷
else cout << ' ' << graph[i][j] << ' ' << "\033[0m"; //不是地雷(數字代表周遭九宮格內地雷數
}
else if (vis[i][j] == 2) cout << "\033[1;36m" << " $ " << "\033[0m"; //旗幟標示
cout << "\033[0m"; //恢復為cout預設型態
}
cout << '\n';
}
//輸出目前 點開的區塊數量 / 旗幟標示的數量
cout << "\033[1;33m\n"
<< "mined places: " << mined_cnt << " /" << 8 * 8 - 10 << '\t'
<< "flagged bombs: " << flag_cnt << " /" << 10 << "\033[0m\n";
return;
}
//檢查graph[row][col]
int check_position(int graph[8][8], int row, int col) {
if (___3___) //不合法的情形
return 0; //不做事 >> 直接回傳0
if (graph[row][col] == -1) return 1; //地雷 >> 回傳1
if (graph[row][col] == 0) return 2; //周圍都沒地雷 >> 回傳2
return 3; //其他 (自己不是地雷,九宮格內有地雷) >> 回傳3
}
//計算九宮格內有幾顆地雷(自己不是)
int bomb_around(int graph[8][8], int row, int col) {
int bomb_num = 0;
for (___4.1___) { //檢查上下及自己三個row
for (___4.2___) { //檢查左右及自己三個col
if (i == 0 && j == 0) continue;
if (___5___) bomb_num++; //是地雷 >> bomb_num+1
}
}
return bomb_num;
}
int main() {
/*
* 變數簡介
* graph[][]: 踩地雷地圖
* vis[][]: 紀錄區塊造訪/標示情形
* mined_cnt: 已經點開的合法區塊數
* flag_cnt: 旗幟標示的區塊數
* cursor_row:游標所在列
* cursor_col:游標所在行
*/
int graph[8][8] = {0}, vis[8][8] = {0};
int mined_cnt = 0, flag_cnt = 0;
int cursor_row = 0, cursor_col = 0;
srand(time(NULL));
CleanScreen();
output(graph, vis, mined_cnt, flag_cnt, cursor_row, cursor_col);
//開始踩地雷
bool fail = false;
while (___6___) { //當 點開區塊數 = 總區塊數-地雷數 >> 成功;
char ch = '\0';
//CleanScreen();
//output(graph, vis, mined_cnt, flag_cnt, cursor_row, cursor_col);
while (ch == '\0') ch = getch();
if ((ch == 'w' || ch == 72) && check_position(graph, cursor_row - 1, cursor_col)) ___7.1___; // 上
else if ((ch == 's' || ch == 80) && check_position(graph, cursor_row + 1, cursor_col)) ___7.2___; // 下
else if ((ch == 'a' || ch == 75) && check_position(graph, cursor_row, cursor_col - 1)) ___7.3___; // 左
else if ((ch == 'd' || ch == 77) && check_position(graph, cursor_row, cursor_col + 1)) ___7.4___; // 右
else if (ch == 'j') { //點開區塊
if (mined_cnt == 0) { //第一次點開區塊,建立踩地雷地圖
for (int i = 0; i < 10; i++) {//填地雷
int pos = ___8___; //產生一個 0 ~ 63 的隨機變數
bool clr_cursor = (pos/8-cursor_row <= 1 && pos/8-cursor_row >= -1);
clr_cursor &= (pos%8-cursor_col <= 1) && (pos%8-cursor_col >= -1);
while (graph[pos/8][pos%8] == -1 || clr_cursor) {
pos = ___8___; //產生一個 0 ~ 63 的隨機變數
clr_cursor = (pos/8-cursor_row <= 1 && pos/8-cursor_row >= -1);
clr_cursor &= (pos%8-cursor_col <= 1) && (pos%8-cursor_col >= -1);
}
graph[pos/8][pos%8] = -1;
}
//計算每個區塊的數字
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (graph[i][j] == 0)
graph[i][j] = ___9___; //計算周遭地雷數
}
}
//踩地雷建圖完成
}
if (vis[cursor_row][cursor_col]) continue; //已經點開/ 標示旗幟之區塊
else if (graph[cursor_row][cursor_col] == -1) { //踩到地雷
for (int t = 0; t < 8 * 8; t++) vis[t/8][t%8] = 1; //標記整張圖都被點開
fail = true; //標記已經踩到地雷
}
else if (graph[cursor_row][cursor_col] > 0) { //周圍九宮格內有地雷 >> 只挖開一格
vis[cursor_row][cursor_col] = 1;
___10___; //更新挖開區塊數
}
else { //點開區塊周圍九宮格沒有地雷
// BFS (不用懂這裡)
queue<int> save;
save.push(cursor_row * 8 + cursor_col);
while (save.size()) {
int cur = save.front();
int cur_row = cur / 8, cur_col = cur % 8;
save.pop();
if (vis[cur_row][cur_col]) continue;
cursor_row = cur_row;
cursor_col = cur_col;
vis[cur_row][cur_col] = 1;
mined_cnt++;
if (check_position(graph, cur_row, cur_col) == 2) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
if (check_position(graph, cur_row+i, cur_col+j) > 1 && !vis[cur_row+i][cur_col+j])
save.push((cur_row+i) * 8 + (cur_col + j));
}
}
}
}
// end BFS
}
}
else if (ch == 'k') { //標示旗幟
if (vis[cursor_row][cursor_col] == 0) { //原本沒被標示旗幟
vis[cursor_row][cursor_col] = 2, flag_cnt++;
}
else if (vis[cursor_row][cursor_col] == 2) { //原本有標示旗幟
vis[cursor_row][cursor_col] = 0, flag_cnt--;
}
}
CleanScreen();
output(graph, vis, mined_cnt, flag_cnt, cursor_row, cursor_col);
if (fail) { //踩到地雷
cout << "\033[1;5;31m\n\t\t"
<< "Stepped on bomb!!"
<< "\033[0m\n";
break;
}
}
//成功結束 >> 輸出 "\\ You Win ! //"
if (!fail) cout << "\033[1;5;32m\n\t\t" << ___11___ << "\033[0m\n";
cout << "\033[0m" << "\n\nPress 'c' to leave\n";
char get_end;
while (get_end = getch()) {
if (get_end == 'c') break;
}
return 0;
}
```
---
## **Part 1.** 選填題
作答的過程中,記得把選了甚麼選項記下來唷 !
就算你超有自信,覺得自己選的一定是對的直接填進就沒問題也一樣,等等需要對答案 !
___
### **Problem 1.** 判斷目前要輸出的區塊是不是游標所在的位置
```C++=34
for (int j = 0; j < 8; j++) cout << setw(3) << j; //輸出直行編號
cout << "\033[0m\n";
for (int i = 0; i < 8; i++) { //i 表示 row 編號
cout << "\033[1;32;45m " << setw(3) << i << "\033[0m"; //輸出橫列編號
for (int j = 0; j < 8; j++) { //j 表示 col 編號
if (__1__) //判斷是否為游標所在區塊
cout << "\033[5;44m"; //文字閃爍
...
}
}
```
:::success
:label: 選項 >> 只有一個是對的喔!
:::
> A ) ```i == cursor_row && j == cursor_col```
> B ) ```j == cursor_row && i == cursor_col```
> C ) ```i == cursor_row || j == cursor_col```
> D ) ```j == cursor_row || i == cursor_col```
---
### **Problem 2.** 判斷是不是地雷 :bomb:
```C++=45
else if (vis[i][j] == 1) { //已經被點開的區塊
if ( _2_ ) cout << "\033[1;31m" << " * " << "\033[0m"; //是地雷
else cout << ' ' << graph[i][j] << ' ' << "\033[0m"; //不是地雷
}
```
:::success
:label: 選項 >> 一樣只有一個對的喔~
:::
> A ) ```graph[i][j] == 1```
> B ) ```graph[i][j] == 0```
> C ) ```graph[i][j] == -1```
> D ) ```vis[i][j] == -1```
---
### **Problem 3.** 檢查位置的合法性
:point_right: Hint:[row][col] 代表的位置不可以超出地圖的範圍
```C++=65
int check_position (int graph[8][8], int row, int col) {
if ( _3_ ) //不合法的情形
return 0; //不做事 >> 直接回傳0
...
```
:::success
:label: 選項 >> 不用懷疑,還是只有一個對的 :face_with_raised_eyebrow:
:::
> A ) ``` (row < 0 && row >= 8) || (col < 0 && col >= 8)```
> B ) ``` (row < 0 || row >= 8) && (col < 0 || col >= 8)```
> C ) ``` row < 0 && row >= 8 && col < 0 && col >= 8```
> D ) ``` row < 0 || row >= 8 || col < 0 || col >= 8```
---
### **Problem 4.** 計算自己周圍九宮格內的地雷數
```C++=77
for ( _4.1_ ) { //檢查上下及自己的三個 row
for ( _4.2_ ) { //檢查左右及自己的三個 col
if (i == 0 && j == 0) continue;
if (...) bomb_num++; //檢查是地雷 >> bomb_num+1
}
}
```
:::info
||||||
|-|-|-|-|-|
||O|O|O||
||O|C|O||
||O|O|O||
||||||
如上面表格的情形,自己是C位,要計算周遭標示O的區塊總共有幾個地雷
:::
:::success
:label: 選項 >> 這題水一點,有兩個選項正確
:::
_4.1_ / _4.2_
> A ) ```int i = -1; i <= 1; i++``` / ```int j = -1; j <= 1; j++```
> B ) ```int i = 0; i <= 2; i++``` / ```int j = 0; j <= 1; j++```
> C ) ```int i = 1; i >= -1; i--``` / ```int j = 1; j >= -1; j--```
> D ) ```int i = 2; i >= 0; i--``` / ```int j = 2; j >= 0; j--```
---
### **Problem 5.** 檢查是不是地雷
```C++=77
for (...) {
for (...) {
if (i == 0 && j == 0) continue;
if ( _5_ ) bomb_num++; //如果檢查是地雷 >> bomb_num+1
}
}
```
:::success
:label: 選項 >> 這題送你,只有一個選項 (絕對不是因為課程組掰不出來)
:::
> A ) ```check_position (graph, row+i, col+j) == 1```
---
### **Problem 6.** while loop 中止條件(成功結束時)
```C++=107
bool fail = false;
while ( _6_ ) { //當 點開區塊數 = 總區塊數-地雷數 >> 成功
char ch = '\0';
...
```
:::info
:point_right: Hint: 當 點開區塊數 = 總區塊數 - 總地雷數 >> 成功結束
:::
:::success
:label: 選項 >> 只有一個是正確的喔
:::
> A ) ```1```
> B ) ```0```
> C ) ```mined_cnt < 8 * 8 - 10```
> D ) ```mined_cnt < 10```
---
### **Problem 7.** 上下左右鍵
```C++=115
if ((ch == 'w' || ch == 72) && ... ) _7.1_ ; //上
else if ((ch == 's' || ch == 80) && ... ) _7.2_ ; //下
else if ((ch == 'a' || ch == 75) && ... ) _7.3_ ; //左
else if ((ch == 'd' || ch == 77) && ... ) _7.4_ ; //右
```
:::success
:label: 選項 >> 四個子題共用下列的選項,7.1 ~ 7.4 都各自要選一個選項
:::
作答形式: _7.1 / _7.2_ / _7.3_ / _7.4_
> A ) ```cursor_col++```
> B ) ```cursor_row++```
> C ) ```cursor_col--```
> D ) ```cursor_row--```
---
### **Problem 8.** 生成隨機變數
```C++=121
for (int i = 0; i < 10; i++) {//填地雷
int pos = ___8___; //產生一個 0 ~ 63 的隨機變數
bool clr_cursor = (pos/8-cursor_row <= 1 && pos/8-cursor_row >= -1);
clr_cursor &= (pos%8-cursor_col <= 1) && (pos%8-cursor_col >= -1);
while (graph[pos/8][pos%8] == -1 || clr_cursor) {
pos = ___8___; //產生一個 0 ~ 63 的隨機變數
clr_cursor = (pos/8-cursor_row <= 1 && pos/8-cursor_row >= -1);
clr_cursor &= (pos%8-cursor_col <= 1) && (pos%8-cursor_col >= -1);
}
graph[pos/8][pos%8] = -1;
}
```
:::info
:point_right: Hint: 隨機產生一個 0 ~ 63 的整數
:point_right: Hint: 常數 RAND_MAX 為函數 rand() 能產出的最大值
:::
:::success
:label: 選項 >> 有兩個是對的喔 (程式裡面兩個標示 _ 8 _ 的地方是一樣的東西
:::
> A ) ```(int) ((double) rand()%RAND_MAX / 64)```
> B ) ```(int) ((double) rand()/RAND_MAX * 8 * 8)```
> C ) ```rand() % 64```
> D ) ```rand() / 64```
---
### **Problem 9.** 呼叫函式計算周遭的地雷數
```C++=134
//計算每個區塊的數字
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (graph[i][j] == 0)
graph[i][j] = ___9___; //計算周遭地雷數
}
//踩地雷建圖完成
}
```
:::success
:label: 選項 >> 只有一個是正確的喔
:::
> A ) ```output (graph, vis, mined_cnt, flag_cnt, cursor_row, cursor_col)```
> B ) ```check_position (graph, cursor_row, cursor_col)```
> C ) ```bomb_around (graph, cursor_row, cursor_col)```
> D ) ```bomb_around (graph, i, j)```
___
### **Problem 10.** 紀錄挖開的區塊數
```C++=149
else if (graph[cursor_row][cursor_col] > 0){ //九宮格內有地雷 >> 只挖開一格
vis[cursor_row][cursor_col] = 1;
_10_ ; //更新挖開區塊數
}
```
:::success
:label: 選項 >> 有兩個是正確的喔
:::
> A ) ```flag_cnt++```
> B ) ```mined_cnt++```
> C ) ```flag_cnt += 1```
> D ) ```mined_cnt = mined_cnt+1```
___
### **Problem 11.** 超難題
:::info
題目敘述:
恭喜玩家終於成功解開整張地圖都沒踩到地雷!
請在螢幕上輸出 "\\\\ You win ! //" 的字樣 (輸出在螢幕上時沒有雙引號)
_ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _ - _
:exclamation: 注意: 輸出 \\\\ You win ! // 完不用自己加換行
:point_right: Hint: 可以自己開一個 cpp 檔案試著輸出上面的字串喔!
:::
```C++=205
//成功結束 >> 輸出 "\\ You Win ! //"
if (!fail) cout << "033[1;5;32m" << _11_ << "033[0m\n";
```
:::danger
:label: 選項 >> 因為是超難題所以就沒有選項啦 :smirk:
直接寫出 __ 11 __ 內該寫入什麼程式碼喔
:::
---
:::warning
### 作答完成
寫完上面11題的答案之後,記得給學長姐們確認過答案,再把答案打入你的程式檔喔!
<img class="text-center" src="https://hackmd.io/_uploads/rJIJUw140.jpg">
:::
---
## Part 2. 程式改寫
:::info
能做到這真的已經超強了! :thumbsup:
接下來試著把踩地雷程式做出以下改變:
1. 8\*8的地圖 變成 16\*16的地圖
2. 10顆地雷 變成 40顆地雷
---
:point_right: Hint: 附上一張貓貓給你能量
<img class="text-center" src="https://i.imgur.com/s8q2j8A.jpg">
:::
---
## Part 3. 教學與相關資料
:::success
#### 如果你真的卡住了的話,可以看下面巨有料的資料喔!!
- 資料 1 -
<iframe src="https://drive.google.com/file/d/1tZUublhnqgiAv1fJCnpx0yOd7Fki-ous/preview" width="700" height="420" allow="autoplay"></iframe>
---
- 資料 2 - [踩地雷程式概念講解](https://www.youtube.com/watch?v=bHb5CFGYz1A)
:::
:::warning
寫完之後就可以玩踩地雷遊戲囉!
:::
<style>
.text-center{
text-align: center; //文字置中
}
.text-left{
text-align: left; //文字靠左
}
.text-right{
text-align: right; //文字靠右
}
</style>