24 个版本
0.0.24 | 2023 年 12 月 12 日 |
---|---|
0.0.23 | 2023 年 12 月 11 日 |
0.0.18 | 2023 年 3 月 29 日 |
#25 in 游戏
用于 monster_ugi
190KB
4K SLoC
monster-chess
概述
monster-chess
是一个用 Rust 编写的奇幻棋移动生成库。它是作为 Chesstastic 项目(旨在允许用户轻松创建棋类变体、与其他人在这些变体中玩游戏以及分析这些变体的游戏)的一部分创建的,或者更具体地说,是 Ampersand 棋引擎。该库旨在处理棋类变体和棋类相关游戏中发生的移动生成和移动验证逻辑。
快速入门
monster-chess
是一个可以安装为 cargo crate 的库。您可以按照以下方式安装它
cargo add monster_chess
然后,您可以按照以下方式导入库
use monster_chess::{games::{chess::Chess, ataxx::Ataxx}, board::game::NORMAL_MODE};
您可以创建您选择的任何游戏,如下所示
let game = Chess::create();
// OR
let game = Ataxx::create();
// OR
let game = MyCustomGame::create();
您可以加载以下默认 FEN 或您自己选择的 FEN 位置。
let board = game.default();
// OR
let board = game.from_fen("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 0 1");
您可以按照以下方式生成合法的移动(对于某些游戏的伪合法移动生成,您可能需要以不同的方式生成。)
// `NORMAL_MODE` is the normal move generation of any game, other move generations generate specialized moves.
let moves = board.generate_legal_moves(NORMAL_MODE);
您可以按照以下方式创建一个位置的可 zobrist 散列值
let hash = game.zobrist.compute(board);
兼容性
当我们说我们与某个游戏兼容时,这并不意味着我们一定会为它提供即插即用的支持,但该游戏可以在 monster-chess
提供的框架下实现。
我们旨在与之兼容的游戏类型
我们可能旨在与之兼容的游戏类型
我们不打算与之兼容的游戏类型
但是,以下游戏将 直接 支持
如果您想知道某个游戏或棋类变体是否与棋类兼容,请想象从基础棋类游戏开始,看看您是否可以执行以下操作来达到您的变体。
- 我可以增加/减少棋盘大小吗?
- 我可以移除现有棋子或添加具有新移动类型的棋子吗?
- 我可以更改棋子的捕获方式(例如,用棋子交换位置而不是捕获它。)
- 能否更改移动限制?(例如,允许进入将军状态)
- 能否更改胜利条件?(例如,通过捕获所有棋子获胜。)
如果是这样,monster-chess
很可能可以与这种变体兼容,但你可能需要添加它。
开箱即用支持
国际象棋
国际象棋 是一种双人对弈游戏,双方在八乘八的棋盘上以相同的棋子开始,必须进行有成效的战斗,直到一方获胜。为了简洁起见,我们假设您已经熟悉国际象棋的规则(如果不熟悉,可以在此阅读),我们只关注与 monster-chess
相关的部分。
在国际象棋中,游戏的最终目标是 将军,即对方的国王处于将军状态(在另一棋子的视线中),且没有可以逃脱将军状态的走法(移动到安全位置、用另一棋子阻挡等)。上图展示了这一点,两个车阻挡了国王所有可能的移动位置,对方的国王无法从车的视线中逃脱。如果对方处于将军状态但可以逃脱,他们必须这样做,你不能让自己处于将军状态。
monster-chess
处理这个问题的方式是使用 伪合法 的走法生成。首先,我们生成任何棋子能够做出的所有走法(即伪合法走法),然后检查在你执行走法后,你的国王是否处于将军状态。为此,我们必须生成对手在你走法后可能做出的 所有可能的走法,并查看其中是否有任何一种走法可以导致国王被捕获。这很昂贵,但有两种方法可以使这个过程更快。
- 对于走法和捕获方式不同的棋子,我们只关注它们能够捕获的格子。
- 对于只能沿一个方向移动直到遇到另一棋子或棋盘边缘的棋子(如象、车、后),我们在检查是否真正处于视线中之前,先检查国王是否甚至处于该棋子的视线中。
尽管如此,这种合法性检查仍然相当昂贵。幸运的是,如果你正在构建国际象棋引擎,这不会很重要,因为在任何给定位置,你只会在走法中搜索一小部分。这意味着,如果你搜索了3步并找到了立即击中,你就不需要检查剩余的走法。
你可以按照以下方式初始化棋盘
use monster_chess::games::chess::Chess;
let chess = Chess::create();
let mut board = chess.from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
然后,我们可以按照以下方式生成走法
let legal_moves = board.generate_legal_moves(NORMAL_MODE);
let pseudolegal_moves = board.generate_moves(NORMAL_MODE);
为了测试和基准测试,monster-chess
提供了一个名为 perft
的方法,它将计算从当前位置出发,深度为 depth
的半步走法中所有可能走法的数量(半步走法是指两位玩家中的一方走的一步走法。)
let perft = board.perft(5, true);
let perft_pseudolegal = board.perft(5, false);
根据我所做的基准测试,monster-chess
可以达到大约每秒 20,000,000 个伪合法走法,每秒 5,000,000 个合法走法。这并不理想,如果你只对性能感兴趣,我推荐使用cozy-chess 包,它的速度至少比 monster-chess
中的国际象棋实现快 25 倍。然而,如果你还想支持国际象棋变体或其他游戏,monster-chess
仍然是一个不错的选择。
需要注意的是,monster-chess
也旨在支持费舍尔随机棋。截至目前,费舍尔随机棋在 monster-chess
的棋类实现中理论上得到支持,但尚未经过测试,且该变体的 FEN 格式尚未得到支持。不过,在 monster-chess
的框架中,作为现有棋类实现的扩展,添加它是微不足道的。
Ataxx
Ataxx 是一种两人游戏,两位玩家在一块七乘七的棋盘上各从一块石子开始,必须争夺谁能控制最多的领土。该游戏以单步棋的变化幅度大而闻名;位置通常不是战术上稳定的。
monster-chess
可以通过编写代表 Ataxx 石子的 StonePiece
实现轻松实现 Ataxx。可以使用查找表找到走法,然后进行屏蔽以避免棋盘上的所有石子。至于移动,我们只需检查是单步还是双步移动,然后在移动石子之前适当处理移动逻辑。
你可以按照以下方式创建 Ataxx 棋盘
use monster_chess::games::chess::Ataxx;
let ataxx = Ataxx::create();
let mut board = ataxx.from_fen("x5o/7/7/7/7/7/o5x x 0 1");
然后,我们可以按照以下方式生成走法
let legal_moves = board.generate_legal_moves(NORMAL_MODE);
let pseudolegal_moves = board.generate_moves(NORMAL_MODE);
由于 Ataxx 的 FEN 符号可能难以找到,我们遵循以下格式:[棋子放置] [移动队伍] [半回合数] [全回合数]
。
对于棋子放置,x
是第一队(黑色)的棋子,而 y
是第二队(白色)的棋子。此外,-
表示间隔,棋盘上的空位。对于移动队伍,x
是黑色,y
是白色。
对于引擎解析的移动,单步移动必须仅通过提供目的地方格来表示,而双步移动表示为 [from][to]
。
实现
位图
monster-chess
使用通用的 位图 实现来扩展到更大的棋盘尺寸,使用定制的 BitBoard
数据类型。
pub struct BitBoard<const T : usize> {
pub data: [ u128; T ]
}
位图设计得这样,如果位图只需要存储一个 u128
,则(理想情况下)将优化为尽可能快。位图支持所有 位运算 操作,以及加法、减法和两个用于 位扫描 的自定义方法。
该库通过接受一个位图,其中有一个被切换的位表示棋子的当前位置,并对其应用位运算来生成棋子的移动。在原始棋中,有三种主要的棋子类型。
- Delta 棋子,如马和将,每次移动都是相同的,无论它们在哪个方格(除非越界方格)。
- 滑行棋子,如象、车和后,每次移动几乎相同,但可以被棋子阻挡。
- 兵.
为了优化Delta棋子和滑动棋子的走法生成,monster-chess
提供了一个攻击查找表。一旦棋盘初始化完毕,如果棋子选择启用攻击表查找以加快走法生成,其走法将会为它可能到达的每个格子生成。这意味着对于国王和骑士,走法生成实际上是瞬时的,只需进行一次攻击表查找。对于车、象和后,攻击表用于检索它们可以从任何给定格子移动的方向,但需要额外的逻辑来排除挡路的棋子。
一旦检索到,攻击表查找将被存储为AttackDirections
,它是Vec<BitBoard>
的一个别名。这意味着对于Delta棋子,它们只需在第一个槽中存储它们的走法,但对于滑动棋子,它们可以存储每个方向的位图。
以下是一个使用monster-chess
棋子系统的国王棋子的示例。
impl Piece for KingPiece {
// get_piece_symbol will show the symbol used for a given piece in FEN notation
fn get_piece_symbol(&self) -> &str {
"k"
}
// generate_lookup_moves will generate moves for attack tables
fn generate_lookup_moves(&self, board: &Board, mut from: BitBoard) -> AttackDirections {
let moves = ...;
vec![ moves ]
}
// can_lookup tells us if we should generate an attack table beforehand
fn can_lookup(&self) -> bool {
true
}
// get_moves will fetch the moves for move generation itself
fn get_moves(&self, board: &Board, from: BitBoard, team: u32) -> BitBoard {
let lookup = self.get_attack_lookup(board);
match lookup {
Some(lookup) => lookup[from.bitscan_reverse() as usize][0],
None => self.generate_moves(board, from)[0]
}
}
}
FEN表示
棋盘状态
由于monster-chess
旨在支持所有棋类变体,因此使用了FEN的一般修改(对于棋类的基础游戏本身有一个特定的FEN版本。)这个棋盘状态的FEN版本与典型的棋类FEN相同,但有以下附加内容
- FEN变体可以支持多于或少于8行和8列。
- FEN变体可以通过在棋子的
get_piece_symbol
方法中定义自定义棋子符号(例如,A
代表大主教)来支持自定义棋子。 - 可以为单个棋子指定附加信息。
- 根据单个棋子,有两种指定棋子类型和团队的方法。
PieceSymbol::Char
将棋子定义为单个字符(例如,p
。)如果有两个团队,P
将代表第一个团队(团队0
),而p
将代表第二个团队(团队1
。)如果超过两个团队,团队将以括号在棋子之后表示。(例如,p{2}
代表第三个团队。)PieceSymbol::Teams
根据团队更改用于棋子的字符。(例如,x
代表玩家一,o
代表玩家二。)
- 如果游戏支持首步标记,那么如果
!
标记跟在棋子后面(例如,p!
),那么该棋子已经至少移动过一次。这是一种处理像首步兵移动和长将权之类的通用方式。
- 根据单个棋子,有两种指定棋子类型和团队的方法。
例如,p!{3}
是一个在第四个团队上移动过一次的兵。(我们使用0作为第一个索引,就像编程中的数组一样。)
FEN状态的选项只有一个,即first_move
。在大多数游戏中,首步没有影响游戏状态,或者FEN表示有其他更简洁的方式来表示首步(例如,在棋类中,根据兵是否在第二排或第六排来检测兵的首步移动。)
FEN参数
象棋等游戏的FEN记法还提供了不在棋盘状态表示本身中的额外信息。例如,吃过路兵的方格、王车易位权或移动方。代码monster-chess
并不原生支持这些作为Board
实现的一部分。相反,各个游戏必须通过实现FenArgument
特质来各自管理其FEN记法的额外参数。
FenArgument
提供了两个主要方法:encode
和decode
。
pub trait FenArgument {
/// `encode` takes in a board, and outputs what this FEN argument's encoded result would be (eg. for a team argument, it could be `"b"`)
fn encode(&self, board: &Board) -> String;
/// `decode` takes in a board and an existing argument, and will modify the board to meet the argument (eg. changing the team to reflect the given arg team of `w`)
fn decode(&self, board: &mut Board, arg: &str) -> Result<(), FenDecodeError>;
}
monster-chess
为你提供了一个参数的实现,即FenTeamArgument
,其定义如下
pub enum FenTeamArgument {
Number,
Teams(Vec<char>),
}
如果你的游戏需要一个参数来表示哪一方需要移动(这几乎是肯定的),使用FenTeamArgument
是必要的,除非你决定定义你自己的表示哪一方需要移动的参数。
monster-chess
还提供了Turns
、SubMoves
(半回合)和FullMoves
的现成实现。
游戏
monster-chess
最终提供了一个名为Game
的结构体,用于描述你的类似棋类游戏的规则。其声明如下
pub struct Game<const T: usize> {
pub pieces: Vec<&'static dyn Piece<T>>,
pub controller: Box<dyn MoveController<T>>,
pub resolution: Box<dyn Resolution<T>>,
pub fen_options: FenOptions<T>
}
注意:对于围棋等可以在方格上放置棋子的游戏,你需要手动处理,通过在MoveController
上实现一个add_moves
方法来生成放置棋子的移动(即from
作为None
的移动),并在MoveController
上实现一个make_drop_move
方法来进行这些移动。
许可证
依赖
~525KB
~11K SLoC