《era 象棋》开发日志

era 象棋0.1.0-alpha lackbfun

象棋怎么说,象棋
eraChess.zip(0.80 MB) 最后更新于 2023-01-08 16:22:53
最新改动 build: 更新 Emuera1824+v16+EMv17+EEv28

国际象棋#

具体玩法自行搜索,此处不再赘述。

参考#

前置知识#

记谱法

首先我们要知道「福斯夫-爱德华兹记号法Forsyth–Edwards Notation(后简写为 FEN)」:

  • 棋子位置数值区域Piece placement data
    • 白方(先行)视角,依次描述从上到下、从左到右的盘面,以 / 分隔横行。
    • 白方 / 黑方 分别以字母 P/pN/nB/bR/rQ/qK/k 表示:
      士兵Pawn骑士kNight主教Bishop1城堡Rook2皇后Queen国王King
    • 各横行的连续空位以数字表示,比如 4 代表「连续 4 个空格」。
  • 轮走棋方Active color:以 w 代表白方White,以 b 代表黑方Black
  • 易位可行性Castling availability:当前是否能够易位,KQ 表示白方可易位,kq 表示黑方可易位。
  • 吃过路兵目标格En passant target square:棋格标记,走棋方吃过路兵后到达的目的地,若无则写 -
  • 半回合计数Halfmove clock:数字,距离上一次吃子或移动兵后的回合数。
    (用于判定 50 回合「自然限著」和局。)
  • 回合数Fullmove number:数字,从开局开始计算的回合数。

这套记谱法也可以用于中国象棋,更新一下棋子类型,禁用王车易位就行。
但我暂时没有做中国象棋的意向,此处且按下不表。

举例

rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR b KQkq d6 0 2

实现#

棋盘的可视化

数据结构

很容易能够想到,我们可以用一个 8 × 8 的二维字符串数组表示棋盘:

#DIMS ChessBoard, 8, 8

然后就简单了:

;;;初始化棋盘
;;;@param (int)clear 是否清空棋盘
;;;@return (int) 没有返回值
@ChessInitBoard(clear=0)
#DIM DYNAMIC clear
    #DIM DYNAMIC i
    VARSET ChessBoard, "."
    SIF clearRETURN
    SPLIT "r n b q k b n r", " ", ChessBoard
    FOR i, 0, 8ChessBoard:1:i '= "p"
    NEXT
    FOR i, 0, 8ChessBoard:6:i '= "P"
    NEXT
    SPLIT "R N B Q K B N R", " ", RESULTS
    FOR i, 0, 8ChessBoard:7:i '= RESULTS:i
    NEXT
    RETURN
ASCII 码
;;;打印当前棋盘(ASCII 码方式)
;;;@return (int) 没有返回值
@ChessPrintBoard()
#DIM DYNAMIC param
    #DIM DYNAMIC i
    #DIM DYNAMIC j
    PRINTL    +------------------------+
    FOR i, 0, 8PRINTS @" {8 - i} | "FOR j, 0, 8    PRINTS @"%ChessBoard:i:j% "    SIF j < 7        PRINTS " "NEXTPRINTL |
    NEXT
    PRINTL    +------------------------+
    PRINTL     a  b  c  d  e  f  g  h
    RETURN
   +------------------------+
 8 | r  n  b  q  k  b  n  r |
 7 | p  p  p  p  .  p  p  p |
 6 | .  .  .  .  .  .  .  . |
 5 | .  .  .  .  p  .  .  . |
 4 | .  .  .  .  P  P  .  . |
 3 | .  .  .  .  .  .  .  . |
 2 | P  P  P  P  .  .  P  P |
 1 | R  N  B  Q  K  B  N  R |
   +------------------------+
     a  b  c  d  e  f  g  h
图像
;;;打印当前棋盘(图像方式)
;;;@return (int) 没有返回值
@ChessDrawBoard()
    #DIM CONST GRID_SIZE = 80
    #DIM CONST GID_CHESS_BOARD = 0x123
    #DIM DYNAMIC i
    #DIM DYNAMIC j
    #DIM DYNAMIC _is_white
    GDISPOSE GID_CHESS_BOARD
    GCREATE GID_CHESS_BOARD, GRID_SIZE * 8, GRID_SIZE * 8
    FOR i, 0, 8FOR j, 0, 8    GSETBRUSH GID_CHESS_BOARD, (i + j) % 2 ? 0xfff0d9b5 # 0xffb58863    GFILLRECTANGLE GID_CHESS_BOARD, j * GRID_SIZE, i * GRID_SIZE, GRID_SIZE, GRID_SIZE    IF ChessBoard:i:j != "."        _is_white = ENCODETOUNI(ChessBoard:i:j) < ENCODETOUNI("b")        GDRAWSPRITE GID_CHESS_BOARD, @"chess_\@ _is_white ? w # b \@%ChessBoard:i:j%", j * GRID_SIZE, i * GRID_SIZE    ENDIFNEXT
    NEXT
    SPRITECREATE "chess_board", GID_CHESS_BOARD
    HTML_PRINT @"<p align='right'><img src='chess_board' height='{CLIENTHEIGHT() / 2}px'><shape type='space' param='500'></p>"
    RETURN

进一步优化:0x88 移动生成算法

描述方法

所谓 0x88 Move Generation Algorithm 简单概括就是:

与我一贯偏好的「十六进制记座标」不谋而合。

ABCDEFGH
80x000x010x020x030x040x050x060x070x08
70x100x110x120x130x140x150x160x170x18
60x200x210x220x230x240x250x260x270x28
50x300x310x320x330x340x350x360x370x38
40x400x410x420x430x440x450x460x470x48
30x500x510x520x530x540x550x560x570x58
20x600x610x620x630x640x650x660x670x68
10x700x710x720x730x740x750x760x770x78
ABCDEFGH

这种设计的好处是什么呢?

  • 求横行(rank)座标:即保留高 4 位:右移 4 位(sq_0x88_idx >> 4)。
  • 求纵列(file)座标:即保留低 4 位(由于最大为 7,实际上保留 3 位即可)——
    70b111与运算sq_0x88_idx & 7)。
  • 已知横行 / 纵列座标,求 0x88 座标系下标:rank_idx << 4 | file_idx

以防有人真的不知道:为什么一定是 4 位?因为 16 进制hexadecimal = 24

简化计算

甚至可以进一步把 8 × 8 的二维数组压缩成长度 64 的一维数组(还就那个降维打击):

  • 已知 0x88 下标,求一维数组下标:(sq_0x88_idx + (sq_0x88_idx & 7)) >> 1
  • 已知一维数组下标,求 0x88 下标:sq_64_idx + (sq_64_idx & ~7)

如此一来,无论你在什么位置,与另一个棋子的相对位置都可以通过简单地加减一个 唯一的差值 获得。比如以 D4 为基准点,原 0x88 座标系就变成了:

ABCDEFGH
8-67-66-65-64-63-62-61-60
7-51-50-49-48-47-46-45-44
6-35-34-33-32-31-30-29-28
5-19-18-17-16-15-14-13-12
4-3-2-101234
31314151617181920
22930313233343536
14546474849505152
当然,如果是求「相对位置关系」,则要反过来。

ABCDEFGH
86766656463626160
75150494847464544
63534333231302928
51918171615141312
43210-1-2-3-4
3-13-14-15-16-17-18-19-20
2-29-30-31-32-33-34-35-36
1-45-46-47-48-49-50-51-52

棋子的移动规则
  1. 不能超出棋盘(off the board)。
  2. 不能吃自己的棋子。
  3. 符合每个棋子特定的移动规则。

在 0x88 算法的基础上,解决这三个问题也变得异常简单。

  1. 可以通过一个特别的、快速的、开销极低的方式判断:& 0x88
    (原理:所有在棋盘内的 0x88 座标 & 0b_1000_1000 结果都应该为 0。)
  2. 直接判断目标位置有没有棋子,如果有,是什么花色就行了。
  3. 最令人头疼的棋子移动规则也可以用数个一维数组来描述。
    • 士兵
      • 黑方:第一步可以 +32,平常只能 +16,吃过路兵可以 +15 / +17
      • 白方:第一步可以 -32,平常只能 -16,吃过路兵可以 -15 / -17
    • 国王:除开自己的九宫格 -17 / -16 / -15 / -1 / +1 / +15 / +16 / +17
    • 骑士:12 点钟方向顺时针 -31 / -14 / 18 / 33 / 31 / 14 / -18 / -33
      (这个斜走日字格的棋子在二维座标系里如何描述是最麻烦的。)
    • 城堡:4 条射线 -16 / +1 / +16 / -1,最多 7 次(不能超出棋盘)就是了。
    • 主教:4 条射线 -15 / +17 / +15 / -17,最多 7 次。
    • 皇后:城堡和主教加起来的 8 条射线。

车 / 象 / 后这三种棋子的路径叫做「射线」是因为会被第一个遇到的东西挡下来。
无论是墙(棋盘边界)还是障碍物(其他棋子)。

如果你愿意像 chess.js 里 那样 不惜通过大量空间换时间追求极限效率的话,甚至可以全部穷举出来列成表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const RAYS = [
   17,  0,  0,  0,  0,  0,  0, 16,  0,  0,  0,  0,  0,  0, 15, 0,
    0, 17,  0,  0,  0,  0,  0, 16,  0,  0,  0,  0,  0, 15,  0, 0,
    0,  0, 17,  0,  0,  0,  0, 16,  0,  0,  0,  0, 15,  0,  0, 0,
    0,  0,  0, 17,  0,  0,  0, 16,  0,  0,  0, 15,  0,  0,  0, 0,
    0,  0,  0,  0, 17,  0,  0, 16,  0,  0, 15,  0,  0,  0,  0, 0,
    0,  0,  0,  0,  0, 17,  0, 16,  0, 15,  0,  0,  0,  0,  0, 0,
    0,  0,  0,  0,  0,  0, 17, 16, 15,  0,  0,  0,  0,  0,  0, 0,
    1,  1,  1,  1,  1,  1,  1,  0, -1, -1,  -1,-1, -1, -1, -1, 0,
    0,  0,  0,  0,  0,  0,-15,-16,-17,  0,  0,  0,  0,  0,  0, 0,
    0,  0,  0,  0,  0,-15,  0,-16,  0,-17,  0,  0,  0,  0,  0, 0,
    0,  0,  0,  0,-15,  0,  0,-16,  0,  0,-17,  0,  0,  0,  0, 0,
    0,  0,  0,-15,  0,  0,  0,-16,  0,  0,  0,-17,  0,  0,  0, 0,
    0,  0,-15,  0,  0,  0,  0,-16,  0,  0,  0,  0,-17,  0,  0, 0,
    0,-15,  0,  0,  0,  0,  0,-16,  0,  0,  0,  0,  0,-17,  0, 0,
  -15,  0,  0,  0,  0,  0,  0,-16,  0,  0,  0,  0,  0,  0,-17
];

人机 AI

TODO

中国象棋#


  1. 国际象棋中的主教(bishop)/ 实际上指的真的是「战象」。
    众所周知,历史上的印度(战象的取材对象)是真有这个兵种的,所以国际象棋中的象比中国象棋里的象吊无数倍:斜行冲锋不限定步数、不会被堵象眼、没有河界限制。
    (其实中国象棋中的「象」定位更接近「相」。以红方为准 红方先行 ,比如 / / 红方都上火器了黑方还在玩投石机)。
    传到欧洲,原本造型的棋子上表示大象嘴巴的开口像一个主教帽,于是就被英国佬叫成了主教,而这个棋子在东欧国家至今仍被称为战象。 ↩︎

  2. 国际象棋中的城堡 / (rook)实际上指的是波斯语中的「Ruhk」,意为「战车」。
    古波斯战车的车身往往被装饰成带城垛的堡垒形状,做成棋子自然在造型上省略了底部的轮子,只剩上面的城垛,结果传到欧洲被以讹传讹当成了城堡(castle)。
    虽然最后还是正名为 Rook,但「王车易位(Castling)」这个叫法仍然流传至今,很难说欧洲人有没有「国王逃到城堡里紧急避难」的脑补在里面。 ↩︎

lackbfun © 2021 - 2024