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:
2026-05-02 15:32:54 +08:00
parent f897536a45
commit 96a94aaddf
25 changed files with 4631 additions and 363 deletions
-143
View File
@@ -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;
}
+38 -1
View File
@@ -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");
}
+6
View File
@@ -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}; // 网络游戏状态
View File
View File
+214 -55
View File
@@ -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");
}
+94 -18
View File
@@ -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()
+616
View File
@@ -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);
}
+66 -12
View File
@@ -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 获取本机 IPws2_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
}
/**
+1 -1
View File
@@ -261,7 +261,7 @@ int load_game_from_file(const char *filename)
FILE *file = fopen(fullpath, "r");
if (!file)
{
return false;
return 0;
}
// 跳过CSV文件头部行