Chess: Refactor game logic into LibChess for use in engines

In the future UCI protocol stuff will also go into LibChess.
This commit is contained in:
Peter Elliott 2020-08-18 15:02:53 -06:00 committed by Andreas Kling
parent ffece9cfba
commit d2cb5e0f48
Notes: sideshowbarker 2024-07-19 03:22:21 +09:00
7 changed files with 165 additions and 148 deletions

View file

@ -1,9 +1,8 @@
set(SOURCES set(SOURCES
main.cpp main.cpp
Chess.cpp
ChessWidget.cpp ChessWidget.cpp
PromotionDialog.cpp PromotionDialog.cpp
) )
serenity_bin(Chess) serenity_bin(Chess)
target_link_libraries(Chess LibGUI) target_link_libraries(Chess LibChess LibGUI)

View file

@ -123,22 +123,22 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
} }
if (board().apply_move(move)) { if (board().apply_move(move)) {
if (board().game_result() != Chess::Result::NotFinished) { if (board().game_result() != Chess::Board::Result::NotFinished) {
bool over = true; bool over = true;
String msg; String msg;
switch (board().game_result()) { switch (board().game_result()) {
case Chess::Result::CheckMate: case Chess::Board::Result::CheckMate:
if (board().turn() == Chess::Colour::White) { if (board().turn() == Chess::Colour::White) {
msg = "Black wins by Checkmate."; msg = "Black wins by Checkmate.";
} else { } else {
msg = "White wins by Checkmate."; msg = "White wins by Checkmate.";
} }
break; break;
case Chess::Result::StaleMate: case Chess::Board::Result::StaleMate:
msg = "Draw by Stalemate."; msg = "Draw by Stalemate.";
break; break;
case Chess::Result::FiftyMoveRule: case Chess::Board::Result::FiftyMoveRule:
update(); update();
if (GUI::MessageBox::show(window(), "50 moves have elapsed without a capture. Claim Draw?", "Claim Draw?", if (GUI::MessageBox::show(window(), "50 moves have elapsed without a capture. Claim Draw?", "Claim Draw?",
GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo) GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
@ -148,10 +148,10 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
over = false; over = false;
} }
break; break;
case Chess::Result::SeventyFiveMoveRule: case Chess::Board::Result::SeventyFiveMoveRule:
msg = "Draw by 75 move rule."; msg = "Draw by 75 move rule.";
break; break;
case Chess::Result::ThreeFoldRepitition: case Chess::Board::Result::ThreeFoldRepitition:
update(); update();
if (GUI::MessageBox::show(window(), "The same board state has repeated three times. Claim Draw?", "Claim Draw?", if (GUI::MessageBox::show(window(), "The same board state has repeated three times. Claim Draw?", "Claim Draw?",
GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo) GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
@ -161,10 +161,10 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event)
over = false; over = false;
} }
break; break;
case Chess::Result::FiveFoldRepitition: case Chess::Board::Result::FiveFoldRepitition:
msg = "Draw by fivefold repitition."; msg = "Draw by fivefold repitition.";
break; break;
case Chess::Result::InsufficientMaterial: case Chess::Board::Result::InsufficientMaterial:
msg = "Draw by insufficient material."; msg = "Draw by insufficient material.";
break; break;
default: default:
@ -239,7 +239,7 @@ RefPtr<Gfx::Bitmap> ChessWidget::get_piece_graphic(const Chess::Piece& piece) co
void ChessWidget::reset() void ChessWidget::reset()
{ {
m_board = Chess(); m_board = Chess::Board();
m_drag_enabled = true; m_drag_enabled = true;
update(); update();
} }

View file

@ -26,11 +26,11 @@
#pragma once #pragma once
#include "Chess.h"
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <AK/NonnullRefPtr.h> #include <AK/NonnullRefPtr.h>
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <LibChess/Chess.h>
#include <LibGUI/Widget.h> #include <LibGUI/Widget.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
@ -47,7 +47,7 @@ public:
virtual void mouseup_event(GUI::MouseEvent&) override; virtual void mouseup_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override; virtual void mousemove_event(GUI::MouseEvent&) override;
Chess& board() { return m_board; }; Chess::Board& board() { return m_board; };
Chess::Colour side() const { return m_side; }; Chess::Colour side() const { return m_side; };
void set_side(Chess::Colour side) { m_side = side; }; void set_side(Chess::Colour side) { m_side = side; };
@ -74,7 +74,7 @@ public:
void set_board_theme(const StringView& name); void set_board_theme(const StringView& name);
private: private:
Chess m_board; Chess::Board m_board;
BoardTheme m_board_theme { "Beige", Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) }; BoardTheme m_board_theme { "Beige", Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) };
Color m_move_highlight_color { Color::from_rgba(0x66ccee00) }; Color m_move_highlight_color { Color::from_rgba(0x66ccee00) };
Chess::Colour m_side { Chess::Colour::White }; Chess::Colour m_side { Chess::Colour::White };

View file

@ -1,5 +1,6 @@
add_subdirectory(LibAudio) add_subdirectory(LibAudio)
add_subdirectory(LibC) add_subdirectory(LibC)
add_subdirectory(LibChess)
add_subdirectory(LibCore) add_subdirectory(LibCore)
add_subdirectory(LibCrypt) add_subdirectory(LibCrypt)
add_subdirectory(LibCrypto) add_subdirectory(LibCrypto)

View file

@ -0,0 +1,6 @@
set(SOURCES
Chess.cpp
)
serenity_lib(LibChess chess)
target_link_libraries(LibChess LibC)

View file

@ -24,15 +24,17 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
#include "Chess.h"
#include <AK/Assertions.h> #include <AK/Assertions.h>
#include <AK/LogStream.h> #include <AK/LogStream.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/StringBuilder.h> #include <AK/StringBuilder.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibChess/Chess.h>
#include <stdlib.h> #include <stdlib.h>
String Chess::char_for_piece(Chess::Type type) namespace Chess {
String char_for_piece(Chess::Type type)
{ {
switch (type) { switch (type) {
case Type::Knight: case Type::Knight:
@ -51,7 +53,12 @@ String Chess::char_for_piece(Chess::Type type)
} }
} }
Chess::Square::Square(const StringView& name) Colour opposing_colour(Colour colour)
{
return (colour == Colour::White) ? Colour::Black : Colour::White;
}
Square::Square(const StringView& name)
{ {
ASSERT(name.length() == 2); ASSERT(name.length() == 2);
char filec = name[0]; char filec = name[0];
@ -72,7 +79,7 @@ Chess::Square::Square(const StringView& name)
} }
} }
String Chess::Square::to_algebraic() const String Square::to_algebraic() const
{ {
StringBuilder builder; StringBuilder builder;
builder.append(file - 'a'); builder.append(file - 'a');
@ -80,7 +87,7 @@ String Chess::Square::to_algebraic() const
return builder.build(); return builder.build();
} }
String Chess::Move::to_long_algebraic() const String Move::to_long_algebraic() const
{ {
StringBuilder builder; StringBuilder builder;
builder.append(from.to_algebraic()); builder.append(from.to_algebraic());
@ -89,7 +96,7 @@ String Chess::Move::to_long_algebraic() const
return builder.build(); return builder.build();
} }
Chess::Chess() Board::Board()
{ {
// Fill empty spaces. // Fill empty spaces.
for (unsigned rank = 2; rank < 6; ++rank) { for (unsigned rank = 2; rank < 6; ++rank) {
@ -129,21 +136,21 @@ Chess::Chess()
set_piece(Square("h8"), { Colour::Black, Type::Rook }); set_piece(Square("h8"), { Colour::Black, Type::Rook });
} }
Chess::Piece Chess::get_piece(const Square& square) const Piece Board::get_piece(const Square& square) const
{ {
ASSERT(square.rank < 8); ASSERT(square.rank < 8);
ASSERT(square.file < 8); ASSERT(square.file < 8);
return m_board[square.rank][square.file]; return m_board[square.rank][square.file];
} }
Chess::Piece Chess::set_piece(const Square& square, const Piece& piece) Piece Board::set_piece(const Square& square, const Piece& piece)
{ {
ASSERT(square.rank < 8); ASSERT(square.rank < 8);
ASSERT(square.file < 8); ASSERT(square.file < 8);
return m_board[square.rank][square.file] = piece; return m_board[square.rank][square.file] = piece;
} }
bool Chess::is_legal(const Move& move, Colour colour) const bool Board::is_legal(const Move& move, Colour colour) const
{ {
if (colour == Colour::None) if (colour == Colour::None)
colour = turn(); colour = turn();
@ -151,7 +158,7 @@ bool Chess::is_legal(const Move& move, Colour colour) const
if (!is_legal_no_check(move, colour)) if (!is_legal_no_check(move, colour))
return false; return false;
Chess clone = *this; Board clone = *this;
clone.apply_illegal_move(move, colour); clone.apply_illegal_move(move, colour);
if (clone.in_check(colour)) if (clone.in_check(colour))
return false; return false;
@ -172,7 +179,7 @@ bool Chess::is_legal(const Move& move, Colour colour) const
} }
} }
for (auto& square : check_squares) { for (auto& square : check_squares) {
Chess clone = *this; Board clone = *this;
clone.set_piece(move.from, EmptyPiece); clone.set_piece(move.from, EmptyPiece);
clone.set_piece(square, { colour, Type::King }); clone.set_piece(square, { colour, Type::King });
if (clone.in_check(colour)) if (clone.in_check(colour))
@ -182,7 +189,7 @@ bool Chess::is_legal(const Move& move, Colour colour) const
return true; return true;
} }
bool Chess::is_legal_no_check(const Move& move, Colour colour) const bool Board::is_legal_no_check(const Move& move, Colour colour) const
{ {
auto piece = get_piece(move.from); auto piece = get_piece(move.from);
if (piece.colour != colour) if (piece.colour != colour)
@ -314,7 +321,7 @@ bool Chess::is_legal_no_check(const Move& move, Colour colour) const
return false; return false;
} }
bool Chess::in_check(Colour colour) const bool Board::in_check(Colour colour) const
{ {
Square king_square = { 50, 50 }; Square king_square = { 50, 50 };
Square::for_each([&](const Square& square) { Square::for_each([&](const Square& square) {
@ -341,7 +348,7 @@ bool Chess::in_check(Colour colour) const
return check; return check;
} }
bool Chess::apply_move(const Move& move, Colour colour) bool Board::apply_move(const Move& move, Colour colour)
{ {
if (colour == Colour::None) if (colour == Colour::None)
colour = turn(); colour = turn();
@ -352,9 +359,9 @@ bool Chess::apply_move(const Move& move, Colour colour)
return apply_illegal_move(move, colour); return apply_illegal_move(move, colour);
} }
bool Chess::apply_illegal_move(const Move& move, Colour colour) bool Board::apply_illegal_move(const Move& move, Colour colour)
{ {
Chess clone = *this; Board clone = *this;
clone.m_previous_states = {}; clone.m_previous_states = {};
auto state_count = 0; auto state_count = 0;
if (m_previous_states.contains(clone)) if (m_previous_states.contains(clone))
@ -431,7 +438,7 @@ bool Chess::apply_illegal_move(const Move& move, Colour colour)
return true; return true;
} }
Chess::Result Chess::game_result() const Board::Result Board::game_result() const
{ {
bool sufficient_material = false; bool sufficient_material = false;
bool no_more_pieces_allowed = false; bool no_more_pieces_allowed = false;
@ -501,7 +508,7 @@ Chess::Result Chess::game_result() const
return Result::StaleMate; return Result::StaleMate;
} }
bool Chess::is_promotion_move(const Move& move, Colour colour) const bool Board::is_promotion_move(const Move& move, Colour colour) const
{ {
if (colour == Colour::None) if (colour == Colour::None)
colour = turn(); colour = turn();
@ -517,7 +524,7 @@ bool Chess::is_promotion_move(const Move& move, Colour colour) const
return false; return false;
} }
bool Chess::operator==(const Chess& other) const bool Board::operator==(const Board& other) const
{ {
bool equal_squares = true; bool equal_squares = true;
Square::for_each([&](Square sq) { Square::for_each([&](Square sq) {
@ -541,3 +548,5 @@ bool Chess::operator==(const Chess& other) const
return turn() == other.turn(); return turn() == other.turn();
} }
}

View file

@ -32,80 +32,82 @@
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Traits.h> #include <AK/Traits.h>
class Chess { namespace Chess {
public:
enum class Type {
Pawn,
Knight,
Bishop,
Rook,
Queen,
King,
None,
};
static String char_for_piece(Type type);
enum class Colour { enum class Type {
White, Pawn,
Black, Knight,
None, Bishop,
}; Rook,
static Colour opposing_colour(Colour colour) { return (colour == Colour::White) ? Colour::Black : Colour::White; } Queen,
King,
None,
};
struct Piece { String char_for_piece(Type type);
Colour colour;
Type type;
bool operator==(const Piece& other) const { return colour == other.colour && type == other.type; }
}; enum class Colour {
static constexpr Piece EmptyPiece = { Colour::None, Type::None }; White,
Black,
None,
};
struct Square { Colour opposing_colour(Colour colour);
unsigned rank; // zero indexed;
unsigned file;
Square(const StringView& name);
Square(const unsigned& rank, const unsigned& file)
: rank(rank)
, file(file)
{
}
bool operator==(const Square& other) const { return rank == other.rank && file == other.file; }
template<typename Callback> struct Piece {
static void for_each(Callback callback) Colour colour;
{ Type type;
for (int rank = 0; rank < 8; ++rank) { bool operator==(const Piece& other) const { return colour == other.colour && type == other.type; }
for (int file = 0; file < 8; ++file) { };
if (callback(Square(rank, file)) == IterationDecision::Break) {
goto exit; constexpr Piece EmptyPiece = { Colour::None, Type::None };
}
} struct Square {
unsigned rank; // zero indexed;
unsigned file;
Square(const StringView& name);
Square(const unsigned& rank, const unsigned& file)
: rank(rank)
, file(file)
{
}
bool operator==(const Square& other) const { return rank == other.rank && file == other.file; }
template<typename Callback>
static void for_each(Callback callback)
{
for (int rank = 0; rank < 8; ++rank) {
for (int file = 0; file < 8; ++file) {
if (callback(Square(rank, file)) == IterationDecision::Break)
return;
} }
exit:;
} }
}
bool in_bounds() const { return rank < 8 && file < 8; } bool in_bounds() const { return rank < 8 && file < 8; }
bool is_light() const { return (rank % 2) != (file % 2); } bool is_light() const { return (rank % 2) != (file % 2); }
String to_algebraic() const; String to_algebraic() const;
}; };
struct Move { struct Move {
Square from; Square from;
Square to; Square to;
Type promote_to; Type promote_to;
Move(const StringView& algebraic); Move(const StringView& algebraic);
Move(const Square& from, const Square& to, const Type& promote_to = Type::None) Move(const Square& from, const Square& to, const Type& promote_to = Type::None)
: from(from) : from(from)
, to(to) , to(to)
, promote_to(promote_to) , promote_to(promote_to)
{ {
} }
bool operator==(const Move& other) const { return from == other.from && to == other.to && promote_to == other.promote_to; } bool operator==(const Move& other) const { return from == other.from && to == other.to && promote_to == other.promote_to; }
String to_long_algebraic() const; String to_long_algebraic() const;
}; };
Chess(); class Board {
public:
Board();
Piece get_piece(const Square&) const; Piece get_piece(const Square&) const;
Piece set_piece(const Square&, const Piece&); Piece set_piece(const Square&, const Piece&);
@ -135,7 +137,7 @@ public:
Colour turn() const { return m_turn; }; Colour turn() const { return m_turn; };
bool operator==(const Chess& other) const; bool operator==(const Board& other) const;
private: private:
bool is_legal_no_check(const Move&, Colour colour) const; bool is_legal_no_check(const Move&, Colour colour) const;
@ -151,41 +153,12 @@ private:
bool m_black_can_castle_kingside { true }; bool m_black_can_castle_kingside { true };
bool m_black_can_castle_queenside { true }; bool m_black_can_castle_queenside { true };
HashMap<Chess, int> m_previous_states; HashMap<Board, int> m_previous_states;
friend struct AK::Traits<Chess>; friend struct AK::Traits<Board>;
};
template<>
struct AK::Traits<Chess::Piece> : public GenericTraits<Chess::Piece> {
static unsigned hash(Chess::Piece piece)
{
return pair_int_hash(static_cast<u32>(piece.colour), static_cast<u32>(piece.type));
}
};
template<>
struct AK::Traits<Chess> : public GenericTraits<Chess> {
static unsigned hash(Chess chess)
{
unsigned hash = 0;
hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_queenside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_kingside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_queenside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside));
Chess::Square::for_each([&](Chess::Square sq) {
hash = pair_int_hash(hash, Traits<Chess::Piece>::hash(chess.get_piece(sq)));
return IterationDecision::Continue;
});
return hash;
}
}; };
template<typename Callback> template<typename Callback>
void Chess::generate_moves(Callback callback, Colour colour) const void Board::generate_moves(Callback callback, Colour colour) const
{ {
if (colour == Colour::None) if (colour == Colour::None)
colour = turn(); colour = turn();
@ -205,27 +178,25 @@ void Chess::generate_moves(Callback callback, Colour colour) const
bool keep_going = true; bool keep_going = true;
if (piece.type == Type::Pawn) { if (piece.type == Type::Pawn) {
for (auto& piece : Vector({Type::None, Type::Knight, Type::Bishop, Type::Rook, Type::Queen})) { for (auto& piece : Vector({ Type::None, Type::Knight, Type::Bishop, Type::Rook, Type::Queen })) {
keep_going = keep_going = try_move({ sq, { sq.rank + 1, sq.file }, piece })
try_move({sq, {sq.rank+1, sq.file}, piece}) && && try_move({ sq, { sq.rank + 2, sq.file }, piece })
try_move({sq, {sq.rank+2, sq.file}, piece}) && && try_move({ sq, { sq.rank - 1, sq.file }, piece })
try_move({sq, {sq.rank-1, sq.file}, piece}) && && try_move({ sq, { sq.rank - 2, sq.file }, piece })
try_move({sq, {sq.rank-2, sq.file}, piece}) && && try_move({ sq, { sq.rank + 1, sq.file + 1 }, piece })
try_move({sq, {sq.rank+1, sq.file+1}}) && && try_move({ sq, { sq.rank + 1, sq.file - 1 }, piece })
try_move({sq, {sq.rank+1, sq.file-1}}) && && try_move({ sq, { sq.rank - 1, sq.file + 1 }, piece })
try_move({sq, {sq.rank-1, sq.file+1}}) && && try_move({ sq, { sq.rank - 1, sq.file - 1 }, piece });
try_move({sq, {sq.rank-1, sq.file-1}});
} }
} else if (piece.type == Type::Knight) { } else if (piece.type == Type::Knight) {
keep_going = keep_going = try_move({ sq, { sq.rank + 2, sq.file + 1 } })
try_move({sq, {sq.rank+2, sq.file+1}}) && && try_move({ sq, { sq.rank + 2, sq.file - 1 } })
try_move({sq, {sq.rank+2, sq.file-1}}) && && try_move({ sq, { sq.rank + 1, sq.file + 2 } })
try_move({sq, {sq.rank+1, sq.file+2}}) && && try_move({ sq, { sq.rank + 1, sq.file - 2 } })
try_move({sq, {sq.rank+1, sq.file-2}}) && && try_move({ sq, { sq.rank - 2, sq.file + 1 } })
try_move({sq, {sq.rank-2, sq.file+1}}) && && try_move({ sq, { sq.rank - 2, sq.file - 1 } })
try_move({sq, {sq.rank-2, sq.file-1}}) && && try_move({ sq, { sq.rank - 1, sq.file + 2 } })
try_move({sq, {sq.rank-1, sq.file+2}}) && && try_move({ sq, { sq.rank - 1, sq.file - 2 } });
try_move({sq, {sq.rank-1, sq.file-2}});
} else if (piece.type == Type::Bishop) { } else if (piece.type == Type::Bishop) {
for (int dr = -1; dr <= 1; dr += 2) { for (int dr = -1; dr <= 1; dr += 2) {
for (int df = -1; df <= 1; df += 2) { for (int df = -1; df <= 1; df += 2) {
@ -280,3 +251,34 @@ void Chess::generate_moves(Callback callback, Colour colour) const
} }
}); });
} }
}
template<>
struct AK::Traits<Chess::Piece> : public GenericTraits<Chess::Piece> {
static unsigned hash(Chess::Piece piece)
{
return pair_int_hash(static_cast<u32>(piece.colour), static_cast<u32>(piece.type));
}
};
template<>
struct AK::Traits<Chess::Board> : public GenericTraits<Chess::Board> {
static unsigned hash(Chess::Board chess)
{
unsigned hash = 0;
hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_queenside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_white_can_castle_kingside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_queenside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside));
hash = pair_int_hash(hash, static_cast<u32>(chess.m_black_can_castle_kingside));
Chess::Square::for_each([&](Chess::Square sq) {
hash = pair_int_hash(hash, Traits<Chess::Piece>::hash(chess.get_piece(sq)));
return IterationDecision::Continue;
});
return hash;
}
};