プロジェクトリソースのダウンロード
- C++ ベースの AI バックギャモン ゲーム プロジェクトのソース コード圧縮パッケージのダウンロード アドレス
- C++ ベースの AI バックギャモン ゲーム プロジェクトのソース コードの Github ダウンロード アドレス
- C++ ベースの AI バックギャモン ゲーム プロジェクトに必要なマテリアル
- C++ ベースの AI バックギャモン ゲーム プロジェクトには EasyX が必要です
プロジェクトの説明
このプロジェクトは C++ 開発に基づいており、全体的には比較的単純です。人間と AI のバックギャモン ゲームを実現し、勝敗の判定や効果音の追加などを行うことができます。私のブログの詳細なチュートリアルに従うのは間違いなく問題ありません。一歩ずつ!
プロジェクト開発ソフトウェア環境
- Windows11
- VS2017
- イージーX
プロジェクト開発ハードウェア環境
- CPU:インテル® Core™ i7-8750H CPU @ 2.20GHz 2.20 GHz
- RAM:24GB
- GPU:NVIDIA GeForce GTX 1060
記事ディレクトリ
序文
以下は、C++ ベースの AI バックギャモン ゲーム プロジェクトの詳細な開発チュートリアルです。各ステップの詳細なメモと図を作成しました。私の手順を段階的に実行する限り、読者は独自の AI バックギャモンを実現できると思いますもちろん、読者の皆様、最高の結果を達成するために、自分の好みに応じてゲーム プロジェクトのマテリアルを調整することもできます。以下がこの記事の全内容です!
ゼロ、プロジェクトのデモ
0.1 マンマシン バックギャモン ゲーム
0.2 黒 (チェスプレイヤー) の勝ち
-
黒 (チェスプレイヤー) 勝利ボード:
-
黒チェス(棋士)の勝利判定結果:
2.3 白 (AI) の勝ち
-
白 (AI) 勝利ボード:
-
ホワイトチェス(AI)勝利判定結果:
1. プロジェクトを作成する
-
Microsoft Visual Studio(以下、VS)を開いた後、「新規」→「プロジェクト」をクリックします。
-
次に、プロジェクト名とプロジェクトの場所を入力し、「OK」をクリックします。
2. 原材料の輸入
-
プロジェクト内に新しい「リソース」フォルダーを作成し、プロジェクトの素材ファイルを保存できるようにします
-
プロジェクトで使用する素材をプロジェクト内の「リソース」フォルダーにインポートします。読者は自分の素材または私の素材を使用できます。私の素材のダウンロードリンクは上記のブログに設置されています
3. プロジェクトの枠組みの設計
3.1 デザインプロジェクトのフレームワーク
- プロジェクト全体の枠組みは次の図に示されており、すべてのコードは次の 4 つのクラスに従って記述されます。
- 男性 (チェスプレイヤー): チェスをプレイする人
- Chess (チェス盤): チェスがプレイされる場所
- AI(人工知能):チェスプレイヤーと対戦するAI
- ChessGame (ゲーム コントロール): ゲームの基本ロジックを制御します。
3.2 プロジェクトフレームワークに従ってクラスを設計する
-
設計したプロジェクトフレームワークに従って、一つ一つ構築していきます。まず Man (チェスプレイヤー) クラスを作成し、「ソース ファイル」を右クリックし、「追加」の「クラス」をクリックします。
-
「クラス名」に「Man」と入力し、「OK」をクリックすると、残りのファイルが自動的に生成されます
-
正常に生成されていることがわかります
-
Man (チェスプレイヤー) クラスと同じ方法で他の 3 つのクラスを作成します。最終的な効果は次の図に示されています。
4. ゲームのメインインターフェイスをデザインする
4.1 Chess (チェス盤) クラスのメイン インターフェイスを設計する
- Chess (チェス盤) クラスのメイン インターフェイスを Chess.h で設計します。これらのメイン インターフェイスは特に実装する必要はありませんが、外部にのみ公開され、外部使用を待機するときにカスタマイズできます。Chess.h のコードは次のとおりです。
#pragma once // 表示落子位置 struct ChessPos { int row; int col; }; // 表示棋子的种类 typedef enum { CHESS_WHITE = -1, // 白棋 CHESS_BLACK = 1 // 黑棋 }chess_kind; class Chess { public: // 棋盘初始化:加载棋盘的图片资源,初始化棋盘的相关数据 void init(); /* 判断在指定坐标(x,y)位置,是否是有效点击, 如果是有效点击,把有效点击的位置(行,列)保存在参数pos中 */ bool clickBoard(int x, int y, ChessPos *pos); // 在棋盘的指定位置(pos), 落子(chess) void chessDown(ChessPos *pos, chess_kind chess); // 获取棋盘的大小(13线、15线、19线) int getGradeSize(); // 获取指定位置是黑棋,还是白棋,还是空白 int getChessData(ChessPos *pos); int getChessData(int row, int col); // 检查棋局是否结束 bool checkOver(); };
4.2 AI(人工知能)クラスのメインインターフェースの設計
-
同様に、AI.h のコードは次のとおりです。
#pragma once #include "Chess.h" class AI { public: // 初始化 void init(Chess *chess); // AI下棋 void go(); };
4.3 Man (チェスプレイヤー) クラスのメインインターフェイスを設計する
-
同様に、Man.h のコードは次のとおりです。
#pragma once #include "Chess.h" class Man { public: // 初始化 void init(Chess *chess); // 下棋动作 void go(); };
4.4 ChessGame (ゲームコントロール) クラスのメインインターフェイスを設計する
-
同様に、ChessGame.h のコードは次のとおりです。
#pragma once class ChessGame { public: // 开始对局 void play(); };
4.5 各インターフェースの具体的な実装を設計する
-
これでプロジェクトの基本的なメイン インターフェイスが生成されましたが、その後のプロジェクト開発の使用を容易にするために、これらのインターフェイスを実装する必要があります。この時点で、新しく作成されたインターフェイス関数の下に緑色の波線があることがわかります。
-
この緑色の波線は、このインターフェイスの特定の実装が生成されていないため、このインターフェイスを実装する必要があることを VS が要求しているものです。緑色の波線の上にマウスを置いて、「考えられる修正を表示」をクリックするだけです。
-
次に、赤いボックスでマークされたオプションを選択します。
-
現時点では、VS はインターフェイスの特定の実装を自動的に完了するのに役立ちますが、当然ながら、内部の特定のコンテンツはさまざまなプロジェクトのニーズに応じて入力する必要があります。この時点では、インターフェイス関数の下にある緑色の波線はもう存在しないので、「Ctrl+s」を押して保存し、閉じるだけで済みます。この時点では、VS はすでにそれを完了しています。
-
他のすべてのインターフェイス関数は、上記の手順に従ってインターフェイスの特定の実装を完了します。これらの手順を 1 つずつ繰り返すことはありません。特定のインターフェイス機能が実装された後のプロジェクト構造を次の図に示します。
5. ゲームの基本フレームワークを設計する
-
この時点でゲーム全体の基本的なインターフェースを作成し、仮実装を行っていますが、まだゲームの枠組みはできていないため、次の作業でゲームの基本的な枠組みを作成することになります。ゲームは ChessGame クラスで制御されるため、各クラスの関数は ChessGame クラスから呼び出す必要があるため、まず ChessGame.h に次のコードを追加します。この時点でゲーム全体の基本的なコンテンツが作成されます。
#pragma once #include "Man.h" #include "AI.h" #include "Chess.h" class ChessGame { public: ChessGame(Man*, AI*, Chess*); // 开始对局 void play(); // 添加数据成员 private: Man* man; AI* ai; Chess* chess; };
-
ゲームの基本的なコンテンツが作成されると、ゲームの基本的なロジックが完成しますが、これは単純なオブジェクト指向ロジックの実装に過ぎず、具体的な開発が行われるわけではありません。現時点で必要なのは、次のコードを ChessGame.cpp に追加することだけです。
#include "ChessGame.h" ChessGame::ChessGame(Man* man, AI* ai, Chess* chess) { this->man = man; this->ai = ai; this->chess = chess; ai->init(chess); man->init(chess); } // 对局(开始五子棋游戏) void ChessGame::play() { // 棋盘初始化 chess->init(); // 开始对局 while (1) { // 首先由棋手走 man->go(); if (chess->checkOver()) { chess->init(); continue; } // 再由AI走 ai->go(); if (chess->checkOver()) { chess->init(); continue; } } }
-
この時点でゲーム全体の基本的な枠組みが完成したので、この枠組みに具体的なコンテンツを追加していきます。もちろん、その前に main 関数を使用して、作成したばかりのフレームを接続する必要があります。まずはゲーム全体のロジックとなるmain.cppを作成し、具体的な内容は後ほど書きます 「ソースファイル」を右クリックし、「追加」→「新しい項目」を選択します。
-
C++ ファイル (.cpp) を選択し、名前を入力して、最後に「追加」をクリックします。
-
次のコードを main.cpp に追加します。
#include <iostream> #include "ChessGame.h" int main(void) { Man man; Chess chess; AI ai; ChessGame game(&man, &ai, &chess); game.play(); return 0; }
-
この時点でテストを実行できます。「デバッグ」の「実行開始 (デバッグなし) (H)」をクリックします。
-
私たちのプログラムには今のところ問題がないことがわかります。
6. ボードの初期化
6.1 EasyXの使用
-
ゲームは描画する必要があるため、グラフィックス プログラムの作成に役立つ EasyX を使用してゲームの描画インターフェイスを完成させます。EasyX のダウンロード リンクもブログの上部にあります。ダウンロード後、ダブルクリックして開きます。
-
「次へ」をクリックします。
-
次に、「インストール」するコンパイラの対応するバージョンを選択します。
-
次に、インストールが成功したことを示すメッセージが表示されます。
6.2 チェス盤のデータ メンバーの設計
-
EasyX グラフィック ライブラリをインストールした後、必要なヘッダー ファイルを Chess.h にいくつか導入する必要があります。
-
次に、チェス盤の初期化に必要なデータを追加する必要があります。Chess.h に次のコードを追加するだけです。
private: IMAGE chessBlackImg; // 黑棋棋子 IMAGE chessWhiteImg; // 白棋棋子 int gradeSize; // 棋盘的大小(13线、15线、17线、19线) int margin_x; // 棋盘的左侧边界 int margin_y; // 棋盘的顶部边界 float chessSize; // 棋子的大小(棋盘的小方格的大小) /* 存储当前棋局的棋子分布数据 例如:chessMap[3][5]表示棋盘的第3行第5列的落子情况(0:空白;1:黑子;-1:白子) */ vector<vector<int>> chessMap; /* 表示现在该谁下棋(落子) true:该黑子走;false:该白子走 */ bool playerFlag;
6.3 チェス盤の構築
-
チェス盤を作成するには、先ほど作成したチェスボード クラスのデータを使用する必要があります。まず、チェス盤を作成する関数を記述する必要があるため、次のコードを Chess.h に追加します。
Chess(int gradeSize, int maiginX, int marginY, float chessSize);
-
次に、作成した関数の上にマウスを置き、「考えられる修正を表示」をクリックします。
-
次に、赤いボックス内のコンテンツを選択します。
-
次に、「Ctrl+S」を押して保存します。
-
次に、今作成したデータを使用してチェス盤を構築します。次のコードを Chess.cpp に追加するだけです。
// 构造棋盘 Chess::Chess(int gradeSize, int marginX, int marginY, float chessSize) { this->gradeSize = gradeSize; this->margin_x = marginX; this->margin_y = marginY; this->chessSize = chessSize; playerFlag = CHESS_BLACK; for (int i = 0; i < gradeSize; i++) { vector<int> row; for (int j = 0; j < gradeSize; j++) { row.push_back(0); } chessMap.push_back(row); } }
-
次に、main.cpp に移動し、作成したコンストラクターを使用して、パラメーターを渡してチェス盤を構築します。
-
次に、もう一度テストしてみます。「デバッグ」の「実行開始 (デバッグなし) (H)」をクリックします。
-
これまでのところ、プログラムには問題がないことがわかります。
6.4 ボードの初期化
-
プロジェクトを右クリックした後、「プロパティ」をクリックします。
-
「全般」の「文字セット」で「マルチバイト文字セットを使用する」を選択します。
-
音楽を再生するには、次のヘッダー ファイルと関連ライブラリを Chess.cpp に追加します。
#include <mmsystem.h> #pragma comment(lib,"winmm.lib")
-
実際のチェス盤を表示し、音楽を再生するには、次のコードを Chess.cpp に追加します。
// 棋盘初始化 void Chess::init() { // 创建游戏窗口 initgraph(897, 895); // 显示棋盘图片 loadimage(0, "resource/棋盘2.jpg"); // 播放开始提示音 mciSendString("play resource/start.wav", 0, 0, 0); // 加载黑棋和白棋棋子的图片 loadimage(&chessBlackImg, "resource/black.png", chessSize, chessSize, true); loadimage(&chessWhiteImg, "resource/white.png", chessSize, chessSize, true); // 棋盘清零 for (int i = 0; i < gradeSize; i++) { for (int j = 0; j < gradeSize; j++) { chessMap[i][j] = 0; } } // 确定谁先下棋 playerFlag = true; }
-
次に、それをテストします。
-
チェス盤が正常に表示され、音楽が正常に再生されたことがわかります。
7. チェスプレーヤーがチェスをプレイすることの実現
7.1 プレーヤーの初期化
-
チェスボード データ メンバーをチェス プレーヤー クラスに追加し、Man.h に次のコードを追加します。
private: Chess* chess;
7.2 チェスプレイヤーのチェス関数の初期化
-
チェス プレーヤー クラスが初期化されたら、ボード クラス ポインターを渡し、Man.cpp の init 関数を次のコードに置き換えるだけです。
// 棋手初始化 void Man::init(Chess * chess) { this->chess = chess; }
-
チェスプレイヤーがチェスをプレイする機能を実現するには、Man.cpp の go 関数を次のコードに置き換えます。
// 棋手下棋 void Man::go() { // 鼠标函数 MOUSEMSG msg; // 落子位置 ChessPos pos; while (1) { // 获取鼠标点击消息 msg = GetMouseMsg(); // 通过chess对象,来判断落子位置是否有效 if (msg.uMsg == WM_LBUTTONDOWN && chess->clickBoard(msg.x, msg.y, &pos)) { break; } } // 落子 chess->chessDown(&pos, CHESS_BLACK); }
7.3 プレイヤーのチェスの局面が有効かどうかの判断
-
チェスをプレイする上で最も重要なポイントは、チェスがプレイされる場所をコンピュータに知らせることです。この問題を解決するにはどうすればよいでしょうか? 以下の図を見ることができます。
-
チェスの駒は 2 つの線の交点、合計 4 つの点に落ちなければならないため、最初にチェスの駒の位置と 4 つの点の間の距離を計算する必要があります。ここで「しきい値」を設定する必要があります。チェスの駒の位置と特定の点の間の距離がこの「しきい値」よりも小さい場合、その点は実際のチェスの駒の位置であると見なされます。 , チェスの駒は置かれません. 半分、コンピュータに保存されている 2 次元配列の添字は 0 から始まることにも注意してください。この時点で必要なのは、次のコードを Chess.cpp に追加することだけです。
#include <math.h> // 判断落子是否有效 bool Chess::clickBoard(int x, int y, ChessPos * pos) { // 真实的落子列坐标 int col = (x - margin_x) / chessSize; // 真实的落子行坐标 int row = (y - margin_y) / chessSize; // 落子的左上角列坐标 int leftTopPosX = margin_x + chessSize * col; // 落子的左上角行坐标 int leftTopPosY = margin_y + chessSize * row; // 鼠标点击位置距离真实落子位置的阈值 int offset = chessSize * 0.4; // 落子距离四个角的距离 int len; // 落子是否有效 bool res = false; do { // 落子距离左上角的距离 len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY)); // 如果落子距离左上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len < offset) { pos->row = row; pos->col = col; if (chessMap[pos->row][pos->col] == 0) { res = true; } break; } // 落子距离右上角的距离 int x2 = leftTopPosX + chessSize; int y2 = leftTopPosY; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len < offset) { pos->row = row; pos->col = col + 1; if (chessMap[pos->row][pos->col] == 0) { res = true; } break; } // 落子距离左下角的距离 x2 = leftTopPosX; y2 = leftTopPosY + chessSize; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len < offset) { pos->row = row + 1; pos->col = col; if (chessMap[pos->row][pos->col] == 0) { res = true; } break; } // 落子距离右下角的距离 x2 = leftTopPosX + chessSize; y2 = leftTopPosY + chessSize; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len < offset) { pos->row = row + 1; pos->col = col + 1; if (chessMap[pos->row][pos->col] == 0) { res = true; } break; } } while (0); // 返回落子是否有效的判断结果 return res; }
-
この時点で、ボールの位置が正しいかどうかを判断できます。コードに問題がないことを確認するために、次のコードを Chess.cpp に追加します。テストが成功したら、次のコードを追加します。 、追加されたコードを削除できます。
-
次のコードを Man.cpp に追加して、配置位置を出力します。同様に、テストが成功したら、追加したコードを削除できます。
-
この時点で、テストのために main.cpp にアクセスできます。
-
ドロップ位置が正しく取得されていることがわかり、コードに問題がないことがわかります。テストが成功したら、上で追加した 2 つのコードを削除します。
7.4 チェスプレイヤーがチェスをプレイしていることを実現する
-
チェス盤の配置を実現するには、まず以下のコードを Chess.cpp に追加します。なお、図面の左側が左上隅であるため、接合点にチェスの駒の中心点が来るようにする必要があります。行ラインと列ラインのチェスの駒の行座標と列座標をグリッドのサイズの 0.5 倍に減らす必要があるため、これには特別な注意が必要です。
// 棋盘落子 void Chess::chessDown(ChessPos * pos, chess_kind chess) { // 加载落子音效 mciSendString("play resource/down7.wav", 0, 0, 0); // 获取棋子的落子位置,需要注意绘图的左边是左上角,所以为了让棋子的中心点在行线和列线的交界处,棋子的行和列坐标都需要减0.5倍的棋格大小 int x = margin_x + chessSize * pos->col - 0.5 * chessSize; int y = margin_y + chessSize * pos->row - 0.5 * chessSize; // 根据棋子类型在对应位置生成棋子图片 if (chess == CHESS_WHITE) { putimage(x, y, &chessWhiteImg); } else { putimage(x, y, &chessBlackImg); } }
-
次に、テストしたところ、チェスの駒は正常に配置でき、効果音には問題がないことがわかりました。ただし、各チェスの駒の周囲には黒い境界線があり、これらの黒い境界線は絶対に存在しないはずです。
-
Easyx は png 形式の画像をサポートしていないため、配置後にチェスの駒に黒い境界線が表示されます。この問題を解決するには、次の関数を Chess.cpp に追加するだけです。
// 解决Easyx不支持png格式图片的函数 void putimagePNG(int x, int y, IMAGE* picture) //x为载入图片的X坐标,y为Y坐标 { // 变量初始化 DWORD* dst = GetImageBuffer(); // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带 DWORD* draw = GetImageBuffer(); DWORD* src = GetImageBuffer(picture); // 获取picture的显存指针 int picture_width = picture->getwidth(); // 获取picture的宽度,EASYX自带 int picture_height = picture->getheight(); // 获取picture的高度,EASYX自带 int graphWidth = getwidth(); // 获取绘图区的宽度,EASYX自带 int graphHeight = getheight(); // 获取绘图区的高度,EASYX自带 int dstX = 0; // 在显存里像素的角标 // 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算 for (int iy = 0; iy < picture_height; iy++) { for (int ix = 0; ix < picture_width; ix++) { int srcX = ix + iy * picture_width; // 在显存里像素的角标 int sa = ((src[srcX] & 0xff000000) >> 24); // 0xAArrggbb;AA是透明度 int sr = ((src[srcX] & 0xff0000) >> 16); // 获取RGB里的R int sg = ((src[srcX] & 0xff00) >> 8); // G int sb = src[srcX] & 0xff; // B if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight) { dstX = (ix + x) + (iy + y) * graphWidth; // 在显存里像素的角标 int dr = ((dst[dstX] & 0xff0000) >> 16); int dg = ((dst[dstX] & 0xff00) >> 8); int db = dst[dstX] & 0xff; draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16) // 公式: Cp=αp*FP+(1-αp)*BP ; αp=sa/255 , FP=sr , BP=dr | ((sg * sa / 255 + dg * (255 - sa) / 255) << 8) // αp=sa/255 , FP=sg , BP=dg | (sb * sa / 255 + db * (255 - sa) / 255); // αp=sa/255 , FP=sb , BP=db } } } }
-
次に、Chess.cpp の Chess::chessDown 関数を以下のように変更します。
-
この時点で、もう一度テストしてみましょう。黒い境界線が消え、効果音に問題がないことがわかります。
-
技エフェクトは実現しましたが、表示するだけで技データをコンピュータに保存するわけではありません。以前に技データを格納するために2次元配列を作成したので、技情報を2次元配列に格納する必要があります。 。まず、Chess.h のプライベートに次の関数を追加します。
// 将落子信息存储到二维数组中 void updateGameMap(ChessPos* pos);
-
次に、次の関数を Chess.cpp に追加します。
// 将落子信息存储在二维数组中 void Chess::updateGameMap(ChessPos * pos) { // 存储落子信息 chessMap[pos->row][pos->col] = playerFlag ? CHESS_BLACK : CHESS_WHITE; // 黑白方交换行棋 playerFlag = !playerFlag; }
-
次に、Chess.cpp の Chess::chessDown 関数で Chess::updateGameMap を呼び出します。
-
この時点で、プレイヤーの動きの情報はコンピューターの 2 次元配列に保存されており、フォローアップ操作に便利です。
8. AIチェスの実現
8.1 AIの初期化
-
AI の初期化を行うときは、2 つのデータ メンバーを考慮する必要があります。
- チェス盤オブジェクト: どの盤でチェスをプレイするかを示します
- スコアリング配列: AI が最適な決定を下せるように、チェス盤上のすべての点の AI の価値評価を保存します。
-
上記の分析に基づいて、まず 2 つのデータ メンバーを AI.h に追加します。
private: // 棋盘对象 Chess* chess; // 评分数组 vector<vector<int>> scoreMap;
-
次に、次のコードを AI.cpp に追加します。
// AI初始化 void AI::init(Chess * chess) { this->chess = chess; int size = chess->getGradeSize(); for (int i = 0; i < size; i++) { vector<int> row; for (int j = 0; j < size; j++) { row.push_back(0); } scoreMap.push_back(row); } }
8.2 AI がチェスをプレイする原理
-
AI チェスの対局原理は、チェス プレーヤーの原理よりもはるかに複雑です。チェス プレーヤーは人工チェスをプレイし、コンピューター プログラムの計算を必要としません。一方、AI チェスは、チェス プレーヤーの位置に応じて、チェスをプレイするのに最適な位置を見つける必要があるからです。最適な戦略、つまり、AI はボード上のすべての配置可能な点のスコアを計算し、最もスコアの高い点を選択して移動する必要があります。配置可能な点のスコアリングについては、次のように理解できます。 : この局面は黒である可能性があります チェスの駒、または白のチェスの駒は、この局面を戦場として想像します。私たちがしなければならないことは、この局面を占領することによって黒の駒が得た価値が大きいか、それとも、この位置を捕捉して白の駒が得た値。黒がこの位置を捕捉してより多くの値を取得した場合、白をここに配置する必要があります。つまり、白が黒を破壊してより多くの値を取得する必要があります。白がこの位置からより多くの値を取得した場合、次に、白がこのポジションをダウンロードしてより多くの価値を取得できるようにします。現時点では AI が白をプレイしているため、AI にできるだけ多くの価値を取得させる必要があります
-
AIの場合、各落下後、落下の周囲8方向にあります。各落下点について、点の8方向でスコア計算を実行する必要があります。スコア計算の基準は、各落下点にすでにピースが何個配置されているかを判断することです。方向 連続したポーンが上にあります。次の図の黒い点で示されているように、ドロップ ポイントの可能性があると仮定します。
-
上の図によると、手の周囲に 8 つの方向があることがわかります。AI はまず、チェスプレイヤーがこの可能な位置で手を出した場合にどれだけの価値があるかを計算し、次に AI がその可能性のある位置で手を出した場合にどれだけの価値があるかを計算します。同じ位置で動きました。では、値の大きさをどのように判断すればよいのでしょうか? この位置に黒または白の駒が置かれている場合、この位置の8方向のうちの1方向に黒または白の駒が何個連続しているかが判断基準となります。黒または白のピースが連続するほど、この位置にピースを配置する価値が高くなります。
-
連続する黒または白のチェスの駒の数に基づいて価値を判断する必要があるため、さまざまな状況の価値を判断するのに役立つ、バックギャモンの一般的なチェスの形についての基本的な理解を得る必要があります。バックギャモンにおける一般的なチェスの形は次のとおりです。
-
2つでも:
最初のケース 2番目のケース -
ライブ 3:
最初のケース 2番目のケース -
死者3人:
最初のケース 2番目のケース -
4人で生きる
最初のケース 2番目のケース -
死んだ4人
最初のケース 2番目のケース -
5回連続(勝利)
最初のケース
-
-
配置状況の違いによって生じるチェスの形状ごとに、AI が判断しやすくするために対応するスコアを与え、最適な配置点を選択する必要があります。さまざまなチェスの色とさまざまなチェスの形の得点基準は、下の図に示されています。この得点基準は最適ではないかもしれませんが、この得点基準に従って設計された AI 五目並べのレベルは、ほとんどのプレイヤーのレベルを超えています。より多くの難易度のバックギャモン プレーヤー レベルに挑戦し、その後の反復的な最適化を実行できます。さらに、私たちのゲームでは、チェスプレイヤーが黒い駒を保持し、AI が白い駒を保持していることに注意してください。
ターゲットチェス 黒のチェス 白いチェス 2つでも 10 10 死んだ3人 30 25 3 つ生きます 40 50 死んだ4人 60 55 4人で生きる 200 10000 5回連続(勝利) 20000 30000
8.3 AI がチェスの試合を採点する
-
AI チェスの対局原理の分析を行ったので、分析結果に従ってコードを記述します。まず、AI のチェスの指し手の価値スコア計算を処理する関数を定義する必要があります。次の関数を に追加します。 h:
private: // AI对棋局进行评分 void calculateScore();
-
次のコードを AI.cpp に追加します。
// AI对棋局进行评分计算 void AI::calculateScore() { // 棋手方(黑棋)有多少个连续的棋子 int personNum = 0; // AI方(白棋)有多少个连续的棋子 int aiNum = 0; // 该方向上空白位的个数 int emptyNum = 0; // 将评分向量数组清零 for (int i = 0; i < scoreMap.size(); i++) { for (int j = 0; j < scoreMap[i].size(); j++) { scoreMap[i][j] = 0; } } // 获取棋盘大小 int size = chess->getGradeSize(); // 对可能的落子点的八个方向进行价值评分计算 for (int row = 0; row < size; row++) { for (int col = 0; col < size; col++) { // 只有当前位置没有棋子才是可能的落子点 if (chess->getChessData(row, col) == 0) { // 控制八个方向 for (int y = -1; y <= 0; y++) { for (int x = -1; x <= 1; x++) { // 重置棋手方(黑棋)有多少个连续的棋子 personNum = 0; // 重置AI方(白棋)有多少个连续的棋子 aiNum = 0; // 重置该方向上空白位的个数 emptyNum = 0; // 消除重复计算 if (y == 0 && x != 1) { continue; } // 原坐标不计算在内 if (!(y == 0 && x == 0)) { // 假设黑棋在该位置落子,会构成什么棋形?此时是黑棋的正向计算 for (int i = 1; i <= 4; i++) { int curRow = row + i * y; int curCol = col + i * x; if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 1) { personNum++; } else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 黑棋的反向计算 for (int i = 1; i <= 4; i++) { int curRow = row - i * y; int curCol = col - i * x; if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 1) { personNum++; } else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 连二 if (personNum == 1) { scoreMap[row][col] += 10; } // 连三 else if (personNum == 3) { // 死三 if (emptyNum == 1) { scoreMap[row][col] += 30; } // 活三 else if (emptyNum == 2) { scoreMap[row][col] += 40; } } // 连四 else if (personNum == 3) { // 死四 if (emptyNum == 1) { scoreMap[row][col] += 60; } // 活四 else if (emptyNum == 2) { scoreMap[row][col] += 200; } } // 连五 else if (personNum == 4) { scoreMap[row][col] += 20000; } // 清空空白棋子个数 emptyNum = 0; // 假设白棋在该位置落子,会构成什么棋形?此时是白棋的正向计算 for (int i = 1; i <= 4; i++) { int curRow = row + i * y; int curCol = col + i * x; if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == -1) { aiNum++; } else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 白棋的反向计算 for (int i = 1; i <= 4; i++) { int curRow = row - i * y; int curCol = col - i * x; if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == -1) { aiNum++; } else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 白色棋子无处可下 if (aiNum == 0) { scoreMap[row][col] += 5; } // 连二 else if (aiNum == 1) { scoreMap[row][col] += 10; } // 连三 else if (aiNum == 3) { // 死三 if (emptyNum == 1) { scoreMap[row][col] += 25; } // 活三 else if (emptyNum == 2) { scoreMap[row][col] += 50; } } // 连四 else if (aiNum == 3) { // 死四 if (emptyNum == 1) { scoreMap[row][col] += 55; } // 活四 else if (emptyNum == 2) { scoreMap[row][col] += 10000; } } // 连五 else if (aiNum >= 4) { scoreMap[row][col] += 30000; } } } } } } } }
8.4 AI によるチェスの実現
-
考えられる各ドロップ ポイントの全方向の値スコアの計算が完了したら、AI に「考えて」、ドロップの最も高い値スコアを持つポイントを選択させることができます。まず、次のコードを AI.h に追加します。
private: // 找出价值评分最高的点落子 ChessPos think();
-
次に、次のコードを Chess.h に追加します。
-
次に、次のコードを AI.cpp に追加します。
// 找出价值评分最高的点落子 ChessPos AI::think() { // 计算各个方向的价值评分 calculateScore(); // 获取棋盘大小 int size = chess->getGradeSize(); // 存储多个价值最大值的点 vector<ChessPos> maxPoints; // 初始价值最大值 int maxScore = 0; // 遍历搜索价值评分最大的点 for (int row = 0; row < size; row++) { for (int col = 0; col < size; col++) { if (chess->getChessData(row, col) == 0) { if (scoreMap[row][col] > maxScore) { maxScore = scoreMap[row][col]; maxPoints.clear(); maxPoints.push_back(ChessPos(row, col)); } else if (scoreMap[row][col] == maxScore) { maxPoints.push_back(ChessPos(row, col)); } } } } // 如果有多个价值最大值点,随机获取一个价值最大值点的下标 int index = rand() % maxPoints.size(); // 返回价值最大值点 return maxPoints[index]; }
-
次に、次のコードを AI.cpp に追加します。
// AI下棋 void AI::go() { // AI计算后的落子点 ChessPos pos = think(); // AI假装思考,给棋手缓冲时间 Sleep(1000); // 在AI计算后的落子点落子 chess->chessDown(&pos, CHESS_WHITE); }
-
Chess.cpp の Chess::getGradeSize 関数と 2 つの Chess::getChessData 関数を次のように変更します。
-
それでテストしてみたら、普通にチェスができるし、知能も悪くないことが分かりました。読者は、AI の知能を高めるために、自分の経験に応じて価値スコアの割り当てを調整できます。
9.勝敗判定の実現
9.1 勝ち負けの扱い
-
まず、Chess.h に次の関数を追加します。その目的は、現時点で誰が勝ち、誰が負けているかを確認し、その結果に従って勝敗を処理することです。
private: // 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false bool checkWin();
-
以下のヘッダー ファイルを Chess.cpp に追加します。
#include <conio.h>
-
Chess.cpp の Chess::checkOver 関数を次のように変更します。
// 胜负判定 bool Chess::checkOver() { // checkWin()函数来检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false if (checkWin()) { // 暂停 Sleep(1500);![请添加图片描述](https://img-blog.csdnimg.cn/4dbfb593cf904d2dbf7a140e2a4bbb9c.png) // 说明黑棋(棋手)赢 if (playerFlag == false) { mciSendString("play resource/不错.mp3", 0, 0, 0); loadimage(0, "resource/胜利.jpg"); } // 说明白棋(AI)赢 else { mciSendString("play resource/失败.mp3", 0, 0, 0); loadimage(0, "resource/失败.jpg"); } // 暂停 _getch(); return true; } return false; }
9.2 勝ち負けの原則
-
結果を処理する上記のプロセスは、結果を決定するための基本的なフレームワークであり、その中核となる部分は、誰が勝ち、誰が負けるかを決定する方法である checkWin 関数です。このように考えることができます。ある位置について、その 8 方向が 5 つのピースに接続されているかどうかを判断する必要がありますが、そのたびに、オフセットに応じて反対方向が 5 つのピースに接続されているかどうかも判断できます。位置を判断できるので、主要な 4 方向と 8 つの副方向を判断するだけで済みます。以下の図に示すように、まず水平方向を判断するとします。
-
あるドロップ位置について、まずその位置から右に連続する 5 つの位置が同じ色であるかどうかを判断し、次に最初のドロップ位置を 1、2、3、4、5 ずつ左にシフトしていることがわかります。位置を決めて同じ色の駒が5つ連続するかどうかを判定し、満たしていれば勝ち、そうでなければ勝ちではありません。このようにして、大方位の判断において、小さな2つの方位を同時に判断し、勝敗の判断を完了することができます。他の方向の勝敗の判断も同様です。
9.3 勝敗判定の実現
-
上記の原理分析により、コードを書くことができます。まず、Chess.h 内の特定のドロップ ポイント位置のデータ メンバーを追加します。
private: // 某一落子点的位置 ChessPos lastPos;
-
次に、次のコードを Chess.cpp の Chess::updateGameMap 関数に追加します。
-
次に、次のコードを Chess.cpp に追加します。
// 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false bool Chess::checkWin() { // 某一落子点的位置 int row = lastPos.row; int col = lastPos.col; // 落子点的水平方向 for (int i = 0; i < 5; i++) { if (((col - i) >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row][col - i] == chessMap[row][col - i + 1]) && (chessMap[row][col - i] == chessMap[row][col - i + 2]) && (chessMap[row][col - i] == chessMap[row][col - i + 3]) && (chessMap[row][col - i] == chessMap[row][col - i + 4])) { return true; } } // 落子点的垂直方向 for (int i = 0; i < 5; i++) { if (((row - i) >= 0) && ((row - i + 4) < gradeSize) && (chessMap[row - i][col] == chessMap[row - i + 1][col]) && (chessMap[row - i][col] == chessMap[row - i + 2][col]) && (chessMap[row - i][col] == chessMap[row - i + 3][col]) && (chessMap[row - i][col] == chessMap[row - i + 4][col])) { return true; } } // 落子点的右斜方向 for (int i = 0; i < 5; i++) { if (((row + i) < gradeSize) && (row + i - 4 >= 0) && (col - i >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row + i][col - i] == chessMap[row + i - 1][col - i + 1]) && (chessMap[row + i][col - i] == chessMap[row + i - 2][col - i + 2]) && (chessMap[row + i][col - i] == chessMap[row + i - 3][col - i + 3]) && (chessMap[row + i][col - i] == chessMap[row + i - 4][col - i + 4])) { return true; } } // 落子点的左斜方向 for (int i = 0; i < 5; i++) { if (((row - i + 4) < gradeSize) && (row - i >= 0) && (col - i >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row - i][col - i] == chessMap[row - i + 1][col - i + 1]) && (chessMap[row - i][col - i] == chessMap[row - i + 2][col - i + 2]) && (chessMap[row - i][col - i] == chessMap[row - i + 3][col - i + 3]) && (chessMap[row - i][col - i] == chessMap[row - i + 4][col - i + 4])) { return true; } } return false; }
-
書いた後、テストできます。
- 黒人(チェスプレイヤー):
- 黒(チェスプレイヤー)が勝つ盤面:
- 黒(チェスプレイヤー)の勝ちの判定:
- ホワイトチェス (AI):
- ホワイトチェス (AI) ウィニングボード:
- 白(AI)勝ちの判定:
- ホワイトチェス (AI) ウィニングボード:
- 黒人(チェスプレイヤー):
-
黒(棋士)が勝っても、白(AI)が勝っても、勝敗判定が正常に表示できることがわかります。Enter キーを押すと、次のラウンドが自動的に開始されます
要約する
上記は、C++ ベースの AI バックギャモン ゲーム プロジェクト開発チュートリアルの全内容です。目標は達成したことがわかりますが、AI の動きの値スコアの最適化など、今後もいくつかの最適化を行う可能性があります。後悔機能、メインインターフェイスメニューなど。待ってください。後で時間があれば、このブログを更新します。読者が勉強するのが好きで興味がある場合は、全体的な考え方が次のとおりであるため、最適化部分を自分で完了することもできます。比較的明確で、ロジックもあまり変わっていないため、最適化も容易です。それではこのブログは一旦終了とさせていただきます、また次のブログでお会いしましょう!