用ncurses庫寫掃雷

2020-09-28 16:00:11


前言

經過一整子的瞎折騰,我終於完成了ncurses版的掃雷。這讓我進一步認識到了ncurses庫的強大。這裡主要的新知識點是"滑鼠事件的獲取和處理",可以參考這篇文章,雖然排版湊合但還是挺實用的。相比寫情詩,掃雷的程式碼量還是比較大的,我的版本前後有100多行的樣子。我用思維導圖整理了我的思路,一個功能基本就一個函數。擴充套件功能我這裡完成了前兩個,但已經夠用了,掃雷的基本操作測試下來很流暢。

一、思路

在這裡插入圖片描述

二、程式碼實現

1.標頭檔案

#include <stdlib.h>         //要用到srand()和rand()生成雷
#include <time.h>           //要用到clock()生成亂數種子
#include <ncurses.h>		//主角不用解釋

2.全域性變數和宏定義

#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;					//用於記錄輸贏的資訊

3.main()

初始化

    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模式

4.功能函數

生成雷區new_mine()

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);
            }
}

遊戲主要操作operate()

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);
        }
    }
}

顯示雷區影象display()

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();						//將緩衝區的所有字元都列印到螢幕上
}

檢查贏check_win()

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庫只是工具,如果思路不清晰明朗就很難完成目標,反之是總有辦法實現的。所以在敲程式碼之前,尤其是做複雜的任務的時候,一定要先做需求分析整理思路,將需要的資料和功能都剖析出來,這樣對程式碼的實現非常有利。養成這種良好的習慣對於提高效率和程式碼品質是十分有利的。