diff --git a/Games/Chess/Chess.cpp b/Games/Chess/Chess.cpp index 88ca8162ec..88a8c220f2 100644 --- a/Games/Chess/Chess.cpp +++ b/Games/Chess/Chess.cpp @@ -305,9 +305,17 @@ bool Chess::apply_move(const Move& move, Colour colour) bool Chess::apply_illegal_move(const Move& move, Colour colour) { + Chess clone = *this; + clone.m_previous_states = {}; + auto state_count = 0; + if (m_previous_states.contains(clone)) + state_count = m_previous_states.get(clone).value(); + m_previous_states.set(clone, state_count + 1); + m_turn = opposing_colour(colour); m_last_move = move; + m_moves_since_capture++; if (move.from == Square("a1") || move.to == Square("a1") || move.from == Square("e1")) m_white_can_castle_queenside = false; @@ -362,8 +370,12 @@ bool Chess::apply_illegal_move(const Move& move, Colour colour) } else { set_piece({ move.to.rank + 1, move.to.file }, EmptyPiece); } + m_moves_since_capture = 0; } + if (get_piece(move.to).colour != Colour::None) + m_moves_since_capture = 0; + set_piece(move.to, get_piece(move.from)); set_piece(move.from, EmptyPiece); @@ -372,6 +384,44 @@ bool Chess::apply_illegal_move(const Move& move, Colour colour) Chess::Result Chess::game_result() const { + bool sufficient_material = false; + bool no_more_pieces_allowed = false; + Optional bishop; + Square::for_each([&](Square sq) { + if (get_piece(sq).type == Type::Queen || get_piece(sq).type == Type::Rook || get_piece(sq).type == Type::Pawn) { + sufficient_material = true; + return IterationDecision::Break; + } + + if (get_piece(sq).type != Type::None && get_piece(sq).type != Type::King && no_more_pieces_allowed) { + sufficient_material = true; + return IterationDecision::Break; + } + + if (get_piece(sq).type == Type::Knight) + no_more_pieces_allowed = true; + + if (get_piece(sq).type == Type::Bishop) { + if (bishop.has_value()) { + if (get_piece(sq).colour == get_piece(bishop.value()).colour) { + sufficient_material = true; + return IterationDecision::Break; + } else if (sq.is_light() != bishop.value().is_light()) { + sufficient_material = true; + return IterationDecision::Break; + } + no_more_pieces_allowed = true; + } else { + bishop = sq; + } + } + + return IterationDecision::Continue; + }); + + if (!sufficient_material) + return Result::InsufficientMaterial; + bool are_legal_moves = false; generate_moves([&](Move m) { (void)m; @@ -379,8 +429,22 @@ Chess::Result Chess::game_result() const return IterationDecision::Break; }); - if (are_legal_moves) + if (are_legal_moves) { + if (m_moves_since_capture >= 75 * 2) + return Result::SeventyFiveMoveRule; + if (m_moves_since_capture == 50 * 2) + return Result::FiftyMoveRule; + + auto repeats = m_previous_states.get(*this); + if (repeats.has_value()) { + if (repeats.value() == 3) + return Result::ThreeFoldRepitition; + if (repeats.value() >= 5) + return Result::FiveFoldRepitition; + } + return Result::NotFinished; + } if (in_check(turn())) return Result::CheckMate; @@ -401,3 +465,28 @@ bool Chess::is_promotion_move(const Move& move, Colour colour) const return false; } + +bool Chess::operator==(const Chess& other) const +{ + bool equal_squares = true; + Square::for_each([&](Square sq) { + if (get_piece(sq) != other.get_piece(sq)) { + equal_squares = false; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (!equal_squares) + return false; + + if (m_white_can_castle_queenside != other.m_white_can_castle_queenside) + return false; + if (m_white_can_castle_kingside != other.m_white_can_castle_kingside) + return false; + if (m_black_can_castle_queenside != other.m_black_can_castle_queenside) + return false; + if (m_black_can_castle_kingside != other.m_black_can_castle_kingside) + return false; + + return turn() == other.turn(); +} diff --git a/Games/Chess/Chess.h b/Games/Chess/Chess.h index 078262cb84..60d49b4da1 100644 --- a/Games/Chess/Chess.h +++ b/Games/Chess/Chess.h @@ -26,6 +26,7 @@ #pragma once +#include #include #include #include @@ -82,6 +83,7 @@ public: } bool in_bounds() const { return rank < 8 && file < 8; } + bool is_light() const { return (rank % 2) != (file % 2); } }; struct Move { @@ -115,7 +117,10 @@ public: CheckMate, StaleMate, FiftyMoveRule, + SeventyFiveMoveRule, ThreeFoldRepitition, + FiveFoldRepitition, + InsufficientMaterial, NotFinished, }; @@ -125,6 +130,8 @@ public: Colour turn() const { return m_turn; }; + bool operator==(const Chess& other) const; + private: bool is_legal_no_check(const Move&, Colour colour) const; bool apply_illegal_move(const Move&, Colour colour); @@ -132,11 +139,15 @@ private: Piece m_board[8][8]; Colour m_turn { Colour::White }; Optional m_last_move; + int m_moves_since_capture { 0 }; bool m_white_can_castle_kingside { true }; bool m_white_can_castle_queenside { true }; bool m_black_can_castle_kingside { true }; bool m_black_can_castle_queenside { true }; + + HashMap m_previous_states; + friend struct AK::Traits; }; template<> @@ -147,6 +158,27 @@ struct AK::Traits : public GenericTraits { } }; +template<> +struct AK::Traits : public GenericTraits { + static unsigned hash(Chess chess) + { + unsigned hash = 0; + hash = pair_int_hash(hash, static_cast(chess.m_white_can_castle_queenside)); + hash = pair_int_hash(hash, static_cast(chess.m_white_can_castle_kingside)); + hash = pair_int_hash(hash, static_cast(chess.m_black_can_castle_queenside)); + hash = pair_int_hash(hash, static_cast(chess.m_black_can_castle_kingside)); + + hash = pair_int_hash(hash, static_cast(chess.m_black_can_castle_kingside)); + + Chess::Square::for_each([&](Chess::Square sq) { + hash = pair_int_hash(hash, Traits::hash(chess.get_piece(sq))); + return IterationDecision::Continue; + }); + + return hash; + } +}; + template void Chess::generate_moves(Callback callback, Colour colour) const { diff --git a/Games/Chess/ChessWidget.cpp b/Games/Chess/ChessWidget.cpp index 539d8979fa..5b671ce8d8 100644 --- a/Games/Chess/ChessWidget.cpp +++ b/Games/Chess/ChessWidget.cpp @@ -62,7 +62,7 @@ void ChessWidget::paint_event(GUI::PaintEvent& event) tile_rect = { (7 - sq.file) * tile_width, sq.rank * tile_height, tile_width, tile_height }; } - painter.fill_rect(tile_rect, ((sq.rank % 2) == (sq.file % 2)) ? board_theme().dark_square_color : board_theme().light_square_color); + painter.fill_rect(tile_rect, (sq.is_light()) ? board_theme().light_square_color : board_theme().dark_square_color); if (board().last_move().has_value() && (board().last_move().value().to == sq || board().last_move().value().from == sq)) { painter.fill_rect(tile_rect, m_move_highlight_color); @@ -124,9 +124,8 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event) if (board().apply_move(move)) { if (board().game_result() != Chess::Result::NotFinished) { - set_drag_enabled(false); - update(); + bool over = true; String msg; switch (board().game_result()) { case Chess::Result::CheckMate: @@ -140,15 +139,42 @@ void ChessWidget::mouseup_event(GUI::MouseEvent& event) msg = "Draw by Stalemate."; break; case Chess::Result::FiftyMoveRule: - msg = "Draw by 50 move rule."; + update(); + 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::Dialog::ExecYes) { + msg = "Draw by 50 move rule."; + } else { + over = false; + } + break; + case Chess::Result::SeventyFiveMoveRule: + msg = "Draw by 75 move rule."; break; case Chess::Result::ThreeFoldRepitition: - msg = "Draw by threefold repitition."; + update(); + 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::Dialog::ExecYes) { + msg = "Draw by threefold repitition."; + } else { + over = false; + } + break; + case Chess::Result::FiveFoldRepitition: + msg = "Draw by fivefold repitition."; + break; + case Chess::Result::InsufficientMaterial: + msg = "Draw by insufficient material."; break; default: ASSERT_NOT_REACHED(); } - GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information); + if (over) { + set_drag_enabled(false); + update(); + GUI::MessageBox::show(window(), msg, "Game Over", GUI::MessageBox::Type::Information); + } } }