經過一整子的瞎折騰,我終於完成了ncurses版的掃雷。這讓我進一步認識到了ncurses庫的強大。這裡主要的新知識點是"滑鼠事件的獲取和處理",可以參考這篇文章,雖然排版湊合但還是挺實用的。相比寫情詩,掃雷的程式碼量還是比較大的,我的版本前後有100多行的樣子。我用思維導圖整理了我的思路,一個功能基本就一個函數。擴充套件功能我這裡完成了前兩個,但已經夠用了,掃雷的基本操作測試下來很流暢。
#include <stdlib.h> //要用到srand()和rand()生成雷
#include <time.h> //要用到clock()生成亂數種子
#include <ncurses.h> //主角不用解釋
#define MINENUM 10 //雷的總數
#define row 9 //雷區的行數
#define line 9 //雷區的列數
int a[row][line]; //0:周圍沒有雷 -1:此處有雷 1~8:周圍雷的數量
int f[row][line]; //0:未探索 1:已探索
int win; //用於記錄輸贏的資訊
initscr(); //初始化螢幕
curs_set(0); //不顯示遊標
noecho(); //禁止輸入的字元出現在螢幕上
keypad(stdscr, TRUE); //因為缺少這行一直出錯
mousemask(BUTTON1_RELEASED,0);//啟用滑鼠事件左鍵釋放,使得getch()能獲取滑鼠事件
MEVENT event; //建立事件變數
while(1){ //這個迴圈使遊戲可以玩很多局
new_mine(MINENUM); //新建雷區
display(); //顯示雷區(剛開始全是不可見的)
while(1){ //這個迴圈使遊戲的操作可以持續
switch (getch()){ //獲取操作
case KEY_MOUSE: //如果是滑鼠事件,進行遊戲操作
win=operate(event);//如果踩雷operate()會返回-2
break;
case 'q': win=-2; //如果輸入的是字元'q'則退出本局遊戲
}
display();
if(win==-2){ //輸掉遊戲(包括中途退出)
printw("You lose!"); break;
}
if(check_win()){ //檢查是否贏得遊戲
printw("You win!"); break;
}
}
if(getch()=='q') break; //如果再次輸入字元'q'則退出遊戲
}
endwin(); //退出curses模式
int add_mine(int r,int l){ //用於累加雷區的數位
if(r>=0&&r<row&&l>=0&&l<line&&a[r][l]!=-1)//排除在界外的情況,而且雷上面不用進行處理
a[r][l]++;
}
int new_mine(int num){
int n=0,r=0,l=0;
win=0; //千萬不要忘記初始化,下一局開始時win要再次初始化為0
for(r=0;r<row;r++){
for(l=0;l<line;l++){
a[r][l]=0; //雷區的狀態也要初始化
f[r][l]=0; //雷區的探索情況也要初始化
}
} //生成雷
srand(clock()); //用clock()生成隨機種子
while(n<num){
r=rand()%row; //用亂數獲取行數
l=rand()%line; //用亂數獲取列數
if(a[r][l]==0){ //確保即使有重複位置的雷也能生成所需的數量的雷
a[r][l]=-1; //放置雷
n++;
}
}
for(r=0;r<row;r++) //佈置雷區數位
for(l=0;l<line;l++)
if(a[r][l]==-1){//在雷的周圍累加數位
add_mine(r-1,l-1);
add_mine(r-1,l);
add_mine(r-1,l+1);
add_mine(r,l-1);
add_mine(r,l+1);
add_mine(r+1,l-1);
add_mine(r+1,l);
add_mine(r+1,l+1);
}
}
int show_mine(int r,int l){ //用於顯示雷區的狀態
if(r>=0&&r<row&&l>=0&&l<line&&a[r][l]!=-1)//排除在界外的情況,而且雷不用顯示
f[r][l]=1;
}
int operate(MEVENT mouse){ //遊戲的主要操作
static int r=0,l=0;
getmouse(&mouse); //翻譯獲取的滑鼠事件
mouse_trafo(&mouse.y,&mouse.x,1); //將獲取的滑鼠的座標轉換為對應的螢幕座標
if (!wenclose(stdscr,mouse.y, mouse.x))
return -1; //如果座標不在螢幕內,直接返回
if(mouse.bstate==BUTTON1_RELEASED){ //當滑鼠事件為左鍵釋放時
l=mouse.x/3; //將滑鼠座標轉化為對應的雷區座標
r=mouse.y/2;
if(a[r][l]==-1){ //該座標上有雷,踩到雷了
for(int i=0;i<=row;i++)
for(int j=0;j<=line;j++)
f[i][j]=1; //全部雷區的狀態都設為可見
return -2; //返回-2
}
show_mine(r,l); //沒踩到雷,就設定該座標的雷區狀態為可見
if(a[r][l]==0){ //如果該座標周圍沒有雷,則設定周圍的雷區狀態為可見
show_mine(r-1,l-1);
show_mine(r-1,l);
show_mine(r-1,l+1);
show_mine(r,l-1);
show_mine(r,l+1);
show_mine(r+1,l-1);
show_mine(r+1,l);
show_mine(r+1,l+1);
}
}
}
int display(){ //用於顯示雷區
int r=0,l=0;
clear(); //清屏
for(r=0;r<row;r++){ //遍歷整個雷區
for(l=0;l<line;l++){
if(f[r][l]==1) //如果該雷區座標狀態為可見
if(a[r][l]==-1) //如果該座標是雷,列印雷
mvaddch(r*2+1,l*3+1,'*');
else //不是雷就列印數位
mvaddch(r*2+1,l*3+1,'0'+a[r][l]);
}
}
//繪製網格,細節就不解釋了
for(r=0;r<row;r++){
for(l=0;l<line;l++){
mvaddch(r*2,l*3+1,'-');
mvaddch(r*2,l*3+2,'-');
mvaddch(r*2,l*3,'|');
mvaddch(r*2+1,l*3,'|');
mvaddch(row*2,l*3,'|');
mvaddch(row*2,l*3+1,'-');
mvaddch(row*2,l*3+2,'-');
}
mvaddch(r*2,line*3,'|');
mvaddch(r*2+1,line*3,'|');
}
mvaddch(row*2,line*3,'|');
refresh(); //將緩衝區的所有字元都列印到螢幕上
}
int check_win(void){ //看看遊戲有沒有贏
int count=0; //用於統計未探索的雷區的數量
for(int i=0;i<row;i++)
for(int j=0;j<line;j++)
count+=1-f[i][j]; //因為未探索是0,已探索是1,只有未探索才+1
return count==MINENUM; //剩下的雷區的數量就是雷的總數,說明雷全找出來了
}
如果中途按q
退出,會判定為輸
我輸的時候
當然也有贏的時候
通過這次用ncurses寫掃雷的嘗試,除了加深對c語言和ncurses庫的理解和熟練度,更讓我瞭解到思路的重要性。c語言和ncurses庫只是工具,如果思路不清晰明朗就很難完成目標,反之是總有辦法實現的。所以在敲程式碼之前,尤其是做複雜的任務的時候,一定要先做需求分析整理思路,將需要的資料和功能都剖析出來,這樣對程式碼的實現非常有利。養成這種良好的習慣對於提高效率和程式碼品質是十分有利的。