本课作品
制作战利品模拟器 LootSimulator
宝箱里的钻石为什么没有每二十次准时出现?把概率拆成一百格,固定随机种子重播实验,先验边界,再看次数和比例。 前置背包:if 分支、循环、ArrayList 和 HashMap。今天让宝箱不再连续开出七根木棍。
开始前:建议先完成第 25–26 课
- Set保存唯一成员
- 第一次 add改变集合,返回 true
- 重复 add不改变,返回 false
01 / 13
开始前:先预测,再让程序回答
本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。
每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。
终端命令:中文占位符换成实际文件名与类名
javac -encoding UTF-8 示例文件名.java
java 示例类名- 预测:先写预期输出,或画清引用和数据的变化。
- 验证:运行后逐行核对,再只改一个条件。
- 解释:说清结果来自 Java 规则、课程合同还是输入数据。
02 / 13
Set 只保留不重复元素
Set 像成就墙。它关心有没有某项记录,不关心第几个,也不允许同一内容重复占位置。
- Set保存唯一成员
- 第一次 add改变集合,返回 true
- 重复 add不改变,返回 false
L27Example01.java · 完整独立示例
import java.util.*;
public class L27Example01 {
public static void main(String[] args) {
Set<String> achievements = new HashSet<>();
System.out.println(achievements.add("首次挖矿"));
System.out.println(achievements.add("首次挖矿"));
System.out.println(achievements.size());
}
}运行结果
true
false
103 / 13
添加 查询与删除
HashSet 的常用操作都围绕成员资格。它没有 get(0),因为集合不承诺位置。
- contains检查成员资格
- remove移除指定成员
- 无索引没有 get(0)
L27Example02.java · 完整独立示例
import java.util.*;
public class L27Example02 {
public static void main(String[] args) {
Set<String> loot = new HashSet<>(Set.of("钻石", "面包"));
System.out.println(loot.contains("钻石"));
System.out.println(loot.remove("钻石"));
System.out.println(loot.contains("钻石"));
}
}运行结果
true
true
falseBadSetIndex.java · 故意错误示例
import java.util.*;
public class BadSetIndex {
public static void main(String[] args) {
Set<String> loot = new HashSet<>();
System.out.println(loot.get(0));
}
}04 / 13
集合运算先复制
Set 可以比较两队玩家或两张掉落表。注意这些方法会直接修改调用它们的集合。
- 交集 retainAll只保留两边都有的
- 并集 addAll包含任意一边的
- 差集 removeAll留下仅在原集合的
L27Example03.java · 完整独立示例
import java.util.*;
public class L27Example03 {
public static void main(String[] args) {
Set<String> a = new HashSet<>(Set.of("钻石", "面包"));
Set<String> b = Set.of("面包", "铁锭");
Set<String> both = new HashSet<>(a); both.retainAll(b);
Set<String> either = new HashSet<>(a); either.addAll(b);
Set<String> onlyA = new HashSet<>(a); onlyA.removeAll(b);
System.out.println(both.equals(Set.of("面包")));
System.out.println(either.size());
System.out.println(onlyA.equals(Set.of("钻石")));
System.out.println(a.size());
}
}运行结果
true
3
true
205 / 13
Random 的上界不包含
random.nextInt(100) 可能得到0到99,共100种整数,永远不会得到100。
- nextInt(100)得到0到99
- nextInt(6)+1得到1到6
- 上界不包含且必须为正数
L27Example04.java · 完整独立示例
import java.util.*;
public class L27Example04 {
public static void main(String[] args) {
Random random = new Random(2026);
boolean inRange = true;
for (int i = 0; i < 100; i++) {
int dice = random.nextInt(6) + 1;
inRange &= dice >= 1 && dice <= 6;
}
System.out.println(inRange);
}
}运行结果
true06 / 13
概率区间必须连续
把0到99排成100格。每格只能属于一种奖励,所有格子都必须有归属。
| 物品 | 随机数区间 | 概率 |
|---|---|---|
| 钻石 | 0–4 | 5% |
| 铁 | 5–24 | 20% |
| 面包 | 25–99 | 75% |
100 个可能值:0 ≤ roll < 100,100 不在取值范围内。
L27Example05.java · 完整独立示例
public class L27Example05 {
static String pickRoll(int roll) {
if (roll < 0 || roll >= 100)
throw new IllegalArgumentException("roll应为0到99");
if (roll < 5) return "钻石";
if (roll < 25) return "铁锭";
return "面包";
}
public static void main(String[] args) {
for (int roll : new int[]{0, 4, 5, 24, 25, 99})
System.out.println(roll + ": " + pickRoll(roll));
}
}运行结果
0: 钻石
4: 钻石
5: 铁锭
24: 铁锭
25: 面包
99: 面包07 / 13
固定种子复现随机测试
随机程序若每次都不同,Bug 可能像末影人一样瞬移。测试时传入固定种子,失败过程就能重播。
- 相同种子提供相同起点
- 相同调用顺序保证随机序列可重播
- 改变调用后续序列会错位
L27Example06.java · 完整独立示例
import java.util.*;
public class L27Example06 {
public static void main(String[] args) {
Random a = new Random(2026), b = new Random(2026);
boolean same = true;
for (int i = 0; i < 20; i++) same &= a.nextInt(100) == b.nextInt(100);
System.out.println(same);
}
}运行结果
true08 / 13
模拟1000次观察频率
5%不表示每20次必出一次钻石。它表示大量独立尝试中,长期比例通常接近5%。
- 次数总和必须等于模拟次数
- 长期比例通常接近设计概率
- 一次实验数量不必恰好50
L27Example07.java · 完整独立示例
import java.util.*;
public class L27Example07 {
static String pickRoll(int roll) {
if (roll < 0 || roll >= 100)
throw new IllegalArgumentException("roll应为0到99");
if (roll < 5) return "钻石";
if (roll < 25) return "铁锭";
return "面包";
}
static String pickLoot(Random random) {
return pickRoll(java.util.Objects.requireNonNull(random).nextInt(100));
}
static Map<String, Integer> simulate(int trials, long seed) {
if (trials < 0 || trials > 1_000_000)
throw new IllegalArgumentException("模拟次数应为0到100万");
Random random = new Random(seed);
Map<String, Integer> counts = new HashMap<>();
for (int i = 0; i < trials; i++)
counts.merge(pickLoot(random), 1, Integer::sum);
return counts;
}
public static void main(String[] args) {
Map<String, Integer> counts = simulate(1000, 2026);
int total = counts.values().stream().mapToInt(Integer::intValue).sum();
System.out.println(total);
System.out.println(counts.values().stream().allMatch(n -> n >= 0));
}
}运行结果
1000
true09 / 13
用 Set 保证奖励不重复
连续抽取三种不同奖励时,Set 能自动拒绝重复。但若候选只有两种,要求三种就会无限循环。
- 目标数量不能超过候选种类
- 尝试上限避免长时间循环
- 结果数量达到上限可能未集齐
L27Example08.java · 完整独立示例
import java.util.*;
public class L27Example08 {
static String pickRoll(int roll) {
if (roll < 0 || roll >= 100)
throw new IllegalArgumentException("roll应为0到99");
if (roll < 5) return "钻石";
if (roll < 25) return "铁锭";
return "面包";
}
static String pickLoot(Random random) {
return pickRoll(java.util.Objects.requireNonNull(random).nextInt(100));
}
static Set<String> collectUnique(Random random, int wanted, int limit) {
if (random == null || wanted < 0 || wanted > 3 || limit < 0)
throw new IllegalArgumentException("检查目标数量和尝试上限");
Set<String> result = new HashSet<>();
for (int attempt = 0; result.size() < wanted && attempt < limit; attempt++)
result.add(pickLoot(random));
return result;
}
public static void main(String[] args) {
Set<String> loot = collectUnique(new Random(2026), 3, 100);
System.out.println(loot.size());
System.out.println(collectUnique(new Random(2026), 3, 0).size());
}
}运行结果
3
010 / 13
挑战 蓝图战利品模拟器
挑战 程序叫 LootSimulator。它用种子2026重复模拟,输出三种奖励数量、总数和发现过的种类数。
- 先验边界0、4、5、24、25、99
- 再定种子2026可重播
- 后查总数不依赖 Set 顺序
验收条件
- roll只能是0到99。
- 钻石区间5格,铁锭20格,面包75格。
- 三种数量相加必须等于1000。
- 相同种子重复运行输出一致。
- Set 中最多出现三种奖励。
| 边界点 | 奖励 |
|---|---|
| 0 | 钻石 |
| 4 | 钻石 |
| 5 | 铁锭 |
| 24 | 铁锭 |
| 25 | 面包 |
| 99 | 面包 |
11 / 13
战利品模拟程序
新建 LootSimulator.java,使用 Java 21 编译运行。
- simulate按种子重复试验
- counts按奖励名计数
- 稳定打印明确钻石、铁锭、面包
LootSimulator.java · 第1/2段,顺序接在同一文件
import java.util.*;
public class LootSimulator {
static String pickRoll(int roll) {
if (roll < 0 || roll >= 100)
throw new IllegalArgumentException("roll应为0到99");
if (roll < 5) return "钻石";
if (roll < 25) return "铁锭";
return "面包";
}
static String pickLoot(Random random) {
return pickRoll(java.util.Objects.requireNonNull(random).nextInt(100));
}
static Map<String, Integer> simulate(int trials, long seed) {
if (trials < 0 || trials > 1_000_000)
throw new IllegalArgumentException("模拟次数应为0到100万");
Random random = new Random(seed);
Map<String, Integer> counts = new HashMap<>();
for (int i = 0; i < trials; i++)
counts.merge(pickLoot(random), 1, Integer::sum);
return counts;
}
static Set<String> collectUnique(Random random, int wanted, int limit) {
if (random == null || wanted < 0 || wanted > 3 || limit < 0)
throw new IllegalArgumentException("检查目标数量和尝试上限");
Set<String> result = new HashSet<>();
for (int attempt = 0; result.size() < wanted && attempt < limit; attempt++)
result.add(pickLoot(random));
return result;LootSimulator.java · 第2/2段,顺序接在同一文件
}
public static void main(String[] args) {
Map<String, Integer> counts = simulate(1000, 2026);
for (String item : List.of("钻石", "铁锭", "面包"))
System.out.println(item + "=" + counts.getOrDefault(item, 0));
System.out.println("总数=" + counts.values().stream()
.mapToInt(Integer::intValue).sum());
System.out.println("发现种类=" + counts.size());
}
}运行结果
钻石=43
铁锭=166
面包=791
总数=1000
发现种类=312 / 13
拓展任务与通关复盘
作业必须附概率区间表和边界测试,不接受只凭肉眼看起来挺随机。
- 概率表每格恰属一种奖励
- 边界表验证相邻分界点
- 模拟表记录次数与频率
三档作业
- 木镐:制作1到6的骰子,运行60次并统计每个点数。
- 铁镐:从奖励池抽取三个不重复物品,候选不足时给出提示。
- 钻石镐:加入保底机制,连续20次未出钻石时下一次必出。
| 自评 | 能 | 还要练 |
|---|---|---|
| 我会使用Set去重 | □ | □ |
| 我能写清概率区间 | □ | □ |
| 我会固定种子测试 | □ | □ |
13 / 13
自己的验收记录
漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。
| 类别 | 你选择的操作 | 核对重点 |
|---|---|---|
| 正常 | 一个常用输入或一次成功动作 | 结果是否逐行一致 |
| 边界 | 范围端点与相邻值 | 有没有多一步、少一项 |
| 错误 | 非法数量、缺失对象或坏数据 | 是否明确拒绝,状态是否保持 |
- Bug 记录:触发条件、实际现象、原因、最小修复。
- 图示记录:为每条箭头说明起点、终点和含义。
- 改编记录:只改一个规则,同时修改预期和测试。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。