diff --git a/Genetic_Chess.vcxproj b/Genetic_Chess.vcxproj index 10b01554..939113e4 100644 --- a/Genetic_Chess.vcxproj +++ b/Genetic_Chess.vcxproj @@ -265,10 +265,12 @@ + + @@ -313,11 +315,13 @@ + + diff --git a/analysis/win_lose_draw_plotting.py b/analysis/win_lose_draw_plotting.py index eba0a58d..3ae13ad2 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') @@ -236,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) @@ -303,3 +315,16 @@ 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_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) diff --git a/src/Game/Game.cpp b/src/Game/Game.cpp index 9b055772..3f01414a 100644 --- a/src/Game/Game.cpp +++ b/src/Game/Game.cpp @@ -107,8 +107,18 @@ 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); + if(decision.resigned_due_to_checkmate()) + { + result.set_resigned_on_checkmate(); + } + 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'; @@ -175,10 +185,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..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" @@ -64,6 +65,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: @@ -84,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 97db7aa2..4252ebc2 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 }; @@ -65,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; @@ -72,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 edf9b18f..3e72ffac 100644 --- a/src/Genes/Gene_Pool.cpp +++ b/src/Genes/Gene_Pool.cpp @@ -166,8 +166,12 @@ 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") + && ! result.resigned_on_checkmate() + && Random::coin_flip()); + 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/Genome.cpp b/src/Genes/Genome.cpp index ac0e604b..f8bc59b5 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()) @@ -257,6 +260,16 @@ 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); +} + +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 78a6dfee..06c5859e 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,12 @@ 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; + void use_centipawn_value(double centipawn_value) 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..ed034966 --- /dev/null +++ b/src/Genes/Resignation_Gene.cpp @@ -0,0 +1,73 @@ +#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) < internal_score_floor) + { + ++under_floor_streak; + if(under_floor_streak > max_under_floor_streak.value()) + { + return true; + } + } + else + { + return false; + } + } + + 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()) + { + 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..b451c363 --- /dev/null +++ b/src/Genes/Resignation_Gene.h @@ -0,0 +1,34 @@ +#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; + void set_centipawn_value(double centipawn_value) const noexcept; + + private: + 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(); + + 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 1965fc86..9e32d944 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" @@ -82,13 +83,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); @@ -108,8 +109,7 @@ const Move& Genetic_AI::choose_move_minimax(const Board& board, const Clock& clo current_variation); report_final_search_stats(result, board); - - return *result.variation_line().front(); + 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 @@ -477,6 +477,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()) @@ -642,6 +647,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 6f895373..6070bbee 100644 --- a/src/Players/Genetic_AI.h +++ b/src/Players/Genetic_AI.h @@ -16,6 +16,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" @@ -73,7 +74,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. //! @@ -281,7 +282,9 @@ 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; + 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; }; diff --git a/src/Players/Move_Decision.cpp b/src/Players/Move_Decision.cpp new file mode 100644 index 00000000..b600a32e --- /dev/null +++ b/src/Players/Move_Decision.cpp @@ -0,0 +1,21 @@ +#include "Move_Decision.h" + +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) +{ +} + +const Move& Move_Decision::move() const noexcept +{ + return chosen_move; +} + +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 new file mode 100644 index 00000000..f9e24ab4 --- /dev/null +++ b/src/Players/Move_Decision.h @@ -0,0 +1,21 @@ +#ifndef MOVE_DECISION_H +#define MOVE_DECISION_H + +#include "Game/Move.h" + +class Move_Decision +{ + public: + 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/Outside_Communicator.h b/src/Players/Outside_Communicator.h index 7614ad0a..ab7c5742 100644 --- a/src/Players/Outside_Communicator.h +++ b/src/Players/Outside_Communicator.h @@ -17,6 +17,7 @@ class Clock; class Board; class Move; +class Move_Decision; class Player; class Proxy_Player; class Game_Result; @@ -63,9 +64,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 38422d16..91f8eb69 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..57005f52 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), false}; } 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 93fde024..b0e0d908 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 7e99caa2..249034a9 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 a4a4926f..cb0a9016 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 d467a6fb..f881812b 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; diff --git a/src/Testing.cpp b/src/Testing.cpp index 5e831e69..f1f3846c 100644 --- a/src/Testing.cpp +++ b/src/Testing.cpp @@ -39,6 +39,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" @@ -233,6 +234,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); @@ -319,6 +321,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); @@ -1495,6 +1498,50 @@ 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({-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({-11.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."); + + 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({-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({-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, + "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..319bc59b 100644 --- a/testing/test_genome.txt +++ b/testing/test_genome.txt @@ -7,6 +7,10 @@ N: 4 R: 8 Q: 16 +Name: Resignation Gene +Score Floor: -5.0 +Max Under Floor Streak: 4 + Name: Sphere of Influence Gene Activation Begin: 0.0 Activation End: 1.0