diff --git a/Makefile b/Makefile index 0e5aa95..bf1122c 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,18 @@ # 编译器设置 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 LDFLAGS = -lws2_32 # IUP路径设置 IUP_PATH = libs/iup-3.31_Win64_dllw6_lib -IUP_INCLUDE = -I$(IUP_PATH)/include +IUP_INCLUDE = "-I$(IUP_PATH)/include" # IUP链接库: iup, gdi32, comdlg32, comctl32, uuid, ole32 -IUP_LIBS = -L$(IUP_PATH) -liup -lgdi32 -lcomdlg32 -lcomctl32 -luuid -lole32 +IUP_LIBS = "-L$(IUP_PATH)" -liup -lgdi32 -lcomdlg32 -lcomctl32 -luuid -lole32 # 目录设置 SRC_DIR = src @@ -20,7 +24,8 @@ BIN_DIR = bin # 源文件 COMMON_SOURCES = $(SRC_DIR)/main.c $(SRC_DIR)/gobang.c $(SRC_DIR)/ai.c $(SRC_DIR)/config.c \ $(SRC_DIR)/game_mode.c $(SRC_DIR)/globals.c $(SRC_DIR)/init_board.c \ - $(SRC_DIR)/network.c $(SRC_DIR)/record.c $(SRC_DIR)/ui.c $(SRC_DIR)/gui.c + $(SRC_DIR)/network.c $(SRC_DIR)/record.c $(SRC_DIR)/ui.c $(SRC_DIR)/gui.c \ + $(SRC_DIR)/gui_menu.c GUI_SOURCES = $(COMMON_SOURCES) CONSOLE_SOURCES = $(COMMON_SOURCES) @@ -35,10 +40,10 @@ GUI_TARGET = $(BIN_DIR)/gobang_gui.exe # 默认目标 all: directories $(CONSOLE_TARGET) $(GUI_TARGET) -# 创建目录 +# 创建目录 (PowerShell 语法) directories: - -mkdir $(OBJ_DIR) - -mkdir $(BIN_DIR) + 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 } # 控制台版本 $(CONSOLE_TARGET): $(COMMON_OBJECTS) @@ -47,18 +52,17 @@ $(CONSOLE_TARGET): $(COMMON_OBJECTS) # GUI版本 $(GUI_TARGET): $(COMMON_OBJECTS) $(CC) $(CFLAGS) $(IUP_INCLUDE) -o $@ $^ $(IUP_LIBS) $(LDFLAGS) - copy $(subst /,\,$(IUP_PATH))\iup.dll $(BIN_DIR) 2>nul || echo Warning: Could not copy iup.dll + Copy-Item -Path "$(subst /,\,$(IUP_PATH))\iup.dll" -Destination "$(BIN_DIR)" -Force # 通用目标文件编译规则 $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(CC) $(CFLAGS) $(IUP_INCLUDE) -c -o $@ $< -# 清理规则 +# 清理规则 (PowerShell 语法) clean: - -del /Q $(OBJ_DIR)\*.o $(BIN_DIR)\*.exe $(BIN_DIR)\*.dll - -rmdir $(OBJ_DIR) - -rmdir $(BIN_DIR) - + if (Test-Path "$(OBJ_DIR)") { Remove-Item -Path "$(OBJ_DIR)" -Recurse -Force } + if (Test-Path "$(BIN_DIR)") { Remove-Item -Path "$(BIN_DIR)" -Recurse -Force } + # 只编译控制台版本 console: directories $(CONSOLE_TARGET) @@ -67,17 +71,17 @@ gui: directories $(GUI_TARGET) # 安装规则(可选) install: all - @echo Installing executables... - copy $(CONSOLE_TARGET) C:\Program Files\Gobang\ 2>nul || echo Install directory not found - copy $(GUI_TARGET) C:\Program Files\Gobang\ 2>nul || echo Install directory not found + Write-Host "Installing executables..." + Copy-Item -Path "$(CONSOLE_TARGET)" -Destination "C:\Program Files\Gobang\" -Force + Copy-Item -Path "$(GUI_TARGET)" -Destination "C:\Program Files\Gobang\" -Force # 运行控制台版本 run-console: $(CONSOLE_TARGET) - .\$(CONSOLE_TARGET) + & ".\$(CONSOLE_TARGET)" # 运行GUI版本 run-gui: $(GUI_TARGET) - .\$(GUI_TARGET) + & ".\$(GUI_TARGET)" # 帮助信息 help: diff --git a/bin/gobang_console.exe b/bin/gobang_console.exe deleted file mode 100644 index dd2b48c..0000000 Binary files a/bin/gobang_console.exe and /dev/null differ diff --git a/bin/gobang_gui.exe b/bin/gobang_gui.exe index e8e6720..be63b0b 100644 Binary files a/bin/gobang_gui.exe and b/bin/gobang_gui.exe differ diff --git a/include/config.h b/include/config.h index 28af3ca..c3f52e9 100644 --- a/include/config.h +++ b/include/config.h @@ -118,8 +118,8 @@ //---------- GUI界面参数 ----------// // 窗口和棋盘配置 -#define WINDOW_WIDTH 800 -#define WINDOW_HEIGHT 600 +#define WINDOW_WIDTH 1000 +#define WINDOW_HEIGHT 800 #define BOARD_OFFSET_X 50 #define BOARD_OFFSET_Y 50 #define CELL_SIZE 30 diff --git a/include/globals.h b/include/globals.h index e7d52cf..4a389ed 100644 --- a/include/globals.h +++ b/include/globals.h @@ -27,6 +27,7 @@ extern int network_timeout; // 网络超时时间 // ==================== AI相关变量 ==================== extern double defense_coefficient; // 防守系数 +extern int ai_difficulty; // AI难度 (1-5) // ==================== 网络相关变量 ==================== extern NetworkGameState network_state; // 网络游戏状态 diff --git a/include/gui.h b/include/gui.h index 2eb9d68..81a3507 100644 --- a/include/gui.h +++ b/include/gui.h @@ -66,4 +66,19 @@ int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y); */ void show_message(const char *message); +/** + * @brief 启动玩家对战模式 + */ +void start_pvp_game_gui(); + +/** + * @brief 启动人机对战模式 + */ +void start_pve_game_gui(); + +/** + * @brief 启动复盘模式 + */ +void start_replay_gui(); + #endif // GUI_H \ No newline at end of file diff --git a/include/gui_menu.h b/include/gui_menu.h new file mode 100644 index 0000000..833ed90 --- /dev/null +++ b/include/gui_menu.h @@ -0,0 +1,19 @@ +#ifndef GUI_MENU_H +#define GUI_MENU_H + +/** + * @brief 创建并显示主菜单 + */ +void create_main_menu(); + +/** + * @brief 显示主菜单 + */ +void show_main_menu(); + +/** + * @brief 隐藏主菜单 + */ +void hide_main_menu(); + +#endif // GUI_MENU_H diff --git a/src/config.c b/src/config.c index f95f8d9..1c0ee95 100644 --- a/src/config.c +++ b/src/config.c @@ -73,8 +73,8 @@ void load_game_config() int difficulty = atoi(line + 14); if (difficulty >= 1 && difficulty <= 5) { - // 根据难度设置AI搜索深度 - // 这里可以添加AI难度相关的配置 + ai_difficulty = difficulty; + defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1; } } } @@ -109,6 +109,9 @@ void save_game_config() fprintf(file, "\n# 网络超时时间 (毫秒)\n"); fprintf(file, "NETWORK_TIMEOUT=%d\n", network_timeout); + fprintf(file, "\n# AI难度 (1-5)\n"); + fprintf(file, "AI_DIFFICULTY=%d\n", ai_difficulty); + fclose(file); printf("配置保存完成\n"); } @@ -124,6 +127,8 @@ void reset_to_default_config() time_limit = DEFAULT_TIME_LIMIT; network_port = DEFAULT_NETWORK_PORT; network_timeout = NETWORK_TIMEOUT_MS; + ai_difficulty = 3; // 默认AI难度 + defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT + (ai_difficulty - 1) * 0.1; printf("已重置为默认配置\n"); } diff --git a/src/globals.c b/src/globals.c index 56daa14..b4d495f 100644 --- a/src/globals.c +++ b/src/globals.c @@ -24,6 +24,7 @@ int network_timeout = NETWORK_TIMEOUT_MS; // 网络超时时间 // ==================== AI相关变量定义 ==================== double defense_coefficient = DEFAULT_DEFENSE_COEFFICIENT; // 防守系数 +int ai_difficulty = 3; // AI难度 (1-5) // ==================== 网络相关变量定义 ==================== NetworkGameState network_state = {0}; // 网络游戏状态 diff --git a/src/gui.c b/src/gui.c index 022ea6f..f844253 100644 --- a/src/gui.c +++ b/src/gui.c @@ -13,13 +13,26 @@ #include "globals.h" #include "init_board.h" #include "gobang.h" +#include "gui_menu.h" +#include "ai.h" +#include "record.h" #include #include #include static Ihandle *dlg = NULL; -static Ihandle *canvas = 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; // For Replay Mode + +// 回调函数 +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 void set_draw_color(Ihandle *ih, unsigned char r, unsigned char g, unsigned char b) @@ -88,14 +101,14 @@ static void draw_stones_iup(Ihandle *ih) 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"); @@ -108,64 +121,23 @@ static void draw_stones_iup(Ihandle *ih) } } } -} -// 绘制UI元素 -static void draw_ui_iup(Ihandle *ih) -{ - int infoX = BOARD_OFFSET_X + BOARD_SIZE * CELL_SIZE + 20; - int infoY = BOARD_OFFSET_Y; - int infoW = 220; - int infoH = 120; - - // 背景 - set_draw_color(ih, 200, 200, 200); // Light Gray - IupSetAttribute(ih, "DRAWSTYLE", "FILL"); - IupDrawRectangle(ih, infoX, infoY, infoX + infoW, infoY + infoH); - - // 边框 - set_draw_color(ih, 0, 0, 0); - IupSetAttribute(ih, "DRAWSTYLE", "STROKE"); - IupDrawRectangle(ih, infoX, infoY, infoX + infoW, infoY + infoH); - - // 文本 - // 注意:IUP使用系统字体,不支持直接加载TTF文件像Raylib那样方便 - // 我们可以尝试设置字体属性 - IupSetAttribute(ih, "DRAWFONT", "SimHei, 14"); - - IupDrawText(ih, "当前玩家:", -1, infoX + 20, infoY + 20, -1, -1); - - int indicatorX = infoX + 30; - int indicatorY = infoY + 30; - - if (!game_over) - { - if (current_player_gui == PLAYER) - { - IupDrawText(ih, "黑子", -1, indicatorX + 20, indicatorY + 10, -1, -1); - set_draw_color(ih, 0, 0, 0); - IupSetAttribute(ih, "DRAWSTYLE", "FILL"); - IupDrawArc(ih, indicatorX, indicatorY + 20 - 5, indicatorX + 10, indicatorY + 20 + 5, 0, 360); - } - else - { - IupDrawText(ih, "白子", -1, indicatorX + 20, indicatorY + 10, -1, -1); - set_draw_color(ih, 255, 255, 255); - IupSetAttribute(ih, "DRAWSTYLE", "FILL"); - IupDrawArc(ih, indicatorX, indicatorY + 20 - 5, indicatorX + 10, indicatorY + 20 + 5, 0, 360); - set_draw_color(ih, 0, 0, 0); - IupSetAttribute(ih, "DRAWSTYLE", "STROKE"); - IupDrawArc(ih, indicatorX, indicatorY + 20 - 5, indicatorX + 10, indicatorY + 20 + 5, 0, 360); - } - } - else + // 标记最后落子位置 (红色小点) + if (step_count > 0) { + // 绘制最后一步的标记 + // 最后一步的坐标是 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); - IupDrawText(ih, "游戏结束", -1, indicatorX, indicatorY + 10, -1, -1); + IupSetAttribute(ih, "DRAWSTYLE", "FILL"); + IupDrawRectangle(ih, cx - 3, cy - 3, cx + 3, cy + 3); } - - set_draw_color(ih, 0, 0, 0); - IupDrawText(ih, status_message, -1, infoX + 10, infoY + 60, -1, -1); } // 屏幕坐标转棋盘坐标 @@ -181,6 +153,32 @@ int screen_to_board(int screen_x, int screen_y, int *board_x, int *board_y) *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); + } +} + // ACTION 回调:负责重绘 static int action_cb(Ihandle *ih) { @@ -189,21 +187,144 @@ static int action_cb(Ihandle *ih) int w, h; IupGetIntInt(ih, "DRAWSIZE", &w, &h); - set_draw_color(ih, 245, 245, 245); // Background (Raylib RAYWHITE approx) + 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); - draw_ui_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 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; + if (dlg) + { + IupHide(dlg); + show_main_menu(); + } + return IUP_DEFAULT; +} + // 鼠标点击回调 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) @@ -224,32 +345,57 @@ static int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *s if (current_player_gui == PLAYER) { sprintf(status_message, "黑子获胜!"); + IupMessage("游戏结束", "黑子获胜!"); } else { sprintf(status_message, "白子获胜!"); + IupMessage("游戏结束", "白子获胜!"); } } else { - // 切换玩家 - current_player_gui = (current_player_gui == PLAYER) ? AI : PLAYER; - if (current_player_gui == PLAYER) + if (gui_game_mode == 0) // PvP { - sprintf(status_message, "轮到黑子"); + 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 + else // PvE { - sprintf(status_message, "轮到白子"); + 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, "无效位置!"); - IupUpdate(ih); + update_ui_labels(); } } } @@ -259,14 +405,204 @@ static int button_cb(Ihandle *ih, int button, int pressed, int x, int y, char *s // 键盘回调 static int k_any_cb(Ihandle *ih, int c) { + (void)ih; if (c == K_ESC) { - gui_loop_running = 0; - return IUP_CLOSE; + if (dlg && IupGetInt(dlg, "VISIBLE")) + { + IupHide(dlg); + show_main_menu(); + return IUP_DEFAULT; + } } return IUP_DEFAULT; } +// 创建游戏窗口 +void create_game_window() +{ + if (dlg) + { + IupDestroy(dlg); // 销毁旧窗口 + dlg = NULL; + } + + // 创建Canvas (Board) + board_canvas = IupCanvas(NULL); + 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"); + + 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); + IupSetAttribute(dlg, "TITLE", "五子棋 - IUP版本"); + IupSetAttribute(dlg, "RESIZE", "NO"); + + IupMap(dlg); +} + +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() +{ + gui_game_mode = 1; + // ai_difficulty is global + 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_replay_gui() +{ + // Open file dialog + Ihandle *file_dlg = IupFileDlg(); + IupSetAttribute(file_dlg, "DIALOGTYPE", "OPEN"); + 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; + + if (load_game_from_file(base_name)) // returns game_mode (non-zero) on success + { + replay_total_steps = step_count; + step_count = 0; + // load_game_from_file already cleared board when reading steps, but steps were read into array + // wait, load_game_from_file calls empty_board() then reads steps. + // But it doesn't set board array. + // So board is empty. + + gui_game_mode = 2; // Replay + create_game_window(); + + IupShowXY(dlg, IUP_CENTER, IUP_CENTER); + sprintf(status_message, "复盘模式 - %s", base_name); + update_ui_labels(); + if (board_canvas) + IupUpdate(board_canvas); + } + else + { + IupMessage("错误", "无法加载复盘文件"); + show_main_menu(); + } + } + else + { + show_main_menu(); + } + + IupDestroy(file_dlg); +} + /** * @brief 初始化GUI */ @@ -281,43 +617,12 @@ int init_gui() // 启用UTF-8模式,确保中文正常显示 IupSetGlobal("UTF8MODE", "YES"); - // 创建Canvas - canvas = IupCanvas(NULL); - IupSetAttribute(canvas, "ACTION", "action_cb"); - IupSetCallback(canvas, "ACTION", (Icallback)action_cb); - IupSetCallback(canvas, "BUTTON_CB", (Icallback)button_cb); - IupSetCallback(canvas, "K_ANY", (Icallback)k_any_cb); + create_main_menu(); + show_main_menu(); - // 设置Canvas大小 - char size[32]; - sprintf(size, "%dx%d", WINDOW_WIDTH, WINDOW_HEIGHT); - IupSetAttribute(canvas, "RASTERSIZE", size); - IupSetAttribute(canvas, "EXPAND", "NO"); - - // 创建Dialog - dlg = IupDialog(canvas); - IupSetAttribute(dlg, "TITLE", "五子棋 - IUP版本"); - IupSetAttribute(dlg, "RESIZE", "NO"); - - IupShowXY(dlg, IUP_CENTER, IUP_CENTER); - - // 初始化游戏状态 - for (int i = 0; i < BOARD_SIZE; i++) - { - for (int j = 0; j < BOARD_SIZE; j++) - { - board[i][j] = EMPTY; - } - } - current_player_gui = PLAYER; - game_over = 0; gui_loop_running = 1; - sprintf(status_message, "游戏开始"); printf("图形化界面初始化成功!(IUP)\n"); - printf("使用鼠标点击棋盘进行落子\n"); - printf("按ESC键退出游戏\n"); - return 0; } @@ -343,7 +648,6 @@ int handle_events() if (!gui_loop_running) return 0; - // 执行一次IUP循环迭代 int ret = IupLoopStep(); if (ret == IUP_CLOSE) { @@ -351,9 +655,6 @@ int handle_events() return 0; } - // 如果窗口被关闭(点X) - // 注意:IupLoopStep会自动处理窗口关闭并返回IUP_CLOSE,除非定义了CLOSE_CB - return 1; } @@ -362,20 +663,11 @@ int handle_events() */ void render_game() { - // 主动刷新Canvas - // 注意:频繁调用IupUpdate可能会导致闪烁或高CPU占用,但在单线程循环模型中通常是必要的 - // 以确保动画或状态更新能及时反映。 - // 在我们的例子中,主要依靠 button_cb 触发 IupUpdate。 - // 但为了兼容原来的循环结构,我们可以保持这个函数。 - // IupUpdate(canvas); - // 实际上不需要每一帧都 Update,只有状态变了才需要。 - // 原来的Raylib代码是每帧都重绘。为了性能,这里我们什么都不做,让事件驱动。 - // 除非有外部事件改变了游戏状态(比如网络消息),那时应该调用 IupUpdate。 - // 鉴于目前是本地PVP/AI,所有状态改变都在 button_cb 里,那里已经调用了 IupUpdate。 + // 事件驱动,不需要手动渲染 } /** - * @brief 绘制棋盘 (保留空函数以兼容接口,实际绘图在 action_cb 中) + * @brief 绘制棋盘 (保留空函数以兼容接口) */ void draw_board() { @@ -405,7 +697,6 @@ void show_message(const char *message) { strncpy(status_message, message, sizeof(status_message) - 1); status_message[sizeof(status_message) - 1] = '\0'; - if (canvas) - IupUpdate(canvas); + update_ui_labels(); printf("%s\n", message); -} \ No newline at end of file +} diff --git a/src/gui_menu.c b/src/gui_menu.c new file mode 100644 index 0000000..51507ff --- /dev/null +++ b/src/gui_menu.c @@ -0,0 +1,264 @@ +#include +#include +#include +#include "gui_menu.h" +#include "gui.h" +#include "globals.h" +#include "config.h" + +static Ihandle *menu_dlg = NULL; + +static int btn_pvp_cb(Ihandle *ih) +{ + (void)ih; + hide_main_menu(); + start_pvp_game_gui(); + return IUP_DEFAULT; +} + +static int btn_pve_cb(Ihandle *ih) +{ + (void)ih; + hide_main_menu(); + start_pve_game_gui(); + return IUP_DEFAULT; +} + +static int btn_replay_cb(Ihandle *ih) +{ + (void)ih; + hide_main_menu(); + start_replay_gui(); + 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; + + 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_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_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); +} + +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); +} \ No newline at end of file