<commit_before>#include "Breeding/Gene_Pool.h"

#include <vector>
#include <iostream>
#include <map>
#include <iomanip>
#include <csignal>
#include <fstream>
#include <cmath>
#include <algorithm>
#include <future>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
#include <array>
#include <cstdio>
#include <filesystem>

#ifndef _WIN32
#include <mutex>
#endif // _WIN32

#include "Players/Genetic_AI.h"
#include "Game/Game.h"
#include "Game/Board.h"
#include "Game/Clock.h"
#include "Game/Game_Result.h"

#include "Utility/String.h"
#include "Utility/Configuration_File.h"
#include "Utility/Random.h"

#include "Exceptions/Bad_Still_Alive_Line.h"

namespace
{
    const std::string stop_key = "Ctrl-c";

    #ifdef _WIN32
    const auto PAUSE_SIGNAL = SIGINT;
    auto quit_gene_pool = false;
    #else
    const auto PAUSE_SIGNAL = SIGTSTP;
    const std::string pause_key = "Ctrl-z";
    std::mutex pause_mutex;
    #endif

    using Gene_Pool_Set = std::vector<std::vector<Genetic_AI>>;

    Gene_Pool_Set load_gene_pool_file(const std::string& load_file);

    void pause_gene_pool(int);

    void write_generation(const Gene_Pool_Set& pools, size_t pool_index, const std::string& genome_file_name);

    template<typename Stat_Map>
    void purge_dead_from_map(const Gene_Pool_Set& pools, Stat_Map& stats);
}

void gene_pool(const std::string& config_file)
{
    // Signal to pause gene pool
    signal(PAUSE_SIGNAL, pause_gene_pool);

    auto config = Configuration_File(config_file);

    // Environment variables
    const auto maximum_simultaneous_games = size_t(config.as_number("maximum simultaneous games"));
    const auto gene_pool_population = size_t(config.as_number("gene pool population"));
    const auto gene_pool_count = size_t(config.as_number("gene pool count"));
    const auto pool_swap_interval = size_t(config.as_number("pool swap interval"));
    const auto sexual_reproduction = config.as_boolean("reproduction type", "sexual", "cloning");
    const auto genome_file_name = config.as_text("gene pool file");

    const int scramble_mutations = 10; // initial number of mutations when creating a new Genetic AI
    size_t rounds_since_last_swap = 0; // Count of complete gene pool rounds where all pools have played a set of games

    // Oscillating game time
    const double minimum_game_time = config.as_number("minimum game time"); // seconds
    const double maximum_game_time = config.as_number("maximum game time"); // seconds
    if(maximum_game_time < minimum_game_time)
    {
        std::cerr << "Minimum game time = " << minimum_game_time << "\n";
        std::cerr << "Maximum game time = " << maximum_game_time << "\n";
        throw std::invalid_argument("Maximum game time must be greater than the minimum game time.");
    }
    double game_time_increment = config.as_number("game time increment"); // seconds
    const bool oscillating_time = config.as_boolean("oscillating time", "yes", "no");
    double game_time = minimum_game_time;

    if(config.any_unused_parameters())
    {
        std::cout << "There were unused parameters in the file: " << config_file << std::endl;
        config.print_unused_parameters();
        std::cout << "\nPress enter to continue or " << stop_key << " to quit ..." << std::endl;
        std::cin.get();
    }

    std::array<size_t, 2> color_wins{}; // indexed with [Color]
    size_t draw_count = 0;

    std::map<size_t, int> most_wins;
    std::map<size_t, Genetic_AI> most_wins_player;

    std::map<size_t, int> most_games_survived;
    std::map<size_t, Genetic_AI> most_games_survived_player;

    // Individual Genetic AI stats
    std::map<Genetic_AI, int> wins;
    std::map<Genetic_AI, int> draws;
    std::map<Genetic_AI, int> games_since_last_win;
    std::map<Genetic_AI, int> consecutive_wins;
    std::map<Genetic_AI, size_t> original_pool;

    std::cout << "Loading gene pool file: " << genome_file_name << " ..." << std::endl;
    auto pools = load_gene_pool_file(genome_file_name);
    while(pools.size() < gene_pool_count)
    {
        pools.push_back({});
    }

    while(pools.size() > gene_pool_count)
    {
        pools.pop_back();
    }

    for(size_t i = 0; i < pools.size(); ++i)
    {
        while(pools[i].size() < gene_pool_population)
        {
            pools[i].push_back(Genetic_AI(scramble_mutations));
        }

        while(pools[i].size() > gene_pool_population)
        {
            pools[i].pop_back();
        }

        for(const auto& ai : pools[i])
        {
            original_pool[ai] = i;
        }
    }

    for(size_t i = 0; i < pools.size(); ++i)
    {
        write_generation(pools, i, genome_file_name);
    }

    size_t starting_pool = 0;
    if(auto genome_file = std::ifstream(genome_file_name))
    {
        std::string line;
        size_t line_number = 0;
        auto previous_line_was_still_alive = false;
        while(std::getline(genome_file, line))
        {
            line = String::trim_outer_whitespace(line);
            ++line_number;
            if(line.empty())
            {
                continue;
            }

            if(String::starts_with(line, "Still Alive"))
            {
                if(previous_line_was_still_alive)
                {
                    continue; // Still alive line was written after loading a previous session
                }

                try
                {
                    auto alive_split = String::split(line, ":");
                    auto pool_number = String::string_to_size_t(alive_split.at(1));
                    starting_pool = (pool_number + 1) % gene_pool_count;
                    if(starting_pool == 0)
                    {
                        rounds_since_last_swap = (rounds_since_last_swap + 1)%pool_swap_interval;
                    }
                }
                catch(const std::exception&)
                {
                    throw Bad_Still_Alive_Line(line_number, line);
                }

                previous_line_was_still_alive = true;
            }
            else
            {
                previous_line_was_still_alive = false;
            }
        }
    }

    std::string game_record_file = genome_file_name +  "_games.pgn";
    if(auto ifs = std::ifstream(game_record_file))
    {
        // Use game time from last run of this gene pool
        std::cout << "Searching " << game_record_file << " for last game time and stats ..." << std::endl;
        std::string line;
        std::string time_line;
        std::string previous_time_line;
        while(std::getline(ifs, line))
        {
            if(String::starts_with(line, "[TimeControl"))
            {
                if(line != time_line)
                {
                    previous_time_line = time_line;
                    time_line = line;
                }
            }
            else if(String::starts_with(line, "[Result"))
            {
                auto result = String::split(line, "\"").at(1);
                if(result == "1-0")
                {
                    color_wins[WHITE]++;
                }
                else if(result == "0-1")
                {
                    color_wins[BLACK]++;
                }
                else if(result == "1/2-1/2")
                {
                    draw_count++;
                }
                else
                {
                    throw std::invalid_argument("Bad PGN Result line: " + line);
                }
            }
        }

        if( ! time_line.empty())
        {
            game_time = std::stod(String::split(time_line, "\"").at(1));
            game_time = std::clamp(game_time, minimum_game_time, maximum_game_time);
        }

        double previous_game_time = game_time;
        if( ! previous_time_line.empty())
        {
            previous_game_time = std::stod(String::split(previous_time_line, "\"").at(1));
        }

        if(previous_game_time > game_time)
        {
            game_time_increment = -std::abs(game_time_increment);
        }

        game_time = std::clamp(game_time + game_time_increment, minimum_game_time, maximum_game_time);

        std::cout << "Done." << std::endl;
    }

    for(size_t pool_index = starting_pool; true; pool_index = (pool_index + 1) % pools.size()) // run forever
    {
        auto& pool = pools[pool_index];

        // Write overall stats
        std::cout << "\n=======================\n\n"
                  << "Gene pool ID: " << pool_index
                  << "  Gene pool size: " << pool.size()
                  << "  Rounds since pool swaps: " << rounds_since_last_swap << "/" << pool_swap_interval
                  << "\nGames: " << color_wins[WHITE] + color_wins[BLACK] + draw_count
                  << "  White wins: " << color_wins[WHITE]
                  << "  Black wins: " << color_wins[BLACK]
                  << "  Draws: " << draw_count
                  << "\nTime: " << game_time << " sec"
                  << "   Gene pool file name: " << genome_file_name << "\n\n";

        #ifdef _WIN32
        std::cout << "Quit after this round: " << stop_key << "    Abort: " << stop_key << " " << stop_key << "\n" << std::endl;
        #else
        std::cout << "Pause: " << pause_key << "    Abort: " << stop_key << "\n" << std::endl;
        #endif // _WIN32

        // The shuffled pool list determines the match-ups. After shuffling the list,
        // adjacent AIs are matched as opponents.
        Random::shuffle(pool);

        std::vector<std::future<Game_Result>> results; // map from matchups to winners (half the size of pool)
        auto clocks = std::vector<Clock>(gene_pool_population/2, Clock(game_time));
        for(size_t index = 0; index < gene_pool_population; index += 2)
        {
            // Limit the number of simultaneous games by waiting for earlier games to finish
            // before starting a new one.
            while(true)
            {
                auto in_progress_games = size_t(std::count_if(results.begin(),
                                                              results.end(),
                                                              [](const auto& r)
                                                              { return r.wait_for(0s) != std::future_status::ready; }));

                if(gene_pool_population > 2*maximum_simultaneous_games &&
                   in_progress_games >= maximum_simultaneous_games)
                {
                    std::this_thread::sleep_for(100ms);
                }
                else
                {
                    break;
                }
            }

            const auto& white = pool[index];
            const auto& black = pool[index + 1];
            results.emplace_back(std::async(std::launch::async,
                                            play_game, Board{}, std::ref(clocks[index/2]), white, black, false, game_record_file));
        }

        // Get results as they come in
        for(size_t index = 0; index < gene_pool_population; index += 2)
        {
            auto& white = pool[index];
            auto& black = pool[index + 1];

            std::cout << white.id() << " vs "
                      << black.id() << ": " << std::flush;

            auto result = results[index/2].get();
            auto winner = result.winner();
            std::cout << color_text(winner) << " (" << result.ending_reason() << ")";

            const auto& winning_player = (winner == WHITE ? white : black);
            if(winner != NONE)
            {
                color_wins[winner]++;
                wins[winning_player]++;
                games_since_last_win[winning_player] = 0;
                consecutive_wins[winning_player]++;
                if(wins[winning_player] >= most_wins[pool_index])
                {
                    most_wins[pool_index] = wins[winning_player];
                    most_wins_player.insert_or_assign(pool_index, winning_player);
                }
            }
            else
            {
                draws[white]++;
                draws[black]++;
                games_since_last_win[white]++;
                games_since_last_win[black]++;
                consecutive_wins[white] = 0;
                consecutive_wins[black] = 0;
                ++draw_count;

                // Draw results in random player being replaced
                const auto& clock = clocks[index/2];
                winner = (clock.time_left(WHITE) < clock.time_left(BLACK) ? WHITE : BLACK);
                std::cout << " --> " << (winner == WHITE ? white : black).id() << " lives";
            }

            auto offspring = sexual_reproduction ? Genetic_AI(white, black) : Genetic_AI(winning_player, winning_player);
            offspring.mutate();
            original_pool[offspring] = pool_index;

            auto& losing_player  = (winner == WHITE ? black : white);
            losing_player = offspring; // offspring replaces loser
            std::cout << std::endl;
        }

        std::sort(pool.begin(), pool.end());
        write_generation(pools, pool_index, genome_file_name);

        purge_dead_from_map(pools, wins);
        purge_dead_from_map(pools, draws);
        purge_dead_from_map(pools, games_since_last_win);
        purge_dead_from_map(pools, consecutive_wins);
        purge_dead_from_map(pools, original_pool);

        for(const auto& ai : pool)
        {
            auto games_survived = wins[ai] + draws[ai];
            if(games_survived >= most_games_survived[pool_index])
            {
                most_games_survived[pool_index] = games_survived;
                most_games_survived_player.insert_or_assign(pool_index, ai);
            }
        }

        // widths of columns for stats printout
        auto id_digits = int(std::floor(std::log10(pool.back().id()) + 1));

        // Write stat headers
        std::cout << '\n' << std::setw(id_digits + 1)  << "ID"
                  << std::setw(7)  << "Wins"
                  << std::setw(8) << "Streak"
                  << std::setw(7)  << "Draws"
                  << std::setw(9)  << "Streak\n";

        // Write stats for each specimen
        for(const auto& ai : pool)
        {
            std::cout << std::setw(id_digits + 1) << ai.id();
            std::cout << std::setw(7)    << wins[ai]
                      << std::setw(8)   << consecutive_wins[ai]
                      << std::setw(7)    << draws[ai]
                      << std::setw(8)   << games_since_last_win[ai]
                      << (original_pool[ai] != pool_index ? " T" : "") << "\n";
        }
        std::cout << std::endl;

        if(most_wins.count(pool_index) > 0)
        {
            std::cout << "Most wins:     " << most_wins.at(pool_index)
                      << " by ID " << most_wins_player.at(pool_index).id() << std::endl;
        }

        if(most_games_survived.count(pool_index) > 0)
        {
            std::cout << "Longest lived: " << most_games_survived.at(pool_index)
                      << " by ID " << most_games_survived_player.at(pool_index).id() << std::endl;
        }

        // Record best AI from all pools.
        auto best_ai = pools.front().front();
        for(const auto& search_pool : pools)
        {
            // Taking the oldest AI (smallest ID) as the best
            best_ai = std::min(best_ai, *std::min_element(search_pool.begin(), search_pool.end()));
        }
        auto best_file_name = genome_file_name + "_best_genome.txt";
        auto temp_best_file_name = best_file_name + ".tmp";
        best_ai.print(temp_best_file_name);
        std::filesystem::rename(temp_best_file_name, best_file_name);

        // Update game time
        game_time += game_time_increment;
        if(game_time > maximum_game_time || game_time < minimum_game_time)
        {
            if(oscillating_time)
            {
                game_time_increment *= -1;
            }
            else
            {
                game_time_increment = 0;
            }
        }
        game_time = std::clamp(game_time, minimum_game_time, maximum_game_time);

        // Mix up the populations of all the gene pools
        if(pools.size() > 1 && pool_index == pools.size() - 1 && ++rounds_since_last_swap >= pool_swap_interval)
        {
            rounds_since_last_swap = 0;

            std::cout << "\n=======================\n\n";
            std::vector<Genetic_AI> all_players;
            for(const auto& gene_pool : pools)
            {
                all_players.insert(all_players.end(), gene_pool.begin(), gene_pool.end());
            }
            size_t max_id = std::max_element(all_players.begin(), all_players.end())->id();

            // Make sure quality has not dropped

            // Pick a random AI from the past (probability proportional to ID)
            auto id_sum = (max_id*(max_id + 1))/2;
            auto random_sum = Random::random_integer(size_t{0}, id_sum);
            auto threshhold_id = int(std::ceil((std::sqrt(0.25 + 2.0*random_sum) - 0.5)));
            std::cout << "Threshold AI ID = " << threshhold_id << std::endl;
            auto threshold_ai = Genetic_AI(genome_file_name, threshhold_id);

            // Replace AIs that lose to the threshold AI with new offsrping
            // with the threshold AI as a parent.
            for(auto& ai : all_players)
            {
                std::cout << "Threshold game: " << threshold_ai.id() << " vs " << ai.id() << " ... " << std::flush;
                auto clock = Clock(game_time);
                auto result = play_game(Board{}, clock, threshold_ai, ai, false, game_record_file + "_threshold.pgn");
                if(result.winner() == WHITE)
                {
                    ai = Genetic_AI(threshold_ai, ai);
                    original_pool[ai] = gene_pool_count; // from nowhere
                    std::cout << "Replaced" << std::endl;
                }
                else
                {
                    std::cout << "Passed" << std::endl;
                }
            }

            // Distribute players randomly among pools
            std::cout << "Shuffling pools ... ";<commit_msg>AI must beat threshold to pass<commit_after>#include "Breeding/Gene_Pool.h"

#include <vector>
#include <iostream>
#include <map>
#include <iomanip>
#include <csignal>
#include <fstream>
#include <cmath>
#include <algorithm>
#include <future>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
#include <array>
#include <cstdio>
#include <filesystem>

#ifndef _WIN32
#include <mutex>
#endif // _WIN32

#include "Players/Genetic_AI.h"
#include "Game/Game.h"
#include "Game/Board.h"
#include "Game/Clock.h"
#include "Game/Game_Result.h"

#include "Utility/String.h"
#include "Utility/Configuration_File.h"
#include "Utility/Random.h"

#include "Exceptions/Bad_Still_Alive_Line.h"

namespace
{
    const std::string stop_key = "Ctrl-c";

    #ifdef _WIN32
    const auto PAUSE_SIGNAL = SIGINT;
    auto quit_gene_pool = false;
    #else
    const auto PAUSE_SIGNAL = SIGTSTP;
    const std::string pause_key = "Ctrl-z";
    std::mutex pause_mutex;
    #endif

    using Gene_Pool_Set = std::vector<std::vector<Genetic_AI>>;

    Gene_Pool_Set load_gene_pool_file(const std::string& load_file);

    void pause_gene_pool(int);

    void write_generation(const Gene_Pool_Set& pools, size_t pool_index, const std::string& genome_file_name);

    template<typename Stat_Map>
    void purge_dead_from_map(const Gene_Pool_Set& pools, Stat_Map& stats);
}

void gene_pool(const std::string& config_file)
{
    // Signal to pause gene pool
    signal(PAUSE_SIGNAL, pause_gene_pool);

    auto config = Configuration_File(config_file);

    // Environment variables
    const auto maximum_simultaneous_games = size_t(config.as_number("maximum simultaneous games"));
    const auto gene_pool_population = size_t(config.as_number("gene pool population"));
    const auto gene_pool_count = size_t(config.as_number("gene pool count"));
    const auto pool_swap_interval = size_t(config.as_number("pool swap interval"));
    const auto sexual_reproduction = config.as_boolean("reproduction type", "sexual", "cloning");
    const auto genome_file_name = config.as_text("gene pool file");

    const int scramble_mutations = 10; // initial number of mutations when creating a new Genetic AI
    size_t rounds_since_last_swap = 0; // Count of complete gene pool rounds where all pools have played a set of games

    // Oscillating game time
    const double minimum_game_time = config.as_number("minimum game time"); // seconds
    const double maximum_game_time = config.as_number("maximum game time"); // seconds
    if(maximum_game_time < minimum_game_time)
    {
        std::cerr << "Minimum game time = " << minimum_game_time << "\n";
        std::cerr << "Maximum game time = " << maximum_game_time << "\n";
        throw std::invalid_argument("Maximum game time must be greater than the minimum game time.");
    }
    double game_time_increment = config.as_number("game time increment"); // seconds
    const bool oscillating_time = config.as_boolean("oscillating time", "yes", "no");
    double game_time = minimum_game_time;

    if(config.any_unused_parameters())
    {
        std::cout << "There were unused parameters in the file: " << config_file << std::endl;
        config.print_unused_parameters();
        std::cout << "\nPress enter to continue or " << stop_key << " to quit ..." << std::endl;
        std::cin.get();
    }

    std::array<size_t, 2> color_wins{}; // indexed with [Color]
    size_t draw_count = 0;

    std::map<size_t, int> most_wins;
    std::map<size_t, Genetic_AI> most_wins_player;

    std::map<size_t, int> most_games_survived;
    std::map<size_t, Genetic_AI> most_games_survived_player;

    // Individual Genetic AI stats
    std::map<Genetic_AI, int> wins;
    std::map<Genetic_AI, int> draws;
    std::map<Genetic_AI, int> games_since_last_win;
    std::map<Genetic_AI, int> consecutive_wins;
    std::map<Genetic_AI, size_t> original_pool;

    std::cout << "Loading gene pool file: " << genome_file_name << " ..." << std::endl;
    auto pools = load_gene_pool_file(genome_file_name);
    while(pools.size() < gene_pool_count)
    {
        pools.push_back({});
    }

    while(pools.size() > gene_pool_count)
    {
        pools.pop_back();
    }

    for(size_t i = 0; i < pools.size(); ++i)
    {
        while(pools[i].size() < gene_pool_population)
        {
            pools[i].push_back(Genetic_AI(scramble_mutations));
        }

        while(pools[i].size() > gene_pool_population)
        {
            pools[i].pop_back();
        }

        for(const auto& ai : pools[i])
        {
            original_pool[ai] = i;
        }
    }

    for(size_t i = 0; i < pools.size(); ++i)
    {
        write_generation(pools, i, genome_file_name);
    }

    size_t starting_pool = 0;
    if(auto genome_file = std::ifstream(genome_file_name))
    {
        std::string line;
        size_t line_number = 0;
        auto previous_line_was_still_alive = false;
        while(std::getline(genome_file, line))
        {
            line = String::trim_outer_whitespace(line);
            ++line_number;
            if(line.empty())
            {
                continue;
            }

            if(String::starts_with(line, "Still Alive"))
            {
                if(previous_line_was_still_alive)
                {
                    continue; // Still alive line was written after loading a previous session
                }

                try
                {
                    auto alive_split = String::split(line, ":");
                    auto pool_number = String::string_to_size_t(alive_split.at(1));
                    starting_pool = (pool_number + 1) % gene_pool_count;
                    if(starting_pool == 0)
                    {
                        rounds_since_last_swap = (rounds_since_last_swap + 1)%pool_swap_interval;
                    }
                }
                catch(const std::exception&)
                {
                    throw Bad_Still_Alive_Line(line_number, line);
                }

                previous_line_was_still_alive = true;
            }
            else
            {
                previous_line_was_still_alive = false;
            }
        }
    }

    std::string game_record_file = genome_file_name +  "_games.pgn";
    if(auto ifs = std::ifstream(game_record_file))
    {
        // Use game time from last run of this gene pool
        std::cout << "Searching " << game_record_file << " for last game time and stats ..." << std::endl;
        std::string line;
        std::string time_line;
        std::string previous_time_line;
        while(std::getline(ifs, line))
        {
            if(String::starts_with(line, "[TimeControl"))
            {
                if(line != time_line)
                {
                    previous_time_line = time_line;
                    time_line = line;
                }
            }
            else if(String::starts_with(line, "[Result"))
            {
                auto result = String::split(line, "\"").at(1);
                if(result == "1-0")
                {
                    color_wins[WHITE]++;
                }
                else if(result == "0-1")
                {
                    color_wins[BLACK]++;
                }
                else if(result == "1/2-1/2")
                {
                    draw_count++;
                }
                else
                {
                    throw std::invalid_argument("Bad PGN Result line: " + line);
                }
            }
        }

        if( ! time_line.empty())
        {
            game_time = std::stod(String::split(time_line, "\"").at(1));
            game_time = std::clamp(game_time, minimum_game_time, maximum_game_time);
        }

        double previous_game_time = game_time;
        if( ! previous_time_line.empty())
        {
            previous_game_time = std::stod(String::split(previous_time_line, "\"").at(1));
        }

        if(previous_game_time > game_time)
        {
            game_time_increment = -std::abs(game_time_increment);
        }

        game_time = std::clamp(game_time + game_time_increment, minimum_game_time, maximum_game_time);

        std::cout << "Done." << std::endl;
    }

    for(size_t pool_index = starting_pool; true; pool_index = (pool_index + 1) % pools.size()) // run forever
    {
        auto& pool = pools[pool_index];

        // Write overall stats
        std::cout << "\n=======================\n\n"
                  << "Gene pool ID: " << pool_index
                  << "  Gene pool size: " << pool.size()
                  << "  Rounds since pool swaps: " << rounds_since_last_swap << "/" << pool_swap_interval
                  << "\nGames: " << color_wins[WHITE] + color_wins[BLACK] + draw_count
                  << "  White wins: " << color_wins[WHITE]
                  << "  Black wins: " << color_wins[BLACK]
                  << "  Draws: " << draw_count
                  << "\nTime: " << game_time << " sec"
                  << "   Gene pool file name: " << genome_file_name << "\n\n";

        #ifdef _WIN32
        std::cout << "Quit after this round: " << stop_key << "    Abort: " << stop_key << " " << stop_key << "\n" << std::endl;
        #else
        std::cout << "Pause: " << pause_key << "    Abort: " << stop_key << "\n" << std::endl;
        #endif // _WIN32

        // The shuffled pool list determines the match-ups. After shuffling the list,
        // adjacent AIs are matched as opponents.
        Random::shuffle(pool);

        std::vector<std::future<Game_Result>> results; // map from matchups to winners (half the size of pool)
        auto clocks = std::vector<Clock>(gene_pool_population/2, Clock(game_time));
        for(size_t index = 0; index < gene_pool_population; index += 2)
        {
            // Limit the number of simultaneous games by waiting for earlier games to finish
            // before starting a new one.
            while(true)
            {
                auto in_progress_games = size_t(std::count_if(results.begin(),
                                                              results.end(),
                                                              [](const auto& r)
                                                              { return r.wait_for(0s) != std::future_status::ready; }));

                if(gene_pool_population > 2*maximum_simultaneous_games &&
                   in_progress_games >= maximum_simultaneous_games)
                {
                    std::this_thread::sleep_for(100ms);
                }
                else
                {
                    break;
                }
            }

            const auto& white = pool[index];
            const auto& black = pool[index + 1];
            results.emplace_back(std::async(std::launch::async,
                                            play_game, Board{}, std::ref(clocks[index/2]), white, black, false, game_record_file));
        }

        // Get results as they come in
        for(size_t index = 0; index < gene_pool_population; index += 2)
        {
            auto& white = pool[index];
            auto& black = pool[index + 1];

            std::cout << white.id() << " vs "
                      << black.id() << ": " << std::flush;

            auto result = results[index/2].get();
            auto winner = result.winner();
            std::cout << color_text(winner) << " (" << result.ending_reason() << ")";

            const auto& winning_player = (winner == WHITE ? white : black);
            if(winner != NONE)
            {
                color_wins[winner]++;
                wins[winning_player]++;
                games_since_last_win[winning_player] = 0;
                consecutive_wins[winning_player]++;
                if(wins[winning_player] >= most_wins[pool_index])
                {
                    most_wins[pool_index] = wins[winning_player];
                    most_wins_player.insert_or_assign(pool_index, winning_player);
                }
            }
            else
            {
                draws[white]++;
                draws[black]++;
                games_since_last_win[white]++;
                games_since_last_win[black]++;
                consecutive_wins[white] = 0;
                consecutive_wins[black] = 0;
                ++draw_count;

                // Draw results in random player being replaced
                const auto& clock = clocks[index/2];
                winner = (clock.time_left(WHITE) < clock.time_left(BLACK) ? WHITE : BLACK);
                std::cout << " --> " << (winner == WHITE ? white : black).id() << " lives";
            }

            auto offspring = sexual_reproduction ? Genetic_AI(white, black) : Genetic_AI(winning_player, winning_player);
            offspring.mutate();
            original_pool[offspring] = pool_index;

            auto& losing_player  = (winner == WHITE ? black : white);
            losing_player = offspring; // offspring replaces loser
            std::cout << std::endl;
        }

        std::sort(pool.begin(), pool.end());
        write_generation(pools, pool_index, genome_file_name);

        purge_dead_from_map(pools, wins);
        purge_dead_from_map(pools, draws);
        purge_dead_from_map(pools, games_since_last_win);
        purge_dead_from_map(pools, consecutive_wins);
        purge_dead_from_map(pools, original_pool);

        for(const auto& ai : pool)
        {
            auto games_survived = wins[ai] + draws[ai];
            if(games_survived >= most_games_survived[pool_index])
            {
                most_games_survived[pool_index] = games_survived;
                most_games_survived_player.insert_or_assign(pool_index, ai);
            }
        }

        // widths of columns for stats printout
        auto id_digits = int(std::floor(std::log10(pool.back().id()) + 1));

        // Write stat headers
        std::cout << '\n' << std::setw(id_digits + 1)  << "ID"
                  << std::setw(7)  << "Wins"
                  << std::setw(8) << "Streak"
                  << std::setw(7)  << "Draws"
                  << std::setw(9)  << "Streak\n";

        // Write stats for each specimen
        for(const auto& ai : pool)
        {
            std::cout << std::setw(id_digits + 1) << ai.id();
            std::cout << std::setw(7)    << wins[ai]
                      << std::setw(8)   << consecutive_wins[ai]
                      << std::setw(7)    << draws[ai]
                      << std::setw(8)   << games_since_last_win[ai]
                      << (original_pool[ai] != pool_index ? " T" : "") << "\n";
        }
        std::cout << std::endl;

        if(most_wins.count(pool_index) > 0)
        {
            std::cout << "Most wins:     " << most_wins.at(pool_index)
                      << " by ID " << most_wins_player.at(pool_index).id() << std::endl;
        }

        if(most_games_survived.count(pool_index) > 0)
        {
            std::cout << "Longest lived: " << most_games_survived.at(pool_index)
                      << " by ID " << most_games_survived_player.at(pool_index).id() << std::endl;
        }

        // Record best AI from all pools.
        auto best_ai = pools.front().front();
        for(const auto& search_pool : pools)
        {
            // Taking the oldest AI (smallest ID) as the best
            best_ai = std::min(best_ai, *std::min_element(search_pool.begin(), search_pool.end()));
        }
        auto best_file_name = genome_file_name + "_best_genome.txt";
        auto temp_best_file_name = best_file_name + ".tmp";
        best_ai.print(temp_best_file_name);
        std::filesystem::rename(temp_best_file_name, best_file_name);

        // Update game time
        game_time += game_time_increment;
        if(game_time > maximum_game_time || game_time < minimum_game_time)
        {
            if(oscillating_time)
            {
                game_time_increment *= -1;
            }
            else
            {
                game_time_increment = 0;
            }
        }
        game_time = std::clamp(game_time, minimum_game_time, maximum_game_time);

        // Mix up the populations of all the gene pools
        if(pools.size() > 1 && pool_index == pools.size() - 1 && ++rounds_since_last_swap >= pool_swap_interval)
        {
            rounds_since_last_swap = 0;

            std::cout << "\n=======================\n\n";
            std::vector<Genetic_AI> all_players;
            for(const auto& gene_pool : pools)
            {
                all_players.insert(all_players.end(), gene_pool.begin(), gene_pool.end());
            }
            size_t max_id = std::max_element(all_players.begin(), all_players.end())->id();

            // Make sure quality has not dropped

            // Pick a random AI from the past (probability proportional to ID)
            auto id_sum = (max_id*(max_id + 1))/2;
            auto random_sum = Random::random_integer(size_t{0}, id_sum);
            auto threshhold_id = int(std::ceil((std::sqrt(0.25 + 2.0*random_sum) - 0.5)));
            std::cout << "Threshold AI ID = " << threshhold_id << std::endl;
            auto threshold_ai = Genetic_AI(genome_file_name, threshhold_id);

            // Replace AIs that lose to the threshold AI with new offsrping
            // with the threshold AI as a parent.
            for(auto& ai : all_players)
            {
                std::cout << "Threshold game: " << threshold_ai.id() << " vs " << ai.id() << " ... " << std::flush;
                auto clock = Clock(game_time);
                auto result = play_game(Board{}, clock, threshold_ai, ai, false, game_record_file + "_threshold.pgn");
                if(result.winner() != BLACK)
                {
                    auto ancestor_original_pool = original_pool[ai];
                    ai = Genetic_AI(threshold_ai, ai);
                    original_pool[ai] = ancestor_original_pool;
                    std::cout << "Replaced" << std::endl;
                }
                else
                {
                    std::cout << "Passed" << std::endl;
                }
            }

            // Distribute players randomly among pools
            std::cout << "Shuffling pools ... ";
{'fraction_non_alphanumeric': 0.12494851022930112, 'fraction_numerical': 0.0045585610325415355, 'mean_word_length': 1.8662731208185754, 'pattern_counts': {'":': 4, '<': 573, '<?xml version=': 0, '>': 95, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2821049', 'n_tokens_mistral': 11287, 'n_tokens_neox': 10621, 'n_words': 2192}