6 Commits

Author SHA1 Message Date
Serendipity c6053585d4 fix(gui): 增大主菜单窗口尺寸防止内容截断
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 16:02:59 +08:00
Serendipity e41e856fe0 feat(gui): 经典木纹风格 UI 视觉美化
- 配色方案:暖木色棋盘、米白窗口背景、深棕按钮,统一视觉语言
- 主菜单:IupFrame 分组(选择模式/功能),主按钮深棕底白字
- 棋盘渲染:渐变棋子(3层同心圆模拟立体感)、坐标标注(A-O/1-15)、
  蓝色圆环最后落子标记、深色棋盘边框、加大星位天元
- 侧边面板:IupFrame 包裹对局信息,按钮统一样式
- 设置页面:IupFrame 分组(基本/AI/大模型),按钮样式统一
- 网络/复盘对话框:IupFrame 分组,配色和按钮样式统一
- 全局字体:SimHei 11 作为默认字体

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 15:57:06 +08:00
Serendipity bf21efbbc0 rename: ENet 库目录重命名为 libs/ENET
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 15:41:48 +08:00
Serendipity b616a4662b rename: IUP 库目录重命名为 libs/IUP
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 15:39:42 +08:00
Serendipity 96a94aaddf 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>
2026-05-02 15:32:54 +08:00
Serendipity f897536a45 refactor(config): 更新时间限制为30分钟并调整配置文件存储格式
refactor(globals): 移除不必要的GUI运行状态标志
refactor(gui): 更新注释以反映使用IUP库
refactor(ai): 修改威胁检测逻辑以提升威胁等级
refactor(config): 修改加载和保存配置的时间限制逻辑
refactor(network): 更新默认网络端口常量
refactor(record): 移除冗余注释并增强复盘步骤的合法性检查
2026-05-02 12:24:27 +08:00
93 changed files with 5758 additions and 1313 deletions
+7
View File
@@ -35,6 +35,13 @@ dist/
# 编译生成的对象文件
obj/
build/
# 临时游戏存档
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)
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);
/**
* @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
+58 -13
View File
@@ -34,7 +34,7 @@
//---------- 游戏设置默认值 ----------//
#define DEFAULT_USE_FORBIDDEN_MOVES false // 默认不启用禁手规则
#define DEFAULT_USE_TIMER 0 // 默认不启用计时器
#define DEFAULT_TIME_LIMIT 30 // 默认时间限制为30(内部存储)
#define DEFAULT_TIME_LIMIT 1800 // 默认时间限制为30分钟(内部以秒存储: 30*60)
//---------- AI参数 ----------//
#define DEFAULT_AI_DEPTH 5 // 默认AI搜索深度
@@ -48,8 +48,6 @@
#define NETWORK_BUFFER_SIZE 1024 // 网络缓冲区大小
// 网络配置
#define DEFAULT_PORT 8888 // 默认端口(与DEFAULT_NETWORK_PORT保持一致)
#define BUFFER_SIZE 1024 // 缓冲区大小(与NETWORK_BUFFER_SIZE保持一致)
#define MAX_IP_LENGTH 16 // 最大IP地址长度
// 网络消息类型
@@ -65,7 +63,7 @@
//---------- 评分参数 ----------//
// 棋型评分 - 用于calculate_step_score函数
#define SCORE_FIVE 0 // 五连
#define SCORE_FIVE 5000 // 五连
#define SCORE_LIVE_FOUR 2000 // 活四
#define SCORE_RUSH_FOUR 1000 // 冲四
#define SCORE_DEAD_FOUR 300 // 死四
@@ -120,23 +118,70 @@
// 窗口和棋盘配置
#define WINDOW_WIDTH 1000
#define WINDOW_HEIGHT 800
#define BOARD_OFFSET_X 50
#define BOARD_OFFSET_Y 50
#define BOARD_OFFSET_X 45
#define BOARD_OFFSET_Y 45
#define CELL_SIZE 30
#define STONE_RADIUS 12
#define STONE_RADIUS 13
// 颜色定义
#define GUI_COLOR_BACKGROUND {240, 217, 181, 255}
#define GUI_COLOR_BOARD_LINE {0, 0, 0, 255}
#define GUI_COLOR_BLACK_STONE {0, 0, 0, 255}
#define GUI_COLOR_WHITE_STONE {255, 255, 255, 255}
#define GUI_COLOR_STONE_BORDER {100, 100, 100, 255}
// 配色方案 - 经典木纹风格 (RGB)
#define CLR_BOARD_BG_R 0xD4 // 棋盘背景 - 暖木色
#define CLR_BOARD_BG_G 0xA5
#define CLR_BOARD_BG_B 0x74
#define CLR_BOARD_BORDER_R 0x8B // 棋盘边框 - 深木色
#define CLR_BOARD_BORDER_G 0x5E
#define CLR_BOARD_BORDER_B 0x3C
#define CLR_WINDOW_BG "245 240 235" // 窗口背景 - 米白
#define CLR_PANEL_BG "237 229 218" // 面板背景 - 浅米色
#define CLR_GRID_LINE_R 0x5C // 网格线 - 深棕
#define CLR_GRID_LINE_G 0x3D
#define CLR_GRID_LINE_B 0x2E
#define CLR_TEXT_TITLE "60 36 21" // 标题文字 - 深棕
#define CLR_TEXT_NORMAL "74 55 40" // 正文文字 - 中棕
#define CLR_TEXT_SECONDARY "139 115 85" // 次要文字 - 浅棕
#define CLR_BTN_PRIMARY_BG "139 94 60" // 主按钮背景 - 深棕
#define CLR_BTN_PRIMARY_FG "255 255 255" // 主按钮文字 - 白色
#define CLR_BTN_NORMAL_BG "237 229 218" // 普通按钮背景 - 浅棕
#define CLR_BTN_NORMAL_FG "107 66 38" // 普通按钮文字 - 深棕
#define CLR_LAST_MOVE_R 0x2E // 最后落子标记 - 蓝色
#define CLR_LAST_MOVE_G 0x86
#define CLR_LAST_MOVE_B 0xAB
#define CLR_STAR_POINT_R 0x3C // 星位/天元 - 深棕
#define CLR_STAR_POINT_G 0x24
#define CLR_STAR_POINT_B 0x15
// 黑子渐变色
#define CLR_BLACK_STONE_R 0x10 // 黑子外圈
#define CLR_BLACK_STONE_G 0x10
#define CLR_BLACK_STONE_B 0x10
#define CLR_BLACK_HIGHLIGHT_R 0x50 // 黑子高光
#define CLR_BLACK_HIGHLIGHT_G 0x50
#define CLR_BLACK_HIGHLIGHT_B 0x50
// 白子渐变色
#define CLR_WHITE_STONE_R 0xF0 // 白子外圈
#define CLR_WHITE_STONE_G 0xF0
#define CLR_WHITE_STONE_B 0xF0
#define CLR_WHITE_HIGHLIGHT_R 0xFF // 白子高光
#define CLR_WHITE_HIGHLIGHT_G 0xFF
#define CLR_WHITE_HIGHLIGHT_B 0xFF
#define CLR_WHITE_BORDER_R 0xA0 // 白子边框
#define CLR_WHITE_BORDER_G 0xA0
#define CLR_WHITE_BORDER_B 0xA0
//---------- 文件路径参数 ----------//
#define RECORDS_DIR "records" // 记录文件目录
#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 加载游戏配置
+6 -2
View File
@@ -29,12 +29,16 @@ 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; // 网络游戏状态
// ==================== GUI相关变量 ====================
// Raylib 不需要暴露窗口和渲染器指针
extern int gui_running; // GUI运行状态标志
extern int current_player_gui; // GUI当前玩家
extern int game_over; // 游戏结束标志
extern char status_message[256]; // 状态消息
+3 -3
View File
@@ -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
+3 -39
View File
@@ -1,9 +1,8 @@
/**
* @file gui.h
* @brief 图形化用户界面头文件
* @note 使用Raylib库实现五子棋的图形化界面
* @note 使用IUP库实现五子棋的图形化界面
* @author 刘航宇
* @date 2025-01-15
*/
#ifndef GUI_H
@@ -17,45 +16,16 @@
/**
* @brief 初始化GUI
* @details 初始化Raylib图形库和游戏界面组件
* @details 初始化IUP图形库和游戏界面组件
* @return 成功返回0,失败返回-1
*/
int init_gui();
/**
* @brief 清理GUI资源
* @details 关闭窗口
*/
void cleanup_gui();
/**
* @brief 渲染游戏画面
* @details 完整的游戏画面渲染流程
*/
void render_game();
/**
* @brief 处理事件
* @details 处理所有Raylib事件并执行相应操作
* @return 继续运行返回1,退出返回0
*/
int handle_events();
/**
* @brief 绘制棋盘
*/
void draw_board();
/**
* @brief 绘制棋子
*/
void draw_stones();
/**
* @brief 绘制UI元素
*/
void draw_ui_elements();
/**
* @brief 屏幕坐标转棋盘坐标
*/
@@ -81,16 +51,10 @@ void start_pve_game_gui();
*/
void start_replay_gui();
/**
* @brief 启动图形化界面模式
* @note 替代原来的 main 函数中的 GUI 分支逻辑
*/
int init_gui(); // Already declared
/**
* @brief 运行图形化界面模式
* @details 主循环处理事件、渲染画面和更新状态
*/
void run_gui_mode();
#endif // GUI_H
#endif // GUI_H
+2 -8
View File
@@ -8,15 +8,9 @@ extern Ihandle *dlg;
extern Ihandle *board_canvas;
extern Ihandle *lbl_player;
extern Ihandle *lbl_status;
extern int gui_loop_running;
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);
@@ -25,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);
+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 "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
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
+3 -145
View File
@@ -137,94 +137,6 @@ int evaluate_pos(int x, int y, int player)
return total_score + position_bonus; // 返回总评估分
}
/**
* @brief α-β()
* @param x
* @param y
* @param player
* @param depth
* @param alpha α()
* @param beta β()
* @param is_maximizing (AI)
* @return int
* @note :
* 1.
* 2.
* 3.
* 4. is_maximizing选择最大/
* 5. 使α-β
*/
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing)
{
// 检查当前落子是否获胜
if (check_win(x, y, player))
{
return (player == AI) ? SEARCH_WIN_BONUS + depth : -SEARCH_WIN_BONUS - depth;
}
// 达到搜索深度或平局
if (depth == 0 || step_count >= BOARD_SIZE * BOARD_SIZE)
{
return evaluate_pos(x, y, AI) - evaluate_pos(x, y, PLAYER);
}
int best_score = is_maximizing ? -1000000 : 1000000;
// 使用移动排序优化搜索效率
ScoredMove candidate_moves[BOARD_SIZE * BOARD_SIZE];
int move_count = generate_candidate_moves(candidate_moves, player);
// 限制搜索的候选移动数量以提高性能
int max_candidates = (depth >= 3) ? 15 : 25; // 深度越大,候选移动越少
if (move_count > max_candidates)
{
move_count = max_candidates;
}
// 遍历排序后的候选移动
for (int idx = 0; idx < move_count; idx++)
{
int i = candidate_moves[idx].x;
int j = candidate_moves[idx].y;
// 模拟当前玩家落子
board[i][j] = player;
step_count++;
// 递归搜索(切换玩家和搜索深度)
int current_score = dfs(i, j, (player == AI) ? PLAYER : AI, depth - 1, alpha, beta, !is_maximizing);
// 撤销落子
board[i][j] = EMPTY;
step_count--;
// 极大值玩家(AI)逻辑
if (is_maximizing)
{
best_score = (current_score > best_score) ? current_score : best_score;
alpha = (best_score > alpha) ? best_score : alpha;
// α剪枝
if (beta <= alpha)
{
break;
}
}
// 极小值玩家(人类)逻辑
else
{
best_score = (current_score < best_score) ? current_score : best_score;
beta = (best_score < beta) ? best_score : beta;
// β剪枝
if (beta <= alpha)
{
break;
}
}
}
return best_score;
}
/**
* @brief AI决策主函数使
* @note
@@ -532,8 +444,9 @@ ThreatLevel detect_threat(int x, int y, int player)
// 恢复棋盘
board[x][y] = EMPTY;
// 如果有多个威胁,提升威胁等级
if (threat_count >= 2 && max_threat >= THREAT_THREE)
// 如果有多个活三威胁,升级为双威胁(如双三、四三等组合)
// 注意:不能把已经是活四的威胁降级为双威胁
if (threat_count >= 2 && max_threat == THREAT_THREE)
{
max_threat = THREAT_DOUBLE;
}
@@ -541,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;
}
+44 -3
View File
@@ -23,7 +23,7 @@ void load_game_config()
return;
}
char line[256];
char line[512];
while (fgets(line, sizeof(line), file))
{
// 去除换行符
@@ -48,7 +48,11 @@ void load_game_config()
}
else if (strncmp(line, "TIME_LIMIT=", 11) == 0)
{
time_limit = atoi(line + 11);
int minutes = atoi(line + 11);
if (minutes > 0)
{
time_limit = minutes * 60; // 配置文件存分钟,内部转为秒
}
}
else if (strncmp(line, "NETWORK_PORT=", 13) == 0)
{
@@ -75,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);
@@ -101,7 +124,7 @@ void save_game_config()
fprintf(file, "\n# 计时器 (0=关闭, 1=开启)\n");
fprintf(file, "USE_TIMER=%d\n", use_timer);
fprintf(file, "\n# 时间限制 (分钟)\n");
fprintf(file, "TIME_LIMIT=%d\n", time_limit);
fprintf(file, "TIME_LIMIT=%d\n", time_limit / 60); // 内部存秒,配置文件存分钟
fprintf(file, "\n# 网络端口 (范围: %d-%d)\n", MIN_NETWORK_PORT, MAX_NETWORK_PORT);
fprintf(file, "NETWORK_PORT=%d\n", network_port);
fprintf(file, "\n# 网络超时时间 (毫秒)\n");
@@ -110,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");
}
@@ -128,5 +161,13 @@ void reset_to_default_config()
ai_difficulty = 3; // 默认AI难度
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
llm_use = DEFAULT_LLM_USE;
strncpy(llm_endpoint, DEFAULT_LLM_ENDPOINT, MAX_LLM_ENDPOINT_LEN - 1);
llm_endpoint[MAX_LLM_ENDPOINT_LEN - 1] = '\0';
strncpy(llm_api_key, DEFAULT_LLM_API_KEY, MAX_LLM_API_KEY_LEN - 1);
llm_api_key[MAX_LLM_API_KEY_LEN - 1] = '\0';
strncpy(llm_model, DEFAULT_LLM_MODEL, MAX_LLM_MODEL_LEN - 1);
llm_model[MAX_LLM_MODEL_LEN - 1] = '\0';
printf("已重置为默认配置\n");
}
+6 -1
View File
@@ -26,11 +26,16 @@ 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}; // 网络游戏状态
// ==================== GUI相关变量定义 ====================
int gui_running = 1; // GUI运行状态标志
int current_player_gui = PLAYER; // GUI当前玩家
int game_over = 0; // 游戏结束标志
char status_message[256] = "五子棋游戏 - 黑子先行"; // 状态消息
View File
-1
View File
@@ -25,7 +25,6 @@ int main(int argc, char *argv[])
{
// 设置控制台编码为UTF-8
#ifdef _WIN32
system("chcp 65001 > nul"); // 设置控制台编码为UTF-8
SetConsoleOutputCP(65001); // 设置控制台输出编码
SetConsoleCP(65001); // 设置控制台输入编码
_mkdir("records");
+3 -3
View File
@@ -11,7 +11,6 @@ Ihandle *dlg = NULL;
Ihandle *board_canvas = NULL;
Ihandle *lbl_player = NULL;
Ihandle *lbl_status = NULL;
int gui_loop_running = 0;
int gui_game_mode = 0; // 0: PvP, 1: PvE, 2: Replay, 3: Network
int replay_total_steps = 0; // 复盘总步数
@@ -29,11 +28,12 @@ int init_gui()
// 启用UTF-8模式,确保中文正常显示
IupSetGlobal("UTF8MODE", "YES");
// 设置全局默认字体
IupSetGlobal("DEFAULTFONT", "SimHei, 11");
create_main_menu();
show_main_menu();
gui_loop_running = 1;
printf("图形化界面初始化成功!(IUP)\n");
return 0;
}
+783
View File
@@ -0,0 +1,783 @@
#include "gui.h"
#include "gui_internal.h"
#include "gui_menu.h"
#include "globals.h"
#include "gobang.h"
#include "ai.h"
#include "record.h"
#include "network.h"
#include "llm_ai.h"
#ifdef _WIN32
#include <windows.h>
#endif
#include <iup.h>
#include <iupdraw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static Ihandle *timer = NULL; // 网络轮询定时器
static Ihandle *llm_timer = NULL; // LLM异步轮询定时器
/**
* @brief 网络事件轮询回调
*/
static int timer_cb(Ihandle *ih)
{
(void)ih;
if (gui_game_mode != 3 || game_over)
return IUP_DEFAULT;
NetworkMessage msg;
// 非阻塞接收消息
if (receive_network_message(&msg, 0))
{
if (msg.type == MSG_MOVE)
{
int bx = msg.x;
int by = msg.y;
int pid = msg.player_id;
if (have_space(bx, by))
{
player_move(bx, by, pid);
if (check_win(bx, by, pid))
{
game_over = 1;
sprintf(status_message, "对手获胜!");
IupMessage("游戏结束", "对手获胜!");
}
else
{
current_player_gui = network_state.local_player_id;
sprintf(status_message, "轮到你落子");
}
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
}
else if (msg.type == MSG_DISCONNECT || msg.type == MSG_SURRENDER)
{
game_over = 1;
sprintf(status_message, "对手已断开连接/认输");
IupMessage("游戏结束", "对手已退出游戏,你赢了!");
update_ui_labels();
}
}
if (!is_network_connected() && !game_over)
{
game_over = 1;
sprintf(status_message, "与服务器断开连接");
IupMessage("错误", "网络连接已断开");
update_ui_labels();
}
return IUP_DEFAULT;
}
/**
* @brief 处理AI落子结果(LLM或算法)
*/
static void process_ai_move_result(void)
{
Step last_step = steps[step_count - 1];
if (check_win(last_step.x, last_step.y, AI))
{
game_over = 1;
sprintf(status_message, "AI获胜!");
IupMessage("游戏结束", "AI获胜!");
}
else
{
current_player_gui = PLAYER;
sprintf(status_message, "轮到玩家");
}
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
/**
* @brief LLM异步轮询定时器回调
*/
static int llm_timer_cb(Ihandle *ih)
{
(void)ih;
int x, y;
int result = llm_ai_poll_result(&x, &y);
if (result == 0)
return IUP_DEFAULT; // 仍在思考
// 停止轮询定时器
if (llm_timer)
{
IupSetAttribute(llm_timer, "RUN", "NO");
}
if (result == 1 && x >= 0 && y >= 0 && player_move(x, y, AI))
{
// LLM成功且落子合法
}
else
{
// LLM失败或坐标非法,回退到算法AI
if (result == 1)
snprintf(status_message, sizeof(status_message), "大模型返回非法位置,使用算法AI");
else
snprintf(status_message, sizeof(status_message), "大模型响应失败,使用算法AI");
update_ui_labels();
ai_move(ai_difficulty);
}
process_ai_move_result();
return IUP_DEFAULT;
}
/**
* @brief MAP_CB 回调:Canvas映射后强制重绘
*/
static int map_cb(Ihandle *ih)
{
(void)ih;
IupUpdate(board_canvas);
return IUP_DEFAULT;
}
/**
* @brief ACTION 回调:负责重绘(经典木纹风格)
*/
int action_cb(Ihandle *ih)
{
HWND hwnd = (HWND)IupGetAttribute(ih, "WID");
if (!hwnd)
return IUP_DEFAULT;
HDC hdc = GetDC(hwnd);
if (!hdc)
return IUP_DEFAULT;
RECT rc;
GetClientRect(hwnd, &rc);
// === 预创建所有 GDI 对象 ===
HBRUSH bg_brush = CreateSolidBrush(RGB(CLR_BOARD_BG_R, CLR_BOARD_BG_G, CLR_BOARD_BG_B));
HBRUSH black_outer = CreateSolidBrush(RGB(CLR_BLACK_STONE_R, CLR_BLACK_STONE_G, CLR_BLACK_STONE_B));
HBRUSH black_core = CreateSolidBrush(RGB(0, 0, 0));
HBRUSH black_hl = CreateSolidBrush(RGB(CLR_BLACK_HIGHLIGHT_R, CLR_BLACK_HIGHLIGHT_G, CLR_BLACK_HIGHLIGHT_B));
HBRUSH white_outer = CreateSolidBrush(RGB(CLR_WHITE_BORDER_R, CLR_WHITE_BORDER_G, CLR_WHITE_BORDER_B));
HBRUSH white_core = CreateSolidBrush(RGB(CLR_WHITE_STONE_R, CLR_WHITE_STONE_G, CLR_WHITE_STONE_B));
HBRUSH white_hl = CreateSolidBrush(RGB(CLR_WHITE_HIGHLIGHT_R, CLR_WHITE_HIGHLIGHT_G, CLR_WHITE_HIGHLIGHT_B));
HBRUSH star_brush = CreateSolidBrush(RGB(CLR_STAR_POINT_R, CLR_STAR_POINT_G, CLR_STAR_POINT_B));
HPEN grid_pen = CreatePen(PS_SOLID, 1, RGB(CLR_GRID_LINE_R, CLR_GRID_LINE_G, CLR_GRID_LINE_B));
HPEN border_pen = CreatePen(PS_SOLID, 2, RGB(CLR_BOARD_BORDER_R, CLR_BOARD_BORDER_G, CLR_BOARD_BORDER_B));
HPEN last_move_pen = CreatePen(PS_SOLID, 2, RGB(CLR_LAST_MOVE_R, CLR_LAST_MOVE_G, CLR_LAST_MOVE_B));
HPEN null_pen = CreatePen(PS_NULL, 0, RGB(0, 0, 0));
HFONT hfont_coord = CreateFont(12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, 0, 0, 0, 0, "SimHei");
// 1. 填充背景
FillRect(hdc, &rc, bg_brush);
// 2. 绘制棋盘边框(深色外框)
HPEN prev_pen = (HPEN)SelectObject(hdc, border_pen);
HBRUSH prev_brush = (HBRUSH)SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
int grid_left = BOARD_OFFSET_X;
int grid_top = BOARD_OFFSET_Y;
int grid_right = BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE;
int grid_bottom = BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE;
Rectangle(hdc, grid_left - 2, grid_top - 2, grid_right + 3, grid_bottom + 3);
// 3. 绘制棋盘网格
SelectObject(hdc, grid_pen);
SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
for (int i = 0; i < BOARD_SIZE; i++)
{
MoveToEx(hdc, grid_left, BOARD_OFFSET_Y + i * CELL_SIZE, NULL);
LineTo(hdc, grid_right, BOARD_OFFSET_Y + i * CELL_SIZE);
MoveToEx(hdc, BOARD_OFFSET_X + i * CELL_SIZE, grid_top, NULL);
LineTo(hdc, BOARD_OFFSET_X + i * CELL_SIZE, grid_bottom);
}
// 4. 星位/天元(实心圆)
SelectObject(hdc, null_pen);
SelectObject(hdc, star_brush);
if (BOARD_SIZE == 15)
{
int stars[] = {3, 7, 11};
for (int si = 0; si < 3; si++)
for (int sj = 0; sj < 3; sj++)
{
int cx = BOARD_OFFSET_X + stars[si] * CELL_SIZE;
int cy = BOARD_OFFSET_Y + stars[sj] * CELL_SIZE;
Ellipse(hdc, cx - 4, cy - 4, cx + 5, cy + 5);
}
}
else if (BOARD_SIZE >= 9)
{
int cx = BOARD_OFFSET_X + (BOARD_SIZE / 2) * CELL_SIZE;
int cy = BOARD_OFFSET_Y + (BOARD_SIZE / 2) * CELL_SIZE;
Ellipse(hdc, cx - 4, cy - 4, cx + 5, cy + 5);
}
// 5. 绘制坐标标注
{
HFONT prev_font = (HFONT)SelectObject(hdc, hfont_coord);
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, RGB(CLR_BOARD_BORDER_R, CLR_BOARD_BORDER_G, CLR_BOARD_BORDER_B));
// 列坐标 (A, B, C, ...)
for (int j = 0; j < BOARD_SIZE; j++)
{
char label[2] = {'A' + j, '\0'};
int tx = BOARD_OFFSET_X + j * CELL_SIZE - 4;
TextOut(hdc, tx, grid_top - 18, label, 1);
TextOut(hdc, tx, grid_bottom + 5, label, 1);
}
// 行坐标 (1, 2, 3, ...)
for (int i = 0; i < BOARD_SIZE; i++)
{
char label[4];
int len = snprintf(label, sizeof(label), "%d", i + 1);
int ty = BOARD_OFFSET_Y + i * CELL_SIZE - 7;
TextOut(hdc, grid_left - 18 - (len > 1 ? 4 : 0), ty, label, len);
TextOut(hdc, grid_right + 6, ty, label, len);
}
SelectObject(hdc, prev_font);
}
// 6. 绘制棋子(渐变效果:3层同心圆)
SelectObject(hdc, null_pen);
for (int i = 0; i < BOARD_SIZE; i++)
for (int j = 0; j < BOARD_SIZE; j++)
{
if (board[i][j] == EMPTY)
continue;
int cx = BOARD_OFFSET_X + j * CELL_SIZE;
int cy = BOARD_OFFSET_Y + i * CELL_SIZE;
if (board[i][j] == PLAYER)
{
// 黑子:外圈深灰 → 中圈黑 → 中心高光
SelectObject(hdc, black_outer);
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
SelectObject(hdc, black_core);
Ellipse(hdc, cx - STONE_RADIUS + 2, cy - STONE_RADIUS + 2,
cx + STONE_RADIUS - 1, cy + STONE_RADIUS - 1);
SelectObject(hdc, black_hl);
Ellipse(hdc, cx - 4, cy - 5, cx - 1, cy - 2);
}
else
{
// 白子:外圈灰边 → 中圈白 → 中心高光
SelectObject(hdc, white_outer);
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
SelectObject(hdc, white_core);
Ellipse(hdc, cx - STONE_RADIUS + 2, cy - STONE_RADIUS + 2,
cx + STONE_RADIUS - 1, cy + STONE_RADIUS - 1);
SelectObject(hdc, white_hl);
Ellipse(hdc, cx - 4, cy - 5, cx - 1, cy - 2);
}
}
// 7. 标记最后落子位置(蓝色圆环)
if (step_count > 0 && step_count <= MAX_STEPS)
{
Step last = steps[step_count - 1];
int cx = BOARD_OFFSET_X + last.y * CELL_SIZE;
int cy = BOARD_OFFSET_Y + last.x * CELL_SIZE;
SelectObject(hdc, last_move_pen);
SelectObject(hdc, (HBRUSH)GetStockObject(NULL_BRUSH));
Ellipse(hdc, cx - 5, cy - 5, cx + 6, cy + 6);
}
// 恢复原始 GDI 对象,然后清理
SelectObject(hdc, prev_pen);
SelectObject(hdc, prev_brush);
ReleaseDC(hwnd, hdc);
DeleteObject(bg_brush);
DeleteObject(black_outer);
DeleteObject(black_core);
DeleteObject(black_hl);
DeleteObject(white_outer);
DeleteObject(white_core);
DeleteObject(white_hl);
DeleteObject(star_brush);
DeleteObject(grid_pen);
DeleteObject(border_pen);
DeleteObject(last_move_pen);
DeleteObject(null_pen);
DeleteObject(hfont_coord);
return IUP_DEFAULT;
}
/**
* @brief 悔棋按钮回调
*/
int btn_undo_cb(Ihandle *ih)
{
(void)ih;
if (game_over)
return IUP_DEFAULT;
int steps_to_undo = 1;
if (gui_game_mode == 1) // PvE
{
steps_to_undo = 2; // 悔棋两步(玩家+AI
}
else if (gui_game_mode == 3) // Network
{
IupMessage("提示", "网络模式暂不支持悔棋");
return IUP_DEFAULT;
}
if (step_count >= steps_to_undo)
{
return_move(steps_to_undo);
// 更新当前玩家
if (step_count % 2 == 0)
current_player_gui = PLAYER;
else
current_player_gui = AI; // or PLAYER2
sprintf(status_message, "已悔棋");
update_ui_labels();
IupUpdate(board_canvas);
}
else
{
sprintf(status_message, "无法悔棋");
update_ui_labels();
}
return IUP_DEFAULT;
}
/**
* @brief 保存按钮回调
*/
int btn_save_cb(Ihandle *ih)
{
(void)ih;
Ihandle *file_dlg = IupFileDlg();
IupSetAttribute(file_dlg, "DIALOGTYPE", "SAVE");
IupSetAttribute(file_dlg, "TITLE", "保存游戏记录");
IupSetAttribute(file_dlg, "FILTER", "*.csv");
IupSetAttribute(file_dlg, "FILTERINFO", "CSV Files");
IupPopup(file_dlg, IUP_CENTER, IUP_CENTER);
if (IupGetInt(file_dlg, "STATUS") != -1)
{
char *filename = IupGetAttribute(file_dlg, "VALUE");
char *base_name = strrchr(filename, '\\');
if (!base_name)
base_name = strrchr(filename, '/');
if (base_name)
base_name++;
else
base_name = filename;
int mode;
if (gui_game_mode == 0)
mode = GAME_MODE_PVP;
else if (gui_game_mode == 3)
mode = GAME_MODE_NETWORK;
else
mode = GAME_MODE_AI;
if (save_game_to_file(base_name, mode) == 0)
{
snprintf(status_message, sizeof(status_message), "保存成功: %s", base_name);
}
else
{
snprintf(status_message, sizeof(status_message), "保存失败");
}
update_ui_labels();
}
IupDestroy(file_dlg);
return IUP_DEFAULT;
}
/**
* @brief 返回菜单回调
*/
int btn_back_cb(Ihandle *ih)
{
(void)ih;
// 停止所有定时器
if (timer)
{
IupSetAttribute(timer, "RUN", "NO");
IupDestroy(timer);
timer = NULL;
}
if (llm_timer)
{
IupSetAttribute(llm_timer, "RUN", "NO");
IupDestroy(llm_timer);
llm_timer = NULL;
}
// 如果是网络模式,彻底清理网络资源
if (gui_game_mode == 3)
{
cleanup_network();
}
// 1. 先显示主菜单
show_main_menu();
// 2. 销毁游戏窗口
if (dlg)
{
Ihandle *old_dlg = dlg;
dlg = NULL; // 先清除全局指针
IupDestroy(old_dlg);
}
return IUP_IGNORE; // 返回 IUP_IGNORE 以阻止默认处理
}
/**
* @brief 鼠标点击回调
*/
int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status)
{
(void)status; // 未使用
if (gui_game_mode == 2)
return IUP_DEFAULT; // 复盘模式禁用点击
if (button == IUP_BUTTON1 && pressed)
{ // 左键按下
if (game_over)
return IUP_DEFAULT;
if (gui_game_mode == 3 && current_player_gui != network_state.local_player_id)
{
return IUP_DEFAULT; // 网络模式下,非自己回合不可落子
}
int board_x, board_y;
if (screen_to_board(x, y, &board_x, &board_y))
{
if (have_space(board_x, board_y))
{
// 执行落子操作
if (player_move(board_x, board_y, current_player_gui))
{
// 检查是否获胜
if (check_win(board_x, board_y, current_player_gui))
{
game_over = 1;
if (gui_game_mode == 3)
send_move(board_x, board_y, current_player_gui); // 发送最后一步
if (current_player_gui == PLAYER)
{
sprintf(status_message, "黑子获胜!");
IupMessage("游戏结束", "黑子获胜!");
}
else
{
sprintf(status_message, "白子获胜!");
IupMessage("游戏结束", "白子获胜!");
}
}
else
{
if (gui_game_mode == 0) // PvP
{
current_player_gui = (current_player_gui == PLAYER) ? AI : PLAYER; // AI 在这里表示玩家2
if (current_player_gui == PLAYER)
sprintf(status_message, "轮到黑子");
else
sprintf(status_message, "轮到白子");
}
else if (gui_game_mode == 3) // Network
{
send_move(board_x, board_y, current_player_gui);
current_player_gui = network_state.remote_player_id;
sprintf(status_message, "等待对手落子...");
}
else // PvE
{
current_player_gui = AI;
update_ui_labels();
IupUpdate(ih); // 立即更新显示
IupFlush(); // 强制刷新事件队列
if (llm_use)
{
// 大模型AI - 异步调用,不阻塞UI
sprintf(status_message, "AI思考中(大模型)...");
update_ui_labels();
// 创建或复用轮询定时器
if (!llm_timer)
{
llm_timer = IupTimer();
IupSetCallback(llm_timer, "ACTION_CB", (Icallback)llm_timer_cb);
IupSetAttribute(llm_timer, "TIME", "100"); // 100ms轮询
}
llm_ai_start_move();
IupSetAttribute(llm_timer, "RUN", "YES");
}
else
{
// 算法AI - 同步调用
sprintf(status_message, "AI思考中...");
update_ui_labels();
ai_move(ai_difficulty);
process_ai_move_result();
}
}
}
update_ui_labels();
IupUpdate(ih); // 请求重绘
}
}
else
{
sprintf(status_message, "无效位置!");
update_ui_labels();
}
}
}
return IUP_DEFAULT;
}
/**
* @brief 键盘回调
*/
int k_any_cb(Ihandle *ih, int c)
{
(void)ih;
if (c == K_ESC)
{
if (dlg && IupGetInt(dlg, "VISIBLE"))
{
btn_back_cb(ih); // 调用返回菜单逻辑
return IUP_DEFAULT;
}
}
return IUP_DEFAULT;
}
/**
* @brief 创建游戏窗口
*/
void create_game_window()
{
if (dlg)
{
IupDestroy(dlg);
dlg = NULL;
}
// 创建Canvas (棋盘)
board_canvas = IupCanvas(NULL);
if (!board_canvas)
printf("ERROR: Failed to create board_canvas\n");
IupSetCallback(board_canvas, "ACTION", (Icallback)action_cb);
IupSetCallback(board_canvas, "BUTTON_CB", (Icallback)button_cb);
IupSetCallback(board_canvas, "K_ANY", (Icallback)k_any_cb);
IupSetCallback(board_canvas, "MAP_CB", (Icallback)map_cb);
// 计算棋盘像素大小(含坐标标注区域)
int board_pixel_size = (BOARD_SIZE - 1) * CELL_SIZE + BOARD_OFFSET_X * 2 + 20;
char size[32];
sprintf(size, "%dx%d", board_pixel_size, board_pixel_size);
IupSetAttribute(board_canvas, "RASTERSIZE", size);
IupSetAttribute(board_canvas, "EXPAND", "NO");
IupSetAttribute(board_canvas, "BORDER", "NO");
IupSetAttribute(board_canvas, "BGCOLOR", "212 165 116");
// === 创建标签 ===
lbl_player = IupLabel("当前玩家: 黑子");
IupSetAttribute(lbl_player, "FONT", "SimHei, 13");
IupSetAttribute(lbl_player, "FGCOLOR", CLR_TEXT_TITLE);
lbl_status = IupLabel("准备开始");
IupSetAttribute(lbl_status, "FONT", "SimHei, 11");
IupSetAttribute(lbl_status, "FGCOLOR", CLR_TEXT_NORMAL);
// === 对局信息面板 ===
Ihandle *info_vbox = IupVbox(lbl_player, lbl_status, NULL);
IupSetAttribute(info_vbox, "GAP", "4");
IupSetAttribute(info_vbox, "MARGIN", "10x8");
Ihandle *frm_info = IupFrame(info_vbox);
IupSetAttribute(frm_info, "TITLE", "对局信息");
IupSetAttribute(frm_info, "FONT", "SimHei, 11");
IupSetAttribute(frm_info, "FGCOLOR", CLR_TEXT_NORMAL);
// === 按钮样式宏 ===
#define SET_BTN_STYLE(btn, w, h, font) \
IupSetAttribute(btn, "SIZE", #w "x" #h); \
IupSetAttribute(btn, "FONT", font); \
IupSetAttribute(btn, "BGCOLOR", CLR_BTN_NORMAL_BG); \
IupSetAttribute(btn, "FGCOLOR", CLR_BTN_NORMAL_FG); \
IupSetAttribute(btn, "FLAT", "YES")
Ihandle *vbox_controls;
if (gui_game_mode == 2) // 复盘模式
{
Ihandle *btn_prev = IupButton("上一步", NULL);
IupSetCallback(btn_prev, "ACTION", (Icallback)btn_replay_prev_cb);
SET_BTN_STYLE(btn_prev, 120, 35, "SimHei, 11");
Ihandle *btn_next = IupButton("下一步", NULL);
IupSetCallback(btn_next, "ACTION", (Icallback)btn_replay_next_cb);
SET_BTN_STYLE(btn_next, 120, 35, "SimHei, 11");
Ihandle *hbox_nav = IupHbox(btn_prev, btn_next, NULL);
IupSetAttribute(hbox_nav, "GAP", "8");
IupSetAttribute(hbox_nav, "ALIGNMENT", "ACENTER");
Ihandle *btn_back = IupButton("返回菜单", NULL);
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
SET_BTN_STYLE(btn_back, 120, 35, "SimHei, 11");
vbox_controls = IupVbox(
frm_info,
hbox_nav,
btn_back,
NULL);
}
else // 游戏模式 (PvP / PvE / Network)
{
Ihandle *btn_undo = IupButton("悔棋", NULL);
IupSetCallback(btn_undo, "ACTION", (Icallback)btn_undo_cb);
SET_BTN_STYLE(btn_undo, 120, 35, "SimHei, 11");
Ihandle *btn_save = IupButton("保存棋谱", NULL);
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_cb);
SET_BTN_STYLE(btn_save, 120, 35, "SimHei, 11");
Ihandle *btn_back = IupButton("返回菜单", NULL);
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
SET_BTN_STYLE(btn_back, 120, 35, "SimHei, 11");
vbox_controls = IupVbox(
frm_info,
btn_undo,
btn_save,
btn_back,
NULL);
}
#undef SET_BTN_STYLE
IupSetAttribute(vbox_controls, "GAP", "10");
IupSetAttribute(vbox_controls, "MARGIN", "10x10");
IupSetAttribute(vbox_controls, "ALIGNMENT", "ACENTER");
Ihandle *hbox_main = IupHbox(board_canvas, vbox_controls, NULL);
IupSetAttribute(hbox_main, "MARGIN", "10x10");
IupSetAttribute(hbox_main, "GAP", "8");
// 创建Dialog
dlg = IupDialog(hbox_main);
if (!dlg)
printf("ERROR: Failed to create dialog\n");
IupSetAttribute(dlg, "TITLE", "五子棋");
IupSetAttribute(dlg, "RESIZE", "NO");
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
// 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单
IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb);
}
void start_pvp_game_gui()
{
gui_game_mode = 0;
empty_board();
current_player_gui = PLAYER;
game_over = 0;
create_game_window();
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
IupFlush(); // 确保窗口完全映射
sprintf(status_message, "玩家对战模式 - 黑方先行");
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
void start_pve_game_gui()
{
gui_game_mode = 1;
empty_board();
current_player_gui = PLAYER;
game_over = 0;
create_game_window();
if (dlg)
{
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
IupFlush();
}
else
{
return;
}
sprintf(status_message, "人机对战模式 - 玩家执黑先行");
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
void start_network_game_gui()
{
gui_game_mode = 3;
empty_board();
current_player_gui = PLAYER1;
game_over = 0;
create_game_window();
if (dlg)
{
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
IupFlush();
}
if (network_state.is_server)
sprintf(status_message, "局域网联机 - 你是主机(黑子),轮到你落子");
else
sprintf(status_message, "局域网联机 - 你是客机(白子),等待对手落子...");
update_ui_labels();
// 强制初始重绘
if (board_canvas)
{
IupUpdate(board_canvas);
}
// 启动网络轮询定时器
timer = IupTimer();
IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb);
IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次
IupSetAttribute(timer, "RUN", "YES");
}
+559
View File
@@ -0,0 +1,559 @@
#include <iup.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "gui_menu.h"
#include "gui.h"
#include "gui_internal.h"
#include "globals.h"
#include "config.h"
#include "network.h"
Ihandle *menu_dlg = NULL;
static int btn_pvp_cb(Ihandle *ih)
{
(void)ih;
start_pvp_game_gui();
IupHide(menu_dlg);
return IUP_DEFAULT;
}
static int btn_pve_cb(Ihandle *ih)
{
(void)ih;
start_pve_game_gui();
IupHide(menu_dlg);
return IUP_DEFAULT;
}
// --- 网络对战相关回调 ---
static int btn_network_host_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
int port = IupGetInt(txt_port, "VALUE");
if (port <= 0 || port > 65535) port = DEFAULT_NETWORK_PORT;
if (create_server(port))
{
IupMessage("成功", "房间创建成功,等待玩家加入...");
IupHide(dlg);
start_network_game_gui();
IupHide(menu_dlg);
}
else
{
IupMessage("错误", "创建房间失败,可能是端口被占用");
}
return IUP_DEFAULT;
}
static int btn_network_join_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_ip = IupGetDialogChild(dlg, "NET_IP");
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
char *ip = IupGetAttribute(txt_ip, "VALUE");
int port = IupGetInt(txt_port, "VALUE");
if (port <= 0 || port > 65535) port = DEFAULT_NETWORK_PORT;
if (connect_to_server(ip, port))
{
IupMessage("成功", "成功加入房间!");
IupHide(dlg);
start_network_game_gui();
IupHide(menu_dlg);
}
else
{
IupMessage("错误", "加入房间失败,请检查IP和端口");
}
return IUP_DEFAULT;
}
static int btn_network_cancel_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
IupHide(dlg);
return IUP_DEFAULT;
}
static int btn_network_cb(Ihandle *ih)
{
(void)ih;
Ihandle *lbl_ip = IupLabel("目标 IP:");
Ihandle *txt_ip = IupText(NULL);
IupSetAttribute(txt_ip, "NAME", "NET_IP");
IupSetAttribute(txt_ip, "VALUE", "127.0.0.1");
IupSetAttribute(txt_ip, "SIZE", "120x");
Ihandle *hbox_ip = IupHbox(lbl_ip, txt_ip, NULL);
IupSetAttribute(hbox_ip, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_ip, "GAP", "10");
Ihandle *lbl_port = IupLabel("端口:");
Ihandle *txt_port = IupText(NULL);
IupSetAttribute(txt_port, "NAME", "NET_PORT");
char port_str[16];
sprintf(port_str, "%d", DEFAULT_NETWORK_PORT);
IupSetAttribute(txt_port, "VALUE", port_str);
IupSetAttribute(txt_port, "SIZE", "60x");
Ihandle *hbox_port = IupHbox(lbl_port, txt_port, NULL);
IupSetAttribute(hbox_port, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_port, "GAP", "10");
Ihandle *vbox_input = IupVbox(hbox_ip, hbox_port, NULL);
IupSetAttribute(vbox_input, "GAP", "8");
IupSetAttribute(vbox_input, "MARGIN", "10x8");
Ihandle *frm_input = IupFrame(vbox_input);
IupSetAttribute(frm_input, "TITLE", "连接设置");
IupSetAttribute(frm_input, "FONT", "SimHei, 11");
IupSetAttribute(frm_input, "FGCOLOR", CLR_TEXT_NORMAL);
Ihandle *btn_host = IupButton("创建房间", NULL);
IupSetCallback(btn_host, "ACTION", (Icallback)btn_network_host_cb);
IupSetAttribute(btn_host, "SIZE", "100x32");
IupSetAttribute(btn_host, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_host, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_host, "FLAT", "YES");
Ihandle *btn_join = IupButton("加入房间", NULL);
IupSetCallback(btn_join, "ACTION", (Icallback)btn_network_join_cb);
IupSetAttribute(btn_join, "SIZE", "100x32");
IupSetAttribute(btn_join, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_join, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_join, "FLAT", "YES");
Ihandle *btn_cancel = IupButton("取消", NULL);
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_network_cancel_cb);
IupSetAttribute(btn_cancel, "SIZE", "80x32");
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_cancel, "FLAT", "YES");
Ihandle *hbox_btns = IupHbox(btn_host, btn_join, btn_cancel, NULL);
IupSetAttribute(hbox_btns, "GAP", "8");
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
Ihandle *vbox = IupVbox(frm_input, hbox_btns, NULL);
IupSetAttribute(vbox, "MARGIN", "15x15");
IupSetAttribute(vbox, "GAP", "12");
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
Ihandle *dlg = IupDialog(vbox);
IupSetAttribute(dlg, "TITLE", "局域网联机");
IupSetAttribute(dlg, "RESIZE", "NO");
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
IupDestroy(dlg);
return IUP_DEFAULT;
}
// --- 网络对战结束 ---
static int btn_replay_cb(Ihandle *ih)
{
(void)ih;
start_replay_gui();
IupHide(menu_dlg);
return IUP_DEFAULT;
}
static int btn_save_settings_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
// Get values
Ihandle *txt_board_size = IupGetDialogChild(dlg, "BOARD_SIZE");
Ihandle *tgl_forbidden = IupGetDialogChild(dlg, "FORBIDDEN");
Ihandle *tgl_timer = IupGetDialogChild(dlg, "TIMER");
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
Ihandle *lst_ai = IupGetDialogChild(dlg, "AI_DIFFICULTY");
// Update globals
int new_size = IupGetInt(txt_board_size, "VALUE");
if (new_size < MIN_BOARD_SIZE)
new_size = MIN_BOARD_SIZE;
if (new_size > MAX_BOARD_SIZE)
new_size = MAX_BOARD_SIZE;
BOARD_SIZE = new_size;
use_forbidden_moves = IupGetInt(tgl_forbidden, "VALUE");
use_timer = IupGetInt(tgl_timer, "VALUE");
if (use_timer)
{
int minutes = IupGetInt(txt_time_limit, "VALUE");
if (minutes < 1)
minutes = 1;
time_limit = minutes * 60;
}
int ai_level = IupGetInt(lst_ai, "VALUE");
if (ai_level < 1)
ai_level = 1;
if (ai_level > 5)
ai_level = 5;
ai_difficulty = ai_level;
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
// LLM 设置
Ihandle *lst_ai_mode = IupGetDialogChild(dlg, "AI_MODE");
Ihandle *txt_endpoint = IupGetDialogChild(dlg, "LLM_ENDPOINT");
Ihandle *txt_apikey = IupGetDialogChild(dlg, "LLM_API_KEY");
Ihandle *txt_model = IupGetDialogChild(dlg, "LLM_MODEL");
int ai_mode = IupGetInt(lst_ai_mode, "VALUE");
llm_use = (ai_mode == 2) ? 1 : 0;
char *endpoint = IupGetAttribute(txt_endpoint, "VALUE");
if (endpoint)
{
strncpy(llm_endpoint, endpoint, MAX_LLM_ENDPOINT_LEN - 1);
llm_endpoint[MAX_LLM_ENDPOINT_LEN - 1] = '\0';
}
char *apikey = IupGetAttribute(txt_apikey, "VALUE");
if (apikey)
{
strncpy(llm_api_key, apikey, MAX_LLM_API_KEY_LEN - 1);
llm_api_key[MAX_LLM_API_KEY_LEN - 1] = '\0';
}
char *model = IupGetAttribute(txt_model, "VALUE");
if (model)
{
strncpy(llm_model, model, MAX_LLM_MODEL_LEN - 1);
llm_model[MAX_LLM_MODEL_LEN - 1] = '\0';
}
// Save config
save_game_config();
IupHide(dlg);
return IUP_DEFAULT;
}
static int btn_cancel_settings_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
IupHide(dlg);
return IUP_DEFAULT;
}
static int tgl_timer_cb(Ihandle *ih, int state)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
IupSetAttribute(txt_time_limit, "ACTIVE", state ? "YES" : "NO");
return IUP_DEFAULT;
}
static int btn_settings_cb(Ihandle *ih)
{
(void)ih;
// === 基本设置 ===
Ihandle *lbl_board_size = IupLabel("棋盘大小:");
Ihandle *txt_board_size = IupText(NULL);
IupSetAttribute(txt_board_size, "NAME", "BOARD_SIZE");
IupSetAttribute(txt_board_size, "SPIN", "YES");
IupSetAttribute(txt_board_size, "SPINMIN", "5");
IupSetAttribute(txt_board_size, "SPINMAX", "25");
IupSetInt(txt_board_size, "VALUE", BOARD_SIZE);
IupSetAttribute(txt_board_size, "SIZE", "50x");
Ihandle *hbox_board = IupHbox(lbl_board_size, txt_board_size, NULL);
IupSetAttribute(hbox_board, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_board, "GAP", "10");
Ihandle *tgl_forbidden = IupToggle("启用禁手规则", NULL);
IupSetAttribute(tgl_forbidden, "NAME", "FORBIDDEN");
IupSetInt(tgl_forbidden, "VALUE", use_forbidden_moves);
Ihandle *tgl_timer = IupToggle("启用计时器", NULL);
IupSetAttribute(tgl_timer, "NAME", "TIMER");
IupSetInt(tgl_timer, "VALUE", use_timer);
IupSetCallback(tgl_timer, "ACTION", (Icallback)tgl_timer_cb);
Ihandle *lbl_time_limit = IupLabel("时间限制(分钟):");
Ihandle *txt_time_limit = IupText(NULL);
IupSetAttribute(txt_time_limit, "NAME", "TIME_LIMIT");
IupSetAttribute(txt_time_limit, "SPIN", "YES");
IupSetAttribute(txt_time_limit, "SPINMIN", "1");
IupSetAttribute(txt_time_limit, "SPINMAX", "60");
IupSetInt(txt_time_limit, "VALUE", time_limit / 60);
IupSetAttribute(txt_time_limit, "ACTIVE", use_timer ? "YES" : "NO");
IupSetAttribute(txt_time_limit, "SIZE", "50x");
Ihandle *hbox_time = IupHbox(lbl_time_limit, txt_time_limit, NULL);
IupSetAttribute(hbox_time, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_time, "GAP", "10");
Ihandle *vbox_basic = IupVbox(hbox_board, tgl_forbidden, tgl_timer, hbox_time, NULL);
IupSetAttribute(vbox_basic, "GAP", "8");
IupSetAttribute(vbox_basic, "MARGIN", "10x8");
Ihandle *frm_basic = IupFrame(vbox_basic);
IupSetAttribute(frm_basic, "TITLE", "基本设置");
IupSetAttribute(frm_basic, "FONT", "SimHei, 11");
IupSetAttribute(frm_basic, "FGCOLOR", CLR_TEXT_NORMAL);
// === AI 设置 ===
Ihandle *lbl_ai = IupLabel("AI 难度:");
Ihandle *lst_ai = IupList(NULL);
IupSetAttribute(lst_ai, "NAME", "AI_DIFFICULTY");
IupSetAttribute(lst_ai, "DROPDOWN", "YES");
IupSetAttribute(lst_ai, "1", "1 简单");
IupSetAttribute(lst_ai, "2", "2 普通");
IupSetAttribute(lst_ai, "3", "3 中等");
IupSetAttribute(lst_ai, "4", "4 困难");
IupSetAttribute(lst_ai, "5", "5 专家");
IupSetInt(lst_ai, "VALUE", ai_difficulty);
IupSetAttribute(lst_ai, "SIZE", "80x");
Ihandle *hbox_ai = IupHbox(lbl_ai, lst_ai, NULL);
IupSetAttribute(hbox_ai, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_ai, "GAP", "10");
Ihandle *lbl_ai_mode = IupLabel("AI模式:");
Ihandle *lst_ai_mode = IupList(NULL);
IupSetAttribute(lst_ai_mode, "NAME", "AI_MODE");
IupSetAttribute(lst_ai_mode, "DROPDOWN", "YES");
IupSetAttribute(lst_ai_mode, "1", "算法AI (本地)");
IupSetAttribute(lst_ai_mode, "2", "大模型AI (在线)");
IupSetInt(lst_ai_mode, "VALUE", llm_use ? 2 : 1);
IupSetAttribute(lst_ai_mode, "SIZE", "120x");
Ihandle *hbox_ai_mode = IupHbox(lbl_ai_mode, lst_ai_mode, NULL);
IupSetAttribute(hbox_ai_mode, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_ai_mode, "GAP", "10");
Ihandle *vbox_ai = IupVbox(hbox_ai, hbox_ai_mode, NULL);
IupSetAttribute(vbox_ai, "GAP", "8");
IupSetAttribute(vbox_ai, "MARGIN", "10x8");
Ihandle *frm_ai = IupFrame(vbox_ai);
IupSetAttribute(frm_ai, "TITLE", "AI 设置");
IupSetAttribute(frm_ai, "FONT", "SimHei, 11");
IupSetAttribute(frm_ai, "FGCOLOR", CLR_TEXT_NORMAL);
// === 大模型设置 ===
Ihandle *lbl_endpoint = IupLabel("API地址:");
Ihandle *txt_endpoint = IupText(NULL);
IupSetAttribute(txt_endpoint, "NAME", "LLM_ENDPOINT");
IupSetAttribute(txt_endpoint, "VALUE", llm_endpoint);
IupSetAttribute(txt_endpoint, "SIZE", "250x");
Ihandle *hbox_endpoint = IupHbox(lbl_endpoint, txt_endpoint, NULL);
IupSetAttribute(hbox_endpoint, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_endpoint, "GAP", "10");
Ihandle *lbl_apikey = IupLabel("API Key:");
Ihandle *txt_apikey = IupText(NULL);
IupSetAttribute(txt_apikey, "NAME", "LLM_API_KEY");
IupSetAttribute(txt_apikey, "VALUE", llm_api_key);
IupSetAttribute(txt_apikey, "PASSWORD", "YES");
IupSetAttribute(txt_apikey, "SIZE", "200x");
Ihandle *hbox_apikey = IupHbox(lbl_apikey, txt_apikey, NULL);
IupSetAttribute(hbox_apikey, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_apikey, "GAP", "10");
Ihandle *lbl_model = IupLabel("模型名称:");
Ihandle *txt_model = IupText(NULL);
IupSetAttribute(txt_model, "NAME", "LLM_MODEL");
IupSetAttribute(txt_model, "VALUE", llm_model);
IupSetAttribute(txt_model, "SIZE", "150x");
Ihandle *hbox_model = IupHbox(lbl_model, txt_model, NULL);
IupSetAttribute(hbox_model, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_model, "GAP", "10");
Ihandle *vbox_llm = IupVbox(hbox_endpoint, hbox_apikey, hbox_model, NULL);
IupSetAttribute(vbox_llm, "GAP", "8");
IupSetAttribute(vbox_llm, "MARGIN", "10x8");
Ihandle *frm_llm = IupFrame(vbox_llm);
IupSetAttribute(frm_llm, "TITLE", "大模型设置");
IupSetAttribute(frm_llm, "FONT", "SimHei, 11");
IupSetAttribute(frm_llm, "FGCOLOR", CLR_TEXT_NORMAL);
// === 按钮 ===
Ihandle *btn_save = IupButton("保存", NULL);
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb);
IupSetAttribute(btn_save, "SIZE", "80x30");
IupSetAttribute(btn_save, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_save, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_save, "FLAT", "YES");
Ihandle *btn_cancel = IupButton("取消", NULL);
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_cancel_settings_cb);
IupSetAttribute(btn_cancel, "SIZE", "80x30");
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_cancel, "FLAT", "YES");
Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
IupSetAttribute(hbox_btns, "GAP", "15");
IupSetAttribute(hbox_btns, "MARGIN", "5x0");
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
// === 主布局 ===
Ihandle *vbox = IupVbox(frm_basic, frm_ai, frm_llm, hbox_btns, NULL);
IupSetAttribute(vbox, "GAP", "10");
IupSetAttribute(vbox, "MARGIN", "15x15");
Ihandle *dlg = IupDialog(vbox);
IupSetAttribute(dlg, "TITLE", "游戏设置");
IupSetAttribute(dlg, "RESIZE", "NO");
IupSetAttribute(dlg, "MINBOX", "NO");
IupSetAttribute(dlg, "MAXBOX", "NO");
IupSetAttribute(dlg, "BGCOLOR", CLR_WINDOW_BG);
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
IupDestroy(dlg);
return IUP_DEFAULT;
}
static int btn_exit_cb(Ihandle *ih)
{
(void)ih;
cleanup_gui(); // 清理GUI资源
exit(0);
return IUP_DEFAULT;
}
/**
* @brief 创建主菜单
*/
void create_main_menu()
{
if (menu_dlg)
return;
// === 标题区 ===
Ihandle *lbl_title = IupLabel("五子棋");
IupSetAttribute(lbl_title, "FONT", "SimHei, 32");
IupSetAttribute(lbl_title, "FGCOLOR", CLR_TEXT_TITLE);
IupSetAttribute(lbl_title, "ALIGNMENT", "ACENTER");
Ihandle *lbl_subtitle = IupLabel("Gobang");
IupSetAttribute(lbl_subtitle, "FONT", "SimHei, 14");
IupSetAttribute(lbl_subtitle, "FGCOLOR", CLR_TEXT_SECONDARY);
IupSetAttribute(lbl_subtitle, "ALIGNMENT", "ACENTER");
// === 游戏模式按钮(主按钮样式:深棕底白字)===
Ihandle *btn_pvp = IupButton("玩家对战", NULL);
IupSetCallback(btn_pvp, "ACTION", (Icallback)btn_pvp_cb);
IupSetAttribute(btn_pvp, "SIZE", "150x38");
IupSetAttribute(btn_pvp, "FONT", "SimHei, 13");
IupSetAttribute(btn_pvp, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_pvp, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_pvp, "FLAT", "YES");
Ihandle *btn_pve = IupButton("人机对战", NULL);
IupSetCallback(btn_pve, "ACTION", (Icallback)btn_pve_cb);
IupSetAttribute(btn_pve, "SIZE", "150x38");
IupSetAttribute(btn_pve, "FONT", "SimHei, 13");
IupSetAttribute(btn_pve, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_pve, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_pve, "FLAT", "YES");
Ihandle *btn_net = IupButton("局域网联机", NULL);
IupSetCallback(btn_net, "ACTION", (Icallback)btn_network_cb);
IupSetAttribute(btn_net, "SIZE", "150x38");
IupSetAttribute(btn_net, "FONT", "SimHei, 13");
IupSetAttribute(btn_net, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_net, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_net, "FLAT", "YES");
Ihandle *vbox_modes = IupVbox(btn_pvp, btn_pve, btn_net, NULL);
IupSetAttribute(vbox_modes, "GAP", "8");
IupSetAttribute(vbox_modes, "ALIGNMENT", "ACENTER");
IupSetAttribute(vbox_modes, "MARGIN", "15x10");
Ihandle *frm_modes = IupFrame(vbox_modes);
IupSetAttribute(frm_modes, "TITLE", "选择模式");
IupSetAttribute(frm_modes, "FONT", "SimHei, 11");
IupSetAttribute(frm_modes, "FGCOLOR", CLR_TEXT_NORMAL);
// === 功能按钮(普通按钮样式:浅棕底深棕字)===
Ihandle *btn_replay = IupButton("复盘回放", NULL);
IupSetCallback(btn_replay, "ACTION", (Icallback)btn_replay_cb);
IupSetAttribute(btn_replay, "SIZE", "120x32");
IupSetAttribute(btn_replay, "FONT", "SimHei, 11");
IupSetAttribute(btn_replay, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_replay, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_replay, "FLAT", "YES");
Ihandle *btn_settings = IupButton("游戏设置", NULL);
IupSetCallback(btn_settings, "ACTION", (Icallback)btn_settings_cb);
IupSetAttribute(btn_settings, "SIZE", "120x32");
IupSetAttribute(btn_settings, "FONT", "SimHei, 11");
IupSetAttribute(btn_settings, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_settings, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_settings, "FLAT", "YES");
Ihandle *btn_exit = IupButton("退出游戏", NULL);
IupSetCallback(btn_exit, "ACTION", (Icallback)btn_exit_cb);
IupSetAttribute(btn_exit, "SIZE", "120x32");
IupSetAttribute(btn_exit, "FONT", "SimHei, 11");
IupSetAttribute(btn_exit, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_exit, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_exit, "FLAT", "YES");
Ihandle *hbox_func = IupHbox(btn_replay, btn_settings, btn_exit, NULL);
IupSetAttribute(hbox_func, "GAP", "10");
IupSetAttribute(hbox_func, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_func, "MARGIN", "10x8");
Ihandle *frm_func = IupFrame(hbox_func);
IupSetAttribute(frm_func, "TITLE", "功能");
IupSetAttribute(frm_func, "FONT", "SimHei, 11");
IupSetAttribute(frm_func, "FGCOLOR", CLR_TEXT_NORMAL);
// === 主布局 ===
Ihandle *vbox = IupVbox(
lbl_title,
lbl_subtitle,
IupLabel(NULL), // 间隔
frm_modes,
frm_func,
NULL);
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
IupSetAttribute(vbox, "GAP", "8");
IupSetAttribute(vbox, "MARGIN", "30x25");
menu_dlg = IupDialog(vbox);
IupSetAttribute(menu_dlg, "TITLE", "五子棋 - 主菜单");
IupSetAttribute(menu_dlg, "RESIZE", "NO");
IupSetAttribute(menu_dlg, "MINBOX", "NO");
IupSetAttribute(menu_dlg, "MAXBOX", "NO");
IupSetAttribute(menu_dlg, "BGCOLOR", CLR_WINDOW_BG);
IupSetAttribute(menu_dlg, "SIZE", "380x520");
// 设置对话框关闭回调 (点X关闭程序)
IupSetCallback(menu_dlg, "CLOSE_CB", (Icallback)btn_exit_cb);
IupMap(menu_dlg); // Map immediately
}
void show_main_menu()
{
if (!menu_dlg)
create_main_menu();
IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER);
}
void hide_main_menu()
{
if (menu_dlg)
IupHide(menu_dlg);
}
+19 -9
View File
@@ -4,6 +4,7 @@
#include "globals.h"
#include "gobang.h"
#include "record.h"
#include "config.h"
#include <iup.h>
#include <stdio.h>
#include <stdlib.h>
@@ -173,20 +174,28 @@ void select_replay_file_gui()
// 创建确定和取消按钮
Ihandle *btn_ok = IupButton("确定", NULL);
IupSetCallback(btn_ok, "ACTION", (Icallback)btn_replay_sel_ok_cb);
IupSetAttribute(btn_ok, "SIZE", "60x30");
IupSetAttribute(btn_ok, "SIZE", "80x32");
IupSetAttribute(btn_ok, "BGCOLOR", CLR_BTN_PRIMARY_BG);
IupSetAttribute(btn_ok, "FGCOLOR", CLR_BTN_PRIMARY_FG);
IupSetAttribute(btn_ok, "FLAT", "YES");
Ihandle *btn_cancel = IupButton("取消", NULL);
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_replay_sel_cancel_cb);
IupSetAttribute(btn_cancel, "SIZE", "60x30");
IupSetAttribute(btn_cancel, "SIZE", "80x32");
IupSetAttribute(btn_cancel, "BGCOLOR", CLR_BTN_NORMAL_BG);
IupSetAttribute(btn_cancel, "FGCOLOR", CLR_BTN_NORMAL_FG);
IupSetAttribute(btn_cancel, "FLAT", "YES");
Ihandle *vbox = IupVbox(
IupLabel("请选择复盘文件:"),
list,
IupHbox(btn_ok, btn_cancel, NULL),
NULL);
Ihandle *lbl_prompt = IupLabel("选择复盘文件:");
IupSetAttribute(lbl_prompt, "FGCOLOR", CLR_TEXT_NORMAL);
Ihandle *hbox_btns = IupHbox(btn_ok, btn_cancel, NULL);
IupSetAttribute(hbox_btns, "GAP", "10");
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
Ihandle *vbox = IupVbox(lbl_prompt, list, hbox_btns, NULL);
IupSetAttribute(vbox, "GAP", "10");
IupSetAttribute(vbox, "MARGIN", "10x10");
IupSetAttribute(vbox, "MARGIN", "15x15");
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
Ihandle *dlg_sel = IupDialog(vbox);
@@ -195,10 +204,11 @@ void select_replay_file_gui()
printf("ERROR: Failed to create selection dialog\n");
return;
}
IupSetAttribute(dlg_sel, "TITLE", "选择复盘文件");
IupSetAttribute(dlg_sel, "TITLE", "复盘回放");
IupSetAttribute(dlg_sel, "MINBOX", "NO");
IupSetAttribute(dlg_sel, "MAXBOX", "NO");
IupSetAttribute(dlg_sel, "RESIZE", "NO");
IupSetAttribute(dlg_sel, "BGCOLOR", CLR_WINDOW_BG);
// 存储列表框句柄到对话框属性
IupSetAttribute(dlg_sel, "MY_LIST", (char *)list);
-531
View File
@@ -1,531 +0,0 @@
#include "gui.h"
#include "gui_internal.h"
#include "gui_menu.h"
#include "globals.h"
#include "gobang.h"
#include "ai.h"
#include "record.h"
#include "network.h"
#include <iup.h>
#include <iupdraw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static Ihandle *timer = NULL; // 网络轮询定时器
/**
* @brief 网络事件轮询回调
*/
static int timer_cb(Ihandle *ih)
{
(void)ih;
if (gui_game_mode != 3 || game_over)
return IUP_DEFAULT;
NetworkMessage msg;
// 非阻塞接收消息
if (receive_network_message(&msg, 0))
{
if (msg.type == MSG_MOVE)
{
int bx = msg.x;
int by = msg.y;
int pid = msg.player_id;
if (have_space(bx, by))
{
player_move(bx, by, pid);
if (check_win(bx, by, pid))
{
game_over = 1;
sprintf(status_message, "对手获胜!");
IupMessage("游戏结束", "对手获胜!");
}
else
{
current_player_gui = network_state.local_player_id;
sprintf(status_message, "轮到你落子");
}
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
}
else if (msg.type == MSG_DISCONNECT || msg.type == MSG_SURRENDER)
{
game_over = 1;
sprintf(status_message, "对手已断开连接/认输");
IupMessage("游戏结束", "对手已退出游戏,你赢了!");
update_ui_labels();
}
}
if (!is_network_connected() && !game_over)
{
game_over = 1;
sprintf(status_message, "与服务器断开连接");
IupMessage("错误", "网络连接已断开");
update_ui_labels();
}
return IUP_DEFAULT;
}
/**
* @brief ACTION 回调:负责重绘
*/
int action_cb(Ihandle *ih)
{
IupDrawBegin(ih);
int w, h;
IupGetIntInt(ih, "DRAWSIZE", &w, &h);
set_draw_color(ih, 240, 217, 181); // 棋盘背景色 (木纹色近似)
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
IupDrawRectangle(ih, 0, 0, w, h);
draw_board_iup(ih);
draw_stones_iup(ih);
IupDrawEnd(ih);
return IUP_DEFAULT;
}
/**
* @brief 悔棋按钮回调
*/
int btn_undo_cb(Ihandle *ih)
{
(void)ih;
if (game_over)
return IUP_DEFAULT;
int steps_to_undo = 1;
if (gui_game_mode == 1) // PvE
{
steps_to_undo = 2; // 悔棋两步(玩家+AI
}
else if (gui_game_mode == 3) // Network
{
IupMessage("提示", "网络模式暂不支持悔棋");
return IUP_DEFAULT;
}
if (step_count >= steps_to_undo)
{
return_move(steps_to_undo);
// 更新当前玩家
if (step_count % 2 == 0)
current_player_gui = PLAYER;
else
current_player_gui = AI; // or PLAYER2
sprintf(status_message, "已悔棋");
update_ui_labels();
IupUpdate(board_canvas);
}
else
{
sprintf(status_message, "无法悔棋");
update_ui_labels();
}
return IUP_DEFAULT;
}
/**
* @brief 保存按钮回调
*/
int btn_save_cb(Ihandle *ih)
{
(void)ih;
Ihandle *file_dlg = IupFileDlg();
IupSetAttribute(file_dlg, "DIALOGTYPE", "SAVE");
IupSetAttribute(file_dlg, "TITLE", "保存游戏记录");
IupSetAttribute(file_dlg, "FILTER", "*.csv");
IupSetAttribute(file_dlg, "FILTERINFO", "CSV Files");
IupPopup(file_dlg, IUP_CENTER, IUP_CENTER);
if (IupGetInt(file_dlg, "STATUS") != -1)
{
char *filename = IupGetAttribute(file_dlg, "VALUE");
char *base_name = strrchr(filename, '\\');
if (!base_name)
base_name = strrchr(filename, '/');
if (base_name)
base_name++;
else
base_name = filename;
int mode = (gui_game_mode == 0) ? GAME_MODE_PVP : GAME_MODE_AI;
if (save_game_to_file(base_name, mode) == 0)
{
sprintf(status_message, "保存成功: %s", base_name);
}
else
{
sprintf(status_message, "保存失败");
}
update_ui_labels();
}
IupDestroy(file_dlg);
return IUP_DEFAULT;
}
/**
* @brief 返回菜单回调
*/
int btn_back_cb(Ihandle *ih)
{
(void)ih;
printf("DEBUG: Back to Menu clicked\n");
// 如果是网络模式,断开连接
if (gui_game_mode == 3)
{
disconnect_network();
if (timer)
{
IupSetAttribute(timer, "RUN", "NO");
IupDestroy(timer);
timer = NULL;
}
}
// 1. 先显示主菜单
show_main_menu();
printf("DEBUG: Main menu shown\n");
// 2. 销毁游戏窗口
if (dlg)
{
Ihandle *old_dlg = dlg;
dlg = NULL; // 先清除全局指针
IupDestroy(old_dlg);
printf("DEBUG: Destroyed game window\n");
}
return IUP_IGNORE; // 返回 IUP_IGNORE 以阻止默认处理
}
/**
* @brief 鼠标点击回调
*/
int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status)
{
(void)status; // 未使用
if (gui_game_mode == 2)
return IUP_DEFAULT; // 复盘模式禁用点击
if (button == IUP_BUTTON1 && pressed)
{ // 左键按下
if (game_over)
return IUP_DEFAULT;
if (gui_game_mode == 3 && current_player_gui != network_state.local_player_id)
{
return IUP_DEFAULT; // 网络模式下,非自己回合不可落子
}
int board_x, board_y;
if (screen_to_board(x, y, &board_x, &board_y))
{
if (have_space(board_x, board_y))
{
// 执行落子操作
if (player_move(board_x, board_y, current_player_gui))
{
// 检查是否获胜
if (check_win(board_x, board_y, current_player_gui))
{
game_over = 1;
if (gui_game_mode == 3)
send_move(board_x, board_y, current_player_gui); // 发送最后一步
if (current_player_gui == PLAYER)
{
sprintf(status_message, "黑子获胜!");
IupMessage("游戏结束", "黑子获胜!");
}
else
{
sprintf(status_message, "白子获胜!");
IupMessage("游戏结束", "白子获胜!");
}
}
else
{
if (gui_game_mode == 0) // PvP
{
current_player_gui = (current_player_gui == PLAYER) ? AI : PLAYER; // AI 在这里表示玩家2
if (current_player_gui == PLAYER)
sprintf(status_message, "轮到黑子");
else
sprintf(status_message, "轮到白子");
}
else if (gui_game_mode == 3) // Network
{
send_move(board_x, board_y, current_player_gui);
current_player_gui = network_state.remote_player_id;
sprintf(status_message, "等待对手落子...");
}
else // PvE
{
current_player_gui = AI;
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))
{
game_over = 1;
sprintf(status_message, "AI获胜!");
IupMessage("游戏结束", "AI获胜!");
}
else
{
current_player_gui = PLAYER;
sprintf(status_message, "轮到玩家");
}
}
}
update_ui_labels();
IupUpdate(ih); // 请求重绘
}
}
else
{
sprintf(status_message, "无效位置!");
update_ui_labels();
}
}
}
return IUP_DEFAULT;
}
/**
* @brief 键盘回调
*/
int k_any_cb(Ihandle *ih, int c)
{
(void)ih;
if (c == K_ESC)
{
if (dlg && IupGetInt(dlg, "VISIBLE"))
{
btn_back_cb(ih); // 调用返回菜单逻辑
return IUP_DEFAULT;
}
}
return IUP_DEFAULT;
}
/**
* @brief 创建游戏窗口
*/
void create_game_window()
{
printf("DEBUG: create_game_window start\n");
if (dlg)
{
IupDestroy(dlg);
dlg = NULL;
}
// 创建Canvas (棋盘)
board_canvas = IupCanvas(NULL);
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);
// 计算棋盘像素大小
int board_pixel_size = BOARD_SIZE * CELL_SIZE + BOARD_OFFSET_X * 2;
char size[32];
sprintf(size, "%dx%d", board_pixel_size, board_pixel_size);
IupSetAttribute(board_canvas, "RASTERSIZE", size);
IupSetAttribute(board_canvas, "EXPAND", "NO");
// 创建标签 (玩家信息和游戏状态)
lbl_player = IupLabel("当前玩家: 黑子");
lbl_status = IupLabel("准备开始");
Ihandle *vbox_controls;
if (gui_game_mode == 2) // 复盘模式
{
Ihandle *btn_prev = IupButton("上一步 (Prev)", NULL);
IupSetCallback(btn_prev, "ACTION", (Icallback)btn_replay_prev_cb);
IupSetAttribute(btn_prev, "SIZE", "100x30");
Ihandle *btn_next = IupButton("下一步 (Next)", NULL);
IupSetCallback(btn_next, "ACTION", (Icallback)btn_replay_next_cb);
IupSetAttribute(btn_next, "SIZE", "100x30");
Ihandle *btn_back = IupButton("返回菜单", NULL);
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
IupSetAttribute(btn_back, "SIZE", "100x30");
vbox_controls = IupVbox(
lbl_player,
lbl_status,
IupLabel(NULL), // Spacer
btn_prev,
btn_next,
IupLabel(NULL), // Spacer
btn_back,
NULL);
}
else // 游戏模式 (PvP / PvE)
{
Ihandle *btn_undo = IupButton("悔棋 (Undo)", NULL);
IupSetCallback(btn_undo, "ACTION", (Icallback)btn_undo_cb);
IupSetAttribute(btn_undo, "SIZE", "100x30");
Ihandle *btn_save = IupButton("保存 (Save)", NULL);
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_cb);
IupSetAttribute(btn_save, "SIZE", "100x30");
Ihandle *btn_back = IupButton("返回菜单", NULL);
IupSetCallback(btn_back, "ACTION", (Icallback)btn_back_cb);
IupSetAttribute(btn_back, "SIZE", "100x30");
vbox_controls = IupVbox(
lbl_player,
lbl_status,
IupLabel(NULL), // Spacer
btn_undo,
btn_save,
IupLabel(NULL), // Spacer
btn_back,
NULL);
}
IupSetAttribute(vbox_controls, "GAP", "15");
IupSetAttribute(vbox_controls, "MARGIN", "10x10");
IupSetAttribute(vbox_controls, "ALIGNMENT", "ACENTER");
Ihandle *hbox_main = IupHbox(board_canvas, vbox_controls, NULL);
IupSetAttribute(hbox_main, "MARGIN", "10x10");
IupSetAttribute(hbox_main, "GAP", "10");
// 创建Dialog
dlg = IupDialog(hbox_main);
if (!dlg)
printf("ERROR: Failed to create dialog\n");
IupSetAttribute(dlg, "TITLE", "五子棋 - IUP版本");
IupSetAttribute(dlg, "RESIZE", "NO");
// 设置 CLOSE_CB 回调,确保点击X也能正确返回菜单
IupSetCallback(dlg, "CLOSE_CB", (Icallback)btn_back_cb);
printf("DEBUG: create_game_window end\n");
}
void start_pvp_game_gui()
{
gui_game_mode = 0;
empty_board();
current_player_gui = PLAYER;
game_over = 0;
create_game_window();
IupShowXY(dlg, IUP_CENTER, IUP_CENTER);
sprintf(status_message, "玩家对战模式 - 黑方先行");
update_ui_labels();
if (board_canvas)
IupUpdate(board_canvas);
}
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");
}
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");
}
if (network_state.is_server)
sprintf(status_message, "局域网联机 - 你是主机(黑子),轮到你落子");
else
sprintf(status_message, "局域网联机 - 你是客机(白子),等待对手落子...");
update_ui_labels();
// 强制初始重绘
if (board_canvas)
{
IupUpdate(board_canvas);
}
// 启动网络轮询定时器
timer = IupTimer();
IupSetCallback(timer, "ACTION_CB", (Icallback)timer_cb);
IupSetAttribute(timer, "TIME", "50"); // 50ms 轮询一次
IupSetAttribute(timer, "RUN", "YES");
printf("DEBUG: start_network_game_gui end\n");
}
-394
View File
@@ -1,394 +0,0 @@
#include <iup.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "gui_menu.h"
#include "gui.h"
#include "gui_internal.h"
#include "globals.h"
#include "config.h"
#include "network.h"
Ihandle *menu_dlg = NULL;
static int btn_pvp_cb(Ihandle *ih)
{
(void)ih;
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
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
return IUP_DEFAULT;
}
// --- 网络对战相关回调 ---
static int btn_network_host_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
int port = IupGetInt(txt_port, "VALUE");
if (port <= 0 || port > 65535) port = DEFAULT_PORT;
if (create_server(port))
{
IupMessage("成功", "房间创建成功,等待玩家加入...");
IupHide(dlg);
start_network_game_gui();
IupHide(menu_dlg);
}
else
{
IupMessage("错误", "创建房间失败,可能是端口被占用");
}
return IUP_DEFAULT;
}
static int btn_network_join_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_ip = IupGetDialogChild(dlg, "NET_IP");
Ihandle *txt_port = IupGetDialogChild(dlg, "NET_PORT");
char *ip = IupGetAttribute(txt_ip, "VALUE");
int port = IupGetInt(txt_port, "VALUE");
if (port <= 0 || port > 65535) port = DEFAULT_PORT;
if (connect_to_server(ip, port))
{
IupMessage("成功", "成功加入房间!");
IupHide(dlg);
start_network_game_gui();
IupHide(menu_dlg);
}
else
{
IupMessage("错误", "加入房间失败,请检查IP和端口");
}
return IUP_DEFAULT;
}
static int btn_network_cancel_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
IupHide(dlg);
return IUP_DEFAULT;
}
static int btn_network_cb(Ihandle *ih)
{
(void)ih;
printf("DEBUG: Opening Network Menu\n");
Ihandle *txt_ip = IupText(NULL);
IupSetAttribute(txt_ip, "NAME", "NET_IP");
IupSetAttribute(txt_ip, "VALUE", "127.0.0.1");
IupSetAttribute(txt_ip, "SIZE", "100x");
Ihandle *txt_port = IupText(NULL);
IupSetAttribute(txt_port, "NAME", "NET_PORT");
char port_str[16];
sprintf(port_str, "%d", DEFAULT_PORT);
IupSetAttribute(txt_port, "VALUE", port_str);
IupSetAttribute(txt_port, "SIZE", "50x");
Ihandle *btn_host = IupButton("创建房间", NULL);
IupSetCallback(btn_host, "ACTION", (Icallback)btn_network_host_cb);
Ihandle *btn_join = IupButton("加入房间", NULL);
IupSetCallback(btn_join, "ACTION", (Icallback)btn_network_join_cb);
Ihandle *btn_cancel = IupButton("取消", NULL);
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_network_cancel_cb);
Ihandle *vbox = IupVbox(
IupHbox(IupLabel("目标 IP: "), txt_ip, NULL),
IupHbox(IupLabel("端口: "), txt_port, NULL),
IupLabel(""),
IupHbox(btn_host, btn_join, btn_cancel, NULL),
NULL
);
IupSetAttribute(vbox, "MARGIN", "20x20");
IupSetAttribute(vbox, "GAP", "10");
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
Ihandle *dlg = IupDialog(vbox);
IupSetAttribute(dlg, "TITLE", "局域网联机");
IupSetAttribute(dlg, "RESIZE", "NO");
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
IupDestroy(dlg);
return IUP_DEFAULT;
}
// --- 网络对战结束 ---
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
return IUP_DEFAULT;
}
static int btn_save_settings_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
// Get values
Ihandle *txt_board_size = IupGetDialogChild(dlg, "BOARD_SIZE");
Ihandle *tgl_forbidden = IupGetDialogChild(dlg, "FORBIDDEN");
Ihandle *tgl_timer = IupGetDialogChild(dlg, "TIMER");
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
Ihandle *lst_ai = IupGetDialogChild(dlg, "AI_DIFFICULTY");
// Update globals
int new_size = IupGetInt(txt_board_size, "VALUE");
if (new_size < MIN_BOARD_SIZE)
new_size = MIN_BOARD_SIZE;
if (new_size > MAX_BOARD_SIZE)
new_size = MAX_BOARD_SIZE;
BOARD_SIZE = new_size;
use_forbidden_moves = IupGetInt(tgl_forbidden, "VALUE");
use_timer = IupGetInt(tgl_timer, "VALUE");
if (use_timer)
{
int minutes = IupGetInt(txt_time_limit, "VALUE");
if (minutes < 1)
minutes = 1;
time_limit = minutes * 60;
}
int ai_level = IupGetInt(lst_ai, "VALUE");
if (ai_level < 1)
ai_level = 1;
if (ai_level > 5)
ai_level = 5;
ai_difficulty = ai_level;
defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1;
// Save config
save_game_config();
IupHide(dlg);
return IUP_DEFAULT;
}
static int btn_cancel_settings_cb(Ihandle *ih)
{
Ihandle *dlg = IupGetDialog(ih);
IupHide(dlg);
return IUP_DEFAULT;
}
static int tgl_timer_cb(Ihandle *ih, int state)
{
Ihandle *dlg = IupGetDialog(ih);
Ihandle *txt_time_limit = IupGetDialogChild(dlg, "TIME_LIMIT");
IupSetAttribute(txt_time_limit, "ACTIVE", state ? "YES" : "NO");
return IUP_DEFAULT;
}
static int btn_settings_cb(Ihandle *ih)
{
(void)ih;
// 1. Board Size
Ihandle *lbl_board_size = IupLabel("棋盘大小 (5-25):");
Ihandle *txt_board_size = IupText(NULL);
IupSetAttribute(txt_board_size, "NAME", "BOARD_SIZE");
IupSetAttribute(txt_board_size, "SPIN", "YES");
IupSetAttribute(txt_board_size, "SPINMIN", "5");
IupSetAttribute(txt_board_size, "SPINMAX", "25");
IupSetInt(txt_board_size, "VALUE", BOARD_SIZE);
IupSetAttribute(txt_board_size, "SIZE", "50x");
// 2. Forbidden Moves
Ihandle *tgl_forbidden = IupToggle("启用禁手规则", NULL);
IupSetAttribute(tgl_forbidden, "NAME", "FORBIDDEN");
IupSetInt(tgl_forbidden, "VALUE", use_forbidden_moves);
// 3. Timer
Ihandle *tgl_timer = IupToggle("启用计时器", NULL);
IupSetAttribute(tgl_timer, "NAME", "TIMER");
IupSetInt(tgl_timer, "VALUE", use_timer);
IupSetCallback(tgl_timer, "ACTION", (Icallback)tgl_timer_cb);
// 4. Time Limit
Ihandle *lbl_time_limit = IupLabel("时间限制 (分钟):");
Ihandle *txt_time_limit = IupText(NULL);
IupSetAttribute(txt_time_limit, "NAME", "TIME_LIMIT");
IupSetAttribute(txt_time_limit, "SPIN", "YES");
IupSetAttribute(txt_time_limit, "SPINMIN", "1");
IupSetAttribute(txt_time_limit, "SPINMAX", "60");
IupSetInt(txt_time_limit, "VALUE", time_limit / 60);
IupSetAttribute(txt_time_limit, "ACTIVE", use_timer ? "YES" : "NO");
IupSetAttribute(txt_time_limit, "SIZE", "50x");
// 5. AI Difficulty
Ihandle *lbl_ai = IupLabel("AI 难度 (1-5):");
Ihandle *lst_ai = IupList(NULL);
IupSetAttribute(lst_ai, "NAME", "AI_DIFFICULTY");
IupSetAttribute(lst_ai, "DROPDOWN", "YES");
IupSetAttribute(lst_ai, "1", "1 (简单)");
IupSetAttribute(lst_ai, "2", "2 (普通)");
IupSetAttribute(lst_ai, "3", "3 (中等)");
IupSetAttribute(lst_ai, "4", "4 (困难)");
IupSetAttribute(lst_ai, "5", "5 (专家)");
IupSetInt(lst_ai, "VALUE", ai_difficulty);
IupSetAttribute(lst_ai, "SIZE", "80x");
// Buttons
Ihandle *btn_save = IupButton("保存", NULL);
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb);
IupSetAttribute(btn_save, "SIZE", "60x");
Ihandle *btn_cancel = IupButton("取消", NULL);
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_cancel_settings_cb);
IupSetAttribute(btn_cancel, "SIZE", "60x");
// Layout
Ihandle *hbox_board = IupHbox(lbl_board_size, txt_board_size, NULL);
IupSetAttribute(hbox_board, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_board, "GAP", "10");
Ihandle *hbox_time = IupHbox(lbl_time_limit, txt_time_limit, NULL);
IupSetAttribute(hbox_time, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_time, "GAP", "10");
Ihandle *hbox_ai = IupHbox(lbl_ai, lst_ai, NULL);
IupSetAttribute(hbox_ai, "ALIGNMENT", "ACENTER");
IupSetAttribute(hbox_ai, "GAP", "10");
Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
IupSetAttribute(hbox_btns, "GAP", "20");
IupSetAttribute(hbox_btns, "MARGIN", "10x0");
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
Ihandle *vbox = IupVbox(
hbox_board,
tgl_forbidden,
tgl_timer,
hbox_time,
hbox_ai,
IupLabel(NULL), // Spacer
hbox_btns,
NULL);
IupSetAttribute(vbox, "GAP", "15");
IupSetAttribute(vbox, "MARGIN", "30x30");
Ihandle *dlg = IupDialog(vbox);
IupSetAttribute(dlg, "TITLE", "游戏设置");
IupSetAttribute(dlg, "RESIZE", "NO");
IupSetAttribute(dlg, "MINBOX", "NO");
IupSetAttribute(dlg, "MAXBOX", "NO");
IupPopup(dlg, IUP_CENTER, IUP_CENTER);
IupDestroy(dlg);
return IUP_DEFAULT;
}
static int btn_exit_cb(Ihandle *ih)
{
(void)ih;
cleanup_gui(); // 清理GUI资源
exit(0);
return IUP_DEFAULT;
}
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");
IupSetAttribute(lbl_title, "ALIGNMENT", "ACENTER");
Ihandle *btn_pvp = IupButton("玩家对战 (PvP)", NULL);
IupSetCallback(btn_pvp, "ACTION", (Icallback)btn_pvp_cb);
IupSetAttribute(btn_pvp, "SIZE", "120x30");
IupSetAttribute(btn_pvp, "FONT", "SimHei, 12");
Ihandle *btn_pve = IupButton("人机对战 (PvE)", NULL);
IupSetCallback(btn_pve, "ACTION", (Icallback)btn_pve_cb);
IupSetAttribute(btn_pve, "SIZE", "120x30");
IupSetAttribute(btn_pve, "FONT", "SimHei, 12");
Ihandle *btn_net = IupButton("局域网联机", NULL);
IupSetCallback(btn_net, "ACTION", (Icallback)btn_network_cb);
IupSetAttribute(btn_net, "SIZE", "120x30");
IupSetAttribute(btn_net, "FONT", "SimHei, 12");
Ihandle *btn_replay = IupButton("复盘模式", NULL);
IupSetCallback(btn_replay, "ACTION", (Icallback)btn_replay_cb);
IupSetAttribute(btn_replay, "SIZE", "120x30");
IupSetAttribute(btn_replay, "FONT", "SimHei, 12");
Ihandle *btn_settings = IupButton("设置", NULL);
IupSetCallback(btn_settings, "ACTION", (Icallback)btn_settings_cb);
IupSetAttribute(btn_settings, "SIZE", "120x30");
IupSetAttribute(btn_settings, "FONT", "SimHei, 12");
Ihandle *btn_exit = IupButton("退出", NULL);
IupSetCallback(btn_exit, "ACTION", (Icallback)btn_exit_cb);
IupSetAttribute(btn_exit, "SIZE", "120x30");
IupSetAttribute(btn_exit, "FONT", "SimHei, 12");
Ihandle *vbox = IupVbox(
lbl_title,
btn_pvp,
btn_pve,
btn_net,
btn_replay,
btn_settings,
btn_exit,
NULL);
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
IupSetAttribute(vbox, "GAP", "15");
IupSetAttribute(vbox, "MARGIN", "40x40");
menu_dlg = IupDialog(vbox);
IupSetAttribute(menu_dlg, "TITLE", "五子棋 - 主菜单");
IupSetAttribute(menu_dlg, "RESIZE", "NO");
IupSetAttribute(menu_dlg, "MINBOX", "NO");
IupSetAttribute(menu_dlg, "MAXBOX", "NO");
// 设置对话框关闭回调 (点X关闭程序)
IupSetCallback(menu_dlg, "CLOSE_CB", (Icallback)btn_exit_cb);
IupMap(menu_dlg); // Map immediately
}
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()
{
if (menu_dlg)
IupHide(menu_dlg);
}
+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);
}
+67 -13
View File
@@ -17,16 +17,22 @@
/**
* @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));
network_state.port = DEFAULT_PORT;
network_state.port = DEFAULT_NETWORK_PORT;
return true;
}
@@ -44,8 +50,13 @@ void cleanup_network()
network_state.host = NULL;
}
enet_deinitialize();
network_state.is_connected = false;
if (enet_initialized)
{
enet_deinitialize();
enet_initialized = false;
}
memset(&network_state, 0, sizeof(NetworkGameState));
}
/**
@@ -53,6 +64,9 @@ void cleanup_network()
*/
bool create_server(int port)
{
if (!init_network())
return false;
ENetAddress address;
// 绑定所有接口
@@ -117,6 +131,9 @@ bool create_server(int port)
*/
bool connect_to_server(const char *ip, int port)
{
if (!init_network())
return false;
// 创建客户端主机
network_state.host = (void *)enet_host_create(NULL, // 创建客户端
1, // 仅允许1个传出连接
@@ -286,17 +303,54 @@ bool is_network_connected()
}
/**
* @brief IP地址
* @brief IP地址
*/
bool get_local_ip(char *ip_buffer, int buffer_size)
{
// ENet 没有直接获取本机局域网 IP 的简单跨平台函数。
// 这里我们可以回退到原生 socket 方法,或者简单返回本地回环。
// 为了不引入额外的系统头文件,暂时返回通用提示。
// 在真实应用中,可以保留之前的 gethostname/gethostbyname 逻辑。
strncpy(ip_buffer, "查看本机网络适配器", buffer_size - 1);
#ifdef _WIN32
// 使用 Winsock 获取本机 IPws2_32 已链接)
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR)
{
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return false;
}
struct hostent *host = gethostbyname(hostname);
if (host == NULL || host->h_addr_list[0] == NULL)
{
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return false;
}
// 遍历所有地址,找一个非回环的 IPv4 地址
for (int i = 0; host->h_addr_list[i] != NULL; i++)
{
struct in_addr addr;
memcpy(&addr, host->h_addr_list[i], sizeof(struct in_addr));
const char *ip = inet_ntoa(addr);
if (ip && strcmp(ip, "127.0.0.1") != 0)
{
strncpy(ip_buffer, ip, buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true;
}
}
// 没找到非回环地址,返回第一个
struct in_addr addr;
memcpy(&addr, host->h_addr_list[0], sizeof(struct in_addr));
const char *ip = inet_ntoa(addr);
strncpy(ip_buffer, ip ? ip : "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true; // 总是返回 true 以允许服务器继续启动
return true;
#else
strncpy(ip_buffer, "127.0.0.1", buffer_size - 1);
ip_buffer[buffer_size - 1] = '\0';
return true;
#endif
}
/**
+12 -25
View File
@@ -23,29 +23,6 @@
#include <dirent.h>
#endif
/**
* @brief
* @note :
* 1.
* 2.
* 3. :
* - /
* - (/AI)
* - (1-based坐标)
* -
* 4. Enter键控制步骤前进
* 5. :
* -
* -
* - MVP
* @note :
* - 使
* - 1-based方便用户理解
* -
* - calculate_final_score()
*/
void calculate_game_scores();
/**
* @brief
*/
@@ -99,7 +76,7 @@ void calculate_game_scores()
void display_game_scores(int game_mode)
{
printf("\n===== 对局评分 =====\n");
double sum_score = (long double)player1_final_score + (long double)player2_final_score;
double sum_score = (double)player1_final_score + (double)player2_final_score;
if (sum_score > 0)
{
@@ -284,7 +261,7 @@ int load_game_from_file(const char *filename)
FILE *file = fopen(fullpath, "r");
if (!file)
{
return false;
return 0;
}
// 跳过CSV文件头部行
@@ -379,11 +356,21 @@ int load_game_from_file(const char *filename)
while (fgets(buffer, sizeof(buffer), file) != NULL)
{
if (step_count >= MAX_STEPS)
{
break; // 防止数组越界
}
if (sscanf(buffer, "%d,%d,%d,%d", &step_num, &steps[step_count].player, &steps[step_count].x, &steps[step_count].y) == 4)
{
// 将1-based坐标转换为0-based坐标
steps[step_count].x--;
steps[step_count].y--;
// 验证坐标合法性
if (steps[step_count].x < 0 || steps[step_count].x >= size ||
steps[step_count].y < 0 || steps[step_count].y >= size)
{
continue; // 跳过非法坐标
}
step_count++;
}
}