《era 象棋》开发日志
国际象棋#
具体玩法自行搜索,此处不再赘述。
参考#
前置知识#
记谱法
首先我们要知道「
棋子位置数值区域 :轮走棋方 :以w
代表白方 ,以b
代表黑方 。易位可行性 :当前是否能够易位,KQ
表示白方可易位,kq
表示黑方可易位。吃过路兵目标格 :棋格标记,走棋方吃过路兵后到达的目的地,若无则写-
。半回合计数 :数字,距离上一次吃子或移动兵后的回合数。
(用于判定 50 回合「自然限著」和局。)回合数 :数字,从开局开始计算的回合数。
这套记谱法也可以用于中国象棋,更新一下棋子类型,禁用王车易位就行。
但我暂时没有做中国象棋的意向,此处且按下不表。
举例
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 简单概括就是:
与我一贯偏好的「十六进制记座标」不谋而合。
A | B | C | D | E | F | G | H | ||
---|---|---|---|---|---|---|---|---|---|
8 | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | |
7 | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 | |
6 | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 | |
5 | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | |
4 | 0x40 | 0x41 | 0x42 | 0x43 | 0x44 | 0x45 | 0x46 | 0x47 | |
3 | 0x50 | 0x51 | 0x52 | 0x53 | 0x54 | 0x55 | 0x56 | 0x57 | |
2 | 0x60 | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | |
1 | 0x70 | 0x71 | 0x72 | 0x73 | 0x74 | 0x75 | 0x76 | 0x77 | |
A | B | C | D | E | F | G | H |
这种设计的好处是什么呢?
- 求横行(
rank
)座标:即保留高 4 位:右移 4 位(sq_0x88_idx >> 4
)。 - 求纵列(
file
)座标:即保留低 4 位(由于最大为 7,实际上保留 3 位即可)——
和7 做与运算(sq_0x88_idx & 7
)。 - 已知横行 / 纵列座标,求 0x88 座标系下标:
rank_idx << 4 | file_idx
。
以防有人真的不知道:为什么一定是 4 位?因为
16 进制 = 24。
简化计算
甚至可以进一步把 8 × 8 的二维数组压缩成长度 64 的一维数组(还就那个降维打击):
- 已知 0x88 下标,求一维数组下标:
(sq_0x88_idx + (sq_0x88_idx & 7)) >> 1
- 已知一维数组下标,求 0x88 下标:
sq_64_idx + (sq_64_idx & ~7)
如此一来,无论你在什么位置,与另一个棋子的相对位置都可以通过简单地加减一个 唯一的差值 获得。比如以 D4 为基准点,原 0x88 座标系就变成了:
A | B | C | D | E | F | G | H | |
---|---|---|---|---|---|---|---|---|
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 | -1 | 0 | 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 |
当然,如果是求「相对位置关系」,则要反过来。
A | B | C | D | E | F | G | H | |
---|---|---|---|---|---|---|---|---|
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 | 1 | 0 | -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 |
棋子的移动规则
- 不能超出棋盘(off the board)。
- 不能吃自己的棋子。
- 符合每个棋子特定的移动规则。
在 0x88 算法的基础上,解决这三个问题也变得异常简单。
- 可以通过一个特别的、快速的、开销极低的方式判断:
& 0x88
。
(原理:所有在棋盘内的 0x88 座标& 0b_1000_1000
结果都应该为0
。) - 直接判断目标位置有没有棋子,如果有,是什么花色就行了。
- 最令人头疼的棋子移动规则也可以用数个一维数组来描述。
- 士兵:
- 黑方:第一步可以
+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 里 那样 不惜通过大量空间换时间追求极限效率的话,甚至可以全部穷举出来列成表:
|
|
人机 AI
TODO
中国象棋#
国际象棋中的主教(bishop)/ 象 实际上指的真的是「战象」。
众所周知,历史上的印度(战象的取材对象)是真有这个兵种的,所以国际象棋中的象比中国象棋里的象吊无数倍:斜行 不限定步数、不会被堵象眼、没有河界限制。
(其实中国象棋中的「象」定位更接近「相」。以红方为准 红方先行 ,比如 兵 / 卒、炮 / 砲红方都上火器了黑方还在玩投石机)。
传到欧洲,原本造型的棋子上表示大象嘴巴的开口像一个主教帽,于是就被英国佬叫成了主教,而这个棋子在东欧国家至今仍被称为战象。 ↩︎国际象棋中的城堡 / 车(rook)实际上指的是波斯语中的「Ruhk」,意为「战车」。
古波斯战车的车身往往被装饰成带城垛的堡垒形状,做成棋子自然在造型上省略了底部的轮子,只剩上面的城垛,结果传到欧洲被以讹传讹当成了城堡(castle)。
虽然最后还是正名为 Rook,但「王车易位(Castling)」这个叫法仍然流传至今,很难说欧洲人有没有「国王逃到城堡里紧急避难」的脑补在里面。 ↩︎