247 lines
7.1 KiB
Markdown
247 lines
7.1 KiB
Markdown
# 第 3 课:随机抽卡——核心来了
|
||
|
||
> **本课目标**:把命令行时学过的 `rand()` + `if/else` **搬进 GUI**。点按钮 → 随机出"普通卡 / 稀有卡 / 超稀有卡",不同稀有度配不同颜色。
|
||
>
|
||
> **预计时间**:40 分钟
|
||
>
|
||
> **学完之后**:你能在回调函数里写随机和判断,让 GUI 程序"每次都不一样"。
|
||
|
||
---
|
||
|
||
## 0. 上课前先理理
|
||
|
||
### 一句话回顾旧知识
|
||
|
||
```cpp
|
||
std::srand(std::time(nullptr)); // 设一次种子
|
||
int r = std::rand() % 100; // 每次生成 0~99 的随机数
|
||
|
||
if (r < 70) { ... 普通 ... }
|
||
else if (r < 95) { ... 稀有 ... }
|
||
else { ... 超稀有 ... }
|
||
```
|
||
|
||
这些你以前用 `std::cout` 打印结果。现在改用 `Fl_Box` 显示。
|
||
|
||
### 概率设计
|
||
|
||
| 随机数 r | 稀有度 | 颜色 | 大约概率 |
|
||
|---------|--------|------|---------|
|
||
| 0 ~ 69 | 普通卡 | 灰色 | 70% |
|
||
| 70 ~ 94 | 稀有卡 | 蓝色 | 25% |
|
||
| 95 ~ 99 | 超稀有卡 | 红色 | 5% |
|
||
|
||
### 这一课认识 2 个新朋友 + 1 个老技巧的新用法
|
||
|
||
| 名字 | 干什么 |
|
||
|-----------------|---------------------------------------------|
|
||
| `sprintf` | 把数字"塞"进字符串里(GUI 不能直接 `cout`) |
|
||
| `fl_rgb_color` | 用 R/G/B 三个数自定义任意颜色 |
|
||
| `labelcolor` | (老朋友)配合上面两个,让稀有度看一眼区分 |
|
||
|
||
---
|
||
|
||
## 1. 起步:先编译空骨架
|
||
|
||
打开 `lesson_gui_3.cpp`。骨架里已经准备好了第 2 课学过的东西(窗口 + 按钮 + 回调 + 全局 `result_box`),但回调里还没写"抽卡"。
|
||
|
||
```bash
|
||
cmake -B build -DLESSON=3
|
||
cmake --build build && ./build/gacha3
|
||
```
|
||
|
||
### ✅ 检查点 1
|
||
- [ ] 窗口出来了,按钮 "抽一张!" 在上面
|
||
- [ ] 下面写着 "点击按钮抽卡吧!"
|
||
- [ ] 点按钮文字会变成 "(写好步骤 4 后这里会变)"(提示当前回调没真正工作)
|
||
|
||
---
|
||
|
||
## 2. 步骤 1:让随机数能用起来
|
||
|
||
### 🤔 先想一下
|
||
- 设随机种子 `srand()` 要放哪儿?回调里行不行?
|
||
- 为什么不能每次点按钮都 `srand` 一下?
|
||
|
||
> 💡 答案:种子只能设一次,否则每次都"重置",结果会重复甚至不变。所以 `srand` 放 `main` 里、`Fl::run()` 之前。
|
||
|
||
### ✏️ 改两个地方
|
||
|
||
**(1) 加 include**(替换对应 TODO):
|
||
|
||
```cpp
|
||
#include <cstdlib> // rand, srand
|
||
#include <ctime> // time
|
||
```
|
||
|
||
**(2) 在 `main` 的第一行加**(替换对应 TODO):
|
||
|
||
```cpp
|
||
std::srand(std::time(nullptr));
|
||
```
|
||
|
||
### ▶️ 编译运行
|
||
|
||
```bash
|
||
cmake --build build && ./build/gacha3
|
||
```
|
||
|
||
### ✅ 检查点 2
|
||
- [ ] 编译没报错
|
||
- [ ] 程序行为和之前一样(因为我们还没真正用 `rand`)
|
||
|
||
---
|
||
|
||
## 3. 步骤 2:在回调里掷骰子
|
||
|
||
### ✏️ 在 `draw_callback` 函数里**第一行**加(替换对应 TODO):
|
||
|
||
```cpp
|
||
int r = std::rand() % 100;
|
||
```
|
||
|
||
> 这一行还看不到效果。下一步把 `r` 显示在窗口上。
|
||
|
||
---
|
||
|
||
## 4. 步骤 3:用 `sprintf` 把数字显示出来
|
||
|
||
### 🤔 先想一下
|
||
- `result_box->label(...)` 只接受字符串。你怎么把整数 `r` 变成字符串?
|
||
- 以前命令行用 `std::cout << "r=" << r`,但 `label` 没有 `<<`。怎么办?
|
||
|
||
### 💡 新工具:`sprintf`
|
||
|
||
```cpp
|
||
char info[50]; // 准备一个 50 字节的盒子
|
||
std::sprintf(info, "本次点数:%d", r); // 把 "本次点数:" + r 的值 塞进盒子
|
||
info_box->label(info); // 把盒子的内容贴到 label 上
|
||
```
|
||
|
||
`%d` 是占位符,运行时被 `r` 的值替换。
|
||
|
||
### ✏️ 改 3 个地方
|
||
|
||
**(1) 顶上加**(替换对应 TODO):
|
||
|
||
```cpp
|
||
#include <cstdio> // sprintf
|
||
```
|
||
|
||
**(2) 在全局区,`result_box` 下面加**(替换对应 TODO):
|
||
|
||
```cpp
|
||
Fl_Box *info_box;
|
||
```
|
||
|
||
**(3) 在 `main` 里,紧挨 `result_box` 的创建代码下面加**(替换对应 TODO):
|
||
|
||
```cpp
|
||
info_box = new Fl_Box(50, 200, 300, 50, "");
|
||
info_box->labelsize(14);
|
||
```
|
||
|
||
**(4) 在 `draw_callback` 里、`int r = ...` 那一行下面加**(替换对应 TODO):
|
||
|
||
```cpp
|
||
char info[50];
|
||
std::sprintf(info, "本次点数:%d", r);
|
||
info_box->label(info);
|
||
```
|
||
|
||
### ▶️ 编译运行
|
||
|
||
### ✅ 检查点 3
|
||
- [ ] 点按钮 → 下方多出一行 "本次点数:XX",每次 XX 都不同
|
||
- [ ] 多点几次,能看到 0~99 各种数值
|
||
|
||
---
|
||
|
||
## 5. 步骤 4:根据 `r` 决定稀有度和颜色 ⭐核心⭐
|
||
|
||
终于到抽卡的灵魂部分了。
|
||
|
||
### ✏️ 把 `draw_callback` 里那行假提示 `result_box->label("(写好步骤 4 后这里会变)");` **整段替换**成:
|
||
|
||
```cpp
|
||
if (r < 70) {
|
||
result_box->label("☆ 普通卡 ☆");
|
||
result_box->labelcolor(fl_rgb_color(128, 128, 128)); // 灰色
|
||
} else if (r < 95) {
|
||
result_box->label("★ 稀有卡 ★");
|
||
result_box->labelcolor(FL_BLUE);
|
||
} else {
|
||
result_box->label("★★★ 超稀有卡!! ★★★");
|
||
result_box->labelcolor(FL_RED);
|
||
}
|
||
```
|
||
|
||
### 💡 新工具:`fl_rgb_color(R, G, B)`
|
||
|
||
每个数 0~255,组合任意颜色:
|
||
|
||
| 颜色 | RGB |
|
||
|------|------|
|
||
| 纯白 | `(255, 255, 255)` |
|
||
| 灰 | `(128, 128, 128)` |
|
||
| 金 | `(255, 200, 0)` |
|
||
| 橙 | `(255, 128, 0)` |
|
||
| 紫 | `(180, 100, 200)` |
|
||
|
||
### ▶️ 编译运行
|
||
|
||
### ✅ 检查点 4
|
||
- [ ] 点按钮 → 看到三种结果之一,且颜色不一样
|
||
- [ ] 多点 20 次,能至少出现 1 次蓝色"稀有"
|
||
- [ ] 多点 50 次以上,能出现红色"超稀有"
|
||
|
||
🎉 **第 3 课通关!** 你的程序"每次都不一样"了。
|
||
|
||
---
|
||
|
||
## 6. 玩一玩
|
||
|
||
1. 把概率改成"超稀有更多":`r < 50` 普通,`r < 80` 稀有,其他超稀有。点 20 次试试感受。
|
||
2. 把超稀有改成金色:`fl_rgb_color(255, 200, 0)`。
|
||
3. 给"超稀有"再加一行祝贺:在那个 `else` 分支里加 `info_box->label("🎉 恭喜抽到超稀有!🎉");`。
|
||
4. **思考题**:如果把 `srand` 那一行从 `main` 删掉,多次运行程序会发生什么?
|
||
|
||
---
|
||
|
||
## 7. 课后挑战(选做)
|
||
|
||
- **A. 加第四档"传说卡"**:`r >= 99`(约 1%),金色,更大字号。注意调整原来 95 的判断。
|
||
- **B. 抽卡计数**:用全局 `int total = 0;` 统计点击次数,每次抽完显示在 `info_box` 上(`"第 N 次:r=XX"`)。
|
||
- **C. 思考题**:`if (r < 70) ... else if (r < 95) ...` —— 为什么 95 那行不需要写 `r >= 70 && r < 95`?
|
||
|
||
---
|
||
|
||
## 8. 卡住了?常见小问题
|
||
|
||
| 现象 | 多半是 |
|
||
|------|-------|
|
||
| 每次结果都一样 | `srand` 没写、或者写在了回调里 |
|
||
| 编译报 `rand` / `srand` / `time` 未定义 | 忘了 `#include <cstdlib>` 或 `<ctime>` |
|
||
| 编译报 `sprintf` 未定义 | 忘了 `#include <cstdio>` |
|
||
| 信息行显示乱码 | `char info[50]` 太小?改成 `info[100]` 试试 |
|
||
| 颜色没变 | `labelcolor` 拼错;或 `fl_rgb_color` 写成了 `FL_rgb_color` |
|
||
|
||
---
|
||
|
||
## 9. 偷看老师答案
|
||
|
||
```bash
|
||
cmake -B build -DLESSON=3 -DUSE_SOLUTION=ON
|
||
cmake --build build && ./build/gacha3
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 下一课预告
|
||
|
||
第 4 课加一个 "十连抽" 按钮:一次抽 10 张全部显示。你会用上**老朋友 `for` 循环**和**字符串拼接**。
|
||
|
||
> 📝 学完记得在 `学习进度与状态-GUI.md` 里把第 3 课打勾。
|
||
|
||
**下一课**:`第04课-十连抽.md`
|