一、项目名称
五子棋
二、项目思路
1、搜集图片材料
2、绘制棋盘
3、实现鼠标下棋
4、用dfs判断输赢
5、实现双方下棋
6、实现AI下棋
(1)使用AI估分函数
(2)设计AI计算深度
(3)优化AI算法(α-β算法)
三、代码实现
1 #include <stdio.h> 2 //加载图形库文件 3 #include <easyx.h> 4 //加载windows库文件 5 #include <Windows.h> 6 //加载音乐文件 7 #include <mmsystem.h> 8 #include <conio.h> 9 #include <stack> 10 //加载winmm.lib 11 #pragma comment(lib,"winmm.lib") 12 //每个格子的宽度 13 #define GRID_W 25 14 //定义左和上的间隔宽度 15 #define INTERVAL (4 * GRID_W) 16 //棋盘长度 17 #define ROW 15 18 //棋盘宽度 19 #define COL 15 20 //存储棋盘状态 21 using namespace std; 22 int Chess[ROW][COL]; 23 //定义玩家和AI操作 24 int f = 1; 25 //定义状态种类 26 enum class Status { None, Black, White }; 27 //定义玩家操作状态集合 28 struct node { 29 int x = 0;//当前操作目标横坐标 30 int y = 0;//当前操作目标纵坐标 31 bool isShow = false;//是否显示下棋框 32 //鼠标当前所在数组对应的下标 33 int row = 0; 34 int col = 0; 35 //棋手状态 36 Status chessPlayer = Status::Black; 37 }op; 38 struct undo { 39 int x; 40 int y; 41 }; 42 //定义悔棋栈区 43 stack <undo> white; 44 stack <undo> black; 45 //定义AI操作状态集合 46 struct aiNode{ int x; 47 int y; 48 int ans; }aiOperation; 49 //悔棋操作 50 int popChess(stack <undo> &s) 51 { 52 if (s.empty()) { 53 MessageBox(GetHWnd(), "您还没有下棋哦>0<", "", MB_OK); 54 return 0; 55 } 56 else { 57 undo v = s.top(); 58 s.pop(); 59 Chess[v.x][v.y] = int(Status::None); 60 return 1; 61 } 62 } 63 //清空栈区函数 64 void clearStack(stack <undo>& s) 65 { 66 s = stack <undo>(); 67 } 68 //判断棋盘满棋 69 bool judgeFullchess() 70 { 71 for (int i = 0; i < ROW; i++) 72 for (int j = 0; j < COL; j++) 73 if (!Chess[i][j]) 74 return false; 75 return true; 76 } 77 //清空棋盘 78 void flushCheckerBoard() 79 { 80 for (int i = 0; i < ROW; i++) 81 for (int j = 0; j < COL; j++) 82 Chess[i][j] = int(Status::None); 83 } 84 //判断赢棋 85 bool judgeWinner(int sx, int sy) 86 { 87 int mx[8] = { 1,-1,1,-1,0,0,1,-1 }; 88 int my[8] = { -1,1,1,-1,1,-1,0,0 }; 89 for (int i = 0; i < 4; i++) { 90 int cnt = 1; 91 int dx = sx; 92 int dy = sy; 93 while (1) { 94 dx = mx[i * 2] + dx; 95 dy = my[i * 2] + dy; 96 if (dx < 0 || dx >= 15 || dy < 0 || dy >= 15) 97 break; 98 if (Chess[dx][dy] != int(op.chessPlayer)) 99 break; 100 cnt++; 101 } 102 dx = sx; 103 dy = sy; 104 while (1) { 105 dx = mx[i * 2 + 1] + dx; 106 dy = my[i * 2 + 1] + dy; 107 if (dx < 0 || dx >= 15 || dy < 0 || dy >= 15) 108 break; 109 if (Chess[dx][dy] != int(op.chessPlayer)) 110 break; 111 cnt++; 112 } 113 if (cnt >= 5) 114 return true; 115 } 116 return false; 117 } 118 //绘制棋盘 119 void drawCheckerBoard() 120 { 121 setlinecolor(BLACK); 122 setlinestyle(PS_SOLID, 1); 123 for (int i = 0; i < 15; i++) { 124 //绘制线条 125 line(INTERVAL, INTERVAL + i * GRID_W, GRID_W * 14 + INTERVAL, INTERVAL + i * GRID_W); 126 line(INTERVAL + i * GRID_W, INTERVAL, INTERVAL + i * GRID_W, GRID_W * 14 + INTERVAL); 127 } 128 //设置线条样式 129 setlinestyle(PS_SOLID, 3); 130 //绘制矩阵,加宽外框 131 rectangle(INTERVAL, INTERVAL, INTERVAL + GRID_W * 14, INTERVAL + GRID_W * 14); 132 //绘制下棋选择框 133 if (op.isShow) {//防止出棋盘还显示下棋框 134 setlinecolor(BLUE); 135 rectangle(op.x - 12, op.y - 12, op.x + 12, op.y + 12); 136 } 137 //绘制棋子 138 for (int i = 0; i < ROW; i++) 139 for (int j = 0; j < COL; j++) { 140 switch (Chess[i][j]) { 141 case 0:break; 142 case 1: 143 setfillcolor(BLACK); 144 solidcircle(i * GRID_W + INTERVAL, j * GRID_W + INTERVAL, 10); 145 break;//矩阵坐标和实际坐标相反 146 case 2: 147 setfillcolor(WHITE); 148 solidcircle(i * GRID_W + INTERVAL, j * GRID_W + INTERVAL, 10); 149 break;//矩阵坐标和实际坐标相反 150 default: 151 break; 152 } 153 } 154 } 155 //鼠标操作 156 void mouseEvent() 157 { 158 //定义一个消息结构体变量 159 ExMessage msg; 160 //获取鼠标消息 161 if (peekmessage(&msg, EM_MOUSE)) {//EM_MOUSE只需要鼠标消息 162 //认输 163 if (msg.message == WM_LBUTTONDOWN) { 164 if (msg.x >= 460 && msg.x <= 485 && msg.y >= 250 && msg.y < 280) { 165 //清空棋子区 166 clearStack(white); 167 clearStack(black); 168 MessageBox(GetHWnd(), "Win", op.chessPlayer == Status::White ? "黑方" : "白方", MB_OK); 169 MessageBox(GetHWnd(), "重新开始", "下轮黑方先手", MB_OK); 170 //棋手状态转换 171 op.chessPlayer = Status::Black; 172 flushCheckerBoard(); 173 return; 174 } 175 else if(msg.x >= 460 && msg.x <= 485 && msg.y >= 285 && msg.y < 315) { 176 //清空棋子区 177 clearStack(white); 178 clearStack(black); 179 MessageBox(GetHWnd(), "重新开始", "平局", MB_OK); 180 MessageBox(GetHWnd(), "", "下轮黑方先手", MB_OK); 181 //棋手状态转换 182 op.chessPlayer = Status::Black; 183 flushCheckerBoard(); 184 return; 185 } 186 else if(msg.x >= 460 && msg.x <= 485 && msg.y >= 215 && msg.y < 245) { 187 //判断是否悔棋 188 int check = 0; 189 if (op.chessPlayer == Status::Black) 190 check = popChess(white); 191 else 192 check = popChess(black); 193 //如果没有悔棋 194 if (!check) 195 return; 196 //棋手状态转换 197 op.chessPlayer = (op.chessPlayer == Status::Black ? Status::White : Status::Black); 198 drawCheckerBoard(); 199 FlushBatchDraw(); 200 return; 201 } 202 } 203 //坐标校准(自动聚焦到棋盘上某个点的中心) 204 op.isShow = false; 205 int intervalGridx = msg.x % GRID_W; 206 int intervalGridy = msg.y % GRID_W; 207 int gridx, gridy; 208 //校准x坐标 209 if (intervalGridx <= GRID_W / 2) 210 gridx = (msg.x / GRID_W) * GRID_W; 211 else 212 gridx = (msg.x / GRID_W + 1) * GRID_W; 213 //校准y坐标 214 if (intervalGridy <= GRID_W / 2) 215 gridy = (msg.y / GRID_W) * GRID_W; 216 else 217 gridy = (msg.y / GRID_W + 1) * GRID_W; 218 //判断是否越出棋盘 219 if (gridx < INTERVAL || gridy < INTERVAL || 220 gridx > INTERVAL + GRID_W * 14 || gridy > INTERVAL + GRID_W * 14) 221 return; 222 op.isShow = true; 223 op.row = (gridx - INTERVAL) / GRID_W; 224 op.col = (gridy - INTERVAL) / GRID_W; 225 op.x = gridx; 226 op.y = gridy; 227 //左击下棋 228 if (msg.message == WM_LBUTTONDOWN && Chess[op.row][op.col] == 0) { 229 //棋盘状态标记 230 Chess[op.row][op.col] = int(op.chessPlayer); 231 if (op.chessPlayer == Status::Black) 232 black.push({ op.row,op.col }); 233 else 234 white.push({ op.row,op.col }); 235 drawCheckerBoard(); 236 FlushBatchDraw(); 237 //判断赢棋 238 if (judgeWinner(op.row, op.col)) { 239 MessageBox(GetHWnd(), "Win", op.chessPlayer == Status::White ? "白方" : "黑方", MB_OK); 240 MessageBox(GetHWnd(), "重新开始", op.chessPlayer == Status::White ? "下轮黑方先手" : "下轮白方先手", MB_OK); 241 //清空棋子栈区 242 clearStack(white); 243 clearStack(black); 244 flushCheckerBoard(); 245 } 246 //棋手状态转换 247 op.chessPlayer = (op.chessPlayer == Status::Black ? Status::White : Status::Black); 248 } 249 //满棋的情况 250 if (judgeFullchess()) { 251 MessageBox(GetHWnd(), "和棋", "重新开始", MB_OK); 252 //清空棋子栈区 253 clearStack(white); 254 clearStack(black); 255 flushCheckerBoard(); 256 } 257 } 258 } 259 //背景音乐 260 void bkmusic() 261 { 262 mciSendString("open ./bkmusic.mp3 alias music", 0, 0, 0); 263 mciSendString("play music repeat", NULL, 0, NULL); 264 } 265 //背景图片 266 void bkimage() 267 { 268 //双缓冲绘图 让数据批量处理,不让图像闪烁 269 BeginBatchDraw(); 270 IMAGE img_bk;//创建图片对象 271 loadimage(&img_bk, "./checkerboard.jpg", 0, 0); 272 putimage(0, 0, &img_bk); 273 setbkmode(TRANSPARENT); 274 settextcolor(BLUE); 275 settextstyle(30, 10, "楷体"); 276 outtextxy(460, 200, "----"); 277 outtextxy(460, 215, "悔棋"); 278 outtextxy(495, 215, "|"); 279 outtextxy(455, 215, "|"); 280 outtextxy(460, 230, "----"); 281 282 outtextxy(460, 235, "----"); 283 outtextxy(460, 250, "认输"); 284 outtextxy(495, 250, "|"); 285 outtextxy(455, 250, "|"); 286 outtextxy(460, 265, "----"); 287 288 outtextxy(460, 270, "----"); 289 outtextxy(460, 285, "求和"); 290 outtextxy(495, 285, "|"); 291 outtextxy(455, 285, "|"); 292 outtextxy(460, 300, "----"); 293 } 294 void menumusic() 295 { 296 mciSendString("open ./menumusic.mp3 alias music", 0, 0, 0); 297 mciSendString("play music repeat", NULL, 0, NULL); 298 } 299 //菜单背景图片 300 void menuImage() 301 { 302 IMAGE img_menubk;//创建图片对象 303 loadimage(&img_menubk, "./menubk.jpg", 0, 0); 304 putimage(0, 0, &img_menubk); 305 } 306 //对AI落子评分 307 void judgeRobotValue(int robotNum, int empty, int sx, int sy) 308 { 309 int cnt = 0; 310 //一连 311 if (robotNum == 0) 312 cnt = 5; 313 //二连 314 else if (robotNum == 1) 315 cnt = 10; 316 //三连 317 else if (robotNum == 2) { 318 //死三 319 if (empty == 1) 320 cnt = 25; 321 //活三 322 else if (empty == 2) 323 cnt = 50; 324 } 325 //连四 326 else if (robotNum == 3) { 327 //死四 328 if (empty == 1) 329 cnt = 55; 330 //活四 331 else if (empty == 2) 332 cnt = 300; 333 } 334 //连五 335 else if (robotNum == 4) 336 cnt = 30000; 337 if (cnt > aiOperation.ans) { 338 aiOperation.x = sx; 339 aiOperation.y = sy; 340 aiOperation.ans = cnt; 341 } 342 } 343 //对玩家落子评分 344 void judgePersonValue(int personNum,int empty,int sx,int sy) 345 { 346 int cnt = 0; 347 //连二 348 if (personNum == 1) { 349 cnt = 10; 350 } 351 //连三 352 else if (personNum == 2) { 353 //死三 354 if (empty == 1) 355 cnt = 30; 356 //活三 357 else if (empty == 2) 358 cnt = 40; 359 } 360 //连四 361 else if (personNum == 3) { 362 //死四 363 if (empty == 1) 364 cnt = 60; 365 //活四 366 else if (empty == 2) 367 cnt = 200; 368 } 369 //连五 370 else if (personNum == 4) 371 cnt = 20000; 372 if (cnt > aiOperation.ans) { 373 aiOperation.x = sx; 374 aiOperation.y = sy; 375 aiOperation.ans = cnt; 376 } 377 } 378 //初始化AI 379 void initAI() 380 { 381 aiOperation.ans = 0; 382 aiOperation.x = 0; 383 aiOperation.y = 0; 384 int mx[8] = { 1,-1,1,-1,0,0,1,-1 }; 385 int my[8] = { -1,1,1,-1,1,-1,0,0 }; 386 //对玩家进行评分 玩家是黑子 387 for(int i = 0;i < ROW;i++) 388 for (int j = 0; j < COL; j++) { 389 int sx = i, sy = j; 390 int empty,personNum,robotNum; 391 if (!Chess[sx][sy]) { 392 int dx = sx, dy = sy; 393 for (int k = 0; k < 4; k++) { 394 personNum = 0; 395 robotNum = 0; 396 empty = 0; 397 //判断玩家落子 398 dx = sx, dy = sy; 399 for (int z = 0; z < 5;z++) { 400 dx = dx + mx[2 * k]; 401 dy = dy + my[2 * k]; 402 if (dx < 0 || dy < 0 || dx >= 15 || dy >= 15) 403 break; 404 //碰到AI棋子就退出 405 if (Chess[dx][dy] == 2) { 406 break; 407 } 408 //空格情况退出 409 if (Chess[dx][dy] == 0) { 410 empty++; 411 break; 412 } 413 personNum++; 414 } 415 dx = sx,dy = sy; 416 for (int z = 0; z < 5;z++) { 417 dx = dx + mx[2 * k + 1]; 418 dy = dy + my[2 * k + 1]; 419 //越界情况退出 420 if (dx < 0 || dy < 0 || dx >= 15 || dy >= 15) 421 break; 422 //碰到AI棋子就退出 423 if (Chess[dx][dy] == 2) { 424 break; 425 } 426 //碰到空格退出 427 if (Chess[dx][dy] == 0) { 428 empty++; 429 break; 430 } 431 personNum++; 432 } 433 judgePersonValue(personNum,empty,sx,sy); 434 //初始化搜索状态 435 empty = 0; 436 dx = sx,dy = sy; 437 //判断AI落子 438 for (int z = 0; z < 5;z++) { 439 dx = dx + mx[2 * k]; 440 dy = dy + my[2 * k]; 441 if (dx < 0 || dy < 0 || dx >= 15 || dy >= 15) 442 break; 443 //碰到玩家棋子就退出 444 if (Chess[dx][dy] == 1) { 445 break; 446 } 447 //空格情况退出 448 if (Chess[dx][dy] == 0) { 449 empty++; 450 break; 451 } 452 robotNum++; 453 } 454 dx = sx,dy = sy; 455 for (int z = 0; z < 5;z++) { 456 dx = dx + mx[2 * k + 1]; 457 dy = dy + my[2 * k + 1]; 458 //越界情况退出 459 if (dx < 0 || dy < 0 || dx >= 15 || dy >= 15) 460 break; 461 //碰到玩家棋子就退出 462 if (Chess[dx][dy] == 1) { 463 break; 464 } 465 //碰到空格退出 466 if (Chess[dx][dy] == 0) { 467 empty++; 468 break; 469 } 470 robotNum++; 471 } 472 judgeRobotValue(robotNum, empty, sx, sy); 473 } 474 } 475 } 476 } 477 //AI操作 478 void aiEvent() 479 { 480 if (f == -1) { 481 initAI(); 482 //AI选择框不显示 483 op.isShow = false; 484 op.row = aiOperation.x; 485 op.col = aiOperation.y; 486 op.x = aiOperation.x * GRID_W + INTERVAL; 487 op.y = aiOperation.y * GRID_W + INTERVAL; 488 //AI下棋 489 if (Chess[op.row][op.col] == 0) { 490 //棋盘状态标记 491 Chess[op.row][op.col] = 2; 492 drawCheckerBoard(); 493 FlushBatchDraw(); 494 //判断赢棋 495 if (judgeWinner(op.row, op.col)) { 496 MessageBox(GetHWnd(), "Win", "AI", MB_OK); 497 MessageBox(GetHWnd(), "重新开始", "下轮玩家先手", MB_OK); 498 clearStack(white); 499 clearStack(black); 500 flushCheckerBoard(); 501 } 502 //棋手状态转换 503 op.chessPlayer = Status::Black; 504 f = 1; 505 } 506 //满棋的情况 507 if (judgeFullchess()) { 508 MessageBox(GetHWnd(), "和棋", "重新开始", MB_OK); 509 clearStack(white); 510 clearStack(black); 511 op.chessPlayer = Status::Black; 512 f = 1; 513 flushCheckerBoard(); 514 } 515 } 516 else { 517 //定义一个消息结构体变量 518 ExMessage msg; 519 //获取鼠标消息 520 if (peekmessage(&msg, EM_MOUSE)) {//EM_MOUSE只需要鼠标消息 521 if (msg.message == WM_LBUTTONDOWN) { 522 if (msg.x >= 460 && msg.x <= 485 && msg.y >= 250 && msg.y < 280) { 523 //清空棋子区 524 clearStack(white); 525 clearStack(black); 526 MessageBox(GetHWnd(), "Win", "AI", MB_OK); 527 MessageBox(GetHWnd(), "重新开始", "下轮玩家先手", MB_OK); 528 //棋手状态转换 529 op.chessPlayer = Status::Black; 530 flushCheckerBoard(); 531 return; 532 } 533 else if (msg.x >= 460 && msg.x <= 485 && msg.y >= 285 && msg.y < 315) { 534 //清空棋子区 535 clearStack(white); 536 clearStack(black); 537 MessageBox(GetHWnd(), "重新开始", "平局", MB_OK); 538 MessageBox(GetHWnd(), "", "下轮黑方先手", MB_OK); 539 //棋手状态转换 540 op.chessPlayer = Status::Black; 541 flushCheckerBoard(); 542 return; 543 } 544 } 545 //坐标校准(自动聚焦到棋盘上某个点的中心) 546 op.isShow = false; 547 int intervalGridx = msg.x % GRID_W; 548 int intervalGridy = msg.y % GRID_W; 549 int gridx, gridy; 550 //校准x坐标 551 if (intervalGridx <= GRID_W / 2) 552 gridx = (msg.x / GRID_W) * GRID_W; 553 else 554 gridx = (msg.x / GRID_W + 1) * GRID_W; 555 //校准y坐标 556 if (intervalGridy <= GRID_W / 2) 557 gridy = (msg.y / GRID_W) * GRID_W; 558 else 559 gridy = (msg.y / GRID_W + 1) * GRID_W; 560 //判断是否越出棋盘 561 if (gridx < INTERVAL || gridy < INTERVAL || 562 gridx > INTERVAL + GRID_W * 14 || gridy > INTERVAL + GRID_W * 14) 563 return; 564 op.isShow = true; 565 op.row = (gridx - INTERVAL) / GRID_W; 566 op.col = (gridy - INTERVAL) / GRID_W; 567 op.x = gridx; 568 op.y = gridy; 569 //左击下棋 570 if (msg.message == WM_LBUTTONDOWN && Chess[op.row][op.col] == 0) { 571 //棋盘状态标记 572 Chess[op.row][op.col] = 1; 573 drawCheckerBoard(); 574 FlushBatchDraw(); 575 //判断赢棋 576 if (judgeWinner(op.row, op.col)) { 577 MessageBox(GetHWnd(), "Win", "玩家", MB_OK); 578 MessageBox(GetHWnd(), "重新开始", "下轮玩家先手", MB_OK); 579 clearStack(white); 580 clearStack(black); 581 op.chessPlayer = Status::Black; 582 f = 1; 583 flushCheckerBoard(); 584 return; 585 } 586 //棋手状态转换 587 op.chessPlayer = Status::White; 588 f = -1; 589 } 590 //满棋的情况 591 if (judgeFullchess()) { 592 MessageBox(GetHWnd(), "和棋", "重新开始", MB_OK); 593 clearStack(white); 594 clearStack(black); 595 op.chessPlayer = Status::Black; 596 f = 1; 597 flushCheckerBoard(); 598 } 599 } 600 } 601 } 602 //双人对战 603 void gameMode1() 604 { 605 bkmusic(); 606 while (1) 607 { 608 //清屏 609 cleardevice(); 610 //加载图片 611 bkimage(); 612 //绘制棋盘 613 drawCheckerBoard(); 614 //鼠标操作 615 mouseEvent(); 616 //刷新 617 FlushBatchDraw(); 618 } 619 //结束绘图 620 EndBatchDraw(); 621 } 622 //人机对战 623 void gameMode2() 624 { 625 bkmusic(); 626 while (1) 627 { 628 //清屏 629 cleardevice(); 630 //加载图片 631 bkimage(); 632 //绘制棋盘 633 drawCheckerBoard(); 634 aiEvent(); 635 //刷新 636 FlushBatchDraw(); 637 } 638 //结束绘图 639 EndBatchDraw(); 640 } 641 //游戏模式 642 void gameMode() 643 {//获取鼠标消息 644 ExMessage msg; 645 int gridx, gridy; 646 while (1) { 647 if (peekmessage(&msg, EM_MOUSE)) {//EM_MOUSE只需要鼠标消息 648 op.isShow = true; 649 if (msg.message == WM_LBUTTONDOWN) { 650 gridx = msg.x; 651 gridy = msg.y; 652 if (gridx >= 170 && gridx <= 420 && gridy >= 170 && gridy < 220) 653 gameMode1(); 654 else if (gridx >= 170 && gridx <= 420 && gridy >= 220 && gridy < 270) 655 gameMode2(); 656 else if (gridx >= 170 && gridx <= 420 && gridy >= 270 && gridy < 320) 657 return; 658 } 659 } 660 } 661 } 662 //开始菜单 663 void menu() 664 { 665 menuImage(); 666 menumusic(); 667 // 设置背景为透明 668 setbkmode(TRANSPARENT); 669 settextcolor(BLUE); 670 settextstyle(50, 15, "楷体"); 671 outtextxy(170, 120, " 游戏菜单"); 672 outtextxy(170, 170, "1.双人对战"); 673 outtextxy(170, 220, "2.人机对战"); 674 outtextxy(170, 270, "3.退出游戏"); 675 gameMode(); 676 } 677 int main() 678 { 679 initgraph(529, 529);//创建窗口 680 menu(); 681 return 0; 682 }
四、项目展示
五、项目总结
第一次写项目,很多函数不熟悉,也是边看教程边熟悉函数,难点在于估分函数的设置以及AI的α-β算法的剪枝算法,希望能够在这个项目的基础上再次优化一下AI的算法。