如何在关系数据库(特别是 Postgres)中存储国际象棋位置

Sam*_*uel 5 postgresql database-design

我正在探索建立一个涉及国际象棋位置分析的网站的想法。国际象棋位置本身使用称为 FEN 的格式进行描述,例如,棋盘的起始位置可以描述为:

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Run Code Online (Sandbox Code Playgroud)

我对如何建模的想法特别感兴趣的部分是直到第一个空间的所有内容。这描述了棋盘从第一级到第八级的每一级的布局。棋子使用字母和大写/小写的组合来描述,以表示棋子类型以及它是否属于黑人玩家(小写)或白人玩家(大写)。整数8表示空格的数量。像 1.e4 这样的移动之后

rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
Run Code Online (Sandbox Code Playgroud)

现在您可以看到第 5 阶部分如何描述 4 个空格,一个白色棋子,然后是 3 个空格。

那么我们如何在像 Postgres 这样的关系数据库中对此进行建模呢?我幼稚的方法是首先将整个 FEN 字符串存储为 a varchar,但是,将来,我想将其迁移到一个结构,使我能够轻松搜索国际象棋位置中的结构。一个例子是,找到数据库中白棋占据 e4 和 e5 位置的所有位置(第 5 个位置的等级段:)3PP3

FEN 字符串的正则表达式搜索可以找到我感兴趣的位置,但是简单地索引 FEN 列就足够了吗?我是否应该考虑将董事会的每个等级存储为单独的列?或者整个棋盘可以以某种方式表示为矩阵。

Mic*_*een 6

如果您想将位置保留为文本,那没问题。但还有比 Postgres 更好的文本编辑器。将每个游戏作为一个文件,并将每个位置作为该文件中的一行(采用 FEN 表示法)将是一个好方法。可以使用您选择的工具(grep;Python)读取和搜索这些文件。按照 Daniel V\xc3\xa9rit\xc3\xa9 的建议扩大空间会让生活变得更轻松。

\n\n

如果您确实想使用 DBMS,Elasticsearch 在查找文本数据中的模式方面享有盛誉。

\n\n

然而,如果您确实想使用关系数据库,我建议您使用关系(即表)来设计数据结构,而不是依赖 SQL 较差的文本解析功能。

\n\n

人们必须在心理上从国际象棋游戏规则转向关系模式设计规则。在后者中,我们定义实体类型以及有关它们的已知事实。实体类型可以是实际的现实世界项目、概念或事件。对于任何给定的问题都有许多可能的本体。具体来说,我们必须将信息的数据库内部表示以及该信息如何格式化以供人类使用分开。我并不认为我的解决方案是绝对意义上的“正确”,只是它来自数据建模的关系角度。其他同样相关的方法也是可能的。

\n\n

需要什么表?最明显的是游戏:

\n\n
Game\n  GameId        int  primary key\n  WhitePlayer   varchar(50)\n  BlackPlayer   varchar(50)\n  .. others\n
Run Code Online (Sandbox Code Playgroud)\n\n

我会将每个部分建模为不同的行。Pawn 1 与 pawn 2 不同,依此类推。我仍然需要记录颜色和类型来重现位置。当棋子被提升时,新棋子必须具有与游戏开始时出现的棋子不同的 PieceId。这是我在下面重新创建职位的方式所必需的。

\n\n
Piece\n  PieceId    smallint  primary key\n  Type       char(1)              -- (P)awn, (R)ook, (K)ing etc.\n  Colour     char(1)              -- (W)hite, (B)lack;\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后会有一个表来存储动作。这里的选择是存储一步棋(如 PGN)或存储整个位置(如 FEN)。存储位置会引入很多冗余数据 - 所有空白方块和本回合中未移动的棋子。所以我会单独存储动作。

\n\n
Move\n  MoveId       int\n  GameId       int foreign key referencing Game.GameId\n  PieceId      int foreign key referencing Piece.PieceId\n  ToRow        char(1)\n  ToColumn     smallint\n  IsRemoved    boolean           -- set when a piece is taken from the board\n\n  primary key (MoveId, PieceId)\n
Run Code Online (Sandbox Code Playgroud)\n\n

该对 (ToRow, ToColumn) 是棋子在移动后结束的位置。我正在考虑 a1-h8 格式。我不需要 FromRow/FromColumn。如果我们需要它,可以从 PieceId 和移动历史记录中重新创建它。我也不会检查国际象棋规则是否得到遵守。这是应用程序不同层的工作。

\n\n

某些国际象棋走法将导致此表中出现两行。例如,在捕捉过程中,将有一排用于拍摄片段,另一行用于拍摄片段。后者将设置 IsRemoved。

\n\n

每个游戏开始时,棋子的初始位置都在 Move 中具有相同的 32 行。这看起来似乎是多余的,但却是重现任何位置所必需的。如果不明确,则必须在每个查询中隐含它,因为某些棋子在游戏过程中可能不会移动。这也允许拼图设置。

\n\n

为了重现一个位置,我们观察到每个棋子将被放置在该位置之前最近移动的位置。以下为伪代码;我没有测试过。

\n\n
select p.GameId, p.MoveId, pc.Colour, pc.Type, q.ToRow, Q.ToColumn\nfrom Move as p\ninner join Move as q\n   on q.GameId = p.GameId\n   and q.MoveId <= p.MoveId\ninner join Piece as pc\n  on pc.PieceId = q.PieceId\nwhere not exists\n( select 1\n  from Move as c\n  where c.GameId = q.GameId\n  and c.PieceId = q.PieceId\n  and c.MoveId <= p.MoveId\n  and c.MoveId >= q.MoveId\n)\nand not exists\n( select 1\n  from Move as d\n  where d.GameId = q.GameId\n  and d.PieceId = q.PieceId\n  and d.MoveId <= p.MoveId\n  and d.IsRemoved = True\n)\n
Run Code Online (Sandbox Code Playgroud)\n\n

将其解读为每场比赛的每一步(别名为 p),即每个位置,找到同一局游戏中在此步之前发生的所有动作(别名为 q)。忽略当前游戏中同一棋子有后续动作或已被捕获的那些动作。

\n\n

一旦可以像上面那样重新创建位置,就可以简单地扩展以查找特定棋子(或类型的棋子)在给定配置中的位置。可以构造查询来查找特定位置的特定片段,然后确保它们同时就位,然后检索该点的剩余片段。我要指出的是,在很多游戏中,白棋的棋子可能会占据 e4 和 e5,因此很多位置都满足这个标准。

\n\n

这是否比简单但愚蠢的文本方法更快只有测试才能知道。

\n\n
\n\n

这个问题引起了我的兴趣。我想知道“关系”答案会是什么样子,因为指定了 RDBMS。我将其视为格丹肯实验。然而,在完成了一些查询之后,我不确定我的本体论是否正确。如果目标是识别位置,我认为位置就是应该存储的对象。这又回到了OP最初的建议。是的,存储中存在冗余,因为大多数部件在位置之间不会发生变化。但实际上,如果每个位置 64 字节,即每场比赛 50 步,那么包含 10,000 场比赛的数据库大约需要 30MB。这是微不足道的。尽管 SQL 不擅长文本解析,但它肯定要决定字符串中给定的偏移量是否包含特定值,例如 e4 和 e5 持有白色棋子。总的来说,我认为最实际的实现是带有显式空间的 FEN。

\n