功能语言的二维棋盘游戏的数据结构


9

我正在用功能编程语言Elixir 创建一个简单的MiniMax实现。因为有很多完美知识的游戏(井字游戏,四连冠,西洋跳棋,国际象棋等),所以此实现可能是为任何这些游戏创建游戏AI的框架。

但是,我面临的一个问题是如何以功能语言正确存储游戏状态。这些游戏主要处理二维游戏板,其中经常执行以下操作:

  • 阅读特定板位的内容
  • 更新特定板位置的内容(返回新的移动可能性时)
  • 考虑连接到当前位置的一个或多个位置的内容(即下一个或上一个水平,垂直或对角线位置)
  • 考虑任何方向上多个连接位置的内容。
  • 考虑整个文件,等级和对角线的内容。
  • 旋转或镜像电路板(以检查对称性,以提供与已经计算出的结果相同的结果)。

大多数功能语言都使用链接列表和元组作为多元素数据结构的基本构建块。但是,这些似乎对这项工作非常不利:

  • 链接列表具有O(n)(线性)查找时间。另外,由于我们无法在板上进行一次扫描来“扫描和更新板”,因此使用列表似乎非常不切实际。
  • 元组具有O(1)(恒定)查找时间。但是,将电路板表示为固定大小的元组将使得很难遍历等级,文件,对角线或其他类型的连续正方形。此外,Elixir和Haskell(这是我所知道的两种功能语言)都缺乏读取元组的第n个元素的语法。这将使得不可能编写适用于任意尺寸电路板的动态解决方案。

Elixir具有内置的Map数据结构(而Haskell具有Data.Map),该结构允许O(log n)(对数)访问元素。现在,我使用一个地图,其中x, y以位置表示键的元组。

这是“可行的”,但是以这种方式滥用地图是错误的,尽管我不知道确切的原因。我正在寻找一种以功能性编程语言存储二维游戏板的更好方法。


1
我无法谈论实践,但是Haskell让我想到了两件事:拉链,允许对数据结构进行固定的“步进”,以及comonads,它们是基于某种我不记得也不正确理解的理论与拉链相关的; )
phipsgabler

这个游戏板有多大?Big O表示算法的扩展方式,而不是算法的速度。在小板上(每个方向上小于100),如果只触摸每个正方形一次,则O(1)与O(n)的关系不太大。
罗伯特·哈维

@RobertHarvey情况会有所不同。举个例子:在国际象棋中,我们有一块64x64的电路板,但是所有的计算都可以检查可能的移动,并确定当前位置的启发式值(材料上的差异,检查中是否为国王,通过的棋子等)。所有需要访问板的正方形。
qqwy

1
您在国际象棋中有一个8x8的棋盘。在像C这样的内存映射语言中,您可以进行数学计算以获取单元的确切地址,但是在内存管理的语言(序数寻址是实现细节)中却不是这样。如果跨越(最多)14个节点进行跳转所花的时间与使用内存管理语言寻址数组元素所花的时间大致相同,这不会令我感到惊讶。
罗伯特·哈维

Answers:


6

Map在这里,A 恰好是正确的基础数据结构。我不确定为什么会令您不安。它具有良好的查找和更新时间,它的大小是动态的,并且很容易从中创建派生数据结构。例如(在haskell中):

filterWithKey (\k _ -> (snd k) == column) -- All pieces in given column
filterWithKey (\k _ -> (fst k) == row)    -- All pieces in given row
mapKeys (\(x, y) -> (-x, y))              -- Mirror

程序员刚开始以完全不变性进行编程时通常很难抓住的另一件事是,您不必只坚持一个数据结构。通常,您选择一种数据结构作为“真理的来源”,但是您可以根据需要生成任意数量的派生,甚至派生派生,并且您知道只要您需要它们,它们就可以保持同步。

这意味着您可以Map在最顶层使用a ,然后切换到ListsArrays进行行分析,然后Arrays以另一种方式索引以进行列分析,然后使用位掩码进行模式分析,然后Strings进行显示。设计良好的功能程序不会传递单个数据结构。它们是一系列步骤,它们采用一个数据结构并发出适合下一步的新结构。

只要您能以最高层可以理解的格式走到另一侧,就不必担心中间数据的重组量。它是不可变的,因此可以在最高层追溯到真相来源的路径。


4

我最近在F#中完成了此操作,最终使用了一维列表(在F#中是单链接列表)。在实践中,O(n)列表索引器的速度并不是人类可用板尺寸的瓶颈。我尝试了2d数组之类的其他类型,但最后还是要权衡选择,要么编写我自己的值相等检查代码,要么将等级和文件转换为索引和索引。后者更简单。我会说先让它工作,然后再根据需要优化您的数据类型。不可能产生足够大的影响。

最后,只要您的电路板操作已按电路板类型和操作正确封装,您的实现就无关紧要。例如,这是我的一些测试对于建立电路板的外观:

let pos r f = {Rank = r; File = f} // immutable record type
// or
let pos r f = OnBoard (r, f) // algebraic type
...
let testBoard =
    Board.createEmpty ()
    |> Board.setPiece p (pos 1 2)
    |> ...

对于此代码(或任何调用代码),董事会的表示方式无关紧要。董事会通过其上的操作所代表的不仅仅是其基础结构。


您好,您的代码发布在某个地方吗?我目前正在F#(一个有趣的项目)中从事象棋游戏,甚至我使用Map <Square,Piece>来代表棋盘,我很想看看您是如何将其封装到棋盘中的类型和模块。
asibahi

不,它没有在任何地方发布。
Kasey Speakman

因此,您介意看一下我目前的实施情况并建议我如何进行改进吗?
asibahi

看了这些类型,我决定采用非常相似的实现,直到获得Position类型。稍后,我将对“代码审查”做更彻底的分析。
Kasey Speakman
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.