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