feat(gui): 添加主菜单和游戏模式选择界面

- 新增 GUI 主菜单,支持玩家对战、人机对战、复盘模式和设置选项
- 新增游戏设置对话框,可配置棋盘大小、禁手、计时器和 AI 难度
- 重构 GUI 游戏窗口,集成悔棋、保存和返回菜单功能
- 更新 Makefile 以支持 PowerShell 并添加新源文件编译
- 调整窗口尺寸为 1000x800 以提供更好的游戏体验
This commit is contained in:
2026-03-17 15:41:48 +08:00
parent d796065bb9
commit 0baab8bec6
11 changed files with 740 additions and 140 deletions
+21 -17
View File
@@ -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:
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -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
+1
View File
@@ -27,6 +27,7 @@ extern int network_timeout; // 网络超时时间
// ==================== AI相关变量 ====================
extern double defense_coefficient; // 防守系数
extern int ai_difficulty; // AI难度 (1-5)
// ==================== 网络相关变量 ====================
extern NetworkGameState network_state; // 网络游戏状态
+15
View File
@@ -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
+19
View File
@@ -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
+7 -2
View File
@@ -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");
}
+1
View File
@@ -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}; // 网络游戏状态
+410 -119
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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);
}
}
+264
View File
@@ -0,0 +1,264 @@
#include <iup.h>
#include <stdlib.h>
#include <stdio.h>
#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);
}