mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-29 17:15:55 +08:00
feat: 集成大模型AI、重构构建系统并修复多项代码质量问题
- 构建系统:Makefile 迁移至 CMakeLists.txt,支持 cJSON 和 WinHTTP - 项目结构:src/ 按功能拆分为 core/、gui/、network/、record/、llm/ 子目录 - 新功能:集成大模型 AI(WinHTTP + cJSON,兼容 OpenAI 协议),支持异步请求 - 渲染修复:IupDraw* 替换为 Windows GDI,修复画布黑屏问题 - 网络修复:ENet 初始化幂等化,实现真实 get_local_ip() (Winsock) - 代码质量:删除死代码 (dfs/count_threats_in_direction),修复头文件守卫, sprintf→snprintf 防溢出,strncpy 安全终止,GDI 资源泄漏修复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "gui_menu.h"
|
||||
#include "globals.h"
|
||||
#include <iup.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// 全局变量定义
|
||||
Ihandle *dlg = NULL;
|
||||
Ihandle *board_canvas = NULL;
|
||||
Ihandle *lbl_player = NULL;
|
||||
Ihandle *lbl_status = NULL;
|
||||
int gui_game_mode = 0; // 0: PvP, 1: PvE, 2: Replay, 3: Network
|
||||
int replay_total_steps = 0; // 复盘总步数
|
||||
|
||||
/**
|
||||
* @brief 初始化GUI
|
||||
*/
|
||||
int init_gui()
|
||||
{
|
||||
if (IupOpen(NULL, NULL) == IUP_ERROR)
|
||||
{
|
||||
printf("IupOpen failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 启用UTF-8模式,确保中文正常显示
|
||||
IupSetGlobal("UTF8MODE", "YES");
|
||||
|
||||
create_main_menu();
|
||||
show_main_menu();
|
||||
|
||||
printf("图形化界面初始化成功!(IUP)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清理GUI资源
|
||||
*/
|
||||
void cleanup_gui()
|
||||
{
|
||||
if (dlg)
|
||||
{
|
||||
IupDestroy(dlg);
|
||||
dlg = NULL;
|
||||
}
|
||||
IupClose();
|
||||
printf("图形化界面已关闭\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 运行图形化界面模式
|
||||
* @note 包含初始化、主循环和清理
|
||||
*/
|
||||
void run_gui_mode()
|
||||
{
|
||||
if (init_gui() == 0)
|
||||
{
|
||||
IupMainLoop(); // 使用IUP的主循环
|
||||
cleanup_gui();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 显示消息
|
||||
*/
|
||||
void show_message(const char *message)
|
||||
{
|
||||
strncpy(status_message, message, sizeof(status_message) - 1);
|
||||
status_message[sizeof(status_message) - 1] = '\0';
|
||||
update_ui_labels();
|
||||
printf("%s\n", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新UI标签状态
|
||||
*/
|
||||
void update_ui_labels()
|
||||
{
|
||||
if (lbl_player)
|
||||
{
|
||||
if (gui_game_mode == 2) // 复盘模式
|
||||
{
|
||||
char buffer[64];
|
||||
sprintf(buffer, "进度: %d / %d", step_count, replay_total_steps);
|
||||
IupSetAttribute(lbl_player, "TITLE", buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (current_player_gui == PLAYER)
|
||||
IupSetAttribute(lbl_player, "TITLE", "当前玩家: 黑子 (玩家)");
|
||||
else
|
||||
IupSetAttribute(lbl_player, "TITLE", "当前玩家: 白子 (AI/玩家2)");
|
||||
}
|
||||
}
|
||||
|
||||
if (lbl_status)
|
||||
{
|
||||
IupSetAttribute(lbl_status, "TITLE", status_message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 屏幕坐标转棋盘坐标
|
||||
*/
|
||||
int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y)
|
||||
{
|
||||
int rel_x = screen_x - BOARD_OFFSET_X;
|
||||
int rel_y = screen_y - BOARD_OFFSET_Y;
|
||||
|
||||
*board_x = (rel_y + CELL_SIZE / 2) / CELL_SIZE; // 注意:行号对应y
|
||||
*board_y = (rel_x + CELL_SIZE / 2) / CELL_SIZE; // 列号对应x
|
||||
|
||||
return (*board_x >= 0 && *board_x < BOARD_SIZE &&
|
||||
*board_y >= 0 && *board_y < BOARD_SIZE);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "gui_internal.h"
|
||||
#include "globals.h"
|
||||
#include "gobang.h" // for BOARD_SIZE, etc.
|
||||
#include <iup.h>
|
||||
#include <iupdraw.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/**
|
||||
* @brief 设置绘图颜色
|
||||
*/
|
||||
void set_draw_color(Ihandle *ih, unsigned char r, unsigned char g, unsigned char b)
|
||||
{
|
||||
char color[32];
|
||||
sprintf(color, "%d %d %d", r, g, b);
|
||||
IupSetAttribute(ih, "DRAWCOLOR", color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制棋盘
|
||||
*/
|
||||
void draw_board_iup(Ihandle *ih)
|
||||
{
|
||||
set_draw_color(ih, 0, 0, 0); // 黑色
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "STROKE");
|
||||
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
// 横线
|
||||
IupDrawLine(ih,
|
||||
BOARD_OFFSET_X,
|
||||
BOARD_OFFSET_Y + i * CELL_SIZE,
|
||||
BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE,
|
||||
BOARD_OFFSET_Y + i * CELL_SIZE);
|
||||
// 竖线
|
||||
IupDrawLine(ih,
|
||||
BOARD_OFFSET_X + i * CELL_SIZE,
|
||||
BOARD_OFFSET_Y,
|
||||
BOARD_OFFSET_X + i * CELL_SIZE,
|
||||
BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE);
|
||||
}
|
||||
|
||||
// 星位/天元
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
int stars[] = {3, 7, 11}; // 15路棋盘的星位坐标
|
||||
if (BOARD_SIZE == 15)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
for (int j = 0; j < 3; j++)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + stars[i] * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + stars[j] * CELL_SIZE;
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int center = BOARD_SIZE / 2;
|
||||
int cx = BOARD_OFFSET_X + center * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + center * CELL_SIZE;
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制棋子
|
||||
*/
|
||||
void draw_stones_iup(Ihandle *ih)
|
||||
{
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] != EMPTY)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + j * CELL_SIZE; // j是x坐标(列)
|
||||
int cy = BOARD_OFFSET_Y + i * CELL_SIZE; // i是y坐标(行)
|
||||
|
||||
if (board[i][j] == PLAYER)
|
||||
{
|
||||
// 黑子
|
||||
set_draw_color(ih, 0, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 白子
|
||||
set_draw_color(ih, 255, 255, 255);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
|
||||
set_draw_color(ih, 0, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "STROKE");
|
||||
IupDrawArc(ih, cx - STONE_RADIUS, cy - STONE_RADIUS, cx + STONE_RADIUS, cy + STONE_RADIUS, 0.0, 360.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记最后落子位置 (红色小点)
|
||||
if (step_count > 0 && step_count <= MAX_STEPS)
|
||||
{
|
||||
// 绘制最后一步的标记
|
||||
// 最后一步的坐标是 steps[step_count-1]
|
||||
// 所以 step_count-1 是最后一步的索引
|
||||
|
||||
Step last = steps[step_count - 1];
|
||||
int cx = BOARD_OFFSET_X + last.y * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + last.x * CELL_SIZE;
|
||||
|
||||
set_draw_color(ih, 255, 0, 0);
|
||||
IupSetAttribute(ih, "DRAWSTYLE", "FILL");
|
||||
IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
#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(240, 217, 181));
|
||||
HBRUSH black_brush = CreateSolidBrush(RGB(0, 0, 0));
|
||||
HBRUSH white_brush = CreateSolidBrush(RGB(255, 255, 255));
|
||||
HBRUSH red_brush = CreateSolidBrush(RGB(255, 0, 0));
|
||||
HPEN grid_pen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
|
||||
HPEN stone_pen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
|
||||
|
||||
// 1. 填充背景
|
||||
FillRect(hdc, &rc, bg_brush);
|
||||
|
||||
// 2. 绘制棋盘网格
|
||||
HPEN prev_pen = (HPEN)SelectObject(hdc, grid_pen);
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
{
|
||||
MoveToEx(hdc, BOARD_OFFSET_X, BOARD_OFFSET_Y + i * CELL_SIZE, NULL);
|
||||
LineTo(hdc, BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE, BOARD_OFFSET_Y + i * CELL_SIZE);
|
||||
MoveToEx(hdc, BOARD_OFFSET_X + i * CELL_SIZE, BOARD_OFFSET_Y, NULL);
|
||||
LineTo(hdc, BOARD_OFFSET_X + i * CELL_SIZE, BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE);
|
||||
}
|
||||
|
||||
// 3. 星位/天元
|
||||
SelectObject(hdc, black_brush);
|
||||
if (BOARD_SIZE == 15)
|
||||
{
|
||||
int stars[] = {3, 7, 11};
|
||||
for (int si = 0; si < 3; si++)
|
||||
for (int sj = 0; sj < 3; sj++)
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + stars[si] * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + stars[sj] * CELL_SIZE;
|
||||
Ellipse(hdc, cx - 3, cy - 3, cx + 4, cy + 4);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int cx = BOARD_OFFSET_X + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + (BOARD_SIZE / 2) * CELL_SIZE;
|
||||
Ellipse(hdc, cx - 3, cy - 3, cx + 4, cy + 4);
|
||||
}
|
||||
|
||||
// 4. 绘制棋子
|
||||
SelectObject(hdc, stone_pen);
|
||||
for (int i = 0; i < BOARD_SIZE; i++)
|
||||
for (int j = 0; j < BOARD_SIZE; j++)
|
||||
{
|
||||
if (board[i][j] == EMPTY)
|
||||
continue;
|
||||
|
||||
int cx = BOARD_OFFSET_X + j * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + i * CELL_SIZE;
|
||||
|
||||
if (board[i][j] == PLAYER)
|
||||
SelectObject(hdc, black_brush);
|
||||
else
|
||||
SelectObject(hdc, white_brush);
|
||||
|
||||
Ellipse(hdc, cx - STONE_RADIUS, cy - STONE_RADIUS,
|
||||
cx + STONE_RADIUS + 1, cy + STONE_RADIUS + 1);
|
||||
}
|
||||
|
||||
// 5. 标记最后落子位置(红色小方块)
|
||||
if (step_count > 0 && step_count <= MAX_STEPS)
|
||||
{
|
||||
Step last = steps[step_count - 1];
|
||||
int cx = BOARD_OFFSET_X + last.y * CELL_SIZE;
|
||||
int cy = BOARD_OFFSET_Y + last.x * CELL_SIZE;
|
||||
RECT mark = {cx - 3, cy - 3, cx + 4, cy + 4};
|
||||
FillRect(hdc, &mark, red_brush);
|
||||
}
|
||||
|
||||
// 恢复原始 GDI 对象,然后清理
|
||||
SelectObject(hdc, prev_pen);
|
||||
ReleaseDC(hwnd, hdc);
|
||||
|
||||
DeleteObject(bg_brush);
|
||||
DeleteObject(black_brush);
|
||||
DeleteObject(white_brush);
|
||||
DeleteObject(red_brush);
|
||||
DeleteObject(grid_pen);
|
||||
DeleteObject(stone_pen);
|
||||
|
||||
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 * 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");
|
||||
IupSetAttribute(board_canvas, "BORDER", "NO");
|
||||
IupSetAttribute(board_canvas, "BGCOLOR", "240 217 181");
|
||||
|
||||
// 创建标签 (玩家信息和游戏状态)
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
#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 *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_NETWORK_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;
|
||||
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;
|
||||
|
||||
// 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");
|
||||
|
||||
// === 大模型AI设置 ===
|
||||
Ihandle *lbl_llm_sep = IupLabel("--- 大模型AI设置 ---");
|
||||
IupSetAttribute(lbl_llm_sep, "ALIGNMENT", "ACENTER");
|
||||
|
||||
// 6. AI 模式选择
|
||||
Ihandle *lbl_ai_mode = IupLabel("AI模式:");
|
||||
Ihandle *lst_ai_mode = IupList(NULL);
|
||||
IupSetAttribute(lst_ai_mode, "NAME", "AI_MODE");
|
||||
IupSetAttribute(lst_ai_mode, "DROPDOWN", "YES");
|
||||
IupSetAttribute(lst_ai_mode, "1", "算法AI (本地)");
|
||||
IupSetAttribute(lst_ai_mode, "2", "大模型AI (在线)");
|
||||
IupSetInt(lst_ai_mode, "VALUE", llm_use ? 2 : 1);
|
||||
IupSetAttribute(lst_ai_mode, "SIZE", "120x");
|
||||
|
||||
// 7. LLM API 地址
|
||||
Ihandle *lbl_endpoint = IupLabel("API地址:");
|
||||
Ihandle *txt_endpoint = IupText(NULL);
|
||||
IupSetAttribute(txt_endpoint, "NAME", "LLM_ENDPOINT");
|
||||
IupSetAttribute(txt_endpoint, "VALUE", llm_endpoint);
|
||||
IupSetAttribute(txt_endpoint, "SIZE", "250x");
|
||||
|
||||
// 8. LLM API Key
|
||||
Ihandle *lbl_apikey = IupLabel("API Key:");
|
||||
Ihandle *txt_apikey = IupText(NULL);
|
||||
IupSetAttribute(txt_apikey, "NAME", "LLM_API_KEY");
|
||||
IupSetAttribute(txt_apikey, "VALUE", llm_api_key);
|
||||
IupSetAttribute(txt_apikey, "PASSWORD", "YES");
|
||||
IupSetAttribute(txt_apikey, "SIZE", "200x");
|
||||
|
||||
// 9. LLM 模型名
|
||||
Ihandle *lbl_model = IupLabel("模型名称:");
|
||||
Ihandle *txt_model = IupText(NULL);
|
||||
IupSetAttribute(txt_model, "NAME", "LLM_MODEL");
|
||||
IupSetAttribute(txt_model, "VALUE", llm_model);
|
||||
IupSetAttribute(txt_model, "SIZE", "150x");
|
||||
|
||||
// Buttons
|
||||
Ihandle *btn_save = IupButton("保存", NULL);
|
||||
IupSetCallback(btn_save, "ACTION", (Icallback)btn_save_settings_cb);
|
||||
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");
|
||||
|
||||
// LLM 设置布局
|
||||
Ihandle *hbox_ai_mode = IupHbox(lbl_ai_mode, lst_ai_mode, NULL);
|
||||
IupSetAttribute(hbox_ai_mode, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_ai_mode, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_endpoint = IupHbox(lbl_endpoint, txt_endpoint, NULL);
|
||||
IupSetAttribute(hbox_endpoint, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_endpoint, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_apikey = IupHbox(lbl_apikey, txt_apikey, NULL);
|
||||
IupSetAttribute(hbox_apikey, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_apikey, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_model = IupHbox(lbl_model, txt_model, NULL);
|
||||
IupSetAttribute(hbox_model, "ALIGNMENT", "ACENTER");
|
||||
IupSetAttribute(hbox_model, "GAP", "10");
|
||||
|
||||
Ihandle *hbox_btns = IupHbox(btn_save, btn_cancel, NULL);
|
||||
IupSetAttribute(hbox_btns, "GAP", "20");
|
||||
IupSetAttribute(hbox_btns, "MARGIN", "10x0");
|
||||
IupSetAttribute(hbox_btns, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *vbox = IupVbox(
|
||||
hbox_board,
|
||||
tgl_forbidden,
|
||||
tgl_timer,
|
||||
hbox_time,
|
||||
hbox_ai,
|
||||
IupLabel(NULL), // Spacer
|
||||
lbl_llm_sep,
|
||||
hbox_ai_mode,
|
||||
hbox_endpoint,
|
||||
hbox_apikey,
|
||||
hbox_model,
|
||||
IupLabel(NULL), // Spacer
|
||||
hbox_btns,
|
||||
NULL);
|
||||
|
||||
IupSetAttribute(vbox, "GAP", "10");
|
||||
IupSetAttribute(vbox, "MARGIN", "20x20");
|
||||
|
||||
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;
|
||||
|
||||
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()
|
||||
{
|
||||
if (!menu_dlg)
|
||||
create_main_menu();
|
||||
IupShowXY(menu_dlg, IUP_CENTER, IUP_CENTER);
|
||||
}
|
||||
|
||||
void hide_main_menu()
|
||||
{
|
||||
if (menu_dlg)
|
||||
IupHide(menu_dlg);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "gui.h"
|
||||
#include "gui_internal.h"
|
||||
#include "gui_menu.h"
|
||||
#include "globals.h"
|
||||
#include "gobang.h"
|
||||
#include "record.h"
|
||||
#include <iup.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// 复盘功能函数
|
||||
|
||||
/**
|
||||
* @brief 复盘文件选择对话框的确定回调
|
||||
*/
|
||||
int btn_replay_sel_ok_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg_sel = IupGetDialog(ih);
|
||||
Ihandle *list = (Ihandle *)IupGetAttribute(dlg_sel, "MY_LIST");
|
||||
char *val = IupGetAttribute(list, "VALUE"); // 返回选中项索引
|
||||
if (!val)
|
||||
return IUP_DEFAULT;
|
||||
|
||||
int index = atoi(val);
|
||||
char *selected_file = IupGetAttributeId(list, "", index);
|
||||
|
||||
if (selected_file)
|
||||
{
|
||||
printf("DEBUG: Loading file %s\n", selected_file);
|
||||
if (load_game_from_file(selected_file))
|
||||
{
|
||||
printf("DEBUG: File loaded successfully\n");
|
||||
replay_total_steps = step_count;
|
||||
step_count = 0;
|
||||
gui_game_mode = 2; // 复盘模式
|
||||
|
||||
// 关闭选择对话框
|
||||
IupHide(dlg_sel);
|
||||
|
||||
// 启动游戏窗口
|
||||
create_game_window();
|
||||
IupShowXY(dlg, IUP_CENTER, IUP_CENTER); // 显示游戏窗口
|
||||
|
||||
sprintf(status_message, "复盘模式 - %s", selected_file);
|
||||
update_ui_labels();
|
||||
if (board_canvas)
|
||||
IupUpdate(board_canvas);
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
IupMessage("错误", "无法加载复盘文件");
|
||||
}
|
||||
}
|
||||
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 复盘文件选择对话框的取消回调
|
||||
*/
|
||||
int btn_replay_sel_cancel_cb(Ihandle *ih)
|
||||
{
|
||||
Ihandle *dlg_sel = IupGetDialog(ih);
|
||||
IupHide(dlg_sel);
|
||||
IupDestroy(dlg_sel);
|
||||
show_main_menu(); // 返回主菜单
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 上一步按钮回调
|
||||
*/
|
||||
int btn_replay_prev_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (step_count > 0)
|
||||
{
|
||||
step_count--;
|
||||
Step s = steps[step_count];
|
||||
board[s.x][s.y] = EMPTY;
|
||||
sprintf(status_message, "回退一步");
|
||||
update_ui_labels();
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 下一步按钮回调
|
||||
*/
|
||||
int btn_replay_next_cb(Ihandle *ih)
|
||||
{
|
||||
(void)ih;
|
||||
if (step_count < replay_total_steps && step_count >= 0) // 确保步数有效
|
||||
{
|
||||
Step s = steps[step_count];
|
||||
board[s.x][s.y] = s.player;
|
||||
step_count++;
|
||||
sprintf(status_message, "前进一步");
|
||||
update_ui_labels();
|
||||
IupUpdate(board_canvas);
|
||||
}
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 选择复盘文件界面
|
||||
*/
|
||||
void select_replay_file_gui()
|
||||
{
|
||||
printf("DEBUG: select_replay_file_gui start\n");
|
||||
// 列出 records/ 目录下的文件
|
||||
char record_files[100][100];
|
||||
int file_count = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
WIN32_FIND_DATA ffd;
|
||||
HANDLE hFind = FindFirstFile("records\\*", &ffd);
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
||||
{
|
||||
// 确保不溢出缓冲区
|
||||
if (file_count < 100)
|
||||
{
|
||||
strncpy(record_files[file_count], ffd.cFileName, 99);
|
||||
record_files[file_count][99] = '\0';
|
||||
file_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (FindNextFile(hFind, &ffd) != 0);
|
||||
FindClose(hFind);
|
||||
}
|
||||
#endif
|
||||
printf("DEBUG: Found %d files\n", file_count);
|
||||
|
||||
if (file_count == 0)
|
||||
{
|
||||
IupMessage("提示", "未找到复盘记录文件 (records/*)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建列表框
|
||||
Ihandle *list = IupList(NULL);
|
||||
if (!list)
|
||||
{
|
||||
printf("ERROR: Failed to create list\n");
|
||||
return;
|
||||
}
|
||||
IupSetAttribute(list, "EXPAND", "YES");
|
||||
IupSetAttribute(list, "VISIBLELINES", "10");
|
||||
|
||||
for (int i = 0; i < file_count; i++)
|
||||
{
|
||||
IupSetAttributeId(list, "", i + 1, record_files[i]);
|
||||
}
|
||||
IupSetAttribute(list, "VALUE", "1"); // 默认选择第一项
|
||||
|
||||
// 创建确定和取消按钮
|
||||
Ihandle *btn_ok = IupButton("确定", NULL);
|
||||
IupSetCallback(btn_ok, "ACTION", (Icallback)btn_replay_sel_ok_cb);
|
||||
IupSetAttribute(btn_ok, "SIZE", "60x30");
|
||||
|
||||
Ihandle *btn_cancel = IupButton("取消", NULL);
|
||||
IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_replay_sel_cancel_cb);
|
||||
IupSetAttribute(btn_cancel, "SIZE", "60x30");
|
||||
|
||||
Ihandle *vbox = IupVbox(
|
||||
IupLabel("请选择复盘文件:"),
|
||||
list,
|
||||
IupHbox(btn_ok, btn_cancel, NULL),
|
||||
NULL);
|
||||
|
||||
IupSetAttribute(vbox, "GAP", "10");
|
||||
IupSetAttribute(vbox, "MARGIN", "10x10");
|
||||
IupSetAttribute(vbox, "ALIGNMENT", "ACENTER");
|
||||
|
||||
Ihandle *dlg_sel = IupDialog(vbox);
|
||||
if (!dlg_sel)
|
||||
{
|
||||
printf("ERROR: Failed to create selection dialog\n");
|
||||
return;
|
||||
}
|
||||
IupSetAttribute(dlg_sel, "TITLE", "选择复盘文件");
|
||||
IupSetAttribute(dlg_sel, "MINBOX", "NO");
|
||||
IupSetAttribute(dlg_sel, "MAXBOX", "NO");
|
||||
IupSetAttribute(dlg_sel, "RESIZE", "NO");
|
||||
|
||||
// 存储列表框句柄到对话框属性
|
||||
IupSetAttribute(dlg_sel, "MY_LIST", (char *)list);
|
||||
|
||||
// 显示对话框 (模态)
|
||||
IupPopup(dlg_sel, IUP_CENTER, IUP_CENTER);
|
||||
// 对话框关闭后销毁
|
||||
IupDestroy(dlg_sel);
|
||||
printf("DEBUG: select_replay_file_gui end\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 启动复盘GUI流程
|
||||
*/
|
||||
void start_replay_gui()
|
||||
{
|
||||
select_replay_file_gui();
|
||||
// 不要在这里隐藏主菜单,等待文件选择完成
|
||||
}
|
||||
Reference in New Issue
Block a user