프로젝트 리소스 다운로드
- C++ 기반 AI 백개먼 게임 프로젝트 소스코드 압축 패키지 다운로드 주소
- C++ 기반 AI 백개먼 게임 프로젝트 소스코드 Github 다운로드 주소
- C++ 기반 AI 백개먼 게임 프로젝트에 필요한 자료
- C++ 기반 AI 백개먼 게임 프로젝트에 필요한 EasyX
프로젝트 설명
이 프로젝트는 C++ 개발을 기반으로 하여 전반적으로 비교적 단순하며 인간과 AI 간의 백개먼 게임을 구현하고 결과를 판단하고 음향 효과를 추가할 수 있습니다. 단계별로!
프로젝트 개발 소프트웨어 환경
- 윈도우11
- VS2017
- 이지엑스
프로젝트 개발 하드웨어 환경
- CPU: 인텔® 코어™ i7-8750H CPU @ 2.20GHz 2.20GHz
- 램: 24GB
- GPU: 엔비디아 지포스 GTX 1060
기사 디렉토리
머리말
다음은 C++ 기반의 AI 주사위 놀이 게임 프로젝트의 상세 개발 튜토리얼이다. 물론 독자 여러분은 최상의 결과를 얻기 위해 자신의 취향에 따라 게임 프로젝트 자료를 조정할 수도 있습니다. 다음은 이 기사의 전체 내용입니다!
제로, 프로젝트 데모
0.1 인간-기계 주사위 놀이 게임
0.2 흑(체스 선수) 승
-
검은색(체스 플레이어) 승리 보드:
-
흑체스(체스 선수) 승리 판정 결과:
2.3 백(AI)이 승리
-
흰색(AI) 승리 보드:
-
흰색 체스(AI) 승리 판정 결과:
1. 프로젝트 생성
-
Microsoft Visual Studio(이하 VS)를 연 후 "새로 만들기" -> "프로젝트"를 클릭합니다.
-
그런 다음 프로젝트 이름과 프로젝트 위치를 입력하고 "확인"을 클릭합니다.
2. 재료 수입
-
프로젝트의 재료 파일을 저장할 준비가 된 프로젝트에 새 "리소스" 폴더를 만듭니다.
-
프로젝트에 사용된 자료를 프로젝트의 "resource" 폴더로 가져옵니다. 독자는 자신의 자료 또는 내 자료를 사용할 수 있습니다. 내 자료의 다운로드 링크는 위 블로그에 있습니다.
3. 프로젝트 프레임워크 설계
3.1 설계 프로젝트 프레임워크
- 전체 프로젝트의 프레임워크는 아래 그림과 같으며 모든 코드는 다음 네 가지 클래스에 따라 작성됩니다.
- Man (chess player): 체스를 두는 사람
- 체스(체스판): 체스를 두는 곳
- AI(인공 지능): 체스 플레이어와 대결하는 AI
- ChessGame(게임 컨트롤): 게임의 기본 로직 제어
3.2 프로젝트 프레임워크에 따른 디자인 클래스
-
방금 디자인한 프로젝트 프레임워크에 따라 하나씩 빌드할 것입니다. 먼저 Man(체스 플레이어) 클래스를 만들고 "소스 파일"을 마우스 오른쪽 버튼으로 클릭한 다음 "추가"에서 "클래스"를 클릭합니다.
-
"Class Name"에 "Man"을 입력하고 "OK"를 클릭하면 나머지 파일이 자동으로 생성됩니다.
-
성공적으로 생성되었음을 알 수 있습니다.
-
Man(체스 선수) 클래스와 동일한 방식으로 다른 세 개의 클래스를 만들고 최종 효과는 아래 그림과 같습니다.
4. 게임의 메인 인터페이스 디자인
4.1 Chess(체스보드) 클래스의 메인 인터페이스 설계
- 우리는 Chess.h에서 Chess(체스보드) 클래스의 메인 인터페이스를 디자인하는데, 이러한 메인 인터페이스는 특별히 구현하지 않아도 외부에 노출되기만 하고 외부 사용을 기다릴 때 개인화할 수 있습니다. 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는 이미 완료했습니다.
-
다른 모든 인터페이스 기능은 위의 단계를 따라 인터페이스의 특정 구현을 완료하고 하나씩 반복하지 않습니다. 특정 인터페이스 기능이 구현된 후의 프로젝트 구조는 다음 그림과 같습니다.
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 체스판 구성
-
방금 생성한 chessboard 클래스의 데이터를 이용하여 체스판을 생성해야 하는데, 먼저 체스판을 생성하는 함수를 작성해야 하므로 다음 코드를 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의 초기화 함수를 다음 코드로 바꿉니다.
// 棋手初始化 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 플레이어의 체스 위치가 유효한지 판단
-
체스를 두는 데 있어 가장 중요한 점은 체스를 두는 곳을 컴퓨터에게 알려주는 것인데, 이 문제를 어떻게 해결해야 할까요? 아래 다이어그램을 볼 수 있습니다.
-
체스 말은 두 선의 교차점, 즉 총 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에 올 수 있습니다.
-
드롭 위치가 올바르게 획득되었음을 알 수 있으며 이는 우리 코드에 문제가 없음을 나타냅니다. 테스트가 성공하면 위에서 추가한 두 코드를 삭제합니다.
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 초기화를 수행할 때 두 가지 데이터 멤버를 고려해야 합니다.
- 체스판 개체: 체스를 두는 보드를 나타냅니다.
- 스코어링 배열: AI가 최적의 결정을 내릴 수 있도록 체스판의 모든 포인트에 대한 AI의 가치 평가를 저장합니다.
-
위의 분석을 기반으로 먼저 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는 보드에 배치할 수 있는 모든 지점의 점수를 계산한 다음 점수가 가장 높은 지점을 선택하여 이동해야 합니다. : 이 위치는 검은색일 수 있습니다 체스 말이나 흰색 체스 말은 이 위치를 전쟁터로 상상합니다. Black이 이 위치를 캡처하여 더 많은 값을 얻으면 White가 여기로 오도록 해야 합니다. 그런 다음 흰색이 이 위치를 다운로드하여 더 많은 가치를 얻도록 합니다. 현재 AI가 흰색을 재생하고 있으므로 AI가 가능한 한 더 많은 가치를 얻도록 해야 합니다.
-
AI의 경우 각 낙하 후 낙하 주변 8방향이 존재하며 각 낙하점에 대해 점의 8방향에서 점수계산을 하여야 한다 점수계산기준은 각각에 이미 몇 개의 조각이 놓여 있는지를 판단하는 것이다 방향.연속 폰은 위입니다. 아래 그림의 검은색 점과 같이 낙하 지점이 있다고 가정합니다.
-
위의 그림에 따르면 이동 방향이 8개임을 알 수 있는데, AI는 먼저 체스 플레이어가 이 가능한 위치에서 이동하면 얼마나 가치가 있을지 계산한 다음 AI가 얼마나 많은 가치를 가질지 계산합니다. 같은 위치에서 움직였습니다. 그렇다면 가치의 크기를 판단하는 방법은 무엇입니까? 연속된 조각의 수를 판단 기준으로 삼을 수 있습니다.검은색 또는 흰색 조각이 이 위치에 놓여 있다면, 이 위치에서 8개 방향 중 한 방향으로 연속된 검은색 또는 흰색 조각은 몇 개입니까? 연속되는 검은색 또는 흰색 조각이 많을수록 이 위치에 조각을 배치하는 가치가 더 큽니다.
-
연속되는 흑색 또는 백색 체스 조각의 수를 기준으로 값을 판단해야 하므로 주사위 놀이의 일반적인 체스 모양에 대한 기본적인 이해가 있어야 다양한 상황의 값을 판단하는 데 도움이 됩니다. 주사위 놀이의 일반적인 체스 모양은 다음과 같습니다.
-
2개라도:
첫 번째 경우 두 번째 경우 -
라이브 세:
첫 번째 경우 두 번째 경우 -
데드 쓰리:
첫 번째 경우 두 번째 경우 -
4 살다
첫 번째 경우 두 번째 경우 -
데드 포
첫 번째 경우 두 번째 경우 -
5연승(승리)
첫 번째 경우
-
-
서로 다른 배치 상황으로 인해 발생하는 각 체스 모양에 대해 AI가 판단을 쉽게 할 수 있도록 해당 점수를 부여하여 최적의 배치 지점을 선택해야 합니다. 다양한 체스 색상과 다양한 체스 모양에 대한 채점 기준은 아래 그림과 같습니다.이 채점 기준은 최적이 아닐 수 있지만 이 채점 기준에 따라 설계된 AI Gomoku의 수준은 대부분의 플레이어 수준을 초과했습니다.필요한 경우 도전 난이도 주사위 놀이 플레이어 수준, 후속 반복 최적화를 수행할 수 있습니다. 또한 우리 게임에서 체스 플레이어는 검은색 조각을 들고 AI는 흰색 조각을 들고 있다는 점에 유의해야 합니다.
대상 체스 검은 체스 흰색 체스 둘이라도 10 10 죽은 세 30 25 셋 살다 40 50 데드 포 60 55 4 살다 200 10000 5연승(승리) 20000 30000
8.3 AI가 체스 게임에서 득점
-
위의 AI 체스 플레이 원리 분석으로 분석 결과에 따라 코드를 작성하겠습니다. 시간:
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 함수와 두 개의 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개 연속으로 나오는지 판단하여 만족하면 이기고 그렇지 않으면 이기지 않습니다. 이와 같이 일반적인 방향의 판단에서 두 개의 작은 방향을 동시에 판단하여 우리의 승패판단을 완성할 수 있다.다른 방향의 승패판단은 같다.
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의 가치 점수 최적화, 후회 기능, 메인 인터페이스 메뉴 등 잠시만요, 나중에 시간이 있으면 이 블로그를 계속 업데이트하겠습니다 독자들이 공부를 좋아하고 관심이 있다면 스스로 최적화 부분을 완료할 수도 있습니다 비교적 명료하고 로직도 많이 바뀌지 않아 최적화도 쉽습니다. 그럼 이 블로그는 당분간 종료하도록 하겠습니다. 다음 블로그에서 만나요!