基于C++的变体棋引擎设计问题

5
我有一个国际象棋变种引擎,可以玩自杀象棋和失败者象棋以及普通象棋。随着时间的推移,我可能会添加更多变种到我的引擎中。该引擎完全采用C++实现,并正确使用OOP。我的问题与这样一个变种引擎的设计有关。
最初,该项目只是一个自杀象棋引擎,而后我逐渐添加了其他变种。为了添加新的变种,我首先尝试了在C++中使用多态性。例如,一个MoveGenerator抽象类有两个子类SuicideMoveGeneratorNormalMoveGenerator,根据用户选择的游戏类型,工厂将实例化正确的子类。但我发现这样做速度要慢得多——显然因为实例化包含虚函数的类并在紧密循环中调用虚函数都是相当低效的。
但是后来我想到可以使用C++模板和模板特化来分离不同变量的逻辑,最大限度地重用代码。这也很合理,因为在这种情况下动态链接实际上并不是必要的,一旦选择了游戏类型,您基本上会一直坚持到游戏结束。C++模板特化正好提供了这个功能-静态多态性。模板参数可以是SUICIDELOSERSNORMAL
enum GameType { LOSERS, NORMAL, SUICIDE };

当用户选择游戏类型后,将实例化适当的游戏对象,并从那里调用的所有内容都将被适当地模板化。例如,如果用户选择自杀象棋:

ComputerPlayer<SUICIDE>

对象被实例化,这种实例化基本上与整个控制流静态地关联在一起。在 ComputerPlayer<SUICIDE> 中的函数将使用 MoveGenerator<SUICIDE>Board<SUICIDE> 等,而相应的 NORMAL 则会适当地工作。

总的来说,这让我在开始时实例化正确的模板化专用类,而不需要任何其他的 if 条件,整个过程都能完美地运行。最好的是,根本没有性能损失!

然而,这种方法的主要缺点是使用模板会使代码变得有点难以阅读。此外,如果未正确处理模板特化,则可能导致重大错误。

我想知道其他变体引擎作者通常如何分离逻辑(并实现代码的良好重用)?我发现C++模板编程非常适合,但如果有更好的方法,我很乐意尝试。特别是,我查看了H G Muller博士的Fairymax引擎,但它使用配置文件来定义游戏规则。我不想这样做,因为我的许多变体具有不同的扩展名,通过将其泛化到配置文件级别,引擎可能不会变得强大。另一个流行的引擎Sjeng到处都是if条件语句,我个人觉得这不是一个好的设计。

任何新的设计见解都将非常有用。


我个人认为,ComputerPlayer<SUICIDE>并不比SuicideComputerPlayer更难理解,但如果您这样认为,您可以真正将其称为SuicideComputerPlayer(带或不带继承),并使用Traits技术(http://www.cantrip.org/traits.html)来定义哪些类属于一起。 - Philipp
谢谢指针。我同意它不是很难读,但是由于一旦大多数类都被模板化,所有方法、调用语义等都使用模板构造,这使得代码不再像以前那样干净,而变得冗长和臃肿。实际上,我对当前的设计感到满意,但想探索是否有更好的选择。 - Goutham
2个回答

6
“在紧密循环中调用虚函数效率低下。”实际上,如果循环中的所有变量都具有相同的动态类型,则我会感到非常惊讶,因为我预计编译器将从其L1缓存中获取相应的指令,因此不会遭受太多损失。但是有一个部分让我担心:“显然,由于实例化包含虚函数的类相当低效。”现在...我真的很惊讶。实例化带有虚函数的类的成本几乎无法与实例化没有任何虚函数的类的成本区分开来:只是多了一个指针,仅此而已(在流行的编译器中,这对应于_vptr)。我猜测你的问题在其他地方。所以我要猜一下:你是否有很多动态实例化?(调用new)如果是这种情况,去除它们会使你受益匪浅。有一种名为“策略”的设计模式非常适合您的特定情况。这种模式的思想实际上类似于使用虚函数,但它确实将这些函数外部化。以下是一个简单的示例:
class StrategyInterface
{
public:
  Move GenerateMove(Player const& player) const;
private:
  virtual Move GenerateMoveImpl(Player const& player) const = 0;
};

class SuicideChessStrategy: public StrategyInterface
{
  virtual Move GenerateMoveImpl(Player const& player) const = 0;
};

// Others

一旦实施,您需要一个函数来获取正确的策略:

StrategyInterface& GetStrategy(GameType gt)
{
  static std::array<StrategyInterface*,3> strategies
    = { new SuicideChessStrategy(), .... };
  return *(strategies[gt]);
}

最后,您可以在不使用继承的情况下委派其他结构的工作:
class Player
{
public:
  Move GenerateMove() const { return GetStrategy(gt).GenerateMove(*this); }

private:
  GameType gt;
};

成本与使用虚函数相似,但您不再需要为游戏的基本对象动态分配内存,而堆栈分配速度更快。

策略模式非常适合。我相信如果我能确保它不会因为虚函数而减慢速度,我就可以使用它。回答你关于动态实例化的问题,是的,在我谈到的非模板设计中,我使用了很多动态实例化,并且似乎没有绕过它的方法。当涉及到使用虚函数时,即使我认为不会有任何可见的性能惩罚,但是我错了。国际象棋引擎需要进行极端的计算 - 每秒评估数十万个节点,即使最慢的减速也变得明显。 - Goutham
好的,您可以通过自己保留函数指针来模拟虚表(增加一个解引用),或者采用模板方式。但是将整个程序作为模板可能会降低性能,因为二进制大小会增加。我猜你得试试看 :) - Matthieu M.

0

我不太确定这是否适合,但是您可能可以通过对原始设计进行一些轻微修改,通过CRTP实现静态多态性。


当前的实现方式非常相似,只是我使用了模板特化(使用枚举GameType模板参数)。 - Goutham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接