mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-30 10:35:55 +08:00
chore: 删除旧 C+IUP 代码,仅保留 Rust+Tauri+React v2.0 代码
This commit is contained in:
-456
@@ -1,456 +0,0 @@
|
||||
/**
|
||||
* @file ai.c
|
||||
* @note 本文件定义了AI模块的函数和变量
|
||||
* @note 包括:
|
||||
* 1. 评估一个落子位置的综合得分(结合进攻和防守)
|
||||
* 2. 评估指定位置的价值
|
||||
* 3. 评估棋盘价值
|
||||
*/
|
||||
|
||||
#include "gobang.h"
|
||||
#include "ai.h"
|
||||
#include "config.h"
|
||||
#include "globals.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ==================== 辅助函数声明 ====================
|
||||
static int compare_moves(const void *a, const void *b);
|
||||
|
||||
/**
|
||||
* @brief 评估一个落子位置的综合得分(结合进攻和防守)
|
||||
* @param x 行坐标
|
||||
* @param y 列坐标
|
||||
* @return int 综合得分
|
||||
*/
|
||||
int evaluate_move(int x, int y)
|
||||
{
|
||||
// 进攻得分:评估AI在此处落子的分数
|
||||
int attack_score = evaluate_pos(x, y, AI);
|
||||
|
||||
// 防守得分:评估玩家在此处落子的分数,作为防守的依据
|
||||
int defense_score = evaluate_pos(x, y, PLAYER);
|
||||
|
||||
// 综合得分,防守权重由 defense_coefficient 控制
|
||||
return attack_score + (int)(defense_score * defense_coefficient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 评估特定位置对当前玩家的战略价值
|
||||
* @param x 行坐标(0-base)
|
||||
* @param y 列坐标(0-base)
|
||||
* @param player 玩家标识(PLAYER/AI)
|
||||
* @return int 综合评估分数(越高表示位置越好)
|
||||
* @note 评分标准:
|
||||
* - 活四:100000 冲四:10000 死四:500
|
||||
* - 活三:5000 眠三:1000 死三:50
|
||||
* - 活二:500 眠二:100 死二:10
|
||||
* - 单子:50(开放)/10(半开放)/1(封闭)
|
||||
* - 中心位置有额外加成
|
||||
*/
|
||||
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 SEARCH_WIN_BONUS; // 返回最大分
|
||||
}
|
||||
|
||||
// 根据连续棋子数评分
|
||||
switch (info.continuous_chess)
|
||||
{
|
||||
case 4: // 四连珠
|
||||
if (info.check_start && info.check_end) // 活四(两端开放)
|
||||
line_scores[i] = AI_SCORE_LIVE_FOUR;
|
||||
else if (info.check_start || info.check_end) // 冲四(一端开放)
|
||||
line_scores[i] = AI_SCORE_RUSH_FOUR;
|
||||
else // 死四(两端封闭)
|
||||
line_scores[i] = AI_SCORE_DEAD_FOUR;
|
||||
break;
|
||||
|
||||
case 3: // 三连珠
|
||||
if (info.check_start && info.check_end) // 活三
|
||||
line_scores[i] = AI_SCORE_LIVE_THREE;
|
||||
else if (info.check_start || info.check_end) // 眠三
|
||||
line_scores[i] = AI_SCORE_SLEEP_THREE;
|
||||
else // 死三
|
||||
line_scores[i] = AI_SCORE_DEAD_THREE;
|
||||
break;
|
||||
|
||||
case 2: // 二连珠
|
||||
if (info.check_start && info.check_end) // 活二
|
||||
line_scores[i] = AI_SCORE_LIVE_TWO;
|
||||
else if (info.check_start || info.check_end) // 眠二
|
||||
line_scores[i] = AI_SCORE_SLEEP_TWO;
|
||||
else // 死二
|
||||
line_scores[i] = AI_SCORE_DEAD_TWO;
|
||||
break;
|
||||
|
||||
case 1: // 单子
|
||||
if (info.check_start && info.check_end) // 开放位置
|
||||
line_scores[i] = AI_SCORE_LIVE_ONE;
|
||||
else if (info.check_start || info.check_end) // 半开放位置
|
||||
line_scores[i] = AI_SCORE_HALF_ONE;
|
||||
else // 封闭位置
|
||||
line_scores[i] = AI_SCORE_DEAD_ONE;
|
||||
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 = AI_POSITION_BONUS_FACTOR * (BOARD_SIZE - distance); // 距离中心越近奖励越高
|
||||
|
||||
board[x][y] = original; // 还原棋盘状态
|
||||
return total_score + position_bonus; // 返回总评估分
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AI决策主函数,使用评估函数和搜索算法选择最佳落子位置
|
||||
* @note 采用两阶段决策逻辑:
|
||||
* 1. 防御阶段:检查并阻止玩家即将获胜的位置(活四、冲四、活三)
|
||||
* 2. 进攻阶段:若无紧急防御需求,使用评估函数选择最佳进攻位置
|
||||
* @note 实现细节:
|
||||
* - 优先处理玩家活四、冲四等危险局面
|
||||
* - 步数>AI_SEARCH_RANGE_THRESHOLD时缩小搜索范围到已有棋子附近AI_NEARBY_RANGE格
|
||||
* - 使用中心位置优先策略
|
||||
*/
|
||||
void ai_move(int depth)
|
||||
{
|
||||
// 如果是第一步,直接下在中心位置附近
|
||||
if (step_count == 0)
|
||||
{
|
||||
int center = BOARD_SIZE / 2;
|
||||
board[center][center] = AI;
|
||||
steps[step_count++] = (Step){AI, center, center};
|
||||
printf("AI落子(%d, %d)\n", center + 1, center + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 使用增强的威胁检测系统
|
||||
ScoredMove candidate_moves[BOARD_SIZE * BOARD_SIZE];
|
||||
int move_count = generate_candidate_moves(candidate_moves, AI);
|
||||
|
||||
// 首先检查是否有直接获胜的机会
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
ThreatLevel ai_threat = detect_threat(i, j, AI);
|
||||
if (ai_threat == THREAT_WIN)
|
||||
{
|
||||
// 直接获胜
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 获胜!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要阻止玩家的威胁
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
ThreatLevel player_threat = detect_threat(i, j, PLAYER);
|
||||
if (player_threat >= THREAT_FOUR)
|
||||
{
|
||||
// 必须阻止玩家的四子威胁
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 防守!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要阻止玩家的活三威胁
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
ThreatLevel player_threat = detect_threat(i, j, PLAYER);
|
||||
if (player_threat == THREAT_THREE)
|
||||
{
|
||||
// 阻止玩家的活三
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 阻止活三!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 寻找最佳进攻位置
|
||||
// 优先考虑能形成威胁的位置
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
ThreatLevel ai_threat = detect_threat(i, j, AI);
|
||||
if (ai_threat >= THREAT_FOUR)
|
||||
{
|
||||
// 形成四子威胁
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 形成威胁!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 寻找能形成活三的位置
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
ThreatLevel ai_threat = detect_threat(i, j, AI);
|
||||
if (ai_threat == THREAT_THREE)
|
||||
{
|
||||
// 形成活三
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 形成活三!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果没有明显的威胁机会,选择评分最高的位置
|
||||
if (move_count > 0)
|
||||
{
|
||||
// candidate_moves已经按分数排序,直接选择第一个
|
||||
int best_x = candidate_moves[0].x;
|
||||
int best_y = candidate_moves[0].y;
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 备用方案:如果没有候选移动,随机选择一个位置
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] == EMPTY)
|
||||
{
|
||||
board[i][j] = AI;
|
||||
steps[step_count++] = (Step){AI, i, j};
|
||||
printf("AI落子(%d, %d) - 备用位置!\n", i + 1, j + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AI增强:辅助函数实现 ====================
|
||||
|
||||
/**
|
||||
* @brief 比较函数,用于移动排序(按分数降序)
|
||||
*/
|
||||
static int compare_moves(const void *a, const void *b)
|
||||
{
|
||||
const ScoredMove *move_a = (const ScoredMove *)a;
|
||||
const ScoredMove *move_b = (const ScoredMove *)b;
|
||||
return move_b->score - move_a->score; // 降序排列
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成候选移动并按评估分数排序
|
||||
* @param moves 存储候选移动的数组
|
||||
* @param player 当前玩家
|
||||
* @return 候选移动数量
|
||||
*/
|
||||
int generate_candidate_moves(ScoredMove *moves, int player)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] != EMPTY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只考虑有意义的位置(附近有棋子)
|
||||
if (step_count > AI_SEARCH_RANGE_THRESHOLD && !is_near_stones(i, j))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算该位置的评估分数
|
||||
moves[count].x = i;
|
||||
moves[count].y = j;
|
||||
|
||||
// 结合威胁检测和位置评估
|
||||
ThreatLevel threat = detect_threat(i, j, player);
|
||||
int base_score = evaluate_move(i, j);
|
||||
|
||||
// 根据威胁等级调整分数
|
||||
switch (threat)
|
||||
{
|
||||
case THREAT_WIN:
|
||||
moves[count].score = base_score + 10000;
|
||||
break;
|
||||
case THREAT_FOUR:
|
||||
moves[count].score = base_score + 5000;
|
||||
break;
|
||||
case THREAT_THREE:
|
||||
moves[count].score = base_score + 2000;
|
||||
break;
|
||||
case THREAT_DOUBLE:
|
||||
moves[count].score = base_score + 1000;
|
||||
break;
|
||||
case THREAT_POTENTIAL:
|
||||
moves[count].score = base_score + 500;
|
||||
break;
|
||||
default:
|
||||
moves[count].score = base_score;
|
||||
break;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// 按分数降序排序
|
||||
qsort(moves, count, sizeof(ScoredMove), compare_moves);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查位置是否在已有棋子附近
|
||||
* @param x, y 要检查的位置
|
||||
* @return 如果附近有棋子返回true
|
||||
*/
|
||||
bool is_near_stones(int x, int y)
|
||||
{
|
||||
for (int di = -AI_NEARBY_RANGE; di <= AI_NEARBY_RANGE; di++)
|
||||
{
|
||||
for (int dj = -AI_NEARBY_RANGE; dj <= AI_NEARBY_RANGE; dj++)
|
||||
{
|
||||
int ni = x + di;
|
||||
int nj = y + dj;
|
||||
if (ni >= 0 && ni < BOARD_SIZE && nj >= 0 && nj < BOARD_SIZE)
|
||||
{
|
||||
if (board[ni][nj] != EMPTY)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检测在指定位置落子的威胁等级
|
||||
* @param x, y 落子位置
|
||||
* @param player 落子玩家
|
||||
* @return 威胁等级
|
||||
*/
|
||||
ThreatLevel detect_threat(int x, int y, int player)
|
||||
{
|
||||
// 模拟落子
|
||||
board[x][y] = player;
|
||||
|
||||
ThreatLevel max_threat = THREAT_NONE;
|
||||
int threat_count = 0;
|
||||
|
||||
// 检查四个方向
|
||||
for (int k = 0; k < 4; k++)
|
||||
{
|
||||
DirInfo info = count_specific_direction(x, y, direction[k][0], direction[k][1], player);
|
||||
ThreatLevel current_threat = THREAT_NONE;
|
||||
|
||||
// 检查是否形成五子连珠(获胜)
|
||||
if (info.continuous_chess >= 5)
|
||||
{
|
||||
current_threat = THREAT_WIN;
|
||||
}
|
||||
// 检查是否形成活四或冲四
|
||||
else if (info.continuous_chess == 4)
|
||||
{
|
||||
if (info.check_start && info.check_end)
|
||||
{
|
||||
current_threat = THREAT_FOUR; // 活四
|
||||
}
|
||||
else if (info.check_start || info.check_end)
|
||||
{
|
||||
current_threat = THREAT_FOUR; // 冲四
|
||||
}
|
||||
}
|
||||
// 检查是否形成活三
|
||||
else if (info.continuous_chess == 3 && info.check_start && info.check_end)
|
||||
{
|
||||
current_threat = THREAT_THREE;
|
||||
}
|
||||
// 检查潜在威胁
|
||||
else if (info.continuous_chess >= 2)
|
||||
{
|
||||
current_threat = THREAT_POTENTIAL;
|
||||
}
|
||||
|
||||
if (current_threat > max_threat)
|
||||
{
|
||||
max_threat = current_threat;
|
||||
}
|
||||
|
||||
if (current_threat >= THREAT_THREE)
|
||||
{
|
||||
threat_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复棋盘
|
||||
board[x][y] = EMPTY;
|
||||
|
||||
// 如果有多个活三威胁,升级为双威胁(如双三、四三等组合)
|
||||
// 注意:不能把已经是活四的威胁降级为双威胁
|
||||
if (threat_count >= 2 && max_threat == THREAT_THREE)
|
||||
{
|
||||
max_threat = THREAT_DOUBLE;
|
||||
}
|
||||
|
||||
return max_threat;
|
||||
}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
/**
|
||||
* @file config.c
|
||||
* @brief 五子棋游戏参数配置源文件
|
||||
* @note 本文件集中定义了五子棋游戏的所有参数配置,便于统一管理和修改
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "globals.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* @brief 加载游戏配置
|
||||
*/
|
||||
void load_game_config()
|
||||
{
|
||||
FILE *file = fopen(CONFIG_FILE, "r");
|
||||
if (file == NULL)
|
||||
{
|
||||
// 配置文件不存在,使用默认配置
|
||||
printf("配置文件不存在,使用默认配置\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), file))
|
||||
{
|
||||
// 去除换行符
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
|
||||
// 解析配置项
|
||||
if (strncmp(line, "BOARD_SIZE=", 11) == 0)
|
||||
{
|
||||
int size = atoi(line + 11);
|
||||
if (size >= MIN_BOARD_SIZE && size <= MAX_BOARD_SIZE)
|
||||
{
|
||||
BOARD_SIZE = size;
|
||||
}
|
||||
}
|
||||
else if (strncmp(line, "USE_FORBIDDEN_MOVES=", 20) == 0)
|
||||
{
|
||||
use_forbidden_moves = (atoi(line + 20) != 0);
|
||||
}
|
||||
else if (strncmp(line, "USE_TIMER=", 10) == 0)
|
||||
{
|
||||
use_timer = atoi(line + 10);
|
||||
}
|
||||
else if (strncmp(line, "TIME_LIMIT=", 11) == 0)
|
||||
{
|
||||
int minutes = atoi(line + 11);
|
||||
if (minutes > 0)
|
||||
{
|
||||
time_limit = minutes * 60; // 配置文件存分钟,内部转为秒
|
||||
}
|
||||
}
|
||||
else if (strncmp(line, "NETWORK_PORT=", 13) == 0)
|
||||
{
|
||||
int port = atoi(line + 13);
|
||||
if (port >= MIN_NETWORK_PORT && port <= MAX_NETWORK_PORT)
|
||||
{
|
||||
network_port = port;
|
||||
}
|
||||
}
|
||||
else if (strncmp(line, "NETWORK_TIMEOUT=", 16) == 0)
|
||||
{
|
||||
int timeout = atoi(line + 16);
|
||||
if (timeout > 0)
|
||||
{
|
||||
network_timeout = timeout;
|
||||
}
|
||||
}
|
||||
else if (strncmp(line, "AI_DIFFICULTY=", 14) == 0)
|
||||
{
|
||||
int difficulty = atoi(line + 14);
|
||||
if (difficulty >= 1 && difficulty <= 5)
|
||||
{
|
||||
ai_difficulty = difficulty;
|
||||
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
|
||||
}
|
||||
}
|
||||
else if (strncmp(line, "LLM_USE=", 8) == 0)
|
||||
{
|
||||
llm_use = (atoi(line + 8) != 0) ? 1 : 0;
|
||||
}
|
||||
else if (strncmp(line, "LLM_ENDPOINT=", 13) == 0)
|
||||
{
|
||||
strncpy(llm_endpoint, line + 13, MAX_LLM_ENDPOINT_LEN - 1);
|
||||
llm_endpoint[MAX_LLM_ENDPOINT_LEN - 1] = '\0';
|
||||
}
|
||||
else if (strncmp(line, "LLM_API_KEY=", 12) == 0)
|
||||
{
|
||||
strncpy(llm_api_key, line + 12, MAX_LLM_API_KEY_LEN - 1);
|
||||
llm_api_key[MAX_LLM_API_KEY_LEN - 1] = '\0';
|
||||
}
|
||||
else if (strncmp(line, "LLM_MODEL=", 10) == 0)
|
||||
{
|
||||
strncpy(llm_model, line + 10, MAX_LLM_MODEL_LEN - 1);
|
||||
llm_model[MAX_LLM_MODEL_LEN - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
printf("配置加载完成\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存游戏配置
|
||||
*/
|
||||
void save_game_config()
|
||||
{
|
||||
FILE *file = fopen(CONFIG_FILE, "w");
|
||||
if (file == NULL)
|
||||
{
|
||||
printf("无法保存配置文件\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(file, "# 五子棋游戏配置文件\n");
|
||||
fprintf(file, "# 棋盘大小 (范围: %d-%d)\n", MIN_BOARD_SIZE, MAX_BOARD_SIZE);
|
||||
fprintf(file, "BOARD_SIZE=%d\n", BOARD_SIZE);
|
||||
fprintf(file, "\n# 禁手规则 (0=关闭, 1=开启)\n");
|
||||
fprintf(file, "USE_FORBIDDEN_MOVES=%d\n", use_forbidden_moves ? 1 : 0);
|
||||
fprintf(file, "\n# 计时器 (0=关闭, 1=开启)\n");
|
||||
fprintf(file, "USE_TIMER=%d\n", use_timer);
|
||||
fprintf(file, "\n# 时间限制 (分钟)\n");
|
||||
fprintf(file, "TIME_LIMIT=%d\n", time_limit / 60); // 内部存秒,配置文件存分钟
|
||||
fprintf(file, "\n# 网络端口 (范围: %d-%d)\n", MIN_NETWORK_PORT, MAX_NETWORK_PORT);
|
||||
fprintf(file, "NETWORK_PORT=%d\n", network_port);
|
||||
fprintf(file, "\n# 网络超时时间 (毫秒)\n");
|
||||
fprintf(file, "NETWORK_TIMEOUT=%d\n", network_timeout);
|
||||
|
||||
fprintf(file, "\n# AI难度 (1-5)\n");
|
||||
fprintf(file, "AI_DIFFICULTY=%d\n", ai_difficulty);
|
||||
|
||||
fprintf(file, "\n# 大模型AI设置\n");
|
||||
fprintf(file, "# 是否使用大模型 (0=算法AI, 1=大模型)\n");
|
||||
fprintf(file, "LLM_USE=%d\n", llm_use);
|
||||
fprintf(file, "# API地址\n");
|
||||
fprintf(file, "LLM_ENDPOINT=%s\n", llm_endpoint);
|
||||
fprintf(file, "# API Key\n");
|
||||
fprintf(file, "LLM_API_KEY=%s\n", llm_api_key);
|
||||
fprintf(file, "# 模型名称\n");
|
||||
fprintf(file, "LLM_MODEL=%s\n", llm_model);
|
||||
|
||||
fclose(file);
|
||||
printf("配置保存完成\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void reset_to_default_config()
|
||||
{
|
||||
BOARD_SIZE = DEFAULT_BOARD_SIZE;
|
||||
use_forbidden_moves = DEFAULT_USE_FORBIDDEN_MOVES;
|
||||
use_timer = DEFAULT_USE_TIMER;
|
||||
time_limit = DEFAULT_TIME_LIMIT;
|
||||
network_port = DEFAULT_NETWORK_PORT;
|
||||
network_timeout = NETWORK_TIMEOUT_MS;
|
||||
ai_difficulty = 3; // 默认AI难度
|
||||
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
|
||||
|
||||
llm_use = DEFAULT_LLM_USE;
|
||||
strncpy(llm_endpoint, DEFAULT_LLM_ENDPOINT, MAX_LLM_ENDPOINT_LEN - 1);
|
||||
llm_endpoint[MAX_LLM_ENDPOINT_LEN - 1] = '\0';
|
||||
strncpy(llm_api_key, DEFAULT_LLM_API_KEY, MAX_LLM_API_KEY_LEN - 1);
|
||||
llm_api_key[MAX_LLM_API_KEY_LEN - 1] = '\0';
|
||||
strncpy(llm_model, DEFAULT_LLM_MODEL, MAX_LLM_MODEL_LEN - 1);
|
||||
llm_model[MAX_LLM_MODEL_LEN - 1] = '\0';
|
||||
|
||||
printf("已重置为默认配置\n");
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* @file globals.c
|
||||
* @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com)
|
||||
* @brief 全局变量定义和初始化文件
|
||||
* @note 集中管理所有全局变量的定义和初始化,提高代码可维护性
|
||||
*/
|
||||
|
||||
#include "globals.h"
|
||||
#include "config.h"
|
||||
|
||||
// ==================== 游戏核心变量定义 ====================
|
||||
int BOARD_SIZE = DEFAULT_BOARD_SIZE; // 实际使用的棋盘尺寸
|
||||
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 = DEFAULT_USE_FORBIDDEN_MOVES; // 是否启用禁手规则
|
||||
int use_timer = DEFAULT_USE_TIMER; // 是否启用计时器
|
||||
int time_limit = DEFAULT_TIME_LIMIT; // 每回合的时间限制(秒)
|
||||
int network_port = DEFAULT_NETWORK_PORT; // 网络端口
|
||||
int network_timeout = NETWORK_TIMEOUT_MS; // 网络超时时间
|
||||
|
||||
// ==================== AI相关变量定义 ====================
|
||||
double defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT; // 防守系数
|
||||
int ai_difficulty = 3; // AI难度 (1-5)
|
||||
|
||||
// ==================== LLM大模型相关变量定义 ====================
|
||||
int llm_use = DEFAULT_LLM_USE; // 是否使用LLM
|
||||
char llm_endpoint[MAX_LLM_ENDPOINT_LEN] = DEFAULT_LLM_ENDPOINT; // API地址
|
||||
char llm_api_key[MAX_LLM_API_KEY_LEN] = DEFAULT_LLM_API_KEY; // API Key
|
||||
char llm_model[MAX_LLM_MODEL_LEN] = DEFAULT_LLM_MODEL; // 模型名
|
||||
|
||||
// ==================== 网络相关变量定义 ====================
|
||||
NetworkGameState network_state = {0}; // 网络游戏状态
|
||||
|
||||
// ==================== GUI相关变量定义 ====================
|
||||
int current_player_gui = PLAYER; // GUI当前玩家
|
||||
int game_over = 0; // 游戏结束标志
|
||||
char status_message[256] = "五子棋游戏 - 黑子先行"; // 状态消息
|
||||
|
||||
// ==================== 记录相关变量定义 ====================
|
||||
int player1_final_score = 0; // 玩家1最终得分
|
||||
int player2_final_score = 0; // 玩家2最终得分
|
||||
int scores_calculated = 0; // 评分计算标志
|
||||
char winner_info[50] = "平局或未完成"; // 存储胜负信息
|
||||
@@ -1,291 +0,0 @@
|
||||
/**
|
||||
* @file gobang.c
|
||||
* @brief 五子棋游戏源文件
|
||||
* @note 本文件定义了五子棋游戏的主要数据结构、函数和全局变量。
|
||||
* 它包含了游戏棋盘的表示、玩家操作、规则检查以及AI决策等功能。
|
||||
*/
|
||||
|
||||
#include "gobang.h"
|
||||
#include "ai.h"
|
||||
#include "record.h"
|
||||
#include "config.h"
|
||||
#include "globals.h"
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
/**
|
||||
* @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, y)位置是否为空
|
||||
* @param x 行坐标(0-base)
|
||||
* @param y 列坐标(0-base)
|
||||
* @return true-空, 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 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 悔棋功能实现
|
||||
*
|
||||
* @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 改进后的评分标准:
|
||||
* - 五连:5000 (提高权重,更强调获胜)
|
||||
* - 活四:2000 冲四:1000 死四:300 (提高权重,强调进攻性)
|
||||
* - 活三:500 眠三:200 死三:80 (提高权重,强调战略价值)
|
||||
* - 活二:100 眠二:40 死二:15 (适当提高权重)
|
||||
* - 开放单子:15 半开放单子:8 封闭单子:2 (适当提高权重)
|
||||
* @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 += SCORE_FIVE;
|
||||
break; // 五连
|
||||
case 4:
|
||||
if (info.check_start && info.check_end)
|
||||
step_score += SCORE_LIVE_FOUR; // 活四
|
||||
else if (info.check_start || info.check_end)
|
||||
step_score += SCORE_RUSH_FOUR; // 冲四
|
||||
else
|
||||
step_score += SCORE_DEAD_FOUR; // 死四
|
||||
break;
|
||||
case 3:
|
||||
if (info.check_start && info.check_end)
|
||||
step_score += SCORE_LIVE_THREE; // 活三
|
||||
else if (info.check_start || info.check_end)
|
||||
step_score += SCORE_SLEEP_THREE; // 眠三
|
||||
else
|
||||
step_score += SCORE_DEAD_THREE; // 死三
|
||||
break;
|
||||
case 2:
|
||||
if (info.check_start && info.check_end)
|
||||
step_score += SCORE_LIVE_TWO; // 活二
|
||||
else if (info.check_start || info.check_end)
|
||||
step_score += SCORE_SLEEP_TWO; // 眠二
|
||||
else
|
||||
step_score += SCORE_DEAD_TWO; // 死二
|
||||
break;
|
||||
case 1:
|
||||
if (info.check_start && info.check_end)
|
||||
step_score += SCORE_LIVE_ONE; // 开放单子
|
||||
else if (info.check_start || info.check_end)
|
||||
step_score += SCORE_HALF_ONE; // 半开放单子
|
||||
else
|
||||
step_score += SCORE_DEAD_ONE; // 封闭单子
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 位置奖励:越靠近中心分数越高
|
||||
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 = POSITION_BONUS_FACTOR * (BOARD_SIZE - distance); // 距离中心越近奖励越高
|
||||
|
||||
return step_score + position_bonus;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @file main.c
|
||||
* @brief 五子棋游戏主函数文件
|
||||
* @note 本文件包含了游戏的主循环、模式选择和游戏初始化等功能
|
||||
* @brief 将以下指令复制到powershell
|
||||
*
|
||||
* !编译(需要IUP库):
|
||||
* mingw32-make gui
|
||||
.\bin\gobang_gui.exe
|
||||
*
|
||||
* @note gcc 为编译器,添加了-lws2_32链接Windows网络库
|
||||
* @note IUP 的路径:libs\iup-3.31_Win64_dllw6_lib
|
||||
* @brief & "D:\Program Files (x86)\Inno Setup 6\iscc.exe" installer\\installer.iss
|
||||
*/
|
||||
|
||||
#include "gui.h"
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <direct.h>
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// 设置控制台编码为UTF-8
|
||||
#ifdef _WIN32
|
||||
SetConsoleOutputCP(65001); // 设置控制台输出编码
|
||||
SetConsoleCP(65001); // 设置控制台输入编码
|
||||
_mkdir("records");
|
||||
#endif
|
||||
|
||||
// 加载游戏配置
|
||||
load_game_config();
|
||||
|
||||
// 启动图形化界面
|
||||
run_gui_mode();
|
||||
return 0;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "gui_menu.h"
|
||||
#include "globals.h"
|
||||
#include <iup.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// 全局变量定义
|
||||
Ihandle *dlg = NULL;
|
||||
Ihandle *board_canvas = NULL;
|
||||
Ihandle *lbl_player = NULL;
|
||||
Ihandle *lbl_status = NULL;
|
||||
int gui_game_mode = 0; // 0: PvP, 1: PvE, 2: Replay, 3: Network
|
||||
int replay_total_steps = 0; // 复盘总步数
|
||||
|
||||
/**
|
||||
* @brief 初始化GUI
|
||||
*/
|
||||
int init_gui()
|
||||
{
|
||||
if (IupOpen(NULL, NULL) == IUP_ERROR)
|
||||
{
|
||||
printf("IupOpen failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 启用UTF-8模式,确保中文正常显示
|
||||
IupSetGlobal("UTF8MODE", "YES");
|
||||
|
||||
// 设置全局默认字体
|
||||
IupSetGlobal("DEFAULTFONT", "SimHei, 11");
|
||||
|
||||
create_main_menu();
|
||||
show_main_menu();
|
||||
|
||||
printf("图形化界面初始化成功!(IUP)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清理GUI资源
|
||||
*/
|
||||
void cleanup_gui()
|
||||
{
|
||||
if (dlg)
|
||||
{
|
||||
IupDestroy(dlg);
|
||||
dlg = NULL;
|
||||
}
|
||||
IupClose();
|
||||
printf("图形化界面已关闭\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 运行图形化界面模式
|
||||
* @note 包含初始化、主循环和清理
|
||||
*/
|
||||
void run_gui_mode()
|
||||
{
|
||||
if (init_gui() == 0)
|
||||
{
|
||||
IupMainLoop(); // 使用IUP的主循环
|
||||
cleanup_gui();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 显示消息
|
||||
*/
|
||||
void show_message(const char *message)
|
||||
{
|
||||
strncpy(status_message, message, sizeof(status_message) - 1);
|
||||
status_message[sizeof(status_message) - 1] = '\0';
|
||||
update_ui_labels();
|
||||
printf("%s\n", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新UI标签状态
|
||||
*/
|
||||
void update_ui_labels()
|
||||
{
|
||||
if (lbl_player)
|
||||
{
|
||||
if (gui_game_mode == 2) // 复盘模式
|
||||
{
|
||||
char buffer[64];
|
||||
sprintf(buffer, "进度: %d / %d", step_count, replay_total_steps);
|
||||
IupSetAttribute(lbl_player, "TITLE", buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (current_player_gui == PLAYER)
|
||||
IupSetAttribute(lbl_player, "TITLE", "当前玩家: 黑子 (玩家)");
|
||||
else
|
||||
IupSetAttribute(lbl_player, "TITLE", "当前玩家: 白子 (AI/玩家2)");
|
||||
}
|
||||
}
|
||||
|
||||
if (lbl_status)
|
||||
{
|
||||
IupSetAttribute(lbl_status, "TITLE", status_message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 屏幕坐标转棋盘坐标
|
||||
*/
|
||||
int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y)
|
||||
{
|
||||
int rel_x = screen_x - BOARD_OFFSET_X;
|
||||
int rel_y = screen_y - BOARD_OFFSET_Y;
|
||||
|
||||
*board_x = (rel_y + CELL_SIZE / 2) / CELL_SIZE; // 注意:行号对应y
|
||||
*board_y = (rel_x + CELL_SIZE / 2) / CELL_SIZE; // 列号对应x
|
||||
|
||||
return (*board_x >= 0 && *board_x < BOARD_SIZE &&
|
||||
*board_y >= 0 && *board_y < BOARD_SIZE);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#include "gui_internal.h"
|
||||
#include "globals.h"
|
||||
#include "gobang.h" // for BOARD_SIZE, etc.
|
||||
#include <iup.h>
|
||||
#include <iupdraw.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/**
|
||||
* @brief 设置绘图颜色
|
||||
*/
|
||||
void set_draw_color(Ihandle *ih, unsigned char r, unsigned char g, unsigned char b)
|
||||
{
|
||||
char color[32];
|
||||
sprintf(color, "%d %d %d", r, g, b);
|
||||
IupSetAttribute(ih, "DRAWCOLOR", color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制棋盘
|
||||
*/
|
||||
void draw_board_iup(Ihandle *ih)
|
||||
{
|
||||
set_draw_color(ih, 0, 0, 0); // 黑色
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "STROKE");
|
||||
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
// 横线
|
||||
IupDrawLine(ih,
|
||||
BOARD_OFFSET_X,
|
||||
BOARD_OFFSET_Y + i * CELL_SIZE,
|
||||
BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE,
|
||||
BOARD_OFFSET_Y + i * CELL_SIZE);
|
||||
// 竖线
|
||||
IupDrawLine(ih,
|
||||
BOARD_OFFSET_X + i * CELL_SIZE,
|
||||
BOARD_OFFSET_Y,
|
||||
BOARD_OFFSET_X + i * CELL_SIZE,
|
||||
BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE);
|
||||
}
|
||||
|
||||
// 星位/天元
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
int stars[] = {3, 7, 11}; // 15路棋盘的星位坐标
|
||||
if (BOARD_SIZE == 15)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + stars[i] * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + stars[j] * CELL_SIZE;
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int center = BOARD_SIZE / 2;
|
||||
int cx = BOARD_OFFSET_X + center * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + center * CELL_SIZE;
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制棋子
|
||||
*/
|
||||
void draw_stones_iup(Ihandle *ih)
|
||||
{
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] != EMPTY)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + j * CELL_SIZE; // j是x坐标(列)
|
||||
int cy = BOARD_OFFSET_Y + i * CELL_SIZE; // i是y坐标(行)
|
||||
|
||||
if (board[i][j] == PLAYER)
|
||||
{
|
||||
// 黑子
|
||||
set_draw_color(ih, 0, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 白子
|
||||
set_draw_color(ih, 255, 255, 255);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
|
||||
set_draw_color(ih, 0, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "STROKE");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记最后落子位置 (红色小点)
|
||||
if (step_count > 0 && step_count <= MAX_STEPS)
|
||||
{
|
||||
// 绘制最后一步的标记
|
||||
// 最后一步的坐标是 steps[step_count-1]
|
||||
// 所以 step_count-1 是最后一步的索引
|
||||
|
||||
Step last = steps[step_count - 1];
|
||||
int cx = BOARD_OFFSET_X + last.y * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + last.x * CELL_SIZE;
|
||||
|
||||
set_draw_color(ih, 255, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
@@ -1,783 +0,0 @@
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "gui_menu.h"
|
||||
#include "globals.h"
|
||||
#include "gobang.h"
|
||||
#include "ai.h"
|
||||
#include "record.h"
|
||||
#include "network.h"
|
||||
#include "llm_ai.h"
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <iup.h>
|
||||
#include <iupdraw.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static Ihandle *timer = NULL; // 网络轮询定时器
|
||||
static Ihandle *llm_timer = NULL; // LLM异步轮询定时器
|
||||
|
||||
/**
|
||||
* @brief 网络事件轮询回调
|
||||
*/
|
||||
static int timer_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (gui_game_mode != 3 || game_over)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
NetworkMessage msg;
|
||||
// 非阻塞接收消息
|
||||
if (receive_network_message(&msg, 0))
|
||||
{
|
||||
if (msg.type == MSG_MOVE)
|
||||
{
|
||||
int bx = msg.x;
|
||||
int by = msg.y;
|
||||
int pid = msg.player_id;
|
||||
|
||||
if (have_space(bx, by))
|
||||
{
|
||||
player_move(bx, by, pid);
|
||||
if (check_win(bx, by, pid))
|
||||
{
|
||||
game_over = 1;
|
||||
sprintf(status_message, "对手获胜!");
|
||||
IupMessage("游戏结束", "对手获胜!");
|
||||
}
|
||||
else
|
||||
{
|
||||
current_player_gui = network_state.local_player_id;
|
||||
sprintf(status_message, "轮到你落子");
|
||||
}
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
}
|
||||
else if (msg.type == MSG_DISCONNECT || msg.type == MSG_SURRENDER)
|
||||
{
|
||||
game_over = 1;
|
||||
sprintf(status_message, "对手已断开连接/认输");
|
||||
IupMessage("游戏结束", "对手已退出游戏,你赢了!");
|
||||
update_ui_labels();
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_network_connected() && !game_over)
|
||||
{
|
||||
game_over = 1;
|
||||
sprintf(status_message, "与服务器断开连接");
|
||||
IupMessage("错误", "网络连接已断开");
|
||||
update_ui_labels();
|
||||
}
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理AI落子结果(LLM或算法)
|
||||
*/
|
||||
static void process_ai_move_result(void)
|
||||
{
|
||||
Step last_step = steps[step_count - 1];
|
||||
if (check_win(last_step.x, last_step.y, AI))
|
||||
{
|
||||
game_over = 1;
|
||||
sprintf(status_message, "AI获胜!");
|
||||
IupMessage("游戏结束", "AI获胜!");
|
||||
}
|
||||
else
|
||||
{
|
||||
current_player_gui = PLAYER;
|
||||
sprintf(status_message, "轮到玩家");
|
||||
}
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief LLM异步轮询定时器回调
|
||||
*/
|
||||
static int llm_timer_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
int x, y;
|
||||
int result = llm_ai_poll_result(&x, &y);
|
||||
|
||||
if (result == 0)
|
||||
return IUP_DEFAULT; // 仍在思考
|
||||
|
||||
// 停止轮询定时器
|
||||
if (llm_timer)
|
||||
{
|
||||
IupSetAttribute(llm_timer, "RUN", "NO");
|
||||
}
|
||||
|
||||
if (result == 1 && x >= 0 && y >= 0 && player_move(x, y, AI))
|
||||
{
|
||||
// LLM成功且落子合法
|
||||
}
|
||||
else
|
||||
{
|
||||
// LLM失败或坐标非法,回退到算法AI
|
||||
if (result == 1)
|
||||
snprintf(status_message, sizeof(status_message), "大模型返回非法位置,使用算法AI");
|
||||
else
|
||||
snprintf(status_message, sizeof(status_message), "大模型响应失败,使用算法AI");
|
||||
update_ui_labels();
|
||||
ai_move(ai_difficulty);
|
||||
}
|
||||
|
||||
process_ai_move_result();
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MAP_CB 回调:Canvas映射后强制重绘
|
||||
*/
|
||||
static int map_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
IupUpdate(board_canvas);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ACTION 回调:负责重绘(经典木纹风格)
|
||||
*/
|
||||
int action_cb(Ihandle *ih)
|
||||
{
|
||||
HWND hwnd = (HWND)IupGetAttribute(ih, "WID");
|
||||
if (!hwnd)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
HDC hdc = GetDC(hwnd);
|
||||
if (!hdc)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
|
||||
// === 预创建所有 GDI 对象 ===
|
||||
HBRUSH bg_brush = CreateSolidBrush(RGB(CLR_BOARD_BG_R, CLR_BOARD_BG_G, CLR_BOARD_BG_B));
|
||||
HBRUSH black_outer = CreateSolidBrush(RGB(CLR_BLACK_STONE_R, CLR_BLACK_STONE_G, CLR_BLACK_STONE_B));
|
||||
HBRUSH black_core = CreateSolidBrush(RGB(0, 0, 0));
|
||||
HBRUSH black_hl = CreateSolidBrush(RGB(CLR_BLACK_HIGHLIGHT_R, CLR_BLACK_HIGHLIGHT_G, CLR_BLACK_HIGHLIGHT_B));
|
||||
HBRUSH white_outer = CreateSolidBrush(RGB(CLR_WHITE_BORDER_R, CLR_WHITE_BORDER_G, CLR_WHITE_BORDER_B));
|
||||
HBRUSH white_core = CreateSolidBrush(RGB(CLR_WHITE_STONE_R, CLR_WHITE_STONE_G, CLR_WHITE_STONE_B));
|
||||
HBRUSH white_hl = CreateSolidBrush(RGB(CLR_WHITE_HIGHLIGHT_R, CLR_WHITE_HIGHLIGHT_G, CLR_WHITE_HIGHLIGHT_B));
|
||||
HBRUSH star_brush = CreateSolidBrush(RGB(CLR_STAR_POINT_R, CLR_STAR_POINT_G, CLR_STAR_POINT_B));
|
||||
HPEN grid_pen = CreatePen(PS_SOLID, 1, RGB(CLR_GRID_LINE_R, CLR_GRID_LINE_G, CLR_GRID_LINE_B));
|
||||
HPEN border_pen = CreatePen(PS_SOLID, 2, RGB(CLR_BOARD_BORDER_R, CLR_BOARD_BORDER_G, CLR_BOARD_BORDER_B));
|
||||
HPEN last_move_pen = CreatePen(PS_SOLID, 2, RGB(CLR_LAST_MOVE_R, CLR_LAST_MOVE_G, CLR_LAST_MOVE_B));
|
||||
HPEN null_pen = CreatePen(PS_NULL, 0, RGB(0, 0, 0));
|
||||
HFONT hfont_coord = CreateFont(12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, 0, 0, 0, 0, "SimHei");
|
||||
|
||||
// 1. 填充背景
|
||||
FillRect(hdc, &rc, bg_brush);
|
||||
|
||||
// 2. 绘制棋盘边框(深色外框)
|
||||
HPEN prev_pen = (HPEN)SelectObject(hdc, border_pen);
|
||||
HBRUSH prev_brush = (HBRUSH)SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
|
||||
int grid_left = BOARD_OFFSET_X;
|
||||
int grid_top = BOARD_OFFSET_Y;
|
||||
int grid_right = BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE;
|
||||
int grid_bottom = BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE;
|
||||
Rectangle(hdc, grid_left - 2, grid_top - 2, grid_right + 3, grid_bottom + 3);
|
||||
|
||||
// 3. 绘制棋盘网格
|
||||
SelectObject(hdc, grid_pen);
|
||||
SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
MoveToEx(hdc, grid_left, BOARD_OFFSET_Y + i * CELL_SIZE, NULL);
|
||||
LineTo(hdc, grid_right, BOARD_OFFSET_Y + i * CELL_SIZE);
|
||||
MoveToEx(hdc, BOARD_OFFSET_X + i * CELL_SIZE, grid_top, NULL);
|
||||
LineTo(hdc, BOARD_OFFSET_X + i * CELL_SIZE, grid_bottom);
|
||||
}
|
||||
|
||||
// 4. 星位/天元(实心圆)
|
||||
SelectObject(hdc, null_pen);
|
||||
SelectObject(hdc, star_brush);
|
||||
if (BOARD_SIZE == 15)
|
||||
{
|
||||
int stars[] = {3, 7, 11};
|
||||
for (int si = 0; si < 3; si++)
|
||||
for (int sj = 0; sj < 3; sj++)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + stars[si] * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + stars[sj] * CELL_SIZE;
|
||||
Ellipse(hdc, cx - 4, cy - 4, cx + 5, cy + 5);
|
||||
}
|
||||
}
|
||||
else if (BOARD_SIZE >= 9)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
Ellipse(hdc, cx - 4, cy - 4, cx + 5, cy + 5);
|
||||
}
|
||||
|
||||
// 5. 绘制坐标标注
|
||||
{
|
||||
HFONT prev_font = (HFONT)SelectObject(hdc, hfont_coord);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
SetTextColor(hdc, RGB(CLR_BOARD_BORDER_R, CLR_BOARD_BORDER_G, CLR_BOARD_BORDER_B));
|
||||
|
||||
// 列坐标 (A, B, C, ...)
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
char label[2] = {'A' + j, '\0'};
|
||||
int tx = BOARD_OFFSET_X + j * CELL_SIZE - 4;
|
||||
TextOut(hdc, tx, grid_top - 18, label, 1);
|
||||
TextOut(hdc, tx, grid_bottom + 5, label, 1);
|
||||
}
|
||||
// 行坐标 (1, 2, 3, ...)
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
char label[4];
|
||||
int len = snprintf(label, sizeof(label), "%d", i + 1);
|
||||
int ty = BOARD_OFFSET_Y + i * CELL_SIZE - 7;
|
||||
TextOut(hdc, grid_left - 18 - (len > 1 ? 4 : 0), ty, label, len);
|
||||
TextOut(hdc, grid_right + 6, ty, label, len);
|
||||
}
|
||||
SelectObject(hdc, prev_font);
|
||||
}
|
||||
|
||||
// 6. 绘制棋子(渐变效果:3层同心圆)
|
||||
SelectObject(hdc, null_pen);
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] == EMPTY)
|
||||
continue;
|
||||
|
||||
int cx = BOARD_OFFSET_X + j * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + i * CELL_SIZE;
|
||||
|
||||
if (board[i][j] == PLAYER)
|
||||
{
|
||||
// 黑子:外圈深灰 → 中圈黑 → 中心高光
|
||||
SelectObject(hdc, black_outer);
|
||||
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
|
||||
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
|
||||
SelectObject(hdc, black_core);
|
||||
Ellipse(hdc, cx - STONE_RADIUS + 2, cy - STONE_RADIUS + 2,
|
||||
cx + STONE_RADIUS - 1, cy + STONE_RADIUS - 1);
|
||||
SelectObject(hdc, black_hl);
|
||||
Ellipse(hdc, cx - 4, cy - 5, cx - 1, cy - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 白子:外圈灰边 → 中圈白 → 中心高光
|
||||
SelectObject(hdc, white_outer);
|
||||
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
|
||||
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
|
||||
SelectObject(hdc, white_core);
|
||||
Ellipse(hdc, cx - STONE_RADIUS + 2, cy - STONE_RADIUS + 2,
|
||||
cx + STONE_RADIUS - 1, cy + STONE_RADIUS - 1);
|
||||
SelectObject(hdc, white_hl);
|
||||
Ellipse(hdc, cx - 4, cy - 5, cx - 1, cy - 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 标记最后落子位置(蓝色圆环)
|
||||
if (step_count > 0 && step_count <= MAX_STEPS)
|
||||
{
|
||||
Step last = steps[step_count - 1];
|
||||
int cx = BOARD_OFFSET_X + last.y * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + last.x * CELL_SIZE;
|
||||
SelectObject(hdc, last_move_pen);
|
||||
SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
|
||||
Ellipse(hdc, cx - 5, cy - 5, cx + 6, cy + 6);
|
||||
}
|
||||
|
||||
// 恢复原始 GDI 对象,然后清理
|
||||
SelectObject(hdc, prev_pen);
|
||||
SelectObject(hdc, prev_brush);
|
||||
ReleaseDC(hwnd, hdc);
|
||||
|
||||
DeleteObject(bg_brush);
|
||||
DeleteObject(black_outer);
|
||||
DeleteObject(black_core);
|
||||
DeleteObject(black_hl);
|
||||
DeleteObject(white_outer);
|
||||
DeleteObject(white_core);
|
||||
DeleteObject(white_hl);
|
||||
DeleteObject(star_brush);
|
||||
DeleteObject(grid_pen);
|
||||
DeleteObject(border_pen);
|
||||
DeleteObject(last_move_pen);
|
||||
DeleteObject(null_pen);
|
||||
DeleteObject(hfont_coord);
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 悔棋按钮回调
|
||||
*/
|
||||
int btn_undo_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (game_over)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
int steps_to_undo = 1;
|
||||
if (gui_game_mode == 1) // PvE
|
||||
{
|
||||
steps_to_undo = 2; // 悔棋两步(玩家+AI)
|
||||
}
|
||||
else if (gui_game_mode == 3) // Network
|
||||
{
|
||||
IupMessage("提示", "网络模式暂不支持悔棋");
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
if (step_count >= steps_to_undo)
|
||||
{
|
||||
return_move(steps_to_undo);
|
||||
|
||||
// 更新当前玩家
|
||||
if (step_count % 2 == 0)
|
||||
current_player_gui = PLAYER;
|
||||
else
|
||||
current_player_gui = AI; // or PLAYER2
|
||||
|
||||
sprintf(status_message, "已悔棋");
|
||||
update_ui_labels();
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf(status_message, "无法悔棋");
|
||||
update_ui_labels();
|
||||
}
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存按钮回调
|
||||
*/
|
||||
int btn_save_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
|
||||
Ihandle *file_dlg = IupFileDlg();
|
||||
IupSetAttribute(file_dlg, "DIALOGTYPE", "SAVE");
|
||||
IupSetAttribute(file_dlg, "TITLE", "保存游戏记录");
|
||||
IupSetAttribute(file_dlg, "FILTER", "*.csv");
|
||||
IupSetAttribute(file_dlg, "FILTERINFO", "CSV Files");
|
||||
|
||||
IupPopup(file_dlg, IUP_CENTER, IUP_CENTER);
|
||||
|
||||
if (IupGetInt(file_dlg, "STATUS") != -1)
|
||||
{
|
||||
char *filename = IupGetAttribute(file_dlg, "VALUE");
|
||||
|
||||
char *base_name = strrchr(filename, '\\');
|
||||
if (!base_name)
|
||||
base_name = strrchr(filename, '/');
|
||||
if (base_name)
|
||||
base_name++;
|
||||
else
|
||||
base_name = filename;
|
||||
|
||||
int mode;
|
||||
if (gui_game_mode == 0)
|
||||
mode = GAME_MODE_PVP;
|
||||
else if (gui_game_mode == 3)
|
||||
mode = GAME_MODE_NETWORK;
|
||||
else
|
||||
mode = GAME_MODE_AI;
|
||||
if (save_game_to_file(base_name, mode) == 0)
|
||||
{
|
||||
snprintf(status_message, sizeof(status_message), "保存成功: %s", base_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(status_message, sizeof(status_message), "保存失败");
|
||||
}
|
||||
update_ui_labels();
|
||||
}
|
||||
|
||||
IupDestroy(file_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 返回菜单回调
|
||||
*/
|
||||
int btn_back_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
|
||||
// 停止所有定时器
|
||||
if (timer)
|
||||
{
|
||||
IupSetAttribute(timer, "RUN", "NO");
|
||||
IupDestroy(timer);
|
||||
timer = NULL;
|
||||
}
|
||||
if (llm_timer)
|
||||
{
|
||||
IupSetAttribute(llm_timer, "RUN", "NO");
|
||||
IupDestroy(llm_timer);
|
||||
llm_timer = NULL;
|
||||
}
|
||||
|
||||
// 如果是网络模式,彻底清理网络资源
|
||||
if (gui_game_mode == 3)
|
||||
{
|
||||
cleanup_network();
|
||||
}
|
||||
|
||||
// 1. 先显示主菜单
|
||||
show_main_menu();
|
||||
|
||||
// 2. 销毁游戏窗口
|
||||
if (dlg)
|
||||
{
|
||||
Ihandle *old_dlg = dlg;
|
||||
dlg = NULL; // 先清除全局指针
|
||||
IupDestroy(old_dlg);
|
||||
}
|
||||
|
||||
return IUP_IGNORE; // 返回 IUP_IGNORE 以阻止默认处理
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 鼠标点击回调
|
||||
*/
|
||||
int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status)
|
||||
{
|
||||
(void)status; // 未使用
|
||||
if (gui_game_mode == 2)
|
||||
return IUP_DEFAULT; // 复盘模式禁用点击
|
||||
|
||||
if (button == IUP_BUTTON1 && pressed)
|
||||
{ // 左键按下
|
||||
if (game_over)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
if (gui_game_mode == 3 && current_player_gui != network_state.local_player_id)
|
||||
{
|
||||
return IUP_DEFAULT; // 网络模式下,非自己回合不可落子
|
||||
}
|
||||
|
||||
int board_x, board_y;
|
||||
if (screen_to_board(x, y, &board_x, &board_y))
|
||||
{
|
||||
if (have_space(board_x, board_y))
|
||||
{
|
||||
// 执行落子操作
|
||||
if (player_move(board_x, board_y, current_player_gui))
|
||||
{
|
||||
// 检查是否获胜
|
||||
if (check_win(board_x, board_y, current_player_gui))
|
||||
{
|
||||
game_over = 1;
|
||||
if (gui_game_mode == 3)
|
||||
send_move(board_x, board_y, current_player_gui); // 发送最后一步
|
||||
if (current_player_gui == PLAYER)
|
||||
{
|
||||
sprintf(status_message, "黑子获胜!");
|
||||
IupMessage("游戏结束", "黑子获胜!");
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf(status_message, "白子获胜!");
|
||||
IupMessage("游戏结束", "白子获胜!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gui_game_mode == 0) // PvP
|
||||
{
|
||||
current_player_gui = (current_player_gui == PLAYER) ? AI : PLAYER; // AI 在这里表示玩家2
|
||||
if (current_player_gui == PLAYER)
|
||||
sprintf(status_message, "轮到黑子");
|
||||
else
|
||||
sprintf(status_message, "轮到白子");
|
||||
}
|
||||
else if (gui_game_mode == 3) // Network
|
||||
{
|
||||
send_move(board_x, board_y, current_player_gui);
|
||||
current_player_gui = network_state.remote_player_id;
|
||||
sprintf(status_message, "等待对手落子...");
|
||||
}
|
||||
else // PvE
|
||||
{
|
||||
current_player_gui = AI;
|
||||
update_ui_labels();
|
||||
IupUpdate(ih); // 立即更新显示
|
||||
IupFlush(); // 强制刷新事件队列
|
||||
|
||||
if (llm_use)
|
||||
{
|
||||
// 大模型AI - 异步调用,不阻塞UI
|
||||
sprintf(status_message, "AI思考中(大模型)...");
|
||||
update_ui_labels();
|
||||
|
||||
// 创建或复用轮询定时器
|
||||
if (!llm_timer)
|
||||
{
|
||||
llm_timer = IupTimer();
|
||||
IupSetCallback(llm_timer, "ACTION_CB", (Icallback)llm_timer_cb);
|
||||
IupSetAttribute(llm_timer, "TIME", "100"); // 100ms轮询
|
||||
}
|
||||
llm_ai_start_move();
|
||||
IupSetAttribute(llm_timer, "RUN", "YES");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 算法AI - 同步调用
|
||||
sprintf(status_message, "AI思考中...");
|
||||
update_ui_labels();
|
||||
|
||||
ai_move(ai_difficulty);
|
||||
process_ai_move_result();
|
||||
}
|
||||
}
|
||||
}
|
||||
update_ui_labels();
|
||||
IupUpdate(ih); // 请求重绘
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf(status_message, "无效位置!");
|
||||
update_ui_labels();
|
||||
}
|
||||
}
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 键盘回调
|
||||
*/
|
||||
int k_any_cb(Ihandle *ih, int c)
|
||||
{
|
||||
(void)ih;
|
||||
if (c == K_ESC)
|
||||
{
|
||||
if (dlg && IupGetInt(dlg, "VISIBLE"))
|
||||
{
|
||||
btn_back_cb(ih); // 调用返回菜单逻辑
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建游戏窗口
|
||||
*/
|
||||
void create_game_window()
|
||||
{
|
||||
if (dlg)
|
||||
{
|
||||
IupDestroy(dlg);
|
||||
dlg = NULL;
|
||||
}
|
||||
|
||||
// 创建Canvas (棋盘)
|
||||
board_canvas = IupCanvas(NULL);
|
||||
if (!board_canvas)
|
||||
printf("ERROR: Failed to create board_canvas\n");
|
||||
|
||||
IupSetCallback(board_canvas, "ACTION", (Icallback)action_cb);
|
||||
IupSetCallback(board_canvas, "BUTTON_CB", (Icallback)button_cb);
|
||||
IupSetCallback(board_canvas, "K_ANY", (Icallback)k_any_cb);
|
||||
IupSetCallback(board_canvas, "MAP_CB", (Icallback)map_cb);
|
||||
|
||||
// 计算棋盘像素大小(含坐标标注区域)
|
||||
int board_pixel_size = (BOARD_SIZE - 1) * CELL_SIZE + BOARD_OFFSET_X * 2 + 20;
|
||||
char size[32];
|
||||
sprintf(size, "%dx%d", board_pixel_size, board_pixel_size);
|
||||
IupSetAttribute(board_canvas, "RASTERSIZE", size);
|
||||
IupSetAttribute(board_canvas, "EXPAND", "NO");
|
||||
IupSetAttribute(board_canvas, "BORDER", "NO");
|
||||
IupSetAttribute(board_canvas, "BGCOLOR", "212 165 116");
|
||||
|
||||
// === 创建标签 ===
|
||||
lbl_player = IupLabel("当前玩家: 黑子");
|
||||
IupSetAttribute(lbl_player, "FONT", "SimHei, 13");
|
||||
IupSetAttribute(lbl_player, "FGCOLOR", CLR_TEXT_TITLE);
|
||||
|
||||
lbl_status = IupLabel("准备开始");
|
||||
IupSetAttribute(lbl_status, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(lbl_status, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 对局信息面板 ===
|
||||
Ihandle *info_vbox = IupVbox(lbl_player, lbl_status, NULL);
|
||||
IupSetAttribute(info_vbox, "GAP", "4");
|
||||
IupSetAttribute(info_vbox, "MARGIN", "10x8");
|
||||
Ihandle *frm_info = IupFrame(info_vbox);
|
||||
IupSetAttribute(frm_info, "TITLE", "对局信息");
|
||||
IupSetAttribute(frm_info, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_info, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 按钮样式宏 ===
|
||||
#define SET_BTN_STYLE(btn, w, h, font) \
|
||||
IupSetAttribute(btn, "SIZE", #w "x" #h); \
|
||||
IupSetAttribute(btn, "FONT", font); \
|
||||
IupSetAttribute(btn, "BGCOLOR", CLR_BTN_NORMAL_BG); \
|
||||
IupSetAttribute(btn, "FGCOLOR", CLR_BTN_NORMAL_FG); \
|
||||
IupSetAttribute(btn, "FLAT", "YES")
|
||||
|
||||
Ihandle *vbox_controls;
|
||||
|
||||
if (gui_game_mode == 2) // 复盘模式
|
||||
{
|
||||
Ihandle *btn_prev = IupButton("上一步", NULL);
|
||||
IupSetCallback(btn_prev, "ACTION", (Icallback)btn_replay_prev_cb);
|
||||
SET_BTN_STYLE(btn_prev, 120, 35, "SimHei, 11");
|
||||
|
||||
Ihandle *btn_next = IupButton("下一步", NULL);
|
||||
IupSetCallback(btn_next, "ACTION", (Icallback)btn_replay_next_cb);
|
||||
SET_BTN_STYLE(btn_next, 120, 35, "SimHei, 11");
|
||||
|
||||
Ihandle *hbox_nav = IupHbox(btn_prev, btn_next, NULL);
|
||||
IupSetAttribute(hbox_nav, "GAP", "8");
|
||||
IupSetAttribute(hbox_nav, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *btn_back = IupButton("返回菜单", NULL);
|
||||
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
|
||||
SET_BTN_STYLE(btn_back, 120, 35, "SimHei, 11");
|
||||
|
||||
vbox_controls = IupVbox(
|
||||
frm_info,
|
||||
hbox_nav,
|
||||
btn_back,
|
||||
NULL);
|
||||
}
|
||||
else // 游戏模式 (PvP / PvE / Network)
|
||||
{
|
||||
Ihandle *btn_undo = IupButton("悔棋", NULL);
|
||||
IupSetCallback(btn_undo, "ACTION", (Icallback)btn_undo_cb);
|
||||
SET_BTN_STYLE(btn_undo, 120, 35, "SimHei, 11");
|
||||
|
||||
Ihandle *btn_save = IupButton("保存棋谱", NULL);
|
||||
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_cb);
|
||||
SET_BTN_STYLE(btn_save, 120, 35, "SimHei, 11");
|
||||
|
||||
Ihandle *btn_back = IupButton("返回菜单", NULL);
|
||||
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
|
||||
SET_BTN_STYLE(btn_back, 120, 35, "SimHei, 11");
|
||||
|
||||
vbox_controls = IupVbox(
|
||||
frm_info,
|
||||
btn_undo,
|
||||
btn_save,
|
||||
btn_back,
|
||||
NULL);
|
||||
}
|
||||
|
||||
#undef SET_BTN_STYLE
|
||||
|
||||
IupSetAttribute(vbox_controls, "GAP", "10");
|
||||
IupSetAttribute(vbox_controls, "MARGIN", "10x10");
|
||||
IupSetAttribute(vbox_controls, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *hbox_main = IupHbox(board_canvas, vbox_controls, NULL);
|
||||
IupSetAttribute(hbox_main, "MARGIN", "10x10");
|
||||
IupSetAttribute(hbox_main, "GAP", "8");
|
||||
|
||||
// 创建Dialog
|
||||
dlg = IupDialog(hbox_main);
|
||||
if (!dlg)
|
||||
printf("ERROR: Failed to create dialog\n");
|
||||
|
||||
IupSetAttribute(dlg, "TITLE", "五子棋");
|
||||
IupSetAttribute(dlg, "RESIZE", "NO");
|
||||
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
|
||||
|
||||
// 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单
|
||||
IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb);
|
||||
}
|
||||
|
||||
void start_pvp_game_gui()
|
||||
{
|
||||
gui_game_mode = 0;
|
||||
empty_board();
|
||||
current_player_gui = PLAYER;
|
||||
game_over = 0;
|
||||
|
||||
create_game_window();
|
||||
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupFlush(); // 确保窗口完全映射
|
||||
sprintf(status_message, "玩家对战模式 - 黑方先行");
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
|
||||
void start_pve_game_gui()
|
||||
{
|
||||
gui_game_mode = 1;
|
||||
empty_board();
|
||||
current_player_gui = PLAYER;
|
||||
game_over = 0;
|
||||
|
||||
create_game_window();
|
||||
|
||||
if (dlg)
|
||||
{
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupFlush();
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sprintf(status_message, "人机对战模式 - 玩家执黑先行");
|
||||
update_ui_labels();
|
||||
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
|
||||
void start_network_game_gui()
|
||||
{
|
||||
gui_game_mode = 3;
|
||||
empty_board();
|
||||
|
||||
current_player_gui = PLAYER1;
|
||||
game_over = 0;
|
||||
|
||||
create_game_window();
|
||||
|
||||
if (dlg)
|
||||
{
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupFlush();
|
||||
}
|
||||
|
||||
if (network_state.is_server)
|
||||
sprintf(status_message, "局域网联机 - 你是主机(黑子),轮到你落子");
|
||||
else
|
||||
sprintf(status_message, "局域网联机 - 你是客机(白子),等待对手落子...");
|
||||
|
||||
update_ui_labels();
|
||||
|
||||
// 强制初始重绘
|
||||
if (board_canvas)
|
||||
{
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
|
||||
// 启动网络轮询定时器
|
||||
timer = IupTimer();
|
||||
IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb);
|
||||
IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次
|
||||
IupSetAttribute(timer, "RUN", "YES");
|
||||
}
|
||||
@@ -1,559 +0,0 @@
|
||||
#include <iup.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "gui_menu.h"
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "globals.h"
|
||||
#include "config.h"
|
||||
#include "network.h"
|
||||
|
||||
Ihandle *menu_dlg = NULL;
|
||||
|
||||
static int btn_pvp_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
start_pvp_game_gui();
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_pve_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
start_pve_game_gui();
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
// --- 网络对战相关回调 ---
|
||||
static int btn_network_host_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
|
||||
int port = IupGetInt(txt_port, "VALUE");
|
||||
if (port <= 0 || port > 65535) port = DEFAULT_NETWORK_PORT;
|
||||
|
||||
if (create_server(port))
|
||||
{
|
||||
IupMessage("成功", "房间创建成功,等待玩家加入...");
|
||||
IupHide(dlg);
|
||||
start_network_game_gui();
|
||||
IupHide(menu_dlg);
|
||||
}
|
||||
else
|
||||
{
|
||||
IupMessage("错误", "创建房间失败,可能是端口被占用");
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_network_join_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
Ihandle *txt_ip = IupGetDialogChild(dlg, "NET_IP");
|
||||
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
|
||||
|
||||
char *ip = IupGetAttribute(txt_ip, "VALUE");
|
||||
int port = IupGetInt(txt_port, "VALUE");
|
||||
if (port <= 0 || port > 65535) port = DEFAULT_NETWORK_PORT;
|
||||
|
||||
if (connect_to_server(ip, port))
|
||||
{
|
||||
IupMessage("成功", "成功加入房间!");
|
||||
IupHide(dlg);
|
||||
start_network_game_gui();
|
||||
IupHide(menu_dlg);
|
||||
}
|
||||
else
|
||||
{
|
||||
IupMessage("错误", "加入房间失败,请检查IP和端口");
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_network_cancel_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
IupHide(dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_network_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
|
||||
Ihandle *lbl_ip = IupLabel("目标 IP:");
|
||||
Ihandle *txt_ip = IupText(NULL);
|
||||
IupSetAttribute(txt_ip, "NAME", "NET_IP");
|
||||
IupSetAttribute(txt_ip, "VALUE", "127.0.0.1");
|
||||
IupSetAttribute(txt_ip, "SIZE", "120x");
|
||||
|
||||
Ihandle *hbox_ip = IupHbox(lbl_ip, txt_ip, NULL);
|
||||
IupSetAttribute(hbox_ip, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_ip, "GAP", "10");
|
||||
|
||||
Ihandle *lbl_port = IupLabel("端口:");
|
||||
Ihandle *txt_port = IupText(NULL);
|
||||
IupSetAttribute(txt_port, "NAME", "NET_PORT");
|
||||
char port_str[16];
|
||||
sprintf(port_str, "%d", DEFAULT_NETWORK_PORT);
|
||||
IupSetAttribute(txt_port, "VALUE", port_str);
|
||||
IupSetAttribute(txt_port, "SIZE", "60x");
|
||||
|
||||
Ihandle *hbox_port = IupHbox(lbl_port, txt_port, NULL);
|
||||
IupSetAttribute(hbox_port, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_port, "GAP", "10");
|
||||
|
||||
Ihandle *vbox_input = IupVbox(hbox_ip, hbox_port, NULL);
|
||||
IupSetAttribute(vbox_input, "GAP", "8");
|
||||
IupSetAttribute(vbox_input, "MARGIN", "10x8");
|
||||
|
||||
Ihandle *frm_input = IupFrame(vbox_input);
|
||||
IupSetAttribute(frm_input, "TITLE", "连接设置");
|
||||
IupSetAttribute(frm_input, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_input, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
Ihandle *btn_host = IupButton("创建房间", NULL);
|
||||
IupSetCallback(btn_host, "ACTION", (Icallback)btn_network_host_cb);
|
||||
IupSetAttribute(btn_host, "SIZE", "100x32");
|
||||
IupSetAttribute(btn_host, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_host, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_host, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_join = IupButton("加入房间", NULL);
|
||||
IupSetCallback(btn_join, "ACTION", (Icallback)btn_network_join_cb);
|
||||
IupSetAttribute(btn_join, "SIZE", "100x32");
|
||||
IupSetAttribute(btn_join, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_join, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_join, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_cancel = IupButton("取消", NULL);
|
||||
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_network_cancel_cb);
|
||||
IupSetAttribute(btn_cancel, "SIZE", "80x32");
|
||||
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_cancel, "FLAT", "YES");
|
||||
|
||||
Ihandle *hbox_btns = IupHbox(btn_host, btn_join, btn_cancel, NULL);
|
||||
IupSetAttribute(hbox_btns, "GAP", "8");
|
||||
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *vbox = IupVbox(frm_input, hbox_btns, NULL);
|
||||
IupSetAttribute(vbox, "MARGIN", "15x15");
|
||||
IupSetAttribute(vbox, "GAP", "12");
|
||||
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *dlg = IupDialog(vbox);
|
||||
IupSetAttribute(dlg, "TITLE", "局域网联机");
|
||||
IupSetAttribute(dlg, "RESIZE", "NO");
|
||||
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
|
||||
|
||||
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupDestroy(dlg);
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
// --- 网络对战结束 ---
|
||||
|
||||
static int btn_replay_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
start_replay_gui();
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_save_settings_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
|
||||
// Get values
|
||||
Ihandle *txt_board_size = IupGetDialogChild(dlg, "BOARD_SIZE");
|
||||
Ihandle *tgl_forbidden = IupGetDialogChild(dlg, "FORBIDDEN");
|
||||
Ihandle *tgl_timer = IupGetDialogChild(dlg, "TIMER");
|
||||
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
|
||||
Ihandle *lst_ai = IupGetDialogChild(dlg, "AI_DIFFICULTY");
|
||||
|
||||
// Update globals
|
||||
int new_size = IupGetInt(txt_board_size, "VALUE");
|
||||
if (new_size < MIN_BOARD_SIZE)
|
||||
new_size = MIN_BOARD_SIZE;
|
||||
if (new_size > MAX_BOARD_SIZE)
|
||||
new_size = MAX_BOARD_SIZE;
|
||||
BOARD_SIZE = new_size;
|
||||
|
||||
use_forbidden_moves = IupGetInt(tgl_forbidden, "VALUE");
|
||||
|
||||
use_timer = IupGetInt(tgl_timer, "VALUE");
|
||||
if (use_timer)
|
||||
{
|
||||
int minutes = IupGetInt(txt_time_limit, "VALUE");
|
||||
if (minutes < 1)
|
||||
minutes = 1;
|
||||
time_limit = minutes * 60;
|
||||
}
|
||||
|
||||
int ai_level = IupGetInt(lst_ai, "VALUE");
|
||||
if (ai_level < 1)
|
||||
ai_level = 1;
|
||||
if (ai_level > 5)
|
||||
ai_level = 5;
|
||||
ai_difficulty = ai_level;
|
||||
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
|
||||
|
||||
// LLM 设置
|
||||
Ihandle *lst_ai_mode = IupGetDialogChild(dlg, "AI_MODE");
|
||||
Ihandle *txt_endpoint = IupGetDialogChild(dlg, "LLM_ENDPOINT");
|
||||
Ihandle *txt_apikey = IupGetDialogChild(dlg, "LLM_API_KEY");
|
||||
Ihandle *txt_model = IupGetDialogChild(dlg, "LLM_MODEL");
|
||||
|
||||
int ai_mode = IupGetInt(lst_ai_mode, "VALUE");
|
||||
llm_use = (ai_mode == 2) ? 1 : 0;
|
||||
|
||||
char *endpoint = IupGetAttribute(txt_endpoint, "VALUE");
|
||||
if (endpoint)
|
||||
{
|
||||
strncpy(llm_endpoint, endpoint, MAX_LLM_ENDPOINT_LEN - 1);
|
||||
llm_endpoint[MAX_LLM_ENDPOINT_LEN - 1] = '\0';
|
||||
}
|
||||
|
||||
char *apikey = IupGetAttribute(txt_apikey, "VALUE");
|
||||
if (apikey)
|
||||
{
|
||||
strncpy(llm_api_key, apikey, MAX_LLM_API_KEY_LEN - 1);
|
||||
llm_api_key[MAX_LLM_API_KEY_LEN - 1] = '\0';
|
||||
}
|
||||
|
||||
char *model = IupGetAttribute(txt_model, "VALUE");
|
||||
if (model)
|
||||
{
|
||||
strncpy(llm_model, model, MAX_LLM_MODEL_LEN - 1);
|
||||
llm_model[MAX_LLM_MODEL_LEN - 1] = '\0';
|
||||
}
|
||||
|
||||
// Save config
|
||||
save_game_config();
|
||||
|
||||
IupHide(dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_cancel_settings_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
IupHide(dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int tgl_timer_cb(Ihandle *ih, int state)
|
||||
{
|
||||
Ihandle *dlg = IupGetDialog(ih);
|
||||
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
|
||||
IupSetAttribute(txt_time_limit, "ACTIVE", state ? "YES" : "NO");
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_settings_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
|
||||
// === 基本设置 ===
|
||||
Ihandle *lbl_board_size = IupLabel("棋盘大小:");
|
||||
Ihandle *txt_board_size = IupText(NULL);
|
||||
IupSetAttribute(txt_board_size, "NAME", "BOARD_SIZE");
|
||||
IupSetAttribute(txt_board_size, "SPIN", "YES");
|
||||
IupSetAttribute(txt_board_size, "SPINMIN", "5");
|
||||
IupSetAttribute(txt_board_size, "SPINMAX", "25");
|
||||
IupSetInt(txt_board_size, "VALUE", BOARD_SIZE);
|
||||
IupSetAttribute(txt_board_size, "SIZE", "50x");
|
||||
|
||||
Ihandle *hbox_board = IupHbox(lbl_board_size, txt_board_size, NULL);
|
||||
IupSetAttribute(hbox_board, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_board, "GAP", "10");
|
||||
|
||||
Ihandle *tgl_forbidden = IupToggle("启用禁手规则", NULL);
|
||||
IupSetAttribute(tgl_forbidden, "NAME", "FORBIDDEN");
|
||||
IupSetInt(tgl_forbidden, "VALUE", use_forbidden_moves);
|
||||
|
||||
Ihandle *tgl_timer = IupToggle("启用计时器", NULL);
|
||||
IupSetAttribute(tgl_timer, "NAME", "TIMER");
|
||||
IupSetInt(tgl_timer, "VALUE", use_timer);
|
||||
IupSetCallback(tgl_timer, "ACTION", (Icallback)tgl_timer_cb);
|
||||
|
||||
Ihandle *lbl_time_limit = IupLabel("时间限制(分钟):");
|
||||
Ihandle *txt_time_limit = IupText(NULL);
|
||||
IupSetAttribute(txt_time_limit, "NAME", "TIME_LIMIT");
|
||||
IupSetAttribute(txt_time_limit, "SPIN", "YES");
|
||||
IupSetAttribute(txt_time_limit, "SPINMIN", "1");
|
||||
IupSetAttribute(txt_time_limit, "SPINMAX", "60");
|
||||
IupSetInt(txt_time_limit, "VALUE", time_limit / 60);
|
||||
IupSetAttribute(txt_time_limit, "ACTIVE", use_timer ? "YES" : "NO");
|
||||
IupSetAttribute(txt_time_limit, "SIZE", "50x");
|
||||
|
||||
Ihandle *hbox_time = IupHbox(lbl_time_limit, txt_time_limit, NULL);
|
||||
IupSetAttribute(hbox_time, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_time, "GAP", "10");
|
||||
|
||||
Ihandle *vbox_basic = IupVbox(hbox_board, tgl_forbidden, tgl_timer, hbox_time, NULL);
|
||||
IupSetAttribute(vbox_basic, "GAP", "8");
|
||||
IupSetAttribute(vbox_basic, "MARGIN", "10x8");
|
||||
|
||||
Ihandle *frm_basic = IupFrame(vbox_basic);
|
||||
IupSetAttribute(frm_basic, "TITLE", "基本设置");
|
||||
IupSetAttribute(frm_basic, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_basic, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === AI 设置 ===
|
||||
Ihandle *lbl_ai = IupLabel("AI 难度:");
|
||||
Ihandle *lst_ai = IupList(NULL);
|
||||
IupSetAttribute(lst_ai, "NAME", "AI_DIFFICULTY");
|
||||
IupSetAttribute(lst_ai, "DROPDOWN", "YES");
|
||||
IupSetAttribute(lst_ai, "1", "1 简单");
|
||||
IupSetAttribute(lst_ai, "2", "2 普通");
|
||||
IupSetAttribute(lst_ai, "3", "3 中等");
|
||||
IupSetAttribute(lst_ai, "4", "4 困难");
|
||||
IupSetAttribute(lst_ai, "5", "5 专家");
|
||||
IupSetInt(lst_ai, "VALUE", ai_difficulty);
|
||||
IupSetAttribute(lst_ai, "SIZE", "80x");
|
||||
|
||||
Ihandle *hbox_ai = IupHbox(lbl_ai, lst_ai, NULL);
|
||||
IupSetAttribute(hbox_ai, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_ai, "GAP", "10");
|
||||
|
||||
Ihandle *lbl_ai_mode = IupLabel("AI模式:");
|
||||
Ihandle *lst_ai_mode = IupList(NULL);
|
||||
IupSetAttribute(lst_ai_mode, "NAME", "AI_MODE");
|
||||
IupSetAttribute(lst_ai_mode, "DROPDOWN", "YES");
|
||||
IupSetAttribute(lst_ai_mode, "1", "算法AI (本地)");
|
||||
IupSetAttribute(lst_ai_mode, "2", "大模型AI (在线)");
|
||||
IupSetInt(lst_ai_mode, "VALUE", llm_use ? 2 : 1);
|
||||
IupSetAttribute(lst_ai_mode, "SIZE", "120x");
|
||||
|
||||
Ihandle *hbox_ai_mode = IupHbox(lbl_ai_mode, lst_ai_mode, NULL);
|
||||
IupSetAttribute(hbox_ai_mode, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_ai_mode, "GAP", "10");
|
||||
|
||||
Ihandle *vbox_ai = IupVbox(hbox_ai, hbox_ai_mode, NULL);
|
||||
IupSetAttribute(vbox_ai, "GAP", "8");
|
||||
IupSetAttribute(vbox_ai, "MARGIN", "10x8");
|
||||
|
||||
Ihandle *frm_ai = IupFrame(vbox_ai);
|
||||
IupSetAttribute(frm_ai, "TITLE", "AI 设置");
|
||||
IupSetAttribute(frm_ai, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_ai, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 大模型设置 ===
|
||||
Ihandle *lbl_endpoint = IupLabel("API地址:");
|
||||
Ihandle *txt_endpoint = IupText(NULL);
|
||||
IupSetAttribute(txt_endpoint, "NAME", "LLM_ENDPOINT");
|
||||
IupSetAttribute(txt_endpoint, "VALUE", llm_endpoint);
|
||||
IupSetAttribute(txt_endpoint, "SIZE", "250x");
|
||||
|
||||
Ihandle *hbox_endpoint = IupHbox(lbl_endpoint, txt_endpoint, NULL);
|
||||
IupSetAttribute(hbox_endpoint, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_endpoint, "GAP", "10");
|
||||
|
||||
Ihandle *lbl_apikey = IupLabel("API Key:");
|
||||
Ihandle *txt_apikey = IupText(NULL);
|
||||
IupSetAttribute(txt_apikey, "NAME", "LLM_API_KEY");
|
||||
IupSetAttribute(txt_apikey, "VALUE", llm_api_key);
|
||||
IupSetAttribute(txt_apikey, "PASSWORD", "YES");
|
||||
IupSetAttribute(txt_apikey, "SIZE", "200x");
|
||||
|
||||
Ihandle *hbox_apikey = IupHbox(lbl_apikey, txt_apikey, NULL);
|
||||
IupSetAttribute(hbox_apikey, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_apikey, "GAP", "10");
|
||||
|
||||
Ihandle *lbl_model = IupLabel("模型名称:");
|
||||
Ihandle *txt_model = IupText(NULL);
|
||||
IupSetAttribute(txt_model, "NAME", "LLM_MODEL");
|
||||
IupSetAttribute(txt_model, "VALUE", llm_model);
|
||||
IupSetAttribute(txt_model, "SIZE", "150x");
|
||||
|
||||
Ihandle *hbox_model = IupHbox(lbl_model, txt_model, NULL);
|
||||
IupSetAttribute(hbox_model, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_model, "GAP", "10");
|
||||
|
||||
Ihandle *vbox_llm = IupVbox(hbox_endpoint, hbox_apikey, hbox_model, NULL);
|
||||
IupSetAttribute(vbox_llm, "GAP", "8");
|
||||
IupSetAttribute(vbox_llm, "MARGIN", "10x8");
|
||||
|
||||
Ihandle *frm_llm = IupFrame(vbox_llm);
|
||||
IupSetAttribute(frm_llm, "TITLE", "大模型设置");
|
||||
IupSetAttribute(frm_llm, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_llm, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 按钮 ===
|
||||
Ihandle *btn_save = IupButton("保存", NULL);
|
||||
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb);
|
||||
IupSetAttribute(btn_save, "SIZE", "80x30");
|
||||
IupSetAttribute(btn_save, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_save, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_save, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_cancel = IupButton("取消", NULL);
|
||||
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_cancel_settings_cb);
|
||||
IupSetAttribute(btn_cancel, "SIZE", "80x30");
|
||||
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_cancel, "FLAT", "YES");
|
||||
|
||||
Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
|
||||
IupSetAttribute(hbox_btns, "GAP", "15");
|
||||
IupSetAttribute(hbox_btns, "MARGIN", "5x0");
|
||||
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
|
||||
|
||||
// === 主布局 ===
|
||||
Ihandle *vbox = IupVbox(frm_basic, frm_ai, frm_llm, hbox_btns, NULL);
|
||||
IupSetAttribute(vbox, "GAP", "10");
|
||||
IupSetAttribute(vbox, "MARGIN", "15x15");
|
||||
|
||||
Ihandle *dlg = IupDialog(vbox);
|
||||
IupSetAttribute(dlg, "TITLE", "游戏设置");
|
||||
IupSetAttribute(dlg, "RESIZE", "NO");
|
||||
IupSetAttribute(dlg, "MINBOX", "NO");
|
||||
IupSetAttribute(dlg, "MAXBOX", "NO");
|
||||
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
|
||||
|
||||
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupDestroy(dlg);
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_exit_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
cleanup_gui(); // 清理GUI资源
|
||||
exit(0);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建主菜单
|
||||
*/
|
||||
void create_main_menu()
|
||||
{
|
||||
if (menu_dlg)
|
||||
return;
|
||||
|
||||
// === 标题区 ===
|
||||
Ihandle *lbl_title = IupLabel("五子棋");
|
||||
IupSetAttribute(lbl_title, "FONT", "SimHei, 32");
|
||||
IupSetAttribute(lbl_title, "FGCOLOR", CLR_TEXT_TITLE);
|
||||
IupSetAttribute(lbl_title, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *lbl_subtitle = IupLabel("Gobang");
|
||||
IupSetAttribute(lbl_subtitle, "FONT", "SimHei, 14");
|
||||
IupSetAttribute(lbl_subtitle, "FGCOLOR", CLR_TEXT_SECONDARY);
|
||||
IupSetAttribute(lbl_subtitle, "ALIGNMENT", "ACENTER");
|
||||
|
||||
// === 游戏模式按钮(主按钮样式:深棕底白字)===
|
||||
Ihandle *btn_pvp = IupButton("玩家对战", NULL);
|
||||
IupSetCallback(btn_pvp, "ACTION", (Icallback)btn_pvp_cb);
|
||||
IupSetAttribute(btn_pvp, "SIZE", "150x38");
|
||||
IupSetAttribute(btn_pvp, "FONT", "SimHei, 13");
|
||||
IupSetAttribute(btn_pvp, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_pvp, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_pvp, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_pve = IupButton("人机对战", NULL);
|
||||
IupSetCallback(btn_pve, "ACTION", (Icallback)btn_pve_cb);
|
||||
IupSetAttribute(btn_pve, "SIZE", "150x38");
|
||||
IupSetAttribute(btn_pve, "FONT", "SimHei, 13");
|
||||
IupSetAttribute(btn_pve, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_pve, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_pve, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_net = IupButton("局域网联机", NULL);
|
||||
IupSetCallback(btn_net, "ACTION", (Icallback)btn_network_cb);
|
||||
IupSetAttribute(btn_net, "SIZE", "150x38");
|
||||
IupSetAttribute(btn_net, "FONT", "SimHei, 13");
|
||||
IupSetAttribute(btn_net, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_net, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_net, "FLAT", "YES");
|
||||
|
||||
Ihandle *vbox_modes = IupVbox(btn_pvp, btn_pve, btn_net, NULL);
|
||||
IupSetAttribute(vbox_modes, "GAP", "8");
|
||||
IupSetAttribute(vbox_modes, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(vbox_modes, "MARGIN", "15x10");
|
||||
|
||||
Ihandle *frm_modes = IupFrame(vbox_modes);
|
||||
IupSetAttribute(frm_modes, "TITLE", "选择模式");
|
||||
IupSetAttribute(frm_modes, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_modes, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 功能按钮(普通按钮样式:浅棕底深棕字)===
|
||||
Ihandle *btn_replay = IupButton("复盘回放", NULL);
|
||||
IupSetCallback(btn_replay, "ACTION", (Icallback)btn_replay_cb);
|
||||
IupSetAttribute(btn_replay, "SIZE", "120x32");
|
||||
IupSetAttribute(btn_replay, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(btn_replay, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_replay, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_replay, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_settings = IupButton("游戏设置", NULL);
|
||||
IupSetCallback(btn_settings, "ACTION", (Icallback)btn_settings_cb);
|
||||
IupSetAttribute(btn_settings, "SIZE", "120x32");
|
||||
IupSetAttribute(btn_settings, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(btn_settings, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_settings, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_settings, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_exit = IupButton("退出游戏", NULL);
|
||||
IupSetCallback(btn_exit, "ACTION", (Icallback)btn_exit_cb);
|
||||
IupSetAttribute(btn_exit, "SIZE", "120x32");
|
||||
IupSetAttribute(btn_exit, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(btn_exit, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_exit, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_exit, "FLAT", "YES");
|
||||
|
||||
Ihandle *hbox_func = IupHbox(btn_replay, btn_settings, btn_exit, NULL);
|
||||
IupSetAttribute(hbox_func, "GAP", "10");
|
||||
IupSetAttribute(hbox_func, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_func, "MARGIN", "10x8");
|
||||
|
||||
Ihandle *frm_func = IupFrame(hbox_func);
|
||||
IupSetAttribute(frm_func, "TITLE", "功能");
|
||||
IupSetAttribute(frm_func, "FONT", "SimHei, 11");
|
||||
IupSetAttribute(frm_func, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
// === 主布局 ===
|
||||
Ihandle *vbox = IupVbox(
|
||||
lbl_title,
|
||||
lbl_subtitle,
|
||||
IupLabel(NULL), // 间隔
|
||||
frm_modes,
|
||||
frm_func,
|
||||
NULL);
|
||||
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(vbox, "GAP", "8");
|
||||
IupSetAttribute(vbox, "MARGIN", "30x25");
|
||||
|
||||
menu_dlg = IupDialog(vbox);
|
||||
IupSetAttribute(menu_dlg, "TITLE", "五子棋 - 主菜单");
|
||||
IupSetAttribute(menu_dlg, "RESIZE", "NO");
|
||||
IupSetAttribute(menu_dlg, "MINBOX", "NO");
|
||||
IupSetAttribute(menu_dlg, "MAXBOX", "NO");
|
||||
IupSetAttribute(menu_dlg, "BGCOLOR", CLR_WINDOW_BG);
|
||||
IupSetAttribute(menu_dlg, "SIZE", "380x520");
|
||||
|
||||
// 设置对话框关闭回调 (点X关闭程序)
|
||||
IupSetCallback(menu_dlg, "CLOSE_CB", (Icallback)btn_exit_cb);
|
||||
IupMap(menu_dlg); // Map immediately
|
||||
}
|
||||
|
||||
void show_main_menu()
|
||||
{
|
||||
if (!menu_dlg)
|
||||
create_main_menu();
|
||||
IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER);
|
||||
}
|
||||
|
||||
void hide_main_menu()
|
||||
{
|
||||
if (menu_dlg)
|
||||
IupHide(menu_dlg);
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "gui_menu.h"
|
||||
#include "globals.h"
|
||||
#include "gobang.h"
|
||||
#include "record.h"
|
||||
#include "config.h"
|
||||
#include <iup.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// 复盘功能函数
|
||||
|
||||
/**
|
||||
* @brief 复盘文件选择对话框的确定回调
|
||||
*/
|
||||
int btn_replay_sel_ok_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg_sel = IupGetDialog(ih);
|
||||
Ihandle *list = (Ihandle *)IupGetAttribute(dlg_sel, "MY_LIST");
|
||||
char *val = IupGetAttribute(list, "VALUE"); // 返回选中项索引
|
||||
if (!val)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
int index = atoi(val);
|
||||
char *selected_file = IupGetAttributeId(list, "", index);
|
||||
|
||||
if (selected_file)
|
||||
{
|
||||
printf("DEBUG: Loading file %s\n", selected_file);
|
||||
if (load_game_from_file(selected_file))
|
||||
{
|
||||
printf("DEBUG: File loaded successfully\n");
|
||||
replay_total_steps = step_count;
|
||||
step_count = 0;
|
||||
gui_game_mode = 2; // 复盘模式
|
||||
|
||||
// 关闭选择对话框
|
||||
IupHide(dlg_sel);
|
||||
|
||||
// 启动游戏窗口
|
||||
create_game_window();
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER); // 显示游戏窗口
|
||||
|
||||
sprintf(status_message, "复盘模式 - %s", selected_file);
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
IupMessage("错误", "无法加载复盘文件");
|
||||
}
|
||||
}
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 复盘文件选择对话框的取消回调
|
||||
*/
|
||||
int btn_replay_sel_cancel_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg_sel = IupGetDialog(ih);
|
||||
IupHide(dlg_sel);
|
||||
IupDestroy(dlg_sel);
|
||||
show_main_menu(); // 返回主菜单
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 上一步按钮回调
|
||||
*/
|
||||
int btn_replay_prev_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (step_count > 0)
|
||||
{
|
||||
step_count--;
|
||||
Step s = steps[step_count];
|
||||
board[s.x][s.y] = EMPTY;
|
||||
sprintf(status_message, "回退一步");
|
||||
update_ui_labels();
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 下一步按钮回调
|
||||
*/
|
||||
int btn_replay_next_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (step_count < replay_total_steps && step_count >= 0) // 确保步数有效
|
||||
{
|
||||
Step s = steps[step_count];
|
||||
board[s.x][s.y] = s.player;
|
||||
step_count++;
|
||||
sprintf(status_message, "前进一步");
|
||||
update_ui_labels();
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 选择复盘文件界面
|
||||
*/
|
||||
void select_replay_file_gui()
|
||||
{
|
||||
printf("DEBUG: select_replay_file_gui start\n");
|
||||
// 列出 records/ 目录下的文件
|
||||
char record_files[100][100];
|
||||
int file_count = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
WIN32_FIND_DATA ffd;
|
||||
HANDLE hFind = FindFirstFile("records\\*", &ffd);
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
||||
{
|
||||
// 确保不溢出缓冲区
|
||||
if (file_count < 100)
|
||||
{
|
||||
strncpy(record_files[file_count], ffd.cFileName, 99);
|
||||
record_files[file_count][99] = '\0';
|
||||
file_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (FindNextFile(hFind, &ffd) != 0);
|
||||
FindClose(hFind);
|
||||
}
|
||||
#endif
|
||||
printf("DEBUG: Found %d files\n", file_count);
|
||||
|
||||
if (file_count == 0)
|
||||
{
|
||||
IupMessage("提示", "未找到复盘记录文件 (records/*)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建列表框
|
||||
Ihandle *list = IupList(NULL);
|
||||
if (!list)
|
||||
{
|
||||
printf("ERROR: Failed to create list\n");
|
||||
return;
|
||||
}
|
||||
IupSetAttribute(list, "EXPAND", "YES");
|
||||
IupSetAttribute(list, "VISIBLELINES", "10");
|
||||
|
||||
for (int i = 0; i < file_count; i++)
|
||||
{
|
||||
IupSetAttributeId(list, "", i + 1, record_files[i]);
|
||||
}
|
||||
IupSetAttribute(list, "VALUE", "1"); // 默认选择第一项
|
||||
|
||||
// 创建确定和取消按钮
|
||||
Ihandle *btn_ok = IupButton("确定", NULL);
|
||||
IupSetCallback(btn_ok, "ACTION", (Icallback)btn_replay_sel_ok_cb);
|
||||
IupSetAttribute(btn_ok, "SIZE", "80x32");
|
||||
IupSetAttribute(btn_ok, "BGCOLOR", CLR_BTN_PRIMARY_BG);
|
||||
IupSetAttribute(btn_ok, "FGCOLOR", CLR_BTN_PRIMARY_FG);
|
||||
IupSetAttribute(btn_ok, "FLAT", "YES");
|
||||
|
||||
Ihandle *btn_cancel = IupButton("取消", NULL);
|
||||
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_replay_sel_cancel_cb);
|
||||
IupSetAttribute(btn_cancel, "SIZE", "80x32");
|
||||
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
|
||||
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
|
||||
IupSetAttribute(btn_cancel, "FLAT", "YES");
|
||||
|
||||
Ihandle *lbl_prompt = IupLabel("选择复盘文件:");
|
||||
IupSetAttribute(lbl_prompt, "FGCOLOR", CLR_TEXT_NORMAL);
|
||||
|
||||
Ihandle *hbox_btns = IupHbox(btn_ok, btn_cancel, NULL);
|
||||
IupSetAttribute(hbox_btns, "GAP", "10");
|
||||
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *vbox = IupVbox(lbl_prompt, list, hbox_btns, NULL);
|
||||
IupSetAttribute(vbox, "GAP", "10");
|
||||
IupSetAttribute(vbox, "MARGIN", "15x15");
|
||||
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *dlg_sel = IupDialog(vbox);
|
||||
if (!dlg_sel)
|
||||
{
|
||||
printf("ERROR: Failed to create selection dialog\n");
|
||||
return;
|
||||
}
|
||||
IupSetAttribute(dlg_sel, "TITLE", "复盘回放");
|
||||
IupSetAttribute(dlg_sel, "MINBOX", "NO");
|
||||
IupSetAttribute(dlg_sel, "MAXBOX", "NO");
|
||||
IupSetAttribute(dlg_sel, "RESIZE", "NO");
|
||||
IupSetAttribute(dlg_sel, "BGCOLOR", CLR_WINDOW_BG);
|
||||
|
||||
// 存储列表框句柄到对话框属性
|
||||
IupSetAttribute(dlg_sel, "MY_LIST", (char *)list);
|
||||
|
||||
// 显示对话框 (模态)
|
||||
IupPopup(dlg_sel, IUP_CENTER, IUP_CENTER);
|
||||
// 对话框关闭后销毁
|
||||
IupDestroy(dlg_sel);
|
||||
printf("DEBUG: select_replay_file_gui end\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 启动复盘GUI流程
|
||||
*/
|
||||
void start_replay_gui()
|
||||
{
|
||||
select_replay_file_gui();
|
||||
// 不要在这里隐藏主菜单,等待文件选择完成
|
||||
}
|
||||
@@ -1,616 +0,0 @@
|
||||
/**
|
||||
* @file llm_ai.c
|
||||
* @brief 大模型AI模块实现
|
||||
* @note 通过OpenAI兼容API调用大模型进行五子棋对弈
|
||||
* 支持 MiniMax、DeepSeek、GPT 等兼容接口
|
||||
*/
|
||||
|
||||
#include "llm_ai.h"
|
||||
#include "globals.h"
|
||||
#include "config.h"
|
||||
#include "gobang.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#include <process.h>
|
||||
#endif
|
||||
|
||||
// ==================== 内部函数声明 ====================
|
||||
|
||||
static bool http_post_json(const char *url, const char *api_key,
|
||||
const char *json_body, char *response, int response_size);
|
||||
static char *build_prompt(void);
|
||||
static char *build_request_json(const char *prompt);
|
||||
static bool parse_response(const char *response, int *out_x, int *out_y);
|
||||
static bool extract_coords(const char *text, int *out_x, int *out_y);
|
||||
|
||||
// ==================== 异步请求支持 ====================
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
typedef struct {
|
||||
int result; // 0=思考中, 1=成功, -1=失败
|
||||
int x, y; // 成功时的坐标
|
||||
HANDLE thread; // 后台线程句柄
|
||||
} LLMAsyncResult;
|
||||
|
||||
static LLMAsyncResult g_llm_async = {0, 0, 0, NULL};
|
||||
|
||||
static unsigned __stdcall llm_thread_func(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int x = -1, y = -1;
|
||||
bool ok = llm_ai_move(&x, &y);
|
||||
g_llm_async.x = x;
|
||||
g_llm_async.y = y;
|
||||
g_llm_async.result = ok ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void llm_ai_start_move(void)
|
||||
{
|
||||
// 等待上一次线程结束(如果有)
|
||||
if (g_llm_async.thread)
|
||||
{
|
||||
WaitForSingleObject(g_llm_async.thread, INFINITE);
|
||||
CloseHandle(g_llm_async.thread);
|
||||
g_llm_async.thread = NULL;
|
||||
}
|
||||
|
||||
g_llm_async.result = 0;
|
||||
g_llm_async.x = -1;
|
||||
g_llm_async.y = -1;
|
||||
|
||||
g_llm_async.thread = (HANDLE)_beginthreadex(NULL, 0, llm_thread_func, NULL, 0, NULL);
|
||||
}
|
||||
|
||||
int llm_ai_poll_result(int *out_x, int *out_y)
|
||||
{
|
||||
if (g_llm_async.thread == NULL)
|
||||
return -1;
|
||||
|
||||
// 检查线程是否完成
|
||||
DWORD wait = WaitForSingleObject(g_llm_async.thread, 0);
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
// 线程已完成
|
||||
CloseHandle(g_llm_async.thread);
|
||||
g_llm_async.thread = NULL;
|
||||
*out_x = g_llm_async.x;
|
||||
*out_y = g_llm_async.y;
|
||||
return g_llm_async.result;
|
||||
}
|
||||
|
||||
return 0; // 仍在思考
|
||||
}
|
||||
|
||||
#else
|
||||
// 非Windows平台的同步回退
|
||||
void llm_ai_start_move(void) {}
|
||||
|
||||
int llm_ai_poll_result(int *out_x, int *out_y)
|
||||
{
|
||||
(void)out_x;
|
||||
(void)out_y;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ==================== 公共接口 ====================
|
||||
|
||||
bool llm_ai_move(int *out_x, int *out_y)
|
||||
{
|
||||
if (llm_api_key[0] == '\0')
|
||||
{
|
||||
printf("[LLM] 错误:未配置API Key\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int retry = 0; retry < LLM_MAX_RETRIES; retry++)
|
||||
{
|
||||
// 1. 构造 prompt
|
||||
char *prompt = build_prompt();
|
||||
if (!prompt)
|
||||
return false;
|
||||
|
||||
// 2. 构造 JSON 请求体
|
||||
char *json_body = build_request_json(prompt);
|
||||
free(prompt);
|
||||
if (!json_body)
|
||||
return false;
|
||||
|
||||
// 3. 发送 HTTP 请求
|
||||
char response[8192] = {0};
|
||||
bool ok = http_post_json(llm_endpoint, llm_api_key, json_body, response, sizeof(response));
|
||||
free(json_body);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
printf("[LLM] HTTP请求失败 (第%d次)\n", retry + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 解析响应
|
||||
if (parse_response(response, out_x, out_y))
|
||||
{
|
||||
// 5. 验证坐标合法性
|
||||
if (*out_x >= 0 && *out_x < BOARD_SIZE &&
|
||||
*out_y >= 0 && *out_y < BOARD_SIZE &&
|
||||
board[*out_x][*out_y] == EMPTY)
|
||||
{
|
||||
printf("[LLM] 落子(%d, %d)\n", *out_x, *out_y);
|
||||
return true;
|
||||
}
|
||||
printf("[LLM] 坐标(%d, %d)非法,重试 (%d/%d)\n", *out_x, *out_y, retry + 1, LLM_MAX_RETRIES);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("[LLM] 解析响应失败,重试 (%d/%d)\n", retry + 1, LLM_MAX_RETRIES);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== Prompt 构造 ====================
|
||||
|
||||
// 检查坐标是否在棋盘范围内
|
||||
static bool in_board(int x, int y)
|
||||
{
|
||||
return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE;
|
||||
}
|
||||
|
||||
static char *build_prompt(void)
|
||||
{
|
||||
// 估算所需空间
|
||||
int max_len = 3072 + step_count * 20 + 512;
|
||||
char *buf = (char *)malloc(max_len);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
|
||||
int pos = 0;
|
||||
|
||||
// 棋盘基本信息(不用()格式,避免被坐标提取误匹配)
|
||||
pos += snprintf(buf + pos, max_len - pos,
|
||||
"棋盘 %d×%d,坐标范围 0-%d\n"
|
||||
"你=白O,对手=黑X\n\n",
|
||||
BOARD_SIZE, BOARD_SIZE, BOARD_SIZE - 1);
|
||||
|
||||
// 黑子位置(用方括号格式,不用圆括号)
|
||||
pos += snprintf(buf + pos, max_len - pos, "黑子X位置:");
|
||||
int black_count = 0;
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
if (board[i][j] == PLAYER || board[i][j] == PLAYER1)
|
||||
{
|
||||
pos += snprintf(buf + pos, max_len - pos, " [%d,%d]", i, j);
|
||||
black_count++;
|
||||
}
|
||||
if (black_count == 0)
|
||||
pos += snprintf(buf + pos, max_len - pos, " 无");
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n");
|
||||
|
||||
// 白子位置
|
||||
pos += snprintf(buf + pos, max_len - pos, "白子O位置:");
|
||||
int white_count = 0;
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
if (board[i][j] == AI || board[i][j] == PLAYER2)
|
||||
{
|
||||
pos += snprintf(buf + pos, max_len - pos, " [%d,%d]", i, j);
|
||||
white_count++;
|
||||
}
|
||||
if (white_count == 0)
|
||||
pos += snprintf(buf + pos, max_len - pos, " 无");
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n");
|
||||
|
||||
// 最近走法
|
||||
if (step_count > 0)
|
||||
{
|
||||
int show_count = step_count < 6 ? step_count : 6;
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n最近%d步:\n", show_count);
|
||||
for (int i = step_count - show_count; i < step_count; i++)
|
||||
{
|
||||
const char *who = (steps[i].player == PLAYER || steps[i].player == PLAYER1) ? "X" : "O";
|
||||
pos += snprintf(buf + pos, max_len - pos, " %s [%d,%d]\n", who, steps[i].x, steps[i].y);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集候选空位(已有棋子周围2格内的空位)
|
||||
char candidate[BOARD_SIZE][BOARD_SIZE];
|
||||
memset(candidate, 0, sizeof(candidate));
|
||||
|
||||
int total_stones = black_count + white_count;
|
||||
if (total_stones == 0)
|
||||
{
|
||||
candidate[BOARD_SIZE / 2][BOARD_SIZE / 2] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] != EMPTY)
|
||||
{
|
||||
// 标记周围2格内的空位
|
||||
for (int di = -2; di <= 2; di++)
|
||||
{
|
||||
for (int dj = -2; dj <= 2; dj++)
|
||||
{
|
||||
int ni = i + di, nj = j + dj;
|
||||
if (in_board(ni, nj) && board[ni][nj] == EMPTY)
|
||||
candidate[ni][nj] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 输出候选位置(用[]格式,避免与LLM回复的()格式冲突)
|
||||
int cand_count = 0;
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n可选空位:\n");
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (candidate[i][j])
|
||||
{
|
||||
pos += snprintf(buf + pos, max_len - pos, "[%d,%d] ", i, j);
|
||||
cand_count++;
|
||||
if (cand_count % 10 == 0)
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += snprintf(buf + pos, max_len - pos, "\n共%d个可选。\n", cand_count);
|
||||
|
||||
// 输出格式要求
|
||||
pos += snprintf(buf + pos, max_len - pos,
|
||||
"\n从上面选一个最佳位置,用(行,列)格式回复。只回复坐标。\n");
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ==================== JSON 构造 ====================
|
||||
|
||||
static char *build_request_json(const char *prompt)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root)
|
||||
return NULL;
|
||||
|
||||
cJSON_AddStringToObject(root, "model", llm_model);
|
||||
|
||||
// messages 数组
|
||||
cJSON *messages = cJSON_AddArrayToObject(root, "messages");
|
||||
|
||||
// system 消息
|
||||
cJSON *sys_msg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(sys_msg, "role", "system");
|
||||
cJSON_AddStringToObject(sys_msg, "content",
|
||||
"你是五子棋AI。从给定的可选空位列表中选一个最佳位置落子。"
|
||||
"只回复(行,列)格式的坐标,不要任何解释。");
|
||||
cJSON_AddItemToArray(messages, sys_msg);
|
||||
|
||||
// user 消息(包含棋盘状态)
|
||||
cJSON *user_msg = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(user_msg, "role", "user");
|
||||
cJSON_AddStringToObject(user_msg, "content", prompt);
|
||||
cJSON_AddItemToArray(messages, user_msg);
|
||||
|
||||
// 其他参数
|
||||
// 推理模型需要更多token(思考+输出),非推理模型够用即可
|
||||
cJSON_AddNumberToObject(root, "temperature", 0.1);
|
||||
cJSON_AddNumberToObject(root, "max_tokens", 512);
|
||||
|
||||
char *json = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return json;
|
||||
}
|
||||
|
||||
// ==================== HTTP 请求(WinHTTP)====================
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static bool http_post_json(const char *url, const char *api_key,
|
||||
const char *json_body, char *response, int response_size)
|
||||
{
|
||||
bool success = false;
|
||||
HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;
|
||||
|
||||
// 解析 URL:提取 host、path、端口、是否 HTTPS
|
||||
WCHAR whost[256] = {0};
|
||||
WCHAR wpath[512] = {0};
|
||||
wchar_t wauth[256] = {0};
|
||||
INTERNET_PORT port = INTERNET_DEFAULT_HTTPS_PORT;
|
||||
BOOL is_https = TRUE;
|
||||
|
||||
// 简单解析 URL
|
||||
const char *host_start = url;
|
||||
const char *path_start = "/";
|
||||
int host_len = 0;
|
||||
|
||||
if (strncmp(url, "https://", 8) == 0)
|
||||
{
|
||||
host_start = url + 8;
|
||||
is_https = TRUE;
|
||||
port = INTERNET_DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
else if (strncmp(url, "http://", 7) == 0)
|
||||
{
|
||||
host_start = url + 7;
|
||||
is_https = FALSE;
|
||||
port = INTERNET_DEFAULT_HTTP_PORT;
|
||||
}
|
||||
|
||||
// 找到 path 的起始位置
|
||||
const char *p = strchr(host_start, '/');
|
||||
if (p)
|
||||
{
|
||||
host_len = (int)(p - host_start);
|
||||
path_start = p;
|
||||
}
|
||||
else
|
||||
{
|
||||
host_len = (int)strlen(host_start);
|
||||
path_start = "/";
|
||||
}
|
||||
|
||||
// 检查是否有端口号
|
||||
const char *colon = strchr(host_start, ':');
|
||||
if (colon && colon < host_start + host_len)
|
||||
{
|
||||
host_len = (int)(colon - host_start);
|
||||
port = (INTERNET_PORT)atoi(colon + 1);
|
||||
}
|
||||
|
||||
// 转换为宽字符
|
||||
MultiByteToWideChar(CP_UTF8, 0, host_start, host_len, whost, 256);
|
||||
MultiByteToWideChar(CP_UTF8, 0, path_start, -1, wpath, 512);
|
||||
|
||||
// 构造 Authorization header
|
||||
wchar_t wapi_key[128] = {0};
|
||||
MultiByteToWideChar(CP_UTF8, 0, api_key, -1, wapi_key, 128);
|
||||
swprintf(wauth, 256, L"Authorization: Bearer %s", wapi_key);
|
||||
|
||||
// 打开 WinHTTP 会话
|
||||
hSession = WinHttpOpen(L"Gobang/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (!hSession)
|
||||
goto cleanup;
|
||||
|
||||
// 设置超时
|
||||
WinHttpSetTimeouts(hSession, 5000, 10000, 15000, LLM_TIMEOUT_MS);
|
||||
|
||||
// 连接服务器
|
||||
hConnect = WinHttpConnect(hSession, whost, port, 0);
|
||||
if (!hConnect)
|
||||
goto cleanup;
|
||||
|
||||
// 创建请求
|
||||
hRequest = WinHttpOpenRequest(hConnect, L"POST", wpath,
|
||||
NULL, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
is_https ? WINHTTP_FLAG_SECURE : 0);
|
||||
if (!hRequest)
|
||||
goto cleanup;
|
||||
|
||||
// 添加请求头
|
||||
WinHttpAddRequestHeaders(hRequest, wauth, -1L, WINHTTP_ADDREQ_FLAG_ADD);
|
||||
WinHttpAddRequestHeaders(hRequest, L"Content-Type: application/json", -1L, WINHTTP_ADDREQ_FLAG_ADD);
|
||||
|
||||
// 发送请求
|
||||
int body_len = (int)strlen(json_body);
|
||||
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
(LPVOID)json_body, body_len, body_len, 0))
|
||||
goto cleanup;
|
||||
|
||||
// 接收响应
|
||||
if (!WinHttpReceiveResponse(hRequest, NULL))
|
||||
goto cleanup;
|
||||
|
||||
// 读取响应体
|
||||
{
|
||||
int total_read = 0;
|
||||
DWORD bytes_available = 0;
|
||||
DWORD bytes_read = 0;
|
||||
|
||||
while (WinHttpQueryDataAvailable(hRequest, &bytes_available) && bytes_available > 0)
|
||||
{
|
||||
if (total_read + (int)bytes_available >= response_size - 1)
|
||||
break;
|
||||
|
||||
WinHttpReadData(hRequest, response + total_read, bytes_available, &bytes_read);
|
||||
total_read += bytes_read;
|
||||
bytes_available = 0;
|
||||
}
|
||||
response[total_read] = '\0';
|
||||
success = (total_read > 0);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (hRequest)
|
||||
WinHttpCloseHandle(hRequest);
|
||||
if (hConnect)
|
||||
WinHttpCloseHandle(hConnect);
|
||||
if (hSession)
|
||||
WinHttpCloseHandle(hSession);
|
||||
return success;
|
||||
}
|
||||
|
||||
#else
|
||||
// 非 Windows 平台的空实现
|
||||
static bool http_post_json(const char *url, const char *api_key,
|
||||
const char *json_body, char *response, int response_size)
|
||||
{
|
||||
(void)url;
|
||||
(void)api_key;
|
||||
(void)json_body;
|
||||
(void)response;
|
||||
(void)response_size;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ==================== 响应解析 ====================
|
||||
|
||||
static bool parse_response(const char *response, int *out_x, int *out_y)
|
||||
{
|
||||
cJSON *root = cJSON_Parse(response);
|
||||
if (!root)
|
||||
{
|
||||
printf("[LLM] JSON解析失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// OpenAI 格式:choices[0].message.content
|
||||
cJSON *choices = cJSON_GetObjectItem(root, "choices");
|
||||
if (!choices || !cJSON_IsArray(choices))
|
||||
{
|
||||
// 兼容某些API的错误格式
|
||||
cJSON *error = cJSON_GetObjectItem(root, "error");
|
||||
if (error)
|
||||
{
|
||||
cJSON *msg = cJSON_GetObjectItem(error, "message");
|
||||
if (msg && cJSON_IsString(msg))
|
||||
printf("[LLM] API错误: %s\n", msg->valuestring);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *first = cJSON_GetArrayItem(choices, 0);
|
||||
if (!first)
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取 content
|
||||
cJSON *message = cJSON_GetObjectItem(first, "message");
|
||||
cJSON *content = NULL;
|
||||
if (message)
|
||||
content = cJSON_GetObjectItem(message, "content");
|
||||
else
|
||||
content = cJSON_GetObjectItem(first, "text"); // 某些API直接返回text
|
||||
|
||||
if (!content || !cJSON_IsString(content))
|
||||
{
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("[LLM] 模型回复: %s\n", content->valuestring);
|
||||
bool ok = extract_coords(content->valuestring, out_x, out_y);
|
||||
cJSON_Delete(root);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ==================== 坐标提取 ====================
|
||||
|
||||
// 跳过所有 <think>...</think> 块,返回处理后的文本(调用者需 free)
|
||||
static char *strip_think_tags(const char *text)
|
||||
{
|
||||
int len = (int)strlen(text);
|
||||
char *buf = (char *)malloc(len + 1);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
|
||||
int out = 0;
|
||||
const char *p = text;
|
||||
|
||||
while (*p)
|
||||
{
|
||||
const char *think_start = strstr(p, "<think>");
|
||||
if (think_start == p)
|
||||
{
|
||||
// 找到 <think> 标签,跳到 </think> 之后
|
||||
const char *think_end = strstr(p, "</think>");
|
||||
if (think_end)
|
||||
{
|
||||
p = think_end + 8; // strlen("</think>")
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有闭合的 <think>,跳过剩余内容
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制 <think> 之前的普通文本
|
||||
int copy_len = think_start ? (int)(think_start - p) : (int)strlen(p);
|
||||
memcpy(buf + out, p, copy_len);
|
||||
out += copy_len;
|
||||
p += copy_len;
|
||||
}
|
||||
|
||||
buf[out] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
// 在文本中查找最后一个 (行,列) 坐标
|
||||
static bool find_last_coord(const char *text, int *out_x, int *out_y)
|
||||
{
|
||||
int last_x = -1, last_y = -1;
|
||||
bool found = false;
|
||||
const char *p = text;
|
||||
|
||||
while (*p)
|
||||
{
|
||||
if (*p == '(')
|
||||
{
|
||||
int x = -1, y = -1;
|
||||
if (sscanf(p, "(%d,%d)", &x, &y) == 2 ||
|
||||
sscanf(p, "(%d, %d)", &x, &y) == 2)
|
||||
{
|
||||
if (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE)
|
||||
{
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
*out_x = last_x;
|
||||
*out_y = last_y;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool extract_coords(const char *text, int *out_x, int *out_y)
|
||||
{
|
||||
// 第一步:去掉 <think>...</think>,从回复正文中提取
|
||||
char *clean = strip_think_tags(text);
|
||||
if (clean)
|
||||
{
|
||||
// 跳过空白字符
|
||||
const char *p = clean;
|
||||
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
|
||||
p++;
|
||||
|
||||
if (*p && find_last_coord(p, out_x, out_y))
|
||||
{
|
||||
free(clean);
|
||||
return true;
|
||||
}
|
||||
free(clean);
|
||||
}
|
||||
|
||||
// 第二步(兜底):从完整文本(含推理)中提取最后一个坐标
|
||||
// 推理模型可能把最终答案写在 <think> 标签里
|
||||
return find_last_coord(text, out_x, out_y);
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
/**
|
||||
* @file network.c
|
||||
* @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com)
|
||||
* @brief 五子棋网络对战模块实现
|
||||
*/
|
||||
|
||||
#include "network.h"
|
||||
#include "gobang.h"
|
||||
#include "config.h"
|
||||
#include "globals.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <enet/enet.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化网络模块
|
||||
*/
|
||||
static bool enet_initialized = false;
|
||||
|
||||
bool init_network()
|
||||
{
|
||||
if (!enet_initialized)
|
||||
{
|
||||
if (enet_initialize() != 0)
|
||||
{
|
||||
printf("An error occurred while initializing ENet.\n");
|
||||
return false;
|
||||
}
|
||||
enet_initialized = true;
|
||||
}
|
||||
|
||||
memset(&network_state, 0, sizeof(NetworkGameState));
|
||||
network_state.port = DEFAULT_NETWORK_PORT;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清理网络模块
|
||||
*/
|
||||
void cleanup_network()
|
||||
{
|
||||
disconnect_network();
|
||||
|
||||
if (network_state.host != NULL)
|
||||
{
|
||||
enet_host_destroy((ENetHost *)network_state.host);
|
||||
network_state.host = NULL;
|
||||
}
|
||||
|
||||
if (enet_initialized)
|
||||
{
|
||||
enet_deinitialize();
|
||||
enet_initialized = false;
|
||||
}
|
||||
|
||||
memset(&network_state, 0, sizeof(NetworkGameState));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建服务器(主机模式)
|
||||
*/
|
||||
bool create_server(int port)
|
||||
{
|
||||
if (!init_network())
|
||||
return false;
|
||||
|
||||
ENetAddress address;
|
||||
|
||||
// 绑定所有接口
|
||||
address.host = ENET_HOST_ANY;
|
||||
address.port = port;
|
||||
|
||||
// 创建服务器主机
|
||||
network_state.host = (void *)enet_host_create(&address,
|
||||
1, // 仅允许1个客户端连接
|
||||
2, // 允许2个通道 (0 和 1)
|
||||
0, // 假设传入带宽无限制
|
||||
0 // 假设传出带宽无限制
|
||||
);
|
||||
|
||||
if (network_state.host == NULL)
|
||||
{
|
||||
printf("创建服务器失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
char local_ip[MAX_IP_LENGTH];
|
||||
if (get_local_ip(local_ip, sizeof(local_ip)))
|
||||
{
|
||||
printf("服务器已启动,等待客户端连接...\n");
|
||||
printf("本机IP地址: %s\n", local_ip);
|
||||
printf("监听端口: %d\n", port);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("服务器已启动,监听端口: %d\n", port);
|
||||
}
|
||||
|
||||
// 阻塞等待客户端连接
|
||||
ENetEvent event;
|
||||
printf("等待连接...\n");
|
||||
// 等待长一点的时间,比如60秒,或者在真实应用中应该放在循环里非阻塞检查
|
||||
if (enet_host_service((ENetHost *)network_state.host, &event, 60000) > 0 &&
|
||||
event.type == ENET_EVENT_TYPE_CONNECT)
|
||||
{
|
||||
network_state.peer = (void *)event.peer;
|
||||
network_state.is_server = true;
|
||||
network_state.is_connected = true;
|
||||
network_state.local_player_id = PLAYER1;
|
||||
network_state.remote_player_id = PLAYER2;
|
||||
network_state.port = port;
|
||||
|
||||
enet_address_get_host_ip(&event.peer->address, network_state.remote_ip, sizeof(network_state.remote_ip));
|
||||
|
||||
printf("客户端已连接: %s\n", network_state.remote_ip);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 超时或失败
|
||||
printf("等待连接超时或失败\n");
|
||||
enet_host_destroy((ENetHost *)network_state.host);
|
||||
network_state.host = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 连接到服务器(客户端模式)
|
||||
*/
|
||||
bool connect_to_server(const char *ip, int port)
|
||||
{
|
||||
if (!init_network())
|
||||
return false;
|
||||
|
||||
// 创建客户端主机
|
||||
network_state.host = (void *)enet_host_create(NULL, // 创建客户端
|
||||
1, // 仅允许1个传出连接
|
||||
2, // 允许2个通道 (0 和 1)
|
||||
0, // 假设传入带宽无限制
|
||||
0 // 假设传出带宽无限制
|
||||
);
|
||||
|
||||
if (network_state.host == NULL)
|
||||
{
|
||||
printf("创建客户端主机失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ENetAddress address;
|
||||
ENetEvent event;
|
||||
ENetPeer *peer;
|
||||
|
||||
enet_address_set_host(&address, ip);
|
||||
address.port = port;
|
||||
|
||||
printf("正在连接到服务器 %s:%d...\n", ip, port);
|
||||
|
||||
peer = enet_host_connect((ENetHost *)network_state.host, &address, 2, 0);
|
||||
if (peer == NULL)
|
||||
{
|
||||
printf("没有可用的对等端来启动连接\n");
|
||||
enet_host_destroy((ENetHost *)network_state.host);
|
||||
network_state.host = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等待连接成功
|
||||
if (enet_host_service((ENetHost *)network_state.host, &event, 5000) > 0 &&
|
||||
event.type == ENET_EVENT_TYPE_CONNECT)
|
||||
{
|
||||
network_state.peer = (void *)peer;
|
||||
network_state.is_server = false;
|
||||
network_state.is_connected = true;
|
||||
network_state.local_player_id = PLAYER2;
|
||||
network_state.remote_player_id = PLAYER1;
|
||||
network_state.port = port;
|
||||
strncpy(network_state.remote_ip, ip, sizeof(network_state.remote_ip) - 1);
|
||||
|
||||
printf("成功连接到服务器\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 连接失败
|
||||
printf("连接服务器失败\n");
|
||||
enet_peer_reset(peer);
|
||||
enet_host_destroy((ENetHost *)network_state.host);
|
||||
network_state.host = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送网络消息
|
||||
*/
|
||||
bool send_network_message(const NetworkMessage *msg)
|
||||
{
|
||||
if (!network_state.is_connected || network_state.peer == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ENetPacket *packet = enet_packet_create(msg, sizeof(NetworkMessage), ENET_PACKET_FLAG_RELIABLE);
|
||||
if (enet_peer_send((ENetPeer *)network_state.peer, 0, packet) < 0)
|
||||
{
|
||||
enet_packet_destroy(packet); // 发送失败需手动销毁
|
||||
return false;
|
||||
}
|
||||
|
||||
// 强制发送
|
||||
enet_host_flush((ENetHost *)network_state.host);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 接收网络消息
|
||||
*/
|
||||
bool receive_network_message(NetworkMessage *msg, int timeout_ms)
|
||||
{
|
||||
if (!network_state.is_connected || network_state.host == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ENetEvent event;
|
||||
int serviceResult = enet_host_service((ENetHost *)network_state.host, &event, timeout_ms);
|
||||
|
||||
if (serviceResult > 0)
|
||||
{
|
||||
switch (event.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
if (event.packet->dataLength == sizeof(NetworkMessage))
|
||||
{
|
||||
memcpy(msg, event.packet->data, sizeof(NetworkMessage));
|
||||
enet_packet_destroy(event.packet);
|
||||
return true;
|
||||
}
|
||||
enet_packet_destroy(event.packet);
|
||||
break;
|
||||
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
network_state.is_connected = false;
|
||||
printf("对方已断开连接\n");
|
||||
network_state.peer = NULL;
|
||||
break;
|
||||
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (serviceResult < 0)
|
||||
{
|
||||
network_state.is_connected = false;
|
||||
printf("网络接收错误\n");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 断开网络连接
|
||||
*/
|
||||
void disconnect_network()
|
||||
{
|
||||
if (network_state.is_connected && network_state.peer != NULL)
|
||||
{
|
||||
ENetEvent event;
|
||||
|
||||
enet_peer_disconnect((ENetPeer *)network_state.peer, 0);
|
||||
|
||||
// 等待断开确认
|
||||
while (enet_host_service((ENetHost *)network_state.host, &event, 3000) > 0)
|
||||
{
|
||||
switch (event.type)
|
||||
{
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
enet_packet_destroy(event.packet);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
puts("断开连接成功");
|
||||
goto DONE;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 超时强制重置
|
||||
enet_peer_reset((ENetPeer *)network_state.peer);
|
||||
DONE:
|
||||
network_state.is_connected = false;
|
||||
network_state.peer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查网络连接状态
|
||||
*/
|
||||
bool is_network_connected()
|
||||
{
|
||||
return network_state.is_connected && network_state.peer != NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取本机局域网IP地址
|
||||
*/
|
||||
bool get_local_ip(char *ip_buffer, int buffer_size)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// 使用 Winsock 获取本机 IP(ws2_32 已链接)
|
||||
char hostname[256];
|
||||
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR)
|
||||
{
|
||||
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
struct hostent *host = gethostbyname(hostname);
|
||||
if (host == NULL || host->h_addr_list[0] == NULL)
|
||||
{
|
||||
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 遍历所有地址,找一个非回环的 IPv4 地址
|
||||
for (int i = 0; host->h_addr_list[i] != NULL; i++)
|
||||
{
|
||||
struct in_addr addr;
|
||||
memcpy(&addr, host->h_addr_list[i], sizeof(struct in_addr));
|
||||
const char *ip = inet_ntoa(addr);
|
||||
if (ip && strcmp(ip, "127.0.0.1") != 0)
|
||||
{
|
||||
strncpy(ip_buffer, ip, buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 没找到非回环地址,返回第一个
|
||||
struct in_addr addr;
|
||||
memcpy(&addr, host->h_addr_list[0], sizeof(struct in_addr));
|
||||
const char *ip = inet_ntoa(addr);
|
||||
strncpy(ip_buffer, ip ? ip : "127.0.0.1", buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return true;
|
||||
#else
|
||||
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送落子消息
|
||||
*/
|
||||
bool send_move(int x, int y, int player_id)
|
||||
{
|
||||
NetworkMessage msg = {0};
|
||||
msg.type = MSG_MOVE;
|
||||
msg.player_id = player_id;
|
||||
msg.x = x;
|
||||
msg.y = y;
|
||||
msg.timestamp = time(NULL);
|
||||
|
||||
return send_network_message(&msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送聊天消息
|
||||
*/
|
||||
bool send_chat_message(const char *message)
|
||||
{
|
||||
NetworkMessage msg = {0};
|
||||
msg.type = MSG_CHAT;
|
||||
msg.player_id = network_state.local_player_id;
|
||||
strncpy(msg.message, message, sizeof(msg.message) - 1);
|
||||
msg.timestamp = time(NULL);
|
||||
|
||||
return send_network_message(&msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送认输消息
|
||||
*/
|
||||
bool send_surrender()
|
||||
{
|
||||
NetworkMessage msg = {0};
|
||||
msg.type = MSG_SURRENDER;
|
||||
msg.player_id = network_state.local_player_id;
|
||||
msg.timestamp = time(NULL);
|
||||
|
||||
return send_network_message(&msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送悔棋请求
|
||||
*/
|
||||
bool send_undo_request(int steps)
|
||||
{
|
||||
NetworkMessage msg = {0};
|
||||
msg.type = MSG_UNDO_REQUEST;
|
||||
msg.player_id = network_state.local_player_id;
|
||||
msg.x = steps; // 使用x字段存储步数
|
||||
msg.timestamp = time(NULL);
|
||||
|
||||
return send_network_message(&msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送悔棋回应
|
||||
*/
|
||||
bool send_undo_response(bool accepted, int steps)
|
||||
{
|
||||
NetworkMessage msg = {0};
|
||||
msg.type = MSG_UNDO_RESPONSE;
|
||||
msg.player_id = network_state.local_player_id;
|
||||
msg.x = steps; // 使用x字段存储步数
|
||||
msg.y = accepted ? 1 : 0; // 使用y字段存储是否同意
|
||||
msg.timestamp = time(NULL);
|
||||
|
||||
return send_network_message(&msg);
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
/**
|
||||
* @file record.c
|
||||
* @brief 游戏复盘与记录源文件
|
||||
* @note 本文件定义了游戏复盘与记录相关的函数和数据结构。
|
||||
* 它负责管理游戏的历史记录、加载和保存游戏文件、计算游戏评分等功能。
|
||||
*/
|
||||
|
||||
#include "record.h"
|
||||
#include "gobang.h"
|
||||
#include "config.h"
|
||||
#include "globals.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 计算游戏评分
|
||||
*/
|
||||
void calculate_game_scores()
|
||||
{
|
||||
// 评估双方表现
|
||||
player1_final_score = 0;
|
||||
player2_final_score = 0;
|
||||
|
||||
// 遍历所有步数,累积每一步的得分,后期步骤权重更高
|
||||
for (int i = 0; i < step_count; i++)
|
||||
{
|
||||
// 计算时间权重因子:步数越靠后,权重越大
|
||||
double time_weight = 1.0 + (double)i / step_count * TIME_WEIGHT_FACTOR; // 最后的步骤权重是开始步骤的(1+TIME_WEIGHT_FACTOR)倍
|
||||
|
||||
if (steps[i].player == PLAYER || steps[i].player == PLAYER1)
|
||||
{
|
||||
player1_final_score += (int)(calculate_step_score(steps[i].x, steps[i].y, steps[i].player) * time_weight);
|
||||
}
|
||||
else
|
||||
{
|
||||
player2_final_score += (int)(calculate_step_score(steps[i].x, steps[i].y, steps[i].player) * time_weight);
|
||||
}
|
||||
}
|
||||
|
||||
// 胜负加权:获胜方获得额外的评分奖励
|
||||
if (step_count > 0)
|
||||
{
|
||||
Step last_step = steps[step_count - 1];
|
||||
if (check_win(last_step.x, last_step.y, last_step.player))
|
||||
{
|
||||
// 获胜方获得额外奖励分数
|
||||
if (last_step.player == PLAYER || last_step.player == PLAYER1)
|
||||
{
|
||||
player1_final_score += WIN_BONUS; // 获胜奖励
|
||||
}
|
||||
else
|
||||
{
|
||||
player2_final_score += WIN_BONUS; // 获胜奖励
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scores_calculated = 1; // 标记评分已计算
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 显示游戏评分结果和MVP评选
|
||||
* @param game_mode 游戏模式(1-人机对战, 2-双人对战)
|
||||
*/
|
||||
void display_game_scores(int game_mode)
|
||||
{
|
||||
printf("\n===== 对局评分 =====\n");
|
||||
double sum_score = (double)player1_final_score + (double)player2_final_score;
|
||||
|
||||
if (sum_score > 0)
|
||||
{
|
||||
if (game_mode == GAME_MODE_AI)
|
||||
{
|
||||
printf("玩家得分: %d, 占比: %.2f%%\n",
|
||||
player1_final_score, (double)player1_final_score * 100.0 / sum_score);
|
||||
printf("AI得分: %d, 占比: %.2f%%\n",
|
||||
player2_final_score, (double)player2_final_score * 100.0 / sum_score);
|
||||
}
|
||||
else if (game_mode == GAME_MODE_PVP)
|
||||
{
|
||||
printf("玩家1(黑棋)得分: %d, 占比: %.2f%%\n",
|
||||
player1_final_score, (double)player1_final_score * 100.0 / sum_score);
|
||||
printf("玩家2(白棋)得分: %d, 占比: %.2f%%\n",
|
||||
player2_final_score, (double)player2_final_score * 100.0 / sum_score);
|
||||
}
|
||||
else if (game_mode == GAME_MODE_NETWORK)
|
||||
{
|
||||
printf("玩家1(黑棋)得分: %d, 占比: %.2f%%\n",
|
||||
player1_final_score, (double)player1_final_score * 100.0 / sum_score);
|
||||
printf("玩家2(白棋)得分: %d, 占比: %.2f%%\n",
|
||||
player2_final_score, (double)player2_final_score * 100.0 / sum_score);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (game_mode == GAME_MODE_AI)
|
||||
{
|
||||
printf("玩家得分: %d\n", player1_final_score);
|
||||
printf("AI得分: %d\n", player2_final_score);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("玩家1(黑棋)得分: %d\n", player1_final_score);
|
||||
printf("玩家2(白棋)得分: %d\n", player2_final_score);
|
||||
}
|
||||
printf("注: 双方得分均为0,无法计算占比\n");
|
||||
}
|
||||
|
||||
// 评选MVP
|
||||
if (player1_final_score > player2_final_score)
|
||||
{
|
||||
printf("\nMVP: %s (领先 %d 分)\n", (game_mode == GAME_MODE_AI) ? "玩家" : "玩家1(黑棋)", player1_final_score - player2_final_score);
|
||||
}
|
||||
else if (player2_final_score > player1_final_score)
|
||||
{
|
||||
printf("\nMVP: %s (领先 %d 分)\n", (game_mode == GAME_MODE_AI) ? "AI" : "玩家2(白棋)", player2_final_score - player1_final_score);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("\n双方势均力敌!\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将当前游戏记录保存到文件
|
||||
*/
|
||||
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; // 文件打开失败
|
||||
}
|
||||
|
||||
// 判断胜负结果
|
||||
strcpy(winner_info, "平局或未完成");
|
||||
if (step_count > 0)
|
||||
{
|
||||
Step last_step = steps[step_count - 1];
|
||||
if (check_win(last_step.x, last_step.y, last_step.player))
|
||||
{
|
||||
if (game_mode == GAME_MODE_AI)
|
||||
{
|
||||
// 人机对战
|
||||
if (last_step.player == PLAYER)
|
||||
{
|
||||
strcpy(winner_info, "玩家获胜");
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(winner_info, "AI获胜");
|
||||
}
|
||||
}
|
||||
else if (game_mode == GAME_MODE_PVP)
|
||||
{
|
||||
// 双人对战
|
||||
if (last_step.player == PLAYER1)
|
||||
{
|
||||
strcpy(winner_info, "玩家1获胜");
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(winner_info, "玩家2获胜");
|
||||
}
|
||||
}
|
||||
else if (game_mode == GAME_MODE_NETWORK)
|
||||
{
|
||||
// 网络对战
|
||||
if (last_step.player == PLAYER1)
|
||||
{
|
||||
strcpy(winner_info, "玩家1获胜");
|
||||
}
|
||||
else
|
||||
{
|
||||
strcpy(winner_info, "玩家2获胜");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 写入CSV文件头部
|
||||
if (fprintf(file, "游戏模式,棋盘大小,玩家1得分,玩家2得分,对局结果\n%d,%d,%d,%d,%s\n\n", game_mode, BOARD_SIZE, player1_final_score, player2_final_score, winner_info) < 0)
|
||||
{
|
||||
fclose(file);
|
||||
return 3; // 文件写入失败
|
||||
}
|
||||
|
||||
// 写入CSV表头
|
||||
if (fprintf(file, "步数,玩家,行坐标,列坐标\n") < 0)
|
||||
{
|
||||
fclose(file);
|
||||
return 3; // 文件写入失败
|
||||
}
|
||||
|
||||
// 写入所有落子步骤(CSV格式)
|
||||
for (int i = 0; i < step_count; i++)
|
||||
{
|
||||
if (fprintf(file, "%d,%d,%d,%d\n", i + 1, steps[i].player, steps[i].x + 1, steps[i].y + 1) < 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 0;
|
||||
}
|
||||
|
||||
// 跳过CSV文件头部行
|
||||
char buffer[256];
|
||||
if (fgets(buffer, sizeof(buffer), file) == NULL) // 跳过"游戏模式,棋盘大小"
|
||||
{
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 读取游戏模式、棋盘大小和评分结果
|
||||
int game_mode, size;
|
||||
|
||||
// 读取数据行
|
||||
if (fgets(buffer, sizeof(buffer), file) == NULL)
|
||||
{
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 尝试读取新格式(包含胜负信息)
|
||||
// Use sscanf instead of fscanf to handle line buffers safely
|
||||
// format: mode,size,score1,score2,winner_info
|
||||
// Note: winner_info might contain newlines if not stripped, but sscanf %s stops at whitespace.
|
||||
// However, CSV usually doesn't have spaces unless in quotes.
|
||||
// Our winner_info is like "Player 1 Wins" or "Draw".
|
||||
// If we use %[^,\n] it reads until comma or newline.
|
||||
|
||||
int read_count = sscanf(buffer, "%d,%d,%d,%d,%49[^,\n]", &game_mode, &size, &player1_final_score, &player2_final_score, winner_info);
|
||||
|
||||
if (read_count == 4)
|
||||
{
|
||||
// 旧格式文件,没有胜负信息
|
||||
strcpy(winner_info, "未知");
|
||||
}
|
||||
else if (read_count != 5)
|
||||
{
|
||||
// 文件格式错误
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (game_mode != GAME_MODE_AI && game_mode != GAME_MODE_PVP && game_mode != GAME_MODE_NETWORK)
|
||||
{
|
||||
fclose(file);
|
||||
return 0; // 无效的游戏模式
|
||||
}
|
||||
if (size < 5 || size > MAX_BOARD_SIZE)
|
||||
{
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 设置评分已计算标志
|
||||
scores_calculated = 1;
|
||||
|
||||
// 跳过空行和表头行
|
||||
// The previous fgets consumed the line with data and its newline.
|
||||
// Next line should be empty or headers.
|
||||
|
||||
// Check next line
|
||||
if (fgets(buffer, sizeof(buffer), file) != NULL)
|
||||
{
|
||||
// If it's just a newline (empty line)
|
||||
if (buffer[0] == '\n' || buffer[0] == '\r')
|
||||
{
|
||||
// Consume one more line for headers
|
||||
fgets(buffer, sizeof(buffer), file);
|
||||
}
|
||||
else if (strncmp(buffer, "步数", 4) != 0)
|
||||
{
|
||||
// If it's not headers, maybe we consumed the empty line already?
|
||||
// Let's be flexible. If it doesn't start with "步数", try reading one more.
|
||||
if (fgets(buffer, sizeof(buffer), file) == NULL)
|
||||
{
|
||||
fclose(file);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now buffer should contain headers "步数..." or we are ready to read data?
|
||||
// Actually, let's just loop until we find a digit or EOF
|
||||
|
||||
// 初始化棋盘
|
||||
BOARD_SIZE = size;
|
||||
empty_board();
|
||||
|
||||
// 读取所有落子步骤
|
||||
step_count = 0;
|
||||
int step_num; // 用于存储步数,但不使用
|
||||
|
||||
while (fgets(buffer, sizeof(buffer), file) != NULL)
|
||||
{
|
||||
if (step_count >= MAX_STEPS)
|
||||
{
|
||||
break; // 防止数组越界
|
||||
}
|
||||
if (sscanf(buffer, "%d,%d,%d,%d", &step_num, &steps[step_count].player, &steps[step_count].x, &steps[step_count].y) == 4)
|
||||
{
|
||||
// 将1-based坐标转换为0-based坐标
|
||||
steps[step_count].x--;
|
||||
steps[step_count].y--;
|
||||
// 验证坐标合法性
|
||||
if (steps[step_count].x < 0 || steps[step_count].x >= size ||
|
||||
steps[step_count].y < 0 || steps[step_count].y >= size)
|
||||
{
|
||||
continue; // 跳过非法坐标
|
||||
}
|
||||
step_count++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return game_mode;
|
||||
}
|
||||
Reference in New Issue
Block a user