在Windows控制台下使用C語言實現俄羅斯方塊

在Windows的控制台下使用C語言實現俄羅斯方塊

這是CSAPP課程的作業,助教問了我情況之後就愉快的決定了
稍微思考了一下,因為是經典的遊戲程序,本身邏輯並不是很複雜,而且肯定也有很多類似的程序了。這裡僅僅提供自己的思路。

簡單思路

俄羅斯方塊本質上來講,是一個在二維地圖上進行操作的遊戲,自然我們需要一張二維圖來表示這個遊戲地圖。然而我們使用的是C語言。。。。好吧我們定義一個int二維數組的全局變量作為這張地圖的表示。

二維地圖也要遵守基本法,自然我們需要給這張圖上每個點做一個定義。當這個點為0的時候,表示這個地方沒有方塊。這個點為1的時候,表示這個點存在一個已經落地的方塊。當這個點大於1的時候,表示這是一個可以活動的點。至於這個可以活動的點具體數值代表什麼,我們之後討論。

我們需要一個對方塊進行操作的函數。這個函數用於處理可活動方塊,包括左右移動和向下活動。如果按下↑鍵,則循環調用向下運動直到觸底。

當進行方塊操作時,我們需要一些判定。比如方塊是否觸底,觸底之後方塊是否足夠一行(一整行就消去然後進行分數累加)等等操作。

我們需要一個對時間進行計數的計數器。為了處理這個時間,我開了一個線程用於統計。每當時間間隔超過1s,自動調用向下移動的函數。

之後是翻轉。這個是最難的操作。稍微將俄羅斯方塊的方塊類型分個類,其中2x2的方塊無需翻轉;正反L形方塊可以放在3x3的矩陣中翻轉,並且翻轉方向是90°x4次;正反閃電形方塊也可以放在3x3的矩陣中進行翻轉,翻轉方向是一次順時針,一次逆時針;“凸”形的方塊和L形類似,可以翻轉四次,並且在3x3的矩陣中判斷;長條形方塊由於有四個,可以單獨進行一次判斷,翻轉方向為順時針、逆時針各一次。具體可以參考後文“翻轉詳解”

文件構成

一個好的程序,就算代碼量不多,也應該具有良好的可讀性和可維護性。所以我將文件分為

  • main.c
  • action.c
  • block.c
  • renderer.c
  • timer.c

main.c

這裡用作處理主線程,包括定義全局變量,處理控制台,開啟後台進程,程式啟動參數設置,隨機一個方塊類型,重置地圖,監聽鍵盤事件等等。

其中監聽鍵盤事件是通過conio.h中的getch()函數實現,並且使用while (1)進行持續監聽。捕捉鍵盤事件之後進行操作。

action.c

這裡主要處理地圖的一些操作。比如封裝了一個操作用來給地圖x,y坐標處賦值n:void set(int xn, int yn, int n)。方塊的移動函數就放在這裡面void moveAction(int d)。

block.c

這裡主要放置用於處理方塊的函數,比如新建方塊:newTetris();凍住方塊(把所有值大於1的坐標值設置成1):frozen()。

renderer.c

這裡用於存放繪製界面的函數。應用了雙重緩衝技術,可以有效的避免閃屏現象,也無需使用goto函數進行坐標定位。

雙重緩衝的原理是這樣的,先新建一個console窗體用作緩衝區,但是此時緩衝區不會顯示。待繪製完成之後,將緩衝區顯示在屏幕上,這樣就避免了print字符時的單行刷新感。

    HANDLE hNewConsole = CreateConsoleScreenBuffer(GENERIC_WRITE | GENERIC_READ,
                         0,
                         NULL,
                         CONSOLE_TEXTMODE_BUFFER,
                         NULL);
    CONSOLE_CURSOR_INFO cci = {1, 0};
    SetConsoleCursorInfo(hNewConsole, &cci);

        /* ......Other code...... */

    WriteConsole(hNewConsole, *statusInfo, strlen(*statusInfo), NULL, NULL);
    WriteConsole(hNewConsole, *scoreInfo, strlen(*scoreInfo), NULL, NULL);
    WriteConsole(hNewConsole, scoreStr, strlen(scoreStr), NULL, NULL);
    SetConsoleActiveScreenBuffer(hNewConsole);

renderer.c

這裡用於運行後台線程(計時用)。因為我只需要以秒為最小單位,所以我調用了函數time()來獲取UNIX時間戳。實際應用中可以調用stdlib.h庫中的clock(),最小單位是ms。

翻轉詳解

翻轉是一個讓人頭疼的事,我定義了幾種處理方法。除了2x2的方塊以外,其他方塊都包含一個中心塊。中心塊的值為3, 4, 5, 6。

當中心塊為3或者4的時候,對以此為中心塊的3x3矩陣進行一次順時針旋轉。旋轉之前會判斷一次是否會與已經固定的方塊碰撞或者撞墻(即無法翻轉)。如果無法翻轉,則不進行操作。中心塊為3和4的區別是,當中心塊為4的方塊進行翻轉后,將4改為5。

那麼就可以很容易知道了,中心塊為5的方塊是進行逆時針翻轉判定用的。

中心塊為6時,表示對長條形進行特殊的判斷,判斷能否翻轉,邏輯與上文類似。

以下是rotate()的源碼

void rotate() {
    //If is lost, return
    if (gameStatus == 2) {
        return;
    }
    bool isFind = false;
    //central x and y (1, 1)
    int cenX, cenY;
    //Find where is the central block
    for (cenY=0; cenY<y; cenY++){
        for (cenX=0; cenX<x; cenX++){
            if (tetMap[cenY][cenX] > 2){
                isFind = true;
                break; 
            }
        }
        if (isFind){
            break;
        }
    }
    int tempMap[10][10];
    int i, j;
    switch (tetMap[cenY][cenX]){
        /* ClockWice */ 
        case 3:
        case 4:
            if (cenX == 0 || cenX == x-1){
                break;
            }
            /* Generate a temp map which size is(3x3) */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    tempMap[i-(cenX-1)][2+(cenY-1)-j] = tetMap[j][i];
                }
            }
            /* Compare two maps and transport values */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tempMap[j-(cenY-1)][i-(cenX-1)] == 2 && tetMap[j][i] == 1){
                        return;
                    }
                }
            }
            /* If can, rotate */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tetMap[j][i] == 2){
                        tetMap[j][i] = 0;
                    }
                }
            }
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tempMap[j-(cenY-1)][i-(cenX-1)] == 2){
                        tetMap[j][i] = tempMap[j-(cenY-1)][i-(cenX-1)];
                    } 
                }
            }
            //If the central block is 4, set it to 5
            if (tetMap[cenY][cenX] == 4){
                tetMap[cenY][cenX] = 5;
            }
            break;
        /* AntiClockWice */ 
        case 5:
            if (cenX == 0 || cenX == x-1){
                break;
            }
            /* Generate a temp map which size is(3x3) */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    tempMap[2+(cenX-1)-i][j-(cenY-1)] = tetMap[j][i];
                }
            }
            /* Compare two maps and transport values */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tempMap[j-(cenY-1)][i-(cenX-1)] == 2 && tetMap[j][i] == 1){
                        return;
                    }
                }
            }
            /* If can, rotate */
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tetMap[j][i] == 2){
                        tetMap[j][i] = 0;
                    }
                }
            }
            for (j=cenY-1; j<cenY+2; j++){
                for (i=cenX-1; i<cenX+2; i++){
                    if (tempMap[j-(cenY-1)][i-(cenX-1)] == 2){
                        tetMap[j][i] = tempMap[j-(cenY-1)][i-(cenX-1)];
                    } 
                }
            }
            //If the central block is 5, set it to 4
            if (tetMap[cenY][cenX] == 5){
                tetMap[cenY][cenX] = 4;
            }
            break;
        /* Resolve rectantangle */
        case 6:
            if (cenX == 0 || cenX > x-3 && tetMap[cenY-1][cenX] == 2){
                break;
            }
            if (tetMap[cenY-1][cenX] == 2){
                if (
                  tetMap[cenY][cenX-1]+
                  tetMap[cenY][cenX+1]+
                  tetMap[cenY][cenX+2] == 0
                ){
                    tetMap[cenY][cenX-1] = 2;
                    tetMap[cenY][cenX+1] = 2;
                    tetMap[cenY][cenX+2] = 2;
                    tetMap[cenY-1][cenX] = 0;
                    tetMap[cenY+1][cenX] = 0;
                    tetMap[cenY+2][cenX] = 0;
                }
            }
            else{
                if (
                  tetMap[cenY-1][cenX]+
                  tetMap[cenY+1][cenX]+
                  tetMap[cenY+2][cenX] == 0
                ){
                    tetMap[cenY][cenX-1] = 0;
                    tetMap[cenY][cenX+1] = 0;
                    tetMap[cenY][cenX+2] = 0;
                    tetMap[cenY-1][cenX] = 2;
                    tetMap[cenY+1][cenX] = 2;
                    tetMap[cenY+2][cenX] = 2;
                }
            }
        /* No action */
        default:
            break;
    } 
}

源碼地址

這玩意兒已經在Github上開源,地址為https://github.com/HoshinoTouko/Tetris

标签: 俄羅斯方塊, C, CSAPP