本篇文章我们着重讲解本系列课程final大作业的前置作业之一:魔兽世界之一:备战 。
**招募告示:**想做一个苏菲2炼金的网站,用于计算最优/次优合成图,涉及算法、Web开发、UI/UX等等,有兴趣请私我哒~
1. 前置知识
对于本题所需要的前置知识,大家需要对前几章的知识点有一定的认识,不太明白的童鞋,可以参考前几章编程习题的讲解文章:
- 从C走进C++
- 类和对象基础
- 类和对象提高
2. 思路解析:数据结构
题目描述较长,所以我们先来分析题目要求我们做什么,然后再分析需要什么样的数据结构和算法。这样自顶向下进行程序设计,可以避免面对长篇幅问题,而出现无从下手的窘境。那么通过阅读题目描述,我们可以得知下面几点:
1)我们需要开始一个Wow的游戏;
2)该游戏一开始会启动两个司令(Commander),红司令和蓝司令,并传递初始生命资源给每个司令,并启动每个司令,让其生产战士单位;
3)每个司令按照一定的顺序依次生成战士单位,每生成一个战士会消耗一定的生命资源,当生命资源不足时,则停止生产;
以上三点就能概括题目需要我们做的任务,这里大家无需纠结细节,只需要知道大的框架,在拟定大框架之后,我们在一步一步填充细节。那么,有了这些任务,我们需要什么数据结构呢?首先,我们需要两个类,一个Commander类,相当于生产者,依据生命资源生产相应的战士单位,一个Wow类,相当于游戏管理者,我们只需要启动Wow类,它能初始化Commander,并命令其开始生产。
有了这两个类,那么接下来的问题就是这些类需要有什么成员变量来存储数据,以及需要什么方法来执行生产。我们先来看看需要什么成员变量,先从Wow类开始,一步步递进到Commander类。
Wow类:
- 时间变量:用于纪录当前时间戳;
- 红司令和蓝司令:用于启动各自的生产程序;
Commander类:
- 颜色:当前司令的颜色;
- 生命资源:纪录当前剩余的生命资源;
- 每个战士的生命资源消耗列表;
- 每个战士的名字列表:因为每个司令生成的战士顺序不一致;
- 每个战士的个数列表:需要纪录每个战士的个数;
- 战士计数:统计当前一共生成了多少个战士;
3. 代码解析:数据结构
有了类和其数据结构的思路,那么具体的实现是什么样的呢?接下来我们就来看看代码如何实现,首先是Wow类:
class WorldOfWarcraft {
const static int WARRIOR_NAMES_RED[ WARRIOR_NUM ];
const static int WARRIOR_NAMES_BLUE[ WARRIOR_NUM ];
// 时间变量
unsigned int c = 0; // timestamp count
// 红司令和蓝司令
Commander comm_red = Commander( red, WARRIOR_NAMES_RED ); // red headquarter
Commander comm_blue = Commander( blue, WARRIOR_NAMES_BLUE ); // blue headquarter
static std::string getTimeStr( unsigned int c );
public:
// 方法暂略
};
这里还有两个额外的变量:WARRIOR_NAMES_RED 和 WARRIOR_NAMES_BLUE,它们分别纪录了每个司令生成战士的顺序,注意题目中每个司令生成战士的顺序是不同的:
红方司令部按照iceman、lion、wolf、ninja、dragon的顺序循环制造武士。
蓝方司令部按照lion、dragon、ninja、iceman、wolf的顺序循环制造武士。
但为什么他们的数据类型是int不是string,这里我们留到解析成员方法的时候再来揭晓哒~接下来是Commander类:
class Commander {
const static std::string WARRIOR_NAMES[ WARRIOR_NUM ]; // global warrior names
const static char* OUTPUT_FORMAT;
// 颜色
const Color c; // color
// 生命资源
int m; // total life points
// 每个战士的名字列表
const int *W_n; // local warrior names
// 每个战士的生命资源消耗列表
const unsigned *M; // Warrior life points
// 战士计数
unsigned int n = 0; // warrior total count
// 每个战士的个数列表
unsigned int C[ WARRIOR_NUM ] = { 0 }; // warrior count
int idx = -1; // warrior index
bool is_stopped = false;
public:
// 方法暂略
};
同样,这里还有四个额外的变量:WARRIOR_NAMES, OUTPUT_FORMAT, idx和is_stopped,其中WARRIOR_NAMES列举了每一个战士的名字,这个内容对于每个司令都是固定的:
武士一共有 dragon 、ninja、iceman、lion、wolf 五种
OUTPUT_FORMAT用于printf()函数输出相应的输出语句,至于idx和is_stopped我们还是依旧留到函数解析的时候在揭晓答案。同时,为了更好的区别颜色和战士,我们声明两个enum类来表示他们:
enum Color {
red,
blue
};
enum Warrior {
dragon, // = 0
ninja, // = 1
iceman, // = 2
lion, // = 3
wolf // = 4
};
这里需要注意,在没有显性地为每个变量声明值的情况下,变量的值会默认从0开始,具体的例子可以参考上面枚举类Warrior,我标注了每个成员的默认值,这里非常重要,敲黑板哒!
3. 思路解析:算法/成员函数
有了数据结构,接下来我们来分析每个类需要实现的成员函数,也就是说他们需要干什么。首先对于Wow类,它只有一个成员函数start():
class WorldOfWarcraft {
// 成员变量暂略
public:
/**
* Start the game.
*
* @param n Case number.
* @param m Total life points.
* @param M Array to indicate life points to generate a warrior.
* */
void start( unsigned int n, unsigned int m, const unsigned int *M );
};
这个start()函数就是启动整个游戏的入口,对于本题,它只会启动两个司令进行生成工作,同时它有三个参数:
n:测试案例编号;
m:每个司令总的生命资源值;
M:每个司令生成每个战士所需生命资源列表;
在开始游戏之后,那Commander类需要做什么事情呢?
class Commander {
// 成员变量暂略
public:
Commander( Color c, const int *W ) : c( c ), W_n( W ) {}
/**
* Can this commander generate next warrior?
*
* @return An index to that warrior is returned if it can, otherwise -1 is returned.
* */
int hasNext();
/**
* Generate a warrior if this commander can do it.
*
* @param t Timestamp string in the format of "XXX"
* */
void generate( std::string &t );
/**
* Initialize resources.
*
* @param m Total life points.
* @param M Array to indicate life points to generate a warrior.
* */
void initResource( int m, const unsigned *M );
};
除去构造函数,它一共有三个成员函数:
hasNext():检查当前司令是否还有生命资源用于生产;
generate():生产一个战士,并输出相应结果;
initResource:初始化资源,比如总的生命资源值;
总的来说,Wow类需要做的就是,启动两个司令,并告诉他们生产相关的信息,而Commander类需要做的就是,根据所要求的资源信息,如果还有资源剩余,每一个时刻生产相应的战士。在理解上面概念之后,我们就可以具体分析每个类成员函数的实现了。
4. 代码解析:算法/成员函数
为了遵循自定向下的程序设计思路,我们先来看看整体代码的入口函数,也就是我们需要一个函数去初始化Wow类,并启动它,之后才会涉及上面我们讲解的内容。这里我们定义一个入口函数caller():
// -------------------------------------------------------
// Entry function
// -------------------------------------------------------
void caller() {
int n, m;
unsigned int M[ WARRIOR_NUM ];
WorldOfWarcraft wow = WorldOfWarcraft();
// 读取测试案例数n
std::cin >> n;
for ( int i = 1; i <= n; i++ ) {
std::cin >> m;
// 读取每个测试案例的M
for ( unsigned int &j : M ) {
std::cin >> j;
}
// 启动游戏进行生产
wow.start( i, m, M );
}
}
这个入口函数做的事情很简单,先读取测试案例数n,然后读取每个测试案例的M,也就是当前生产战士的资源消耗列表,之后就是启动游戏进行生产。
4.1 Wow类
接下来是Wow类的成员函数start():
void WorldOfWarcraft::start( unsigned int n, unsigned int m, const unsigned int *M ) {
std::cout << "Case:" << n << std::endl;
// 对每个司令进行资源分配
c = 0;
comm_red.initResource( m, M );
comm_blue.initResource( m, M );
std::string s;
// 循环询问是否还有司令有生产能力,有则进行生产
while ( comm_red.hasNext() >= 0 || comm_blue.hasNext() >= 0 ) {
s = getTimeStr( c );
comm_red.generate( s );
comm_blue.generate( s );
c++;
}
// 打印结束生产的语句
s = getTimeStr( c );
comm_red.generate( s );
comm_blue.generate( s );
}
这个函数也很简单,首先对每个司令进行资源分配,然后循环询问是否还有司令有生产能力,有则进行生产。但是这里注意在while循环结束后,还需要调用一次生产语句,有可能有小伙伴要问,这是为什么呢?这是因为我们需要打印结束生产的语句,当while结束的时候,我们只知道这个时候结束生产了,但是我们还需要打印一下相关结果。这么做的结果当然是我们将生产语句都放在Commander类的generate()函数中了。最后还一个额外的私有成员函数getTimeStr():
std::string WorldOfWarcraft::getTimeStr( unsigned int c ) {
std::string s = std::to_string( c );
assert( s.size() < 4 );
switch ( s.size() ) {
// e.g. 1 => 001
case 1:
s.insert( 0, 2, '0' );
break;
// e.g. 22 => 022
case 2:
s.insert( 0, 1, '0' );
break;
}
return s;
}
这个函数是根据当前时间戳生产相应的时间字符串的,也就是在前面补0,比如当前时间是1,那么需要打印的字符串是001,如果是22,则是022,依次类推。
4.2 Commander类
承接Wow类里面调用Commander类的方法顺序,我们从initResource()开始分析:
void Commander::initResource( int m, const unsigned int *M ) {
this->m = m;
this->M = M;
n = 0;
std::fill( std::begin( C ), std::end( C ), 0 );
idx = -1;
is_stopped = false;
}
这个函数就是初始化/重置每个成员变量,记住不要忘记其他变量,因为程序运行时需要多次进行生产,比如战士计数就行进行重置为0。接下来是hasNext():
int Commander::hasNext() {
// Have to explicitly cast M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] to int,
// otherwise the result will always be unsigned int, which is never less than 0.
if ( m - ( int ) M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] >= 0 )
return ( idx + 1 ) % WARRIOR_NUM;
else if ( m - ( int ) M[ W_n[ ( idx + 2 ) % WARRIOR_NUM ] ] >= 0 )
return ( idx + 2 ) % WARRIOR_NUM;
else if ( m - ( int ) M[ W_n[ ( idx + 3 ) % WARRIOR_NUM ] ] >= 0 )
return ( idx + 3 ) % WARRIOR_NUM;
else if ( m - ( int ) M[ W_n[ ( idx + 4 ) % WARRIOR_NUM ] ] >= 0 )
return ( idx + 4 ) % WARRIOR_NUM;
else if ( m - ( int ) M[ W_n[ ( idx + 5 ) % WARRIOR_NUM ] ] >= 0 )
return ( idx + 5 ) % WARRIOR_NUM;
return -1;
}
一眼望去,这个函数到底是干嘛的?这些if-else语句究竟在判断什么?我们先抛开if-else语句,先来看看这个函数的返回值。这个函数的返回值似乎总是( n % m ),这里n和m都是非负整数,那么我们知道一个非负整数模上另一个非负整数,那么结果范围一定是:[ 0, m ),再加上末尾返回的-1,那么这个函数返回值范围为:[ -1, m ),讲解到这里,大家对于这样的返回值能想到什么?什么东西能用到这样的返回值呢?没错哒,数组下标哒,也就是说如果返回非负整数范围,我们就需要到某个数组中取出对应的内容,如果返回-1,则表示无法从该数组中取出值。如果这个数组是战士的生产列表,那么该返回值就表示当前需要生产的战士下标,-1则表示没有生产能力。
那么接下来的问题就是,这个数组究竟是哪一个成员变量呢?不知道大家还记得我们之前提到了一个比较特殊的成员变量:
// 每个战士的名字列表
const int *W_n; // local warrior names
这个成员变量明明纪录的是名字,但类型为什么是int呢?这里我们把每个战士的名字映射到了一个数组下标当中,也就是枚举类Warrior里面定义的下标:
enum Warrior {
dragon, // = 0
ninja, // = 1
iceman, // = 2
lion, // = 3
wolf // = 4
};
// 通过上面的Warrior枚举类,也就说我们我们定义了这样一个数据关系:
0 1 2 3 4
dragon ninja iceman lion wolf
那么我们可以通过这个下标的映射关系,将每个司令单独的战士名字,战士消耗的生命资源,映射到同一个下标下面。为了方便理解,我们举一个例子:
// 红司令生产战士顺序
0 1 2 3 4 // 正常下标
iceman lion wolf ninja dragon // 战士名字, 即WARRIOR_NAMES
1 2 3 4 5 // 生命资源消耗值,即M
// 将其映射到之前定义的枚举数组
0 1 2 3 4 // 正常下标
iceman lion wolf ninja dragon // 战士名字, 即WARRIOR_NAMES
1 2 3 4 5 // 生命资源消耗值,即M
2 3 4 1 0 // 枚举Warrior数组下标,即即W_n
// 假设当前hasnext()返回下标2,也就是说对于红司令的生产战士序列,它需要生产第三个战士,也就是wolf
// 那么我们可以通过W_n[ idx ] = 4获取它在枚举Warrior数据的全局下标,然后通过WARRIOR_NAMES和M分别获取该战士的名字和所消耗的生命资源
// 即WARRIOR_NAMES[ 4 ] = "wolf" 和 M[ 4 ]= 3
// 有童鞋可以有疑问,这样映射我们用正常下标就可以了,为什么要用枚举下标?
// 这是因为这里只有一个司令,如果有多个司令呢?我们知道所有司司令的WARRIOR_NAMES和M其实都是指向同一个数组,即:
0 1 2 3 4 // 枚举下标
dragon ninja iceman lion wolf // 枚举成员变量/战士名字
5 4 1 2 3 // 生命资源消耗值
// 那么仅仅用上面的正常下标就不行了,除非每个司令都保存一个自己的WARRIOR_NAMES和M,但是这样空间开销就太大了
在理解了函数的返回值,接下来我们来分析一下if-else的判断条件代表什么?
// 解析:
m - ( int ) M[ W_n[ ( idx + k ) % WARRIOR_NUM ] ], where 1 <= k <= 5
// 首先,我们逐层剖析,从最里层开始:
( idx + k ) % WARRIOR_NUM
// 我们先解释一下的idx的意义,它表示上一轮司令已经生产的战士下标,比如红司令上一轮生产到wolf,那么idx = 2
// 那么为什么需要从1加到k呢?我们把目光暂时看到之前生产战士顺序的列表上面:
// 红司令生产战士顺序
0 1 2 3 4 // 正常下标
iceman lion wolf ninja dragon // 战士名字, 即WARRIOR_NAMES
^
idx
// 那么如果我们分别加上1,2,3,有什么效果呢?
0 1 2 3 4 // 正常下标
iceman lion wolf ninja dragon // 战士名字, 即WARRIOR_NAMES
^ ^ ^
idx idx + 1 idx + 2
// 我们发现idx发生了右偏移,也就是说指向wolf后面可以生产的两个战士,即nijja和dragon
// 或者我们可以说当前我们需要从idx + 1开始生产,也就是从nijia开始生产
// 如果资源不够资源生产nijia,我们就看看能不能生产dragon
// 那么我们继续加上4,5呢?这不是下标溢出了么?
// 这里就需要 % WARRIOR_NUM ( = 5 ),也就是说让溢出的下标从0重新开始:
0 1 2 3 4 // 正常下标
iceman lion wolf ninja dragon // 战士名字, 即WARRIOR_NAMES
^ ^ ^ ^ ^
idx_3 idx_4 idx_5 idx_1 idx_2
// where idx_5 = idx or ( idx + 5 ) % WARRIOR_NUM, idx_1 = ( idx + 1 ) % WARRIOR_NUM
// idx_2 = ( idx + 2 ) % WARRIOR_NUM, idx_3 = idx + 3 = ( idx + 3 ) % WARRIOR_NUM,
// idx + 4 = ( idx + 4 ) % WARRIOR_NUM
通过上面分析,我们知道了这个函数其实就是实现了下面这个功能:
如果司令部中的生命元不足以制造某个按顺序应该制造的武士,那么司令部就试图制造下一个。如果所有武士都不能制造了,则司令部停止制造武士。
当idx偏移了5次,还是不能生产,那么就返回-1,表示没有能力生产任何战士单位。那么if-eise里面的判断条件也非常好理解了:
( idx + 1 ) % WARRIOR_NUM // 表示当前需要生产的战士在当前司令生产列举中下标
W_n[ ( idx + 1 ) % WARRIOR_NUM ] // 表示这个战士在枚举下标,也就是全局下标
M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] // 表示这个战士需要消耗多少生命资源
m - M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] // 表示如果生产这个战士,还剩多少生命资源
// 也就是说上面的结果如果小于0,表示资源不够生产这个战士
这里需要注意一点,我们显性地转换了 M[ W_n[ ( idx + 1 ) % WARRIOR_NUM ] ] 到 int,否则最后结果会被转换成unsigned int,那么结果永远不可能为负数,也就是说司令有无限的生产能力,这不是游戏里面常见的bug嘛!如果hasnext()返回一个非负整数,那么我们就需要调用generate()函数进行生产了:
void Commander::generate( std::string &t ) {
// 再次检查能否生产,并获取战士下标idx
idx = hasNext();
if ( idx < 0 ) {
// 没有生产能力了,检查是否是第一次进入次代码
if ( !is_stopped ) {
// 如果是,则输出结束结果
is_stopped = !is_stopped;
printf(
"%s %s headquarter stops making warriors\n",
t.c_str(),
COLOR_NAMES[ c ]
);
}
return;
}
// 还有能力继续生产,获取战士相应的数据
int idx_global = W_n[ idx ];
unsigned int warrior_m = M[ idx_global ];
const char *color_name = COLOR_NAMES[ c ];
const char *warrior_name = WARRIOR_NAMES[ idx_global ].c_str();
// 纪录消耗的生命资源
m -= warrior_m;
// 进行结果输出
// https://stackoverflow.com/questions/10865957/how-to-use-printf-with-stdstring
printf(
OUTPUT_FORMAT,
t.c_str(), // timestamp
color_name,
warrior_name,
++n, // Number
warrior_m, // life/strength point
++C[ idx ], // Warrior count
warrior_name,
color_name
);
}
这个函数其实就做了两件事:1)再次检查能否生产,并获取战士下标idx;2)并输出相应的结果。从上面代码也可以看出is_stopped的作用,它用来纪录是否是第一次结束生产,如果是则需要输出结束文本,从这里也可以看出我们为什么要在start()的while结束之后再次调用这个generate()函数,因为我们需要输出结束文本。到此,我们讲解完了本题的所有思路,以及程序设计思路,代码解析,有问题的童鞋可以留言或者发邮箱(见页面下方邮件图标)给我。
5. 项目代码
※ 以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))。
※ 因为本题是课程final大作业的前置作业,所以最终代码会针对后面题目的要求进行修改。之所以这么做是让大家能从一个大项目的角度来看待整个作业,而不是仅仅是一个作业,所以我们会着重于模块化,可读性,可维护性,因此那种把整个代码放在一个文件的情况在这个作业是不会出现的。但是为了应对作业提交的要求(提交只能是一个文件),这里都会提供一个可供提交的版本,大家可以对比提交版本和最终版本的区别,看看我们究竟是从一个小项目一步步创建一个大型项目的,这个能力在后期大家项目中也是非常有用的能力之一。
※ 由于篇幅所限,这里只会给到所有类、成员变量/函数的定义,具体实现请见github相关文件。
// 这里只给到相关定义,完整代码请移步到github
enum Color {
red,
blue
};
enum Warrior {
dragon, // = 0
ninja, // = 1
iceman, // = 2
lion, // = 3
wolf // = 4
};
/**
* Class to generate warriors
* */
class Commander {
const static std::string WARRIOR_NAMES[ WARRIOR_NUM ]; // global warrior names
const static char* OUTPUT_FORMAT;
const Color c; // color
int m; // total life points
const int *W_n; // local warrior names
const unsigned *M; // Warrior life points
unsigned int n = 0; // warrior total count
unsigned int C[ WARRIOR_NUM ] = { 0 }; // warrior count
int idx = -1; // warrior index
bool is_stopped = false;
public:
Commander( Color c, const int *W ) : c( c ), W_n( W ) {}
/**
* Can this commander generate next warrior?
*
* @return An index to that warrior is returned if it can, otherwise -1 is returned.
* */
int hasNext();
/**
* Generate a warrior if this commander can do it.
*
* @param t Timestamp string in the format of "XXX"
* */
void generate( std::string &t );
/**
* Initialize resources.
*
* @param m Total life points.
* @param M Array to indicate life points to generate a warrior.
* */
void initResource( int m, const unsigned *M );
};
/**
* Class to coordinate the game.
* */
class WorldOfWarcraft {
const static int WARRIOR_NAMES_RED[ WARRIOR_NUM ];
const static int WARRIOR_NAMES_BLUE[ WARRIOR_NUM ];
unsigned int c = 0; // timestamp count
Commander comm_red = Commander( red, WARRIOR_NAMES_RED ); // red headquarter
Commander comm_blue = Commander( blue, WARRIOR_NAMES_BLUE ); // blue headquarter
static std::string getTimeStr( unsigned int c );
public:
/**
* Start the game.
*
* @param n Case number.
* @param m Total life points.
* @param M Array to indicate life points to generate a warrior.
* */
void start( unsigned int n, unsigned int m, const unsigned int *M );
};
上一章:类和对象提高
下一章:运算符重载
6. 参考资料
- C++程序设计
- pixiv illustration: Arcaea CG 0-3
7. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;