表达式解析
既然从CLI的参数开始说起,那么我们就来看看一个DPS模拟器如何解析输入的参数,“解析”说得玄乎,其实大体意思就是“读懂”。
我们只讲实现原理,与具体实现无关。
这一部分其实很像代码编译器/解释器。SimC参数本身就是一种代码,我们在程序运行的初始阶段要对它予以解释。
输入参数的解析
首先是输入参数的解析。其实这很简单,举一例:
Code (c):
warrior=Aean
我们逐字扫描。一开始,我们一定会扫描到一个关键字,所以我们的程序期望得到一个关键字。
扫描到'w'
w是一个字符,把它暂时存起来。现在我们存了一个“w”,并且期望它是一个还未写完的关键字。
扫描到'a'
a也是一个字符,把它和w存在一起。现在我们存了一个“wa”,并且期望它是一个还未写完的关键字。
扫描到'r'
r也是一个字符。现在我们存了“war”
……
扫描到'r'
现在我们存了“warrior”。虽然它已经是一个关键字了,但我们仍要继续扫描。因为某些关键字会是另一些关键字的前缀,比如“actions”和“actions.pre_combat”。
扫描到'='
扫描到等号是一个转折点。期望得到一个关键字并扫描到一个等号,这意味着关键字已经全部录入进来。现在我们可以检查我们存起来的“warrior”是不是一个关键字了。
很显然“warrior”是一个关键字,它声明了一个战士玩家,其值为玩家的名字。
现在我们可以调用warrior_t类的构造函数,等待输入表达式中的值部分,为warrior_t实例的名字属性赋值。
现在程序期望得到一个“战士的名字”类型的值。
依次扫描到'A' 'e' 'a' 'n'
它们都是字符,所以把它们存起来。
扫描到'\0',也就是字串结尾的标志位。
这说明了此条表达式已经结束,所以也意味着值已经输入完毕了。
检查已经存起来的“Aean”是否可以转化为一个战士的名字?战士的名字应该是个字串,很显然“Aean”是个字串。OK,现在给刚刚构造的warrior_t的名字属性赋值。
最后,我们把这个战士加入到需要模拟的团队里来。
由于这条表达式刚刚声明了一个战士,所以接下来,除非再声明一个其他玩家或者敌人,所有的表达式都应该是描述战士Aean或者描述模拟设置的。
假设我们将参数错写成:
Code (c):
warrioraa=Aean
当程序扫描到'='时,存储的字串是“warrioraa”。程序期望它是一个关键字,但它不是关键字。这时程序报错:“无法识别关键字warrioraa”。
copy关键字就是将调用player_t派生类构造函数改为调用其复制构造函数,这样新角色将获得上一个角色已经获得的全部属性。
main_hand关键字的参数可能看起来复杂一些,但无非也就是按这个顺序进行的,只不过是一个weapon_t类的参数(上限、下限、速度、类型、名字……)全都在这一行表达式里赋了值,一次赋了很多的值。
追加符的实现
某些关键字(例如“actions”)支持用等号“=”赋值,也支持用追加符“+=”进行追加。
其实也很好实现,只要将“actions”和“actions+”视作不同的关键字就可以了。
如果使用追加符,第一次扫描到'='时程序所保存的字串是“actions+”,这时对动作优先级列表字串进行追加。使用等号时,程序第一次扫描到'='所保存的字串会是“actions”,这时抛弃动作优先级列表字串现存的内容,予以重新赋值。
条件逻辑的实现
输入的动作优先级列表必须要搭配一定的条件逻辑,才能构成优先级。
对这种逻辑表达式,我们举一例:
Code (c):
if=(blood=2|death=2)&!time<5
这种表达式采用二叉树来表示是绝佳的。设想定义一个二叉树结点类型expr_t,具有左值lvalue指向另一个结点、
右值rvalue指向另一个结点,和自身求值函数eval
expr_t.eval = expr_t.lvalue->eval (某种运算) expr_t.rvalue->eval
就可以模拟整个逻辑表达式。例如上述表达式可以构建出这样一棵二叉树:
单目运算符(variable)可以让其存储一个函数指针,每次调用eval的时候访问指定函数来求值。单目运算符(constant)就很简单了,存储一个double类型的数据,然后每次调用eval都返回它。
这样我们每次扫描动作优先级列表的时候,只要访问根节点的eval就可以得到整个逻辑表达式的真假。
我们简单看一下SimC的代码片段,然后就结束。
基类:
Code (c):
class expr_t //这是树中所有节点类型的基类
{
std::string name_; //保存名字。
protected: //下面这两句函数重载,负责将类型转换正确。其他类型只要转型成double即可。
template <typename T> static double coerce( T t ) { return static_cast<double>( t ); }
static double coerce( timespan_t t ) { return t.total_seconds(); } //对timespan_t的特殊处理,这是SimC的时间类型。用秒为单位返回。
virtual double evaluate() = 0; //evaluate是一个纯虚函数,具体实现由派生类去定义。
public:
expr_t( const std::string& name ) : name_( name ) {} //构造函数,只要名字。
virtual ~expr_t() {} //析构函数是虚的,因为基类中还没有定义子节点,只有单目和双目类型才有子节点。
const std::string& name() { return name_; } //取名字。
double eval() { return evaluate(); } //派生类覆写了evaluate,eval总是返回evaluate的值。
bool success() { return eval() != 0; } //根节点需要这个东西。如果根节点的eval不为零,说明式子成立。否则不成立。
static expr_t* parse( action_t*, const std::string& expr_str ); //这个函数是字串解析的入口,它负责构建整棵树。
};
变量节点(引用型):
Code (c):
template <typename T> //模板。
class ref_expr_t : public expr_t //这是引用型的变量节点类型,继承自基类expr_t
{
const T& t; //存储一个类型为T的引用,T由模板给出。通过引用取到的值是可以随着被引用量一起变化的,所以是变量。
virtual double evaluate() { return coerce( t ); } //强制转型后就直接输出。
public:
ref_expr_t( const std::string& name, const T& t_ ) : expr_t( name ), t( t_ ) {} //构造函数,给基类命名,然后将引用存起来。
};
变量节点(函数型):
Code (c):
template <typename F> //模板。这里F应该是一种函数指针。
class fn_expr_t : public expr_t //函数型的变量节点类型,继承自基类expr_t
{
F f; //存储一个函数指针。
virtual double evaluate() { return coerce( f() ); } //通过函数指针调用指定的函数来取值,强制转型后输出。
public:
fn_expr_t( const std::string& name, F f_ ) : //构造函数。
expr_t( name ), f( f_ ) {} //成员初值列:给基类命名,存储函数指针f。
};
//===============================================
//这个函数型变量节点类型,看不明白就看下面这段代码。
//这是用于构造这类节点的两个函数。
//函数1:调用普通函数的。
template <typename F> //F就是函数签名。
inline expr_t* make_fn_expr( const std::string& name, F f ) //两个参数:名字,和符合F签名的函数指针f。
{ return new fn_expr_t<F>( name, f ); } //对于普通的函数,直接构建就可以了。
//函数2:调用一个类成员函数的。
template <typename F, typename T> //F仍然是函数的签名,T是该函数所在类的类型。
inline expr_t* make_mem_fn_expr( const std::string& name, T& t, F f ) //三个参数:多输入一个指向类实例的引用。
//要知道,所有的“类成员函数”都有一个隐含的参数,就是this指针。
//所以类成员函数表面上没有参数的,其实有一个参数。表面上有两个参数的,实际有三个参数。
//进行调用的时候,只提供表面上签名所要求的那些参数是不够的。你必须提供这个隐含的this指针才能成功调用。
//否则一个类的实例千千万,鬼知道你要调用的是哪一个实例的?
{ return make_fn_expr( name, std::bind( std::mem_fn( f ), &t ) ); } //把隐含的参数this补上,使用std::bind来完成。
//===============================================
常量节点:
Code (c):
class const_expr_t : public expr_t //常数结点类型,继承自基类expr_t
{
double value; //在其中存储一个double类型的数据作为常量。
public:
const_expr_t( const std::string& name, double value_ ) : //构造函数
expr_t( name ), value( value_ ) {} //成员初值列:给基类命名(名字是用来在出错时报错的),给存储的double赋值。
double evaluate() // override 重写基类的evaluate函数,
{ return value; } //使其直接返回存储的double就行了。
};
单目运算符:
Code (c):
template <double ( *F )( double )> //使用模板可以减少代码量。接受一个参数为double、返回值为double的函数指针。
class expr_unary_t : public expr_t //这些是单目运算符。继承自基类expr_t
{
expr_t* input; //单目运算符没有左值和右值,只有单个输入。
public:
expr_unary_t( const std::string& n, expr_t* i ) : //构造函数
expr_t( n ), input( i ) //给基类命名,给指向子节点的指针赋值。
{ assert( input ); } //断言指针不应该是空的。这是个Debug语句,没有实际效果。
~expr_unary_t() { delete input; } //自己析构的时候,顺带将子节点也析构掉。
//这样只要对根节点进行析构,就可以析构整棵逻辑树。
double evaluate() // override 重写基类的evaluate函数,
{ return F( input -> eval() ); } //将子节点的eval返回值,经由模板函数F做某种运算(逻辑非、相反数、绝对值、取整等等),然后送给父节点。
};
双目运算符:
Code (c):
class binary_base_t : public expr_t //这是所有双目运算符的基类,继承自expr_t。
{
protected: //Protected型的变量,自己和自己的派生类可以调用,而其他人不行。
expr_t* left; //左值指针
expr_t* right; //右值指针
public:
binary_base_t( const std::string& n, expr_t* l, expr_t* r ) : //构造函数,除了名字之外,当然还需要指向左值和右值的指针。
expr_t( n ), left( l ), right( r ) //初值列
{
assert( left ); //断言左值指针非空
assert( right ); //断言右值指针非空
}
~binary_base_t() { delete left; delete right; } //析构时,将左值节点和右值节点一起析构。
}; //这样只要对根节点进行析构,就可以析构整棵逻辑树。
template <template<typename> class F> //套了两层模板。实际上,“template<typename> class F”是一些STL的签名,后边列出来。
class expr_binary_t : public binary_base_t //这是真正的双目表达式类型,继承上面那个类。
{ //由于上面那个类声明了protected类型的左值和右值指针,这里就无需再声明private类型的左值和右值指针了。
public:
expr_binary_t( const std::string& n, expr_t* l, expr_t* r ) : //构造函数
binary_base_t( n, l, r ) //用上面那个类的构造函数。
{}
double evaluate() // override 覆写evaluate,将左值节点的eval和右值节点的eval做F运算,
{ return F<double>()( left -> eval(), right -> eval() ); } //输入和输出都是double。
};
//这里F其实代表了一些STL:
//std::plus
//std::minus
//std::multiplies
//std::divides
//std::equal_to
//std::not_equal_to
//std::less
//std::less_equal
//std::greater
//std::greater_equal
//这些都是数值运算。所以做成了一个模板类expr_binary_t。
//如果我想创建一个加法节点,就写 new expr_binary_t<std::plus>( name, left, right ) 。
//减法就把plus换成minus。模板大大减少了代码量,为每种计算创建一个形式相同的类,这种无聊工作由编译器承担。
//逻辑运算的数值的类型不是double,所以为逻辑运算“与”和“或”专门写了类。
class logical_and_t : public binary_base_t //与类
{
public:
logical_and_t( const std::string& n, expr_t* l, expr_t* r ) :
binary_base_t( n, l, r )
{}
double evaluate() // override
{ return left -> eval() && right -> eval(); } //左右相与
};
class logical_or_t : public binary_base_t //或类
{
public:
logical_or_t( const std::string& n, expr_t* l, expr_t* r ) :
binary_base_t( n, l, r )
{}
double evaluate() // override
{ return left -> eval() || right -> eval(); } //左右相或
};