用C語言編寫小遊戲——“井字棋”

chengxuyuan997發表於2018-07-27

 

用C++編寫遊戲容易嗎?有什麼開源的小遊戲嗎?能分享一下嗎? 這個答案中,我提到學習遊戲程式設計可從回合制遊戲開始,例如井字棋。

考慮到一些初學者的學習需求,我就寫一個井字棋的教程吧。


關於怎麼快速學C/C++,可以加下小編的C/C++學習群:341+636+727,不管你是小白還是大牛,小編我都歡迎,不定期分享乾貨,歡迎初學和進階中的小夥伴。

每天晚上20:00都會開直播給大家分享C/C++遊戲程式設計學習知識和路線方法,群裡會不定期更新最新的教程和學習方法,最後祝所有程式設計師都能夠走上人生巔峰,讓程式碼將夢想照進現實

 

 

1. 遊戲狀態的表示

首先,我認為表示方法(representation)是程式設計中應最先要考慮的事情。對於回合制遊戲,我們需要儲存一個回合中的遊戲狀態(game state)。

以下用一個結構體表示井字棋一個回合中的狀態,並加入函式作初始化:

typedef struct {
    int board[3][3];    // -1 = empty, 0 = O, 1 = X
    int turn;           // O first} state;void init(state* s) {
    int i, j;
    for (j = 0; j < 3; j++)
        for (i = 0; i < 3; i++)
            s->board[j][i] = -1;
    s->turn = 0;}

 

以上用二維陣列儲存棋盤(board)是其中一種表示方式,另一種方式則是記錄每個回合下棋子的位置。我們採用前者是因為它較容易實現勝負判定。有些回合制遊戲可能使用冗餘的表示方式,以方便實現各種規則。

而使用結構體而不是直接用全域性變數,可帶來一些優點,例如增強可讀性及內聚性。

 

2. 顯示遊戲狀態

編寫遊戲時,我們通常希望先顯示遊戲狀態,之後才加入其他規則,因為這樣可以方便測試。

我希望用這樣的文字顯示遊戲狀態,當空置時寫上位置編號(1-9),以方便玩家輸入下棋位置:

 1 | 2 | 3 
---+---+---
 4 | 5 | 6 
---+---+---
 7 | 8 | 9 

 

簡單直白地編寫程式碼的話:

void display(const state* s) {
    int i, j;
    for (j = 0; j < 3; j++) {
        for (i = 0; i < 3; i++) {
            switch (s->board[j][i]) {
                case -1: printf(" %d ", j * 3 + i + 1); break;
                case  0: printf(" O "); break;
                case  1: printf(" X "); break;
            }
            if (i < 2)
                printf("|");
            else
                printf("\n");
        }
        if (j < 2)
            printf("---+---+---\n");
        else
            printf("\n");
    }}

 

由於 display() 只讀而不改變遊戲狀態,所以其引數型別為 const state*

 

我們稍壓縮一下程式碼:

void display(const state* s) {
    int i, j;
    for (j = 0; j < 3; printf(++j < 3 ? "---+---+---\n" : "\n"))
        for (i = 0; i < 3; putchar("||\n"[i++]))
            printf(" %c ", s->board[j][i] == -1 ? '1' + j * 3 + i : "OX"[s->board[j][i]]);}

 

我們可以加入 main() 函式去顯示初始化的狀態:

int main() {
    state s;
    init(&s);
    display(&s);}

 

 

 

3. 實現下棋

然後,我們加入第一個遊戲規則,就是下棋:

int move(state* s, int i, int j) {
    if (s->board[j][i] != -1)
        return 0;
    s->board[j][i] = s->turn++ % 2;
    return 1;}

 

函式內做了一個合法性判斷,如果該位置已有棋子,則返回 0 表示失敗。成功的話,在偶數回合填入 0,表示 O;奇數回合填入 1,表示 X;然後都把回合加一。

 

更改 main() 簡單測試:

int main() {
    state s;
    init(&s);
    display(&s);
    move(&s, 1, 1);
    display(&s);
    move(&s, 0, 1);
    display(&s);}

 

輸出:

 

 

 

4. 處理輸入

在每一回閤中,提示當前玩家(O 或 X),並讓玩家輸入一個下棋位置(1-9),如果位置不合法,則重新輸入:

void human(state* s) {
    char c;
    do {
        printf("%c: ", "OX"[s->turn % 2]);
        c = getchar();
        while (getchar() != '\n');
        printf("\n");
    } while (c < '1' || c > '9' || !move(s, (c - '1') % 3, (c - '1') / 3));}

 

在標準輸入中,要到Enter鍵才能處理輸入,所以這裡我們讀了第一個輸入字元後,就忽略其他字元直到讀到換行符。我們把表示位置的字元轉換成二維陣列索引。

 

然後,就可以修改 main() 實現二人下棋的流程:

int main() {
    state s;
    init(&s);
    display(&s);
    while (s.turn < 9) {
        human(&s);
        display(&s);
    } }

 

 

5. 勝負判定

眾所周知,井字棋的勝利條件,是有三個棋子在橫線、直線或斜線連成一線。我們實現一個 evaluate() 函式去評估棋局的狀態,如果 O 勝出則返回 1,X 勝出則返回 -1,不分勝負則返回 0:

#define CHECK(j1, i1, j2, i2, j3, i3) \    if (s->board[j1][i1] != -1 && s->board[j1][i1] == s->board[j2][i2] && s->board[j1][i1] == s->board[j3][i3]) \        return s->board[j1][i1] == 0 ? 1 : -1;int evaluate(const state* s) {
    int i;
    for (i = 0; i < 3; i++) {
        CHECK(i, 0, i, 1, i, 2);    // horizontal
        CHECK(0, i, 1, i, 2, i);    // vertical
    }
    CHECK(0, 0, 1, 1, 2, 2);        // diagnoal
    CHECK(0, 2, 1, 1, 2, 0);        // diagnoal
    return 0;}

上面的程式碼使用了一個巨集 CHECK() 去檢測三個位置是否都為相同的棋子,如是則直接返回勝方。

 

最後,我們在 main() 中,待每次下棋及顯示狀態後, 判定是否出現勝方,如果到達第 9 個回合(回合從 0 開始),則判定是平局(draw):

int main() {
    state s;
    init(&s);
    display(&s);
    while (s.turn < 9) {
        human(&s);
        display(&s);
        switch (evaluate(&s)) {
            case  1: printf("O win\n"); return 0;
            case -1: printf("X win\n"); return 0;
        }
    } 
    printf("Draw\n");}

 

 

6. 總結

本篇實現了二人井字棋,它是一個簡單的回合制遊戲。我們先選擇了遊戲的狀態表示方式(state結構體及init()函式),然後把狀態以文字形式顯示(display()函式),加入每回合下棋規則(move()函式),以及人類玩家的輸入處理(human()函式),並作勝負判定(evaluate()函式),最後在main()裡則實現了按回合的迴圈及輸出勝負結果。

雖然這個遊戲本身以及 60 行的示例程式碼都很簡單,但這個框架可以用於實現其他(更復雜的)回合制遊戲。實時遊戲(如動作遊戲)的主要區別,其實也只在於把輸入部分做成非阻塞的函式,而該迴圈則稱為遊戲迴圈(game loop)。

相關文章