01-0002 C++實現控制檯五子棋[缺少AI]

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();//卡屏
            }
                
        }
    }
    
}

備註:上面有詳盡註釋,要仔細看註釋。

  1. 其中的cin.ignore()函數是爲了在應對輸入了3、4個char字符之後,只讀入兩個的情況。
  2. cin.get()函數可能在不同的系統上面,需要的數目不一樣,根據測試結果進行添加。
  3. 其中的board.show_board(*p)的返回值沒有用到,本應該判斷輸入情況的。
  4. 在之後的版本中應該將玩家輸入的判斷放在函數中,把模式的選擇放在函數中,可能Chessboard類會變得很大
  5. 還有AI部分需要擴展,可能需要一個臨時的二維數組,需要新建一個類,輔助其中一個玩家下棋。

4.AI部分[有待實現]

cin.get();//卡屏,有待實現

5.Gif展示

5.1 棋盤展示

在這裏插入圖片描述

5.2 下棋展示

在這裏插入圖片描述

6.源碼

五子棋源碼-C++[缺少AI]
備註:VS2019,可以直接導圖工程文件,或者自己一個個的添加。

發佈了16 篇原創文章 · 獲贊 9 · 訪問量 4677
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章