C++實現控制檯的五子棋
1.什麼樣子的程序?
1.1 控制檯 || 應用界面?
如果是控制檯:繪製界面的函數需要自己寫,不是很方便,在不同的電腦下顯示的還有可能不一樣,顯示起來不是特別的美觀,但是控制檯的刷新很快,人眼一般難以識別,這個程序主要是爲了寫個五子棋,不求太多。
如果有應用界面:自然是美觀,但是需要學習圖形庫,上次的那個EasyX是真個用的噁心,需要些界面與操作之間的交互,會比較麻煩,一大堆的字符轉化也會讓碼代碼的過程變得不愉快。
決定:直接在控制檯繪製界面,使用製表符來繪製棋盤。
1.2 面向過程 || 面向對象?
面向過程:一大堆的函數,main()中的邏輯會很長,寫代碼的時候很亂,但是可以下手就開始寫,不用想太多,調試就好了。
面向對象:各司其職,不同的人設計出來的邏輯結構也不一樣,C++推出三種編程模式:過程編程,面向對象,泛型編程,這其中有先後關係,使用面向對象會不會高級一點?會不會更加簡單合理一點?
決定:面向對象寫這個程序。
1.3類的劃分 && 信息存儲?
棋子要有,玩家要有,棋盤要有,這是最基本的三個類。
玩家落子,棋盤容納棋子,棋盤存儲每個棋子的信息,棋盤甚至可以盤算哪一方贏了。玩家落子的時候,棋盤接收一個棋子然後改變棋盤的信息,玩家下了n=1個棋子。
棋盤的信息,自然而然的想到用二維數組[動態的],而玩家擁有的棋子信息,用vector、stack、list等存儲都可以,我選擇了vector。
最終:只設置上述三個類,使用動態二維數組存儲棋子信息,使用vector來存儲棋手落子的信息。
1.4 輸入座標 || 上下左右鍵?
輸入座標:設想了幾種輸入座標的方式:帶括號的輸入數字,或者字母,中間用逗號或者是空格隔開,輸入的檢測需要額外的代碼實現。
方向鍵:方向鍵會方便一點,但是這個時候需要設置光標提示玩家在屏幕中的位置,此時需要多來一部分代碼實現,就會破壞棋盤繪製的邏輯。
最終:決定用a-o的字幕表示棋盤行列,輸出的時候不用考慮兩位數佔位問題。輸入的格式爲“ab”、“a b”,不帶引號,中間可以有空格或者無空格。[利用了cin的特性]
相關鏈接:
鏈接1:10000以內unicode對照表
鏈接2:markdown修改字體、顏色
鏈接3:五子棋源碼-C++
2.幾個類的劃分&&負責的功能
2.1 Piece類
代碼:
#pragma once
class Piece {
public:
int x;//x座標
int y;//y座標
char color;//顏色'B'or'W'
Piece(int, int, char);//通過座標與顏色初始化
~Piece();//析構函數顯式[說不定以後會有用]
};
備註:上述文件爲Piece.h文件,是棋子類。文件實現起來很簡單,此處不給出cpp文件的實現。由於方便操作,這裏直接將所有的內容都設置成了public。
2.2 Player類
代碼:
#pragma once
#include "Piece.h"
#include <vector>
#include <string>
using namespace std;
class Player {
public:
vector<Piece> p;//玩家擁有棋子
char kind;//玩家是哪一方
string p_inf;//玩家的個人信息,之後需要拆開,看着更加的正規一點,目前假定所有的個人信息都一樣
//char mode;//棋手是電腦還是普通玩家,模式選擇的時候使用
Player();
Player(char);//規定玩家是哪一方
~Player();//事實上要清楚vector中的元素,其他的不用管
Piece * drop_gobang(int,int);//玩家落子,產生一個棋子加入隊列,記錄玩家落子的信息,並返回該棋子,在棋盤中有所顯示。表示玩家要在哪裏落子,步驟就是先生成一個棋子,然後將棋子放在那個位置,邏輯合理。
};
備註:上述文件爲Player.h,而且註釋已經很清楚了,爲更加明確,需要格外的注意其中的玩家信息,在後期玩家有各種各樣的信息,需要將其拆開,來記錄玩家的所有信息,相應的構造函數需要新加重載。一定要仔細的看註釋。析構函數需要單獨寫,後面會提到。
2.3 Chessboard類
代碼:
#pragma once
#include "Piece.h"
#include <iostream>
using namespace std;
class Chessboard {
public:
int x_axis;//橫線數目
int y_axis;//豎線數目
int** board;//動態二維數組
Chessboard();//設置默認參數,15*15,初始化動態二維數組的相關信息
Chessboard(int,int);//設置可拓展棋盤
void show_board();//繪製棋盤
bool show_board(const Piece&);//重載,添加棋子並判斷是否成功添加,成功返回true,否則返回false,修改二維數組並重新繪製棋盤
void show_rule();//單純的展示規則,按鍵之後刷新
bool referee(const Piece&);//判斷新的落子是否獲勝,如果贏了,就展示獲勝的信息
void reset();//重置棋局
private:
void ini_board_arr();//初始化棋盤,就最開始調用一次,設置成了私有
};
備註:上述文件爲Chessboard.h,每一個變量或者函數的含義都在註釋裏面了,一定要仔細看註釋。
注意下面兩函數:
bool show_board(const Piece&);//重載,添加棋子並判斷是否成功添加,成功返回true,否則返回false,修改二維數組並重新繪製棋盤
bool referee(const Piece&);//判斷新的落子是否獲勝,如果贏了,就展示獲勝的信息
函數1:返回bool類型是爲了方便在main函數中寫輸入邏輯。可以設置一下返回int類型,判斷是哪種輸入錯誤,或者是改成void類型,在main函數中進行判斷,然後直接輸入。
函數2:判斷是否有獲勝的信息,任意一方獲勝都應該停止棋局。這裏無需傳遞int類型的值,當前棋手落子之後出現獲勝局面一定是他獲勝,不需要額外的信息。
2.4 整體結構
備註:參考上述三個類的之間的include關係,可以得到結構,此處不再給出。
3.函數解釋
3.1 Piece* Player::drop_gobang(int x, int y);
//玩家落子
Piece* Player::drop_gobang(int x, int y) {
Piece *piece = new Piece(x, y, this->kind);
p.push_back(*piece);//這些內存後來都會在Player的析構函數中刪除掉
return piece;
}
備註:這裏存儲玩家棋子信息,可能對以後的AI有所幫助,但是實際上並沒有太大作用,單純方便拓展。
3.2 Player::~Player();
//清空向量空間
Player::~Player() {
vector<Piece>().swap(this->p);//與臨時對象交換以釋放內存
}
備註:Vector清空之後並不會減少容量,clear函數只是清空了每一個數據塊之中的數據,但是這個空間還是vector的。上述方法是與臨時對象交換,臨時對象會在該函數週期結束之後自動銷燬,而這個vector就會變成空,是一個效率挺高,代碼挺少的方法。
3.3 void Chessboard::reset();
//重置棋盤數組
void Chessboard::reset() {
for (int i = 0; i < x_axis; i++) {
for (int j = 0; j < y_axis; j++) {
if (i == 0 || j == 0 || i == x_axis - 1 || j == y_axis - 1)
board[i][j] = 9;
else
board[i][j] = 0;
}
}
}
備註:加入棋盤是1515的,這裏繪製的是1717的,在外圍的一圈都標註成9,棋盤位置標註成0,如果是黑子,將會是1,白子是-1,是爲了方便檢測,才這樣刻意設置。[大了一圈]
3.4 void Chessboard::ini_board_arr();
//初始化棋盤數組--分配空間
void Chessboard::ini_board_arr() {
board = new int* [x_axis];
for (int i = 0; i < x_axis; i++) {
board[i] = new int[y_axis];
}
reset();
}
備註:如上述方法,初始化動態二維數組,然後調用了reset()函數,再對數據進行初始化。[先分配空間,然後初始化數據]
3.5 void Chessboard::show_board();
//繪製棋盤,通過製表符等
void Chessboard::show_board() {
//先不要管是什麼字體,能用就好,最終是要直接讀取鼠標信息的
cout << " ";
for (int i = 1; i < this->x_axis-1; i++) {
cout << (char)(i + 97-1) << " ";
}
cout << endl;
for (int i = 1; i < this->x_axis-1; i++) {
for (int j = 0; j < this->y_axis-1; j++) {
if (j == 0)
cout << (char)(i + 97-1);
else if (this->board[i][j] == 0) {
if (j > 1 && j < this->y_axis - 2)
cout << (i == 1 ? "┬─" : i == (x_axis - 2) ? "┴─" : "┼─");
else if (j == 1)
cout << (i == 1 ? "┌─" : i == (x_axis - 2) ? "└─" : "├─");
else if(j==(y_axis-2))
cout << (i == 1 ? "┐" : i == (x_axis - 2) ? "┘" : "┤");
}
else if (this->board[i][j] == 1)
cout << "●";
else
cout << "○";
}
cout << endl;
}
}
步驟:
- 1.兩個必要的循環,分別繪製行、列
- 2.繪製行分爲兩部分,第一行,與剩下的15行。第一行是ASCII碼轉字符繪製每一列的標識,剩下的15行分別繪製[此時看列]。
- 3.15列中的第一列是字母,轉換後輸出。
- 4.第二列的第一行是什麼,中間是什麼,最後一行是什麼。
- 5.中間幾列的第一行是什麼,中間是什麼,最後是什麼。
- 6.最後一列的第一行是什麼,中間是什麼,最後是什麼。
- 7.如果其中有一些地方有值,需要進行替換,分別對應黑子白子。
備註:步驟很白話,簡單易懂,不解釋。
cout << (i == 1 ? "┬─" : i == (x_axis - 2) ? "┴─" : "┼─");
備註:三元表達式接連使用,如果是第一行,就使用"┬─",否則判斷是否是最後一行,如果是則"┴─",否則就是在中間,使用"┼─"。
3.6 bool Chessboard::referee(const Piece& p);
//落子後檢查是否獲勝,棋盤是0-16,其中0、16行列是餘出來方便檢測的
bool Chessboard::referee(const Piece& p) {
bool dir=true;//判斷正向查,還是反向查
int cha_n=0;//判斷折回查詢的次數
int t_x = p.x;//落子點的x座標,存儲起來,作爲逆向查找的起點
int t_y = p.y;//落子點的y座標,存儲起來,作爲逆向查找的起點
int t_r = this->board[t_x][t_y];//當前棋子所代表的值
int d[4][2] = { {1,1},{1,-1},{0,1},{1,0} };//二維數組,規定查找的方向
//分爲四個方向進行檢測
stack<int> s;
for (int i = 0; i < 4; i++) {
stack<int>().swap(s);//清空棧內元素[清理緩存]
s.push(1);//將當前棋子壓入棧中
cha_n = 0;//交換次數重置爲0
while (cha_n < 2) {
//根據dir判斷應該查找的方向,可以將三元表達式拆開來看,會變得很清楚
if ((dir ? this->board[t_x+=d[i][0]][t_y+= d[i][1]] : this->board[t_x+=(-d[i][0])][t_y+=(-d[i][1])]) == t_r)
s.push(1);//遇到一個相同顏色的棋子就入棧一個
else {
dir = !dir;//設置反向查找,第二次反向的時候剛好進入下一輪
t_x = p.x;//反向的時候應當歸位
t_y = p.y;
cha_n++;//反向一次,就記錄一次
}
}
if (s.size() >= 5)
return true;
}
return false;
}
備註:上述代碼是我能想到的最簡單的代碼了,設定四個正方向,設置逆向查找的標誌,設置一個棧來存儲能夠找到的信息,一般來說斜着五子的結果比較多,所以將斜方向判斷放在前面,設置一個二維數組用來存儲方向信息。每次反向的時候重置查找起始位置的信息,並將反向標誌dir取反,記錄反向的次數,如果是反向了兩次就跳出循環,此時的位置信息,dir等已經被重置,判斷棧裏面的元素是不是超過5個,是的話就說明出現勝利的局面,不是的話進入下一輪循環[記得重置記錄量][一定要仔細看]。
備註:由於之前設置棋盤的時候多設置了外面一圈,數字爲9,所以在這個檢測函數裏面,不需要對邊緣進行檢測,因爲始終都在圈內查找。
3.7 main函數中的邏輯
int main() {
//如何在C++中輸出Unicode編碼的字符
Chessboard board;//定義棋盤,調用空構造函數,進行初始化
Player p1('B');//定義並初始化玩家一
Player p2('W');//定義並初始化玩家二
Piece* p;//臨時棋子,指針類型
bool win_signal;//勝利信號,用來判斷是否當前局面可以定勝負
bool draw_signal;//根據玩家輸入判斷是否成功下棋
char x, y;//棋子的位置,需要輸入
int player_contral;//控制應該下棋的玩家
system("mode con cols=33 lines=20");//初始化界面大小
system("color 09");//修改界面字體的顏色,0是背景色,9是字體色
board.show_rule();//展示五子棋的規則
while (1) {//循環下棋
win_signal = false;//初始化,剛開始無人勝利
system("cls");//清屏
player_contral = 1;//P1先落子
board.reset();//重置棋盤信息
board.show_board();//顯示棋盤
while (!win_signal) {
cout << "請" << (player_contral % 2 == 1 ? "P1" : "P2") << "輸入落子位置:";
draw_signal = false;
while (!draw_signal) {//落子不成功就循環
cin >> x >> y;//讀入兩個數據
cin.ignore();//清除當前行多餘的數據
if (x < 'a' || x>'o' || y < 'a' || y>'o') {//如果超出行列範圍
system("cls");//清屏
board.show_board();//不改變信息,重新顯示棋盤
cout << "落子在棋局之外,請" << (player_contral % 2 == 1 ? "P1" : "P2")<<"重新輸入:";
}
else if (board.board[(int)x - 97 + 1][(int)y - 97 + 1] != 0) {//如果輸入的位置已經有棋子
system("cls");//清屏
board.show_board();//重示棋盤
cout << "此處已落子,請" << (player_contral % 2 == 1 ? "P1" : "P2") << "重新輸入:";
}
else
draw_signal = true;//否則落子成功,會跳出循環↑
}
if (player_contral++ % 2 == 1)//判斷是否是玩家1落子
p = p1.drop_gobang((int)x - 97 + 1, (int)y - 97 + 1);//轉成int類型,因爲第一行第一列是邊界,需要加上1
else
p = p2.drop_gobang((int)x - 97 + 1, (int)y - 97 + 1);
system("cls");
board.show_board(*p);//落子,並重新展示信息
win_signal = board.referee(*p);//是否獲勝
if (win_signal) {//如果獲勝
cout << ((player_contral - 1) % 2 == 1 ? "P1" : "P2") << "獲勝!!!\n是否再來一局,按任意鍵繼續...";
cin.get();//吸收enter字符,好開始下一輪
cin.get();//卡屏
}
}
}
}
備註:上面有詳盡註釋,要仔細看註釋。
- 其中的cin.ignore()函數是爲了在應對輸入了3、4個char字符之後,只讀入兩個的情況。
- cin.get()函數可能在不同的系統上面,需要的數目不一樣,根據測試結果進行添加。
- 其中的board.show_board(*p)的返回值沒有用到,本應該判斷輸入情況的。
- 在之後的版本中應該將玩家輸入的判斷放在函數中,把模式的選擇放在函數中,
可能Chessboard類會變得很大。 - 還有AI部分需要擴展,可能需要一個臨時的二維數組,需要新建一個類,輔助其中一個玩家下棋。
4.AI部分[有待實現]
cin.get();//卡屏,有待實現
5.Gif展示
5.1 棋盤展示
5.2 下棋展示
6.源碼
五子棋源碼-C++[缺少AI]
備註:VS2019,可以直接導圖工程文件,或者自己一個個的添加。