Files
chouka/第06课-完整抽卡游戏.md
T
2026-05-16 11:09:02 +08:00

362 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第 6 课:完整抽卡游戏
**本课目标**:把前 5 课全部整合,做出一个完整的抽卡游戏——多个按钮、金币系统、累计统计。这是 GUI 教程的最终成果。
**复习**:前面所有的 FLTK 知识 + 旧课程的全部 C++ 基础。
---
## 1. 最终游戏长什么样
```
┌────────── 抽卡大冒险 ──────────┐
│ │
│ 🎴 单抽 🎴🎴 十连抽 │
│ (扣10金币) (扣100金币) │
│ │
│ ┌── 抽卡结果 ──────────────┐ │
│ │ │ │
│ │ ★ 稀有卡 ★ │ │
│ │ 冰霜之狼 │ │
│ │ │ │
│ └──────────────────────────┘ │
│ │
│ 累计:普通 12 稀有 5 超稀有 2│
│ │
│ 金币:350 💰 │
│ │
│ [ 重置 ] [ 退出 ] │
│ │
└─────────────────────────────────┘
```
---
## 2. 游戏规则
| 操作 | 消耗金币 | 说明 |
|------|---------|------|
| 单抽 | 10 金币 | 抽一张卡 |
| 十连抽 | 100 金币 | 抽十张,显示汇总 |
| 重置 | 0 金币 | 金币回到初始值,统计清零 |
| 退出 | 0 金币 | 关闭窗口 |
初始金币 500,金币不够时不能抽卡,显示提示。
---
## 3. 完整示例程序
保存为 `lesson_gui_6.cpp`。这是教程的最终成果,约 120 行。
```cpp
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Box.H>
#include <cstdlib>
#include <ctime>
#include <string>
// ===== 卡片数据 =====
std::string normal_cards[5] = {
"小火龙", "水跃鱼", "绿草龟", "小电鼠", "土拨仔"
};
std::string rare_cards[5] = {
"烈焰飞龙", "冰霜之狼", "暗影豹", "风暴鹰", "水晶鹿"
};
std::string super_cards[3] = {
"雷霆神龙", "凤凰王", "深海巨兽"
};
// ===== 游戏状态(全局) =====
int coins = 500;
int total_normal = 0;
int total_rare = 0;
int total_super = 0;
// ===== 全局控件 =====
Fl_Box *result_box;
Fl_Box *stats_box;
Fl_Box *coins_box;
// ===== 更新金币显示 =====
void update_coins_display() {
char buf[50];
std::sprintf(buf, "金币:%d 💰", coins);
coins_box->label(buf);
if (coins < 100) {
coins_box->labelcolor(FL_RED);
} else {
coins_box->labelcolor(fl_rgb_color(0, 100, 0));
}
}
// ===== 更新统计显示 =====
void update_stats_display() {
char buf[100];
std::sprintf(buf, "累计:普通 %d | 稀有 %d | 超稀有 %d",
total_normal, total_rare, total_super);
stats_box->label(buf);
}
// ===== 获得一张卡的名字和稀有度 =====
// rare_level: 0=普通, 1=稀有, 2=超稀有
std::string get_card(int &rare_level) {
int r = std::rand() % 100;
if (r < 70) {
rare_level = 0;
total_normal++;
return normal_cards[std::rand() % 5];
} else if (r < 95) {
rare_level = 1;
total_rare++;
return rare_cards[std::rand() % 5];
} else {
rare_level = 2;
total_super++;
return super_cards[std::rand() % 3];
}
}
// ===== 单抽回调 =====
void draw_one_callback(Fl_Widget *w, void *data) {
if (coins < 10) {
result_box->label("金币不够!单抽需要 10 金币。\n试试点\"重置\"获得新金币。");
result_box->labelcolor(FL_RED);
result_box->labelsize(16);
return;
}
coins -= 10;
update_coins_display();
int level = 0;
std::string card = get_card(level);
std::string result;
if (level == 0) {
result = "☆ 普通卡 ☆\n " + card;
result_box->labelcolor(fl_rgb_color(128, 128, 128));
} else if (level == 1) {
result = "★ 稀有卡 ★\n " + card;
result_box->labelcolor(FL_BLUE);
} else {
result = "★★★ 超稀有卡!! ★★★\n " + card + " 🎉";
result_box->labelcolor(FL_RED);
}
result_box->labelsize(24);
result_box->label(result.c_str());
update_stats_display();
}
// ===== 十连抽回调 =====
void draw_ten_callback(Fl_Widget *w, void *data) {
if (coins < 100) {
result_box->label("金币不够!十连抽需要 100 金币。\n试试点\"重置\"获得新金币。");
result_box->labelcolor(FL_RED);
result_box->labelsize(16);
return;
}
coins -= 100;
update_coins_display();
std::string all = "";
int ten_normal = 0, ten_rare = 0, ten_super = 0;
for (int i = 1; i <= 10; i++) {
int level = 0;
std::string card = get_card(level);
all += "第" + std::to_string(i) + "抽:";
if (level == 0) {
all += "普通 " + card;
ten_normal++;
} else if (level == 1) {
all += "★稀有 " + card;
ten_rare++;
} else {
all += "★★★超稀有 " + card;
ten_super++;
}
all += "\n";
}
// 汇总行
char summary[80];
std::sprintf(summary, "\n--- 本次:普通%d 稀有%d 超稀有%d ---",
ten_normal, ten_rare, ten_super);
all += summary;
result_box->labelsize(14);
result_box->label(all.c_str());
result_box->labelcolor(FL_BLACK);
update_stats_display();
}
// ===== 重置回调 =====
void reset_callback(Fl_Widget *w, void *data) {
coins = 500;
total_normal = 0;
total_rare = 0;
total_super = 0;
update_coins_display();
update_stats_display();
result_box->labelsize(20);
result_box->label("游戏已重置!\n金币回到 500,统计清零。\n开始抽卡吧!");
result_box->labelcolor(FL_BLACK);
}
// ===== 退出回调 =====
void quit_callback(Fl_Widget *w, void *data) {
window->hide(); // window 是全局的?
// 用 FLTK 的方式退出
exit(0);
}
int main() {
std::srand(std::time(nullptr));
Fl_Window *window = new Fl_Window(440, 560, "抽卡大冒险");
// ===== 顶部:操作按钮 =====
Fl_Button *btn1 = new Fl_Button(30, 15, 150, 50, "🎴 单抽\n(10金币)");
btn1->labelsize(16);
btn1->callback(draw_one_callback);
Fl_Button *btn10 = new Fl_Button(260, 15, 150, 50, "🎴🎴 十连抽\n(100金币)");
btn10->labelsize(16);
btn10->callback(draw_ten_callback);
// ===== 金币显示 =====
coins_box = new Fl_Box(30, 75, 380, 30, "");
coins_box->labelsize(20);
coins_box->labelfont(FL_BOLD);
update_coins_display();
// ===== 结果显示区 =====
result_box = new Fl_Box(20, 115, 400, 270, "欢迎来到抽卡大冒险!\n点击上方按钮开始抽卡~");
result_box->labelsize(20);
result_box->align(FL_ALIGN_TOP | FL_ALIGN_LEFT);
result_box->box(FL_DOWN_BOX);
// ===== 累计统计 =====
stats_box = new Fl_Box(20, 400, 400, 30, "");
stats_box->labelsize(14);
update_stats_display();
// ===== 底部按钮 =====
Fl_Button *btn_reset = new Fl_Button(100, 450, 100, 40, "重置");
btn_reset->callback(reset_callback);
Fl_Button *btn_quit = new Fl_Button(240, 450, 100, 40, "退出");
btn_quit->callback(quit_callback);
window->end();
window->show();
return Fl::run();
}
```
---
## 4. 代码架构总结
回顾这 6 课,你的程序结构从第 1 课的简单窗口,发展到了完整游戏:
```
全局区域
├── 卡片数据(数组)
├── 游戏状态(金币、统计)
├── 全局控件指针
回调函数
├── get_card() ← 封装"抽一张卡"的逻辑(函数复用!)
├── draw_one_callback() ← 单抽
├── draw_ten_callback() ← 十连抽
├── reset_callback() ← 重置
├── quit_callback() ← 退出
辅助函数
├── update_coins_display()
├── update_stats_display()
main()
├── 创建窗口
├── 创建所有控件
├── 绑定回调
└── Fl::run()
```
注意 `get_card()` 函数——它把"随机 + 判断 + 统计"打包成一个函数,单抽和十连抽都调用它。这就是旧课程第 8 课学的**函数复用**!
---
## 5. 新知识点
### 封装函数提高复用(旧知识的新应用)
```cpp
std::string get_card(int &rare_level) {
// 随机 → 判断 → 统计 → 返回卡名
}
```
- `int &rare_level` —— 引用参数,函数可以修改外面的变量
- 单抽和十连抽都调同一个函数,不重复代码
### `return` 提前退出
```cpp
if (coins < 10) {
result_box->label("金币不够!...");
return; // 直接结束函数,不继续抽卡
}
```
---
## 6. 本课课堂小练习
1. 调整游戏参数:初始金币改 1000,单抽改 20 金币,看游戏节奏变化。
2. 在十连抽结果中增加一行"保底提示",如果没有出超稀有,写一句"再接再厉!"。
3. 把卡片名字全部换成你自创的主题(比如三国武将、星座、美食……)。
---
## 7. 本课课后作业(选做 / 拓展)
以下是你可以自己加的进阶功能:
- **幸运计数器**:统计"已经多少抽没出超稀有了",超过 30 抽提示"保底在望!"
- **图鉴系统**:加一个按钮,用文字列表显示你收集到的所有不重复卡片。
- **卡片仓库**:用 `std::vector` 存每次抽到的卡,可以查看历史记录。
- **皮肤/主题**:改窗口颜色、按钮颜色,做一套你喜欢的主题。
---
## 8. 常见小问题
- **退出按钮无响应**:检查 `quit_callback` 里是否写了 `exit(0)`。FLTK 中可以用 `exit(0)` 简单退出。
- **金币显示不更新**:每次改 `coins` 后是否调了 `update_coins_display()`
- **编译报错 `window` 未声明**:如果退出回调用了 `window->hide()``window` 需要是全局变量或传参数。
---
## 🎉 恭喜!
你完成了从命令行到 GUI 的跨越!你现在能够:
- 用 FLTK 创建窗口和控件
- 用回调函数响应按钮点击
- 把随机/判断/循环/数组等 C++ 知识用在 GUI 程序里
- 设计一个完整的小游戏
从第 1 课的 `std::cout << "欢迎来到抽卡世界!"` 到现在这个带窗口、按钮、金币系统的完整游戏,你只用了 6 课!
---
*学完请在 `学习进度与状态-GUI.md` 里更新第 6 课的状态。*