【c语言】震惊!300行代码教你写出 N子棋 无敌AI人机(三子棋(井字棋)、四子棋、五子棋和六子棋等)

您所在的位置:网站首页 井字棋的规则和走法视频讲解 【c语言】震惊!300行代码教你写出 N子棋 无敌AI人机(三子棋(井字棋)、四子棋、五子棋和六子棋等)

【c语言】震惊!300行代码教你写出 N子棋 无敌AI人机(三子棋(井字棋)、四子棋、五子棋和六子棋等)

#【c语言】震惊!300行代码教你写出 N子棋 无敌AI人机(三子棋(井字棋)、四子棋、五子棋和六子棋等)| 来源: 网络整理| 查看: 265

目录 前言:1. 游戏规则与程序设计2. 极小化极大算法概述3. 极小化极大算法的实现步骤(1)建立评估函数:(2)极小化极大搜索:(3)剪枝优化:(水平有限没有实现) 实现效果视频展示AI三子棋实现效果视频展示(两局平局)AI四子棋实现效果视频展示(一输一赢)AI五子棋实现效果视频展示(一输一赢)AI六子棋实现效果(奋战半小时,被ai打败了) 展示所创建的函数关键步骤代码解析1.判断N子棋游戏是否结束并返回胜利方 char check_win( )2. 判断N子棋游戏加权连子数,以及棋型并给予相应的得分 int evaluate_count( )3.极小化极大算法 int minimax( ) 完整代码+详细注释头文件 game.h源文件 game.c (300行代码)源文件 test.c

前言:

N子棋是一种经典的棋类游戏,它在计算机科学领域有着广泛的应用。本文将介绍如何使用C语言编写N子棋游戏程序,并详细解析使用极小化极大算法实现AI对战的方法。

1. 游戏规则与程序设计

N子棋游戏的规则已经在前文提到,我们需要先定义一个设定大小的棋盘数组作为游戏的核心数据结构。使用C语言的二维数组可以很方便地表示棋盘状态。玩家和AI轮流落子,通过输入坐标来在空位上下子,并判断是否获胜。

2. 极小化极大算法概述

极小化极大算法(Minimax Algorithm)是一种常用于博弈游戏的决策算法,在N子棋中也可以有效地应用。该算法通过递归搜索所有可能的落子情况,并给每个局面评分,以找到最优的走法。

3. 极小化极大算法的实现步骤 (1)建立评估函数:

为了给每个局面评分,我们需要定义一个评估函数。评估函数的设计需要考虑棋子的布局、攻防情况等因素,以尽可能准确地评估当前局面的优劣。

(2)极小化极大搜索:

通过递归搜索所有可能的落子情况,每一步轮到AI时,它会选择对自己最有利的走法;每一步轮到玩家时,它会选择对AI最不利的走法。通过不断深入搜索并更新评分,AI可以找到最优的走法。

(3)剪枝优化:(水平有限没有实现)

由于N子棋的搜索空间很大,为了减少搜索时间,我们可以使用剪枝技术,如Alpha-Beta剪枝。这种剪枝算法可以排除一些明显不会被选择的走法,从而加速搜索过程。

实现效果视频展示 AI三子棋实现效果视频展示(两局平局)

链接:https://live.csdn.net/v/314862?spm=1001.2014.3001.5501

AI三子棋

AI四子棋实现效果视频展示(一输一赢)

链接:https://live.csdn.net/v/314866?spm=1001.2014.3001.5501

AI 四子棋

AI五子棋实现效果视频展示(一输一赢)

链接:https://live.csdn.net/v/314872?spm=1001.2014.3001.5501

AI 五子棋

AI六子棋实现效果(奋战半小时,被ai打败了)

链接:https://live.csdn.net/v/314891?spm=1001.2014.3001.5501

AI 六子棋,我竟然被自己写的AI打败了

展示所创建的函数 //game.h #pragma once #include #include #include #include //定义棋盘大小(可更改) #define board_size 15 // 定义玩家和AI的棋子类型 #define PLAYER 'X' #define AI 'O' // 定义符号 #define EMPTY ' ' #define NOFULL '-' #define FULL '*' //初始化棋盘 void init_board(char board[board_size][board_size]); //打印棋盘 void display_board(char board[board_size][board_size]); //判断位置是否为空 int isPositionEmpty(char board[board_size][board_size], int x, int y); //玩家下棋 void player_move(char board[board_size][board_size]); // 判断当前位置是否为有效的落子点 bool isValidMove(int x, int y); // 判断N子棋游戏加权连子数,以及棋型并给予相应的得分 char check_win(char board[board_size][board_size]); // 判断N子棋游戏连子数 int evaluate_count(char board[board_size][board_size]); //评估当前局面的得分 int evaluate(char board[board_size][board_size], int i, int j); // 极小化极大算法 int minimax(char board[board_size][board_size], int depth, int isMaximizingPlayer, int i, int j); // AI 下棋 void ai_move(char board[board_size][board_size]); 关键步骤代码解析 1.判断N子棋游戏是否结束并返回胜利方 char check_win( )

这段代码是一个用于判断五子棋游戏是否结束并返回胜利方的函数 check_win。函数接受一个二维字符数组 board 作为参数,表示棋盘,并返回一个字符表示游戏结果。

函数首先定义了四个方向:水平、垂直、左上到右下、右上到左下。然后通过两层循环遍历棋盘上的每个位置。对于每个位置,如果没有棋子,则跳过;否则,获取当前位置的颜色(玩家或AI)。

接下来,使用四个方向进行遍历并统计连续相同颜色的棋子数。遍历过程中,通过增量 dx 和 dy 来改变当前位置的坐标,不断在当前方向上移动。使用 isValidMove 函数判断新的位置是否合法(在棋盘范围内),并继续判断该位置的棋子颜色是否与当前颜色相同。如果是相同的颜色,则将计数器 count 自增,表示连续相同颜色的棋子数增加了一个。

如果任何一个方向上的连子数达到胜利条件(WinNum,即五子棋规则中连成一排的棋子数),则返回胜利方的颜色。

如果棋盘上没有空位置,表示平局,返回平局标志 FULL。

//game.c // 判断N子棋游戏是否结束并返回胜利方 char check_win(char board[board_size][board_size]) { // 定义四个方向:水平、垂直、左上到右下、右上到左下 int directions[4][2] = { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; int n = board_size;// 棋盘大小 for (int i = 0; i if (board[i][j] == EMPTY) { continue; // 当前位置没有棋子,跳过 } char player = board[i][j]; for (int k = 0; k // 沿当前方向连续相同颜色的棋子数 count++; x += dx; y += dy; } if (count >= WinNum) { return player; // 有一方获胜,返回胜利方 } } } } // 判断棋盘是否已满,即平局 for (int i = 0; i if (board[i][j] == EMPTY) { return NOFULL; // 棋盘还未满 } } } return FULL; // 棋盘已满平局 } 2. 判断N子棋游戏加权连子数,以及棋型并给予相应的得分 int evaluate_count( )

首先,判断游戏是否胜利的函数 check_win 使用了一个二维数组 board 来表示棋盘,其中 board_size 是棋盘的大小。函数通过遍历棋盘上的每个位置,并检查每个位置是否有棋子,如果当前位置没有棋子则跳过。如果当前位置有棋子,则使用四个固定方向(水平、垂直、左上到右下、右上到左下)进行遍历,统计连续相同颜色的棋子数。如果有任何一个方向上的连子数达到胜利条件(WinNum,即五子棋规则中连成一排的棋子数),则返回胜利方。如果棋盘上没有空位置,表示平局,返回平局标志。

其次,计算加权连子数以及棋型得分的函数 evaluate_count 也使用了一个二维数组 board 来表示棋盘。函数遍历棋盘上的每个位置,并检查每个位置是否有棋子,如果没有棋子则跳过。如果当前位置有棋子,则使用四个固定方向进行遍历,统计连续相同颜色的棋子数。根据当前棋子的颜色和连子数,给出相应的得分。AI得分的计算基于连子数的平方,而玩家得分则是减去连子数的平方。同时,根据不同的棋型(如活五、活四、活三等),给予特定的得分。

这两个函数的实现细节还依赖了其他辅助函数,如判断当前位置是否合法的 isValidMove 函数、判断目标位置是否为空的 isPositionEmpty 函数等(详细可见完整代码部分)。

//game.c // 判断N子棋游戏是否结束并返回胜利方 char check_win(char board[board_size][board_size]) { // 定义四个方向:水平、垂直、左上到右下、右上到左下 int directions[4][2] = { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; int n = board_size;// 棋盘大小 for (int i = 0; i if (board[i][j] == EMPTY) { continue; // 当前位置没有棋子,跳过 } char player = board[i][j]; for (int k = 0; k // 沿当前方向连续相同颜色的棋子数 count++; x += dx; y += dy; } if (count >= WinNum) { return player; // 有一方获胜,返回胜利方 } } } } // 判断棋盘是否已满,即平局 for (int i = 0; i if (board[i][j] == EMPTY) { return NOFULL; // 棋盘还未满 } } } return FULL; // 棋盘已满平局 } // 判断N子棋游戏加权连子数,以及棋型并给予相应的得分 int evaluate_count(char board[board_size][board_size]) { int directions[4][2] = { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; // 定义四个方向的增量数组,分别表示横向、纵向、左上到右下斜向、右上到左下斜向 int n = board_size; int count; int sorce = 0; // 初始化计数器和得分 for (int i = 0; i if (board[i][j] == EMPTY) { continue; // 当前位置没有棋子,跳过 } char player = board[i][j]; for (int k = 0; k count++; x += dx; y += dy; } // 在当前方向上统计连续的棋子个数,直到边界或者不是同一种棋子 if (player == AI) { sorce += 20 * (int)pow(count, 2);// AI连成其他长度的连子,基于平方的得分增加 } if (player == PLAYER) { sorce -= 30 * (int)pow(count, 2); // 玩家连成其他长度的连子,基于平方的得分减少 } // 判断棋型并给予相应的得分 if (count == WinNum) { if (player == AI) sorce += 1000; // “活五”,AI得分加1000 else if (player == PLAYER) sorce -= 1500; // “活五”,玩家得分减1500 } else if (count == WinNum - 1) { if ((player == AI && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, i - dx, j - dy)) || (player == PLAYER && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, i - dx, j - dy))) { if (player == AI) sorce += 800; // “活四”,AI得分加800 else if (player == PLAYER) sorce -= 1200; // “活四”,玩家得分减1200 } } else if (count == WinNum - 2) { if ((player == AI && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, x - 2 * dx, y - 2 * dy) && isPositionEmpty(board, i - dx, j - dy) && isPositionEmpty(board, i + 2 * dx, j + 2 * dy)) || (player == PLAYER && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, x - 2 * dx, y - 2 * dy) && isPositionEmpty(board, i - dx, j - dy) && isPositionEmpty(board, i + 2 * dx, j + 2 * dy))) { if (player == AI) sorce += 300; // “活三”,AI得分加300 else if (player == PLAYER) sorce -= 400; // “活三”,玩家得分减400 } } } } } return sorce; } 3.极小化极大算法 int minimax( )

这段代码是一个使用极小化极大算法评估当前局面得分的函数 minimax,该函数接受一个二维字符数组 board 作为参数,表示棋盘状态,以及当前搜索深度 depth、当前玩家是否为最大化玩家 isMaximizingPlayer 以及当前位置 (i, j)。

函数首先判断是否达到指定深度 或 游戏已经结束,如果是则返回当前局面的得分,调用 evaluate 函数计算基本得分。如果游戏未结束,则根据当前玩家是最大化玩家还是最小化玩家进行不同的处理。

如果是最大化玩家(即AI),函数会初始化最大得分为负无穷大,并通过两层循环遍历棋盘上的每个位置。对于每个空格的位置,假设AI在该位置下棋,然后递归调用 minimax 函数切换到最小化玩家,继续搜索子节点的得分。在递归结束后,将该位置恢复为空格,并更新最大得分 maxEval。

如果是最小化玩家(即玩家),函数会初始化最小得分为正无穷大,并通过两层循环遍历棋盘上的每个位置。对于每个空格的位置,假设玩家在该位置下棋,然后递归调用 minimax 函数切换到最大化玩家,继续搜索子节点的得分。在递归结束后,将该位置恢复为空格,并更新最小得分 minEval。

最终,如果当前玩家为最大化玩家,则返回最大得分 maxEval;如果当前玩家为最小化玩家,则返回最小得分 minEval。

需要注意的是,这段代码中使用了一个评估函数 evaluate,用于计算每个局面的得分。在 evaluate 函数中,首先调用 evaluate_count 函数计算基本得分,然后根据棋盘中心的距离对基本得分进行调整,离中心越远得分越低。最后返回调整后的得分。

//game.c //评估当前局面的得分 int evaluate(char board[board_size][board_size], int i, int j) { int sorce = evaluate_count(board);// 先调用evaluate_count函数计算基本得分 sorce += (1000/sqrt((i + 1 - board_size/2) * (i + 1 - board_size/2) + (j + 1 - board_size/2) * (j + 1 - board_size/2))/ board_size); // 根据棋盘中心的距离对基本得分进行调整,离中心越远,得分越低 return sorce; } // 极小化极大算法 int minimax(char board[board_size][board_size], int depth, int isMaximizingPlayer, int i, int j) { // 如果达到指定深度或游戏结束,则返回当前局面的得分 if (depth == 0 || check_win(board) != NOFULL) { return evaluate(board, i, j); // 如果游戏结束,则返回当前局面的得分 } if (isMaximizingPlayer) // 最大化玩家(AI) { int maxEval = -1000000;// 初始化最大得分为负无穷大 int i, j; for (i = 0; i if (board[i][j] == EMPTY) { // 检查该位置是否为空格 board[i][j] = AI; // 假设AI在该位置下棋 int eval = minimax(board, depth - 1, 0, i, j); // 递归搜索子节点,切换到最小化玩家 board[i][j] = EMPTY; // 恢复该位置为空格 if (eval > maxEval) { maxEval = eval; // 更新最大得分 } } } } return maxEval; } else { // 最小化玩家(玩家) int minEval = 1000000; // 初始化最小得分为正无穷大 int i, j; for (i = 0; i if (board[i][j] == EMPTY) { // 检查该位置是否为空格 board[i][j] = PLAYER; // 假设玩家在该位置下棋 int eval = minimax(board, depth - 1, 1, i, j); // 递归搜索子节点,切换到最小化玩家 board[i][j] = EMPTY; // 恢复该位置为空格 if (eval int i = 0; int j = 0; for (i = 0; i board[i][j] = EMPTY; } } } //打印棋盘 void display_board(char board[board_size][board_size]) { int i = 0; int j = 0; int tmp = 0; printf(" "); for (i = 1; i printf(" "); } printf(" %2d", i); } printf("\n"); for (i = 0; i for (j = 0; j printf("---|"); } else { printf(" |"); } } } else { for (j = 0; j printf("%2d|", i / 2 + 1); } else { tmp = i / 2; printf(" %c |", board[tmp][j - 1]); } } } printf("\n"); } } // 判断位置是否为空 int isPositionEmpty(char board[board_size][board_size], int x, int y) { return board[x][y] == EMPTY; } //玩家下棋 void player_move(char board[board_size][board_size]) { int x = 0; int y = 0; while (1) { scanf("%d%d", &y, &x); //判断输入是否合法 if ((x >= 1 && x = 1 && y board[x - 1][y - 1] = PLAYER; break; } else { printf("该位置已有棋子, 请重新输入!\n"); } } else { printf("输入非法,请重新输入!\n"); } } } // 判断当前位置是否为有效的落子点 bool isValidMove(int x, int y) { // 判断 (x, y) 是否在棋盘范围内,这里假设棋盘大小为board_size if (x = board_size || y = board_size) { return false; } return true; } // 判断五N子棋游戏是否结束并返回胜利方 char check_win(char board[board_size][board_size]) { // 定义四个方向:水平、垂直、左上到右下、右上到左下 int directions[4][2] = { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; int n = board_size;// 棋盘大小 for (int i = 0; i if (board[i][j] == EMPTY) { continue; // 当前位置没有棋子,跳过 } char player = board[i][j]; for (int k = 0; k // 沿当前方向连续相同颜色的棋子数 count++; x += dx; y += dy; } if (count >= WinNum) { return player; // 有一方获胜,返回胜利方 } } } } // 判断棋盘是否已满,即平局 for (int i = 0; i if (board[i][j] == EMPTY) { return NOFULL; // 棋盘还未满 } } } return FULL; // 棋盘已满平局 } // 判断五N子棋游戏加权连子数,以及棋型并给予相应的得分 int evaluate_count(char board[board_size][board_size]) { int directions[4][2] = { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; // 定义四个方向的增量数组,分别表示横向、纵向、左上到右下斜向、右上到左下斜向 int n = board_size; int count; int sorce = 0; // 初始化计数器和得分 for (int i = 0; i if (board[i][j] == EMPTY) { continue; // 当前位置没有棋子,跳过 } char player = board[i][j]; for (int k = 0; k count++; x += dx; y += dy; } // 在当前方向上统计连续的棋子个数,直到边界或者不是同一种棋子 if (player == AI) { sorce += 20 * (int)pow(count, 2);// AI连成其他长度的连子,基于平方的得分增加 } if (player == PLAYER) { sorce -= 30 * (int)pow(count, 2); // 玩家连成其他长度的连子,基于平方的得分减少 } // 判断棋型并给予相应的得分 if (count == WinNum) { if (player == AI) sorce += 1000; // “活五”,AI得分加1000 else if (player == PLAYER) sorce -= 1500; // “活五”,玩家得分减1500 } else if (count == WinNum - 1) { if ((player == AI && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, i - dx, j - dy)) || (player == PLAYER && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, i - dx, j - dy))) { if (player == AI) sorce += 800; // “活四”,AI得分加800 else if (player == PLAYER) sorce -= 1200; // “活四”,玩家得分减1200 } } else if (count == WinNum - 2) { if ((player == AI && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, x - 2 * dx, y - 2 * dy) && isPositionEmpty(board, i - dx, j - dy) && isPositionEmpty(board, i + 2 * dx, j + 2 * dy)) || (player == PLAYER && isPositionEmpty(board, x + dx, y + dy) && isPositionEmpty(board, x - 2 * dx, y - 2 * dy) && isPositionEmpty(board, i - dx, j - dy) && isPositionEmpty(board, i + 2 * dx, j + 2 * dy))) { if (player == AI) sorce += 300; // “活三”,AI得分加300 else if (player == PLAYER) sorce -= 400; // “活三”,玩家得分减400 } } } } } return sorce; } //评估当前局面的得分 int evaluate(char board[board_size][board_size], int i, int j) { int sorce = evaluate_count(board);// 先调用evaluate_count函数计算基本得分 sorce += (1000/sqrt((i + 1 - board_size/2) * (i + 1 - board_size/2) + (j + 1 - board_size/2) * (j + 1 - board_size/2))/ board_size); // 根据棋盘中心的距离对基本得分进行调整,离中心越远,得分越低 return sorce; } // 极小化极大算法 int minimax(char board[board_size][board_size], int depth, int isMaximizingPlayer, int i, int j) { // 如果达到指定深度或游戏结束,则返回当前局面的得分 if (depth == 0 || check_win(board) != NOFULL) { return evaluate(board, i, j); // 如果游戏结束,则返回当前局面的得分 } if (isMaximizingPlayer) // 最大化玩家(AI) { int maxEval = -1000000;// 初始化最大得分为负无穷大 int i, j; for (i = 0; i if (board[i][j] == EMPTY) { // 检查该位置是否为空格 board[i][j] = AI; // 假设AI在该位置下棋 int eval = minimax(board, depth - 1, 0, i, j); // 递归搜索子节点,切换到最小化玩家 board[i][j] = EMPTY; // 恢复该位置为空格 if (eval > maxEval) { maxEval = eval; // 更新最大得分 } } } } return maxEval; } else { // 最小化玩家(玩家) int minEval = 1000000; // 初始化最小得分为正无穷大 int i, j; for (i = 0; i if (board[i][j] == EMPTY) { // 检查该位置是否为空格 board[i][j] = PLAYER; // 假设玩家在该位置下棋 int eval = minimax(board, depth - 1, 1, i, j); // 递归搜索子节点,切换到最小化玩家 board[i][j] = EMPTY; // 恢复该位置为空格 if (eval int bestEval = -1000000;// 初始化最优得分为负无穷大 int bestRow = -1;// 初始化最优位置的行号 int bestCol = -1;// 初始化最优位置的列号 int depth = 0;//递归深度,深度越高耗时越长 // 根据棋盘大小确定递归深度 if (board_size depth = 5; } else if (board_size depth = 3; } else{ depth = 2; } int i, j; for (i = 0; i if (board[i][j] == EMPTY) { board[i][j] = AI; // 假设AI在该位置下棋 int eval = minimax(board, depth, 0, i, j); // 调用极小化极大算法计算得分 board[i][j] = EMPTY; // 恢复该位置为空格 if (eval > bestEval) { bestEval = eval; // 更新最优得分 bestRow = i;// 更新最优位置的行号 bestCol = j;// 更新最优位置的列号 } } } } // 在最优位置下棋 board[bestRow][bestCol] = AI; } 源文件 test.c //test.c #define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" //定义胜利条件 int WinNum = 0; void menu() { printf("********************\n"); printf("****** 1.play ******\n"); printf("****** 0.exit ******\n"); printf("********************\n"); printf("请选择:>"); } bool display_win(char board[board_size][board_size]) { if (check_win(board) == PLAYER) { display_board(board); // 最终显示棋盘 printf("玩家胜利!\n"); return true; } else if (check_win(board) == AI) { display_board(board); // 最终显示棋盘 printf("AI 胜利!\n"); return true; } else if (check_win(board) == FULL) { display_board(board); // 最终显示棋盘 printf("平局!\n"); return true; } return false; } void game(char board[board_size][board_size]) { int is_player_turn = 0; // 玩家先手 printf("************************\n"); printf("****** 1.玩家先手 ******\n"); printf("****** 0.AI先手 *******\n"); printf("************************\n"); printf("请输入:>"); scanf("%d", &is_player_turn); int turn = 0; // 回合数 init_board(board); // 游戏循环 while (1) { turn++; if (is_player_turn) { system("cls");// 清屏 printf("***** %d子棋游戏!*****\n", WinNum); display_board(board); // 显示棋盘 printf("请玩家输入坐标下棋:>"); player_move(board); // 玩家下棋 if (display_win(board)) break; system("cls"); display_board(board); // 显示棋盘 printf("AI正在下棋!\n"); ai_move(board); // AI 下棋 if (display_win(board)) break; } else { system("cls");// 清屏 printf("***** %d子棋游戏!*****\n", WinNum); display_board(board); // 显示棋盘 printf("AI正在下棋!\n"); if (turn == 1) { board[board_size/2][board_size / 2] = AI; } else { ai_move(board); // AI 下棋 if (display_win(board)) break; } system("cls");// 清屏 printf("***** %d子棋游戏!*****\n", WinNum); display_board(board); // 显示棋盘 printf("请玩家输入坐标下棋:>"); player_move(board); // 玩家下棋 if (display_win(board)) break; } } printf("总共进行了 %d 回合。\n", turn); } int main() { while (1) { menu(); int input = 0; scanf("%d", & input); if (input) { while (1) { printf("请输入连胜棋子个数:>"); scanf("%d", &WinNum); if (WinNum > board_size) { printf("输入的连胜棋子个数已超过棋盘大小,请重新输入,或者调整棋盘大小\n"); } else { printf("**** %d子棋游戏开始 ****\n", WinNum); break; } } } else { printf("已退出%d子棋游戏!\n", WinNum); break; } char board[board_size][board_size] = { 0 }; //创建棋盘 game(board); } return 0; }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3