mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35:55 +08:00
feat: 集成大模型AI、重构构建系统并修复多项代码质量问题
- 构建系统:Makefile 迁移至 CMakeLists.txt,支持 cJSON 和 WinHTTP - 项目结构:src/ 按功能拆分为 core/、gui/、network/、record/、llm/ 子目录 - 新功能:集成大模型 AI(WinHTTP + cJSON,兼容 OpenAI 协议),支持异步请求 - 渲染修复:IupDraw* 替换为 Windows GDI,修复画布黑屏问题 - 网络修复:ENet 初始化幂等化,实现真实 get_local_ip() (Winsock) - 代码质量:删除死代码 (dfs/count_threats_in_direction),修复头文件守卫, sprintf→snprintf 防溢出,strncpy 安全终止,GDI 资源泄漏修复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -137,94 +137,6 @@ int evaluate_pos(int x, int y, int player)
|
||||
return total_score + position_bonus; // 返回总评估分
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 带α-β剪枝的深度优先搜索(极小极大算法实现)
|
||||
* @param x 当前行坐标
|
||||
* @param y 当前列坐标
|
||||
* @param player 当前玩家
|
||||
* @param depth 剩余搜索深度
|
||||
* @param alpha α值(当前最大值)
|
||||
* @param beta β值(当前最小值)
|
||||
* @param is_maximizing 是否为极大化玩家(AI)
|
||||
* @return int 最佳评估分数
|
||||
* @note 算法流程:
|
||||
* 1. 检查是否获胜或达到搜索深度
|
||||
* 2. 遍历所有可能落子位置
|
||||
* 3. 递归评估每个位置的分数
|
||||
* 4. 根据is_maximizing选择最大/最小值
|
||||
* 5. 使用α-β剪枝优化搜索过程
|
||||
*/
|
||||
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing)
|
||||
{
|
||||
// 检查当前落子是否获胜
|
||||
if (check_win(x, y, player))
|
||||
{
|
||||
return (player == AI) ? SEARCH_WIN_BONUS + depth : -SEARCH_WIN_BONUS - depth;
|
||||
}
|
||||
|
||||
// 达到搜索深度或平局
|
||||
if (depth == 0 || step_count >= BOARD_SIZE * BOARD_SIZE)
|
||||
{
|
||||
return evaluate_pos(x, y, AI) - evaluate_pos(x, y, PLAYER);
|
||||
}
|
||||
|
||||
int best_score = is_maximizing ? -1000000 : 1000000;
|
||||
|
||||
// 使用移动排序优化搜索效率
|
||||
ScoredMove candidate_moves[BOARD_SIZE * BOARD_SIZE];
|
||||
int move_count = generate_candidate_moves(candidate_moves, player);
|
||||
|
||||
// 限制搜索的候选移动数量以提高性能
|
||||
int max_candidates = (depth >= 3) ? 15 : 25; // 深度越大,候选移动越少
|
||||
if (move_count > max_candidates)
|
||||
{
|
||||
move_count = max_candidates;
|
||||
}
|
||||
|
||||
// 遍历排序后的候选移动
|
||||
for (int idx = 0; idx < move_count; idx++)
|
||||
{
|
||||
int i = candidate_moves[idx].x;
|
||||
int j = candidate_moves[idx].y;
|
||||
|
||||
// 模拟当前玩家落子
|
||||
board[i][j] = player;
|
||||
step_count++;
|
||||
|
||||
// 递归搜索(切换玩家和搜索深度)
|
||||
int current_score = dfs(i, j, (player == AI) ? PLAYER : AI, depth - 1, alpha, beta, !is_maximizing);
|
||||
|
||||
// 撤销落子
|
||||
board[i][j] = EMPTY;
|
||||
step_count--;
|
||||
|
||||
// 极大值玩家(AI)逻辑
|
||||
if (is_maximizing)
|
||||
{
|
||||
best_score = (current_score > best_score) ? current_score : best_score;
|
||||
alpha = (best_score > alpha) ? best_score : alpha;
|
||||
// α剪枝
|
||||
if (beta <= alpha)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 极小值玩家(人类)逻辑
|
||||
else
|
||||
{
|
||||
best_score = (current_score < best_score) ? current_score : best_score;
|
||||
beta = (best_score < beta) ? best_score : beta;
|
||||
// β剪枝
|
||||
if (beta <= alpha)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best_score;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AI决策主函数,使用评估函数和搜索算法选择最佳落子位置
|
||||
* @note 采用两阶段决策逻辑:
|
||||
@@ -542,58 +454,3 @@ ThreatLevel detect_threat(int x, int y, int player)
|
||||
return max_threat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算指定方向的威胁数量
|
||||
* @param x, y 起始位置
|
||||
* @param dx, dy 方向向量
|
||||
* @param player 玩家
|
||||
* @return 威胁数量
|
||||
*/
|
||||
int count_threats_in_direction(int x, int y, int dx, int dy, int player)
|
||||
{
|
||||
int threats = 0;
|
||||
|
||||
// 向前搜索
|
||||
for (int i = 1; i < 5; i++)
|
||||
{
|
||||
int nx = x + i * dx;
|
||||
int ny = y + i * dy;
|
||||
|
||||
if (nx < 0 || nx >= BOARD_SIZE || ny < 0 || ny >= BOARD_SIZE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (board[nx][ny] == player)
|
||||
{
|
||||
threats++;
|
||||
}
|
||||
else if (board[nx][ny] != EMPTY)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 向后搜索
|
||||
for (int i = 1; i < 5; i++)
|
||||
{
|
||||
int nx = x - i * dx;
|
||||
int ny = y - i * dy;
|
||||
|
||||
if (nx < 0 || nx >= BOARD_SIZE || ny < 0 || ny >= BOARD_SIZE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (board[nx][ny] == player)
|
||||
{
|
||||
threats++;
|
||||
}
|
||||
else if (board[nx][ny] != EMPTY)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return threats;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ void load_game_config()
|
||||
return;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), file))
|
||||
{
|
||||
// 去除换行符
|
||||
@@ -79,6 +79,25 @@ void load_game_config()
|
||||
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);
|
||||
@@ -114,6 +133,16 @@ void save_game_config()
|
||||
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");
|
||||
}
|
||||
@@ -132,5 +161,13 @@ void reset_to_default_config()
|
||||
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");
|
||||
}
|
||||
@@ -26,6 +26,12 @@ int network_timeout = NETWORK_TIMEOUT_MS; // 网络超时时间
|
||||
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}; // 网络游戏状态
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#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>
|
||||
@@ -13,6 +17,7 @@
|
||||
#include <string.h>
|
||||
|
||||
static Ihandle *timer = NULL; // 网络轮询定时器
|
||||
static Ihandle *llm_timer = NULL; // LLM异步轮询定时器
|
||||
|
||||
/**
|
||||
* @brief 网络事件轮询回调
|
||||
@@ -72,24 +77,173 @@ static int timer_cb(Ihandle *ih)
|
||||
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)
|
||||
{
|
||||
IupDrawBegin(ih);
|
||||
HWND hwnd = (HWND)IupGetAttribute(ih, "WID");
|
||||
if (!hwnd)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
int w, h;
|
||||
IupGetIntInt(ih, "DRAWSIZE", &w, &h);
|
||||
HDC hdc = GetDC(hwnd);
|
||||
if (!hdc)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
set_draw_color(ih, 240, 217, 181); // 棋盘背景色 (木纹色近似)
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawRectangle(ih, 0, 0, w, h);
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
|
||||
draw_board_iup(ih);
|
||||
draw_stones_iup(ih);
|
||||
// 预创建所有 GDI 对象(避免循环内反复创建销毁)
|
||||
HBRUSH bg_brush = CreateSolidBrush(RGB(240, 217, 181));
|
||||
HBRUSH black_brush = CreateSolidBrush(RGB(0, 0, 0));
|
||||
HBRUSH white_brush = CreateSolidBrush(RGB(255, 255, 255));
|
||||
HBRUSH red_brush = CreateSolidBrush(RGB(255, 0, 0));
|
||||
HPEN grid_pen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
|
||||
HPEN stone_pen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
|
||||
|
||||
// 1. 填充背景
|
||||
FillRect(hdc, &rc, bg_brush);
|
||||
|
||||
// 2. 绘制棋盘网格
|
||||
HPEN prev_pen = (HPEN)SelectObject(hdc, grid_pen);
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
MoveToEx(hdc, BOARD_OFFSET_X, BOARD_OFFSET_Y + i * CELL_SIZE, NULL);
|
||||
LineTo(hdc, BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE, BOARD_OFFSET_Y + i * CELL_SIZE);
|
||||
MoveToEx(hdc, BOARD_OFFSET_X + i * CELL_SIZE, BOARD_OFFSET_Y, NULL);
|
||||
LineTo(hdc, BOARD_OFFSET_X + i * CELL_SIZE, BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE);
|
||||
}
|
||||
|
||||
// 3. 星位/天元
|
||||
SelectObject(hdc, black_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 - 3, cy - 3, cx + 4, cy + 4);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
Ellipse(hdc, cx - 3, cy - 3, cx + 4, cy + 4);
|
||||
}
|
||||
|
||||
// 4. 绘制棋子
|
||||
SelectObject(hdc, stone_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_brush);
|
||||
else
|
||||
SelectObject(hdc, white_brush);
|
||||
|
||||
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
|
||||
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
|
||||
}
|
||||
|
||||
// 5. 标记最后落子位置(红色小方块)
|
||||
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;
|
||||
RECT mark = {cx - 3, cy - 3, cx + 4, cy + 4};
|
||||
FillRect(hdc, &mark, red_brush);
|
||||
}
|
||||
|
||||
// 恢复原始 GDI 对象,然后清理
|
||||
SelectObject(hdc, prev_pen);
|
||||
ReleaseDC(hwnd, hdc);
|
||||
|
||||
DeleteObject(bg_brush);
|
||||
DeleteObject(black_brush);
|
||||
DeleteObject(white_brush);
|
||||
DeleteObject(red_brush);
|
||||
DeleteObject(grid_pen);
|
||||
DeleteObject(stone_pen);
|
||||
|
||||
IupDrawEnd(ih);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -163,14 +317,20 @@ int btn_save_cb(Ihandle *ih)
|
||||
else
|
||||
base_name = filename;
|
||||
|
||||
int mode = (gui_game_mode == 0) ? GAME_MODE_PVP : GAME_MODE_AI;
|
||||
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)
|
||||
{
|
||||
sprintf(status_message, "保存成功: %s", base_name);
|
||||
snprintf(status_message, sizeof(status_message), "保存成功: %s", base_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf(status_message, "保存失败");
|
||||
snprintf(status_message, sizeof(status_message), "保存失败");
|
||||
}
|
||||
update_ui_labels();
|
||||
}
|
||||
@@ -185,23 +345,29 @@ int btn_save_cb(Ihandle *ih)
|
||||
int btn_back_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
printf("DEBUG: Back to Menu clicked\n");
|
||||
|
||||
// 如果是网络模式,断开连接
|
||||
// 停止所有定时器
|
||||
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)
|
||||
{
|
||||
disconnect_network();
|
||||
if (timer)
|
||||
{
|
||||
IupSetAttribute(timer, "RUN", "NO");
|
||||
IupDestroy(timer);
|
||||
timer = NULL;
|
||||
}
|
||||
cleanup_network();
|
||||
}
|
||||
|
||||
// 1. 先显示主菜单
|
||||
show_main_menu();
|
||||
printf("DEBUG: Main menu shown\n");
|
||||
|
||||
// 2. 销毁游戏窗口
|
||||
if (dlg)
|
||||
@@ -209,7 +375,6 @@ int btn_back_cb(Ihandle *ih)
|
||||
Ihandle *old_dlg = dlg;
|
||||
dlg = NULL; // 先清除全局指针
|
||||
IupDestroy(old_dlg);
|
||||
printf("DEBUG: Destroyed game window\n");
|
||||
}
|
||||
|
||||
return IUP_IGNORE; // 返回 IUP_IGNORE 以阻止默认处理
|
||||
@@ -278,25 +443,34 @@ int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status)
|
||||
else // PvE
|
||||
{
|
||||
current_player_gui = AI;
|
||||
sprintf(status_message, "AI思考中...");
|
||||
update_ui_labels();
|
||||
IupUpdate(ih); // 立即更新显示
|
||||
IupFlush(); // 强制刷新事件队列
|
||||
|
||||
// AI 回合
|
||||
ai_move(ai_difficulty);
|
||||
|
||||
Step last_step = steps[step_count - 1];
|
||||
if (check_win(last_step.x, last_step.y, AI))
|
||||
if (llm_use)
|
||||
{
|
||||
game_over = 1;
|
||||
sprintf(status_message, "AI获胜!");
|
||||
IupMessage("游戏结束", "AI获胜!");
|
||||
// 大模型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
|
||||
{
|
||||
current_player_gui = PLAYER;
|
||||
sprintf(status_message, "轮到玩家");
|
||||
// 算法AI - 同步调用
|
||||
sprintf(status_message, "AI思考中...");
|
||||
update_ui_labels();
|
||||
|
||||
ai_move(ai_difficulty);
|
||||
process_ai_move_result();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,8 +510,6 @@ int k_any_cb(Ihandle *ih, int c)
|
||||
*/
|
||||
void create_game_window()
|
||||
{
|
||||
printf("DEBUG: create_game_window start\n");
|
||||
|
||||
if (dlg)
|
||||
{
|
||||
IupDestroy(dlg);
|
||||
@@ -349,10 +521,10 @@ void create_game_window()
|
||||
if (!board_canvas)
|
||||
printf("ERROR: Failed to create board_canvas\n");
|
||||
|
||||
IupSetAttribute(board_canvas, "ACTION", "action_cb");
|
||||
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 * CELL_SIZE + BOARD_OFFSET_X * 2;
|
||||
@@ -360,6 +532,8 @@ void create_game_window()
|
||||
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", "240 217 181");
|
||||
|
||||
// 创建标签 (玩家信息和游戏状态)
|
||||
lbl_player = IupLabel("当前玩家: 黑子");
|
||||
@@ -434,8 +608,6 @@ void create_game_window()
|
||||
|
||||
// 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单
|
||||
IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb);
|
||||
|
||||
printf("DEBUG: create_game_window end\n");
|
||||
}
|
||||
|
||||
void start_pvp_game_gui()
|
||||
@@ -448,6 +620,7 @@ void start_pvp_game_gui()
|
||||
create_game_window();
|
||||
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
IupFlush(); // 确保窗口完全映射
|
||||
sprintf(status_message, "玩家对战模式 - 黑方先行");
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
@@ -456,56 +629,44 @@ void start_pvp_game_gui()
|
||||
|
||||
void start_pve_game_gui()
|
||||
{
|
||||
printf("DEBUG: start_pve_game_gui start\n");
|
||||
gui_game_mode = 1;
|
||||
// ai_difficulty 是全局变量
|
||||
empty_board();
|
||||
current_player_gui = PLAYER;
|
||||
game_over = 0;
|
||||
|
||||
create_game_window();
|
||||
printf("DEBUG: create_game_window returned\n");
|
||||
|
||||
if (dlg)
|
||||
{
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
printf("DEBUG: IupShowXY called\n");
|
||||
IupFlush();
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("ERROR: dlg is NULL in start_pve_game_gui\n");
|
||||
return;
|
||||
}
|
||||
|
||||
sprintf(status_message, "人机对战模式 - 玩家执黑先行");
|
||||
update_ui_labels();
|
||||
printf("DEBUG: update_ui_labels returned\n");
|
||||
|
||||
// 强制初始重绘
|
||||
if (board_canvas)
|
||||
{
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
printf("DEBUG: start_pve_game_gui end\n");
|
||||
}
|
||||
|
||||
void start_network_game_gui()
|
||||
{
|
||||
printf("DEBUG: start_network_game_gui start\n");
|
||||
gui_game_mode = 3;
|
||||
empty_board();
|
||||
|
||||
// 主机执黑先行
|
||||
current_player_gui = PLAYER1;
|
||||
game_over = 0;
|
||||
|
||||
create_game_window();
|
||||
printf("DEBUG: create_game_window returned\n");
|
||||
|
||||
if (dlg)
|
||||
{
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
|
||||
printf("DEBUG: IupShowXY called\n");
|
||||
IupFlush();
|
||||
}
|
||||
|
||||
if (network_state.is_server)
|
||||
@@ -526,6 +687,4 @@ void start_network_game_gui()
|
||||
IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb);
|
||||
IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次
|
||||
IupSetAttribute(timer, "RUN", "YES");
|
||||
|
||||
printf("DEBUG: start_network_game_gui end\n");
|
||||
}
|
||||
@@ -14,20 +14,16 @@ Ihandle *menu_dlg = NULL;
|
||||
static int btn_pvp_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
printf("DEBUG: Starting PvP Game\n");
|
||||
// hide_main_menu(); // DO NOT HIDE MAIN MENU YET
|
||||
start_pvp_game_gui();
|
||||
IupHide(menu_dlg); // Hide main menu manually AFTER game window created
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
static int btn_pve_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
printf("DEBUG: Starting PvE Game\n");
|
||||
// hide_main_menu(); // DO NOT HIDE MAIN MENU YET
|
||||
start_pve_game_gui();
|
||||
IupHide(menu_dlg); // Hide main menu manually AFTER game window created
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -87,7 +83,6 @@ static int btn_network_cancel_cb(Ihandle *ih)
|
||||
static int btn_network_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
printf("DEBUG: Opening Network Menu\n");
|
||||
|
||||
Ihandle *txt_ip = IupText(NULL);
|
||||
IupSetAttribute(txt_ip, "NAME", "NET_IP");
|
||||
@@ -135,10 +130,8 @@ static int btn_network_cb(Ihandle *ih)
|
||||
static int btn_replay_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
printf("DEBUG: Starting Replay Mode\n");
|
||||
// hide_main_menu(); // Don't hide main menu yet, wait for file selection
|
||||
start_replay_gui();
|
||||
IupHide(menu_dlg); // Hide main menu
|
||||
IupHide(menu_dlg);
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -180,6 +173,36 @@ static int btn_save_settings_cb(Ihandle *ih)
|
||||
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();
|
||||
|
||||
@@ -251,6 +274,42 @@ static int btn_settings_cb(Ihandle *ih)
|
||||
IupSetInt(lst_ai, "VALUE", ai_difficulty);
|
||||
IupSetAttribute(lst_ai, "SIZE", "80x");
|
||||
|
||||
// === 大模型AI设置 ===
|
||||
Ihandle *lbl_llm_sep = IupLabel("--- 大模型AI设置 ---");
|
||||
IupSetAttribute(lbl_llm_sep, "ALIGNMENT", "ACENTER");
|
||||
|
||||
// 6. AI 模式选择
|
||||
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");
|
||||
|
||||
// 7. LLM API 地址
|
||||
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");
|
||||
|
||||
// 8. LLM API Key
|
||||
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");
|
||||
|
||||
// 9. LLM 模型名
|
||||
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");
|
||||
|
||||
// Buttons
|
||||
Ihandle *btn_save = IupButton("保存", NULL);
|
||||
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb);
|
||||
@@ -273,6 +332,23 @@ static int btn_settings_cb(Ihandle *ih)
|
||||
IupSetAttribute(hbox_ai, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_ai, "GAP", "10");
|
||||
|
||||
// LLM 设置布局
|
||||
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 *hbox_endpoint = IupHbox(lbl_endpoint, txt_endpoint, NULL);
|
||||
IupSetAttribute(hbox_endpoint, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_endpoint, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_apikey = IupHbox(lbl_apikey, txt_apikey, NULL);
|
||||
IupSetAttribute(hbox_apikey, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_apikey, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_model = IupHbox(lbl_model, txt_model, NULL);
|
||||
IupSetAttribute(hbox_model, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_model, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
|
||||
IupSetAttribute(hbox_btns, "GAP", "20");
|
||||
IupSetAttribute(hbox_btns, "MARGIN", "10x0");
|
||||
@@ -285,11 +361,17 @@ static int btn_settings_cb(Ihandle *ih)
|
||||
hbox_time,
|
||||
hbox_ai,
|
||||
IupLabel(NULL), // Spacer
|
||||
lbl_llm_sep,
|
||||
hbox_ai_mode,
|
||||
hbox_endpoint,
|
||||
hbox_apikey,
|
||||
hbox_model,
|
||||
IupLabel(NULL), // Spacer
|
||||
hbox_btns,
|
||||
NULL);
|
||||
|
||||
IupSetAttribute(vbox, "GAP", "15");
|
||||
IupSetAttribute(vbox, "MARGIN", "30x30");
|
||||
IupSetAttribute(vbox, "GAP", "10");
|
||||
IupSetAttribute(vbox, "MARGIN", "20x20");
|
||||
|
||||
Ihandle *dlg = IupDialog(vbox);
|
||||
IupSetAttribute(dlg, "TITLE", "游戏设置");
|
||||
@@ -315,7 +397,6 @@ void create_main_menu()
|
||||
{
|
||||
if (menu_dlg)
|
||||
return;
|
||||
printf("DEBUG: create_main_menu\n");
|
||||
|
||||
Ihandle *lbl_title = IupLabel("五子棋 (Gobang)");
|
||||
IupSetAttribute(lbl_title, "FONT", "SimHei, 24");
|
||||
@@ -377,14 +458,9 @@ void create_main_menu()
|
||||
|
||||
void show_main_menu()
|
||||
{
|
||||
printf("DEBUG: show_main_menu start\n");
|
||||
if (!menu_dlg)
|
||||
{
|
||||
printf("DEBUG: Creating main menu\n");
|
||||
create_main_menu();
|
||||
}
|
||||
IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER);
|
||||
printf("DEBUG: show_main_menu end\n");
|
||||
}
|
||||
|
||||
void hide_main_menu()
|
||||
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
@@ -17,12 +17,18 @@
|
||||
/**
|
||||
* @brief 初始化网络模块
|
||||
*/
|
||||
static bool enet_initialized = false;
|
||||
|
||||
bool init_network()
|
||||
{
|
||||
if (enet_initialize() != 0)
|
||||
if (!enet_initialized)
|
||||
{
|
||||
printf("An error occurred while initializing ENet.\n");
|
||||
return false;
|
||||
if (enet_initialize() != 0)
|
||||
{
|
||||
printf("An error occurred while initializing ENet.\n");
|
||||
return false;
|
||||
}
|
||||
enet_initialized = true;
|
||||
}
|
||||
|
||||
memset(&network_state, 0, sizeof(NetworkGameState));
|
||||
@@ -44,8 +50,13 @@ void cleanup_network()
|
||||
network_state.host = NULL;
|
||||
}
|
||||
|
||||
enet_deinitialize();
|
||||
network_state.is_connected = false;
|
||||
if (enet_initialized)
|
||||
{
|
||||
enet_deinitialize();
|
||||
enet_initialized = false;
|
||||
}
|
||||
|
||||
memset(&network_state, 0, sizeof(NetworkGameState));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +64,9 @@ void cleanup_network()
|
||||
*/
|
||||
bool create_server(int port)
|
||||
{
|
||||
if (!init_network())
|
||||
return false;
|
||||
|
||||
ENetAddress address;
|
||||
|
||||
// 绑定所有接口
|
||||
@@ -117,6 +131,9 @@ bool create_server(int port)
|
||||
*/
|
||||
bool connect_to_server(const char *ip, int port)
|
||||
{
|
||||
if (!init_network())
|
||||
return false;
|
||||
|
||||
// 创建客户端主机
|
||||
network_state.host = (void *)enet_host_create(NULL, // 创建客户端
|
||||
1, // 仅允许1个传出连接
|
||||
@@ -286,17 +303,54 @@ bool is_network_connected()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取本机IP地址
|
||||
* @brief 获取本机局域网IP地址
|
||||
*/
|
||||
bool get_local_ip(char *ip_buffer, int buffer_size)
|
||||
{
|
||||
// ENet 没有直接获取本机局域网 IP 的简单跨平台函数。
|
||||
// 这里我们可以回退到原生 socket 方法,或者简单返回本地回环。
|
||||
// 为了不引入额外的系统头文件,暂时返回通用提示。
|
||||
// 在真实应用中,可以保留之前的 gethostname/gethostbyname 逻辑。
|
||||
strncpy(ip_buffer, "查看本机网络适配器", buffer_size - 1);
|
||||
#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; // 总是返回 true 以允许服务器继续启动
|
||||
return true;
|
||||
#else
|
||||
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
|
||||
ip_buffer[buffer_size - 1] = '\0';
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +261,7 @@ int load_game_from_file(const char *filename)
|
||||
FILE *file = fopen(fullpath, "r");
|
||||
if (!file)
|
||||
{
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 跳过CSV文件头部行
|
||||
Reference in New Issue
Block a user