Files
Gobang-Game/gobang.c
T
2025-06-30 21:02:13 +08:00

715 lines
22 KiB
C
Raw 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 "gobang.h"
#include "game_mode.h"
#include "ai.h"
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
// 全局变量定义
int BOARD_SIZE = 15; // 实际使用的棋盘尺寸(默认15)
int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE] = {0}; // 棋盘状态存储数组(默认棋盘全空为0)
Step steps[MAX_STEPS]; // 存储所有落子步骤的数组
const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; // 四个方向:向下、向右、右下、左下
int step_count = 0; // 当前步数计数器
bool use_forbidden_moves = false; // 默认不启用禁手规则
int use_timer = 0; // 默认不启用计时器
int time_limit = 30; // 默认时间限制为30秒
/**
* @brief 初始化棋盘为全空状态并重置步数计数器
* 清空棋盘数组并将所有位置设为EMPTY,同时将step_count重置为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; // 重置步数计数器
}
/**
* @brief 打印当前棋盘状态
* 以可读格式输出棋盘,包括行列号和棋子状态
* 玩家棋子显示为'x'AI棋子显示为'○',空位显示为'·'
*/
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"); // 每行结束换行
}
}
/**
* @brief 检查指定位置是否有效且为空
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 位置有效且为空
* @return false 位置无效或已被占用
*/
bool have_space(int x, int y)
{
// 校验坐标是否在范围内且该位置为空
return (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && board[x][y] == EMPTY);
}
/**
* @brief 配置棋盘大小
*
* @param player1 玩家1
* @param player2 玩家2
*/
void setup_board_size()
{
printf("通常棋盘大小分为休闲棋盘(13X13)、标准棋盘(15X15)和特殊棋盘(19X19)\n");
char prompt[100];
sprintf(prompt, "请输入棋盘大小(5~%d)(默认为标准棋盘):\n", MAX_BOARD_SIZE);
BOARD_SIZE = get_integer_input(prompt, 5, MAX_BOARD_SIZE);
}
/**
* @brief Set the up game options object
* 配置游戏选项,包括禁手规则、计时器和时间限制
*/
void setup_game_options()
{
use_forbidden_moves = get_integer_input("是否启用禁手规则 (1-是, 0-否): ", 0, 1);
use_timer = get_integer_input("是否启用计时器 (1-是, 0-否): ", 0, 1);
if (use_timer)
{
time_limit = get_integer_input("请输入每回合的时间限制 (1~60分钟): ", 1, 60) * 60;
}
}
/**
* @brief 确定先手玩家
*
* @param player1
* @param player2
* @return int player1 or player2
*/
int determine_first_player(int player1, int player2)
{
char prompt[100];
sprintf(prompt, "请选择先手方 (1 for Player %d, 2 for Player %d): ", player1, player2);
int first_player_choice = get_integer_input(prompt, 1, 2);
if (first_player_choice == 1)
{
return player1;
}
else
{
return player2;
}
}
/**
* @brief 检查是否为禁手
*
* @param x
* @param y
* @param player
* @return true
* @return false
*/
bool is_forbidden_move(int x, int y, int player)
{
if (!use_forbidden_moves)
{
return false;
}
if (player != PLAYER && player != PLAYER1)
{
return false;
}
board[x][y] = player;
int three_count = 0;
int four_count = 0;
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)
{
board[x][y] = EMPTY;
return true; // 长连禁手
}
if (info.continuous_chess == 3 && info.check_start && info.check_end)
{
three_count++;
}
if (info.continuous_chess == 4 && (info.check_start || info.check_end))
{
four_count++;
}
}
board[x][y] = EMPTY;
if (three_count >= 2 || four_count >= 2)
{
return true; // 三三或四四禁手
}
return false;
}
/**
* @brief 执行玩家落子操作
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 落子成功
* @return false 落子失败(位置无效)
*/
bool player_move(int x, int y, int player)
{
// 位置无效则返回false
if (!have_space(x, y))
return false;
if (is_forbidden_move(x, y, player))
{
printf("禁手!请选择其他位置。\n");
return false;
}
// 更新棋盘状态
board[x][y] = player;
// 记录落子步骤:玩家标识和坐标
steps[step_count++] = (Step){player, x, y};
return true;
}
/**
* @brief 计算特定方向上连续同色棋子数量
* @param x 起始行坐标
* @param y 起始列坐标
* @param dx 行方向增量(-1,0,1)
* @param dy 列方向增量(-1,0,1)
* @param player 玩家标识(PLAYER/AI)
* @return DirInfo 包含连续棋子数和方向开放状态的结构体
* @note 检查正反两个方向,统计连续棋子数并判断端点是否开放
*/
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; // 四个方向都没有五连珠
}
/**
* @brief 复盘游戏全过程并展示评分
* @note 实现流程:
* 1. 初始化临时复盘棋盘
* 2. 按步数顺序逐步重现每个落子
* 3. 每步显示:
* - 当前步数/总步数
* - 落子方(玩家/AI)
* - 落子位置(1-based坐标)
* - 当前棋盘状态
* 4. 通过用户按Enter键控制步骤前进
* 5. 复盘结束后自动进入评分环节:
* - 评估双方表现
* - 显示得分
* - 评选MVP
* @note 技术细节:
* - 使用独立临时棋盘避免影响主游戏状态
* - 坐标显示转换为1-based方便用户理解
* - 包含输入缓冲区清理防止意外输入
* - 评分环节调用evaluate_performance()函数
*/
void review_process(int game_mode)
{
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; // 在临时棋盘上落子
// 打印当前步骤信息
// 根据游戏模式显示不同的标题和玩家信息
if (game_mode == 1)
{
// 人机对战
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);
}
else
{
// 双人对战
printf("\n===== 五子棋双人对战(%dX%d棋盘) =====", BOARD_SIZE, BOARD_SIZE);
printf("\n 第%d步/%d步: %s 落子于(%d, %d)\n",
i + 1, step_count,
(s.player == PLAYER1) ? "玩家1(黑棋)" : "玩家2(白棋)",
s.x + 1, s.y + 1);
}
// 打印当前复盘棋盘
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 || temp_board[row][col] == PLAYER1)
printf("x ");
else if (temp_board[row][col] == AI || temp_board[row][col] == PLAYER2)
printf("");
else
printf("· ");
}
printf("\n"); // 行结束换行
}
// 如果不是最后一步,等待用户按键继续
if (i < step_count - 1)
{
printf("\n按Enter继续下一步...");
while (getchar() != '\n')
; // 等待回车
}
}
printf("\n复盘结束!按Enter查看评分...");
getchar(); // 等待用户按键
// 评估双方表现
printf("\n===== 对局评分 =====\n");
int player1_score = 0, player2_score = 0;
// 遍历所有步数,累积每一步的得分
for (int i = 0; i < step_count; i++)
{
if (steps[i].player == PLAYER || steps[i].player == PLAYER1)
{
player1_score += calculate_step_score(steps[i].x, steps[i].y, steps[i].player);
}
else
{
player2_score += calculate_step_score(steps[i].x, steps[i].y, steps[i].player);
}
}
double sum_score = (long double)player1_score + (long double)player2_score;
if (sum_score > 0)
{
if (game_mode == 1)
{
printf("玩家得分: %d, 占比: %.2f%%\n",
player1_score, (double)player1_score * 100.0 / sum_score);
printf("AI得分: %d, 占比: %.2f%%\n",
player2_score, (double)player2_score * 100.0 / sum_score);
}
else
{
printf("玩家1(黑棋)得分: %d, 占比: %.2f%%\n",
player1_score, (double)player1_score * 100.0 / sum_score);
printf("玩家2(白棋)得分: %d, 占比: %.2f%%\n",
player2_score, (double)player2_score * 100.0 / sum_score);
}
}
else
{
if (game_mode == 1)
{
printf("玩家得分: %d\n", player1_score);
printf("AI得分: %d\n", player2_score);
}
else
{
printf("玩家1(黑棋)得分: %d\n", player1_score);
printf("玩家2(白棋)得分: %d\n", player2_score);
}
printf("注: 双方得分均为0,无法计算占比\n");
}
// 评选MVP
if (player1_score > player2_score)
{
printf("\nMVP: %s (领先 %d 分)\n", (game_mode == 1) ? "玩家" : "玩家1(黑棋)", player1_score - player2_score);
}
else if (player2_score > player1_score)
{
printf("\nMVP: %s (领先 %d 分)\n", (game_mode == 1) ? "AI" : "玩家2(白棋)", player2_score - player1_score);
}
else
{
printf("\n双方势均力敌!\n");
}
getchar();
}
/**
* @brief 处理游戏结束后的记录保存
* @return int 保存状态码(0-成功, 1-目录创建失败, 2-文件打开失败, 3-文件写入失败)
*/
void handle_save_record(int game_mode)
{
int save_choice = 0;
printf("===== 游戏结束 =====\n");
printf("是否保存游戏记录? (1-是, 0-否): ");
scanf("%d", &save_choice);
if (save_choice == 1)
{
time_t now = time(NULL);
struct tm *t = localtime(&now);
char filename[256];
strftime(filename, sizeof(filename), "%Y%m%d_%H%M%S.txt", t);
int save_status = save_game_to_file(filename, game_mode);
switch (save_status)
{
case 0: // 成功
printf("\n游戏记录已成功保存至: %s\n", filename);
printf("您可以使用以下命令进行复盘: .\\gobang.exe -l %s\n", filename);
break;
case 1: // 目录创建失败
printf("\n游戏记录保存失败: 无法创建 'records' 目录。\n");
printf("请检查程序是否具有足够的写入权限或磁盘空间是否充足。\n");
break;
case 2: // 文件打开失败
printf("\n游戏记录保存失败: 无法在路径 '%s' 创建文件。\n", filename);
printf("请检查路径是否有效以及程序是否具有写入权限。\n");
break;
case 3: // 文件写入失败
printf("\n游戏记录保存失败: 写入文件时发生错误。\n");
printf("请检查磁盘空间是否已满。\n");
break;
default:
printf("\n游戏记录保存失败: 发生未知错误。\n");
break;
}
}
}
/**
* @brief 悔棋功能实现
*
* @param steps_to_undo 要悔棋的步数
* @return true 悔棋成功
* @return false 悔棋失败(步数不足)
*/
bool return_move(int steps_to_undo)
{
if (step_count < steps_to_undo)
{
return false;
}
for (int i = 0; i < steps_to_undo; i++)
{
if (step_count > 0)
{
step_count--;
board[steps[step_count].x][steps[step_count].y] = EMPTY;
}
}
return true;
}
/**
* @brief 评估玩家在整盘棋局中的表现
* @param player 要评估的玩家(PLAYER/AI)
* @return int 总分(已考虑方向重复计算)
* @note 评分标准:
* - 五连:2500
* - 活四:1000 冲四:500 死四:250
* - 活三:250 眠三:100 死三:50
* - 活二:50 眠二:20 死二:10
* - 开放单子:10 半开放单子:5 封闭单子:1
* @note 实现细节:
* 1. 遍历棋盘所有位置
* 2. 对每个棋子检查四个方向
* 3. 统计所有连子情况并评分
* 4. 最终分数除以4(消除方向重复计算影响)
*/
int calculate_step_score(int x, int y, int player)
{
int step_score = 0;
// 检查四个方向
for (int k = 0; k < 4; k++)
{
DirInfo info = count_specific_direction(x, y, direction[k][0], direction[k][1], player);
// 根据连子数评分
switch (info.continuous_chess)
{
case 5:
step_score += 2500;
break; // 五连
case 4:
if (info.check_start && info.check_end)
step_score += 1000; // 活四
else if (info.check_start || info.check_end)
step_score += 500; // 冲四
else
step_score += 250; // 死四
break;
case 3:
if (info.check_start && info.check_end)
step_score += 250; // 活三
else if (info.check_start || info.check_end)
step_score += 100; // 眠三
else
step_score += 50; // 死三
break;
case 2:
if (info.check_start && info.check_end)
step_score += 50; // 活二
else if (info.check_start || info.check_end)
step_score += 20; // 眠二
else
step_score += 10; // 死二
break;
case 1:
if (info.check_start && info.check_end)
step_score += 10; // 开放单子
else if (info.check_start || info.check_end)
step_score += 5; // 半开放单子
else
step_score += 1; // 封闭单子
break;
}
}
return step_score;
}
/**
* @brief 评估玩家在整盘棋局中的表现
* @param player 要评估的玩家(PLAYER/AI)
* @return int 总分(已考虑方向重复计算)
* @note 评分标准:
* - 五连:2500
* - 活四:1000 冲四:500 死四:250
* - 活三:250 眠三:100 死三:50
* - 活二:50 眠二:20 死二:10
* - 开放单子:10 半开放单子:5 封闭单子:1
* @note 实现细节:
* 1. 遍历棋盘所有位置
* 2. 对每个棋子检查四个方向
* 3. 统计所有连子情况并评分
* 4. 最终分数除以4(消除方向重复计算影响)
*/
int evaluate_performance(int player)
{
int total_score = 0;
// 遍历棋盘所有位置
for (int i = 0; i < BOARD_SIZE; i++)
{
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] == player)
{
total_score += calculate_step_score(i, j, player);
}
}
}
return total_score / 4; // 每个方向都计算了,需要除以4
}
/**
* @brief 将当前游戏记录保存到文件
* @param filename 要保存的文件名
* @return int 错误码:
* 0: 成功
* 1: 目录创建失败
* 2: 文件打开失败
* 3: 文件写入失败
*/
int save_game_to_file(const char *filename, int game_mode)
{
// 创建records目录(如果不存在)
struct stat st = {0};
if (stat("records", &st) == -1)
{
if (mkdir("records") != 0)
{
// 检查是否目录已存在(多线程情况下可能被其他线程创建)
if (stat("records", &st) == -1)
{
#ifdef _WIN32
printf("错误:无法创建records目录\n");
printf("可能原因:\n");
printf("1. 没有写入权限 - 请尝试以管理员身份运行\n");
printf("2. 防病毒软件阻止 - 请检查安全软件设置\n");
printf("3. 路径无效 - 请检查工作目录\n");
#else
perror("创建目录失败");
#endif
return 1; // 目录创建失败
}
}
}
// 打开文件
char fullpath[256];
snprintf(fullpath, sizeof(fullpath), "records/%s", filename);
FILE *file = fopen(fullpath, "w");
if (!file)
{
return 2; // 文件打开失败
}
// 写入游戏模式和棋盘大小
if (fprintf(file, "%d\n%d\n", game_mode, BOARD_SIZE) < 0)
{
fclose(file);
return 3; // 文件写入失败
}
// 写入所有落子步骤
for (int i = 0; i < step_count; i++)
{
if (fprintf(file, "%d %d %d\n", steps[i].player, steps[i].x, steps[i].y) < 0)
{
fclose(file);
return 3; // 文件写入失败
}
}
if (fclose(file) != 0)
{
return 3; // 文件关闭/写入失败
}
return 0; // 成功
}
/**
* @brief 从文件加载游戏记录
* @param filename 要加载的文件名
* @return true 加载成功
* @return false 加载失败
*/
int load_game_from_file(const char *filename)
{
// 打开文件
char fullpath[256];
snprintf(fullpath, sizeof(fullpath), "records/%s", filename);
FILE *file = fopen(fullpath, "r");
if (!file)
{
return false;
}
// 读取游戏模式和棋盘大小
int game_mode, size;
if (fscanf(file, "%d", &game_mode) != 1 || (game_mode != 1 && game_mode != 2))
{
fclose(file);
return 0; // 无效的游戏模式
}
if (fscanf(file, "%d", &size) != 1 || size < 5 || size > MAX_BOARD_SIZE)
{
fclose(file);
return false;
}
// 初始化棋盘
BOARD_SIZE = size;
empty_board();
// 读取所有落子步骤
step_count = 0;
while (fscanf(file, "%d %d %d", &steps[step_count].player, &steps[step_count].x, &steps[step_count].y) == 3)
{
step_count++;
}
fclose(file);
return game_mode;
}