📢完整的题目要求和源代码请按下面指示前往我的Github仓库~

BUAA-2023-OOpre

题目信息

第三次作业指导书

第零部分:提交要求 && Junit要求

请保证提交项目的顶层目录存在两个文件夹:srctest(命名需严格与此保持一致),请将作业的功能代码存放于src文件夹下,同时将相关junit测试类代码文件存放于test文件夹下,以保证评测的正常进行(评测时只会针对src目录下的文件进行程序功能的评测以及代码风格检测,也就是说,test目录下的junit测试代码风格不会被检测)。参考目录结构如下:

1
2
3
4
5
6
7
8
|-src
|- Bottle.java
|- Equipment.java
|- ...
|-test
|- BottleTest.java
|- EquipmentTest.java
|- ...

本次作业,要求Junit测试覆盖率保证method >= 90%line >= 60%。(idea显示的覆盖率和评测测到的覆盖率可能略有差别,请同学们以评测为准)

第一部分:训练目标

学习并掌握“管理对象”-容器,熟悉 ArrayList 、HashMap 、HashSet 的使用

掌握对象的层次结构,通过编写更多的类进行类的层次结构的理解

第二部分:题目描述

背景

本次作业基于第二次的内容开发,同学们应当在实现第二次题目所要求的内容的前提下基于第二次的代码完成本次作业。

好的,经过上次作业的准备,我们的冒险者可以拥有一些装备和一些药水瓶,但是想要外出冒险,不可能带着所有的装备和所有的药水瓶瓶罐。因此在本次作业,我们新增了一个叫 背包 的概念。

同时为了量化冒险者的状态,我们为冒险者引入了两个属性:体力(HitPoint),等级(level)

体力代表冒险者当前的体力值,保证在程序正确运行时,冒险者体力时刻大于 0 。

等级,决定了他的背包的容量(后面会详细规定)

同时,关于等级,我们需要引入食物类(Food),通过使用食物可以提升冒险者的等级

在本次作业中,你需要实现的任务是:

在第一次作业的基础上完成冒险者新增加属性的管理

实现冒险者的背包功能,并依据要求约束实现背包内物品数目的控制

背包限制

在上一次的作业里,我们定义了添加的概念(add),这个仅仅是让这名冒险者拥有了这个物品,但是他并没有携带这个物品。我们认为,当且仅当这个物品属于该冒险者且在该名冒险者的背包中,才算他携带了这个物品。

下面对每类物品给出携带与使用的规定。

装备

限制

冒险者只能携带一件同名装备

若冒险者已经携带了名字为 sword 的装备(该装备 id 为 $1$),下一次再尝试携带另一个名字为 sword 的不同装备(该装备id为 $2$)时,原本 id 为 $1$ 的 sword 会被顶替。注意被顶替的装备依然属于此冒险者。

药水瓶

当冒险者携带药水瓶 A 时,他才能使用该药水瓶 A,否则为使用失败。

冒险者使用某药水瓶时,若药水瓶不为空,则冒险者的体力增加 $x$($x$ 为该药水瓶的容积),药水瓶变空。

若药水瓶为空,冒险者的体力增加为 0,同样视作使用成功。同时,为了给继续携带药水瓶腾出空间,在使用后冒险者将丢弃该空药水瓶,即该药水瓶将不再被该冒险者拥有。对于这个丢弃行为我们不需要进行输出。

限制

冒险者只能携带 max_bottles 个同名的药水瓶,其中max_bottles的值满足

例如,若冒险者等级为 19, 则 max_bottles 的值是 $4$。假如在这时,该冒险者携带了四个同名药水瓶 water,之后再尝试携带同名的药水瓶后时,他的状态不会有任何改变,也不需要输出任何内容,他也不会携带新的同名药水瓶。

食物

当冒险者携带食物 A 的时候,他才能使用该食物 A,否则为使用失败。

当冒险者使用食物时,他将消耗掉该食物(从此该食物不再属于该冒险者),冒险者的等级提升 $x$($x$ 为食物的能量)。

比如冒险者当前等级为 1,拥有食物 A,A 的能量为 2。冒险者在携带后使用食物 A,则冒险者不再拥有食物 A,同时冒险者的等级变为 3。

限制

背包对食物的数量没有任何限制。

特别的,我们规定,假设冒险者 A 尝试携带的物品 B ,本身已经在冒险者背包里了,那么该条指令不会造成任何影响(不需要任何输出,同时物品 B 依旧在冒险者的背包里 )

操作要求

在本次作业中,初始时,你没有需要管理的冒险者,我们通过若干条操作指令来修改当前的状态:

第2-6条同第一次作业

加入一个需要管理的冒险者(新加入的冒险者不携带任何药水瓶、食物和装备,并且等级为 $1$,初始体力为 $500$)


给某个冒险者增加一个药水瓶


删除某个冒险者的某个药水瓶


给某个冒险者增加一个装备


删除某个冒险者的某个装备


给某个冒险者的某个装备提升一个星级(星级加1)


给冒险者增加一个食物


删除冒险者的一个食物


冒险者尝试携带他拥有的某件装备


冒险者尝试携带他拥有的某个药水瓶


冒险者尝试携带他拥有的某个食物


冒险者使用某个药水瓶


冒险者使用某个食物

值得注意的是,在12和13中,我们采用 name 来确定所使用的物品。假设当前冒险者携带了多个同名物品,则使用 id最小的那个。

同时,指令使用名字为name的物品时,如果冒险者携带了名字为该 name 的物品,则视为使用成功,(使用空瓶也算使用成功),假设冒险者没有携带名字为该 name 的物品,则视为使用失败,不产生任何其他影响。两种情况需按照下面的输入输出格式进行输出

输入输出格式

第一行一个整数 $n$,表示操作的个数。

接下来的 $n$ 行,每行一个形如 {type} {attribute} 的操作,{type}{attribute} 间、若干个 {attribute} 间使用若干个空格分割,操作输入形式及其含义如下

具体要求表
typeattribute意义输出(每条对应占一行)
1{adv_id} {name}加入一个 ID 为 {adv_id}、名字为 {name} 的冒险者
2{adv_id} {bot_id} {name} {capacity}给 ID 为 {adv_id} 的冒险者增加一个药水瓶,药水瓶的 ID、名字、容量分别为 {bot_id}{name}{capacity}且默认为已装满
3{adv_id} {bot_id}将 ID 为 {adv_id} 的冒险者的 id 为 {bot_id} 的药水瓶删除{一个整数} {一个字符串},整数为删除后冒险者药水瓶数目,字符串为删除的药水瓶的name
4{adv_id} {equ_id} {name} {star}给 ID 为 {adv_id} 的冒险者增加一个装备,装备的 ID、名字、星级分别为 {equ_id}{name}{star}
5{adv_id} {equ_id}将 ID 为 {adv_id} 的冒险者的 id 为 {equ_id} 的装备删除{一个整数} {一个字符串},整数为删除后冒险者装备数目,字符串为删除的装备的name
6{adv_id} {equ_id}将 ID 为 {adv_id} 的冒险者的 id 为 {equ_id} 的装备提升一个星级{一个字符串} {一个整数},字符串为装备的name,整数为装备升星后的星级
7{adv_id} {food_id} {name} {energy}给 ID 为 {adv_id} 的冒险者增加一个食物,食物的 ID、名字、能量分别为 {equ_id}{name}{energy}
8{adv_id} {food_id}将 ID 为 {adv_id} 的冒险者的 id 为 {food_id} 的食物删除{一个整数} {一个字符串},整数为删除后冒险者食物数目,字符串为删除的食物的 name
9{adv_id} {equ_id}ID 为 {adv_id} 的冒险者尝试携带ID为 {equ_id} 的装备
10{adv_id} {bot_id}ID 为 {adv_id} 的冒险者尝试携带ID为 {bot_id} 的瓶子
11{adv_id} {food_id}ID 为 {adv_id} 的冒险者尝试携带ID为 {food_id} 的食物
12{adv_id} {name}ID 为 {adv_id} 的冒险者尝试使用他拥有的名字为{name}的药水瓶成功:{一个整数A} {一个整数B},整数A为被使用药水瓶的id,整数B为该冒险者使用该药水瓶后的体力值
失败: fail to use {name},name为本条指令输入中的name)
13{adv_id} {name}ID 为 {adv_id} 的冒险者尝试使用他拥有的名字为{name}的食物成功:{一个整数A} {一个整数B},整数A为被使用食物的 id,整数B为该冒险者使用该食物后的等级
失败: fail to eat {name},name 为本条指令输入中的 name

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
16
1 123456 advName
2 123456 123 bottleName 40
10 123456 123
2 123456 321 bottleName 20
10 123456 321
12 123456 bottleName
12 123456 bottleName
12 123456 bottleName
7 123456 111 foodName 10
7 123456 888 candy 20
8 123456 888
13 123456 foodName
11 123456 111
13 123456 foodName
7 123456 222 foodName 30
8 123456 222
1
2
3
4
5
6
7
123 540
123 540
fail to use bottleName
1 candy
fail to eat foodName
111 11
0 foodName

数据限制

变量约束

变量约束
变量类型说明
id整数取值范围:0 - 2147483647
name字符串保证不会出现空白字符,长度区间: (0,40)
capacity整数取值范围:0 - 2147483647
star整数取值范围:0 - 2147483647
energy整数取值范围: 0-2147483647
level整数取值范围:1 - 2147483647
HitPoint整数取值范围:0 - 2147483647

注意,变量约束指的是,在程序运行时,输入和对应属性值均保证在表格中给出的范围内。

操作约束

操作约束
  1. 保证所有的冒险者、药水瓶、装备、食物id均不相同
  2. 保证删除了的药水瓶/装备/食物的 id 不会再次出现
  3. 操作 2-6 保证所有冒险者均已存在
  4. 操作 3/5/6/8 保证该冒险者拥有操作中提到 id 的药水瓶/装备/食物
  5. 保证增加的装备,食物和药水瓶原本不存在
  6. 操作数满足$1\le n\le2000$
  7. 操作 9-11保证该冒险者拥有操作中提到 id 的药水瓶/装备/食物
  8. 操作 12-13 保证提到的物品已经被携带

关于第三次作业的解析与说明

第三次作业在第二次的基础上进行扩展,本文也从已经通过第二次作业强测的程序基础上进行讲解分析。

注意:划删除线的部分并非过时信息!

Part 1

指导书要求分析

指导书扩展要求后,要求为:
实现以下类

1
2
3
4
Adventure :ID,名字,药水瓶和装备各自的容器,背包,体力(HitPoint),等级(level)
Bottle:ID,名字,容量(capacity)
Equipment:ID,名字,星级(star)
Food:ID,名字,能量(energy)

以及实现以下操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.加入一个需要管理的冒险者(新加入的冒险者不携带任何药水瓶和装备)
2.给某个冒险者增加一个药水瓶
3.删除某个冒险者的某个药水瓶
4.给某个冒险者增加一个装备
5.删除某个冒险者的某个装备
6.给某个冒险者的某个装备提升一个星级
* 其中,提升星级的意思是,新星级=原有星级+1
7.给冒险者增加一个食物
8.删除冒险者的一个食物
9.冒险者尝试携带他拥有的某件装备
10.冒险者尝试携带他拥有的某个药水瓶
11.冒险者尝试携带他拥有的某个食物
12.冒险者使用某个药水瓶
13.冒险者使用某个食物

其中,新增的背包要求是本次作业的核心,引用如下:

背包限制
在上一次的作业里,我们定义了添加的概念(add),这个仅仅是让这名冒险者拥有了这个物品,但是他并没有携带这个物品。我们认为,当且仅当这个物品属于该冒险者且在该名冒险者的背包中,才算他携带了这个物品。下面对每类物品给出携带与使用的规定。

装备

  • 限制
    冒险者只能携带一件同名装备
    若冒险者已经携带了名字为 sword 的装备(该装备 id 为 1),下一次再尝试携带另一个名字为 sword 的不同装备(该装备id
    2)时,原本 id 为 1 的 sword 会被顶替。注意被顶替的装备依然属于此冒险者。

药水瓶
当冒险者携带药水瓶 A 时,他才能使用该药水瓶 A,否则为使用失败。
冒险者使用某药水瓶时,若药水瓶不为空,则冒险者的体力增加 x(x 为该药水瓶的容积),药水瓶变空。
若药水瓶为空,冒险者的体力增加为 0,同样视作使用成功。同时,为了给继续携带药水瓶腾出空间,在使用后冒险者将丢弃该空药水瓶,即该药水瓶将不再被该冒险者拥有。对于这个丢弃行为我们不需要进行输出。

  • 限制
    冒险者只能携带 max_bottles 个同名的药水瓶,其中max_bottles的值满足 max_bottles = level / 5 + 1
    例如,若冒险者等级为 19, 则 max_bottles 的值是 4。假如在这时,该冒险者携带了四个同名药水瓶 water
    ,之后再尝试携带同名的药水瓶后时,他的状态不会有任何改变,也不需要输出任何内容,他也不会携带新的同名药水瓶。

食物
当冒险者携带食物 A 的时候,他才能使用该食物 A,否则为使用失败。
当冒险者使用食物时,他将消耗掉该食物(从此该食物不再属于该冒险者),冒险者的等级提升 x(x 为食物的能量)。
比如冒险者当前等级为 1,拥有食物 A,A 的能量为 2。冒险者在携带后使用食物 A,则冒险者不再拥有食物 A,同时冒险者的等级变为3。

  • 限制
    背包对食物的数量没有任何限制。

特别的,我们规定,假设冒险者 A 尝试携带的物品 B ,本身已经在冒险者背包里了,那么该条指令不会造成任何影响(不需要任何输出,同时物品B 依旧在冒险者的背包里 )

思路要点

应该分成两个部分

基于上次作业形式的直接扩展与修改

具体有:

Food类的实现与管理

remove型函数返回类型的优化

本次作业的核心新增特性:背包管理

具体有:

携带动作:takenBottlestakenEquipmentstakenFoods的实现与管理

使用动作:useBottleeatFood的实现

具体分析

针对思路要点里面的思路进行实现

1. Food类的实现与管理

这点很简单。
Food类和先前的Bottle类、Equipment类内部结构很像,foods容器的管理也和先前的bottlesequipments容器的管理很像,还有addremove函数的实现也很像,所以这里不再赘述,参照前一作业模板即可。

2. remove型函数返回类型的优化

这点也不是很难。
由于第四点使用动作要求使用成功与失败返回不同的形式,因此不能直接返回一个PrintInfo类类型。最简单的思路就是采用直接打印的形式,分情况打印不同的信息。

3. 携带动作:takenBottlestakenEquipmentstakenFoods的实现与管理

三、四两点是本次作业的核心。
携带动作的要求上面已经引用。我目前的实现思路是将BottleEquipmentFood三个物品分别对应设置Adventure类中的三个私有属性:takenBottlestakenEquipmentstakenFoods,分别用于管理冒险者携带的药水瓶、装备、食物。

  • 将来有需要可以将其封装成Package类。

重点是用什么容器类型实现这三个属性
我们应该注意到,对背包物品的使用是以name属性来查找,并且取id最小的那个。因此,以takenBottles为例,我们应该使用的是HashMap<String , TreeMap<Integer,Bottle>>类型。
HashMapString做为key,提供了按名字查找的方法,简洁高效;
TreeMapInteger做为key,提供了按id升序排列的一系列同名物品,我们要用的就是一系列同名物的第一个。
由于携带要求不同,几个属性的实现也有所不同,具体如下:
对于takenBottles,应该根据maxBots,先判断是否已有同名物品,若没有则新建一个以该物品名字为key,新建映射;若有,则找到value(TreeMap),判断是否已有Bottle数是否已达到上限maxBots,若达到上限则不做任何操作,若未达到上限则在value中新增这个Bottle
对于takenEquipments,由于其特殊性,类型可以简化为HashMap<String , Equipment>,每次无需判断直接加入,HashMapput方法会自动覆盖同名物品。
对于takenFoods,和takenBottles类似,但是不需要判断上限,省略该步即可。

4. 使用动作:useBottleeatFood的实现

使用动作的要求上面已经引用。
首先判断是否有所需名字的物品,没有,按格式输出;有,则利用TreeMap特性,取出第一个物品,进行使用。
TreeMap提供了firstEntry方法和pollFirstEntry方法,前者只返回第一个映射,即TreeMapid最小的那个,后者返回并移除这个映射。
对于useBottle,先只返回,按要求分空与不空进行处理(hp等),切记当一个名字下没有物品时(TreeMap为空),要在HashMap中移除这个映射
对于eatFood,直接返回并移除,按要求进行处理(level等),切记重算maxBot,同样当一个名字下没有物品时(TreeMap为空),要在HashMap中移除这个映射

最后

在主程序Main类中,单独使用main方法已经太长,可以把每种不同的操作封装成一个方法,然后在main
方法中调用这些方法,这样可以使得main方法更加简洁,也方便调试。
本部分就大功告成了。

Part 2

仍然是Junit测试,这次有覆盖率要求,按照教程操作即可。

Part 2新增内容

在后续的过程中,我发现不少人对于Junit的覆盖率要求比较头疼。在正式提交评测前,我本人也对Main类的结构进行了重新修改,这里分享一下我的大致结构。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int n;
HashMap<Integer, Adventure> advs = new HashMap<>();
Scanner scanner = new Scanner(System.in);
n = Integer.parseInt(scanner.nextLine().trim()); //注意,直接用nextInt()会出错
for (int i = 0; i < n; i++) {
String nextLine = scanner.nextLine(); // 以一行字符串的形式读入一条指令
makeChoice(advs, getOrders(nextLine));
/*getOrders把一行指令拆成各部分,返回一个ArrayList<String>,其中第一个元素是指令类型,后面的元素是指令的参数
makeChoice根据指令类型,调用相应的方法,switch语句就在该方法中*/
}
}

需要遵守的原则是,main方法中尽量只有输入处理,且所有输入处理都在这里面完成。然后对其他所有方法编写单元测试。

Part 3 bug修复记录

Bug 001

说明

remove型函数执行时,需要将背包里的相应物品也一并删除!

改动详情

Adventure.java文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//现第70行
- if (takenBottles.containsKey(bottleName))
+ if (takenBottles.containsKey(bottleName) && !takenBottles.get(bottleName).isEmpty())

//现第92行
- if (takenFoods.containsKey(foodName))
+ if (takenFoods.containsKey(foodName) && !takenFoods.get(foodName).isEmpty())

//现第111行
+ if (takenBottles.containsKey(name)) {
+ takenBottles.get(name).remove(bottleId);
+ if (takenBottles.get(name).isEmpty()) {
+ takenBottles.remove(name);
+ }
+ }


//现第123行
+ if (takenEquipments.containsKey(name)) {
+ int id = takenEquipments.get(name).getId();
+ if (id == equipmentId) {
+ takenEquipments.remove(name);
+ }
+ }

//现第135行
+ if (takenFoods.containsKey(name)) {
+ takenFoods.get(name).remove(foodId);
+ if (takenFoods.get(name).isEmpty()) {
+ takenFoods.remove(name);
+ }
+ }