diff --git a/Makefile b/Makefile index 095ac45..9a7e491 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,9 @@ 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.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 # 目标文件 (src/xxx.c -> obj/xxx.o) diff --git a/bin/gobang_gui.exe b/bin/gobang_gui.exe new file mode 100644 index 0000000..ecf2bac Binary files /dev/null and b/bin/gobang_gui.exe differ diff --git a/bin/iup.dll b/bin/iup.dll new file mode 100644 index 0000000..aceb679 Binary files /dev/null and b/bin/iup.dll differ diff --git a/include/gui_internal.h b/include/gui_internal.h new file mode 100644 index 0000000..14461f0 --- /dev/null +++ b/include/gui_internal.h @@ -0,0 +1,40 @@ +#ifndef GUI_INTERNAL_H +#define GUI_INTERNAL_H + +#include + +// 全局变量声明 (在 gui_core.c 中定义) +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 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); + +// 游戏窗口 (在 gui_game.c 中定义) +void create_game_window(); +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); +int btn_back_cb(Ihandle *ih); +int btn_undo_cb(Ihandle *ih); +int btn_save_cb(Ihandle *ih); + +// 复盘功能 (在 gui_replay.c 中定义) +void select_replay_file_gui(); +int btn_replay_prev_cb(Ihandle *ih); +int btn_replay_next_cb(Ihandle *ih); +int btn_replay_sel_ok_cb(Ihandle *ih); +int btn_replay_sel_cancel_cb(Ihandle *ih); + +#endif // GUI_INTERNAL_H diff --git a/src/gui.c b/src/gui.c deleted file mode 100644 index 2d7feeb..0000000 --- a/src/gui.c +++ /dev/null @@ -1,845 +0,0 @@ -/** - * @file gui.c - * @brief 图形化用户界面实现文件 - * @note 使用IUP库实现五子棋的图形化界面 - * @author 刘航宇 - * @date 2025-01-15 - */ - -#include "gui.h" -#include -#include -#include "globals.h" -#include "gobang.h" -#include "gui_menu.h" -#include "ai.h" -#include "record.h" -#include -#include -#include - -static Ihandle *dlg = NULL; -static Ihandle *board_canvas = NULL; // 重命名为 board_canvas -static Ihandle *lbl_player = NULL; -static Ihandle *lbl_status = NULL; -static int gui_loop_running = 0; -static int gui_game_mode = 0; // 0: PvP, 1: PvE, 2: Replay -static int replay_total_steps = 0; // 为了复盘而记录的总步数 - -// 回调函数 -static int action_cb(Ihandle *ih); -static int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status); -static int k_any_cb(Ihandle *ih, int c); -void create_game_window(); // 移除前向声明 -static int btn_replay_sel_ok_cb(Ihandle *ih); -static int btn_replay_sel_cancel_cb(Ihandle *ih); - -// 辅助函数:设置绘图颜色 -static 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); -} - -// 绘制棋盘 -static void draw_board_iup(Ihandle *ih) -{ - set_draw_color(ih, 0, 0, 0); // Black - 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); - } -} - -// 绘制棋子 -static 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 是最后一步的索引 - // 所以 steps[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); - } -} - -// 屏幕坐标转棋盘坐标 -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); -} - -// 更新UI标签状态 -static void update_ui_labels() -{ - if (lbl_player) - { - if (gui_game_mode == 2) // Replay - { - 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 显示消息 - */ -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); -} - -// ACTION 回调:负责重绘 -static 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; -} - -// 悔棋按钮回调 -static 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) - } - - 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; -} - -// 保存按钮回调 -static 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; -} - -// Replay Prev Callback -static 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; -} - -// Replay Next Callback -static int btn_replay_next_cb(Ihandle *ih) -{ - (void)ih; - if (step_count < replay_total_steps && step_count >= 0) // Ensure step_count is valid - { - 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; -} - -// 返回菜单回调 -static int btn_back_cb(Ihandle *ih) -{ - (void)ih; - printf("DEBUG: Back to Menu clicked\n"); - - // 1. Show main menu FIRST - show_main_menu(); - printf("DEBUG: Main menu shown\n"); - - // 2. Destroy game window - // Important: We must destroy the game window to free resources and potentially stop its event loop contribution. - // But we must NOT close the application. - // If we only Hide, it stays in memory. - // If we Destroy, it's gone. - - if (dlg) - { - // IupHide(dlg); // Hiding is safer if Destroy causes loop exit - // printf("DEBUG: Hiding game window\n"); - Ihandle *old_dlg = dlg; - dlg = NULL; // Clear global pointer first - IupDestroy(old_dlg); - printf("DEBUG: Destroyed game window\n"); - } - - return IUP_IGNORE; // Return IUP_IGNORE to prevent default processing (like closing if CLOSE_CB) -} - -// 鼠标点击回调 -static int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *status) -{ - (void)status; // Unused - if (gui_game_mode == 2) - return IUP_DEFAULT; // Replay mode: disable clicks - - if (button == IUP_BUTTON1 && pressed) - { // 左键按下 - if (game_over) - 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 (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 here means Player 2 - if (current_player_gui == PLAYER) - sprintf(status_message, "轮到黑子"); - else - 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; -} - -// 键盘回调 -static int k_any_cb(Ihandle *ih, int c) -{ - (void)ih; - if (c == K_ESC) - { - if (dlg && IupGetInt(dlg, "VISIBLE")) - { - IupHide(dlg); - show_main_menu(); - return IUP_DEFAULT; - } - } - return IUP_DEFAULT; -} - -// 创建游戏窗口 -void create_game_window() -{ - printf("DEBUG: create_game_window start\n"); - // if (dlg) - // { - // IupDestroy(dlg); // 销毁旧窗口 - // dlg = NULL; - // } - // Only destroy if it exists and is not the current dialog? - // Actually, creating a new dialog while old one is valid is fine, but we should destroy old one to save memory. - // However, if destroying dlg causes main loop to exit because it was the last visible window (if main menu was hidden)... - - if (dlg) - { - IupDestroy(dlg); - dlg = NULL; - } - - // 创建Canvas (Board) - 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("当前玩家: 黑子"); - // IupSetAttribute(lbl_player, "FONT", "SimHei, 14"); // Comment out potentially problematic font setting - - lbl_status = IupLabel("准备开始"); - // IupSetAttribute(lbl_status, "FONT", "SimHei, 12"); - - Ihandle *vbox_controls; - - if (gui_game_mode == 2) // Replay - { - 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 // Game Mode - { - 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"); - - IupMap(dlg); - 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 is global - 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"); - - // Force initial draw - if (board_canvas) - { - IupUpdate(board_canvas); - // IupFlush(); // Comment out to check if this caused the crash - } - printf("DEBUG: start_pve_game_gui end\n"); -} - -#ifdef _WIN32 -#include -#endif - -// 选择复盘文件 -static void select_replay_file_gui() -{ - // List files in records/ directory - char record_files[100][100]; - int file_count = 0; - -#ifdef _WIN32 - WIN32_FIND_DATA ffd; - HANDLE hFind = FindFirstFile("records\\*.csv", &ffd); - if (hFind != INVALID_HANDLE_VALUE) - { - do - { - if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) - { - strcpy(record_files[file_count++], ffd.cFileName); - if (file_count >= 100) - break; - } - } while (FindNextFile(hFind, &ffd) != 0); - FindClose(hFind); - } -#endif - - if (file_count == 0) - { - IupMessage("提示", "未找到复盘记录文件 (records/*.csv)"); - show_main_menu(); - return; - } - - // 创建列表框 - Ihandle *list = IupList(NULL); - 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, "MARGIN", "10x10"); - IupSetAttribute(vbox, "GAP", "10"); - IupSetAttribute(vbox, "ALIGNMENT", "ACENTER"); - - Ihandle *dlg_sel = IupDialog(vbox); - 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); -} - -static int btn_replay_sel_ok_cb(Ihandle *ih) -{ - Ihandle *dlg = IupGetDialog(ih); - Ihandle *list = (Ihandle *)IupGetAttribute(dlg, "MY_LIST"); - char *filename = IupGetAttribute(list, "VALUE"); // Returns index - int index = atoi(filename); - char *selected_file = IupGetAttributeId(list, "", index); - - if (selected_file) - { - if (load_game_from_file(selected_file)) - { - replay_total_steps = step_count; - step_count = 0; - gui_game_mode = 2; // Replay - create_game_window(); - IupShowXY(dlg, IUP_CENTER, IUP_CENTER); // 展示 - - Ihandle *dlg_sel = dlg; - IupHide(dlg_sel); - IupDestroy(dlg_sel); - - 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("错误", "无法加载复盘文件"); - } - } - - IupHide(dlg); - IupDestroy(dlg); - show_main_menu(); - return IUP_DEFAULT; -} - -static int btn_replay_sel_cancel_cb(Ihandle *ih) -{ - Ihandle *dlg = IupGetDialog(ih); - IupHide(dlg); - IupDestroy(dlg); - show_main_menu(); - return IUP_DEFAULT; -} - -void start_replay_gui() -{ - select_replay_file_gui(); -} - -/** - * @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(); - - gui_loop_running = 1; - - printf("图形化界面初始化成功!(IUP)\n"); - return 0; -} - -/** - * @brief 清理GUI资源 - */ -void cleanup_gui() -{ - if (dlg) - { - IupDestroy(dlg); - dlg = NULL; - } - IupClose(); - printf("图形化界面已关闭\n"); -} - -/** - * @brief 处理事件 - */ -int handle_events() -{ - if (!gui_loop_running) - return 0; - - int ret = IupLoopStep(); - if (ret == IUP_CLOSE) - { - gui_loop_running = 0; - return 0; - } - - return 1; -} - -/** - * @brief 渲染游戏画面 - */ -void render_game() -{ - // 事件驱动,不需要手动渲染 -} - -/** - * @brief 绘制棋盘 (保留空函数以兼容接口) - */ -void draw_board() -{ - // Implemented in action_cb -} - -/** - * @brief 绘制棋子 (保留空函数以兼容接口) - */ -void draw_stones() -{ - // Implemented in action_cb -} - -/** - * @brief 绘制UI元素 (保留空函数以兼容接口) - */ -void draw_ui_elements() -{ - // Implemented in action_cb -} - -/** - * @brief 运行图形化界面模式 - * @note 包含初始化、主循环和清理 - */ -void run_gui_mode() -{ - if (init_gui() == 0) - { - IupMainLoop(); // 使用IUP的主循环 - cleanup_gui(); - } -} \ No newline at end of file diff --git a/src/gui_core.c b/src/gui_core.c new file mode 100644 index 0000000..cae2049 --- /dev/null +++ b/src/gui_core.c @@ -0,0 +1,120 @@ +#include "gui.h" +#include "gui_internal.h" +#include "gui_menu.h" +#include "globals.h" +#include +#include +#include + +// 全局变量定义 +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 +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(); + + gui_loop_running = 1; + + 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); +} diff --git a/src/gui_draw.c b/src/gui_draw.c new file mode 100644 index 0000000..fb4a550 --- /dev/null +++ b/src/gui_draw.c @@ -0,0 +1,117 @@ +#include "gui_internal.h" +#include "globals.h" +#include "gobang.h" // for BOARD_SIZE, etc. +#include +#include +#include + +/** + * @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); + } +} diff --git a/src/gui_game.c b/src/gui_game.c new file mode 100644 index 0000000..3534011 --- /dev/null +++ b/src/gui_game.c @@ -0,0 +1,398 @@ +#include "gui.h" +#include "gui_internal.h" +#include "gui_menu.h" +#include "globals.h" +#include "gobang.h" +#include "ai.h" +#include "record.h" +#include +#include +#include +#include +#include + +/** + * @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) + } + + 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"); + + // 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; + + 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 (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 // 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"); +} diff --git a/src/gui_menu.c b/src/gui_menu.c index ae435f2..c7485be 100644 --- a/src/gui_menu.c +++ b/src/gui_menu.c @@ -31,8 +31,10 @@ static int btn_pve_cb(Ihandle *ih) static int btn_replay_cb(Ihandle *ih) { (void)ih; - hide_main_menu(); + 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; } diff --git a/src/gui_replay.c b/src/gui_replay.c new file mode 100644 index 0000000..1256db0 --- /dev/null +++ b/src/gui_replay.c @@ -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 +#include +#include +#include + +#ifdef _WIN32 +#include +#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(); + // 不要在这里隐藏主菜单,等待文件选择完成 +} diff --git a/src/record.c b/src/record.c index b279848..24787e2 100644 --- a/src/record.c +++ b/src/record.c @@ -298,8 +298,22 @@ int load_game_from_file(const char *filename) // 读取游戏模式、棋盘大小和评分结果 int game_mode, size; + // 读取数据行 + if (fgets(buffer, sizeof(buffer), file) == NULL) + { + fclose(file); + return 0; + } + // 尝试读取新格式(包含胜负信息) - int read_count = fscanf(file, "%d,%d,%d,%d,%49s", &game_mode, &size, &player1_final_score, &player2_final_score, winner_info); + // Use sscanf instead of fscanf to handle line buffers safely + // format: mode,size,score1,score2,winner_info + // Note: winner_info might contain newlines if not stripped, but sscanf %s stops at whitespace. + // However, CSV usually doesn't have spaces unless in quotes. + // Our winner_info is like "Player 1 Wins" or "Draw". + // If we use %[^,\n] it reads until comma or newline. + + int read_count = sscanf(buffer, "%d,%d,%d,%d,%49[^,\n]", &game_mode, &size, &player1_final_score, &player2_final_score, winner_info); if (read_count == 4) { @@ -321,16 +335,39 @@ int load_game_from_file(const char *filename) if (size < 5 || size > MAX_BOARD_SIZE) { fclose(file); - return false; + return 0; } // 设置评分已计算标志 scores_calculated = 1; // 跳过空行和表头行 - fgets(buffer, sizeof(buffer), file); // 跳过换行 - fgets(buffer, sizeof(buffer), file); // 跳过空行 - fgets(buffer, sizeof(buffer), file); // 跳过"步数,玩家,行坐标,列坐标" + // The previous fgets consumed the line with data and its newline. + // Next line should be empty or headers. + + // Check next line + if (fgets(buffer, sizeof(buffer), file) != NULL) + { + // If it's just a newline (empty line) + if (buffer[0] == '\n' || buffer[0] == '\r') + { + // Consume one more line for headers + fgets(buffer, sizeof(buffer), file); + } + else if (strncmp(buffer, "步数", 4) != 0) + { + // If it's not headers, maybe we consumed the empty line already? + // Let's be flexible. If it doesn't start with "步数", try reading one more. + if (fgets(buffer, sizeof(buffer), file) == NULL) + { + fclose(file); + return 0; + } + } + } + + // Now buffer should contain headers "步数..." or we are ready to read data? + // Actually, let's just loop until we find a digit or EOF // 初始化棋盘 BOARD_SIZE = size; @@ -339,12 +376,16 @@ int load_game_from_file(const char *filename) // 读取所有落子步骤 step_count = 0; int step_num; // 用于存储步数,但不使用 - while (fscanf(file, "%d,%d,%d,%d", &step_num, &steps[step_count].player, &steps[step_count].x, &steps[step_count].y) == 4) + + while (fgets(buffer, sizeof(buffer), file) != NULL) { - // 将1-based坐标转换为0-based坐标 - steps[step_count].x--; - steps[step_count].y--; - step_count++; + 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--; + step_count++; + } } fclose(file);