From 7fea8325f6068252387c64caf0ac5ab7411cc2f4 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Wed, 7 Aug 2024 22:42:56 -0700 Subject: [PATCH 01/12] Create Resignation Gene Not involved in gameplay, yet. --- Genetic_Chess.vcxproj | 2 + src/Genes/Genome.cpp | 8 ++++ src/Genes/Genome.h | 5 ++- src/Genes/Resignation_Gene.cpp | 67 ++++++++++++++++++++++++++++++++++ src/Genes/Resignation_Gene.h | 33 +++++++++++++++++ src/Players/Genetic_AI.cpp | 5 ++- 6 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/Genes/Resignation_Gene.cpp create mode 100644 src/Genes/Resignation_Gene.h diff --git a/Genetic_Chess.vcxproj b/Genetic_Chess.vcxproj index e4deae62..901d1025 100644 --- a/Genetic_Chess.vcxproj +++ b/Genetic_Chess.vcxproj @@ -264,6 +264,7 @@ + @@ -312,6 +313,7 @@ + diff --git a/src/Genes/Genome.cpp b/src/Genes/Genome.cpp index e29663f6..f35c0399 100644 --- a/src/Genes/Genome.cpp +++ b/src/Genes/Genome.cpp @@ -30,6 +30,7 @@ #include "Genes/Checkmate_Material_Gene.h" #include "Genes/Pawn_Structure_Gene.h" #include "Genes/Move_Sorting_Gene.h" +#include "Genes/Resignation_Gene.h" namespace { @@ -42,6 +43,7 @@ Genome::Genome() noexcept : std::make_unique(), std::make_unique(), std::make_unique(), + std::make_unique(), std::make_unique(nullptr), std::make_unique(), std::make_unique(), @@ -60,6 +62,7 @@ Genome::Genome() noexcept : assert(gene_reference().name() == "Piece Strength Gene"); assert(gene_reference().name() == "Look Ahead Gene"); assert(gene_reference().name() == "Move Sorting Gene"); + assert(gene_reference().name() == "Resignation Gene"); } Genome::Genome(const Genome& other) noexcept : id_number(other.id()) @@ -260,6 +263,11 @@ void Genome::print(std::ostream& os) const noexcept os << "END\n\n"; } +bool Genome::should_resign(const std::vector& commentary, const Piece_Color perspective) const noexcept +{ + return gene_reference().should_resign(commentary, perspective); +} + Clock::seconds Genome::time_to_examine(const Board& board, const Clock& clock) const noexcept { return gene_reference().time_to_examine(board, clock); diff --git a/src/Genes/Genome.h b/src/Genes/Genome.h index 78a6dfee..61150a0e 100644 --- a/src/Genes/Genome.h +++ b/src/Genes/Genome.h @@ -14,6 +14,7 @@ class Board; class Move; +class Game_Tree_Node_Result; //! \brief A software analog to a biological chromosome containing a collection of Gene instances that control the chess player's behavior. class Genome @@ -120,9 +121,11 @@ class Genome //! \param os The output stream. void print(std::ostream& os) const noexcept; + bool should_resign(const std::vector& commentary, const Piece_Color perspective) const noexcept; + private: int id_number; - std::array, 14> genome; + std::array, 15> genome; double score_board(const Board& board, Piece_Color perspective, size_t depth) const noexcept; void reset_piece_strength_gene() noexcept; diff --git a/src/Genes/Resignation_Gene.cpp b/src/Genes/Resignation_Gene.cpp new file mode 100644 index 00000000..957afa50 --- /dev/null +++ b/src/Genes/Resignation_Gene.cpp @@ -0,0 +1,67 @@ +#include "Resignation_Gene.h" + +#include "Gene_Value.h" +#include "Utility/Random.h" +#include "Game/Color.h" +#include "Players/Game_Tree_Node_Result.h" + +Resignation_Gene::Resignation_Gene() noexcept : Clonable_Gene("Resignation Gene") +{ +} + +bool Resignation_Gene::should_resign(const std::vector& commentary, const Piece_Color perspective) const noexcept +{ + auto under_floor_streak = 0; + for(auto it = commentary.rbegin(); it != commentary.rend(); ++it) + { + if(it->corrected_score(perspective) < board_score_floor.value()) + { + ++under_floor_streak; + if(under_floor_streak > max_under_floor_streak.value()) + { + return true; + } + } + else + { + return false; + } + } + + return false; +} + +void Resignation_Gene::gene_specific_mutation() noexcept +{ + if(Random::coin_flip()) + { + board_score_floor.mutate(); + } + else + { + max_under_floor_streak.mutate(); + if(max_under_floor_streak.value() < 0.0) + { + max_under_floor_streak.value() *= -1; + } + } +} + +void Resignation_Gene::adjust_properties(std::map& properties) const noexcept +{ + delete_activations(properties); + delete_priorities(properties); + board_score_floor.write_to_map(properties); + max_under_floor_streak.write_to_map(properties); +} + +void Resignation_Gene::load_gene_properties(const std::map& properties) +{ + board_score_floor.load_from_map(properties); + max_under_floor_streak.load_from_map(properties); +} + +double Resignation_Gene::score_board(const Board&, Piece_Color, size_t) const noexcept +{ + return 0.0; +} diff --git a/src/Genes/Resignation_Gene.h b/src/Genes/Resignation_Gene.h new file mode 100644 index 00000000..57af0221 --- /dev/null +++ b/src/Genes/Resignation_Gene.h @@ -0,0 +1,33 @@ +#ifndef RESIGNATION_GENE_h + +#include "Gene.h" +#include "Gene_Value.h" +#include "Game/Color.h" + +#include + +class Game_Tree_Node_Result; + +class Resignation_Gene : public Clonable_Gene +{ + public: + //! \brief Index for locating the gene in the genome + static const size_t genome_index = 3; + + Resignation_Gene() noexcept; + + bool should_resign(const std::vector& commentary, Piece_Color perspective) const noexcept; + + private: + Gene_Value board_score_floor{"Score Floor", 0.0, 1.0}; + Gene_Value max_under_floor_streak{"Max Under Floor Streak", 20.0, 1.0}; + + void gene_specific_mutation() noexcept override; + void adjust_properties(std::map& properties) const noexcept override; + void load_gene_properties(const std::map& properties) override; + + double score_board(const Board& board, Piece_Color perspective, size_t depth) const noexcept override; +}; + + +#endif // !RESIGNATION_GENE_h diff --git a/src/Players/Genetic_AI.cpp b/src/Players/Genetic_AI.cpp index 527d7582..a4eb0e92 100644 --- a/src/Players/Genetic_AI.cpp +++ b/src/Players/Genetic_AI.cpp @@ -111,7 +111,10 @@ const Move& Genetic_AI::choose_move_minimax(const Board& board, const Clock& clo current_variation); report_final_search_stats(result, board); - + if(genome.should_resign(commentary, board.whose_turn())) + { + // Do something clever here + } return *result.variation_line().front(); } From 668951c6bb9df389ba8091ed14018b2b394aa9c6 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Thu, 8 Aug 2024 21:59:33 -0700 Subject: [PATCH 02/12] Create class for move and resignation choice --- Genetic_Chess.vcxproj | 2 ++ src/Game/Game.cpp | 12 +++++++++--- src/Game/Game_Result.cpp | 2 ++ src/Game/Game_Result.h | 1 + src/Players/Genetic_AI.cpp | 11 ++++------- src/Players/Genetic_AI.h | 5 +++-- src/Players/Move_Decision.cpp | 15 +++++++++++++++ src/Players/Move_Decision.h | 19 +++++++++++++++++++ src/Players/Outside_Communicator.h | 7 ++++--- src/Players/Player.h | 3 ++- src/Players/Random_AI.cpp | 5 +++-- src/Players/Random_AI.h | 3 ++- src/Players/UCI_Mediator.cpp | 13 +++++++------ src/Players/UCI_Mediator.h | 7 ++++--- src/Players/Xboard_Mediator.cpp | 25 ++++++++++++++++++------- src/Players/Xboard_Mediator.h | 7 ++++--- 16 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 src/Players/Move_Decision.cpp create mode 100644 src/Players/Move_Decision.h diff --git a/Genetic_Chess.vcxproj b/Genetic_Chess.vcxproj index 901d1025..7a1acffd 100644 --- a/Genetic_Chess.vcxproj +++ b/Genetic_Chess.vcxproj @@ -269,6 +269,7 @@ + @@ -319,6 +320,7 @@ + diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp index 0d440e4e..5224b264 100644 --- a/src/Game/Game.cpp +++ b/src/Game/Game.cpp @@ -36,8 +36,14 @@ Game_Result play_game(Board board, while( ! result.game_has_ended()) { const auto& player = board.whose_turn() == Piece_Color::WHITE ? white : black; - const auto& move_chosen = player.choose_move(board, game_clock); + const auto decision = player.choose_move(board, game_clock); + if(decision.resigned()) + { + result = Game_Result(opposite(board.whose_turn()), Game_Result_Type::RESIGNATION); + break; + } + const auto& move_chosen = decision.move(); if(Player::thinking_mode() != Thinking_Output_Type::NO_THINKING) { std::cout << player.name() << " chose " << move_chosen.algebraic(board) << '\n'; @@ -103,10 +109,10 @@ void play_game_with_outsider(const Player& player, print_game_record = true; player_color = board.whose_turn(); - const auto& chosen_move = player.choose_move(board, clock); + const auto& decision = player.choose_move(board, clock); clock.punch(board); - game_result = outsider->handle_move(board, chosen_move, game_record); + game_result = outsider->handle_decision(board, decision, game_record); } while( ! game_result.game_has_ended()); outsider->log("Game ended with: " + game_result.ending_reason()); diff --git a/src/Game/Game_Result.cpp b/src/Game/Game_Result.cpp index 0b962b75..c81be4bb 100644 --- a/src/Game/Game_Result.cpp +++ b/src/Game/Game_Result.cpp @@ -64,6 +64,8 @@ std::string Game_Result::ending_reason() const noexcept return "Time forfeiture"; case Game_Result_Type::TIME_EXPIRED_WITH_INSUFFICIENT_MATERIAL: return "Time expired with insufficient material"; + case Game_Result_Type::RESIGNATION: + return color_text(opposite(static_cast(winner()))) + " resigned"; case Game_Result_Type::OTHER: return alternate_reason; default: diff --git a/src/Game/Game_Result.h b/src/Game/Game_Result.h index 97db7aa2..fc7a4b20 100644 --- a/src/Game/Game_Result.h +++ b/src/Game/Game_Result.h @@ -20,6 +20,7 @@ enum class Game_Result_Type INSUFFICIENT_MATERIAL, TIME_FORFEIT, TIME_EXPIRED_WITH_INSUFFICIENT_MATERIAL, + RESIGNATION, OTHER }; diff --git a/src/Players/Genetic_AI.cpp b/src/Players/Genetic_AI.cpp index a4eb0e92..c5e1a74d 100644 --- a/src/Players/Genetic_AI.cpp +++ b/src/Players/Genetic_AI.cpp @@ -13,6 +13,7 @@ using namespace std::chrono_literals; #include "Players/Game_Tree_Node_Result.h" #include "Players/Alpha_Beta_Value.h" #include "Players/Thinking.h" +#include "Players/Move_Decision.h" #include "Game/Board.h" #include "Game/Clock.h" #include "Game/Game_Result.h" @@ -85,13 +86,13 @@ int Genetic_AI::id() const noexcept return genome.id(); } -const Move& Genetic_AI::choose_move(const Board& board, const Clock& clock) const noexcept +Move_Decision Genetic_AI::choose_move(const Board& board, const Clock& clock) const noexcept { reset_search_stats(board); return choose_move_minimax(board, clock); } -const Move& Genetic_AI::choose_move_minimax(const Board& board, const Clock& clock) const noexcept +Move_Decision Genetic_AI::choose_move_minimax(const Board& board, const Clock& clock) const noexcept { auto principal_variation = get_legal_principal_variation(board); const auto progress_of_game = game_progress(board); @@ -111,11 +112,7 @@ const Move& Genetic_AI::choose_move_minimax(const Board& board, const Clock& clo current_variation); report_final_search_stats(result, board); - if(genome.should_resign(commentary, board.whose_turn())) - { - // Do something clever here - } - return *result.variation_line().front(); + return {*result.variation_line().front(), genome.should_resign(commentary, board.whose_turn())}; } std::vector Genetic_AI::get_legal_principal_variation(const Board& board) const noexcept diff --git a/src/Players/Genetic_AI.h b/src/Players/Genetic_AI.h index 5c2ff32d..b0afeca7 100644 --- a/src/Players/Genetic_AI.h +++ b/src/Players/Genetic_AI.h @@ -15,6 +15,7 @@ #include "Players/Game_Tree_Node_Result.h" #include "Players/Alpha_Beta_Value.h" +#include "Players/Move_Decision.h" #include "Utility/Fixed_Capacity_Vector.h" #include "Genes/Genome.h" @@ -72,7 +73,7 @@ class Genetic_AI : public Player //! due to search cutoffs due to alpha-beta pruning. //! \param board The current state of the game. //! \param clock The game clock telling how much time is left in the game. - const Move& choose_move(const Board& board, const Clock& clock) const noexcept override; + Move_Decision choose_move(const Board& board, const Clock& clock) const noexcept override; //! \brief Prints the expected future variation and score for the chosen move. //! @@ -277,7 +278,7 @@ class Genetic_AI : public Player //! update the evaluation speed to a more reasonable starting value. void calibrate_thinking_speed() const noexcept; - const Move& choose_move_minimax(const Board& board, const Clock& clock) const noexcept; + Move_Decision choose_move_minimax(const Board& board, const Clock& clock) const noexcept; std::vector get_legal_principal_variation(const Board& board) const noexcept; }; diff --git a/src/Players/Move_Decision.cpp b/src/Players/Move_Decision.cpp new file mode 100644 index 00000000..7ccfc458 --- /dev/null +++ b/src/Players/Move_Decision.cpp @@ -0,0 +1,15 @@ +#include "Move_Decision.h" + +Move_Decision::Move_Decision(const Move& move, bool resigned) noexcept : chosen_move(move), player_resigned(resigned) +{ +} + +const Move& Move_Decision::move() const noexcept +{ + return chosen_move; +} + +bool Move_Decision::resigned() const noexcept +{ + return player_resigned; +} diff --git a/src/Players/Move_Decision.h b/src/Players/Move_Decision.h new file mode 100644 index 00000000..1e7c060a --- /dev/null +++ b/src/Players/Move_Decision.h @@ -0,0 +1,19 @@ +#ifndef MOVE_DECISION_H +#define MOVE_DECISION_H + +#include "Game/Move.h" + +class Move_Decision +{ + public: + Move_Decision(const Move& move, bool resigned) noexcept; + const Move& move() const noexcept; + bool resigned() const noexcept; + + private: + const Move& chosen_move; + bool player_resigned; +}; + +#endif // !MOVE_DECISION_H + diff --git a/src/Players/Outside_Communicator.h b/src/Players/Outside_Communicator.h index 377eb821..455ec95f 100644 --- a/src/Players/Outside_Communicator.h +++ b/src/Players/Outside_Communicator.h @@ -11,6 +11,7 @@ class Clock; class Board; class Move; +class Move_Decision; class Player; class Proxy_Player; class Game_Result; @@ -55,9 +56,9 @@ class Outside_Communicator //! \param board The Board used for the game. //! \param move The move picked by the local AI. //! \param move_list The list of moves in the game so far. - virtual Game_Result handle_move(Board& board, - const Move& move, - std::vector& move_list) const = 0; + virtual Game_Result handle_decision(Board& board, + const Move_Decision& decision, + std::vector& move_list) const = 0; //! \brief Create a player for the purposes of calling Board::print_game_record() Proxy_Player create_proxy_player() const noexcept; diff --git a/src/Players/Player.h b/src/Players/Player.h index b2c274dc..bc012eb3 100644 --- a/src/Players/Player.h +++ b/src/Players/Player.h @@ -8,6 +8,7 @@ class Board; class Clock; class Move; +class Move_Decision; //! \brief This class represents chess players and encapsulates move-choosing methods. class Player @@ -21,7 +22,7 @@ class Player //! choosing moves here. //! \param board The current board position. The players choose the move from board.legal_moves(). //! \param clock The game clock--allowing the player to decide how much time to spend choosing a move. - virtual const Move& choose_move(const Board& board, const Clock& clock) const noexcept = 0; + virtual Move_Decision choose_move(const Board& board, const Clock& clock) const noexcept = 0; //! \brief Reset player internals (if any) for a new game virtual void reset() const noexcept; diff --git a/src/Players/Random_AI.cpp b/src/Players/Random_AI.cpp index b7f61853..29496204 100644 --- a/src/Players/Random_AI.cpp +++ b/src/Players/Random_AI.cpp @@ -5,13 +5,14 @@ #include "Game/Board.h" #include "Game/Move.h" +#include "Players/Move_Decision.h" #include "Utility/Random.h" class Clock; -const Move& Random_AI::choose_move(const Board& board, const Clock&) const noexcept +Move_Decision Random_AI::choose_move(const Board& board, const Clock&) const noexcept { - return *Random::random_element(board.legal_moves()); + return {*Random::random_element(board.legal_moves()), Random::success_probability(1, 1000)}; } std::string Random_AI::name() const noexcept diff --git a/src/Players/Random_AI.h b/src/Players/Random_AI.h index 34283429..23333e0a 100644 --- a/src/Players/Random_AI.h +++ b/src/Players/Random_AI.h @@ -8,6 +8,7 @@ class Board; class Clock; class Move; +class Move_Decision; //! \brief Plays a game by picking a random legal move during its turn. class Random_AI : public Player @@ -17,7 +18,7 @@ class Random_AI : public Player //! //! \param board The current state of the board. //! \param clock The game clock. - const Move& choose_move(const Board& board, const Clock& clock) const noexcept override; + Move_Decision choose_move(const Board& board, const Clock& clock) const noexcept override; std::string name() const noexcept override; std::string author() const noexcept override; }; diff --git a/src/Players/UCI_Mediator.cpp b/src/Players/UCI_Mediator.cpp index d11d5782..da8be2b9 100644 --- a/src/Players/UCI_Mediator.cpp +++ b/src/Players/UCI_Mediator.cpp @@ -7,6 +7,7 @@ using namespace std::chrono_literals; #include "Players/Player.h" +#include "Players//Move_Decision.h" #include "Game/Board.h" #include "Game/Clock.h" #include "Game/Game_Result.h" @@ -232,13 +233,13 @@ Game_Result UCI_Mediator::setup_turn(Board& board, Clock& clock, std::vector& move_list) const +Game_Result UCI_Mediator::handle_decision(Board& board, + const Move_Decision& decision, + std::vector& move_list) const { - send_command("bestmove " + move.coordinates()); - move_list.push_back(&move); - return board.play_move(move); + send_command("bestmove " + decision.move().coordinates()); + move_list.push_back(&decision.move()); + return board.play_move(decision.move()); } std::string UCI_Mediator::listener(Clock&) diff --git a/src/Players/UCI_Mediator.h b/src/Players/UCI_Mediator.h index 48aef8a1..9f997fff 100644 --- a/src/Players/UCI_Mediator.h +++ b/src/Players/UCI_Mediator.h @@ -11,6 +11,7 @@ class Player; class Board; class Clock; class Move; +class Move_Decision; class Game_Result; //! \brief A class that mediates communication with a GUI via the UCI protocol. @@ -26,9 +27,9 @@ class UCI_Mediator : public Outside_Communicator Clock& clock, std::vector& move_list, const Player& player) override; - Game_Result handle_move(Board& board, - const Move& move, - std::vector& move_list) const override; + Game_Result handle_decision(Board& board, + const Move_Decision& decision, + std::vector& move_list) const override; private: std::string listener(Clock& clock) override; diff --git a/src/Players/Xboard_Mediator.cpp b/src/Players/Xboard_Mediator.cpp index ff088eab..3ce2eec2 100644 --- a/src/Players/Xboard_Mediator.cpp +++ b/src/Players/Xboard_Mediator.cpp @@ -13,6 +13,7 @@ using namespace std::chrono_literals; #include "Game/Move.h" #include "Players/Player.h" +#include "Players/Move_Decision.h" #include "Utility/Exceptions.h" #include "Utility/String.h" @@ -291,23 +292,33 @@ bool Xboard_Mediator::undo_move(std::vector& move_list, const std:: } } -Game_Result Xboard_Mediator::handle_move(Board& board, const Move& move, std::vector& move_list) const +Game_Result Xboard_Mediator::handle_decision(Board& board, const Move_Decision& decision, std::vector& move_list) const { if(in_force_mode) { - log("Ignoring move: " + move.coordinates()); + log("Ignoring move: " + decision.move().coordinates() + (decision.resigned() ? " resigned" : "")); return {}; } else { - send_command("move " + move.coordinates()); - move_list.push_back(&move); - const auto result = board.play_move(move); - if(result.game_has_ended()) + if(decision.resigned()) { + const auto result = Game_Result(opposite(board.whose_turn()), Game_Result_Type::RESIGNATION); report_end_of_game(result); + return result; + } + else + { + const auto& move = decision.move(); + send_command("move " + move.coordinates()); + move_list.push_back(&move); + const auto result = board.play_move(move); + if(result.game_has_ended()) + { + report_end_of_game(result); + } + return result; } - return result; } } diff --git a/src/Players/Xboard_Mediator.h b/src/Players/Xboard_Mediator.h index 46c32525..e6c04c02 100644 --- a/src/Players/Xboard_Mediator.h +++ b/src/Players/Xboard_Mediator.h @@ -10,6 +10,7 @@ class Board; class Clock; class Move; +class Move_Decision; class Player; class Game_Result; @@ -28,9 +29,9 @@ class Xboard_Mediator : public Outside_Communicator Clock& clock, std::vector& move_list, const Player& player) override; - Game_Result handle_move(Board& board, - const Move& move, - std::vector& move_list) const override; + Game_Result handle_decision(Board& board, + const Move_Decision& decision, + std::vector& move_list) const override; private: bool in_force_mode = true; From a9cf866f235d0966b58ddd07b58d4707048c70b3 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Thu, 8 Aug 2024 22:18:17 -0700 Subject: [PATCH 03/12] Change mating prospects of losing Genetic_AI If a player loses during a gene pool game, the parents of the offspring are chosen as follows: - If the losing player lost due to checkmate or time forfeiture, the offspring is a clone of the winner. - If the losing player resigned, the offspring is produced from mating the winner and loser. --- src/Genes/Gene_Pool.cpp | 4 +++- src/Genes/Resignation_Gene.h | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Genes/Gene_Pool.cpp b/src/Genes/Gene_Pool.cpp index a8a9d706..14235dad 100644 --- a/src/Genes/Gene_Pool.cpp +++ b/src/Genes/Gene_Pool.cpp @@ -161,8 +161,10 @@ void gene_pool(const std::string& config_file) const auto mating_winner = (winner == Winner_Color::NONE ? (Random::coin_flip() ? Winner_Color::WHITE : Winner_Color::BLACK) : winner); auto& winning_player = (mating_winner == Winner_Color::WHITE ? white : black); auto& losing_player = (winning_player.id() == white.id() ? black : white); + const auto loser_mates = winner == Winner_Color::NONE || String::contains(result.ending_reason(), "resign"); + const auto& mating_player = (loser_mates ? losing_player : winning_player); - auto offspring = Genetic_AI(white, black); + auto offspring = Genetic_AI(winning_player, mating_player); offspring.mutate(mutation_rate); offspring.print(genome_file_name); losing_player = offspring; diff --git a/src/Genes/Resignation_Gene.h b/src/Genes/Resignation_Gene.h index 57af0221..34b971b7 100644 --- a/src/Genes/Resignation_Gene.h +++ b/src/Genes/Resignation_Gene.h @@ -29,5 +29,4 @@ class Resignation_Gene : public Clonable_Gene double score_board(const Board& board, Piece_Color perspective, size_t depth) const noexcept override; }; - #endif // !RESIGNATION_GENE_h From e9c5e9817eb0c81978d433af0b22803e3fb30663 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Thu, 8 Aug 2024 23:41:55 -0700 Subject: [PATCH 04/12] Add resignations to plots --- analysis/win_lose_draw_plotting.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/analysis/win_lose_draw_plotting.py b/analysis/win_lose_draw_plotting.py index eba0a58d..1460098e 100644 --- a/analysis/win_lose_draw_plotting.py +++ b/analysis/win_lose_draw_plotting.py @@ -16,6 +16,8 @@ class Game_Ending(Enum): MATERIAL_DRAW = auto() STALEMATE = auto() TIME_WITHOUT_MATERIAL = auto() + WHITE_RESIGNATION = auto() + BLACK_RESIGNATION = auto() def extract_game_endings(game_file_name: str): @@ -51,6 +53,10 @@ def extract_game_endings(game_file_name: str): result_type = Game_Ending.STALEMATE elif result_text.lower() == 'time expired with insufficient material': result_type = Game_Ending.TIME_WITHOUT_MATERIAL + elif result_text.lower() == "white resigned": + result_type = Game_Ending.WHITE_RESIGNATION + elif result_text.lower() == "black resigned": + result_type = Game_Ending.BLACK_RESIGNATION else: raise Exception('Unrecognized result type: ' + result_text) except KeyError: @@ -119,6 +125,8 @@ def draw_result_plot(result_data, label): material = result_type == Game_Ending.MATERIAL_DRAW no_legal = result_type == Game_Ending.STALEMATE time_and_material = result_type == Game_Ending.TIME_WITHOUT_MATERIAL + white_resignations = result_type == Game_Ending.WHITE_RESIGNATION + black_resignations = result_type == Game_Ending.BLACK_RESIGNATION number_of_games = len(game_number) outcome_figure, outcome_axes = plt.subplots() @@ -138,6 +146,8 @@ def draw_outcome_plot(outcome, label): draw_outcome_plot(material, "Material draw") draw_outcome_plot(no_legal, "Stalemate") draw_outcome_plot(time_and_material, "Time w/o material") + draw_outcome_plot(white_resignations, "White resigned") + draw_outcome_plot(black_resignations, "Black resigned") outcome_axes.set_xlabel('Games played') outcome_axes.set_ylabel('Percentage') From f9c08e20520e0f3ead921c45a283439e7c521e8a Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Fri, 9 Aug 2024 00:53:00 -0700 Subject: [PATCH 05/12] Plot game lengths where player resigned --- analysis/win_lose_draw_plotting.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/analysis/win_lose_draw_plotting.py b/analysis/win_lose_draw_plotting.py index 1460098e..c10d4069 100644 --- a/analysis/win_lose_draw_plotting.py +++ b/analysis/win_lose_draw_plotting.py @@ -313,3 +313,14 @@ def draw_outcome_plot(outcome, label): timeout_figure.savefig(f'{file_name}_moves_in_game_histogram_timeout.{pic_ext}', **common.picture_file_args) plt.close(timeout_figure) + + resignation_games = (white_resignations | black_resignations) + resignation_counts, resignation_bins = np.histogram(moves_in_game[resignation_games], range(1, max(moves_in_game) + 1)) + resignation_figure, resignation_axes = plt.subplots() + resignation_axes.bar(resignation_bins[0:-1], resignation_counts, width=1, facecolor=bar_color, edgecolor=bar_color, linewidth=bar_line_width) + resignation_axes.set_title("Resignation game lengths") + resignation_axes.set_xlabel("Moves in Game") + resignation_axes.set_ylabel(f"Counts (total = {sum(resignation_games)})") + resignation_axes.set_xlim(0, max_game_length_display) + + resignation_figure.savefig(f"{file_name}_moves_in_game_histogram_resignation.{pic_ext}", **common.picture_file_args) From 90f5ab1d7d5a56d272c1f6afeec3f7d3c8c3f66d Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Fri, 9 Aug 2024 13:22:46 -0700 Subject: [PATCH 06/12] Add log-normal fit to resignation histogram --- analysis/win_lose_draw_plotting.py | 56 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/analysis/win_lose_draw_plotting.py b/analysis/win_lose_draw_plotting.py index c10d4069..3ae13ad2 100644 --- a/analysis/win_lose_draw_plotting.py +++ b/analysis/win_lose_draw_plotting.py @@ -246,36 +246,38 @@ def draw_outcome_plot(outcome, label): plt.close(move_count_histogram_figure) winning_games_lengths = moves_in_game[white_checkmates | black_checkmates] - winning_move_counts, winning_move_bins = np.histogram(winning_games_lengths, range(1, max(moves_in_game) + 1)) + winning_move_counts, winning_move_bins = np.histogram(winning_games_lengths, range(max(moves_in_game) + 1)) checkmate_figure, checkmate_axes = plt.subplots() checkmate_axes.bar(winning_move_bins[0:-1], winning_move_counts, width=1, facecolor=bar_color, edgecolor=bar_color, linewidth=bar_line_width, label='All checkmates') checkmate_axes.set_title('Checkmate game lengths') checkmate_axes.set_xlim(0, max_game_length_display) - # Log-normal fit - bins_fit = winning_move_bins[winning_move_bins > 0] - mean_log = np.mean(np.log(winning_games_lengths)) - std_log = np.std(np.log(winning_games_lengths)) - winning_games_count = len(winning_games_lengths) - fit = winning_games_count*np.exp(-.5*np.power((np.log(bins_fit) - mean_log)/std_log, 2))/(bins_fit*std_log*np.sqrt(2*np.pi)) - checkmate_axes.plot(bins_fit, fit, linewidth=line_width, label='Log-normal fit') - - checkmate_axes.set_xlabel('Moves in Game') - checkmate_axes.set_ylabel(f'Counts (total = {winning_games_count})') - - stats = [f'Mean = {np.mean(winning_games_lengths):.2f}', - f'Median = {np.median(winning_games_lengths):.2f}', - f'Std. Dev. = {np.std(winning_games_lengths):.2f}', - f'Min = {min(winning_games_lengths)}', - f'Max = {max(winning_games_lengths)}', - '', - f'Log-Norm Peak = {np.exp(mean_log - np.power(std_log, 2)):.2f}', - f'Log-Norm Width = {std_log:.2f}'] - - xl = checkmate_axes.get_xlim() - yl = checkmate_axes.get_ylim() - checkmate_axes.text(0.65*xl[1], 0.5*yl[1], '\n'.join(stats), fontsize=stat_text_size) - + def log_normal_fit(game_lengths, game_length_bins, axes): + game_lengths = game_lengths[game_lengths > 1] + bins_fit = game_length_bins[game_length_bins > 0] + mean_log = np.mean(np.log(game_lengths)) + std_log = np.std(np.log(game_lengths)) + games_count = len(game_lengths) + fit = games_count*np.exp(-.5*np.power((np.log(bins_fit) - mean_log)/std_log, 2))/(bins_fit*std_log*np.sqrt(2*np.pi)) + axes.plot(bins_fit, fit, linewidth=line_width, label='Log-normal fit') + + axes.set_xlabel('Moves in Game') + axes.set_ylabel(f'Counts (total = {games_count})') + + stats = [f'Mean = {np.mean(game_lengths):.2f}', + f'Median = {np.median(game_lengths):.2f}', + f'Std. Dev. = {np.std(game_lengths):.2f}', + f'Min = {min(game_lengths)}', + f'Max = {max(game_lengths)}', + '', + f'Log-Norm Peak = {np.exp(mean_log - np.power(std_log, 2)):.2f}', + f'Log-Norm Width = {std_log:.2f}'] + + xl = axes.get_xlim() + yl = axes.get_ylim() + axes.text(0.65*xl[1], 0.5*yl[1], '\n'.join(stats), fontsize=stat_text_size) + + log_normal_fit(winning_games_lengths, winning_move_bins, checkmate_axes) checkmate_axes.legend(fontsize=common.plot_params["legend text size"]) checkmate_figure.savefig(f'{file_name}_moves_in_game_histogram_checkmate.{pic_ext}', **common.picture_file_args) plt.close(checkmate_figure) @@ -315,12 +317,14 @@ def draw_outcome_plot(outcome, label): plt.close(timeout_figure) resignation_games = (white_resignations | black_resignations) - resignation_counts, resignation_bins = np.histogram(moves_in_game[resignation_games], range(1, max(moves_in_game) + 1)) + resignation_game_lengths = moves_in_game[resignation_games] + resignation_counts, resignation_bins = np.histogram(resignation_game_lengths, range(max(moves_in_game) + 1)) resignation_figure, resignation_axes = plt.subplots() resignation_axes.bar(resignation_bins[0:-1], resignation_counts, width=1, facecolor=bar_color, edgecolor=bar_color, linewidth=bar_line_width) resignation_axes.set_title("Resignation game lengths") resignation_axes.set_xlabel("Moves in Game") resignation_axes.set_ylabel(f"Counts (total = {sum(resignation_games)})") resignation_axes.set_xlim(0, max_game_length_display) + log_normal_fit(resignation_game_lengths, resignation_bins, resignation_axes) resignation_figure.savefig(f"{file_name}_moves_in_game_histogram_resignation.{pic_ext}", **common.picture_file_args) From 261e104397b7692f4c4ae5a6935d1066db8a1e03 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Fri, 9 Aug 2024 22:12:46 -0700 Subject: [PATCH 07/12] Scale resignation score after creation/mutation The value of the Board Score Floor is now in units of pawn value. Previously, it was an arbitrary number based on the internal scores assigned to board positions. --- src/Genes/Genome.cpp | 5 +++++ src/Genes/Genome.h | 1 + src/Genes/Resignation_Gene.cpp | 8 +++++++- src/Genes/Resignation_Gene.h | 2 ++ src/Players/Genetic_AI.cpp | 6 ++++++ src/Players/Genetic_AI.h | 2 ++ 6 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Genes/Genome.cpp b/src/Genes/Genome.cpp index f35c0399..bb2eb171 100644 --- a/src/Genes/Genome.cpp +++ b/src/Genes/Genome.cpp @@ -268,6 +268,11 @@ bool Genome::should_resign(const std::vector& commentary, return gene_reference().should_resign(commentary, perspective); } +void Genome::use_centipawn_value(const double centipawn_value) const noexcept +{ + gene_reference().set_centipawn_value(centipawn_value); +} + Clock::seconds Genome::time_to_examine(const Board& board, const Clock& clock) const noexcept { return gene_reference().time_to_examine(board, clock); diff --git a/src/Genes/Genome.h b/src/Genes/Genome.h index 61150a0e..06c5859e 100644 --- a/src/Genes/Genome.h +++ b/src/Genes/Genome.h @@ -122,6 +122,7 @@ class Genome void print(std::ostream& os) const noexcept; bool should_resign(const std::vector& commentary, const Piece_Color perspective) const noexcept; + void use_centipawn_value(double centipawn_value) const noexcept; private: int id_number; diff --git a/src/Genes/Resignation_Gene.cpp b/src/Genes/Resignation_Gene.cpp index 957afa50..ed034966 100644 --- a/src/Genes/Resignation_Gene.cpp +++ b/src/Genes/Resignation_Gene.cpp @@ -14,7 +14,7 @@ bool Resignation_Gene::should_resign(const std::vector& c auto under_floor_streak = 0; for(auto it = commentary.rbegin(); it != commentary.rend(); ++it) { - if(it->corrected_score(perspective) < board_score_floor.value()) + if(it->corrected_score(perspective) < internal_score_floor) { ++under_floor_streak; if(under_floor_streak > max_under_floor_streak.value()) @@ -31,6 +31,12 @@ bool Resignation_Gene::should_resign(const std::vector& c return false; } +void Resignation_Gene::set_centipawn_value(const double input_centipawn_value) const noexcept +{ + const auto pawn_value = input_centipawn_value*100.0; + internal_score_floor = board_score_floor.value()*pawn_value; +} + void Resignation_Gene::gene_specific_mutation() noexcept { if(Random::coin_flip()) diff --git a/src/Genes/Resignation_Gene.h b/src/Genes/Resignation_Gene.h index 34b971b7..2fddc23e 100644 --- a/src/Genes/Resignation_Gene.h +++ b/src/Genes/Resignation_Gene.h @@ -17,10 +17,12 @@ class Resignation_Gene : public Clonable_Gene Resignation_Gene() noexcept; bool should_resign(const std::vector& commentary, Piece_Color perspective) const noexcept; + void set_centipawn_value(double centipawn_value) const noexcept; private: Gene_Value board_score_floor{"Score Floor", 0.0, 1.0}; Gene_Value max_under_floor_streak{"Max Under Floor Streak", 20.0, 1.0}; + mutable double internal_score_floor = board_score_floor.value(); void gene_specific_mutation() noexcept override; void adjust_properties(std::map& properties) const noexcept override; diff --git a/src/Players/Genetic_AI.cpp b/src/Players/Genetic_AI.cpp index c5e1a74d..6d9fb70e 100644 --- a/src/Players/Genetic_AI.cpp +++ b/src/Players/Genetic_AI.cpp @@ -482,6 +482,11 @@ void Genetic_AI::calibrate_thinking_speed() const noexcept reset(); } +void Genetic_AI::send_centipawn_value_to_genome() const noexcept +{ + genome.use_centipawn_value(centipawn_value()); +} + double Genetic_AI::assign_score(const Board& board, const Game_Result& move_result, Piece_Color perspective, size_t depth) const noexcept { if(move_result.game_has_ended()) @@ -647,6 +652,7 @@ void Genetic_AI::recalibrate_self() const noexcept { calibrate_thinking_speed(); calculate_centipawn_value(); + send_centipawn_value_to_genome(); } void Genetic_AI::reset() const noexcept diff --git a/src/Players/Genetic_AI.h b/src/Players/Genetic_AI.h index b0afeca7..49f3c1d2 100644 --- a/src/Players/Genetic_AI.h +++ b/src/Players/Genetic_AI.h @@ -278,6 +278,8 @@ class Genetic_AI : public Player //! update the evaluation speed to a more reasonable starting value. void calibrate_thinking_speed() const noexcept; + void send_centipawn_value_to_genome() const noexcept; + Move_Decision choose_move_minimax(const Board& board, const Clock& clock) const noexcept; std::vector get_legal_principal_variation(const Board& board) const noexcept; From 6784571a614fb5925058455e911a2635148e4cc9 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Fri, 9 Aug 2024 22:13:07 -0700 Subject: [PATCH 08/12] Add tests for Resignation Gene --- src/Testing.cpp | 44 +++++++++++++++++++++++++++++++++++++++++ testing/test_genome.txt | 4 ++++ 2 files changed, 48 insertions(+) diff --git a/src/Testing.cpp b/src/Testing.cpp index 6c0f6013..5e6f0f27 100644 --- a/src/Testing.cpp +++ b/src/Testing.cpp @@ -38,6 +38,7 @@ using namespace std::chrono_literals; #include "Genes/Checkmate_Material_Gene.h" #include "Genes/Pawn_Structure_Gene.h" #include "Genes/Move_Sorting_Gene.h" +#include "Genes/Resignation_Gene.h" #include "Utility/String.h" #include "Utility/Random.h" @@ -232,6 +233,7 @@ namespace void checkmate_material_gene_tests(bool& tests_passed); void sphere_of_influence_gene_tests(bool& tests_passed); void pawn_structure_gene_tests(bool& tests_passed); + void resignation_gene_tests(bool& tests_passed); void game_progress_on_new_board_is_zero(bool& tests_passed); void game_progress_where_one_side_has_only_king_is_one(bool& tests_passed); @@ -1489,6 +1491,48 @@ namespace pawn_structure_gene.test(tests_passed, board4, Piece_Color::BLACK, 0.4/8); } + void resignation_gene_tests(bool& tests_passed) + { + auto resignation_gene = Resignation_Gene(); + resignation_gene.read_from("testing/test_genome.txt"); + std::vector commentary; + commentary.push_back({1.0, Piece_Color::WHITE, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, + "Resigns too early and with too high a score."); + + commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, + "Resigns after too few low-scoring moves."); + + commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == true, + "Does not resigns after sufficient low-scoring moves."); + + resignation_gene.set_centipawn_value(0.1); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, + "Should not resign after rescaling value of pawn."); + + commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, + "Resigned after too few low-scoring moves (after rescaling)."); + + commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == true, + "Did not resign after sufficient low-scoring moves (after rescaling)."); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::BLACK) == false, + "Should not resign due to high scores (from Black's perspective)."); + + commentary.push_back({30.0, Piece_Color::BLACK, {}}); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, + "Should not resign after a recent high score."); + test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::BLACK) == false, + "Should not resign after an insufficiently low score."); + } + void game_progress_on_new_board_is_zero(bool& tests_passed) { auto piece_strength_gene = Piece_Strength_Gene(); diff --git a/testing/test_genome.txt b/testing/test_genome.txt index 3f89da76..7692e2a0 100644 --- a/testing/test_genome.txt +++ b/testing/test_genome.txt @@ -7,6 +7,10 @@ N: 4 R: 8 Q: 16 +Name: Resignation Gene +Board Score Floor: -5.0 +Max Under Floor Streak = 4 + Name: Sphere of Influence Gene Activation Begin: 0.0 Activation End: 1.0 From fe2919f16a704cbd2032c01890d86c532ffa12f1 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Sat, 10 Aug 2024 00:00:15 -0700 Subject: [PATCH 09/12] Make resigning a worse result than a draw Results are as follows: 1. Checkmate or time forfeit: Offspring is clone of winner and replaces loser. 2. Draw: offspring is product of both players and replaces one by coin flip 3. Resign: offspring is clone of winner or product of both players by coin flip, loser is replaced. --- src/Genes/Gene_Pool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Genes/Gene_Pool.cpp b/src/Genes/Gene_Pool.cpp index 14235dad..79a9fc73 100644 --- a/src/Genes/Gene_Pool.cpp +++ b/src/Genes/Gene_Pool.cpp @@ -161,7 +161,7 @@ void gene_pool(const std::string& config_file) const auto mating_winner = (winner == Winner_Color::NONE ? (Random::coin_flip() ? Winner_Color::WHITE : Winner_Color::BLACK) : winner); auto& winning_player = (mating_winner == Winner_Color::WHITE ? white : black); auto& losing_player = (winning_player.id() == white.id() ? black : white); - const auto loser_mates = winner == Winner_Color::NONE || String::contains(result.ending_reason(), "resign"); + const auto loser_mates = winner == Winner_Color::NONE || (String::contains(result.ending_reason(), "resign") && Random::coin_flip()); const auto& mating_player = (loser_mates ? losing_player : winning_player); auto offspring = Genetic_AI(winning_player, mating_player); From 64592317385dfae86901401d4d52def9cf8406bd Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Sat, 10 Aug 2024 00:32:55 -0700 Subject: [PATCH 10/12] Start with less imposing resignation defaults Start with values that won't have half of players resigning on the first move. --- src/Genes/Resignation_Gene.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Genes/Resignation_Gene.h b/src/Genes/Resignation_Gene.h index 2fddc23e..b451c363 100644 --- a/src/Genes/Resignation_Gene.h +++ b/src/Genes/Resignation_Gene.h @@ -20,7 +20,7 @@ class Resignation_Gene : public Clonable_Gene void set_centipawn_value(double centipawn_value) const noexcept; private: - Gene_Value board_score_floor{"Score Floor", 0.0, 1.0}; + Gene_Value board_score_floor{"Score Floor", -10.0, 1.0}; Gene_Value max_under_floor_streak{"Max Under Floor Streak", 20.0, 1.0}; mutable double internal_score_floor = board_score_floor.value(); From 6a39ce41342e552df8b7c96bb61e6cdd8fa22855 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Sat, 10 Aug 2024 00:34:43 -0700 Subject: [PATCH 11/12] Fix resignation tests 1. Call resignation test so it actually runs. 2. Fix test genome syntax. 3. Fix tests so they correspond to Max Under Floor Streak. For point 3, if a player has a max streak of 4, then it will resign on the fifth move that is under the score floor. --- src/Testing.cpp | 21 ++++++++++++--------- testing/test_genome.txt | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Testing.cpp b/src/Testing.cpp index 5e6f0f27..e88d5a6f 100644 --- a/src/Testing.cpp +++ b/src/Testing.cpp @@ -320,6 +320,7 @@ bool run_tests() total_force_gene_tests(tests_passed); checkmate_material_gene_tests(tests_passed); pawn_structure_gene_tests(tests_passed); + resignation_gene_tests(tests_passed); game_progress_on_new_board_is_zero(tests_passed); game_progress_where_one_side_has_only_king_is_one(tests_passed); @@ -1500,27 +1501,29 @@ namespace test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, "Resigns too early and with too high a score."); - commentary.push_back({-6.0, Piece_Color::WHITE, {}}); - commentary.push_back({-6.0, Piece_Color::WHITE, {}}); - commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + commentary.push_back({-11.0, Piece_Color::WHITE, {}}); + commentary.push_back({-11.0, Piece_Color::WHITE, {}}); + commentary.push_back({-11.0, Piece_Color::WHITE, {}}); + commentary.push_back({-11.0, Piece_Color::WHITE, {}}); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, "Resigns after too few low-scoring moves."); - commentary.push_back({-6.0, Piece_Color::WHITE, {}}); + commentary.push_back({-11.0, Piece_Color::WHITE, {}}); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == true, - "Does not resigns after sufficient low-scoring moves."); + "Did not resign after sufficient low-scoring moves."); resignation_gene.set_centipawn_value(0.1); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, "Should not resign after rescaling value of pawn."); - commentary.push_back({-60.0, Piece_Color::WHITE, {}}); - commentary.push_back({-60.0, Piece_Color::WHITE, {}}); - commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + commentary.push_back({-110.0, Piece_Color::WHITE, {}}); + commentary.push_back({-110.0, Piece_Color::WHITE, {}}); + commentary.push_back({-110.0, Piece_Color::WHITE, {}}); + commentary.push_back({-110.0, Piece_Color::WHITE, {}}); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == false, "Resigned after too few low-scoring moves (after rescaling)."); - commentary.push_back({-60.0, Piece_Color::WHITE, {}}); + commentary.push_back({-110.0, Piece_Color::WHITE, {}}); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::WHITE) == true, "Did not resign after sufficient low-scoring moves (after rescaling)."); test_result(tests_passed, resignation_gene.should_resign(commentary, Piece_Color::BLACK) == false, diff --git a/testing/test_genome.txt b/testing/test_genome.txt index 7692e2a0..319bc59b 100644 --- a/testing/test_genome.txt +++ b/testing/test_genome.txt @@ -8,8 +8,8 @@ R: 8 Q: 16 Name: Resignation Gene -Board Score Floor: -5.0 -Max Under Floor Streak = 4 +Score Floor: -5.0 +Max Under Floor Streak: 4 Name: Sphere of Influence Gene Activation Begin: 0.0 From 6edab08d84ad4213500f4e4c8b9c04afa965fe4e Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Sat, 10 Aug 2024 19:38:06 -0700 Subject: [PATCH 12/12] Resigning due to checkmate counts as checkmate In a gene pool, don't allow AIs to not face full consequences of losing to checkmate by resigning just before. --- src/Game/Game.cpp | 4 ++++ src/Game/Game_Result.cpp | 11 +++++++++++ src/Game/Game_Result.h | 4 ++++ src/Genes/Gene_Pool.cpp | 4 +++- src/Players/Genetic_AI.cpp | 2 +- src/Players/Move_Decision.cpp | 8 +++++++- src/Players/Move_Decision.h | 4 +++- src/Players/Random_AI.cpp | 2 +- 8 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp index 5224b264..c47dee63 100644 --- a/src/Game/Game.cpp +++ b/src/Game/Game.cpp @@ -40,6 +40,10 @@ Game_Result play_game(Board board, if(decision.resigned()) { result = Game_Result(opposite(board.whose_turn()), Game_Result_Type::RESIGNATION); + if(decision.resigned_due_to_checkmate()) + { + result.set_resigned_on_checkmate(); + } break; } diff --git a/src/Game/Game_Result.cpp b/src/Game/Game_Result.cpp index c81be4bb..f51ccb0e 100644 --- a/src/Game/Game_Result.cpp +++ b/src/Game/Game_Result.cpp @@ -1,6 +1,7 @@ #include "Game/Game_Result.h" #include +#include #include "Game/Color.h" @@ -86,6 +87,16 @@ std::string Game_Result::game_ending_annotation() const noexcept } } +void Game_Result::set_resigned_on_checkmate() noexcept +{ + resigned_due_to_checkmate = true; +} + +bool Game_Result::resigned_on_checkmate() const noexcept +{ + return resigned_due_to_checkmate; +} + bool Game_Result::game_has_ended_by_rule() const noexcept { return game_has_ended() && cause != Game_Result_Type::OTHER; diff --git a/src/Game/Game_Result.h b/src/Game/Game_Result.h index fc7a4b20..4252ebc2 100644 --- a/src/Game/Game_Result.h +++ b/src/Game/Game_Result.h @@ -66,6 +66,9 @@ class Game_Result //! \brief Returns the part of the PGN move annotation that goes after the # (checkmate) or + (check). std::string game_ending_annotation() const noexcept; + void set_resigned_on_checkmate() noexcept; + bool resigned_on_checkmate() const noexcept; + //! \brief Returns whether or not the program should shutdown after getting this result. bool exit_program() const noexcept; @@ -73,6 +76,7 @@ class Game_Result Winner_Color victor; Game_Result_Type cause; std::string alternate_reason; + bool resigned_due_to_checkmate = false; bool shutdown_program; bool game_has_ended_by_rule() const noexcept; diff --git a/src/Genes/Gene_Pool.cpp b/src/Genes/Gene_Pool.cpp index 79a9fc73..0b30b7cc 100644 --- a/src/Genes/Gene_Pool.cpp +++ b/src/Genes/Gene_Pool.cpp @@ -161,7 +161,9 @@ void gene_pool(const std::string& config_file) const auto mating_winner = (winner == Winner_Color::NONE ? (Random::coin_flip() ? Winner_Color::WHITE : Winner_Color::BLACK) : winner); auto& winning_player = (mating_winner == Winner_Color::WHITE ? white : black); auto& losing_player = (winning_player.id() == white.id() ? black : white); - const auto loser_mates = winner == Winner_Color::NONE || (String::contains(result.ending_reason(), "resign") && Random::coin_flip()); + const auto loser_mates = winner == Winner_Color::NONE || (String::contains(result.ending_reason(), "resign") + && ! result.resigned_on_checkmate() + && Random::coin_flip()); const auto& mating_player = (loser_mates ? losing_player : winning_player); auto offspring = Genetic_AI(winning_player, mating_player); diff --git a/src/Players/Genetic_AI.cpp b/src/Players/Genetic_AI.cpp index 6d9fb70e..efe08a2b 100644 --- a/src/Players/Genetic_AI.cpp +++ b/src/Players/Genetic_AI.cpp @@ -112,7 +112,7 @@ Move_Decision Genetic_AI::choose_move_minimax(const Board& board, const Clock& c current_variation); report_final_search_stats(result, board); - return {*result.variation_line().front(), genome.should_resign(commentary, board.whose_turn())}; + return {*result.variation_line().front(), genome.should_resign(commentary, board.whose_turn()), result.is_losing_for(board.whose_turn())}; } std::vector Genetic_AI::get_legal_principal_variation(const Board& board) const noexcept diff --git a/src/Players/Move_Decision.cpp b/src/Players/Move_Decision.cpp index 7ccfc458..b600a32e 100644 --- a/src/Players/Move_Decision.cpp +++ b/src/Players/Move_Decision.cpp @@ -1,6 +1,7 @@ #include "Move_Decision.h" -Move_Decision::Move_Decision(const Move& move, bool resigned) noexcept : chosen_move(move), player_resigned(resigned) +Move_Decision::Move_Decision(const Move& move, const bool resigned, const bool checkmate) noexcept + : chosen_move(move), player_resigned(resigned), game_ended_in_checkmate(checkmate) { } @@ -13,3 +14,8 @@ bool Move_Decision::resigned() const noexcept { return player_resigned; } + +bool Move_Decision::resigned_due_to_checkmate() const noexcept +{ + return game_ended_in_checkmate && resigned(); +} diff --git a/src/Players/Move_Decision.h b/src/Players/Move_Decision.h index 1e7c060a..f9e24ab4 100644 --- a/src/Players/Move_Decision.h +++ b/src/Players/Move_Decision.h @@ -6,13 +6,15 @@ class Move_Decision { public: - Move_Decision(const Move& move, bool resigned) noexcept; + Move_Decision(const Move& move, bool resigned, bool checkmate) noexcept; const Move& move() const noexcept; bool resigned() const noexcept; + bool resigned_due_to_checkmate() const noexcept; private: const Move& chosen_move; bool player_resigned; + bool game_ended_in_checkmate; }; #endif // !MOVE_DECISION_H diff --git a/src/Players/Random_AI.cpp b/src/Players/Random_AI.cpp index 29496204..57005f52 100644 --- a/src/Players/Random_AI.cpp +++ b/src/Players/Random_AI.cpp @@ -12,7 +12,7 @@ class Clock; Move_Decision Random_AI::choose_move(const Board& board, const Clock&) const noexcept { - return {*Random::random_element(board.legal_moves()), Random::success_probability(1, 1000)}; + return {*Random::random_element(board.legal_moves()), Random::success_probability(1, 1000), false}; } std::string Random_AI::name() const noexcept