📢完整的题目要求和源代码请按下面指示前往我的Github仓库~
第四次作业指导书
第零部分:提交要求 && Junit要求
请保证提交项目的顶层目录存在两个文件夹:src
和test
(命名需严格与此保持一致),请将作业的功能代码存放于src
文件夹下,同时将相关junit测试类代码文件存放于test
文件夹下,以保证评测的正常进行(评测时只会针对src
目录下的文件进行程序功能的评测以及代码风格检测,也就是说,test
目录下的junit测试代码风格不会被检测)。参考目录结构如下:
1 | |-src |
本次作业,要求Junit测试覆盖率保证method >= 90%
,line >= 60%
,branch >= 60%
。(idea
显示的覆盖率和评测
测到的覆盖率可能略有差别,请同学们以评测为准。同时请不要使用assert
进行断言以免造成不必要的覆盖率损失)
第一部分:训练目标
- 对输入进行解析,掌握正则表达式的初步用法
第二部分:题目描述
背景
本次作业仍将基于 第2次作业 与 第3次作业 的内容开发,同学们应当在实现前序题目所要求内容的前提下基于前序作业的代码完成本次作业。
在本次作业中,我们将引入战斗模式与战斗日志(FightLog)的概念。
在战斗模式中,冒险者能且仅能发起攻击或使用药水,并且保证战斗模式结束后所有进入战斗模式的冒险者体力均大于 0。
- 战斗模式中使用药水,可以实现冒险者体力的增加,具体使用要求同前序作业中关于药水使用的定义。
- 战斗模式中发起攻击,分为一对一攻击,和一对多攻击,可以实现被攻击冒险者体力的减少,具体的计算公式和输入格式将在后文给出。
同时我们引入战斗日志的查询功能,主要包括查询某日期的战斗日志,查询某冒险者发起攻击的日志,查询某冒险者受到攻击的日志。
在本次作业中,你需要实现的任务是:
- 在第三次作业的基础上完成战斗模式中冒险者相关属性的增减。
- 实现战斗模式与战斗日志的记录与查询功能。
注意:冒险者的名字保证唯一,药水瓶、装备和食物的名字均可以重复
操作要求
在本次作业中,初始时,你没有需要管理的冒险者,我们通过若干条操作指令来修改当前的状态:
(对于第1-13条,若无特殊说明,则要求和限制同第三次作业)
加入一个需要管理的冒险者(新加入的冒险者不携带任何药水瓶、食物和装备,并且等级为 $1$,初始体力为 $500$)
给某个冒险者增加一个药水瓶
删除某个冒险者的某个药水瓶
给某个冒险者增加一个装备
删除某个冒险者的某个装备
给某个冒险者的某个装备提升一个星级(星级加1)
给冒险者增加一个食物
删除冒险者的一个食物
冒险者尝试携带他拥有的某件装备
冒险者尝试携带他拥有的某个药水瓶
冒险者尝试携带他拥有的某个食物
冒险者使用某个药水瓶
冒险者使用某个食物
指定部分冒险者进入战斗模式及战斗模式中所发生的事件
查询战斗模式下某日期发生的事件
查询战斗模式下某冒险者发起的攻击
查询战斗模式下某冒险者受到的攻击
战斗模式下受到攻击时体力减少量的计算
被攻击者体力的减少量为攻击者使用的武器的星级 乘以 攻击者的等级(一对一攻击和一对多攻击均如此计算):healthPoint_decrease = star * level
输入格式
第一行一个整数 n,表示操作的个数。
接下来的 n 个指令,每条指令占一行,是一个形如 {type} {attribute}
的操作,{type}
和 {attribute}
间、若干个 {attribute}
间使用若干个空格分割,操作输入形式及其含义如下。战斗日志的内容同样每条占一行,但是注意战斗日志内容不属于指令,指令数目n中不包含战斗日志占有的行数
在操作14中,除了本身的指令占一行外,其余的fightLog每条占一行。保证操作14除了fightLog外所有 注意 {type}
{attribute}
均在一行内 具体要求表
type attribute 意义 输出(每条对应占一行) 1 {adv_id} {name}
加入一个 ID 为 {adv_id}
、名字为 {name}
的冒险者 (新加入的冒险者不携带任何瓶子和装备,并且等级为1,初始血量为500)无 2 {adv_id} {bot_id} {name} {capacity}
给 ID 为 {adv_id}
的冒险者增加一个药水瓶,药水瓶的 ID、名字、容量分别为 {bot_id}
、{name}
、{capacity}
,且默认为已装满(isEmpty
==false
)无 3 {adv_id} {bot_id}
将 ID 为 {adv_id}
的冒险者的 id 为 {bot_id}
的药水瓶删除{一个整数} {一个字符串}
,整数为删除后冒险者药水瓶数目,字符串为删除的药水瓶的name4 {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}
的装备删除{一个整数} {一个字符串}
,整数为删除后冒险者装备数目,字符串为删除的装备的name6 {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}
的食物删除{一个整数} {一个字符串}
,整数为删除后冒险者食物数目,字符串为删除的食物的name9 {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)14 m k {adv_name_1} {adv_name_2}
…{adv_name_m}
m
为进入战斗模式的人数,k
为此次战斗模式下战斗日志的条数,name 为 {adv_name_j}
的冒险者进入战斗模式,(m、k和name在一行)接下来的 k
行均为战斗日志首先第一行输出 Enter Fight Mode
,接下来 k 行输出 k 条战斗日志的反馈,参见下文关于战斗日志输出的表格15 YYYY/MM
查询在 YYYY/MM
发生的有效战斗日志按输入顺序每条日志输出一行:使用药水: YYYY/MM {adv_name_1} used {name}
、一对一攻击:YYYY/MM {adv_name_1} attacked {adv_name_2} with {name}
、一对多攻击:YYYY/MM {adv_name_1} AOE-attacked with {name}
。如果不存在符合条件的日志,输出No Matched Log
16 adv_id
查询 id 为 adv_id
的冒险者在战斗模式下作为攻击者的有效战斗日志按输入顺序每条日志输出一行:一对一攻击: YYYY/MM {adv_name_1} attacked {adv_name_2} with {name}
、一对多攻击:YYYY/MM {adv_name_1} AOE-attacked with {name}
。如果不存在符合条件的日志,输出No Matched Log
17 adv_id
查询 id 为 adv_id
的冒险者在战斗模式下作为被攻击者的有效战斗日志按输入顺序每条日志输出一行:一对一攻击: YYYY/MM {adv_name_1} attacked {adv_name_2} with {name}
、一对多攻击:YYYY/MM {adv_name_1} AOE-attacked with {name}
。如果不存在符合条件的日志,输出No Matched Log
关于战斗日志的具体说明
输入格式 | 意义 | 输出格式 |
---|---|---|
YYYY/MM-{adv_name_1}-{name} |
在 YYYY/MM 这个月,名字为 {adv_name_1} 的冒险者使用了名字为 {name} 的药水 |
成功:{一个整数A} {一个整数B} ,整数A为该被使用药水的id,整数B为该冒险者使用该药水后的体力值 失败: Fight log error |
YYYY/MM-{adv_name_1}@{adv_name_2}-{name} |
在 YYYY/MM 这个月,名字为 {adv_name_1} 的冒险者对名字为 {adv_name_2} 的冒险者发起攻击,使用了名字为 {name} 的装备 |
成功:{一个整数A} {一个整数B} ,整数A为被攻击者的id,整数B为该冒险者受到攻击后的体力值 失败: Fight log error |
YYYY/MM-{adv_name_1}@#-{name} |
在 YYYY/MM 这个月,名字为 {adv_name_1} 的冒险者对 剩余所有进入战斗模式的冒险者发起群体攻击,使用了名字为 {name} 的装备 |
成功:按照 进入战斗状态 的次序输出受攻击冒险者被攻击后的体力值,以一个空格隔开 失败: Fight log error |
上述 “Fight log error” 的输出场景为:非法冒险者名(冒险者不处于战斗模式)、非法药水名(该药水未被携带)、非法武器名(该武器未被携带)。保证战斗日志不会出现其他形式的错误。一旦出现错误,则该行战斗日志无效,不产生任何作用,同时 不应出现在15/16/17号命令的查询中
特别地,对于一对多攻击,只要出现上述错误场景,视这条战斗日志无效,所有被攻击者均不会损失体力。
YYYY/MM
代表输入的字符串位数必然为4位数字/2位数字,你在输出的时候也应当采取这样的格式
保证实际月份值在1-12月之间。
特别提醒:在有些程序的实现里,从字符匹配角度来看,一个战斗日志可能既符合YYYY/MM-{adv_name_1}-{name}
也符合YYYY/MM-{adv_name_1}@{adv_name_2}-{name}
,但是在数据限制的情况下有唯一意义,请思考这一点并将其应用于你的程序
样例
1 | 8 |
1 | Enter Fight Mode |
数据限制
变量约束
变量约束
变量 | 类型 | 说明 |
---|---|---|
id | 整数 | 取值范围:0 - 2147483647 |
name | 字符串 | 保证不会出现空白字符, @ , - , # ;长度区间: (0,40) |
capacity | 整数 | 取值范围:1 - 2147483647 |
star | 整数 | 取值范围:0 - 2147483647 |
level | 整数 | 取值范围:1 - 2147483647 |
hitPoint | 整数 | 取值范围:1 - 2147483647 |
energy | 整数 | 取值范围: 0 - 2147483647 |
表格中a-b指变量范围为[a,b]
注意,变量约束指的是,在程序正确运行时,输入和对应属性值均保证在表格中给出的范围内。
操作约束
操作约束
- 保证所有的冒险者、药水瓶、装备、食物 id 均不相同,冒险者之间name均不相同
- 保证删除了的药水瓶/装备/食物的 id 不会再次出现
- 2-6/14/16-17保证所有 id 对应的冒险者均已存在
- 3/5/6/8保证该冒险者拥有操作中提到 id 的药水瓶/装备/食物
- 保证增加的装备,食物和药水瓶原本不存在
- 操作数满足1≤n≤2000
- 9-11保证该冒险者拥有操作中提到 id 的药水瓶/装备/食物
- 12-13 不保证以提到的 name 为名字的物品已经被携带
- 14保证战斗模式结束时,任意一个冒险者的体力均大于0
- 保证战斗日志出现的时候一定处于战斗模式
- 同一次战斗模式下,保证日志输入中日期随输入顺序单调不减;如果多次进入战斗模式,进入战斗模式的日期也随输入顺序单调不减
- 保证 14 中所有 提到的冒险者name均存在
关于第四次作业的解析与说明
第四次作业在第三次的基础上进行扩展,本文也从已经通过第三次作业强测的程序基础上进行讲解分析。
注意:划删除线的部分并非过时信息!
Part 1
指导书要求分析
这一部分就是上面的指导书内容,不再重复。
具体分析
经过本人的实践,我自身在编程中遇到如下三点核心问题:
通过name
属性实现冒险者的快速查找
在前三次作业中,我都是利用id
的唯一性,以此作为键值构建管理冒险者的HashMap,但是在本次作业中,由于name
属性的引入,我不得不考虑如何通过name
属性快速查找到对应的冒险者。
目前,我的方法是:
构建一个从
id
属性到name
属性的HashMap,将其做为一个过渡的查询工具。当每次提供name
属性用来查询时,先通过该过渡容器找到id
,再返回到原容器中查询。
这样虽然增加了一次查询,但是查询均使用hash原理,比直接遍历快上不少。
新增特性:战斗日志
事实上这部分反而不难。
输入解析
根据三种战斗日志格式的第二部分:{adv_name_1}
或 {adv_name_1}@{adv_name_2}
或 {adv_name_1}@#
第一种情况没有@
也没有#
;第二种情况只有@
;第三种情况有@
和#
。
分别识别后用split
函数提取信息处理即可。
日志管理
在战斗日志有效时需要写入日志以便查询。
因为有三种查询方式,为了避免简单遍历导致速度降低,我用三个容器管理三种形式的日志。1
2
3HashMap<String, ArrayList<String>> logByDate = new HashMap<>();
HashMap<Integer, ArrayList<String>> logByAttacker = new HashMap<>();
HashMap<Integer, ArrayList<String>> logByAttacked = new HashMap<>();logByDate
支持按YYYY/MM
日期格式查询;logByAttacker
支持按攻击者id
属性查询;logByAttacked
支持按被攻击者id
属性查询;至于如何写入日志并查询比较简单,不再详述。
Main类重构历程
目标
解决随着选项增多,Main类中switch
语句的臃肿问题(容易超出60行限制)。
解决方案
确保每个
case
只调用一个函数解决对应业务逻辑。修改每个上述函数(后称业务函数)的参数表,只允许有一个参数,尽量不要有返回值。
这是为了配合使用
Consumer
函数式接口,使得每个业务函数都可以看作一个对象(像是C语言中的函数指针),能够给Consumer
“赋值”(说法可能不严谨,体会一下意思)。
- 构建一个
HashMap
成员,将Consumer
作为HashMap
的值,将选项作为HashMap
的键。
例如:
1 HashMap<Integer, Consumer<ArrayList<String>>> actionsMap = new HashMap<>();
在这里我们通过1个Integer
类型的键值来获取1个Consumer
类型的值,这个Consumer
类型的值就是我们的业务函数。
给Consumer
指定的类型就是唯一参数的声明类型。
- 实际调用时,通过
HashMap
的get
方法获取对应的Consumer
,再调用Consumer
的accept
方法。
例如:
1 actionsMap.get(1).accept(args);
这里等价于case
为1时,调用相应的函数。
这里args
是传给业务函数的实际参数。accept
方法是Consumer
提供的方法,用来接收参数并调用业务函数。
- 现在可以去掉
switch
语句块了。
补充
除了Consumer
函数式接口,还有其他很多函数式接口,感兴趣可以自行了解。
这种方法称为表驱动法,可以有效减少代码量,提高可读性。
Part 2
Junit测试,按上个文档新增内容的格式基础上扩展即可。
题目信息