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
+7
View File
@@ -35,6 +35,13 @@ dist/
# 编译生成的对象文件 # 编译生成的对象文件
obj/ obj/
build/
# 临时游戏存档 # 临时游戏存档
records/ records/
# 运行时配置(含 API Key
bin/gobang_config.ini
# 编译产物
bin/gobang_gui.exe
+87
View File
@@ -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
)
# === cJSONJSON处理库)===
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 "运行五子棋游戏"
)
-102
View File
@@ -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
-15
View File
@@ -32,13 +32,6 @@ int evaluate_move(int x, int y);
*/ */
int evaluate_pos(int x, int y, int player); 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下棋 * @brief AI下棋
* *
@@ -71,13 +64,5 @@ bool is_near_stones(int x, int y);
*/ */
ThreatLevel detect_threat(int x, int y, int player); 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 #endif // AI_H
+11
View File
@@ -135,6 +135,17 @@
#define CONFIG_FILE "gobang_config.ini" // 配置文件路径 #define CONFIG_FILE "gobang_config.ini" // 配置文件路径
#define MAX_PATH_LENGTH 256 // 最大路径长度 #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 加载游戏配置 * @brief 加载游戏配置
+6
View File
@@ -29,6 +29,12 @@ extern int network_timeout; // 网络超时时间
extern double defense_coefficient; // 防守系数 extern double defense_coefficient; // 防守系数
extern int ai_difficulty; // AI难度 (1-5) 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; // 网络游戏状态 extern NetworkGameState network_state; // 网络游戏状态
+3 -3
View File
@@ -5,8 +5,8 @@
* 它包含了游戏棋盘的表示、玩家操作、规则检查以及AI决策等功能。 * 它包含了游戏棋盘的表示、玩家操作、规则检查以及AI决策等功能。
*/ */
#ifndef GO_BANG_H #ifndef GOBANG_H
#define GO_BANG_H #define GOBANG_H
#include <stdio.h> #include <stdio.h>
#include <stdbool.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); int calculate_step_score(int x, int y, int player);
#endif // GO_BANG_H #endif // GOBANG_H
+2 -7
View File
@@ -8,14 +8,9 @@ extern Ihandle *dlg;
extern Ihandle *board_canvas; extern Ihandle *board_canvas;
extern Ihandle *lbl_player; extern Ihandle *lbl_player;
extern Ihandle *lbl_status; 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; // 复盘总步数 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 中定义) // 核心功能 (在 gui_core.c 中定义)
void update_ui_labels(); void update_ui_labels();
int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y); 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 create_game_window();
void start_pvp_game_gui(); void start_pvp_game_gui();
void start_pve_game_gui(); void start_pve_game_gui();
void start_network_game_gui(); // 新增:启动网络对战游戏窗口 void start_network_game_gui();
int action_cb(Ihandle *ih); int action_cb(Ihandle *ih);
int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status); int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status);
int k_any_cb(Ihandle *ih, int c); int k_any_cb(Ihandle *ih, int c);
+35
View File
@@ -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
View File
@@ -13,13 +13,9 @@
#include "type.h" #include "type.h"
#include "config.h" #include "config.h"
#include <stdbool.h> #include <stdbool.h>
#include <enet/enet.h> // 引入 ENet 头文件 #include <enet/enet.h>
// 网络状态结构体在 type.h 中定义,我们需要修改 type.h 来包含 ENet 类型 // network_state 的 extern 声明在 globals.h 中
// 或者在这里重新定义(如果 type.h 中可以移除旧的定义)
// 全局网络状态
extern NetworkGameState network_state;
// 函数声明 // 函数声明
+3143
View File
File diff suppressed because it is too large Load Diff
+300
View File
@@ -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
-143
View File
@@ -137,94 +137,6 @@ int evaluate_pos(int x, int y, int player)
return total_score + position_bonus; // 返回总评估分 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决策主函数使 * @brief AI决策主函数使
* @note * @note
@@ -542,58 +454,3 @@ ThreatLevel detect_threat(int x, int y, int player)
return max_threat; 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; return;
} }
char line[256]; char line[512];
while (fgets(line, sizeof(line), file)) while (fgets(line, sizeof(line), file))
{ {
// 去除换行符 // 去除换行符
@@ -79,6 +79,25 @@ void load_game_config()
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1; 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); fclose(file);
@@ -114,6 +133,16 @@ void save_game_config()
fprintf(file, "\n# AI难度 (1-5)\n"); fprintf(file, "\n# AI难度 (1-5)\n");
fprintf(file, "AI_DIFFICULTY=%d\n", ai_difficulty); 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); fclose(file);
printf("配置保存完成\n"); printf("配置保存完成\n");
} }
@@ -132,5 +161,13 @@ void reset_to_default_config()
ai_difficulty = 3; // 默认AI难度 ai_difficulty = 3; // 默认AI难度
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1; 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"); printf("已重置为默认配置\n");
} }
+6
View File
@@ -26,6 +26,12 @@ int network_timeout = NETWORK_TIMEOUT_MS; // 网络超时时间
double defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT; // 防守系数 double defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT; // 防守系数
int ai_difficulty = 3; // AI难度 (1-5) 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}; // 网络游戏状态 NetworkGameState network_state = {0}; // 网络游戏状态
View File
View File
+210 -51
View File
@@ -6,6 +6,10 @@
#include "ai.h" #include "ai.h"
#include "record.h" #include "record.h"
#include "network.h" #include "network.h"
#include "llm_ai.h"
#ifdef _WIN32
#include <windows.h>
#endif
#include <iup.h> #include <iup.h>
#include <iupdraw.h> #include <iupdraw.h>
#include <stdio.h> #include <stdio.h>
@@ -13,6 +17,7 @@
#include <string.h> #include <string.h>
static Ihandle *timer = NULL; // 网络轮询定时器 static Ihandle *timer = NULL; // 网络轮询定时器
static Ihandle *llm_timer = NULL; // LLM异步轮询定时器
/** /**
* @brief * @brief
@@ -72,24 +77,173 @@ static int timer_cb(Ihandle *ih)
return IUP_DEFAULT; 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 * @brief ACTION
*/ */
int action_cb(Ihandle *ih) int action_cb(Ihandle *ih)
{ {
IupDrawBegin(ih); HWND hwnd = (HWND)IupGetAttribute(ih, "WID");
if (!hwnd)
return IUP_DEFAULT;
int w, h; HDC hdc = GetDC(hwnd);
IupGetIntInt(ih, "DRAWSIZE", &w, &h); if (!hdc)
return IUP_DEFAULT;
set_draw_color(ih, 240, 217, 181); // 棋盘背景色 (木纹色近似) RECT rc;
IupSetAttribute(ih, "DRAWSTYLE", "FILL"); GetClientRect(hwnd, &rc);
IupDrawRectangle(ih, 0, 0, w, h);
draw_board_iup(ih); // 预创建所有 GDI 对象(避免循环内反复创建销毁)
draw_stones_iup(ih); 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; return IUP_DEFAULT;
} }
@@ -163,14 +317,20 @@ int btn_save_cb(Ihandle *ih)
else else
base_name = filename; 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) 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 else
{ {
sprintf(status_message, "保存失败"); snprintf(status_message, sizeof(status_message), "保存失败");
} }
update_ui_labels(); update_ui_labels();
} }
@@ -185,23 +345,29 @@ int btn_save_cb(Ihandle *ih)
int btn_back_cb(Ihandle *ih) int btn_back_cb(Ihandle *ih)
{ {
(void)ih; (void)ih;
printf("DEBUG: Back to Menu clicked\n");
// 如果是网络模式,断开连接 // 停止所有定时器
if (gui_game_mode == 3)
{
disconnect_network();
if (timer) if (timer)
{ {
IupSetAttribute(timer, "RUN", "NO"); IupSetAttribute(timer, "RUN", "NO");
IupDestroy(timer); IupDestroy(timer);
timer = NULL; timer = NULL;
} }
if (llm_timer)
{
IupSetAttribute(llm_timer, "RUN", "NO");
IupDestroy(llm_timer);
llm_timer = NULL;
}
// 如果是网络模式,彻底清理网络资源
if (gui_game_mode == 3)
{
cleanup_network();
} }
// 1. 先显示主菜单 // 1. 先显示主菜单
show_main_menu(); show_main_menu();
printf("DEBUG: Main menu shown\n");
// 2. 销毁游戏窗口 // 2. 销毁游戏窗口
if (dlg) if (dlg)
@@ -209,7 +375,6 @@ int btn_back_cb(Ihandle *ih)
Ihandle *old_dlg = dlg; Ihandle *old_dlg = dlg;
dlg = NULL; // 先清除全局指针 dlg = NULL; // 先清除全局指针
IupDestroy(old_dlg); IupDestroy(old_dlg);
printf("DEBUG: Destroyed game window\n");
} }
return IUP_IGNORE; // 返回 IUP_IGNORE 以阻止默认处理 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 else // PvE
{ {
current_player_gui = AI; current_player_gui = AI;
sprintf(status_message, "AI思考中...");
update_ui_labels(); update_ui_labels();
IupUpdate(ih); // 立即更新显示 IupUpdate(ih); // 立即更新显示
IupFlush(); // 强制刷新事件队列 IupFlush(); // 强制刷新事件队列
// AI 回合 if (llm_use)
ai_move(ai_difficulty);
Step last_step = steps[step_count - 1];
if (check_win(last_step.x, last_step.y, AI))
{ {
game_over = 1; // 大模型AI - 异步调用,不阻塞UI
sprintf(status_message, "AI获胜!"); sprintf(status_message, "AI思考中(大模型)...");
IupMessage("游戏结束", "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 else
{ {
current_player_gui = PLAYER; // 算法AI - 同步调用
sprintf(status_message, "轮到玩家"); 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() void create_game_window()
{ {
printf("DEBUG: create_game_window start\n");
if (dlg) if (dlg)
{ {
IupDestroy(dlg); IupDestroy(dlg);
@@ -349,10 +521,10 @@ void create_game_window()
if (!board_canvas) if (!board_canvas)
printf("ERROR: Failed to create board_canvas\n"); printf("ERROR: Failed to create board_canvas\n");
IupSetAttribute(board_canvas, "ACTION", "action_cb");
IupSetCallback(board_canvas, "ACTION", (Icallback)action_cb); IupSetCallback(board_canvas, "ACTION", (Icallback)action_cb);
IupSetCallback(board_canvas, "BUTTON_CB", (Icallback)button_cb); IupSetCallback(board_canvas, "BUTTON_CB", (Icallback)button_cb);
IupSetCallback(board_canvas, "K_ANY", (Icallback)k_any_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; 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); sprintf(size, "%dx%d", board_pixel_size, board_pixel_size);
IupSetAttribute(board_canvas, "RASTERSIZE", size); IupSetAttribute(board_canvas, "RASTERSIZE", size);
IupSetAttribute(board_canvas, "EXPAND", "NO"); IupSetAttribute(board_canvas, "EXPAND", "NO");
IupSetAttribute(board_canvas, "BORDER", "NO");
IupSetAttribute(board_canvas, "BGCOLOR", "240 217 181");
// 创建标签 (玩家信息和游戏状态) // 创建标签 (玩家信息和游戏状态)
lbl_player = IupLabel("当前玩家: 黑子"); lbl_player = IupLabel("当前玩家: 黑子");
@@ -434,8 +608,6 @@ void create_game_window()
// 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单 // 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单
IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb); IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb);
printf("DEBUG: create_game_window end\n");
} }
void start_pvp_game_gui() void start_pvp_game_gui()
@@ -448,6 +620,7 @@ void start_pvp_game_gui()
create_game_window(); create_game_window();
IupShowXY(dlg, IUP_CENTER, IUP_CENTER); IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
IupFlush(); // 确保窗口完全映射
sprintf(status_message, "玩家对战模式 - 黑方先行"); sprintf(status_message, "玩家对战模式 - 黑方先行");
update_ui_labels(); update_ui_labels();
if (board_canvas) if (board_canvas)
@@ -456,56 +629,44 @@ void start_pvp_game_gui()
void start_pve_game_gui() void start_pve_game_gui()
{ {
printf("DEBUG: start_pve_game_gui start\n");
gui_game_mode = 1; gui_game_mode = 1;
// ai_difficulty 是全局变量
empty_board(); empty_board();
current_player_gui = PLAYER; current_player_gui = PLAYER;
game_over = 0; game_over = 0;
create_game_window(); create_game_window();
printf("DEBUG: create_game_window returned\n");
if (dlg) if (dlg)
{ {
IupShowXY(dlg, IUP_CENTER, IUP_CENTER); IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
printf("DEBUG: IupShowXY called\n"); IupFlush();
} }
else else
{ {
printf("ERROR: dlg is NULL in start_pve_game_gui\n");
return; return;
} }
sprintf(status_message, "人机对战模式 - 玩家执黑先行"); sprintf(status_message, "人机对战模式 - 玩家执黑先行");
update_ui_labels(); update_ui_labels();
printf("DEBUG: update_ui_labels returned\n");
// 强制初始重绘
if (board_canvas) if (board_canvas)
{
IupUpdate(board_canvas); IupUpdate(board_canvas);
}
printf("DEBUG: start_pve_game_gui end\n");
} }
void start_network_game_gui() void start_network_game_gui()
{ {
printf("DEBUG: start_network_game_gui start\n");
gui_game_mode = 3; gui_game_mode = 3;
empty_board(); empty_board();
// 主机执黑先行
current_player_gui = PLAYER1; current_player_gui = PLAYER1;
game_over = 0; game_over = 0;
create_game_window(); create_game_window();
printf("DEBUG: create_game_window returned\n");
if (dlg) if (dlg)
{ {
IupShowXY(dlg, IUP_CENTER, IUP_CENTER); IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
printf("DEBUG: IupShowXY called\n"); IupFlush();
} }
if (network_state.is_server) if (network_state.is_server)
@@ -526,6 +687,4 @@ void start_network_game_gui()
IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb); IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb);
IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次 IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次
IupSetAttribute(timer, "RUN", "YES"); 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) static int btn_pvp_cb(Ihandle *ih)
{ {
(void)ih; (void)ih;
printf("DEBUG: Starting PvP Game\n");
// hide_main_menu(); // DO NOT HIDE MAIN MENU YET
start_pvp_game_gui(); start_pvp_game_gui();
IupHide(menu_dlg); // Hide main menu manually AFTER game window created IupHide(menu_dlg);
return IUP_DEFAULT; return IUP_DEFAULT;
} }
static int btn_pve_cb(Ihandle *ih) static int btn_pve_cb(Ihandle *ih)
{ {
(void)ih; (void)ih;
printf("DEBUG: Starting PvE Game\n");
// hide_main_menu(); // DO NOT HIDE MAIN MENU YET
start_pve_game_gui(); start_pve_game_gui();
IupHide(menu_dlg); // Hide main menu manually AFTER game window created IupHide(menu_dlg);
return IUP_DEFAULT; return IUP_DEFAULT;
} }
@@ -87,7 +83,6 @@ static int btn_network_cancel_cb(Ihandle *ih)
static int btn_network_cb(Ihandle *ih) static int btn_network_cb(Ihandle *ih)
{ {
(void)ih; (void)ih;
printf("DEBUG: Opening Network Menu\n");
Ihandle *txt_ip = IupText(NULL); Ihandle *txt_ip = IupText(NULL);
IupSetAttribute(txt_ip, "NAME", "NET_IP"); IupSetAttribute(txt_ip, "NAME", "NET_IP");
@@ -135,10 +130,8 @@ static int btn_network_cb(Ihandle *ih)
static int btn_replay_cb(Ihandle *ih) static int btn_replay_cb(Ihandle *ih)
{ {
(void)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(); start_replay_gui();
IupHide(menu_dlg); // Hide main menu IupHide(menu_dlg);
return IUP_DEFAULT; return IUP_DEFAULT;
} }
@@ -180,6 +173,36 @@ static int btn_save_settings_cb(Ihandle *ih)
ai_difficulty = ai_level; ai_difficulty = ai_level;
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1; 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 config
save_game_config(); save_game_config();
@@ -251,6 +274,42 @@ static int btn_settings_cb(Ihandle *ih)
IupSetInt(lst_ai, "VALUE", ai_difficulty); IupSetInt(lst_ai, "VALUE", ai_difficulty);
IupSetAttribute(lst_ai, "SIZE", "80x"); 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 // Buttons
Ihandle *btn_save = IupButton("保存", NULL); Ihandle *btn_save = IupButton("保存", NULL);
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb); 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, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_ai, "GAP", "10"); 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); Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
IupSetAttribute(hbox_btns, "GAP", "20"); IupSetAttribute(hbox_btns, "GAP", "20");
IupSetAttribute(hbox_btns, "MARGIN", "10x0"); IupSetAttribute(hbox_btns, "MARGIN", "10x0");
@@ -285,11 +361,17 @@ static int btn_settings_cb(Ihandle *ih)
hbox_time, hbox_time,
hbox_ai, hbox_ai,
IupLabel(NULL), // Spacer IupLabel(NULL), // Spacer
lbl_llm_sep,
hbox_ai_mode,
hbox_endpoint,
hbox_apikey,
hbox_model,
IupLabel(NULL), // Spacer
hbox_btns, hbox_btns,
NULL); NULL);
IupSetAttribute(vbox, "GAP", "15"); IupSetAttribute(vbox, "GAP", "10");
IupSetAttribute(vbox, "MARGIN", "30x30"); IupSetAttribute(vbox, "MARGIN", "20x20");
Ihandle *dlg = IupDialog(vbox); Ihandle *dlg = IupDialog(vbox);
IupSetAttribute(dlg, "TITLE", "游戏设置"); IupSetAttribute(dlg, "TITLE", "游戏设置");
@@ -315,7 +397,6 @@ void create_main_menu()
{ {
if (menu_dlg) if (menu_dlg)
return; return;
printf("DEBUG: create_main_menu\n");
Ihandle *lbl_title = IupLabel("五子棋 (Gobang)"); Ihandle *lbl_title = IupLabel("五子棋 (Gobang)");
IupSetAttribute(lbl_title, "FONT", "SimHei, 24"); IupSetAttribute(lbl_title, "FONT", "SimHei, 24");
@@ -377,14 +458,9 @@ void create_main_menu()
void show_main_menu() void show_main_menu()
{ {
printf("DEBUG: show_main_menu start\n");
if (!menu_dlg) if (!menu_dlg)
{
printf("DEBUG: Creating main menu\n");
create_main_menu(); create_main_menu();
}
IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER); IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER);
printf("DEBUG: show_main_menu end\n");
} }
void hide_main_menu() 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);
}
+62 -8
View File
@@ -17,13 +17,19 @@
/** /**
* @brief * @brief
*/ */
static bool enet_initialized = false;
bool init_network() bool init_network()
{ {
if (!enet_initialized)
{
if (enet_initialize() != 0) if (enet_initialize() != 0)
{ {
printf("An error occurred while initializing ENet.\n"); printf("An error occurred while initializing ENet.\n");
return false; return false;
} }
enet_initialized = true;
}
memset(&network_state, 0, sizeof(NetworkGameState)); memset(&network_state, 0, sizeof(NetworkGameState));
network_state.port = DEFAULT_NETWORK_PORT; network_state.port = DEFAULT_NETWORK_PORT;
@@ -44,8 +50,13 @@ void cleanup_network()
network_state.host = NULL; network_state.host = NULL;
} }
if (enet_initialized)
{
enet_deinitialize(); enet_deinitialize();
network_state.is_connected = false; enet_initialized = false;
}
memset(&network_state, 0, sizeof(NetworkGameState));
} }
/** /**
@@ -53,6 +64,9 @@ void cleanup_network()
*/ */
bool create_server(int port) bool create_server(int port)
{ {
if (!init_network())
return false;
ENetAddress address; ENetAddress address;
// 绑定所有接口 // 绑定所有接口
@@ -117,6 +131,9 @@ bool create_server(int port)
*/ */
bool connect_to_server(const char *ip, int port) bool connect_to_server(const char *ip, int port)
{ {
if (!init_network())
return false;
// 创建客户端主机 // 创建客户端主机
network_state.host = (void *)enet_host_create(NULL, // 创建客户端 network_state.host = (void *)enet_host_create(NULL, // 创建客户端
1, // 仅允许1个传出连接 1, // 仅允许1个传出连接
@@ -286,17 +303,54 @@ bool is_network_connected()
} }
/** /**
* @brief IP地址 * @brief IP地址
*/ */
bool get_local_ip(char *ip_buffer, int buffer_size) bool get_local_ip(char *ip_buffer, int buffer_size)
{ {
// ENet 没有直接获取本机局域网 IP 的简单跨平台函数。 #ifdef _WIN32
// 这里我们可以回退到原生 socket 方法,或者简单返回本地回环。 // 使用 Winsock 获取本机 IPws2_32 已链接)
// 为了不引入额外的系统头文件,暂时返回通用提示。 char hostname[256];
// 在真实应用中,可以保留之前的 gethostname/gethostbyname 逻辑。 if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR)
strncpy(ip_buffer, "查看本机网络适配器", buffer_size - 1); {
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0'; ip_buffer[buffer_size - 1] = '\0';
return true; // 总是返回 true 以允许服务器继续启动 return false;
}
struct hostent *host = gethostbyname(hostname);
if (host == NULL || host->h_addr_list[0] == NULL)
{
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return false;
}
// 遍历所有地址,找一个非回环的 IPv4 地址
for (int i = 0; host->h_addr_list[i] != NULL; i++)
{
struct in_addr addr;
memcpy(&addr, host->h_addr_list[i], sizeof(struct in_addr));
const char *ip = inet_ntoa(addr);
if (ip && strcmp(ip, "127.0.0.1") != 0)
{
strncpy(ip_buffer, ip, buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true;
}
}
// 没找到非回环地址,返回第一个
struct in_addr addr;
memcpy(&addr, host->h_addr_list[0], sizeof(struct in_addr));
const char *ip = inet_ntoa(addr);
strncpy(ip_buffer, ip ? ip : "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true;
#else
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true;
#endif
} }
/** /**
+1 -1
View File
@@ -261,7 +261,7 @@ int load_game_from_file(const char *filename)
FILE *file = fopen(fullpath, "r"); FILE *file = fopen(fullpath, "r");
if (!file) if (!file)
{ {
return false; return 0;
} }
// 跳过CSV文件头部行 // 跳过CSV文件头部行