Files

595 lines
17 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
/**
* @brief 将指令复制到powershell
* gcc "课上代码练习\五子棋\五子棋 copy 3.c" -o output/五子棋 copy 3.exe
* @brief 将指令复制到powershell
* .\output\五子棋 copy 3.exe
*/
// !宏定义
#define MAX_BOARD_SIZE 25 //* 最大支持棋盘尺寸(5x5到25x25)
int BOARD_SIZE = 15; //* 实际使用的棋盘尺寸(默认15)
#define PLAYER 1 //* 玩家棋子标识符
#define AI 2 //* AI棋子标识符
#define EMPTY 0 //* 空位置标识符
#define MAX_STEPS (MAX_BOARD_SIZE * MAX_BOARD_SIZE) //* 最大步数(棋盘总格数)
// !全局变量
int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE] = {0}; //* 棋盘状态存储数组(默认棋盘全空为0)
int AI_DEPTH = 3; //* AI思考深度(默认3层)
const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; //* 四个方向:向下、向右、右下、左下
// !落子步骤结构体
typedef struct // 落子结构体
{
int player; // 落子方标识
int x, y; // 坐标位置
} Step;
// !方向信息结构体
typedef struct
{
int continuous_chess; // 连续棋子数量
bool check_start; // 序列起点方向是否开放(空位)
bool check_end; // 序列终点方向是否开放(空位)
} DirInfo;
Step steps[MAX_STEPS]; // 存储所有落子步骤的数组
int step_count = 0; // 当前步数计数器
// !初始化棋盘:清空棋盘状态和步数记录
void empty_board();
// !打印当前棋盘状态
void print_board();
// !检查位置是否有效(在棋盘范围内且为空位)
bool have_space(int x, int y);
// !玩家落子操作
bool player_move(int x, int y);
// !计算特定方向上连续同色棋子数量
DirInfo count_specific_direction(int x, int y, int dx, int dy, int player);
// !检查特定位置落子后是否获胜
bool check_win(int x, int y, int player);
// !评估特定位置对当前玩家的价值
int evaluate_pos(int x, int y, int player);
// !带α-β剪枝的深度优先搜索(极小极大算法实现)
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing);
// !AI落子算法
void ai_move(int depth);
// !复盘游戏过程
void review_process();
// !主函数:游戏流程控制
int main()
{
// 初始化阶段:获取棋盘尺寸和AI难度
printf("===== 五子棋人机对战 =====\n");
printf("通常棋盘大小分为休闲棋盘(13X13)、标准棋盘(15X15)和特殊棋盘(19X19)\n");
printf("请输入棋盘大小(5~%d)(默认为标准棋盘):", MAX_BOARD_SIZE);
scanf("%d", &BOARD_SIZE);
// 校验输入是否合法,不合法时使用默认值
if (BOARD_SIZE < 5 || BOARD_SIZE > MAX_BOARD_SIZE)
{
BOARD_SIZE = 15;
printf("输入无效,使用默认标准棋盘15X15\n");
}
printf("请输入AI思考深度(1-5, 数值越大AI越强但思考时间越长, 默认为3):");
scanf("%d", &AI_DEPTH);
// 校验AI深度范围
if (AI_DEPTH < 1 || AI_DEPTH > 5)
{
AI_DEPTH = 3;
printf("输入无效,使用默认深度3\n");
}
empty_board(); // 初始化棋盘
printf("===== 五子棋人机对战(%dX%d棋盘, AI深度%d) =====", BOARD_SIZE, BOARD_SIZE, AI_DEPTH);
print_board(); // 打印初始空棋盘
// 游戏主循环
while (1)
{
// 玩家回合
int x, y;
printf("\n请输入落子坐标(行 列,1~%d):", BOARD_SIZE);
scanf("%d %d", &x, &y);
// 转换用户输入的1-base坐标为0-base索引
x--;
y--;
// 验证并执行玩家移动
if (!player_move(x, y)) // 无效位置处理
{
printf("坐标无效!请重新输入。\n");
continue; // 跳回循环开头重新输入
}
print_board(); // 更新后打印棋盘
// 检查玩家是否获胜
if (check_win(x, y, PLAYER))
{
printf("\n玩家获胜!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
// AI回合
printf("\nAI思考中...\n");
ai_move(AI_DEPTH); // AI计算最佳落子位置
print_board(); // 展示AI落子后的棋盘
// 检查AI是否获胜(通过最后一步)
Step last_step = steps[step_count - 1]; // 获取最后一步
if (check_win(last_step.x, last_step.y, AI))
{
printf("\nAI获胜!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
// 检查平局(棋盘已满)
if (step_count == BOARD_SIZE * BOARD_SIZE)
{
printf("\n平局!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
}
return 0;
}
// !初始化棋盘:清空棋盘状态和步数记录
void empty_board()
{
// 初始化棋盘状态为全空
for (int i = 0; i < BOARD_SIZE; i++)
{
for (int j = 0; j < BOARD_SIZE; j++)
{
board[i][j] = EMPTY;
}
}
step_count = 0; // 重置步数计数器
}
// !打印当前棋盘状态
void print_board()
{
// 打印列号(1-BOARD_SIZE显示)
printf("\n ");
for (int i = 0; i < BOARD_SIZE; i++)
{
printf("%2d", i + 1);
if (i + 1 == 9) // 处理列号9和10+的对齐
printf(" ");
}
printf("\n");
// 逐行打印棋盘内容
for (int i = 0; i < BOARD_SIZE; i++)
{
printf("%2d ", i + 1); // 打印行号(1-BOARD_SIZE
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] == PLAYER)
printf("x "); // 玩家棋子
else if (board[i][j] == AI)
printf(""); // AI棋子(使用○显示)
else
printf("· "); // 空位
}
printf("\n"); // 每行结束换行
}
}
// !检查位置是否有效(在棋盘范围内且为空位)
bool have_space(int x, int y)
{
// 校验坐标是否在范围内且该位置为空
return (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && board[x][y] == EMPTY);
}
// !玩家落子操作
bool player_move(int x, int y)
{
// 位置无效则返回false
if (!have_space(x, y))
return false;
// 更新棋盘状态
board[x][y] = PLAYER;
// 记录落子步骤:玩家标识和坐标
steps[step_count++] = (Step){PLAYER, x, y};
return true;
}
// !计算特定方向上连续同色棋子数量
DirInfo count_specific_direction(int x, int y, int dx, int dy, int player)
{
DirInfo info;
info.continuous_chess = 1; // 起始位置已经有一个棋子
info.check_start = false; // 起点方向是否开放
info.check_end = false; // 终点方向是否开放
// 检查正方向(dx, dy
int nx = x + dx, ny = y + dy;
while (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && board[nx][ny] == player)
{
info.continuous_chess++; // 连续棋子计数增加
nx += dx; // 沿当前方向前进
ny += dy;
}
// 判断正方向端点是否开放(遇到空位)
if (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE)
if (board[nx][ny] == EMPTY)
info.check_end = true;
// 检查反方向(-dx, -dy
nx = x - dx, ny = y - dy;
while (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && board[nx][ny] == player)
{
info.continuous_chess++; // 连续棋子计数增加
nx -= dx; // 沿相反方向前进
ny -= dy;
}
// 判断反方向端点是否开放(遇到空位)
if (nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE)
if (board[nx][ny] == EMPTY)
info.check_start = true;
return info;
}
// !检查特定位置落子后是否获胜
bool check_win(int x, int y, int player)
{
// 检查四个方向是否存在五连珠
for (int i = 0; i < 4; i++)
{
DirInfo info = count_specific_direction(x, y, direction[i][0], direction[i][1], player);
if (info.continuous_chess >= 5) // 连续棋子>=5即获胜
return true;
}
return false; // 四个方向都没有五连珠
}
// !评估特定位置对当前玩家的价值
int evaluate_pos(int x, int y, int player)
{
// 保存原始值用于还原
int original = board[x][y];
// 模拟在该位置落子
board[x][y] = player;
int total_score = 0; // 总分
int line_scores[4] = {0}; // 四个方向的得分
// 遍历四个方向进行评估
for (int i = 0; i < 4; i++)
{
int dx = direction[i][0], dy = direction[i][1];
// 获取当前方向上的棋型信息
DirInfo info = count_specific_direction(x, y, dx, dy, player);
// 直接形成五连珠为必胜
if (info.continuous_chess >= 5)
{
board[x][y] = original; // 还原棋盘
return 1000000; // 返回最大分
}
// 根据连续棋子数评分
switch (info.continuous_chess)
{
case 4: // 四连珠
if (info.check_start && info.check_end) // 活四(两端开放)
line_scores[i] = 100000;
else if (info.check_start || info.check_end) // 冲四(一端开放)
line_scores[i] = 10000;
else // 死四(两端封闭)
line_scores[i] = 500;
break;
case 3: // 三连珠
if (info.check_start && info.check_end) // 活三
line_scores[i] = 5000;
else if (info.check_start || info.check_end) // 眠三
line_scores[i] = 1000;
else // 死三
line_scores[i] = 50;
break;
case 2: // 二连珠
if (info.check_start && info.check_end) // 活二
line_scores[i] = 500;
else if (info.check_start || info.check_end) // 眠二
line_scores[i] = 100;
else // 死二
line_scores[i] = 10;
break;
case 1: // 单子
if (info.check_start && info.check_end) // 开放位置
line_scores[i] = 50;
else if (info.check_start || info.check_end) // 半开放位置
line_scores[i] = 10;
else // 封闭位置
line_scores[i] = 1;
break;
}
}
// 计算总分(最高方向分+其他方向分加权)
int max_score = 0;
int sum_score = 0;
for (int i = 0; i < 4; i++)
{
if (line_scores[i] > max_score)
max_score = line_scores[i];
sum_score += line_scores[i];
}
total_score = max_score * 10 + sum_score; // 主方向权重更高
// 位置奖励:越靠近中心分数越高
int center_x = BOARD_SIZE / 2;
int center_y = BOARD_SIZE / 2;
int distance = abs(x - center_x) + abs(y - center_y); // 曼哈顿距离
int position_bonus = 50 * (BOARD_SIZE - distance); // 距离中心越近奖励越高
board[x][y] = original; // 还原棋盘状态
return total_score + position_bonus; // 返回总评估分
}
// !带α-β剪枝的深度优先搜索(极小极大算法实现)
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing)
{
// 检查当前落子是否获胜
if (check_win(x, y, player))
{
return (player == AI) ? 1000000 + depth : -1000000 - depth;
}
// 达到搜索深度或平局
if (depth == 0 || step_count >= BOARD_SIZE * BOARD_SIZE)
{
return evaluate_pos(x, y, AI) - evaluate_pos(x, y, PLAYER);
}
int best_score = is_maximizing ? -1000000 : 1000000;
// 遍历所有可能落子位置
for (int i = 0; i < BOARD_SIZE; i++)
{
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] != EMPTY)
continue;
// 模拟当前玩家落子
board[i][j] = player;
step_count++;
// 递归搜索(切换玩家和搜索深度)
int current_score = dfs(i, j, (player == AI) ? PLAYER : AI, depth - 1, alpha, beta, !is_maximizing);
// 撤销落子
board[i][j] = EMPTY;
step_count--;
// 极大值玩家(AI)逻辑
if (is_maximizing)
{
best_score = (current_score > best_score) ? current_score : best_score;
alpha = (best_score > alpha) ? best_score : alpha;
// α剪枝
if (beta <= alpha)
{
break;
}
}
// 极小值玩家(人类)逻辑
else
{
best_score = (current_score < best_score) ? current_score : best_score;
beta = (best_score < beta) ? best_score : beta;
// β剪枝
if (beta <= alpha)
{
break;
}
}
}
if ((is_maximizing && best_score >= beta) || (!is_maximizing && best_score <= alpha))
{
break; // 提前退出外层循环
}
}
return best_score;
}
// !使用带α-β剪枝的DFS极小极大算法更新AI落子
void ai_move(int depth)
{
// 1. 首先检查是否需要阻止玩家的四子连棋或三子活棋
for (int i = 0; i < BOARD_SIZE; i++)
{
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] != EMPTY)
continue;
// 模拟玩家在此位置落子
board[i][j] = PLAYER;
bool need_block = false;
// 检查四个方向
for (int k = 0; k < 4; k++)
{
DirInfo info = count_specific_direction(i, j, direction[k][0], direction[k][1], PLAYER);
// 如果玩家能形成四子连棋且至少一端开放
if (info.continuous_chess >= 4 && (info.check_start || info.check_end))
{
need_block = true;
break;
}
// 如果玩家能形成三子活棋且两端开放
if (info.continuous_chess == 3 && info.check_start && info.check_end)
{
need_block = true;
break;
}
}
board[i][j] = EMPTY; // 恢复棋盘
if (need_block)
{
// 必须在此位置落子阻止
board[i][j] = AI;
steps[step_count++] = (Step){AI, i, j};
printf("AI落子(%d, %d)\n", i + 1, j + 1);
return;
}
}
}
// 2. 如果没有需要立即阻止的情况,则正常评估
int best_score = -1000000;
int best_x = -1, best_y = -1;
// 遍历棋盘所有空位
for (int i = 0; i < BOARD_SIZE; i++)
{
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] != EMPTY)
continue;
// 只考虑已有棋子附近(2格范围内)
bool has_nearby_stone = false;
for (int di = -2; di <= 2; di++)
{
for (int dj = -2; dj <= 2; dj++)
{
int ni = i + di;
int nj = j + dj;
if (ni >= 0 && ni < BOARD_SIZE &&
nj >= 0 && nj < BOARD_SIZE)
{
if (board[ni][nj] != EMPTY)
{
has_nearby_stone = true;
break;
}
}
}
if (has_nearby_stone)
break;
}
if (!has_nearby_stone && step_count > 10)
continue;
// 模拟AI落子
board[i][j] = AI;
int current_score = dfs(i, j, PLAYER, depth, -1000000, 1000000, false);
board[i][j] = EMPTY;
// 更新最佳位置
if (current_score > best_score)
{
best_score = current_score;
best_x = i;
best_y = j;
}
}
}
// 执行最佳落子
if (best_x != -1 && best_y != -1)
{
board[best_x][best_y] = AI;
steps[step_count++] = (Step){AI, best_x, best_y};
printf("AI落子(%d, %d)\n", best_x + 1, best_y + 1);
}
}
// !复盘游戏过程
void review_process()
{
printf("\n===== 复盘记录(总步数:%d) =====\n", step_count);
// 清空输入缓冲区
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
// 创建临时复盘棋盘
int temp_board[MAX_BOARD_SIZE][MAX_BOARD_SIZE];
memset(temp_board, EMPTY, sizeof(temp_board)); // 初始化为空棋盘
// 逐步重现游戏过程
for (int i = 0; i < step_count; i++)
{
Step s = steps[i]; // 获取当前步骤
temp_board[s.x][s.y] = s.player; // 在临时棋盘上落子
// 打印当前步骤信息
printf("\n===== 五子棋人机对战(%dX%d棋盘) =====", BOARD_SIZE, BOARD_SIZE);
printf("\n 第%d步/%d步: %s 落子于(%d, %d)\n",
i + 1, step_count,
(s.player == PLAYER) ? "玩家" : "AI", // 三目运算符选择显示文本
s.x + 1, s.y + 1); // 显示1-base坐标
// 打印当前复盘棋盘
printf(" ");
for (int col = 0; col < BOARD_SIZE; col++)
printf("%2d", col + 1); // 列号
printf("\n");
for (int row = 0; row < BOARD_SIZE; row++)
{
printf("%2d ", row + 1); // 行号
for (int col = 0; col < BOARD_SIZE; col++)
{
if (temp_board[row][col] == PLAYER)
printf("x ");
else if (temp_board[row][col] == AI)
printf("");
else
printf("· ");
}
printf("\n"); // 行结束换行
}
// 如果不是最后一步,等待用户按键继续
if (i < step_count - 1)
{
printf("\n按Enter继续下一步...");
while (getchar() != '\n')
; // 等待回车
}
}
printf("\n复盘结束!按Enter返回...");
getchar(); // 等待用户按键
}