用C語言實作井字棋
在學習完電磁炮的二連載之後,今天我将帶領大家學習用c語言編寫雙人井字棋的遊戲。
01
遊戲狀态的表示
首先,我認為表示方法(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)是其中一種表示方式,另一種方式則是記錄每個回合下棋子的位置。我們采用前者是因為它較容易實作勝負判定。有些回合制遊戲可能使用備援的表示方式,以友善實作各種規則。
而使用結構體而不是直接用全局變量,可帶來一些優點,例如增強可讀性及内聚性。
02
顯示遊戲狀态
編寫遊戲時,我們通常希望先顯示遊戲狀态,之後才加入其他規則,因為這樣可以友善測試。
我希望用這樣的文本顯示遊戲狀态,當空置時寫上位置編号(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);
}
03
實作下棋
然後,我們加入第一個遊戲規則,就是下棋:
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);
}
04
處理輸入
在每一回合中,提示目前玩家(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 '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);
}
}
05
勝負判定
衆所周知,井字棋的勝利條件,是有三個棋子在橫線、直線或斜線連成一線。我們實作一個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); // diagonal
CHECK(0, 2, 1, 1, 2, 0); // diagonal
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");
}
06
總結
本篇實作了二人井字棋,它是一個簡單的回合制遊戲。我們先選擇了遊戲的狀态表示方式(state結構體及init()函數),然後把狀态以文本形式顯示(display()函數),加入每回合下棋規則(move()函數),以及人類玩家的輸入處理(human()函數),并作勝負判定(evaluate()函數),最後在main()裡則實作了按回合的循環及輸出勝負結果。
雖然這個遊戲本身以及 60 行的示例代碼都很簡單,但這個架構可以用于實作其他(更複雜的)回合制遊戲。實時遊戲(如動作遊戲)的主要差別,其實也隻在于把輸入部分做成非阻塞的函數,而該循環則稱為遊戲循環(game loop)。
看完這篇推文,大家可以快去自己動手實踐一下啦!
關注獵狐有驚喜!
文案|丁一如
排版|王金婷