面向对象程序设计实践:命令行国际象棋
一个使用 c++ 编写的命令行国际象棋, 实现了显示棋盘,调整布局,检查移动是否合法,判断胜负等功能, 可以调用 stockfish 查询下一步的策略。
用于在没有gui的环境下下国际象棋。
chess 启动游戏
help ? 显示帮助信息
quit exit 退出
move <notation> 比如 move e2e4 移动棋子
undo 取消上一步的移动
redo 重做
available 显示可行的移动
randmove 随机选择一步
show 显示棋盘
fenstring 显示 Forsyth-Edwards Notation
history 显示历史记录
hint 调用 stockfish 走一步
setboard [standard|chess960|random] 三种模式 setboard setboard standard 标准 setboard chess960 chess960 setboard random 随机
_ [n] 执行上一条成功的命令 n 次
显示效果:
current turn 118 r . . q . r . k7 . p p . . p p p6 p . n p . n . .5 . . b N p . B b4 . . B . P . . .3 P . . P . N . P2 . P P . . P P .1 R . Q . . R K .a b c d e f g h
>> move d1h5current turn 38 r n b q k b n r7 p p p p p . . p6 . . . . . p . .5 . . . . . . p Q4 . . . . P . . .3 . . N . . . . .2 P P P P . P P P1 R . B . K B N Ra b c d e f g hcheckmate, white wins
项目结构
├── CMakeLists.txt
├── CMakeUserPresets.json
├── README.md
├── conanfile.py
├── src
│   ├── ai.cpp
│   ├── game.cpp
│   ├── header.hpp
│   ├── help.cpp
│   ├── main.cpp
│   └── move_rules.cpp
└── tests
    ├── CMakeLists.txt
    └── test1.cpp
game.cpp 处理游戏的时间轴
help.cpp 帮助信息
move_rules.cpp 处理棋子移动规则
ai.cpp 调用 stockfish
main.cpp 处理命令行输入
test1.cpp 测试
uml 图
@startuml
class Coord {
  int col
  int row
  bool operator==(const Coord &other) const
  std::string to_string() const
}
enum PieceType {
  NONE
  PAWN
  KNIGHT
  BISHOP
  ROOK
  QUEEN
  KING
}
enum Player {
  NONE
  WHITE
  BLACK
}
class Piece {
  PieceType type
  Player owner
  bool moved
  Piece()
  Piece(PieceType t, Player o, bool m = true)
  bool operator==(const Piece &other) const
}
class Move {
  std::string s
  std::vector<Coord> remove
  std::vector<std::pair<Coord, Coord>> move
  std::vector<std::pair<Coord, PieceType>> promote
  bool operator==(const Move &other) const
}
class Board {
  std::array<std::array<Piece, ROWS>, COLS> board
  Player active
  Move last_move
  Board()
  Board(std::string const &fen1, ...)
  Piece &at(Coord c)
  Piece const &at(Coord c) const
  Board move(Move m) const
  std::array<std::array<bool, ROWS>, COLS> pos_under_attack() const
  bool opposite_king_under_attack() const
  bool is_checkmate() const
  bool is_stalemate() const
  std::vector<Move> get_all_moves() const
  std::string fen_without_clock() const
  void show(std::ostream &os) const
}
class Game {
  std::unordered_map<std::uint64_t, int> visited
  std::vector<Board> board_history
  std::vector<Move> moves
  std::vector<Move> next_moves
  std::vector<int> halfmove_clock
  int fullmove_number
  Game(Board &&start)
  Game(std::string const &fenstring)
  std::vector<Move> get_all_moves()
  void _play(Move move)
  void play(Move move)
  void play(std::string const &move_str)
  void undo()
  void redo()
  std::string fenstring() const
  void show_current_board(std::ostream &os) const
  void show_move_history(std::ostream &os) const
  void show_board_history(std::ostream &os) const
}
Piece o-- PieceType : composition
Piece o-- Player : composition
Move <.. Coord : dependency
Move <.. PieceType : dependency
Board --> Piece : uses
Board --> Player : uses
Board --> Move : uses
Game --> Board : uses
Game --> Move : uses
@endumlstruct Coord {
  int col, row;
  bool operator==(const Coord &other) const = default;
  std::string to_string() const {
    return std::string(1, 'a' + col) + std::to_string(row + 1);
  }
};
bool out_of_bounds(Coord c);坐标类,用于表示棋盘上的位置,to_string() 方法用于将坐标转换为字符串表示如 e2, out_of_bounds() 方法用于判断坐标是否越界。
enum class PieceType {
  NONE,
  PAWN,
  KNIGHT,
  BISHOP,
  ROOK,
  QUEEN,
  KING,
};
enum class Player {
  NONE,
  WHITE,
  BLACK,
};
struct Piece {
  PieceType type;
  Player owner;
  bool moved;
  Piece() : type(PieceType::NONE), owner(Player::NONE), moved(true) {}
  Piece(PieceType t, Player o, bool m = true) : type(t), owner(o), moved(m) {}
  bool operator==(const Piece &other) const = default;
};PieceType 棋子类型,Player 玩家,Piece 棋子类。
Piece::moved 表示是否已经移动过,用于记录国王、城堡、兵是否移动过,以此得到不同的可行操作集合,该属性对其它棋子没有作用。
struct Move {
  std::string s;
  std::vector<Coord> remove;
  std::vector<std::pair<Coord, Coord>> move;
  std::vector<std::pair<Coord, PieceType>> promote;
  bool operator==(const Move &other) const { return s == other.s; }
};Move 类用于表示一种合法移动,它可以被分解为三个部分,移除 0 个或 1 个棋子,移动 1 个或 2 个棋子,升变 0 个或 1 个兵。
记录移动对应的字符串,方便和玩家选择的移动比较。
struct Board {
  // e2 -> [4, 1]
  std::array<std::array<Piece, ROWS>, COLS> board;
  Player active;
  Move last_move;
  Board() = default;
  Board(std::string const &fen1, std::string const &fen2,
        std::string const &fen3, std::string const &fen4);
  Piece &at(Coord c) { return board[c.col][c.row]; }
  Piece const &at(Coord c) const { return board[c.col][c.row]; }
  Board move(Move m) const;
  std::array<std::array<bool, ROWS>, COLS> pos_under_attack() const;
  bool opposite_king_under_attack() const;
  bool is_checkmate() const;
  bool is_stalemate() const;
  // bool is_legal() const;
  std::vector<Move> get_all_moves() const;
  std::string fen_without_clock() const;
  void show(std::ostream &os) const;
};
Board standard();
Board c960();
Board random_start();棋盘类,board 是一个二维数组,active 表示当前的玩家,last_move 表示上一步的移动。
记录 last_move 一方面为了判断吃过路兵规则,另一方面为了将最后移动的棋子显示为不同的颜色。
move() 方法用于生成下一步的棋盘,
pos_under_attack() 方法判断哪些位置被当前玩家攻击,主要用于检测易位条件是否满足以及国王是否被攻击。
opposite_king_under_attack() 判断对方的国王是否被攻击。根据规则,一方的回合开始时对方的国王不应该处于被攻击的状态。如果返回 true 说明上一步不合法。
get_all_moves() 获取所有合法的移动(让自己的国王不被攻击的移动是合法的)
is_checkmate() is_stalemate() 如果一方没有合法移动,根据国王是否被攻击得到将死或平局的结果。
fen_without_clock() 返回 Forsyth-Edwards Notation 字符串,不包含 Halfmove clock 和 Fullmove number 如 rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -
standard() 返回标准开局。
c960() 返回随机 chess960 开局。
struct Game {
  std::unordered_map<std::uint64_t, int> visited;
  std::vector<Board> board_history;
  std::vector<Move> moves, next_moves;
  std::vector<int> halfmove_clock;
  int fullmove_number;
  Game(Board &&start);
  Game(std::string const &fenstring);
  std::vector<Move> get_all_moves();
  void _play(Move move);
  void play(Move move);
  void play(std::string const &move_str);
  void undo();
  void redo();
  std::string fenstring() const;
  void show_current_board(std::ostream &os) const;
  void show_move_history(std::ostream &os) const;
  void show_board_history(std::ostream &os) const;
};visited 记录所有出现过的局面的哈希值的数据结构,用于实现 3-fold repetition (出现 3 次相同局面和棋) 规则(尚未实现)。
board_history halfmove_clock moves 记录历史局面和移动方式,用于打印历史记录和实现撤销操作。
next_moves 被撤销的操作,用于实现重做。
_play(Move) play(Move) 调用 Board::move() 得到接下来的棋盘并记录。
play(string) 在 get_all_moves() 中寻找玩家选择的移动并执行,没找到则抛出异常。
undo() redo() 撤销、重做。
fenstring() 返回当前局面的 Forsyth-Edwards Notation 字符串。在 fen_without_clock() 的基础上增加了 halfmove_clock 和 fullmove_number。
show_current_board() 打印当前局面。
show_move_history() 打印历史记录。
std::vector<Coord> knight_move(Coord c);
std::vector<Coord> king_move(Coord c);
std::vector<Coord> pos_under_pawn_attack(Coord c, Board const &board);
std::vector<Coord> rook_move(Coord c, Board const &board);
std::vector<Coord> bishop_move(Coord c, Board const &board);
std::vector<Coord> queen_move(Coord c, Board const &board);按规则计算棋子的可行移动。
std::string get_move_from_stockfish(std::string const &fen);
std::string exec(std::string const &pos)调用 stockfish 得到下一步的移动,使用了 pipe() fork() execlp() 系统调用。
bool execute(Game &game, std::string const &input,
             std::string const &last, std::ostream &os);解析用户输入,调用相应的函数,返回是否成功。返回值用于实现 _ 选项执行上一条有效的指令。
todo: 实现哈希表记录出现过的局面、实现 chess960 的易位、实现随机盘面、测试兵的升变
测的不是很多可能有很多 bug
看到参考项目里有中国象棋于是就想写一个国际象棋,一开始大大低估了实现的难度
中国象棋相比国际象棋有很多好的性质,实现起来会容易很多
中国象棋的盘面状态只与所有棋子的位置有关,而国际象棋需要处理过路兵和易位的机会
中国象棋的一次移动都可以表示为将一个棋子移动到另一个位置,而国际象棋需要处理同时移动两个棋子和吃掉移动棋子目标位置之外的情况
国际象棋不允许送将,无子可动会和棋,判断和棋和将死需要很多逻辑
所以很多象棋变体需要吃掉国王(将帅)而不是将死就可以
一开始用了很多时间考虑如何实现会方便增加其它功能
看到一个 python 的实现定义了棋子类,所有棋子都继承自这个类, 这样的好处是检查可行移动时可以直接调用虚函数, 但考虑到这样写会把各个棋子的字符串表示分散在不同类里,于是没有采用这种写法
没有编写多文件 c++ 项目的经验,写的时候遇到了各种链接错误
文件组织的有点混乱, .hpp 文件只写了一个,棋盘的显示和棋子移动的逻辑没有完全分离都放在 Board 类里了
没有很好地优化,有些内容被重复计算,
让我再写一次应该会更规范一点
要注意 c++ 局部变量不会自动初始化 (╥﹏╥)
学习了 conan cmake gtest 等工具的使用
学习了 <unistd.h>
stringstream 会被一些库认为是不能显示颜色的,需要传入 std::cout 而不是使用 sstream 然后返回字符串
- 
Forsyth-Edwards Notation https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
 - 
python chess https://github.com/marcusbuffett/command-line-chess
 - 
wikipedia chess https://en.wikipedia.org/wiki/Chess
 - 
uci protocol https://backscattering.de/chess/uci/
 - 
Portable_Game_Notation https://en.wikipedia.org/wiki/Portable_Game_Notation
 - 
stockfish commands https://official-stockfish.github.io/docs/stockfish-wiki/UCI-&-Commands.html
 - 
threefold repetition https://en.wikipedia.org/wiki/Threefold_repetition
 - 
Fifty-move rule https://en.wikipedia.org/wiki/Fifty-move_rule