国际象棋编程,Scala,JVM:动态调度的代价是多少?

时间:2014-07-18 15:20:46

标签: scala jvm chess

我的目标是写一个基本的国际象棋玩AI。它并不需要令人难以置信,但我希望它能够在对游戏有一定程度熟悉程度的人中发挥某种程度的能力。

我有一个名为Piece的特征,它有抽象方法canMakeMove(m:Move,b:Board)和allMovesFrom(p:Position,b:Board)。出于显而易见的原因,这些方法对于程序逻辑很重要,并且由具体类King,Queen,Pawn,Rook,Bishop和Knight实现。因此在其他地方,例如在确定特定板是否具有King的代码中,这些方法在类型为抽象类型Piece的值上调用,(piece canMakeMove(...,... ))所以调用的实际方法是在运行时通过动态调度确定的。

我想知道这对于国际象棋AI程序来说是否太昂贵了,因为它必须多次执行此代码。在网上浏览并阅读有关国际象棋编程的更多内容之后,我发现国际象棋棋盘最常见的表现形式不像我的Vector [Vector [Option [Piece]],而是一个int矩阵(' bit board& #39;)可能会对电路板中的值使用switch语句来实现我目前依靠动态调度来实现的效果。这会阻止我的AI达到可行的性能水平吗?

1 个答案:

答案 0 :(得分:5)

我会在这个答案中建议你在编程中遇到一个常见的陷阱。 我从同事那里学到了一个非常重要的智慧,它已经一次又一次证明了它的价值:

  

只有在问题发生时解决问题,而不是更早

请记住,很长一段时间里都有国际象棋程序和国际象棋电脑。 C64上甚至还有国际象棋程序,还有那些嵌入棋盘的小型旅行棋牌计算机,所以你可以轻松携带它们。

这些系统的资源远远少于现在的计算机几个数量级。 如果你有一个类似于在那些旧系统上运行的算法,并且该算法是在Scala中实现的,使用动态调度,宽大的向量而不是位数组等,它们仍然比它们在旧硬件上更快从过去几天开始。

尽管如此,为国际象棋问题编写一个适度的搜索启发式算法对于单个开发人员来说是一项艰巨的任务。 当面对一项巨大的任务时,一个人直观地倾向于寻找能够解决问题的第一个问题,希望这自然会导致你可以解决的下一个小问题,等等,最终领先彻底解决这个巨大的问题。分而治之。

但是,我个人的经验告诉我,这不是解决更大编程问题的好方法。 通常,如果你遵循这条路径,你最终会得到一个真正优化的棋盘表示,只消耗很少的内存,并尽可能地优化运行时。

然后你就陷入了困境。你把所有的精力放在了这个,你为你的棋盘级别而感到骄傲,不知怎的,你真的不满意你觉得没有更接近实际上有一个工作的国际象棋AI。

我在开始时提到的规则就派上用场了。 简而言之:不要优化。然而。如果有必要,您仍可以稍后进行优化。

相反,请执行以下操作:

  • 制作代表棋盘的类或特征。
  • 在开头留空 - 没有方法或成员。
  • 对表示作品,棋盘位置,移动等的类型执行相同的操作。
  • 然后,当您开始实施国际象棋AI时,请使用方法填写这些类型。但只会添加您真正需要的方法,并且仅在您需要时才添加,而不是任何单一方法
  • 如果需要,您可以在开始时使用特征,稍后您可以生成更优化的特征版本。

通过这种方式,尽可能快地获得国际象棋AI的第一个版本,它可以做一些有趣的事情。 由你来定义什么"有趣"在这种情况下意味着。

如果国际象棋AI的第一个版本很糟糕,并且因为它做出了非常糟糕的决定而输掉了所有游戏,那也没关系。 它应该只有一件事:它应该做一些你至少非常有趣的事情。

然后,确定您对国际象棋AI的最大问题。 考虑如何解决它,提出一个计划以最简单的方式解决那个问题,即使你的程序员本能告诉你做一些更复杂的事情。 抵制快速深入研究复杂性的冲动,因为您编写的最复杂的代码最终将成为您最大的问题,也是您代码库中最不灵活的部分。

简短,简单,几乎是愚蠢的解决方案。原型很多,在短时间内创建简单版本,并且只在需要时进行优化。

至于优化 - 这真的不是问题。 例如,让我们说在开始时你认为定义这样的董事会职位是个好主意:

case class Position(x:Int, y:Int)

稍后,您认为仅使用一组int来表示董事会职位将是一个更好的选择。只需用以下内容替换您之前的定义:

type Position = (Int, Int)

你去吧。进行更改,编译代码,编译错误将显示代码中需要适应的所有位置。重构这件事并不需要很长时间。

即使是在线下,您也可以决定,由于电路板上只有64个可能的位置,您可以轻松地用0到64之间的数字表示位置 - 保留数字64用于"关闭-board&#34 ;. 同样,这很容易改变:

type Position = Byte

保存,编译,修复编译错误。不应超过15分钟。

通过这个例子,我想说明你不应该害怕以后优化,只有在实际发生时等待解决问题。 这总是需要一些重构,但通常不值得一提。 我倾向于将此视为"按摩代码"。

当然,我们知道初级程序员有时会写出意大利面条代码"以后很难重构。 也许这就是为什么几乎每个开发人员都知道后期优化的恐惧,以及早期优化的冲动。

唯一真正解决这种"意大利面条代码"是多次经历这个痛苦的过程。过了一段时间,您将对如何编写可以轻松优化和重构的代码形成非常好的直觉。 这将匹配所有常见的编程智慧,如关注点分离,简短方法,封装等。 在这一点上,我会推荐这本书"清洁代码"作为一个很好的指导方针。

更多提示:

  • 将您的计划视为一座桥梁。你站在桥的一边,什么都没有开始。在远端,有你的愿景 - 一个体面的象棋AI。两端之间存在差距。你的计划将成为桥梁。不要把你所有的精力放在优化你放在桥上的第一块砖上。只有一块完美砖块的桥梁不会成功。首先构建一个原型桥 - 蹩脚,但它连接两端。当你拥有它时,一步一步地增强蹩脚的桥梁会更容易。
  • 将所有功能抽象为特征和类。警惕代码重复 - 当你发现错误或需要进行重构时,重复的代码可能会成为破坏者。
  • 只要代码简单并解决问题,就不要害怕编写看起来不聪明的代码。代码读取得很好,看起来几乎就像算法的直观描述。
  • 保持你的方法简短。一至六行。
  • 确保您的代码不会过于深入 - 具有一点Scala经验的每个人都应该能够理解您的代码正在做什么。
  • 花一些时间来为你的概念找到好名字是一件好事。也许甚至有一些标准来命名国际象棋AI的某些部分。首先做一些研究是件好事。
  • 在进行研究时,查看是否有一些可用的Scala / Java库可用于问题的重要部分。您仍然可以在以后使用自己的实现替换库,但在开始时,使用其他人的知识可以让您快速开始创建第一个原型版本" bridge"。一切都可以帮助你缩小差距"很快就好了。
  • 不要将你的代码视为永恒,完美,不可改变的东西。也许你有志于在Scala中写出最好的棋盘表示。不要这样做。永恒,完美的代码无法轻易地重构或更改。可以轻松重构的代码是非常好的代码。重构代码是程序员的50%。
  • 跳转到自动化测试。对于Scala,使用scalatest库可能是一个不错的选择。使用像SBT这样的工具,它将在每个构建上运行所有自动化测试。如果您对所编写的类有某些假设,请将这些假设硬编码到自动化测试中。一开始会感觉有点愚蠢和多余。但是,当您的一个自动化测试第一次向您显示本来很难找到的错误的原因时,编写这些自动化测试的所有时间都将得到支付。
  • 有关自动化测试的高级主题是代码覆盖率。您可以使用生成覆盖率报告的工具,自动化测试涵盖了多少代码。根据我自己的经验,我建议jacoco4sbt
  • 最后,强制性引用:"优化是所有邪恶的根源。" - 这个有很多智慧。

祝你好运,并且充满乐趣!