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:
@@ -35,6 +35,13 @@ dist/
|
||||
|
||||
# 编译生成的对象文件
|
||||
obj/
|
||||
build/
|
||||
|
||||
# 临时游戏存档
|
||||
records/
|
||||
|
||||
# 运行时配置(含 API Key)
|
||||
bin/gobang_config.ini
|
||||
|
||||
# 编译产物
|
||||
bin/gobang_gui.exe
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(Gobang VERSION 9.0 LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# === 输出目录:与原 Makefile 保持一致 ===
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
|
||||
|
||||
# === 编译选项 ===
|
||||
add_compile_options(-Wall -Wextra -O2)
|
||||
add_compile_options(-finput-charset=UTF-8 -fexec-charset=UTF-8)
|
||||
|
||||
# === ENet(从源码编译静态库)===
|
||||
set(ENET_DIR ${CMAKE_SOURCE_DIR}/libs/enet)
|
||||
add_library(enet STATIC
|
||||
${ENET_DIR}/callbacks.c
|
||||
${ENET_DIR}/compress.c
|
||||
${ENET_DIR}/host.c
|
||||
${ENET_DIR}/list.c
|
||||
${ENET_DIR}/packet.c
|
||||
${ENET_DIR}/peer.c
|
||||
${ENET_DIR}/protocol.c
|
||||
${ENET_DIR}/win32.c
|
||||
)
|
||||
target_include_directories(enet PUBLIC ${ENET_DIR}/include)
|
||||
|
||||
# === IUP(预构建导入库)===
|
||||
set(IUP_DIR ${CMAKE_SOURCE_DIR}/libs/iup-3.31_Win64_dllw6_lib)
|
||||
add_library(iup SHARED IMPORTED)
|
||||
set_target_properties(iup PROPERTIES
|
||||
IMPORTED_IMPLIB ${IUP_DIR}/libiup.a
|
||||
IMPORTED_LOCATION ${IUP_DIR}/iup.dll
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${IUP_DIR}/include
|
||||
)
|
||||
|
||||
# === cJSON(JSON处理库)===
|
||||
add_library(cjson STATIC ${CMAKE_SOURCE_DIR}/libs/cJSON/cJSON.c)
|
||||
target_include_directories(cjson PUBLIC ${CMAKE_SOURCE_DIR}/libs/cJSON)
|
||||
|
||||
# === 主可执行文件 ===
|
||||
add_executable(gobang_gui
|
||||
src/core/main.c
|
||||
src/core/globals.c
|
||||
src/core/config.c
|
||||
src/core/gobang.c
|
||||
src/core/ai.c
|
||||
src/network/network.c
|
||||
src/record/record.c
|
||||
src/gui/gui_core.c
|
||||
src/gui/gui_game.c
|
||||
src/gui/gui_menu.c
|
||||
src/gui/gui_replay.c
|
||||
src/llm/llm_ai.c
|
||||
)
|
||||
|
||||
target_include_directories(gobang_gui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
target_link_libraries(gobang_gui PRIVATE
|
||||
enet
|
||||
cjson
|
||||
iup
|
||||
winhttp
|
||||
ws2_32
|
||||
winmm
|
||||
gdi32
|
||||
comdlg32
|
||||
comctl32
|
||||
uuid
|
||||
ole32
|
||||
)
|
||||
|
||||
# === 构建后拷贝 iup.dll 到 bin/ ===
|
||||
add_custom_command(TARGET gobang_gui POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${IUP_DIR}/iup.dll
|
||||
$<TARGET_FILE_DIR:gobang_gui>
|
||||
COMMENT "拷贝 iup.dll 到输出目录"
|
||||
)
|
||||
|
||||
# === 快捷运行目标 ===
|
||||
add_custom_target(run
|
||||
COMMAND gobang_gui
|
||||
DEPENDS gobang_gui
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/bin
|
||||
COMMENT "运行五子棋游戏"
|
||||
)
|
||||
@@ -1,102 +0,0 @@
|
||||
# 五子棋游戏 Makefile
|
||||
# 支持编译GUI版本 (IUP)
|
||||
|
||||
# 编译器设置
|
||||
CC = gcc
|
||||
# 显式指定 Shell 为 PowerShell
|
||||
SHELL = D:/PowerShell/PowerShell-7.5.4/PowerShell.exe
|
||||
.SHELLFLAGS = -NoProfile -Command
|
||||
|
||||
CFLAGS = -Wall -Wextra -std=c17 -O2 -Iinclude -finput-charset=UTF-8 -fexec-charset=UTF-8
|
||||
# ENet 包含路径
|
||||
ENET_INC = -Ilibs/enet/include
|
||||
CFLAGS += $(ENET_INC)
|
||||
|
||||
LDFLAGS = -lws2_32 -lwinmm
|
||||
|
||||
|
||||
# IUP路径设置
|
||||
IUP_PATH = libs/iup-3.31_Win64_dllw6_lib
|
||||
IUP_INCLUDE = "-I$(IUP_PATH)/include"
|
||||
# IUP链接库: iup, gdi32, comdlg32, comctl32, uuid, ole32
|
||||
IUP_LIBS = "-L$(IUP_PATH)" -liup -lgdi32 -lcomdlg32 -lcomctl32 -luuid -lole32
|
||||
|
||||
# 目录设置
|
||||
SRC_DIR = src
|
||||
OBJ_DIR = obj
|
||||
BIN_DIR = bin
|
||||
|
||||
# 源文件
|
||||
COMMON_SOURCES = $(SRC_DIR)/gobang.c $(SRC_DIR)/ai.c $(SRC_DIR)/config.c \
|
||||
$(SRC_DIR)/globals.c \
|
||||
$(SRC_DIR)/network.c $(SRC_DIR)/record.c \
|
||||
$(SRC_DIR)/gui_core.c $(SRC_DIR)/gui_draw.c \
|
||||
$(SRC_DIR)/gui_game.c $(SRC_DIR)/gui_replay.c \
|
||||
$(SRC_DIR)/gui_menu.c
|
||||
|
||||
# ENet 源文件
|
||||
ENET_SOURCES = libs/enet/callbacks.c libs/enet/compress.c libs/enet/host.c \
|
||||
libs/enet/list.c libs/enet/packet.c libs/enet/peer.c \
|
||||
libs/enet/protocol.c libs/enet/win32.c
|
||||
|
||||
# 目标文件 (src/xxx.c -> obj/xxx.o)
|
||||
COMMON_OBJECTS = $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(COMMON_SOURCES))
|
||||
ENET_OBJECTS = $(patsubst libs/enet/%.c,$(OBJ_DIR)/enet_%.o,$(ENET_SOURCES))
|
||||
|
||||
# 可执行文件
|
||||
GUI_TARGET = $(BIN_DIR)/gobang_gui.exe
|
||||
|
||||
# 默认目标
|
||||
all: directories $(GUI_TARGET)
|
||||
|
||||
# 创建目录 (PowerShell 语法)
|
||||
directories:
|
||||
if (!(Test-Path "$(OBJ_DIR)")) { New-Item -ItemType Directory -Path "$(OBJ_DIR)" | Out-Null }
|
||||
if (!(Test-Path "$(BIN_DIR)")) { New-Item -ItemType Directory -Path "$(BIN_DIR)" | Out-Null }
|
||||
|
||||
# GUI版本
|
||||
$(GUI_TARGET): $(COMMON_OBJECTS) $(ENET_OBJECTS) $(OBJ_DIR)/main.o
|
||||
$(CC) $(CFLAGS) $(IUP_INCLUDE) -o $@ $^ $(IUP_LIBS) $(LDFLAGS)
|
||||
Copy-Item -Path "$(subst /,\,$(IUP_PATH))\iup.dll" -Destination "$(BIN_DIR)" -Force
|
||||
|
||||
# 通用目标文件编译规则
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
|
||||
$(CC) $(CFLAGS) $(IUP_INCLUDE) -c -o $@ $<
|
||||
|
||||
# 编译 ENet 源文件
|
||||
$(OBJ_DIR)/enet_%.o: libs/enet/%.c | directories
|
||||
$(CC) $(CFLAGS) $(ENET_INC) -c -o $@ $<
|
||||
|
||||
# 编译 main.c
|
||||
$(OBJ_DIR)/main.o: $(SRC_DIR)/main.c
|
||||
$(CC) $(CFLAGS) $(IUP_INCLUDE) -c -o $@ $<
|
||||
|
||||
# 清理规则 (PowerShell 语法)
|
||||
clean:
|
||||
if (Test-Path "$(OBJ_DIR)") { Remove-Item -Path "$(OBJ_DIR)" -Recurse -Force }
|
||||
if (Test-Path "$(BIN_DIR)") { Remove-Item -Path "$(BIN_DIR)" -Recurse -Force }
|
||||
|
||||
# 编译GUI版本
|
||||
gui: directories $(GUI_TARGET)
|
||||
|
||||
# 安装规则(可选)
|
||||
install: all
|
||||
Write-Host "Installing executables..."
|
||||
Copy-Item -Path "$(GUI_TARGET)" -Destination "C:\Program Files\Gobang\" -Force
|
||||
|
||||
# 运行GUI版本
|
||||
run-gui: $(GUI_TARGET)
|
||||
& ".\$(GUI_TARGET)"
|
||||
|
||||
# 帮助信息
|
||||
help:
|
||||
@echo Available targets:
|
||||
@echo all - Build GUI version
|
||||
@echo gui - Build GUI version
|
||||
@echo clean - Remove all object files and executables
|
||||
@echo run-gui - Build and run GUI version
|
||||
@echo install - Install executables to system directory
|
||||
@echo help - Show this help message
|
||||
|
||||
# 声明伪目标
|
||||
.PHONY: all clean gui install run-gui help directories
|
||||
@@ -32,13 +32,6 @@ int evaluate_move(int x, int y);
|
||||
*/
|
||||
int evaluate_pos(int x, int y, int player);
|
||||
|
||||
/**
|
||||
* @brief 评估棋盘价值
|
||||
*
|
||||
* @param player 玩家标识(PLAYER/AI)
|
||||
*/
|
||||
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing);
|
||||
|
||||
/**
|
||||
* @brief AI下棋
|
||||
*
|
||||
@@ -71,13 +64,5 @@ bool is_near_stones(int x, int y);
|
||||
*/
|
||||
ThreatLevel detect_threat(int x, int y, int player);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
#endif // AI_H
|
||||
@@ -135,6 +135,17 @@
|
||||
#define CONFIG_FILE "gobang_config.ini" // 配置文件路径
|
||||
#define MAX_PATH_LENGTH 256 // 最大路径长度
|
||||
|
||||
//---------- LLM大模型参数 ----------//
|
||||
#define DEFAULT_LLM_USE 0 // 默认不使用LLM (0=算法AI, 1=大模型)
|
||||
#define DEFAULT_LLM_ENDPOINT "https://api.minimax.chat/v1/chat/completions" // 默认API地址
|
||||
#define DEFAULT_LLM_API_KEY "" // 默认API Key (需用户填写)
|
||||
#define DEFAULT_LLM_MODEL "MiniMax-Text-01" // 默认模型名
|
||||
#define MAX_LLM_ENDPOINT_LEN 256 // API地址最大长度
|
||||
#define MAX_LLM_API_KEY_LEN 128 // API Key最大长度
|
||||
#define MAX_LLM_MODEL_LEN 64 // 模型名最大长度
|
||||
#define LLM_MAX_RETRIES 3 // LLM返回非法坐标时最大重试次数
|
||||
#define LLM_TIMEOUT_MS 30000 // LLM HTTP请求超时(毫秒)
|
||||
|
||||
//---------- 配置管理函数声明 ----------//
|
||||
/**
|
||||
* @brief 加载游戏配置
|
||||
|
||||
@@ -29,6 +29,12 @@ extern int network_timeout; // 网络超时时间
|
||||
extern double defense_coefficient; // 防守系数
|
||||
extern int ai_difficulty; // AI难度 (1-5)
|
||||
|
||||
// ==================== LLM大模型相关变量 ====================
|
||||
extern int llm_use; // 是否使用LLM (0=算法AI, 1=大模型)
|
||||
extern char llm_endpoint[MAX_LLM_ENDPOINT_LEN]; // API地址
|
||||
extern char llm_api_key[MAX_LLM_API_KEY_LEN]; // API Key
|
||||
extern char llm_model[MAX_LLM_MODEL_LEN]; // 模型名
|
||||
|
||||
// ==================== 网络相关变量 ====================
|
||||
extern NetworkGameState network_state; // 网络游戏状态
|
||||
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@
|
||||
* 它包含了游戏棋盘的表示、玩家操作、规则检查以及AI决策等功能。
|
||||
*/
|
||||
|
||||
#ifndef GO_BANG_H
|
||||
#define GO_BANG_H
|
||||
#ifndef GOBANG_H
|
||||
#define GOBANG_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
@@ -88,4 +88,4 @@ bool return_move(int steps_to_undo);
|
||||
*/
|
||||
int calculate_step_score(int x, int y, int player);
|
||||
|
||||
#endif // GO_BANG_H
|
||||
#endif // GOBANG_H
|
||||
@@ -8,14 +8,9 @@ extern Ihandle *dlg;
|
||||
extern Ihandle *board_canvas;
|
||||
extern Ihandle *lbl_player;
|
||||
extern Ihandle *lbl_status;
|
||||
extern int gui_game_mode; // 0: PvP, 1: PvE, 2: Replay
|
||||
extern int gui_game_mode; // 0: PvP, 1: PvE, 2: Replay, 3: Network
|
||||
extern int replay_total_steps; // 复盘总步数
|
||||
|
||||
// 绘图函数 (在 gui_draw.c 中定义)
|
||||
void set_draw_color(Ihandle *ih, unsigned char r, unsigned char g, unsigned char b);
|
||||
void draw_board_iup(Ihandle *ih);
|
||||
void draw_stones_iup(Ihandle *ih);
|
||||
|
||||
// 核心功能 (在 gui_core.c 中定义)
|
||||
void update_ui_labels();
|
||||
int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y);
|
||||
@@ -24,7 +19,7 @@ int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y);
|
||||
void create_game_window();
|
||||
void start_pvp_game_gui();
|
||||
void start_pve_game_gui();
|
||||
void start_network_game_gui(); // 新增:启动网络对战游戏窗口
|
||||
void start_network_game_gui();
|
||||
int action_cb(Ihandle *ih);
|
||||
int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status);
|
||||
int k_any_cb(Ihandle *ih, int c);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file llm_ai.h
|
||||
* @brief 大模型AI模块头文件
|
||||
* @note 通过OpenAI兼容API调用大模型进行五子棋对弈
|
||||
*/
|
||||
|
||||
#ifndef LLM_AI_H
|
||||
#define LLM_AI_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* @brief 调用大模型获取落子坐标(同步,会阻塞)
|
||||
* @param out_x 输出:落子行坐标 (0-based)
|
||||
* @param out_y 输出:落子列坐标 (0-based)
|
||||
* @return true 获取成功,坐标合法且位置为空
|
||||
* @return false 获取失败(网络错误/坐标非法/重试耗尽)
|
||||
*/
|
||||
bool llm_ai_move(int *out_x, int *out_y);
|
||||
|
||||
/**
|
||||
* @brief 异步启动大模型思考(后台线程)
|
||||
* @note 调用后用 llm_ai_poll_result 轮询结果
|
||||
*/
|
||||
void llm_ai_start_move(void);
|
||||
|
||||
/**
|
||||
* @brief 轮询大模型结果(非阻塞)
|
||||
* @param out_x 输出:落子行坐标
|
||||
* @param out_y 输出:落子列坐标
|
||||
* @return 0 仍在思考, 1 成功获取坐标, -1 失败(应回退算法AI)
|
||||
*/
|
||||
int llm_ai_poll_result(int *out_x, int *out_y);
|
||||
|
||||
#endif // LLM_AI_H
|
||||
+2
-6
@@ -13,13 +13,9 @@
|
||||
#include "type.h"
|
||||
#include "config.h"
|
||||
#include <stdbool.h>
|
||||
#include <enet/enet.h> // 引入 ENet 头文件
|
||||
#include <enet/enet.h>
|
||||
|
||||
// 网络状态结构体在 type.h 中定义,我们需要修改 type.h 来包含 ENet 类型
|
||||
// 或者在这里重新定义(如果 type.h 中可以移除旧的定义)
|
||||
|
||||
// 全局网络状态
|
||||
extern NetworkGameState network_state;
|
||||
// network_state 的 extern 声明在 globals.h 中
|
||||
|
||||
// 函数声明
|
||||
|
||||
|
||||
+3143
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef cJSON__h
|
||||
#define cJSON__h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
|
||||
#define __WINDOWS__
|
||||
#endif
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
|
||||
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
|
||||
|
||||
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
|
||||
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
|
||||
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
|
||||
|
||||
For *nix builds that support visibility attribute, you can define similar behavior by
|
||||
|
||||
setting default visibility to hidden by adding
|
||||
-fvisibility=hidden (for gcc)
|
||||
or
|
||||
-xldscope=hidden (for sun cc)
|
||||
to CFLAGS
|
||||
|
||||
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
|
||||
|
||||
*/
|
||||
|
||||
#define CJSON_CDECL __cdecl
|
||||
#define CJSON_STDCALL __stdcall
|
||||
|
||||
/* export symbols by default, this is necessary for copy pasting the C and header file */
|
||||
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_EXPORT_SYMBOLS
|
||||
#endif
|
||||
|
||||
#if defined(CJSON_HIDE_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) type CJSON_STDCALL
|
||||
#elif defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
|
||||
#elif defined(CJSON_IMPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
|
||||
#endif
|
||||
#else /* !__WINDOWS__ */
|
||||
#define CJSON_CDECL
|
||||
#define CJSON_STDCALL
|
||||
|
||||
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
|
||||
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
|
||||
#else
|
||||
#define CJSON_PUBLIC(type) type
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* project version */
|
||||
#define CJSON_VERSION_MAJOR 1
|
||||
#define CJSON_VERSION_MINOR 7
|
||||
#define CJSON_VERSION_PATCH 18
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* cJSON Types: */
|
||||
#define cJSON_Invalid (0)
|
||||
#define cJSON_False (1 << 0)
|
||||
#define cJSON_True (1 << 1)
|
||||
#define cJSON_NULL (1 << 2)
|
||||
#define cJSON_Number (1 << 3)
|
||||
#define cJSON_String (1 << 4)
|
||||
#define cJSON_Array (1 << 5)
|
||||
#define cJSON_Object (1 << 6)
|
||||
#define cJSON_Raw (1 << 7) /* raw json */
|
||||
|
||||
#define cJSON_IsReference 256
|
||||
#define cJSON_StringIsConst 512
|
||||
|
||||
/* The cJSON structure: */
|
||||
typedef struct cJSON
|
||||
{
|
||||
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
|
||||
struct cJSON *next;
|
||||
struct cJSON *prev;
|
||||
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
|
||||
struct cJSON *child;
|
||||
|
||||
/* The type of the item, as above. */
|
||||
int type;
|
||||
|
||||
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
|
||||
char *valuestring;
|
||||
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
|
||||
int valueint;
|
||||
/* The item's number, if type==cJSON_Number */
|
||||
double valuedouble;
|
||||
|
||||
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
|
||||
char *string;
|
||||
} cJSON;
|
||||
|
||||
typedef struct cJSON_Hooks
|
||||
{
|
||||
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
|
||||
void *(CJSON_CDECL *malloc_fn)(size_t sz);
|
||||
void (CJSON_CDECL *free_fn)(void *ptr);
|
||||
} cJSON_Hooks;
|
||||
|
||||
typedef int cJSON_bool;
|
||||
|
||||
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_NESTING_LIMIT
|
||||
#define CJSON_NESTING_LIMIT 1000
|
||||
#endif
|
||||
|
||||
/* returns the version of cJSON as a string */
|
||||
CJSON_PUBLIC(const char*) cJSON_Version(void);
|
||||
|
||||
/* Supply malloc, realloc and free functions to cJSON */
|
||||
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||
|
||||
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
|
||||
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
|
||||
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
|
||||
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
|
||||
/* Render a cJSON entity to text for transfer/storage. */
|
||||
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
|
||||
/* Render a cJSON entity to text for transfer/storage without any formatting. */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
|
||||
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
|
||||
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
|
||||
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
|
||||
/* Delete a cJSON entity and all subentities. */
|
||||
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
|
||||
|
||||
/* Returns the number of items in an array (or object). */
|
||||
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
|
||||
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
|
||||
/* Get item "string" from object. Case insensitive. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
|
||||
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
|
||||
|
||||
/* Check item type and return its value */
|
||||
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
|
||||
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
|
||||
|
||||
/* These functions check the type of an item */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
|
||||
|
||||
/* These calls create a cJSON item of the appropriate type. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
|
||||
/* raw json */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
|
||||
|
||||
/* Create a string where valuestring references a string so
|
||||
* it will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
|
||||
/* Create an object/array that only references it's elements so
|
||||
* they will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
|
||||
|
||||
/* These utilities create an Array of count items.
|
||||
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
|
||||
|
||||
/* Append item to the specified array/object. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
|
||||
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
|
||||
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
|
||||
* writing to `item->string` */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
|
||||
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
|
||||
|
||||
/* Remove/Detach items from Arrays/Objects. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
|
||||
/* Update array items. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
|
||||
|
||||
/* Duplicate a cJSON item */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
|
||||
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
|
||||
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
|
||||
* The item->next and ->prev pointers are always zero on return from Duplicate. */
|
||||
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
|
||||
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
|
||||
|
||||
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
|
||||
* The input pointer json cannot point to a read-only address area, such as a string constant,
|
||||
* but should point to a readable and writable address area. */
|
||||
CJSON_PUBLIC(void) cJSON_Minify(char *json);
|
||||
|
||||
/* Helper functions for creating and adding items to an object at the same time.
|
||||
* They return the added item or NULL on failure. */
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
|
||||
|
||||
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
|
||||
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
|
||||
/* helper for the cJSON_SetNumberValue macro */
|
||||
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
|
||||
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
|
||||
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
|
||||
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
|
||||
|
||||
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
|
||||
#define cJSON_SetBoolValue(object, boolValue) ( \
|
||||
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
|
||||
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
|
||||
cJSON_Invalid\
|
||||
)
|
||||
|
||||
/* Macro for iterating over an array or object */
|
||||
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
|
||||
|
||||
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
|
||||
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
|
||||
CJSON_PUBLIC(void) cJSON_free(void *object);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -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